Skip to content

Commit

Permalink
feat(mechanics): Customizable text substitutions (#6356)
Browse files Browse the repository at this point in the history
  • Loading branch information
Amazinite authored Nov 24, 2021
1 parent 368127d commit 30cf6d8
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 1 deletion.
6 changes: 6 additions & 0 deletions EndlessSky.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
DFAAE2AA1FD4A27B0072C0A8 /* ImageSet.cpp in Sources */ = {isa = PBXBuildFile; fileRef = DFAAE2A81FD4A27B0072C0A8 /* ImageSet.cpp */; };
ED5E4F2ABA89E9DE00603E69 /* TestContext.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A3134EC4B1CCA5546A10C3EF /* TestContext.cpp */; };
F55745BDBC50E15DCEB2ED5B /* layout.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 9BCF4321AF819E944EC02FB9 /* layout.hpp */; settings = {ATTRIBUTES = (Project, ); }; };
F78A44BAA8252F6D53B24B69 /* TextReplacements.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A4E42FEB3B265A91D5BD2FC /* TextReplacements.cpp */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -222,10 +223,12 @@
62C311181CE172D000409D91 /* Flotsam.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Flotsam.cpp; path = source/Flotsam.cpp; sourceTree = "<group>"; };
62C311191CE172D000409D91 /* Flotsam.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Flotsam.h; path = source/Flotsam.h; sourceTree = "<group>"; };
6833448DAEB9861D28445DD5 /* GameAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GameAction.h; path = source/GameAction.h; sourceTree = "<group>"; };
6A4E42FEB3B265A91D5BD2FC /* TextReplacements.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = TextReplacements.cpp; path = source/TextReplacements.cpp; sourceTree = "<group>"; };
6A5716311E25BE6F00585EB2 /* CollisionSet.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CollisionSet.cpp; path = source/CollisionSet.cpp; sourceTree = "<group>"; };
6A5716321E25BE6F00585EB2 /* CollisionSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CollisionSet.h; path = source/CollisionSet.h; sourceTree = "<group>"; };
6DCF4CF2972F569E6DBB8578 /* CategoryTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CategoryTypes.h; path = source/CategoryTypes.h; sourceTree = "<group>"; };
86AB4B6E9C4C0490AE7F029B /* EsUuid.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = EsUuid.h; path = source/EsUuid.h; sourceTree = "<group>"; };
86D9414E818561143BD298BC /* TextReplacements.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TextReplacements.h; path = source/TextReplacements.h; sourceTree = "<group>"; };
8E8A4C648B242742B22A34FA /* Weather.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Weather.cpp; path = source/Weather.cpp; sourceTree = "<group>"; };
950742538F8CECF5D4168FBC /* EsUuid.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = EsUuid.cpp; path = source/EsUuid.cpp; sourceTree = "<group>"; };
98104FFDA18E40F4A712A8BE /* CoreStartData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CoreStartData.h; path = source/CoreStartData.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -809,6 +812,8 @@
6833448DAEB9861D28445DD5 /* GameAction.h */,
A2A948EE929342C8F84C65D1 /* TestContext.h */,
A3134EC4B1CCA5546A10C3EF /* TestContext.cpp */,
6A4E42FEB3B265A91D5BD2FC /* TextReplacements.cpp */,
86D9414E818561143BD298BC /* TextReplacements.h */,
);
name = source;
sourceTree = "<group>";
Expand Down Expand Up @@ -1157,6 +1162,7 @@
03DC4253AA8390FE0A8FB4EA /* MaskManager.cpp in Sources */,
87D6407E8B579EB502BFBCE5 /* GameAction.cpp in Sources */,
ED5E4F2ABA89E9DE00603E69 /* TestContext.cpp in Sources */,
F78A44BAA8252F6D53B24B69 /* TextReplacements.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
2 changes: 2 additions & 0 deletions EndlessSkyLib.cbp
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@
<Unit filename="source/TestContext.h" />
<Unit filename="source/TestData.cpp" />
<Unit filename="source/TestData.h" />
<Unit filename="source/TextReplacements.cpp" />
<Unit filename="source/TextReplacements.h" />
<Unit filename="source/Trade.cpp" />
<Unit filename="source/Trade.h" />
<Unit filename="source/TradingPanel.cpp" />
Expand Down
16 changes: 16 additions & 0 deletions source/GameData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ namespace {
Set<Galaxy> defaultGalaxies;
Set<Sale<Ship>> defaultShipSales;
Set<Sale<Outfit>> defaultOutfitSales;
TextReplacements defaultSubstitutions;

Politics politics;
vector<StartConditions> startConditions;
Expand Down Expand Up @@ -132,6 +133,8 @@ namespace {

MaskManager maskManager;

TextReplacements substitutions;

const Government *playerGovernment = nullptr;

// TODO (C++14): make these 3 methods generic lambdas visible only to the CheckReferences method.
Expand Down Expand Up @@ -251,6 +254,7 @@ bool GameData::BeginLoad(const char * const *argv)
defaultGalaxies = galaxies;
defaultShipSales = shipSales;
defaultOutfitSales = outfitSales;
defaultSubstitutions = substitutions;
playerGovernment = governments.Get("Escort");

politics.Reset();
Expand Down Expand Up @@ -486,6 +490,7 @@ void GameData::Revert()
galaxies.Revert(defaultGalaxies);
shipSales.Revert(defaultShipSales);
outfitSales.Revert(defaultOutfitSales);
substitutions.Revert(defaultSubstitutions);
for(auto &it : persons)
it.second.Restore();

Expand Down Expand Up @@ -652,6 +657,8 @@ void GameData::Change(const DataNode &node)
systems.Get(node.Token(1))->Link(systems.Get(node.Token(2)));
else if(node.Token(0) == "unlink" && node.Size() >= 3)
systems.Get(node.Token(1))->Unlink(systems.Get(node.Token(2)));
else if(node.Token(0) == "substitutions" && node.HasChildren())
substitutions.Load(node);
else
node.PrintTrace("Invalid \"event\" data:");
}
Expand Down Expand Up @@ -1006,6 +1013,13 @@ MaskManager &GameData::GetMaskManager()



const TextReplacements &GameData::GetTextReplacements()
{
return substitutions;
}



void GameData::LoadSources()
{
sources.clear();
Expand Down Expand Up @@ -1202,6 +1216,8 @@ void GameData::LoadFile(const string &path, bool debugMode)
text += child.Token(0);
}
}
else if(key == "substitutions" && node.HasChildren())
substitutions.Load(node);
else
node.PrintTrace("Skipping unrecognized root object:");
}
Expand Down
3 changes: 3 additions & 0 deletions source/GameData.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class StartConditions;
class System;
class Test;
class TestData;
class TextReplacements;



Expand Down Expand Up @@ -153,6 +154,8 @@ class GameData {

static MaskManager &GetMaskManager();

static const TextReplacements &GetTextReplacements();


private:
static void LoadSources();
Expand Down
1 change: 1 addition & 0 deletions source/GameEvent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace {
"planet",
"shipyard",
"system",
"substitutions",
};
}

Expand Down
6 changes: 6 additions & 0 deletions source/Mission.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,8 @@ void Mission::Load(const DataNode &node)
}
else if(child.Token(0) == "stopover" && child.HasChildren())
stopoverFilters.emplace_back(child);
else if(child.Token(0) == "substitutions" && child.HasChildren())
substitutions.Load(child);
else if(child.Token(0) == "npc")
npcs.emplace_back(child);
else if(child.Token(0) == "on" && child.Size() >= 2 && child.Token(1) == "enter")
Expand Down Expand Up @@ -835,6 +837,8 @@ string Mission::BlockedMessage(const PlayerInfo &player)
return "";

map<string, string> subs;
GameData::GetTextReplacements().Substitutions(subs, player.Conditions());
substitutions.Substitutions(subs, player.Conditions());
subs["<first>"] = player.FirstName();
subs["<last>"] = player.LastName();
if(flagship)
Expand Down Expand Up @@ -1248,6 +1252,8 @@ Mission Mission::Instantiate(const PlayerInfo &player, const shared_ptr<Ship> &b

// Generate the substitutions map.
map<string, string> subs;
GameData::GetTextReplacements().Substitutions(subs, player.Conditions());
substitutions.Substitutions(subs, player.Conditions());
subs["<commodity>"] = result.cargo;
subs["<tons>"] = to_string(result.cargoSize) + (result.cargoSize == 1 ? " ton" : " tons");
subs["<cargo>"] = subs["<tons>"] + " of " + subs["<commodity>"];
Expand Down
4 changes: 4 additions & 0 deletions source/Mission.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
#include "LocationFilter.h"
#include "MissionAction.h"
#include "NPC.h"
#include "TextReplacements.h"

#include <list>
#include <map>
Expand Down Expand Up @@ -225,6 +226,9 @@ class Mission {
std::set<const Planet *> visitedStopovers;
std::set<const System *> visitedWaypoints;

// User-defined text replacements unique to this mission:
TextReplacements substitutions;

// NPCs:
std::list<NPC> npcs;

Expand Down
2 changes: 2 additions & 0 deletions source/MissionAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
#include "Outfit.h"
#include "PlayerInfo.h"
#include "Ship.h"
#include "TextReplacements.h"
#include "UI.h"

using namespace std;
Expand Down Expand Up @@ -288,6 +289,7 @@ void MissionAction::Do(PlayerInfo &player, UI *ui, const System *destination, co
else if(!dialogText.empty() && ui)
{
map<string, string> subs;
GameData::GetTextReplacements().Substitutions(subs, player.Conditions());
subs["<first>"] = player.FirstName();
subs["<last>"] = player.LastName();
if(player.Flagship())
Expand Down
8 changes: 7 additions & 1 deletion source/Ship.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
#include "Sprite.h"
#include "StellarObject.h"
#include "System.h"
#include "TextReplacements.h"
#include "Visual.h"

#include <algorithm>
Expand Down Expand Up @@ -1318,7 +1319,13 @@ void Ship::SetHail(const Phrase &phrase)

string Ship::GetHail(const PlayerInfo &player) const
{
string hailStr = hail ? hail->Get() : government ? government->GetHail(isDisabled) : "";

if(hailStr.empty())
return hailStr;

map<string, string> subs;
GameData::GetTextReplacements().Substitutions(subs, player.Conditions());

subs["<first>"] = player.FirstName();
subs["<last>"] = player.LastName();
Expand All @@ -1330,7 +1337,6 @@ string Ship::GetHail(const PlayerInfo &player) const
subs["<date>"] = player.GetDate().ToString();
subs["<day>"] = player.GetDate().LongString();

string hailStr = hail ? hail->Get() : government ? government->GetHail(isDisabled) : "";
return Format::Replace(hailStr, subs);
}

Expand Down
92 changes: 92 additions & 0 deletions source/TextReplacements.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* TextReplacements.cpp
Copyright (c) 2021 by Amazinite
Endless Sky is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
*/

