File: VarText.cpp

package info (click to toggle)
freeorion 0.5.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 194,940 kB
  • sloc: cpp: 186,508; python: 40,969; ansic: 1,164; xml: 719; makefile: 32; sh: 7
file content (431 lines) | stat: -rw-r--r-- 19,059 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#include "VarText.h"

#include "../universe/NamedValueRefManager.h"
#include "../universe/ValueRefs.h"
#include "../universe/Universe.h"
#include "../universe/ShipDesign.h"
#include "../universe/System.h"
#include "../universe/Planet.h"
#include "../universe/Species.h"
#include "../Empire/Empire.h"
#include "i18n.h"
#include "Logger.h"
#include "AppInterface.h"

#include <boost/xpressive/xpressive.hpp>

#if __has_include(<charconv>)
#include <charconv>
#endif
#include <functional>
#include <map>

namespace xpr = boost::xpressive;

class Tech;
class Policy;
class BuildingType;
class Special;
class Species;
class FieldType;
class ShipHull;
class ShipPart;
const Tech*         GetTech(std::string_view name);
const Policy*       GetPolicy(std::string_view name);
const BuildingType* GetBuildingType(std::string_view name);
const Special*      GetSpecial(std::string_view name);
const FieldType*    GetFieldType(std::string_view name);
const ShipHull*     GetShipHull(std::string_view name);
const ShipPart*     GetShipPart(std::string_view name);

namespace {
    constexpr int32_t ToIntCX(std::string_view sv, int default_result = -1) {
        if (sv.empty())
            return default_result;

        bool is_negative = (sv.front() == '-');
        sv = sv.substr(is_negative);
        for (auto c : sv)
            if (c > '9' || c < '0')
                return default_result;

        int64_t retval = 0;
        for (auto c : sv) {
            retval *= 10;
            retval += (c - '0');
        }

        retval *= (is_negative ? -1 : 1);

        constexpr int32_t max_int = std::numeric_limits<int32_t>::max();
        constexpr int32_t min_int = std::numeric_limits<int32_t>::min();

        return (retval > max_int) ? max_int :
            (retval < min_int) ? min_int :
            static_cast<int32_t>(retval);
    }
    static_assert(ToIntCX("2147483647") == 2147483647);
    static_assert(ToIntCX("-104") == -104);
    static_assert(ToIntCX("banana", -10) == -10);
    static_assert(ToIntCX("-banana") == -1);
    static_assert(ToIntCX("-0banana", -20) == -20);
    static_assert(ToIntCX("") == -1);
    static_assert(ToIntCX("0") == 0);
    static_assert(ToIntCX("0000") == 0);
    static_assert(ToIntCX("-0000") == 0);

    // wrapper for converting string to integer
    int ToInt(std::string_view sv, int default_result = -1) {
#if defined(__cpp_lib_to_chars)
        int retval = default_result;
        std::from_chars(sv.data(), sv.data() + sv.size(), retval);
        return retval;
#else
        return ToIntCX(sv, default_result);
#endif
    }

    //! Return @p content surrounded by the given @p tags.
    //!
    //! @param content
    //!     The text that should be wrapped into @p tags.
    //! @param tag
    //!     The tags that should be wrapping the given @p content.  The tag
    //!     shouldn't contain whitespace.
    //! @param data
    //!     Additional data assigned to the @p tag.
    //!
    //! @return
    //!     The tagged content.
    std::string WithTags(std::string_view content, std::string_view tag, std::string_view data) {
        std::string retval;
        retval.reserve(1 + tag.size() + 1 + data.size() + 1 + content.size() + 2 + tag.size() + 2);
        retval.append("<").append(tag).append(" ").append(data).append(">")
              .append(content)
              .append("</").append(tag).append(">");
        return retval;
    }

    //! Get string substitute for a tag that is a universe object
    boost::optional<std::string> UniverseObjectString(
        std::string_view data, std::string_view tag, const ObjectMap& objects)
    {
        const int object_id = ToInt(data, INVALID_OBJECT_ID);
        auto obj = objects.getRaw(object_id);
        if (!obj)
            return boost::none;

        return WithTags(GetVisibleObjectName(*obj), tag, data); // TODO: pass universe instead of relying on client's
    }

    //! Returns substitution string for an in-Universe ship design tag
    boost::optional<std::string> ShipDesignString(std::string_view data,
                                                  const Universe& universe)
    {
        const int design_id = ToInt(data, INVALID_DESIGN_ID);
        if (const auto design = universe.GetShipDesign(design_id))
            return WithTags(design->Name(), VarText::DESIGN_ID_TAG, data);

        return UserString("FW_UNKNOWN_DESIGN_NAME");
    }

