Skip to content

Commit f80f8a9

Browse files
NQNStudiosCelticMinstrel
authored andcommitted
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
1 parent f066290 commit f80f8a9

File tree

11 files changed

+135
-1
lines changed

11 files changed

+135
-1
lines changed

rsrc/schemas/scenario.xsd

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
<xs:element name="email" type="xs:string"/>
4141
</xs:sequence>
4242
</xs:complexType>
43+
<xs:complexType name="feature-flags">
44+
<xs:sequence>
45+
<xs:any minOccurs="0" maxOccurs="unbounded"/>
46+
</xs:sequence>
47+
</xs:complexType>
4348
<xs:complexType name="scenText">
4449
<xs:sequence>
4550
<xs:element name="teaser" minOccurs="0" maxOccurs="2" type="xs:string"/>
@@ -353,6 +358,7 @@
353358
<xs:element name="version" type="xs:string"/>
354359
<xs:element name="language" type="xs:language"/>
355360
<xs:element name="author" type="author"/>
361+
<xs:element name="feature-flags" type="feature-flags"/>
356362
<xs:element name="text" type="scenText"/>
357363
<xs:element name="ratings" type="ratings"/>
358364
<xs:element name="flags" type="scenFlags"/>

src/fileio/fileio_scen.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "mathutil.hpp"
2424
#include "gzstream.h"
2525
#include "tarball.hpp"
26+
#include "replay.hpp"
2627