#include "TextReplacements.h"

#include "ConditionSet.h"
#include "DataNode.h"
#include "PlayerInfo.h"

#include <set>

using namespace std;



// Load a substitutions node.
void TextReplacements::Load(const DataNode &node)
{
// Check for reserved keys. Only some hardcoded replacement keys are
// reserved, as these ones are done on the fly after all other replacements
// have been done.
const set<string> reserved = {"<first>", "<last>", "<ship>"};

for(const DataNode &child : node)
{
if(child.Size() < 2)
{
child.PrintTrace("Skipping improper substitution syntax:");
continue;
}

string key = child.Token(0);
if(key.empty())
{
child.PrintTrace("Cannot replace an empty string:");
continue;
}
if(key.front() != '<')
{
key = "<" + key;
child.PrintTrace("Warning: text replacements must be prefixed by \"<\":");
}
if(key.back() != '>')
{
key += ">";
child.PrintTrace("Warning: text replacements must be suffixed by \">\":");
}
if(reserved.count(key))
{
child.PrintTrace("Skipping reserved substitution key \"" + key + "\":");
continue;
}

ConditionSet toSubstitute(child);
substitutions.emplace_back(key, make_pair(std::move(toSubstitute), child.Token(1)));
}
}



// Clear this TextReplacement's substitutions and insert the substitutions of other.
void TextReplacements::Revert(TextReplacements &other)
{
substitutions.clear();
substitutions.insert(substitutions.begin(), other.substitutions.begin(), other.substitutions.end());
}