    //! Returns substitution string for a predefined ship design tag
    boost::optional<std::string> PredefinedShipDesignString(std::string_view data, const ScriptingContext& context) {
        if (const ShipDesign* design = context.ContextUniverse().GetGenericShipDesign(data))
            return WithTags(design->Name(), VarText::PREDEFINED_DESIGN_TAG, data);
        return boost::none;
    }

    boost::optional<std::string> SpeciesString(std::string_view data, const SpeciesManager& sm) {
        if (!sm.GetSpecies(data))
            return boost::none;
        return WithTags(UserString(data), VarText::SPECIES_TAG, data);
    }

    boost::optional<std::string> MeterTypeString(std::string_view data) {
        MeterType meter_type = MeterTypeFromString(data, MeterType::INVALID_METER_TYPE);

        if (meter_type > MeterType::INVALID_METER_TYPE && meter_type < MeterType::NUM_METER_TYPES) {
            auto mt_string{to_string(meter_type)};
            if (UserStringExists(mt_string))
                return WithTags(UserString(mt_string), VarText::METER_TYPE_TAG, mt_string);
            else
                return std::string{mt_string};
        }

        return boost::none;
    }

    //! Returns substitution string for an empire
    boost::optional<std::string> EmpireString(
        std::string_view data, const EmpireManager::const_container_type& empires)
    {
        const int id = ToInt(data, ALL_EMPIRES);
        auto it = empires.find(id);
        if (it != empires.end())
            return WithTags(it->second->Name(), VarText::EMPIRE_ID_TAG, data);

        return boost::none;
    }

    //! Returns substitution string for tag and data, where \a data is looked up using the
    //! GetByName function and the returned name is wrapped in \a tag to linkifying it.
    //! If GetByName returns an empty optional or null pointer, then an empty optional<string>
    //! is returned
    template <typename T, const T* (*GetByName)(std::string_view)>
    boost::optional<std::string> NameString(std::string_view data, std::string_view tag) {
        if (!GetByName(data))
            return boost::none;
        return WithTags(UserString(data), tag, data);
    }


    //! Functions to evaluate to get substitution of user-readable text for a tag, indexed by tag
    using NoContextTagStringFunc = std::function<boost::optional<std::string> (std::string_view)>;
    const std::array<std::pair<std::string_view, NoContextTagStringFunc>, 13> no_context_substitution_map{{
        {VarText::TEXT_TAG, +[](std::string_view data)
            { return UserString(data); }},
        {VarText::RAW_TEXT_TAG, +[](std::string_view data)
            { return std::string{data}; }},
        {VarText::COMBAT_ID_TAG, [](std::string_view data)
            { return WithTags(UserString("COMBAT"), VarText::COMBAT_ID_TAG, data); }},
        {VarText::TECH_TAG, [](std::string_view data)
            { return NameString<Tech, GetTech>(data, VarText::TECH_TAG); }},
        {VarText::POLICY_TAG, [](std::string_view data)
            { return NameString<Policy, GetPolicy>(data, VarText::POLICY_TAG); }},
        {VarText::BUILDING_TYPE_TAG, [](std::string_view data)
            { return NameString<BuildingType, GetBuildingType>(data, VarText::BUILDING_TYPE_TAG); }},
        {VarText::SHIP_HULL_TAG, [](std::string_view data)
            { return NameString<ShipHull, GetShipHull>(data, VarText::SHIP_HULL_TAG); }},
        {VarText::SHIP_PART_TAG, [](std::string_view data)
            { return NameString<ShipPart, GetShipPart>(data, VarText::SHIP_PART_TAG); }},
        {VarText::SPECIAL_TAG, [](std::string_view data)
            { return NameString<Special, GetSpecial>(data, VarText::SPECIAL_TAG); }},
        {VarText::FIELD_TYPE_TAG, [](std::string_view data)
            { return NameString<FieldType, GetFieldType>(data, VarText::FIELD_TYPE_TAG); }},
        {VarText::METER_TYPE_TAG, MeterTypeString},
        {VarText::FOCS_VALUE_TAG, [](std::string_view data) -> boost::optional<std::string>
            {
                if (const ValueRef::ValueRefBase* vr = GetValueRefBase(data))
                    return WithTags(UserString(data), VarText::FOCS_VALUE_TAG, vr->EvalAsString());
                else
                    return WithTags(data, VarText::FOCS_VALUE_TAG, UserString("UNKNOWN_VALUE_REF_NAME"));
            }},
        {VarText::USER_STRING_TAG, [](std::string_view data)
            { return UserString(data); }},
    }};