2728
#include "porting.hpp"
2829
#include "fileio/resmgr/res_image.hpp"
@@ -769,6 +770,9 @@ void readScenarioFromXml(ticpp::Document&& data, cScenario& scenario) {
769770
} else if(type == "author") {
770771
elem->FirstChildElement("name")->GetText(&scenario.contact_info[0], false);
771772
elem->FirstChildElement("email")->GetText(&scenario.contact_info[1], false);
773+
} else if(type == "feature-flags") {
774+
// If the function fits, use it
775+
scenario.feature_flags = info_from_action(*elem);
772776
} else if(type == "text") {
773777
Iterator<Element> info;
774778
int found_teasers = 0, found_intro = 0;

src/game/boe.global.hpp

+25
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <sstream>
99
#include <map>
1010
#include "boe.consts.hpp"
11+
#include "universe/universe.hpp"
1112

1213
#define ASB add_string_to_buf
1314
#define PSD univ.party.stuff_done
@@ -31,4 +32,28 @@ extern std::map<std::string, int> startup_button_indices;
3132
extern std::map<int, std::string> startup_button_names;
3233
extern std::map<int, std::string> startup_button_names_v1;
3334

35+
extern cUniverse univ;
36+
extern std::map<std::string, std::vector<std::string>> feature_flags;
37+
38+
inline bool has_feature_flag(std::string flag, std::string version) {
39+
auto iter = feature_flags.find(flag);
40+
if(iter == feature_flags.end()) return false;
41+
std::vector<std::string> versions = iter->second;
42+
return std::find(versions.begin(), versions.end(), version) != versions.end();
43+
}
44+
45+
// Return the version of a feature that SHOULD BE USED in the currently running game.
46+
inline std::string get_feature_version(std::string flag) {
47+
// If a scenario is loaded and specifies the flag, use that version.
48+
if(!univ.party.scen_name.empty()){
49+
std::string scenario_flag = univ.scenario.get_feature_flag(flag);
50+
if(!scenario_flag.empty()) return scenario_flag;
51+
}
52+
// Otherwise, use the most recent version of the feature supported by this build,
53+
// or by the build that recorded the current replay.
54+
auto iter = feature_flags.find(flag);
55+
if(iter == feature_flags.end()) return "";
56+
return iter->second.back();
57+
}
58+
3459
#endif

src/game/boe.main.cpp

+59
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ boost::optional<location> scen_arg_out_sec, scen_arg_loc;
8989
extern std::string last_load_file;
9090
std::string help_text_rsrc = "help";
9191

92+
/*
93+
// Example feature flags:
94+
{
95+
// A build which supports both V2 and V3 of the updated graphics sheet:
96+
{"graphics-sheet", {"V2", "V3"}}
97+
}
98+
*/
99+
std::map<std::string,std::vector<std::string>> feature_flags = {};
100+
92101
struct cParseEntrance {
93102
boost::optional<short>& opt;
94103
cParseEntrance(boost::optional<short>& opt) : opt(opt) {}
@@ -925,6 +934,50 @@ static void replay_next_action() {
925934
replay_action(pop_next_action());
926935
}
927936

937+
static void record_feature_flags() {
938+
Element next_action("feature_flags");
939+
for(auto& p : feature_flags){
940+
Element next_flag(p.first);
941+
std::vector<std::string> supported_versions = p.second;
942+
for(std::string version : supported_versions){
943+
Element next_version("version");
944+
Text version_text(version);
945+
next_version.InsertEndChild(version_text);
946+
next_flag.InsertEndChild(next_version);
947+
}
948+
next_action.InsertEndChild(next_flag);
949+
}
950+
record_action(next_action);
951+
}
952+
953+
static void replay_feature_flags() {
954+
std::map<std::string,std::vector<std::string>> recorded_flags = {};
955+
if(has_next_action("feature_flags")){
956+
Element action = pop_next_action();
957+
Element* next_flag = action.FirstChildElement(false);
958+
while(next_flag){
959+
std::string flag = next_flag->Value();
960+
std::vector<std::string> supported_versions;
961+
Element* next_version = next_flag->FirstChildElement(false);
962+
while(next_version){
963+
std::string version = next_version->GetText();
964+
// The game build needs to support the feature version that the replay had
965+
if(!has_feature_flag(flag, version)){
966+
std::string error = "This replay requires a feature that is not supported in your version of Blades of Exile: " + flag + " should support '" + version + "'";
967+
throw error;
968+
}
969+
supported_versions.push_back(version);
970+
next_version = next_version->NextSiblingElement(false);
971+
}
972+
recorded_flags[flag] = supported_versions;
973+
next_flag = next_flag->NextSiblingElement(false);
974+
}
975+
}
976+
977+
feature_flags = recorded_flags;
978+
}
979+
980+
928981
void init_boe(int argc, char* argv[]) {
929982
set_up_apple_events();
930983
init_directories(argv[0]);
@@ -947,6 +1000,12 @@ void init_boe(int argc, char* argv[]) {
9471000
set_cursor(watch_curs);
9481001
init_buf();
9491002

1003+
if(recording){
1004+
record_feature_flags();
1005+
}else if(replaying){
1006+
replay_feature_flags();
1007+
}
1008+
9501009
// Seed the RNG
9511010
if(replaying) {
9521011
Element& srand_element = pop_next_action("srand");

src/game/boe.party.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ void put_party_in_scen(std::string scen_name, bool force) {
171171
show_get_items("Choose stored items to keep:", saved_item_refs, pc, true);
172172
}
173173

174+
// Make sure the game build supports all the scenario's features
175+
for(auto pair : univ.scenario.feature_flags){
176+
if(!has_feature_flag(pair.first, pair.second)){
177+
showError("This scenario requires a feature that is not supported in your version of Blades of Exile: " + pair.first + " should support '" + pair.second + "'");
178+
return;
179+
}
180+
}
181+
174182
univ.enter_scenario(scen_name);
175183

176184
// if at this point, startup must be over, so make this call to make sure we're ready,

src/scenario/scenario.cpp

+10
Original file line numberDiff line numberDiff line change
@@ -590,4 +590,14 @@ std::vector<town_entrance_t> cScenario::find_town_entrances(int town_num) {
590590
}
591591
}
592592
return matching_entrances;
593+
}
594+
595+
bool cScenario::has_feature_flag(std::string flag) {
596+
return this->feature_flags.find(flag) != this->feature_flags.end();
597+
}
598+
599+
std::string cScenario::get_feature_flag(std::string flag) {
600+
std::map<std::string, std::string>::const_iterator iter = this->feature_flags.find(flag);
601+
if(iter == this->feature_flags.end()) return "";
602+
return iter->second;
593603
}

src/scenario/scenario.hpp

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ class cScenario {
6565
location where_start,out_sec_start,out_start;
6666
size_t which_town_start;
6767
spec_num_t init_spec;
68+
std::map<std::string,std::string> feature_flags;
69+
bool has_feature_flag(std::string flag);
70+
std::string get_feature_flag(std::string flag);
71+
6872
std::array<spec_loc_t,10> town_mods;
6973
std::array<rectangle,3> store_item_rects;
7074
std::array<short,3> store_item_towns;

src/scenedit/scen.core.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -2960,6 +2960,8 @@ bool build_scenario() {
29602960
scenario.contact_info[0] = author;
29612961
scenario.default_ground = grass ? 2 : 0;
29622962

2963+
scenario.feature_flags = {};
2964+
29632965
fs::path basePath = progDir/"Blades of Exile Base"/"bladbase.boes";
29642966
if(!fs::exists(basePath)) {
29652967
basePath = basePath.parent_path()/"bladbase.exs";

src/scenedit/scen.fileio.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ void writeScenarioToXml(ticpp::Printer&& data, cScenario& scenario) {
133133
data.PushElement("name", scenario.contact_info[0]);
134134
data.PushElement("email", scenario.contact_info[1]);
135135
data.CloseElement("author");
136+
data.OpenElement("feature-flags");
137+
for(auto& p : scenario.feature_flags){
138+
data.PushElement(p.first, p.second);
139+
}
140+
data.CloseElement("feature-flags");
136141
data.OpenElement("text");
137142
data.PushElement("teaser", scenario.who_wrote[0]);
138143
data.PushElement("teaser", scenario.who_wrote[1]);

src/tools/replay.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ void record_action(std::string action_type, std::map<std::string,std::string> in
141141
log_document.SaveFile(log_file);
142142
}
143143

144+
void record_action(Element& action) {
145+
Element* root = log_document.FirstChildElement();
146+
root->InsertEndChild(action);
147+
log_document.SaveFile(log_file);
148+
}
149+
144150
void record_field_input(cKey key) {
145151
std::map<std::string,std::string> info;
146152
info["spec"] = bool_to_str(key.spec);

src/tools/replay.hpp

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
#include "game/boe.newgraph.hpp"
1111

1212
// Input recording system
13-
namespace ticpp { class Element; }
13+
namespace ticpp {
14+
class Element;
15+
class Text;
16+
}
1417
using ticpp::Element;
18+
using ticpp::Text;
1519

1620
struct word_rect_t;
1721

@@ -30,6 +34,7 @@ extern std::string last_action_type;
3034
extern bool init_action_log(std::string command, std::string file);
3135
extern void record_action(std::string action_type, std::string inner_text, bool cdata = false);
3236
extern void record_action(std::string action_type, std::map<std::string,std::string> info);
37+
extern void record_action(Element& action);
3338
extern void record_field_input(cKey key);
3439
extern bool has_next_action(std::string type = "");
3540
extern std::string next_action_type();

0 commit comments

Comments
 (0)