mirror of
https://github.com/amyinspace/MagicSetEditor2.git
synced 2026-06-10 04:57:00 -04:00
Add to_json and from_json script functions
This commit is contained in:
@@ -7,7 +7,7 @@ Creates a new [[type:card]] object. The card is not automatically added to a set
|
|||||||
|
|
||||||
The argument is a map from card field names to values, for example @new_card([name: "My Card"])@ creates a card with the name @"My Card"@, and all other fields at their default value.
|
The argument is a map from card field names to values, for example @new_card([name: "My Card"])@ creates a card with the name @"My Card"@, and all other fields at their default value.
|
||||||
|
|
||||||
The map can also contain the following built-in keys: notes, id, linked_card_1 to linked_card_4, linked_relation_1 to linked_relation_4, stylesheet, and styling_data. For styling_data, the value must itself be a map from styling field names to values. Be sure to define a stylesheet before styling_data.
|
The map can also contain the following built-in keys: notes, id, linked_card_1 to linked_card_4, linked_relation_1 to linked_relation_4, stylesheet, styling_data, and extra_data. For styling_data and extra_data, the value must itself be a map from field names to values. Be sure to define a stylesheet before these.
|
||||||
|
|
||||||
NOTE: you should use underscores instead of spaces in field names.
|
NOTE: you should use underscores instead of spaces in field names.
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ Field::~Field() {}
|
|||||||
|
|
||||||
void Field::initDependencies(Context& ctx, const Dependency& dep) const {
|
void Field::initDependencies(Context& ctx, const Dependency& dep) const {
|
||||||
sort_script.initDependencies(ctx, dep);
|
sort_script.initDependencies(ctx, dep);
|
||||||
import_script.initDependencies(ctx, dep);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IMPLEMENT_REFLECTION(Field) {
|
IMPLEMENT_REFLECTION(Field) {
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ wxDataFormat CardsDataObject::format = _("application/x-mse-cards");
|
|||||||
|
|
||||||
CardsDataObject::CardsDataObject(const SetP& set, const vector<CardP>& cards) {
|
CardsDataObject::CardsDataObject(const SetP& set, const vector<CardP>& cards) {
|
||||||
// set the stylesheet, so when deserializing we know whos style options we are reading
|
// set the stylesheet, so when deserializing we know whos style options we are reading
|
||||||
bool* has_styling = new bool[cards.size()];
|
vector<bool> has_styling;
|
||||||
for (size_t i = 0 ; i < cards.size() ; ++i) {
|
for (size_t i = 0 ; i < cards.size() ; ++i) {
|
||||||
has_styling[i] = cards[i]->has_styling && !cards[i]->stylesheet;
|
has_styling.push_back(cards[i]->has_styling && !cards[i]->stylesheet);
|
||||||
if (has_styling[i]) {
|
if (has_styling[i]) {
|
||||||
cards[i]->stylesheet = set->stylesheet;
|
cards[i]->stylesheet = set->stylesheet;
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,6 @@ CardsDataObject::CardsDataObject(const SetP& set, const vector<CardP>& cards) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetFormat(format);
|
SetFormat(format);
|
||||||
delete [] has_styling;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CardsDataObject::CardsDataObject() {
|
CardsDataObject::CardsDataObject() {
|
||||||
|
|||||||
@@ -91,11 +91,11 @@ public:
|
|||||||
Keyword() : fixed(false), valid(false) {}
|
Keyword() : fixed(false), valid(false) {}
|
||||||
|
|
||||||
String keyword; ///< The keyword, only for human use
|
String keyword; ///< The keyword, only for human use
|
||||||
String rules; ///< Rules/explanation
|
String rules; ///< Rules/explanation
|
||||||
String match; ///< String to match, <atom-param> tags are used for parameters
|
String match; ///< String to match, <atom-param> tags are used for parameters
|
||||||
vector<KeywordParamP> parameters; ///< The types of parameters
|
vector<KeywordParamP> parameters; ///< The types of parameters
|
||||||
StringScript reminder; ///< Reminder text of the keyword
|
StringScript reminder; ///< Reminder text of the keyword
|
||||||
String mode; ///< Mode of use, can be used by scripts (only gives the name)
|
String mode; ///< Mode of use, can be used by scripts (only gives the name)
|
||||||
/// Regular expression to match and split parameters, automatically generated.
|
/// Regular expression to match and split parameters, automatically generated.
|
||||||
/** The regex has exactly 2 * parameters.size() + 1 captures (excluding the entire match, caputure 0),
|
/** The regex has exactly 2 * parameters.size() + 1 captures (excluding the entire match, caputure 0),
|
||||||
* captures 1,3,... capture the plain text of the match string
|
* captures 1,3,... capture the plain text of the match string
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ bool PackType::update(Context& ctx) {
|
|||||||
|
|
||||||
bool PackItem::update(Context& ctx) {
|
bool PackItem::update(Context& ctx) {
|
||||||
return amount.update(ctx)
|
return amount.update(ctx)
|
||||||
| weight.update(ctx);
|
|| weight.update(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ public:
|
|||||||
/// The values on the fields of the set
|
/// The values on the fields of the set
|
||||||
/** The indices should correspond to the set_fields in the Game */
|
/** The indices should correspond to the set_fields in the Game */
|
||||||
IndexMap<FieldP, ValueP> data;
|
IndexMap<FieldP, ValueP> data;
|
||||||
/// Extra values for specitic stylesheets, indexed by stylesheet name
|
/// Extra values for specific stylesheets, indexed by stylesheet name
|
||||||
DelayedIndexMaps<FieldP,ValueP> styling_data;
|
DelayedIndexMaps<FieldP,ValueP> styling_data;
|
||||||
vector<CardP> cards; ///< The cards in the set
|
vector<CardP> cards; ///< The cards in the set
|
||||||
vector<KeywordP> keywords; ///< Additional keywords used in this set
|
vector<KeywordP> keywords; ///< Additional keywords used in this set
|
||||||
|
|||||||
@@ -19,10 +19,11 @@
|
|||||||
#include <data/stylesheet.hpp>
|
#include <data/stylesheet.hpp>
|
||||||
#include <data/action/set.hpp>
|
#include <data/action/set.hpp>
|
||||||
#include <gui/add_json_window.hpp>
|
#include <gui/add_json_window.hpp>
|
||||||
|
#include <script/functions/json.hpp>
|
||||||
#include <script/functions/construction_helper.hpp>
|
#include <script/functions/construction_helper.hpp>
|
||||||
#include <wx/statline.h>
|
#include <wx/statline.h>
|
||||||
#include <boost/json/src.hpp>
|
|
||||||
#include <boost/json.hpp>
|
#include <boost/json.hpp>
|
||||||
|
#include <boost/json/src.hpp>
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------- : AddJSON
|
// ----------------------------------------------------------------------------- : AddJSON
|
||||||
|
|
||||||
@@ -63,54 +64,6 @@ AddJSONWindow::AddJSONWindow(Window* parent, const SetP& set, bool sizer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScriptValueP json_to_script(boost::json::value& jv) {
|
|
||||||
if (jv == nullptr) return script_nil;
|
|
||||||
else if (jv.is_null()) return script_nil;
|
|
||||||
else if (jv.is_bool()) return to_script(jv.get_bool());
|
|
||||||
else if (jv.is_double()) return to_script(jv.get_double());
|
|
||||||
else if (jv.is_int64()) {
|
|
||||||
int integer = jv.get_int64();
|
|
||||||
return to_script(integer);
|
|
||||||
}
|
|
||||||
else if (jv.is_uint64()) {
|
|
||||||
int integer = jv.get_uint64();
|
|
||||||
return to_script(integer);
|
|
||||||
}
|
|
||||||
else if (jv.is_string()) {
|
|
||||||
std::string stdstring = boost::json::value_to<std::string>(jv);
|
|
||||||
String wxstring(stdstring.c_str(), wxConvUTF8);
|
|
||||||
return to_script(wxstring);
|
|
||||||
}
|
|
||||||
else if (jv.is_array()) {
|
|
||||||
boost::json::array array = jv.get_array();
|
|
||||||
ScriptCustomCollectionP result = make_intrusive<ScriptCustomCollection>();
|
|
||||||
for (int i = 0; i < array.size(); ++i) {
|
|
||||||
boost::json::value jvalue = array[i];
|
|
||||||
ScriptValueP value = json_to_script(jvalue);
|
|
||||||
result->value.push_back(value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else if (jv.is_object()) {
|
|
||||||
boost::json::object object = jv.get_object();
|
|
||||||
ScriptCustomCollectionP result = make_intrusive<ScriptCustomCollection>();
|
|
||||||
for (auto it = object.begin(); it != object.end(); ++it) {
|
|
||||||
boost::json::string_view jview = it->key();
|
|
||||||
std::string_view stdview = std::string_view(jview.data(), jview.size());
|
|
||||||
std::string stdstring = { stdview.begin(), stdview.end() };
|
|
||||||
String key(stdstring.c_str(), wxConvUTF8);
|
|
||||||
boost::json::value jvalue = it->value();
|
|
||||||
ScriptValueP value = json_to_script(jvalue);
|
|
||||||
result->key_value[key] = value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
queue_message(MESSAGE_ERROR, _ERROR_("add card json unknown type"));
|
|
||||||
return script_nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AddJSONWindow::setJSONType() {
|
void AddJSONWindow::setJSONType() {
|
||||||
int sel = json_type->GetSelection();
|
int sel = json_type->GetSelection();
|
||||||
if (sel == json_type->GetCount() - 1) { // Custom type
|
if (sel == json_type->GetCount() - 1) { // Custom type
|
||||||
@@ -210,7 +163,7 @@ void AddJSONWindow::onBrowseFiles(wxCommandEvent&) {
|
|||||||
auto& card = card_array[i].as_object();
|
auto& card = card_array[i].as_object();
|
||||||
for (int h = 0; h < headers_out.size(); ++h) {
|
for (int h = 0; h < headers_out.size(); ++h) {
|
||||||
auto& value = card[headers_out[h].ToStdString()];
|
auto& value = card[headers_out[h].ToStdString()];
|
||||||
row.push_back(json_to_script(value));
|
row.push_back(json_to_mse(value, set.get()));
|
||||||
}
|
}
|
||||||
table_out.push_back(row);
|
table_out.push_back(row);
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-27
@@ -102,36 +102,32 @@ CardsPanel::CardsPanel(Window* parent, int id)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CardsPanel::updateCardCounts() {
|
void CardsPanel::updateCardCounts() {
|
||||||
if (counts) {
|
if (counts && card_list && set) {
|
||||||
if (card_list && set) {
|
int selected = card_list->GetSelectedItemCount();
|
||||||
int selected = card_list->GetSelectedItemCount();
|
int filtered = card_list->GetItemCount();
|
||||||
int filtered = card_list->GetItemCount();
|
int total = set->cards.size();
|
||||||
int total = set->cards.size();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selected_cards_count == selected
|
selected_cards_count == selected
|
||||||
&& filtered_cards_count == filtered
|
&& filtered_cards_count == filtered
|
||||||
&& total_cards_count == total
|
&& total_cards_count == total
|
||||||
) return;
|
&& !counts->GetLabel().empty()
|
||||||
|
) return;
|
||||||
|
|
||||||
selected_cards_count = selected;
|
selected_cards_count = selected;
|
||||||
filtered_cards_count = filtered;
|
filtered_cards_count = filtered;
|
||||||
total_cards_count = total;
|
total_cards_count = total;
|
||||||
|
|
||||||
if (filtered == total) {
|
if (filtered == total) {
|
||||||
counts->SetLabel(_TOOL_2_("card counts 2",
|
counts->SetLabel(_TOOL_2_("card counts 2",
|
||||||
wxString::Format(wxT("%i"), selected),
|
wxString::Format(wxT("%i"), selected),
|
||||||
wxString::Format(wxT("%i"), total)));
|
wxString::Format(wxT("%i"), total)));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
counts->SetLabel(_TOOL_3_("card counts 3",
|
counts->SetLabel(_TOOL_3_("card counts 3",
|
||||||
wxString::Format(wxT("%i"), selected),
|
wxString::Format(wxT("%i"), selected),
|
||||||
wxString::Format(wxT("%i"), filtered),
|
wxString::Format(wxT("%i"), filtered),
|
||||||
wxString::Format(wxT("%i"), total)));
|
wxString::Format(wxT("%i"), total)));
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
counts->SetLabel(_(""));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
#include <util/version.hpp>
|
#include <util/version.hpp>
|
||||||
#include <script/functions/functions.hpp>
|
#include <script/functions/functions.hpp>
|
||||||
#include <script/functions/util.hpp>
|
#include <script/functions/util.hpp>
|
||||||
|
#include <script/functions/construction_helper.hpp>
|
||||||
|
#include <script/functions/json.hpp>
|
||||||
#include <util/tagged_string.hpp>
|
#include <util/tagged_string.hpp>
|
||||||
#include <util/spec_sort.hpp>
|
#include <util/spec_sort.hpp>
|
||||||
#include <util/error.hpp>
|
#include <util/error.hpp>
|
||||||
@@ -21,6 +23,8 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
#include <wx/filename.h>
|
#include <wx/filename.h>
|
||||||
#include <wx/stdpaths.h>
|
#include <wx/stdpaths.h>
|
||||||
|
#include <wx/wfstream.h>
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------- : Debugging
|
// ----------------------------------------------------------------------------- : Debugging
|
||||||
@@ -257,6 +261,24 @@ SCRIPT_FUNCTION(to_code) {
|
|||||||
SCRIPT_RETURN(input->toCode());
|
SCRIPT_RETURN(input->toCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCRIPT_FUNCTION(to_json) {
|
||||||
|
SCRIPT_PARAM_C(ScriptValueP, input);
|
||||||
|
SCRIPT_PARAM_C(Set*, set);
|
||||||
|
SCRIPT_PARAM_DEFAULT(bool, pretty_print, true);
|
||||||
|
boost::json::value jv = mse_to_json(input, set);
|
||||||
|
|
||||||
|
queue_message(MESSAGE_ERROR, json_pretty_print(jv));
|
||||||
|
|
||||||
|
if (pretty_print) return to_script(json_pretty_print(jv));
|
||||||
|
else return to_script(json_ugly_print(jv));
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_FUNCTION(from_json) {
|
||||||
|
SCRIPT_PARAM_C(ScriptValueP, input);
|
||||||
|
SCRIPT_PARAM_C(Set*, set);
|
||||||
|
return json_to_mse(input, set);
|
||||||
|
}
|
||||||
|
|
||||||
SCRIPT_FUNCTION(type_name) {
|
SCRIPT_FUNCTION(type_name) {
|
||||||
SCRIPT_PARAM_C(ScriptValueP, input);
|
SCRIPT_PARAM_C(ScriptValueP, input);
|
||||||
SCRIPT_RETURN(input->typeName());
|
SCRIPT_RETURN(input->typeName());
|
||||||
@@ -818,6 +840,8 @@ void init_script_basic_functions(Context& ctx) {
|
|||||||
ctx.setVariable(_("to_color"), script_to_color);
|
ctx.setVariable(_("to_color"), script_to_color);
|
||||||
ctx.setVariable(_("to_date"), script_to_date);
|
ctx.setVariable(_("to_date"), script_to_date);
|
||||||
ctx.setVariable(_("to_code"), script_to_code);
|
ctx.setVariable(_("to_code"), script_to_code);
|
||||||
|
ctx.setVariable(_("to_json"), script_to_json);
|
||||||
|
ctx.setVariable(_("from_json"), script_from_json);
|
||||||
ctx.setVariable(_("type_name"), script_type_name);
|
ctx.setVariable(_("type_name"), script_type_name);
|
||||||
ctx.setVariable(_("make_map"), script_make_map);
|
ctx.setVariable(_("make_map"), script_make_map);
|
||||||
ctx.setVariable(_("get_card_styling"), script_get_card_styling);
|
ctx.setVariable(_("get_card_styling"), script_get_card_styling);
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ SCRIPT_FUNCTION(new_card) {
|
|||||||
if (key == script_nil) continue;
|
if (key == script_nil) continue;
|
||||||
String key_name = key->toString();
|
String key_name = key->toString();
|
||||||
// check if the given value is for a built-in field
|
// check if the given value is for a built-in field
|
||||||
if (set_builtin_container(game, new_card, value, key_name, ignore_field_not_found)) continue;
|
if (set_builtin_container(*game, new_card, value, key_name, ignore_field_not_found)) continue;
|
||||||
// find the field value (container) that corresponds to the given value
|
// find the field value (container) that corresponds to the given value
|
||||||
Value* container = get_container(game, new_card, key_name, ignore_field_not_found);
|
Value* container = get_card_field_container(*game, new_card->data, key_name, ignore_field_not_found);
|
||||||
if (container == nullptr) continue;
|
if (container == nullptr) continue;
|
||||||
FieldP field = container->fieldP;
|
FieldP field = container->fieldP;
|
||||||
// if the field has a construction script, set the value and card context variables to be the given value and this card, run script
|
// if the field has a construction script, set the value and card context variables to be the given value and this card, run script
|
||||||
@@ -59,9 +59,9 @@ SCRIPT_FUNCTION(new_card) {
|
|||||||
if (script_key == script_nil) continue;
|
if (script_key == script_nil) continue;
|
||||||
String script_key_name = script_key->toString();
|
String script_key_name = script_key->toString();
|
||||||
// check if the script value is for a built-in field
|
// check if the script value is for a built-in field
|
||||||
if (set_builtin_container(game, new_card, script_value, script_key_name, ignore_field_not_found)) continue;
|
if (set_builtin_container(*game, new_card, script_value, script_key_name, ignore_field_not_found)) continue;
|
||||||
// find the field value that corresponds to the script value
|
// find the field value that corresponds to the script value
|
||||||
Value* script_container = get_container(game, new_card, script_key_name, ignore_field_not_found);
|
Value* script_container = get_card_field_container(*game, new_card->data, script_key_name, ignore_field_not_found);
|
||||||
if (script_container == nullptr) continue;
|
if (script_container == nullptr) continue;
|
||||||
// set the field value to the script value
|
// set the field value to the script value
|
||||||
set_container(script_container, script_value, script_key_name);
|
set_container(script_container, script_value, script_key_name);
|
||||||
@@ -94,9 +94,9 @@ SCRIPT_FUNCTION(new_card) {
|
|||||||
if (script_key == script_nil) continue;
|
if (script_key == script_nil) continue;
|
||||||
String script_key_name = script_key->toString();
|
String script_key_name = script_key->toString();
|
||||||
// check if the script value is for a built-in field
|
// check if the script value is for a built-in field
|
||||||
if (set_builtin_container(game, new_card, script_value, script_key_name, ignore_field_not_found)) continue;
|
if (set_builtin_container(*game, new_card, script_value, script_key_name, ignore_field_not_found)) continue;
|
||||||
// find the field value that corresponds to the script value
|
// find the field value that corresponds to the script value
|
||||||
Value* script_container = get_container(game, new_card, script_key_name, ignore_field_not_found);
|
Value* script_container = get_card_field_container(*game, new_card->data, script_key_name, ignore_field_not_found);
|
||||||
if (script_container == nullptr) continue;
|
if (script_container == nullptr) continue;
|
||||||
// set the field value to the script value
|
// set the field value to the script value
|
||||||
set_container(script_container, script_value, script_key_name);
|
set_container(script_container, script_value, script_key_name);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
//| License: GNU General Public License 2 or later (see file COPYING) |
|
//| License: GNU General Public License 2 or later (see file COPYING) |
|
||||||
//+----------------------------------------------------------------------------+
|
//+----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------- : Includes
|
// ----------------------------------------------------------------------------- : Includes
|
||||||
|
|
||||||
#include <util/prec.hpp>
|
#include <util/prec.hpp>
|
||||||
@@ -12,33 +14,46 @@
|
|||||||
#include <data/field/package_choice.hpp>
|
#include <data/field/package_choice.hpp>
|
||||||
#include <data/field/color.hpp>
|
#include <data/field/color.hpp>
|
||||||
#include <data/field/image.hpp>
|
#include <data/field/image.hpp>
|
||||||
|
#include <data/field/symbol.hpp>
|
||||||
#include <data/action/set.hpp>
|
#include <data/action/set.hpp>
|
||||||
#include <data/game.hpp>
|
#include <data/game.hpp>
|
||||||
#include <data/set.hpp>
|
#include <data/set.hpp>
|
||||||
#include <data/stylesheet.hpp>
|
#include <data/stylesheet.hpp>
|
||||||
#include <data/card.hpp>
|
#include <data/card.hpp>
|
||||||
#include <util/error.hpp>
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------- : Helper functions
|
// ----------------------------------------------------------------------------- : Helper functions
|
||||||
|
|
||||||
static Value* get_container(GameP& game, CardP& card, String key_name, bool ignore_field_not_found) {
|
inline static Value* get_card_field_container(Game& game, IndexMap<FieldP, ValueP>& map, String& key_name, bool ignore_field_not_found) {
|
||||||
// find value container to update
|
// find value container to update
|
||||||
IndexMap<FieldP, ValueP>::const_iterator value_it = card->data.find(key_name);
|
IndexMap<FieldP, ValueP>::const_iterator it = map.find(key_name);
|
||||||
if (value_it == card->data.end()) {
|
if (it == map.end()) {
|
||||||
// look among alternate names
|
// look among alternate names
|
||||||
map<String, String>::iterator alt_name_it = game->card_fields_alt_names.find(unified_form(key_name));
|
std::map<String, String>::iterator alt_name_it = game.card_fields_alt_names.find(unified_form(key_name));
|
||||||
if (alt_name_it != game->card_fields_alt_names.end()) {
|
if (alt_name_it != game.card_fields_alt_names.end()) {
|
||||||
value_it = card->data.find(alt_name_it->second);
|
it = map.find(alt_name_it->second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (value_it == card->data.end()) {
|
if (it == map.end()) {
|
||||||
if (ignore_field_not_found) return nullptr;
|
if (ignore_field_not_found) return nullptr;
|
||||||
throw ScriptError(_ERROR_1_("no field with name", key_name));
|
throw ScriptError(_ERROR_2_("no field with name", _TYPE_("card"), key_name));
|
||||||
}
|
}
|
||||||
return value_it->get();
|
return it->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_container(Value* container, ScriptValueP& value, String key_name) {
|
inline static Value* get_container(IndexMap<FieldP, ValueP>& map, String& type, String& key_name, bool ignore_field_not_found) {
|
||||||
|
// find value container to update
|
||||||
|
IndexMap<FieldP, ValueP>::const_iterator it = map.find(key_name);
|
||||||
|
if (it == map.end()) {
|
||||||
|
it = map.find(key_name.Lower());
|
||||||
|
if (it == map.end()) {
|
||||||
|
if (ignore_field_not_found) return nullptr;
|
||||||
|
throw ScriptError(_ERROR_2_("no field with name", _TYPE_V_(type), key_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return it->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static void set_container(Value* container, ScriptValueP& value, String key_name) {
|
||||||
// set the given value into the container
|
// set the given value into the container
|
||||||
if (TextValue* tvalue = dynamic_cast<TextValue*>(container)) {
|
if (TextValue* tvalue = dynamic_cast<TextValue*>(container)) {
|
||||||
tvalue->value = value->toString();
|
tvalue->value = value->toString();
|
||||||
@@ -53,23 +68,36 @@ static void set_container(Value* container, ScriptValueP& value, String key_name
|
|||||||
cvalue->value = value->toColor();
|
cvalue->value = value->toColor();
|
||||||
}
|
}
|
||||||
else if (ImageValue* ivalue = dynamic_cast<ImageValue*>(container)) {
|
else if (ImageValue* ivalue = dynamic_cast<ImageValue*>(container)) {
|
||||||
wxFileName fname(static_cast<ExternalImage*>(value.get())->toString());
|
if (ExternalImage* image = dynamic_cast<ExternalImage*>(value.get())) {
|
||||||
ivalue->filename = LocalFileName::fromReadString(fname.GetName(), "");
|
wxFileName fname(image->toString());
|
||||||
|
ivalue->filename = LocalFileName::fromReadString(fname.GetName(), "");
|
||||||
|
} else if (value->type() == SCRIPT_STRING) {
|
||||||
|
ivalue->filename = LocalFileName::fromReadString(value->toString(), "");
|
||||||
|
} else {
|
||||||
|
throw ScriptError(_ERROR_1_("cant set image value", key_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (SymbolValue* svalue = dynamic_cast<SymbolValue*>(container)) {
|
||||||
|
if (value->type() == SCRIPT_STRING) {
|
||||||
|
svalue->filename = LocalFileName::fromReadString(value->toString(), "");
|
||||||
|
} else {
|
||||||
|
throw ScriptError(_ERROR_1_("cant set symbol value", key_name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw ScriptError(_ERROR_1_("can't set value", key_name));
|
throw ScriptError(_ERROR_1_("cant set value", key_name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool set_builtin_container(GameP& game, CardP& card, ScriptValueP& value, String key_name, bool ignore_field_not_found) {
|
inline static bool set_builtin_container(const Game& game, CardP& card, ScriptValueP& value, String key_name, bool ignore_field_not_found) {
|
||||||
// check if the given value is for a built-in field, if found set it and return true
|
// check if the given value is for a built-in field, if found set it and return true
|
||||||
key_name = unified_form(key_name);
|
key_name = unified_form(key_name);
|
||||||
if (key_name == _("notes") || key_name == _("note")) {
|
if (key_name == _("notes") || key_name == _("note")) {
|
||||||
card->notes = value->toString();
|
card->notes = value->toString();
|
||||||
return true;
|
return true;
|
||||||
} else if (key_name == _("style") || key_name == _("stylesheet") || key_name == _("template")) {
|
} else if (key_name == _("style") || key_name == _("stylesheet") || key_name == _("template")) {
|
||||||
if (trim(value->toString()) != wxEmptyString) {
|
if (!trim(value->toString()).empty()) {
|
||||||
card->stylesheet = StyleSheet::byGameAndName(*game, value->toString());
|
card->stylesheet = StyleSheet::byGameAndName(game, value->toString());
|
||||||
if (card->stylesheet) card->styling_data.init(card->stylesheet->styling_fields);
|
if (card->stylesheet) card->styling_data.init(card->stylesheet->styling_fields);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -110,35 +138,34 @@ static bool set_builtin_container(GameP& game, CardP& card, ScriptValueP& value,
|
|||||||
// card->linked_relation_4 = value->toString();
|
// card->linked_relation_4 = value->toString();
|
||||||
// return true;
|
// return true;
|
||||||
//}
|
//}
|
||||||
else if (key_name == _("styling_data") || key_name == _("style_data") || key_name == _("stylesheet_data") || key_name == _("template_data") || key_name == _("styling")
|
else if (key_name == _("styling_data") || key_name == _("style_data") || key_name == _("stylesheet_data") || key_name == _("template_data") || key_name == _("styling")
|
||||||
|| key_name == _("styling_fields") || key_name == _("style_fields") || key_name == _("stylesheet_fields") || key_name == _("template_fields")) {
|
|| key_name == _("styling_fields") || key_name == _("style_fields") || key_name == _("stylesheet_fields") || key_name == _("template_fields")
|
||||||
|
|| key_name == _("extra_data") || key_name == _("extra_fields") || key_name == _("extra_card_data") || key_name == _("extra_card_fields")) {
|
||||||
|
bool is_extra = key_name == _("extra_data") || key_name == _("extra_fields") || key_name == _("extra_card_data") || key_name == _("extra_card_fields");
|
||||||
|
String type = is_extra ? _("extra") : _("styling");
|
||||||
if (value->type() != SCRIPT_COLLECTION) {
|
if (value->type() != SCRIPT_COLLECTION) {
|
||||||
throw ScriptError(_ERROR_("styling data not map"));
|
throw ScriptError(_ERROR_1_("styling data not map", type));
|
||||||
}
|
}
|
||||||
ScriptValueP value_it = value->makeIterator();
|
if (!card->stylesheet) {
|
||||||
ScriptValueP value_key;
|
throw ScriptError(_ERROR_1_("styling data without stylesheet", type));
|
||||||
while (ScriptValueP value_value = value_it->next(&value_key)) {
|
}
|
||||||
assert(value_key);
|
IndexMap<FieldP, ValueP>& data = is_extra ? card->extraDataFor(*card->stylesheet) : card->styling_data;
|
||||||
if (value_key == script_nil) continue;
|
ScriptValueP it = value->makeIterator();
|
||||||
String value_key_name = value_key->toString();
|
ScriptValueP key;
|
||||||
IndexMap<FieldP, ValueP>::const_iterator style_it = card->styling_data.find(value_key_name);
|
while (ScriptValueP value = it->next(&key)) {
|
||||||
if (style_it == card->styling_data.end()) {
|
assert(key);
|
||||||
style_it = card->styling_data.find(value_key_name.Lower());
|
if (key == script_nil) continue;
|
||||||
if (style_it == card->styling_data.end()) {
|
String key_name = key->toString();
|
||||||
if (!ignore_field_not_found) throw ScriptError(_ERROR_1_("no style field with name", value_key_name));
|
Value* container = get_container(data, type, key_name, ignore_field_not_found);
|
||||||
continue;
|
set_container(container, value, key_name);
|
||||||
}
|
if (!is_extra) card->has_styling = true;
|
||||||
}
|
|
||||||
Value* value_container = style_it->get();
|
|
||||||
set_container(value_container, value_value, value_key_name);
|
|
||||||
card->has_styling = true;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool check_table_headers(GameP& game, std::vector<String>& headers, const String& file_extension, String& missing_fields_out) {
|
inline static bool check_table_headers(GameP& game, std::vector<String>& headers, const String& file_extension, String& missing_fields_out) {
|
||||||
if (headers.empty()) {
|
if (headers.empty()) {
|
||||||
queue_message(MESSAGE_ERROR, _("Empty headers given"));
|
queue_message(MESSAGE_ERROR, _("Empty headers given"));
|
||||||
return false;
|
return false;
|
||||||
@@ -176,7 +203,7 @@ static bool check_table_headers(GameP& game, std::vector<String>& headers, const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool cards_from_table(SetP& set, vector<String>& headers, std::vector<std::vector<ScriptValueP>>& table, bool ignore_field_not_found, const String& file_extension, vector<CardP>& cards_out) {
|
inline static bool cards_from_table(SetP& set, vector<String>& headers, std::vector<std::vector<ScriptValueP>>& table, bool ignore_field_not_found, const String& file_extension, vector<CardP>& cards_out) {
|
||||||
// ensure table is square
|
// ensure table is square
|
||||||
int count = headers.size();
|
int count = headers.size();
|
||||||
for (int y = 0; y < table.size(); ++y) {
|
for (int y = 0; y < table.size(); ++y) {
|
||||||
@@ -212,5 +239,3 @@ static bool cards_from_table(SetP& set, vector<String>& headers, std::vector<std
|
|||||||
if (ctx_ignore) ctx.setVariable("ignore_field_not_found", ctx_ignore);
|
if (ctx_ignore) ctx.setVariable("ignore_field_not_found", ctx_ignore);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,596 @@
|
|||||||
|
//+----------------------------------------------------------------------------+
|
||||||
|
//| Description: Magic Set Editor - Program to make Magic (tm) cards |
|
||||||
|
//| Copyright: (C) Twan van Laarhoven and the other MSE developers |
|
||||||
|
//| License: GNU General Public License 2 or later (see file COPYING) |
|
||||||
|
//+----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------- : Includes
|
||||||
|
|
||||||
|
#include <util/prec.hpp>
|
||||||
|
#include <data/set.hpp>
|
||||||
|
#include <data/card.hpp>
|
||||||
|
#include <data/pack.hpp>
|
||||||
|
#include <data/format/clipboard.hpp>
|
||||||
|
#include <script/functions/construction_helper.hpp>
|
||||||
|
#include <boost/json.hpp>
|
||||||
|
#include <sstream>
|
||||||
|
#include <wx/sstream.h>
|
||||||
|
|
||||||
|
|
||||||
|
// All this isn't great, but it will do for now. Idealy you would create JsonWriter and JsonReader classes
|
||||||
|
// that inherit from Writer and Reader, and just have a switch to go from normal mode to JSON mode...
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------- : JSON to String
|
||||||
|
|
||||||
|
inline static void pretty_print(std::ostream& os, boost::json::value const& jv, std::string* indent = nullptr)
|
||||||
|
{
|
||||||
|
std::string indent_;
|
||||||
|
if(! indent)
|
||||||
|
indent = &indent_;
|
||||||
|
switch(jv.kind())
|
||||||
|
{
|
||||||
|
case boost::json::kind::object:
|
||||||
|
{
|
||||||
|
os << "{\n";
|
||||||
|
indent->append(4, ' ');
|
||||||
|
auto const& obj = jv.get_object();
|
||||||
|
if(! obj.empty())
|
||||||
|
{
|
||||||
|
auto it = obj.begin();
|
||||||
|
for(;;)
|
||||||
|
{
|
||||||
|
os << *indent << boost::json::serialize(it->key()) << " : ";
|
||||||
|
pretty_print(os, it->value(), indent);
|
||||||
|
if(++it == obj.end())
|
||||||
|
break;
|
||||||
|
os << ",\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << "\n";
|
||||||
|
indent->resize(indent->size() - 4);
|
||||||
|
os << *indent << "}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case boost::json::kind::array:
|
||||||
|
{
|
||||||
|
os << "[\n";
|
||||||
|
indent->append(4, ' ');
|
||||||
|
auto const& arr = jv.get_array();
|
||||||
|
if(! arr.empty())
|
||||||
|
{
|
||||||
|
auto it = arr.begin();
|
||||||
|
for(;;)
|
||||||
|
{
|
||||||
|
os << *indent;
|
||||||
|
pretty_print( os, *it, indent);
|
||||||
|
if(++it == arr.end())
|
||||||
|
break;
|
||||||
|
os << ",\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << "\n";
|
||||||
|
indent->resize(indent->size() - 4);
|
||||||
|
os << *indent << "]";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case boost::json::kind::string:
|
||||||
|
{
|
||||||
|
os << boost::json::serialize(jv.get_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case boost::json::kind::uint64:
|
||||||
|
case boost::json::kind::int64:
|
||||||
|
case boost::json::kind::double_:
|
||||||
|
os << jv;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case boost::json::kind::bool_:
|
||||||
|
if(jv.get_bool())
|
||||||
|
os << "true";
|
||||||
|
else
|
||||||
|
os << "false";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case boost::json::kind::null:
|
||||||
|
os << "null";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(indent->empty())
|
||||||
|
os << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static String json_pretty_print(boost::json::value const& jv, std::string* indent = nullptr) {
|
||||||
|
std::ostringstream stream;
|
||||||
|
pretty_print(stream, jv, indent);
|
||||||
|
String string = wxString(stream.str().c_str());
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static String json_ugly_print(boost::json::value const& jv) {
|
||||||
|
String string = wxString(boost::json::serialize(jv).c_str());
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------- : JSON to MSE
|
||||||
|
|
||||||
|
inline static ScriptValueP json_to_mse(boost::json::value& jv, Set* set);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static void read(T& out, boost::json::object& jv, const char value_name[]) {
|
||||||
|
if (!jv.contains(value_name)) return;
|
||||||
|
else {
|
||||||
|
wxStringInputStream stream = {_("")};
|
||||||
|
Reader reader(stream, nullptr, _(""));
|
||||||
|
reader.setValue(wxString(jv[value_name].as_string().c_str()));
|
||||||
|
reader.handle(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// templates don't work with enums? are you kidding me with this language?
|
||||||
|
static void read(PackSelectType& out, boost::json::object& jv, const char value_name[]) {
|
||||||
|
if (!jv.contains(value_name)) return;
|
||||||
|
else {
|
||||||
|
wxStringInputStream stream = {_("")};
|
||||||
|
Reader reader(stream, nullptr, _(""));
|
||||||
|
reader.setValue(wxString(jv[value_name].as_string().c_str()));
|
||||||
|
reader.handle(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static PackItemP json_to_mse_pack_item(boost::json::object& jv) {
|
||||||
|
PackItemP pack_item = make_intrusive<PackItem>();
|
||||||
|
read(pack_item->name, jv, "name");
|
||||||
|
read(pack_item->amount, jv, "amount");
|
||||||
|
read(pack_item->weight, jv, "weight");
|
||||||
|
return pack_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static PackTypeP json_to_mse_pack_type(boost::json::object& jv) {
|
||||||
|
PackTypeP pack_type = make_intrusive<PackType>();
|
||||||
|
read(pack_type->name, jv, "name");
|
||||||
|
read(pack_type->enabled, jv, "enabled");
|
||||||
|
read(pack_type->selectable, jv, "selectable");
|
||||||
|
read(pack_type->summary, jv, "summary");
|
||||||
|
read(pack_type->select, jv, "select");
|
||||||
|
if (jv.contains("items") && jv["items"].is_array()) {
|
||||||
|
boost::json::array pack_itemsv = jv["items"].as_array();
|
||||||
|
for (int i = 0; i < pack_itemsv.size(); i++) {
|
||||||
|
boost::json::object pack_itemv = pack_itemsv[i].as_object();
|
||||||
|
pack_type->items.emplace_back(json_to_mse_pack_item(pack_itemv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pack_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static KeywordP json_to_mse_keyword(boost::json::object& jv) {
|
||||||
|
KeywordP keyword = make_intrusive<Keyword>();
|
||||||
|
read(keyword->keyword, jv, "keyword");
|
||||||
|
read(keyword->match, jv, "match");
|
||||||
|
read(keyword->reminder, jv, "reminder");
|
||||||
|
read(keyword->rules, jv, "rules");
|
||||||
|
read(keyword->mode, jv, "mode");
|
||||||
|
return keyword;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static CardP json_to_mse_card(boost::json::object& jv, Set* set) {
|
||||||
|
CardP card = make_intrusive<Card>(*set->game);
|
||||||
|
read(card->time_created, jv, "time_created");
|
||||||
|
read(card->time_modified, jv, "time_modified");
|
||||||
|
read(card->notes, jv, "notes");
|
||||||
|
//read(card->uid, jv, "uid");
|
||||||
|
//read(card->linked_card_1, jv, "linked_card_1");
|
||||||
|
//read(card->linked_card_2, jv, "linked_card_2");
|
||||||
|
//read(card->linked_card_3, jv, "linked_card_3");
|
||||||
|
//read(card->linked_card_4, jv, "linked_card_4");
|
||||||
|
//read(card->linked_relation_1, jv, "linked_relation_1");
|
||||||
|
//read(card->linked_relation_2, jv, "linked_relation_2");
|
||||||
|
//read(card->linked_relation_3, jv, "linked_relation_3");
|
||||||
|
//read(card->linked_relation_4, jv, "linked_relation_4");
|
||||||
|
// card fields
|
||||||
|
if (jv.contains("data") && jv["data"].is_object()) {
|
||||||
|
boost::json::object datav = jv["data"].as_object();
|
||||||
|
for (auto it = datav.begin(); it != datav.end(); ++it) {
|
||||||
|
String key_name = wxString(it->key_c_str());
|
||||||
|
Value* container = get_card_field_container(*set->game, card->data, key_name, false);
|
||||||
|
ScriptValueP value = json_to_mse(it->value(), set);
|
||||||
|
set_container(container, value, key_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stylesheet
|
||||||
|
if (jv.contains("stylesheet")) card->stylesheet = StyleSheet::byGameAndName(*set->game, wxString(jv["stylesheet"].as_string().c_str()));
|
||||||
|
if (card->stylesheet) {
|
||||||
|
// styling fields
|
||||||
|
card->styling_data.init(card->stylesheet->styling_fields);
|
||||||
|
if (jv.contains("styling_data") && jv["styling_data"].is_object()) {
|
||||||
|
boost::json::object datav = jv["styling_data"].as_object();
|
||||||
|
for (auto it = datav.begin(); it != datav.end(); ++it) {
|
||||||
|
String key_name = wxString(it->key_c_str());
|
||||||
|
Value* container = get_container(card->styling_data, wxString("styling"), key_name, false);
|
||||||
|
ScriptValueP value = json_to_mse(it->value(), set);
|
||||||
|
set_container(container, value, key_name);
|
||||||
|
card->has_styling = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// extra card fields
|
||||||
|
if (jv.contains("extra_data") && jv["extra_data"].is_object()) {
|
||||||
|
boost::json::object datav = jv["extra_data"].as_object();
|
||||||
|
for (auto it = datav.begin(); it != datav.end(); ++it) {
|
||||||
|
StyleSheetP& stylesheet = StyleSheet::byGameAndName(*set->game, it->key_c_str());
|
||||||
|
if (!stylesheet) continue;
|
||||||
|
IndexMap<FieldP, ValueP>& stylesheet_data = card->extraDataFor(*stylesheet);
|
||||||
|
boost::json::object stylesheet_datav = it->value().as_object();
|
||||||
|
for (auto stylesheet_it = stylesheet_datav.begin(); stylesheet_it != stylesheet_datav.end(); ++stylesheet_it) {
|
||||||
|
String key_name = wxString(stylesheet_it->key_c_str());
|
||||||
|
Value* container = get_container(stylesheet_data, wxString("extra card"), key_name, false);
|
||||||
|
ScriptValueP value = json_to_mse(stylesheet_it->value(), set);
|
||||||
|
set_container(container, value, key_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static SetP json_to_mse_set(boost::json::object& jv) {
|
||||||
|
if (!jv.contains("game")) {
|
||||||
|
throw ScriptError(_ERROR_("json set without game"));
|
||||||
|
}
|
||||||
|
if (!jv.contains("stylesheet")) {
|
||||||
|
throw ScriptError(_ERROR_("json set without stylesheet"));
|
||||||
|
}
|
||||||
|
GameP& game = Game::byName(wxString(jv["game"].as_string().c_str()));
|
||||||
|
StyleSheetP& stylesheet = StyleSheet::byGameAndName(*game, wxString(jv["stylesheet"].as_string().c_str()));
|
||||||
|
SetP& set = make_intrusive<Set>(stylesheet);
|
||||||
|
// set fields
|
||||||
|
if (jv.contains("set_info") && jv["set_info"].is_object()) {
|
||||||
|
boost::json::object datav = jv["set_info"].as_object();
|
||||||
|
for (auto it = datav.begin(); it != datav.end(); ++it) {
|
||||||
|
String key_name = wxString(it->key_c_str());
|
||||||
|
Value* container = get_container(set->data, wxString("set"), key_name, false);
|
||||||
|
ScriptValueP value = json_to_mse(it->value(), set.get());
|
||||||
|
set_container(container, value, key_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// styling
|
||||||
|
if (jv.contains("styling") && jv["styling"].is_object()) {
|
||||||
|
boost::json::object datav = jv["styling"].as_object();
|
||||||
|
for (auto it = datav.begin(); it != datav.end(); ++it) {
|
||||||
|
StyleSheetP& stylesheet = StyleSheet::byGameAndName(*set->game, it->key_c_str());
|
||||||
|
if (!stylesheet) continue;
|
||||||
|
IndexMap<FieldP, ValueP>& stylesheet_data = set->stylingDataFor(*stylesheet);
|
||||||
|
boost::json::object stylesheet_datav = it->value().as_object();
|
||||||
|
for (auto stylesheet_it = stylesheet_datav.begin(); stylesheet_it != stylesheet_datav.end(); ++stylesheet_it) {
|
||||||
|
String key_name = wxString(stylesheet_it->key_c_str());
|
||||||
|
Value* container = get_container(stylesheet_data, wxString("styling"), key_name, false);
|
||||||
|
ScriptValueP value = json_to_mse(stylesheet_it->value(), set.get());
|
||||||
|
set_container(container, value, key_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cards
|
||||||
|
if (jv.contains("cards") && jv["cards"].is_array()) {
|
||||||
|
boost::json::array cardsv = jv["cards"].as_array();
|
||||||
|
for (int i = 0; i < cardsv.size(); i++) {
|
||||||
|
boost::json::object cardv = cardsv[i].as_object();
|
||||||
|
set->cards.emplace_back(json_to_mse_card(cardv, set.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// keywords
|
||||||
|
if (jv.contains("keywords") && jv["keywords"].is_array()) {
|
||||||
|
boost::json::array keywordsv = jv["keywords"].as_array();
|
||||||
|
for (int i = 0; i < keywordsv.size(); i++) {
|
||||||
|
boost::json::object keywordv = keywordsv[i].as_object();
|
||||||
|
set->keywords.emplace_back(json_to_mse_keyword(keywordv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// pack types
|
||||||
|
if (jv.contains("pack_types") && jv["pack_types"].is_array()) {
|
||||||
|
boost::json::array pack_typesv = jv["pack_types"].as_array();
|
||||||
|
for (int i = 0; i < pack_typesv.size(); i++) {
|
||||||
|
boost::json::object pack_typev = pack_typesv[i].as_object();
|
||||||
|
set->pack_types.emplace_back(json_to_mse_pack_type(pack_typev));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static ScriptValueP json_to_mse(boost::json::value& jv, Set* set) {
|
||||||
|
if (jv == nullptr) return script_nil;
|
||||||
|
else if (jv.is_null()) return script_nil;
|
||||||
|
else if (jv.is_bool()) return to_script(jv.get_bool());
|
||||||
|
else if (jv.is_double()) return to_script(jv.get_double());
|
||||||
|
else if (jv.is_int64()) {
|
||||||
|
int integer = jv.get_int64();
|
||||||
|
return to_script(integer);
|
||||||
|
}
|
||||||
|
else if (jv.is_uint64()) {
|
||||||
|
int integer = jv.get_uint64();
|
||||||
|
return to_script(integer);
|
||||||
|
}
|
||||||
|
else if (jv.is_string()) {
|
||||||
|
std::string stdstring = boost::json::value_to<std::string>(jv);
|
||||||
|
String wxstring(stdstring.c_str(), wxConvUTF8);
|
||||||
|
return to_script(wxstring);
|
||||||
|
}
|
||||||
|
else if (jv.is_array()) {
|
||||||
|
boost::json::array array = jv.get_array();
|
||||||
|
ScriptCustomCollectionP result = make_intrusive<ScriptCustomCollection>();
|
||||||
|
for (int i = 0; i < array.size(); ++i) {
|
||||||
|
boost::json::value jvalue = array[i];
|
||||||
|
ScriptValueP value = json_to_mse(jvalue, set);
|
||||||
|
result->value.push_back(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else if (jv.is_object()) {
|
||||||
|
boost::json::object object = jv.get_object();
|
||||||
|
if (object.contains("mse_object_type")) {
|
||||||
|
boost::json::string mse_object_type = object["mse_object_type"].as_string();
|
||||||
|
if (mse_object_type == "set") return make_intrusive<ScriptObject<SetP>> (json_to_mse_set(object));
|
||||||
|
if (mse_object_type == "card") return make_intrusive<ScriptObject<CardP>> (json_to_mse_card(object, set));
|
||||||
|
if (mse_object_type == "keyword") return make_intrusive<ScriptObject<KeywordP>> (json_to_mse_keyword(object));
|
||||||
|
if (mse_object_type == "pack_type") return make_intrusive<ScriptObject<PackTypeP>>(json_to_mse_pack_type(object));
|
||||||
|
if (mse_object_type == "pack_item") return make_intrusive<ScriptObject<PackItemP>>(json_to_mse_pack_item(object));
|
||||||
|
queue_message(MESSAGE_ERROR, _ERROR_("json unknown type") + _("(") + wxString(mse_object_type.c_str()) + _(")"));
|
||||||
|
return script_nil;
|
||||||
|
}
|
||||||
|
ScriptCustomCollectionP result = make_intrusive<ScriptCustomCollection>();
|
||||||
|
for (auto it = object.begin(); it != object.end(); ++it) {
|
||||||
|
boost::json::string_view jview = it->key();
|
||||||
|
std::string_view stdview = std::string_view(jview.data(), jview.size());
|
||||||
|
std::string stdstring = { stdview.begin(), stdview.end() };
|
||||||
|
String key(stdstring.c_str(), wxConvUTF8);
|
||||||
|
boost::json::value jvalue = it->value();
|
||||||
|
ScriptValueP value = json_to_mse(jvalue, set);
|
||||||
|
result->key_value[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queue_message(MESSAGE_ERROR, _ERROR_("json unknown type"));
|
||||||
|
return script_nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline static ScriptValueP json_to_mse(String& string, Set* set) {
|
||||||
|
try {
|
||||||
|
boost::system::error_code ec;
|
||||||
|
boost::json::parse_options options;
|
||||||
|
options.allow_invalid_utf8 = true;
|
||||||
|
boost::json::value jv = boost::json::parse(string.ToStdString(), ec, {}, options);
|
||||||
|
if(ec) queue_message(MESSAGE_ERROR, _ERROR_("json cant parse") + _("\n\n") + ec.message());
|
||||||
|
return json_to_mse(jv, set);
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
queue_message(MESSAGE_ERROR, _ERROR_("json cant parse"));
|
||||||
|
return script_nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline static ScriptValueP json_to_mse(ScriptValueP& sv, Set* set) {
|
||||||
|
try {
|
||||||
|
String string = sv->toString();
|
||||||
|
return json_to_mse(string, set);
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
queue_message(MESSAGE_ERROR, _ERROR_("json cant convert"));
|
||||||
|
return script_nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------- : MSE to JSON
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static void write(boost::json::object& out, const String& name, T& value) {
|
||||||
|
wxStringOutputStream stream;
|
||||||
|
Writer writer(stream);
|
||||||
|
writer.indentation = -1000;
|
||||||
|
writer.handle(name, value);
|
||||||
|
String string = stream.GetString();
|
||||||
|
if (!string.empty()) {
|
||||||
|
if (string.StartsWith(name + ":")) string = string.substr(name.length() + 1).Trim(false);
|
||||||
|
if (string.EndsWith("\n")) string = string.substr(0, string.length() - 1);
|
||||||
|
out.emplace(name.ToStdString(), string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write(boost::json::object& out, const String& name, IndexMap<FieldP, ValueP>& map) {
|
||||||
|
boost::json::object indexmapv;
|
||||||
|
for (IndexMap<FieldP, ValueP>::iterator it = map.begin(); it != map.end(); ++it) {
|
||||||
|
write(indexmapv, (*it)->fieldP->name, *it);
|
||||||
|
}
|
||||||
|
if (!indexmapv.empty()) out.emplace(name.ToStdString(), indexmapv);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write(boost::json::object& out, const String& name, DelayedIndexMaps<FieldP,ValueP>& map) {
|
||||||
|
boost::json::object delayedindexmapv;
|
||||||
|
for (auto it = map.data.begin() ; it != map.data.end() ; ++it) {
|
||||||
|
write(delayedindexmapv, it->first, it->second->read_data);
|
||||||
|
}
|
||||||
|
if (!delayedindexmapv.empty()) out.emplace(name.ToStdString(), delayedindexmapv);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static boost::json::object mse_to_json(PackItemP& item) {
|
||||||
|
boost::json::object itemv;
|
||||||
|
itemv.emplace("mse_object_type", "pack_item");
|
||||||
|
write(itemv, "name", item->name);
|
||||||
|
write(itemv, "amount", item->amount);
|
||||||
|
write(itemv, "weight", item->weight);
|
||||||
|
return itemv;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static boost::json::object mse_to_json(PackTypeP& pack) {
|
||||||
|
boost::json::object packv;
|
||||||
|
packv.emplace("mse_object_type", "pack_type");
|
||||||
|
write(packv, "name", pack->name);
|
||||||
|
write(packv, "enabled", pack->enabled);
|
||||||
|
write(packv, "selectable", pack->selectable);
|
||||||
|
write(packv, "summary", pack->summary);
|
||||||
|
write(packv, "select", pack->select);
|
||||||
|
write(packv, "filter", pack->filter);
|
||||||
|
boost::json::array itemsv;
|
||||||
|
for (auto item : pack->items) {
|
||||||
|
itemsv.emplace_back(mse_to_json(item));
|
||||||
|
}
|
||||||
|
packv.emplace("items", itemsv);
|
||||||
|
return packv;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static boost::json::object mse_to_json(KeywordP& keyword) {
|
||||||
|
boost::json::object keywordv;
|
||||||
|
keywordv.emplace("mse_object_type", "keyword");
|
||||||
|
write(keywordv, "keyword", keyword->keyword);
|
||||||
|
write(keywordv, "match", keyword->match);
|
||||||
|
write(keywordv, "reminder", keyword->reminder);
|
||||||
|
write(keywordv, "rules", keyword->rules);
|
||||||
|
write(keywordv, "mode", keyword->mode);
|
||||||
|
return keywordv;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static boost::json::object mse_to_json(CardP& card, Set* set) {
|
||||||
|
boost::json::object cardv;
|
||||||
|
cardv.emplace("mse_object_type", "card");
|
||||||
|
// built-in values
|
||||||
|
write(cardv, "time_created", card->time_created);
|
||||||
|
write(cardv, "time_modified", card->time_modified);
|
||||||
|
write(cardv, "notes", card->notes);
|
||||||
|
//write(cardv, "uid", card->uid);
|
||||||
|
//write(cardv, "linked_card_1", card->linked_card_1);
|
||||||
|
//write(cardv, "linked_card_2", card->linked_card_2);
|
||||||
|
//write(cardv, "linked_card_3", card->linked_card_3);
|
||||||
|
//write(cardv, "linked_card_4", card->linked_card_4);
|
||||||
|
//write(cardv, "linked_relation_1", card->linked_relation_1);
|
||||||
|
//write(cardv, "linked_relation_2", card->linked_relation_2);
|
||||||
|
//write(cardv, "linked_relation_3", card->linked_relation_3);
|
||||||
|
//write(cardv, "linked_relation_4", card->linked_relation_4);
|
||||||
|
// card fields
|
||||||
|
write(cardv, "data", card->data);
|
||||||
|
// stylesheet
|
||||||
|
bool change_stylesheet = set && !card->stylesheet;
|
||||||
|
if (change_stylesheet) {
|
||||||
|
card->stylesheet = set->stylesheet;
|
||||||
|
}
|
||||||
|
if (card->stylesheet) {
|
||||||
|
write(cardv, "stylesheet", card->stylesheet);
|
||||||
|
write(cardv, "stylesheet_version", card->stylesheet->version);
|
||||||
|
// extra card fields
|
||||||
|
write(cardv, "extra_data", card->extra_data);
|
||||||
|
}
|
||||||
|
// style
|
||||||
|
write(cardv, "has_styling", card->has_styling);
|
||||||
|
if (card->has_styling) {
|
||||||
|
write(cardv, "styling_data", card->styling_data);
|
||||||
|
}
|
||||||
|
// restore stylesheet
|
||||||
|
if (change_stylesheet) {
|
||||||
|
card->stylesheet = StyleSheetP();
|
||||||
|
}
|
||||||
|
// done
|
||||||
|
return cardv;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static boost::json::object mse_to_json(Set* set) {
|
||||||
|
boost::json::object setv;
|
||||||
|
setv.emplace("mse_object_type", "set");
|
||||||
|
// built-in values
|
||||||
|
write(setv, "mse_version", set->fileVersion());
|
||||||
|
write(setv, "game", set->game);
|
||||||
|
write(setv, "game_version", set->game->version);
|
||||||
|
write(setv, "stylesheet", set->stylesheet);
|
||||||
|
write(setv, "stylesheet_version", set->stylesheet->version);
|
||||||
|
// set fields
|
||||||
|
write(setv, "set_info", set->data);
|
||||||
|
// styling
|
||||||
|
write(setv, "styling", set->styling_data);
|
||||||
|
// cards
|
||||||
|
boost::json::array cardsv;
|
||||||
|
for (auto card : set->cards) {
|
||||||
|
cardsv.emplace_back(mse_to_json(card, set));
|
||||||
|
}
|
||||||
|
setv.emplace("cards", cardsv);
|
||||||
|
// keywords
|
||||||
|
boost::json::array keywordsv;
|
||||||
|
for (auto keyword : set->keywords) {
|
||||||
|
keywordsv.emplace_back(mse_to_json(keyword));
|
||||||
|
}
|
||||||
|
if (!keywordsv.empty()) setv.emplace("keywords", keywordsv);
|
||||||
|
// pack types
|
||||||
|
boost::json::array pack_typesv;
|
||||||
|
for (auto pack_type : set->pack_types) {
|
||||||
|
pack_typesv.emplace_back(mse_to_json(pack_type));
|
||||||
|
}
|
||||||
|
if (!pack_typesv.empty()) setv.emplace("pack_types", pack_typesv);
|
||||||
|
// done
|
||||||
|
return setv;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static boost::json::value mse_to_json(ScriptValueP& sv, Set* set) {
|
||||||
|
ScriptType type = sv->type();
|
||||||
|
// special types
|
||||||
|
if (ScriptObject<PackItemP>* i = dynamic_cast<ScriptObject<PackItemP>*>(sv.get())) return mse_to_json(i->getValue());
|
||||||
|
if (ScriptObject<PackTypeP>* t = dynamic_cast<ScriptObject<PackTypeP>*>(sv.get())) return mse_to_json(t->getValue());
|
||||||
|
if (ScriptObject<KeywordP>* k = dynamic_cast<ScriptObject<KeywordP>*> (sv.get())) return mse_to_json(k->getValue());
|
||||||
|
if (ScriptObject<CardP>* c = dynamic_cast<ScriptObject<CardP>*> (sv.get())) return mse_to_json(c->getValue(), set);
|
||||||
|
if (ScriptObject<SetP>* z = dynamic_cast<ScriptObject<SetP>*> (sv.get())) return mse_to_json(z->getValue().get());
|
||||||
|
if (ScriptObject<Set*>* s = dynamic_cast<ScriptObject<Set*>*> (sv.get())) return mse_to_json(s->getValue());
|
||||||
|
|
||||||
|
// primitive types
|
||||||
|
if (type == SCRIPT_NIL) return boost::json::value(nullptr);
|
||||||
|
if (type == SCRIPT_INT) return boost::json::value(sv->toInt());
|
||||||
|
if (type == SCRIPT_DOUBLE) return boost::json::value(sv->toDouble());
|
||||||
|
if (type == SCRIPT_BOOL) return boost::json::value(sv->toBool());
|
||||||
|
if (type == SCRIPT_STRING) return boost::json::value(sv->toString());
|
||||||
|
if (type == SCRIPT_REGEX) return boost::json::value(sv->toString());
|
||||||
|
if (type == SCRIPT_COLOR) return boost::json::value(format_color(sv->toColor()));
|
||||||
|
if (type == SCRIPT_DATETIME) return boost::json::value(sv->toDateTime().FormatISOCombined(' '));
|
||||||
|
if (type == SCRIPT_COLLECTION) {
|
||||||
|
ScriptCustomCollection* custom = dynamic_cast<ScriptCustomCollection*>(sv.get());
|
||||||
|
if (custom) {
|
||||||
|
if (custom->value.size() > 0) {
|
||||||
|
boost::json::array array;
|
||||||
|
for (int i = 0; i < custom->value.size(); i++) {
|
||||||
|
array.emplace_back(mse_to_json(custom->value[i], set));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
} else if (custom->key_value.size() > 0) {
|
||||||
|
boost::json::object object;
|
||||||
|
map<String, ScriptValueP>::iterator it;
|
||||||
|
for (it = custom->key_value.begin(); it != custom->key_value.end(); it++) {
|
||||||
|
object.emplace(it->first.ToStdString(), mse_to_json(it->second, set));
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ScriptConcatCollection* concat = dynamic_cast<ScriptConcatCollection*>(sv.get());
|
||||||
|
if (concat) {
|
||||||
|
boost::json::value a = mse_to_json(concat->getA(), set);
|
||||||
|
boost::json::value b = mse_to_json(concat->getB(), set);
|
||||||
|
if (a.is_array() && b.is_array()) {
|
||||||
|
boost::json::array array_a = a.get_array();
|
||||||
|
boost::json::array array_b = b.get_array();
|
||||||
|
for (int i = 0; i < array_b.size(); i++) {
|
||||||
|
array_a.emplace_back(array_b[i]);
|
||||||
|
}
|
||||||
|
return array_a;
|
||||||
|
} else if (a.is_object() && b.is_object()) {
|
||||||
|
boost::json::object object_a = a.get_object();
|
||||||
|
boost::json::object object_b = b.get_object();
|
||||||
|
for (auto it = object_b.begin(); it != object_b.end(); ++it) {
|
||||||
|
object_a.emplace(it->key(), it->value());
|
||||||
|
}
|
||||||
|
return object_a;
|
||||||
|
} else {
|
||||||
|
queue_message(MESSAGE_ERROR, _ERROR_("json cant concat"));
|
||||||
|
return boost::json::value(nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue_message(MESSAGE_ERROR, _ERROR_1_("json unknown script type", sv->typeName()));
|
||||||
|
return boost::json::value(nullptr);
|
||||||
|
}
|
||||||
@@ -223,7 +223,10 @@ public:
|
|||||||
inline ScriptConcatCollection(ScriptValueP a, ScriptValueP b) : a(a), b(b) {}
|
inline ScriptConcatCollection(ScriptValueP a, ScriptValueP b) : a(a), b(b) {}
|
||||||
ScriptValueP getMember(const String& name) const override;
|
ScriptValueP getMember(const String& name) const override;
|
||||||
ScriptValueP getIndex(int index) const override;
|
ScriptValueP getIndex(int index) const override;
|
||||||
ScriptValueP makeIterator() const override;
|
ScriptValueP makeIterator() const override;
|
||||||
|
ScriptValueP getA() { return a; }
|
||||||
|
ScriptValueP getB() { return b; }
|
||||||
|
|
||||||
int itemCount() const override { return a->itemCount() + b->itemCount(); }
|
int itemCount() const override { return a->itemCount() + b->itemCount(); }
|
||||||
/// Collections can be compared by comparing pointers
|
/// Collections can be compared by comparing pointers
|
||||||
CompareWhat compareAs(String&, void const*& compare_ptr) const override {
|
CompareWhat compareAs(String&, void const*& compare_ptr) const override {
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ public:
|
|||||||
IndexMap<Key,Value>& get(const String& name, const vector<Key>& init_with);
|
IndexMap<Key,Value>& get(const String& name, const vector<Key>& init_with);
|
||||||
/// Clear the delayed index map
|
/// Clear the delayed index map
|
||||||
void clear();
|
void clear();
|
||||||
private:
|
|
||||||
map<String, intrusive_ptr<DelayedIndexMapsData<Key,Value>>> data;
|
map<String, intrusive_ptr<DelayedIndexMapsData<Key,Value>>> data;
|
||||||
|
private:
|
||||||
friend class Reader;
|
friend class Reader;
|
||||||
friend class Writer;
|
friend class Writer;
|
||||||
friend class GetDefaultMember;
|
friend class GetDefaultMember;
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ public:
|
|||||||
inline Packaged* getPackage() const { return package; }
|
inline Packaged* getPackage() const { return package; }
|
||||||
|
|
||||||
String addLocale(String);
|
String addLocale(String);
|
||||||
|
|
||||||
|
/// Set the value that will be returned by the next getValue() call (may mess up the state of the reader)
|
||||||
|
inline void setValue(const String& value) { state = UNHANDLED; previous_value = value; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// --------------------------------------------------- : Data
|
// --------------------------------------------------- : Data
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
using boost::tribool;
|
using boost::tribool;
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------- : Writer
|
// ----------------------------------------------------------------------------- : Writer
|
||||||
|
|
||||||
|
Writer::Writer(OutputStream& output)
|
||||||
|
: indentation(0)
|
||||||
|
, output(output)
|
||||||
|
, stream(output, wxEOL_UNIX, wxMBConvUTF8())
|
||||||
|
{}
|
||||||
|
|
||||||
Writer::Writer(OutputStream& output, Version file_app_version)
|
Writer::Writer(OutputStream& output, Version file_app_version)
|
||||||
: indentation(0)
|
: indentation(0)
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ DECLARE_POINTER_TYPE(StyleSheet);
|
|||||||
class Writer {
|
class Writer {
|
||||||
public:
|
public:
|
||||||
/// Construct a writer that writes to the given output stream
|
/// Construct a writer that writes to the given output stream
|
||||||
|
Writer(OutputStream& output);
|
||||||
Writer(OutputStream& output, Version file_app_version);
|
Writer(OutputStream& output, Version file_app_version);
|
||||||
|
|
||||||
/// Tell the reflection code we are not reading
|
/// Tell the reflection code we are not reading
|
||||||
static constexpr bool isReading = false;
|
static constexpr bool isReading = false;
|
||||||
static constexpr bool isWriting = true;
|
static constexpr bool isWriting = true;
|
||||||
@@ -72,11 +73,11 @@ public:
|
|||||||
// special behaviour
|
// special behaviour
|
||||||
void handle(const GameP&);
|
void handle(const GameP&);
|
||||||
void handle(const StyleSheetP&);
|
void handle(const StyleSheetP&);
|
||||||
|
|
||||||
private:
|
|
||||||
// --------------------------------------------------- : Data
|
|
||||||
/// Indentation of the current block
|
/// Indentation of the current block
|
||||||
int indentation;
|
int indentation;
|
||||||
|
private:
|
||||||
|
// --------------------------------------------------- : Data
|
||||||
/// Blocks opened to which nothing has been written
|
/// Blocks opened to which nothing has been written
|
||||||
vector<const Char*> pending_opened;
|
vector<const Char*> pending_opened;
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ String tr(const String&, const String& subcat, const String& key, DefaultLocaleF
|
|||||||
#define _TITLE_(s) tr(LOCALE_CAT_TITLE, _(s))
|
#define _TITLE_(s) tr(LOCALE_CAT_TITLE, _(s))
|
||||||
/// A localized string for type names in scripts
|
/// A localized string for type names in scripts
|
||||||
#define _TYPE_(s) tr(LOCALE_CAT_TYPE, _(s))
|
#define _TYPE_(s) tr(LOCALE_CAT_TYPE, _(s))
|
||||||
|
#define _TYPE_V_(s) tr(LOCALE_CAT_TYPE, s )
|
||||||
/// A localized string for action names
|
/// A localized string for action names
|
||||||
#define _ACTION_(s) tr(LOCALE_CAT_ACTION, _(s))
|
#define _ACTION_(s) tr(LOCALE_CAT_ACTION, _(s))
|
||||||
/// A localized string for error messages
|
/// A localized string for error messages
|
||||||
|
|||||||
Reference in New Issue
Block a user