    using ContextTagStringFunc = std::function<boost::optional<std::string> (std::string_view, const ScriptingContext&)>;
    const std::array<std::pair<std::string_view, ContextTagStringFunc>, 12> context_substitution_map{{
        {VarText::PLANET_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return UniverseObjectString(data, VarText::PLANET_ID_TAG, context.ContextObjects()); }},
        {VarText::SYSTEM_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return UniverseObjectString(data, VarText::SYSTEM_ID_TAG, context.ContextObjects()); }},
        {VarText::SHIP_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return UniverseObjectString(data, VarText::SHIP_ID_TAG, context.ContextObjects()); }},
        {VarText::FLEET_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return UniverseObjectString(data, VarText::FLEET_ID_TAG, context.ContextObjects()); }},
        {VarText::BUILDING_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return UniverseObjectString(data, VarText::BUILDING_ID_TAG, context.ContextObjects()); }},
        {VarText::FIELD_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return UniverseObjectString(data, VarText::FIELD_ID_TAG, context.ContextObjects()); }},
        {VarText::SPECIES_TAG, [](std::string_view data, const ScriptingContext& context)
            { return SpeciesString(data, context.species); }},
        {VarText::DESIGN_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return ShipDesignString(data, context.ContextUniverse()); }},
        {VarText::PREDEFINED_DESIGN_TAG, PredefinedShipDesignString},
        {VarText::EMPIRE_ID_TAG, [](std::string_view data, const ScriptingContext& context)
            { return EmpireString(data, context.Empires().GetEmpires()); }},
        {VarText::PLANET_TYPE_TAG, [](std::string_view data, const ScriptingContext& context)
            {
                // Assume that we have no userstring which is also a number
                if (UserStringExists(data))
                    return UserString(data);
                const int planet_id = ToInt(data, INVALID_OBJECT_ID);
                if (auto planet = context.ContextObjects().getRaw<Planet>(planet_id))
                    return UserString(to_string(planet->Type()));
                return UserString("UNKNOWN_PLANET");
            }},
        {VarText::ENVIRONMENT_TAG, [](std::string_view data, const ScriptingContext& context)
            {
                // Assume that we have no userstring which is also a number
                if (UserStringExists(data))
                    return UserString(data);
                const int planet_id = ToInt(data, INVALID_OBJECT_ID);
                if (auto planet = context.ContextObjects().getRaw<Planet>(planet_id))
                    return UserString(to_string(planet->EnvironmentForSpecies(context.species)));
                return UserString("UNKNOWN_PLANET");
            }},
    }};

    // .first = result  .second = was there a substitution matching \a tag
    std::tuple<boost::optional<std::string>, bool> EvalContextSub(
        std::string_view tag, std::string_view value, const ScriptingContext& context)
    {
        const auto it = std::find_if(context_substitution_map.begin(), context_substitution_map.end(),
                                     [tag](const auto& e) { return e.first == tag; });
        if (it == context_substitution_map.end())
            return {boost::none, false}; // no such substitution found
        const auto& sub_func = it->second;
        auto opt_string = sub_func(value, context); // may be empty optional, but substitution was found
        return {opt_string, true};
    }

    // .first = result  .second = was there a substitution matching \a tag
    std::tuple<boost::optional<std::string>, bool> EvalNoContextSub(
        std::string_view tag, std::string_view value)
    {
        const auto it = std::find_if(no_context_substitution_map.begin(), no_context_substitution_map.end(),
                                     [tag](const auto& e) { return e.first == tag; });
        if (it == no_context_substitution_map.end())
            return {boost::none, false}; // no such substitution found
        const auto& sub_func = it->second;
        auto opt_string = sub_func(value); // may be empty optional, but substitution was found
        return {opt_string, true};
    }

    std::tuple<std::string_view, std::string_view, std::string_view, bool> GetLabelTagViews(
        const std::map<std::string, std::string, std::less<>>& variables, xpr::smatch const& match)
    {
        // Labelled variables have the form %tag:label%,  unlabelled are just %tag%
        // Use the label value. When missing, use the tag submatch as label instead.

        const bool have_label = match[2].matched;

        //const auto& full_match = match[0];
        const auto& tag_match = match[1];
        const auto& label_match = match[have_label ? 2 : 1];

        //std::cout << "match length: " << match.length() << (have_label ? " with label" : " without label") << std::endl;
        //std::cout << "full: " << full_match << "   tag: " << tag_match << "   label: " << label_match << std::endl;

        static constexpr decltype(label_match.length()) zero = 0;

        const std::size_t label_sz = static_cast<std::size_t>(std::max(zero, label_match.length()));
        const std::string_view label(&*label_match.first, label_sz);

        // look up child
        const auto elem = variables.find(label);
        //std::cout << (elem == variables.end() ? "... label not in variables": "found label in variables") << std::endl;

        if (elem == variables.end())
            return {"", "", "", false};

        const std::size_t tag_sz = static_cast<std::size_t>(std::max(zero, tag_match.length()));
        const std::string_view tag(&*tag_match.first, tag_sz);

        return {label, elem->second, tag, true};
    }


    //! Looks up the given match in the Universe and returns the Universe
    //! entities value. If the lookup or the substitution fails, sets
    //! \a valid to false.
    std::string Substitute(const std::map<std::string, std::string, std::less<>>& variables,
                           bool& valid, xpr::smatch const& match,
                           const ScriptingContext* context)
    {
        // Labelled variables have the form %tag:label%,  unlabelled are just %tag%
        // Use the label value. When missing, use the tag submatch as label instead.
        auto [label, variable_value, tag, label_found] = GetLabelTagViews(variables, match);
        if (!label_found) {
            ErrorLogger() << "Substitute: No substitution function found for label: " << label << "  from token: " << match.str();
            valid = false;
            return UserString("ERROR");
        }

        const auto [sub_opt, sub_found] = [context](std::string_view tag, std::string_view variable_value) {
            if (context) {
                auto retval = EvalContextSub(tag, variable_value, *context);
                if (std::get<1>(retval))
                    return retval;
            }
            return EvalNoContextSub(tag, variable_value);
        }(tag, variable_value);


        if (!sub_found) {
            ErrorLogger() << "No substitution found for tag: " << tag << " from token: " << match.str();
            valid = false;
            return UserString("ERROR");
        } else if (!sub_opt) {
            ErrorLogger() << "Substitution for tag: " << tag << " and value: " << variable_value << " returned empty optional<string>";
            valid = false;
            return UserString("ERROR");
        } else {
            valid = true;
            return *sub_opt;
        }
    }
}