// Add new text replacements to the given map after evaltuating all possible replacements.
// This text replacement will overwrite the value of any existing keys in the given map
// if the map and this TextReplacements share a key.
void TextReplacements::Substitutions(map<string, string> &subs, const map<string, int64_t> &conditions) const
{
for(const auto &sub : substitutions)
{
const string &key = sub.first;
const ConditionSet &toSub = sub.second.first;
const string &replacement = sub.second.second;
if(toSub.Test(conditions))
subs[key] = replacement;
}
}
52 changes: 52 additions & 0 deletions source/TextReplacements.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* TextReplacements.h
Copyright (c) 2021 by Amazinite
Endless Sky is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.
Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
*/

#ifndef TEXT_REPLACEMENTS_H_
#define TEXT_REPLACEMENTS_H_

#include <map>
#include <string>
#include <vector>

class ConditionSet;
class DataNode;
class PlayerInfo;



// A class containing a list of text replacements. Text replacements consist of a
// key to search for and the text to replace it with. One key can have multiple potential
// replacement texts, with the specific text chosen being defined by whichever replacement
// is the last valid replacement for that key, with replacement validity being defined
// by a ConditionSet.
class TextReplacements {
public:
// Load a substitutions node.
void Load(const DataNode &node);

// Clear this TextReplacement's substitutions and insert the substitutions of other.
void Revert(TextReplacements &other);

// Add new text replacements to the given map after evaltuating all possible replacements.
// This TextReplacements will overwrite the value of any existing keys in the given map
// if the map and this TextReplacements share a key.
void Substitutions(std::map<std::string, std::string> &subs, const std::map<std::string, int64_t> &conditions) const;


private:
// Vector with "string to be replaced", "condition when to replace", and "replacement text".
std::vector<std::pair<std::string, std::pair<ConditionSet, std::string>>> substitutions;
};



#endif

0 comments on commit 30cf6d8

Please sign in to comment.