Revert 2006-06-17T03:07:01Z!rusty@rustcorp.com.au and 2006-06-18T05:27:27Z!rusty@rustcorp.com.au
These two were the start of infrastructure to speed up saving, but incomplete. They will not be complete for 1.2, so revert them.
This commit is contained in:
parent
bc8a6f9643
commit
efefdb54ae
11 changed files with 70 additions and 325 deletions
|
@ -23,7 +23,6 @@
|
|||
#include "preferences.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "util.hpp"
|
||||
#include "wesconfig.h"
|
||||
#include "serialization/binary_or_text.hpp"
|
||||
#include "serialization/binary_wml.hpp"
|
||||
#include "serialization/parser.hpp"
|
||||
|
@ -360,34 +359,6 @@ void write_player(const player_info& player, config& cfg)
|
|||
cfg["can_recruit"] = can_recruit_str;
|
||||
}
|
||||
|
||||
void write_player(config_writer &out, const player_info& player)
|
||||
{
|
||||
char buf[50];
|
||||
snprintf(buf,sizeof(buf),"%d",player.gold);
|
||||
|
||||
out.write_key_val("gold", buf);
|
||||
|
||||
for(std::vector<unit>::const_iterator i = player.available_units.begin();
|
||||
i != player.available_units.end(); ++i) {
|
||||
config new_cfg;
|
||||
i->write(new_cfg);
|
||||
out.write_child("unit",new_cfg);
|
||||
}
|
||||
|
||||
std::stringstream can_recruit;
|
||||
std::copy(player.can_recruit.begin(),player.can_recruit.end(),std::ostream_iterator<std::string>(can_recruit,","));
|
||||
std::string can_recruit_str = can_recruit.str();
|
||||
|
||||
//remove the trailing comma
|
||||
if(can_recruit_str.empty() == false) {
|
||||
can_recruit_str.resize(can_recruit_str.size()-1);
|
||||
}
|
||||
|
||||
out.write_key_val("can_recruit", can_recruit_str);
|
||||
}
|
||||
|
||||
|
||||
// Deprecated, use other write_game below.
|
||||
void write_game(const game_state& game, config& cfg/*, WRITE_GAME_MODE mode*/)
|
||||
{
|
||||
log_scope("write_game");
|
||||
|
@ -425,38 +396,6 @@ void write_game(const game_state& game, config& cfg/*, WRITE_GAME_MODE mode*/)
|
|||
}
|
||||
//}
|
||||
|
||||
void write_game(config_writer &out, const game_state& game)
|
||||
{
|
||||
log_scope("write_game");
|
||||
|
||||
out.write_key_val("label", game.label);
|
||||
out.write_key_val("version", game_config::version);
|
||||
out.write_key_val("scenario", game.scenario);
|
||||
out.write_key_val("campaign", game.campaign);
|
||||
out.write_key_val("campaign_type", game.campaign_type);
|
||||
out.write_key_val("difficulty", game.difficulty);
|
||||
out.write_key_val("campaign_define", game.campaign_define);
|
||||
out.write_child("variables", game.variables);
|
||||
|
||||
for(std::map<std::string, player_info>::const_iterator i=game.players.begin();
|
||||
i!=game.players.end(); ++i) {
|
||||
out.open_child("player");
|
||||
out.write_key_val("save_id", i->first);
|
||||
write_player(out, i->second);
|
||||
out.close_child("player");
|
||||
}
|
||||
|
||||
if(game.replay_data.child("replay") == NULL) {
|
||||
out.write_child("replay", game.replay_data);
|
||||
}
|
||||
|
||||
out.write_child("snapshot",game.snapshot);
|
||||
out.write_child("replay_start",game.starting_pos);
|
||||
out.open_child("statistics");
|
||||
statistics::write_stats(out);
|
||||
out.close_child("statistics");
|
||||
}
|
||||
|
||||
//a structure for comparing to save_info objects based on their modified time.
|
||||
//if the times are equal, will order based on the name
|
||||
struct save_info_less_time {
|
||||
|
@ -533,48 +472,38 @@ void load_game(const game_data& data, const std::string& name, game_state& state
|
|||
}
|
||||
|
||||
//throws game::save_game_failed
|
||||
scoped_ostream open_save_game(const std::string &label)
|
||||
void save_game(const game_state& state)
|
||||
{
|
||||
std::string name = label;
|
||||
log_scope("save_game");
|
||||
|
||||
std::string name = state.label;
|
||||
std::replace(name.begin(),name.end(),' ','_');
|
||||
|
||||
config cfg;
|
||||
try {
|
||||
return scoped_ostream(ostream_file(get_saves_dir() + "/" + name));
|
||||
} catch(io_exception& e) {
|
||||
throw game::save_game_failed(e.what());
|
||||
}
|
||||
}
|
||||
write_game(state,cfg);
|
||||
|
||||
void finish_save_game(config_writer &out, const game_state& state, const std::string &label)
|
||||
{
|
||||
std::string name = label;
|
||||
std::replace(name.begin(),name.end(),' ','_');
|
||||
std::string fname(get_saves_dir() + "/" + name);
|
||||
|
||||
try {
|
||||
if(!out.good()) {
|
||||
throw game::save_game_failed(_("Could not write to file"));
|
||||
const std::string fname = get_saves_dir() + "/" + name;
|
||||
{
|
||||
scoped_ostream savefile = ostream_file(fname);
|
||||
write_possibly_compressed(*savefile, cfg, preferences::compress_saves());
|
||||
if(savefile->good() == false) {
|
||||
throw game::save_game_failed(_("Could not write to file"));
|
||||
}
|
||||
}
|
||||
|
||||
config& summary = save_summary(label);
|
||||
config& summary = save_summary(state.label);
|
||||
extract_summary_data_from_save(state,summary);
|
||||
const int mod_time = static_cast<int>(file_create_time(fname));
|
||||
summary["mod_time"] = str_cast(mod_time);
|
||||
|
||||
write_save_index();
|
||||
|
||||
} catch(io_exception& e) {
|
||||
throw game::save_game_failed(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
//throws game::save_game_failed
|
||||
void save_game(const game_state& state)
|
||||
{
|
||||
scoped_ostream os(open_save_game(state.label));
|
||||
config_writer out(*os, preferences::compress_saves(), PACKAGE);
|
||||
write_game(out, state);
|
||||
finish_save_game(out, state, state.label);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool save_index_loaded = false;
|
||||
config save_index_cfg;
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#ifndef GAME_STATUS_HPP_INCLUDED
|
||||
#define GAME_STATUS_HPP_INCLUDED
|
||||
|
||||
#include "filesystem.hpp"
|
||||
#include "unit.hpp"
|
||||
|
||||
#include <time.h>
|
||||
|
@ -164,15 +163,10 @@ void read_save_file(const std::string& name, config& cfg, std::string* error_log
|
|||
|
||||
game_state read_game(const game_data& data, const config* cfg);
|
||||
void write_game(const game_state& game, config& cfg/*, WRITE_GAME_MODE mode=WRITE_FULL_GAME*/);
|
||||
void write_game(config_writer &out, const game_state& game);
|
||||
|
||||
// function returns true iff there is already savegame with that name
|
||||
bool save_game_exists(const std::string & name);
|
||||
|
||||
//throws game::save_game_failed
|
||||
scoped_ostream open_save_game(const std::string &label);
|
||||
void finish_save_game(config_writer &out, const game_state& state, const std::string &label);
|
||||
|
||||
//functions to load/save games.
|
||||
void load_game(const game_data& data, const std::string& name, game_state& state, std::string* error_log);
|
||||
//throws gamestatus::save_game_failed
|
||||
|
|
|
@ -30,8 +30,6 @@
|
|||
#include "unit_display.hpp"
|
||||
#include "util.hpp"
|
||||
#include "wassert.hpp"
|
||||
#include "wesconfig.h"
|
||||
#include "serialization/binary_or_text.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
@ -224,10 +222,7 @@ void replay::save_game(const std::string& label, const config& snapshot,
|
|||
|
||||
saveInfo_.label = label;
|
||||
|
||||
scoped_ostream os(open_save_game(label));
|
||||
config_writer out(*os, preferences::compress_saves(), PACKAGE);
|
||||
::write_game(out, saveInfo_);
|
||||
finish_save_game(out, saveInfo_, saveInfo_.label);
|
||||
::save_game(saveInfo_);
|
||||
|
||||
saveInfo_.replay_data = config();
|
||||
saveInfo_.snapshot = config();
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
#include "unit.hpp"
|
||||
|
||||
class display;
|
||||
class config_writer;
|
||||
|
||||
struct verification_manager
|
||||
{
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
#include "global.hpp"
|
||||
|
||||
#include "binary_or_text.hpp"
|
||||
#include "config.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "serialization/binary_wml.hpp"
|
||||
|
@ -41,40 +40,3 @@ void write_possibly_compressed(std::ostream &out, config &cfg, bool compress)
|
|||
else
|
||||
write(out, cfg);
|
||||
}
|
||||
|
||||
config_writer::config_writer(std::ostream &out, bool compress, const std::string &textdomain)
|
||||
: out_(out), compress_(compress), level_(0), textdomain_(textdomain)
|
||||
{
|
||||
}
|
||||
|
||||
void config_writer::write(const config &cfg)
|
||||
{
|
||||
::write(out_, cfg, level_);
|
||||
}
|
||||
|
||||
void config_writer::write_child(const std::string &key, const config &cfg)
|
||||
{
|
||||
open_child(key);
|
||||
::write(out_, cfg, level_);
|
||||
close_child(key);
|
||||
}
|
||||
|
||||
void config_writer::write_key_val(const std::string &key, const std::string &value)
|
||||
{
|
||||
::write_key_val(out_, key, value, level_, textdomain_);
|
||||
}
|
||||
|
||||
void config_writer::open_child(const std::string &key)
|
||||
{
|
||||
::write_open_child(out_, key, level_++);
|
||||
}
|
||||
|
||||
void config_writer::close_child(const std::string &key)
|
||||
{
|
||||
::write_close_child(out_, key, --level_);
|
||||
}
|
||||
|
||||
bool config_writer::good() const
|
||||
{
|
||||
return out_.good();
|
||||
}
|
||||
|
|
|
@ -27,23 +27,4 @@ bool detect_format_and_read(config &cfg, std::istream &in, std::string* error_lo
|
|||
//function which writes a file, compressed or not depending on a flag
|
||||
void write_possibly_compressed(std::ostream &out, config &cfg, bool compress);
|
||||
|
||||
// Class for writing a config out to a file in pieces.
|
||||
class config_writer
|
||||
{
|
||||
public:
|
||||
config_writer(std::ostream &out, bool compress, const std::string &textdomain);
|
||||
|
||||
void write(const config &cfg);
|
||||
void write_child(const std::string &key, const config &cfg);
|
||||
void write_key_val(const std::string &key, const std::string &value);
|
||||
void open_child(const std::string &key);
|
||||
void close_child(const std::string &key);
|
||||
bool good() const;
|
||||
|
||||
private:
|
||||
std::ostream &out_;
|
||||
bool compress_;
|
||||
unsigned int level_;
|
||||
std::string textdomain_;
|
||||
};
|
||||
#endif
|
||||
|
|
|
@ -348,76 +348,58 @@ static std::string escaped_string(const std::string& value) {
|
|||
return std::string(res.begin(), res.end());
|
||||
}
|
||||
|
||||
void write_key_val(std::ostream &out, const std::string &key, const t_string &value, unsigned int level, std::string& textdomain)
|
||||
{
|
||||
bool first = true;
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(t_string::walker w(value); !w.eos(); w.next()) {
|
||||
std::string part(w.begin(), w.end());
|
||||
|
||||
if(w.translatable()) {
|
||||
if(w.textdomain() != textdomain) {
|
||||
out << TextdomainPrefix
|
||||
<< w.textdomain()
|
||||
<< TextdomainPostfix;
|
||||
textdomain = w.textdomain();
|
||||
}
|
||||
|
||||
if(first) {
|
||||
out << std::string(level, '\t')
|
||||
<< key
|
||||
<< AttributeEquals;
|
||||
}
|
||||
|
||||
out << TranslatableAttributePrefix
|
||||
<< escaped_string(part)
|
||||
<< AttributePostfix;
|
||||
|
||||
} else {
|
||||
if(first) {
|
||||
out << std::string(level, '\t')
|
||||
<< key
|
||||
<< AttributeEquals;
|
||||
}
|
||||
|
||||
out << AttributePrefix
|
||||
<< escaped_string(part)
|
||||
<< AttributePostfix;
|
||||
}
|
||||
|
||||
if(w.last()) {
|
||||
out << AttributeEndPostfix;
|
||||
} else {
|
||||
out << AttributeContPostfix;
|
||||
out << std::string(level+1, '\t');
|
||||
}
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
void write_open_child(std::ostream &out, const std::string &child, unsigned int level)
|
||||
{
|
||||
out << std::string(level, '\t')
|
||||
<< ElementPrefix << child << ElementPostfix;
|
||||
}
|
||||
|
||||
void write_close_child(std::ostream &out, const std::string &child, unsigned int level)
|
||||
{
|
||||
out << std::string(level, '\t')
|
||||
<< EndElementPrefix << child << EndElementPostfix;
|
||||
}
|
||||
|
||||
static void write_internal(config const &cfg, std::ostream &out, std::string& textdomain, size_t tab = 0)
|
||||
{
|
||||
if (tab > max_recursion_levels)
|
||||
throw config::error("Too many recursion levels in config write");
|
||||
|
||||
for(string_map::const_iterator i = cfg.values.begin(), i_end = cfg.values.end(); i != i_end; ++i) {
|
||||
write_key_val(out, i->first, i->second, tab, textdomain);
|
||||
if (!i->second.empty()) {
|
||||
bool first = true;
|
||||
|
||||
for(t_string::walker w(i->second); !w.eos(); w.next()) {
|
||||
std::string part(w.begin(), w.end());
|
||||
|
||||
if(w.translatable()) {
|
||||
if(w.textdomain() != textdomain) {
|
||||
out << TextdomainPrefix
|
||||
<< w.textdomain()
|
||||
<< TextdomainPostfix;
|
||||
textdomain = w.textdomain();
|
||||
}
|
||||
|
||||
if(first) {
|
||||
out << std::string(tab, '\t')
|
||||
<< i->first
|
||||
<< AttributeEquals;
|
||||
}
|
||||
|
||||
out << TranslatableAttributePrefix
|
||||
<< escaped_string(part)
|
||||
<< AttributePostfix;
|
||||
|
||||
} else {
|
||||
if(first) {
|
||||
out << std::string(tab, '\t')
|
||||
<< i->first
|
||||
<< AttributeEquals;
|
||||
}
|
||||
|
||||
out << AttributePrefix
|
||||
<< escaped_string(part)
|
||||
<< AttributePostfix;
|
||||
}
|
||||
|
||||
if(w.last()) {
|
||||
out << AttributeEndPostfix;
|
||||
} else {
|
||||
out << AttributeContPostfix;
|
||||
out << std::string(tab+1, '\t');
|
||||
}
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(config::all_children_iterator j = cfg.ordered_begin(), j_end = cfg.ordered_end(); j != j_end; ++j) {
|
||||
|
@ -425,15 +407,16 @@ static void write_internal(config const &cfg, std::ostream &out, std::string& te
|
|||
const std::string& name = *item.first;
|
||||
const config& cfg = *item.second;
|
||||
|
||||
write_open_child(out, name, tab);
|
||||
out << std::string(tab, '\t')
|
||||
<< ElementPrefix << name << ElementPostfix;
|
||||
write_internal(cfg, out, textdomain, tab + 1);
|
||||
write_close_child(out, name, tab);
|
||||
out << std::string(tab, '\t')
|
||||
<< EndElementPrefix << name << EndElementPostfix;
|
||||
}
|
||||
}
|
||||
|
||||
void write(std::ostream &out, config const &cfg, unsigned int level)
|
||||
void write(std::ostream &out, config const &cfg)
|
||||
{
|
||||
std::string textdomain = PACKAGE;
|
||||
write_internal(cfg, out, textdomain, level);
|
||||
write_internal(cfg, out, textdomain);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,14 +20,10 @@
|
|||
#include <vector>
|
||||
|
||||
class config;
|
||||
class t_string;
|
||||
|
||||
//read data in, clobbering existing data.
|
||||
void read(config &cfg, std::istream &in, std::string* error_log = NULL); //throws config::error
|
||||
|
||||
void write(std::ostream &out, config const &cfg, unsigned int level=0);
|
||||
void write_key_val(std::ostream &out, const std::string &key, const t_string &value, unsigned int level, std::string &textdomain);
|
||||
void write_open_child(std::ostream &out, const std::string &child, unsigned int level);
|
||||
void write_close_child(std::ostream &out, const std::string &child, unsigned int level);
|
||||
void write(std::ostream &out, config const &cfg);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
#include "statistics.hpp"
|
||||
#include "util.hpp"
|
||||
#include "log.hpp"
|
||||
#include "serialization/binary_or_text.hpp"
|
||||
|
||||
#define ERR_NG lg::err(lg::engine)
|
||||
|
||||
|
@ -38,7 +37,6 @@ struct scenario_stats
|
|||
explicit scenario_stats(const config& cfg);
|
||||
|
||||
config write() const;
|
||||
void write(config_writer &out) const;
|
||||
|
||||
std::vector<stats> team_stats;
|
||||
std::string scenario_name;
|
||||
|
@ -64,16 +62,6 @@ config scenario_stats::write() const
|
|||
return res;
|
||||
}
|
||||
|
||||
void scenario_stats::write(config_writer &out) const
|
||||
{
|
||||
out.write_key_val("scenario", scenario_name);
|
||||
for(std::vector<stats>::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
|
||||
out.open_child("team");
|
||||
i->write(out);
|
||||
out.close_child("team");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<scenario_stats> master_stats;
|
||||
|
||||
stats& get_stats(int team)
|
||||
|
@ -103,15 +91,6 @@ config write_str_int_map(const stats::str_int_map& m)
|
|||
return res;
|
||||
}
|
||||
|
||||
void write_str_int_map(config_writer &out, const stats::str_int_map& m)
|
||||
{
|
||||
for(stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
||||
char buf[50];
|
||||
snprintf(buf,sizeof(buf),"%d",i->second);
|
||||
out.write_key_val(i->first, buf);
|
||||
}
|
||||
}
|
||||
|
||||
stats::str_int_map read_str_int_map(const config& cfg)
|
||||
{
|
||||
stats::str_int_map m;
|
||||
|
@ -137,19 +116,6 @@ config write_battle_result_map(const stats::battle_result_map& m)
|
|||
return res;
|
||||
}
|
||||
|
||||
void write_battle_result_map(config_writer &out, const stats::battle_result_map& m)
|
||||
{
|
||||
for(stats::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
||||
out.open_child("sequence");
|
||||
write_str_int_map(out, i->second);
|
||||
|
||||
char buf[50];
|
||||
snprintf(buf,sizeof(buf),"%d",i->first);
|
||||
out.write_key_val("_num", buf);
|
||||
out.close_child("sequence");
|
||||
}
|
||||
}
|
||||
|
||||
stats::battle_result_map read_battle_result_map(const config& cfg)
|
||||
{
|
||||
stats::battle_result_map m;
|
||||
|
@ -243,50 +209,6 @@ config stats::write() const
|
|||
return res;
|
||||
}
|
||||
|
||||
void stats::write(config_writer &out) const
|
||||
{
|
||||
out.open_child("recruits");
|
||||
write_str_int_map(out, recruits);
|
||||
out.close_child("recruits");
|
||||
out.open_child("recalls");
|
||||
write_str_int_map(out, recalls);
|
||||
out.close_child("recalls");
|
||||
out.open_child("advances");
|
||||
write_str_int_map(out, advanced_to);
|
||||
out.close_child("advances");
|
||||
out.open_child("deaths");
|
||||
write_str_int_map(out, deaths);
|
||||
out.close_child("deaths");
|
||||
out.open_child("killed");
|
||||
write_str_int_map(out, killed);
|
||||
out.close_child("killed");
|
||||
out.open_child("attacks");
|
||||
write_battle_result_map(out, attacks);
|
||||
out.close_child("attacks");
|
||||
out.open_child("defends");
|
||||
write_battle_result_map(out, defends);
|
||||
out.close_child("defends");
|
||||
|
||||
char buf[50];
|
||||
snprintf(buf,sizeof(buf),"%d",recruit_cost);
|
||||
out.write_key_val("recruit_cost", buf);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%d",recall_cost);
|
||||
out.write_key_val("recall_cost", buf);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%d",damage_inflicted);
|
||||
out.write_key_val("damage_inflicted", buf);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%d",damage_taken);
|
||||
out.write_key_val("damage_taken", buf);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%d",expected_damage_inflicted);
|
||||
out.write_key_val("expected_damage_inflicted", buf);
|
||||
|
||||
snprintf(buf,sizeof(buf),"%d",expected_damage_taken);
|
||||
out.write_key_val("expected_damage_taken", buf);
|
||||
}
|
||||
|
||||
void stats::read(const config& cfg)
|
||||
{
|
||||
if(cfg.child("recruits")) {
|
||||
|
@ -508,17 +430,6 @@ config write_stats()
|
|||
return res;
|
||||
}
|
||||
|
||||
void write_stats(config_writer &out)
|
||||
{
|
||||
out.write_key_val("mid_scenario", mid_scenario ? "true" : "false");
|
||||
|
||||
for(std::vector<scenario_stats>::const_iterator i = master_stats.begin(); i != master_stats.end(); ++i) {
|
||||
out.open_child("scenario");
|
||||
i->write(out);
|
||||
out.close_child("scenario");
|
||||
}
|
||||
}
|
||||
|
||||
void read_stats(const config& cfg)
|
||||
{
|
||||
fresh_stats();
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
#ifndef STATISTICS_HPP_INCLUDED
|
||||
#define STATISTICS_HPP_INCLUDED
|
||||
|
||||
class config_writer;
|
||||
class unit;
|
||||
#include "actions.hpp"
|
||||
|
||||
|
@ -26,7 +25,6 @@ namespace statistics
|
|||
explicit stats(const config& cfg);
|
||||
|
||||
config write() const;
|
||||
void write(config_writer &out) const;
|
||||
void read(const config& cfg);
|
||||
|
||||
typedef std::map<std::string,int> str_int_map;
|
||||
|
@ -96,7 +94,6 @@ namespace statistics
|
|||
void advance_unit(const unit& u);
|
||||
|
||||
config write_stats();
|
||||
void write_stats(config_writer &out);
|
||||
void read_stats(const config& cfg);
|
||||
void fresh_stats();
|
||||
void clear_current_scenario();
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
class unit;
|
||||
class display;
|
||||
class gamestatus;
|
||||
class config_writer;
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
@ -146,7 +145,6 @@ class unit
|
|||
*/
|
||||
void read(const config& cfg);
|
||||
void write(config& cfg) const;
|
||||
void write(config_writer& out) const;
|
||||
|
||||
void assign_role(const std::string& role);
|
||||
const std::vector<attack_type>& attacks() const;
|
||||
|
|
Loading…
Add table
Reference in a new issue