VarText::VarText(std::string template_string, bool stringtable_lookup) :
    m_template_string(std::move(template_string)),
    m_stringtable_lookup_flag(stringtable_lookup)
{}

const std::string& VarText::GetText(const ScriptingContext& context) const {
    if (m_text.empty())
        GenerateVarText(&context);
    return m_text;
}

const std::string& VarText::GetText() const {
    if (m_text.empty())
        GenerateVarText(nullptr);
    return m_text;
}

bool VarText::Validate(const ScriptingContext& context) const {
    if (m_text.empty())
        GenerateVarText(&context);
    return m_validated;
}

bool VarText::Validate() const {
    if (m_text.empty())
        GenerateVarText(nullptr);
    return m_validated;
}

void VarText::SetTemplateString(std::string template_string, bool stringtable_lookup) {
    m_template_string = std::move(template_string);
    m_stringtable_lookup_flag = stringtable_lookup;
}

std::vector<std::string_view> VarText::GetVariableTags() const {
    std::vector<std::string_view> retval;
    retval.reserve(m_variables.size());
    auto rng = m_variables | range_keys;
    range_copy(rng, std::back_inserter(retval));
    return retval;
}

void VarText::AddVariable(std::string tag, std::string data)
{ m_variables[std::move(tag)] = std::move(data); }

void VarText::AddVariables(std::vector<std::pair<std::string, std::string>>&& data) {
    for (auto& dat : data)
        m_variables.insert(std::move(dat));
}

void VarText::GenerateVarText(const ScriptingContext* context) const {
    // generate a string complete with substituted variables and hyperlinks
    // the procedure here is to replace any tokens within %% with variables of
    // the same name in the SitRep XML data
    m_text.clear();
    m_validated = true;
    if (m_template_string.empty())
        return;

    // get string into which to substitute variables
    const auto& template_str = m_stringtable_lookup_flag ? UserString(m_template_string) : m_template_string;

    auto sub = [this, &context](const auto& match) -> std::string
    { return Substitute(m_variables, m_validated, match, context); };

    xpr::sregex var = '%' >> (xpr::s1 = -+xpr::_w) >> !(':' >> (xpr::s2 = -+xpr::_w)) >> '%';
    m_text = xpr::regex_replace(template_str, var, sub);
}