more internal changes to the unit display code,
...unit display themselves when moving instead of being displayed by someone else
This commit is contained in:
parent
34bfb4d5a1
commit
56de0453f3
10 changed files with 159 additions and 68 deletions
|
@ -276,6 +276,14 @@ int animated<T,T_void_value>::get_last_frame_time() const
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
template<typename T, typename T_void_value>
|
||||
void animated<T, T_void_value>::synchronize_start(animated<T> &a, animated<T> &b,int acceleration)
|
||||
{
|
||||
int start_time = minimum<int>(a.get_first_frame_time(),b.get_first_frame_time());
|
||||
a.start_animation(start_time,1,acceleration);
|
||||
b.start_animation(start_time,1,acceleration);
|
||||
}
|
||||
// Force compilation of the following template instantiations
|
||||
|
||||
#include "image.hpp"
|
||||
|
|
|
@ -71,6 +71,9 @@ public:
|
|||
const T& get_first_frame() const;
|
||||
const T& get_last_frame() const;
|
||||
|
||||
|
||||
static void synchronize_start(animated<T> &a, animated<T>& b,int acceleration);
|
||||
|
||||
private:
|
||||
struct frame
|
||||
{
|
||||
|
|
|
@ -1533,7 +1533,7 @@ bool event_handler::handle_event_command(const queued_event& event_info,
|
|||
|
||||
anim.update_current_frame();
|
||||
}
|
||||
unit_image = image::get_image(u->second.image());
|
||||
unit_image = image::get_image(u->second.image_loc());
|
||||
screen->draw_tile(u->first.x,u->first.y,unit_image);
|
||||
screen->update_display();
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ enum ORIENTATION { NORMAL, REVERSE };
|
|||
///function to add a haloing effect using 'image'
|
||||
///centered on (x,y)
|
||||
///returns the handle to the halo object
|
||||
///0 is the invalid handle
|
||||
int add(int x, int y, const std::string& image, ORIENTATION orientation=NORMAL, int lifetime_cycles=-1);
|
||||
|
||||
///function to set the position of an existing haloing
|
||||
|
|
|
@ -2123,7 +2123,7 @@ void turn_info::recall()
|
|||
for(std::vector<unit>::const_iterator u = recall_list.begin(); u != recall_list.end(); ++u) {
|
||||
std::stringstream option;
|
||||
const std::string& description = u->description().empty() ? "-" : u->description();
|
||||
option << IMAGE_PREFIX << u->image() << COLUMN_SEPARATOR
|
||||
option << IMAGE_PREFIX << u->absolute_image() << COLUMN_SEPARATOR
|
||||
<< u->type().language_name() << COLUMN_SEPARATOR
|
||||
<< description << COLUMN_SEPARATOR
|
||||
<< u->type().level() << COLUMN_SEPARATOR
|
||||
|
|
53
src/unit.cpp
53
src/unit.cpp
|
@ -25,6 +25,8 @@
|
|||
#include "util.hpp"
|
||||
#include "wassert.hpp"
|
||||
#include "serialization/string_utils.hpp"
|
||||
#include "halo.hpp"
|
||||
#include "display.hpp"
|
||||
|
||||
//DEBUG
|
||||
#include <iostream>
|
||||
|
@ -69,9 +71,12 @@ unit::unit(const game_data& data, const config& cfg) :
|
|||
state_(STATE_NORMAL),
|
||||
moves_(0), user_end_turn_(false), facingLeft_(true),
|
||||
resting_(false), hold_position_(false), recruit_(false),
|
||||
guardian_(false), upkeep_(UPKEEP_FREE),anim_(NULL)
|
||||
guardian_(false), upkeep_(UPKEEP_FREE),anim_(NULL),
|
||||
unit_halo_(0),unit_anim_halo_(0)
|
||||
{
|
||||
read(data,cfg);
|
||||
|
||||
|
||||
}
|
||||
|
||||
unit_race::GENDER unit::generate_gender(const unit_type& type, bool gen)
|
||||
|
@ -83,7 +88,6 @@ unit_race::GENDER unit::generate_gender(const unit_type& type, bool gen)
|
|||
return unit_race::MALE;
|
||||
}
|
||||
}
|
||||
|
||||
//constructor for creating a new unit
|
||||
unit::unit(const unit_type* t, int side, bool use_traits, bool dummy_unit, unit_race::GENDER gender) :
|
||||
gender_(dummy_unit ? gender : generate_gender(*t,use_traits)),
|
||||
|
@ -101,7 +105,8 @@ unit::unit(const unit_type* t, int side, bool use_traits, bool dummy_unit, unit_
|
|||
attacks_(type_->attacks()),
|
||||
backupAttacks_(type_->attacks()),
|
||||
guardian_(false), upkeep_(UPKEEP_FULL_PRICE),
|
||||
unrenamable_(false),anim_(NULL)
|
||||
unrenamable_(false),anim_(NULL),unit_halo_(0),
|
||||
unit_anim_halo_(0)
|
||||
{
|
||||
//dummy units used by the 'move_unit_fake' command don't need to have a side.
|
||||
if(dummy_unit == false) validate_side(side_);
|
||||
|
@ -142,7 +147,8 @@ unit::unit(const unit_type* t, const unit& u) :
|
|||
modifications_(u.modifications_),
|
||||
traitsDescription_(u.traitsDescription_),
|
||||
guardian_(false), upkeep_(u.upkeep_),
|
||||
unrenamable_(u.unrenamable_),anim_(NULL)
|
||||
unrenamable_(u.unrenamable_),anim_(NULL),unit_halo_(0),
|
||||
unit_anim_halo_(0)
|
||||
{
|
||||
validate_side(side_);
|
||||
|
||||
|
@ -154,6 +160,12 @@ unit::unit(const unit_type* t, const unit& u) :
|
|||
statusFlags_.clear();
|
||||
}
|
||||
|
||||
unit::~unit()
|
||||
{
|
||||
if(unit_halo_) {
|
||||
halo::remove(unit_halo_);
|
||||
}
|
||||
}
|
||||
void unit::generate_traits()
|
||||
{
|
||||
if(!traitsDescription_.empty())
|
||||
|
@ -1043,6 +1055,7 @@ void unit::set_standing()
|
|||
|
||||
void unit::set_defending(bool hits, std::string range, int start_frame, int acceleration)
|
||||
{
|
||||
update_frame();
|
||||
state_ = STATE_DEFENDING;
|
||||
if(anim_) {
|
||||
delete anim_;
|
||||
|
@ -1050,6 +1063,7 @@ void unit::set_defending(bool hits, std::string range, int start_frame, int acce
|
|||
}
|
||||
anim_ = new defensive_animation(type_->defend_animation(hits,range));
|
||||
anim_->start_animation(start_frame,1,acceleration);
|
||||
anim_->update_current_frame();
|
||||
}
|
||||
|
||||
void unit::update_frame()
|
||||
|
@ -1495,6 +1509,37 @@ bool unit::is_flying() const
|
|||
return type().movement_type().is_flying();
|
||||
}
|
||||
|
||||
void unit::refresh_unit(display& disp,const int& x, const int& y, const double& submerge)
|
||||
{
|
||||
gamemap::location hex = disp.hex_clicked_on(x+disp.hex_size()/2,y+disp.hex_size()/2);
|
||||
gamemap::location adjacent[6];
|
||||
get_adjacent_tiles(hex, adjacent);
|
||||
|
||||
surface image(image::get_image(image_loc()));
|
||||
if (!facing_left()) {
|
||||
image.assign(image::reverse_image(image));
|
||||
}
|
||||
disp.draw_tile(hex.x, hex.y);
|
||||
for(int tile = 0; tile != 6; ++tile) {
|
||||
disp.draw_tile(adjacent[tile].x, adjacent[tile].y);
|
||||
}
|
||||
disp.draw_unit(x, y, image, false, ftofxp(1.0), 0, 0.0, submerge);
|
||||
if(!unit_halo_ && !type().image_halo().empty()) {
|
||||
unit_halo_ = halo::add(0,0,type().image_halo());
|
||||
}
|
||||
if(unit_halo_) {
|
||||
int d = disp.hex_size() / 2;
|
||||
halo::set_location(unit_halo_, x+ d, y+ d);
|
||||
}
|
||||
|
||||
|
||||
|
||||
disp.update_display();
|
||||
events::pump();
|
||||
}
|
||||
|
||||
|
||||
|
||||
int team_units(const unit_map& units, unsigned int side)
|
||||
{
|
||||
int res = 0;
|
||||
|
|
15
src/unit.hpp
15
src/unit.hpp
|
@ -25,6 +25,7 @@
|
|||
#include <vector>
|
||||
|
||||
class unit;
|
||||
class display;
|
||||
|
||||
typedef std::map<gamemap::location,unit> unit_map;
|
||||
|
||||
|
@ -33,11 +34,13 @@ class unit
|
|||
public:
|
||||
friend struct unit_movement_resetter;
|
||||
|
||||
unit(const unit& u) { *this=u ; unit_halo_ = 0; }
|
||||
unit(const game_data& data, const config& cfg);
|
||||
unit(const unit_type* t, int side, bool use_traits=false, bool dummy_unit=false, unit_race::GENDER gender=unit_race::MALE);
|
||||
|
||||
//a constructor used when advancing a unit
|
||||
unit(const unit_type* t, const unit& u);
|
||||
virtual ~unit();
|
||||
const unit_type& type() const;
|
||||
std::string name() const;
|
||||
const std::string& description() const;
|
||||
|
@ -135,8 +138,9 @@ public:
|
|||
|
||||
//gets the unit image that should currently be displayed
|
||||
//(could be in the middle of an attack etc)
|
||||
const std::string& image() const;
|
||||
const std::string& absolute_image() const {return type_->image();}
|
||||
const image::locator image_loc() const;
|
||||
void refresh_unit(display& disp,const int& x,const int& y,const double& submerge);
|
||||
|
||||
void set_standing();
|
||||
void set_defending(bool hits, std::string range, int start_frame, int acceleration);
|
||||
|
@ -183,15 +187,17 @@ public:
|
|||
// MOVED if moved and then pressed "end turn"
|
||||
// NOT_MOVED if not moved and pressed "end turn"
|
||||
enum MOVES { ATTACKED=-1, MOVED=-2, NOT_MOVED=-3 };
|
||||
enum STATE { STATE_NORMAL, STATE_ATTACKING, STATE_DEFENDING,
|
||||
STATE_LEADING, STATE_HEALING, STATE_WALKING};
|
||||
STATE state() const {return state_;}
|
||||
private:
|
||||
const std::string& image() const;
|
||||
unit_race::GENDER generate_gender(const unit_type& type, bool use_genders);
|
||||
unit_race::GENDER gender_;
|
||||
std::string variation_;
|
||||
|
||||
const unit_type* type_;
|
||||
|
||||
enum STATE { STATE_NORMAL, STATE_ATTACKING, STATE_DEFENDING,
|
||||
STATE_LEADING, STATE_HEALING, STATE_WALKING};
|
||||
STATE state_;
|
||||
const attack_type* attackType_;
|
||||
int attackingMilliseconds_;
|
||||
|
@ -254,6 +260,9 @@ private:
|
|||
void remove_temporary_modifications();
|
||||
void generate_traits();
|
||||
void generate_traits_description();
|
||||
int unit_halo_;
|
||||
int unit_anim_halo_;
|
||||
|
||||
};
|
||||
|
||||
//object which temporarily resets a unit's movement
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
namespace
|
||||
{
|
||||
|
||||
void move_unit_between(display& disp, const gamemap& map, const gamemap::location& a, const gamemap::location& b, unit& u)
|
||||
void teleport_unit_between(display& disp, const gamemap& map, const gamemap::location& a, const gamemap::location& b, unit& u)
|
||||
{
|
||||
if(disp.update_locked() || disp.fogged(a.x,a.y) && disp.fogged(b.x,b.y)) {
|
||||
return;
|
||||
|
@ -57,8 +57,6 @@ void move_unit_between(display& disp, const gamemap& map, const gamemap::locatio
|
|||
|
||||
LOG_DP << "submerge: " << src_submerge << " -> " << dst_submerge << "\n";
|
||||
|
||||
const gamemap::TERRAIN terrain = map.get_terrain(b);
|
||||
|
||||
const int acceleration = disp.turbo() ? 5:1;
|
||||
|
||||
gamemap::location src_adjacent[6];
|
||||
|
@ -73,10 +71,9 @@ void move_unit_between(display& disp, const gamemap& map, const gamemap::locatio
|
|||
halo_effect.assign(halo::add(0,0,halo));
|
||||
}
|
||||
|
||||
const unit_animation *teleport_animation_p = u.type().teleport_animation();
|
||||
bool teleport_unit = teleport_animation_p && !tiles_adjacent(a, b);
|
||||
if (teleport_unit && !disp.fogged(a.x, a.y)) { // teleport
|
||||
unit_animation teleport_animation = *teleport_animation_p;
|
||||
const unit_animation &teleport_animation_p = u.type().teleport_animation();
|
||||
if (!disp.fogged(a.x, a.y)) { // teleport
|
||||
unit_animation teleport_animation = teleport_animation_p;
|
||||
int animation_time;
|
||||
const int begin_at = teleport_animation.get_first_frame_time();
|
||||
teleport_animation.start_animation(begin_at,1, acceleration);
|
||||
|
@ -99,57 +96,20 @@ void move_unit_between(display& disp, const gamemap& map, const gamemap::locatio
|
|||
for(int tile = 0; tile != 6; ++tile) {
|
||||
disp.draw_tile(src_adjacent[tile].x, src_adjacent[tile].y);
|
||||
}
|
||||
disp.draw_unit(xsrc,ysrc,image,false, ftofxp(1.0), 0, 0.0, src_submerge);
|
||||
disp.draw_unit(xsrc,ysrc - src_height_adjust,image,false, ftofxp(1.0), 0, 0.0, src_submerge);
|
||||
disp.update_display();
|
||||
events::pump();
|
||||
teleport_animation.update_current_frame();
|
||||
animation_time = teleport_animation.get_animation_time();
|
||||
}
|
||||
}
|
||||
|
||||
const int total_mvt_time = 150 * u.movement_cost(map,terrain)/acceleration;
|
||||
const unsigned int start_time = SDL_GetTicks();
|
||||
int mvt_time = SDL_GetTicks() -start_time;
|
||||
while(mvt_time < total_mvt_time) {
|
||||
u.set_walking(map.underlying_mvt_terrain(src_terrain),a.get_relative_dir(b),acceleration);
|
||||
surface image(image::get_image(u.image_loc()));
|
||||
if (!face_left) {
|
||||
image.assign(image::reverse_image(image));
|
||||
}
|
||||
const int xloc = xsrc + int(double(xdst-xsrc)*(double(mvt_time)/total_mvt_time));
|
||||
const int yloc = ysrc + int(double(ydst-ysrc)*(double(mvt_time)/total_mvt_time));
|
||||
disp.scroll_to_tile(b.x,b.y,display::ONSCREEN);
|
||||
disp.draw_tile(a.x, a.y);
|
||||
for(int tile = 0; tile != 6; ++tile) {
|
||||
disp.draw_tile(src_adjacent[tile].x, src_adjacent[tile].y);
|
||||
disp.draw_tile(dst_adjacent[tile].x, dst_adjacent[tile].y);
|
||||
}
|
||||
|
||||
if (!teleport_unit) {
|
||||
const int height_adjust = src_height_adjust + int(double(dst_height_adjust - src_height_adjust) * (double(mvt_time) / total_mvt_time));
|
||||
const double submerge = src_submerge + int(double(dst_submerge - src_submerge) * (double(mvt_time) / total_mvt_time));
|
||||
const int xpos = xloc;
|
||||
const int ypos = yloc - height_adjust;
|
||||
disp.draw_unit(xpos, ypos, image, false, ftofxp(1.0), 0, 0.0, submerge);
|
||||
|
||||
if (halo_effect != 0) {
|
||||
int d = disp.hex_size() / 2;
|
||||
halo::set_location(halo_effect, xpos + d, ypos + d);
|
||||
}
|
||||
}
|
||||
|
||||
disp.draw_tile(a.x,a.y);
|
||||
disp.update_display();
|
||||
events::pump();
|
||||
|
||||
|
||||
|
||||
mvt_time = SDL_GetTicks() -start_time;
|
||||
teleport_animation.update_current_frame();
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (teleport_unit && !disp.fogged(b.x, b.y)) { // teleport
|
||||
unit_animation teleport_animation = *teleport_animation_p;
|
||||
if (!disp.fogged(b.x, b.y)) { // teleport
|
||||
unit_animation teleport_animation = teleport_animation_p;
|
||||
int animation_time;
|
||||
const int end_at = teleport_animation.get_last_frame_time();
|
||||
teleport_animation.start_animation(0,1, acceleration);
|
||||
|
@ -173,7 +133,7 @@ void move_unit_between(display& disp, const gamemap& map, const gamemap::locatio
|
|||
for(int tile = 0; tile != 6; ++tile) {
|
||||
disp.draw_tile(dst_adjacent[tile].x,dst_adjacent[tile].y);
|
||||
}
|
||||
disp.draw_unit(xdst, ydst, image, false, ftofxp(1.0), 0, 0.0, dst_submerge);
|
||||
disp.draw_unit(xdst, ydst - dst_height_adjust, image, false, ftofxp(1.0), 0, 0.0, dst_submerge);
|
||||
disp.update_display();
|
||||
events::pump();
|
||||
teleport_animation.update_current_frame();
|
||||
|
@ -182,6 +142,61 @@ void move_unit_between(display& disp, const gamemap& map, const gamemap::locatio
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void move_unit_between(display& disp, const gamemap& map, const gamemap::location& a, const gamemap::location& b, unit& u)
|
||||
{
|
||||
if(disp.update_locked() || disp.fogged(a.x,a.y) && disp.fogged(b.x,b.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int xsrc = disp.get_location_x(a);
|
||||
const int ysrc = disp.get_location_y(a);
|
||||
const int xdst = disp.get_location_x(b);
|
||||
const int ydst = disp.get_location_y(b);
|
||||
|
||||
const gamemap::TERRAIN src_terrain = map.get_terrain(a);
|
||||
const gamemap::TERRAIN dst_terrain = map.get_terrain(b);
|
||||
|
||||
const int src_height_adjust = u.is_flying() ? 0 : int(map.get_terrain_info(src_terrain).unit_height_adjust() * disp.zoom());
|
||||
const int dst_height_adjust = u.is_flying() ? 0 : int(map.get_terrain_info(dst_terrain).unit_height_adjust() * disp.zoom());
|
||||
|
||||
const double src_submerge = u.is_flying() ? 0.0 : map.get_terrain_info(src_terrain).unit_submerge();
|
||||
const double dst_submerge = u.is_flying() ? 0.0 : map.get_terrain_info(dst_terrain).unit_submerge();
|
||||
|
||||
LOG_DP << "submerge: " << src_submerge << " -> " << dst_submerge << "\n";
|
||||
|
||||
const int acceleration = disp.turbo() ? 5:1;
|
||||
|
||||
gamemap::location src_adjacent[6];
|
||||
get_adjacent_tiles(a, src_adjacent);
|
||||
|
||||
gamemap::location dst_adjacent[6];
|
||||
get_adjacent_tiles(b, dst_adjacent);
|
||||
|
||||
const std::string& halo = u.type().image_halo();
|
||||
util::scoped_resource<int,halo::remover> halo_effect(0);
|
||||
if(halo.empty() == false && !disp.fogged(b.x,b.y)) {
|
||||
halo_effect.assign(halo::add(0,0,halo));
|
||||
}
|
||||
|
||||
const int total_mvt_time = 150 * u.movement_cost(map,dst_terrain)/acceleration;
|
||||
const unsigned int start_time = SDL_GetTicks();
|
||||
int mvt_time = SDL_GetTicks() -start_time;
|
||||
disp.scroll_to_tiles(a.x,a.y,b.x,b.y,display::ONSCREEN);
|
||||
while(mvt_time < total_mvt_time) {
|
||||
u.set_walking(map.underlying_mvt_terrain(src_terrain),a.get_relative_dir(b),acceleration);
|
||||
|
||||
const int height_adjust = src_height_adjust + int(double(dst_height_adjust - src_height_adjust) * (double(mvt_time) / total_mvt_time));
|
||||
const double submerge = src_submerge + int(double(dst_submerge - src_submerge) * (double(mvt_time) / total_mvt_time));
|
||||
const int xloc = xsrc + int(double(xdst-xsrc)*(double(mvt_time)/total_mvt_time));
|
||||
const int yloc = ysrc + int(double(ydst-ysrc)*(double(mvt_time)/total_mvt_time)) - height_adjust;
|
||||
u.refresh_unit(disp,xloc,yloc,submerge);
|
||||
|
||||
mvt_time = SDL_GetTicks() -start_time;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace unit_display
|
||||
|
@ -224,7 +239,11 @@ void move_unit(display& disp, const gamemap& map, const std::vector<gamemap::loc
|
|||
}
|
||||
|
||||
if(!invisible) {
|
||||
move_unit_between(disp,map,path[i],path[i+1],u);
|
||||
if( !tiles_adjacent(path[i], path[i+1])) {
|
||||
teleport_unit_between(disp,map,path[i],path[i+1],u);
|
||||
} else {
|
||||
move_unit_between(disp,map,path[i],path[i+1],u);
|
||||
}
|
||||
previous_visible = true;
|
||||
} else if(previous_visible) {
|
||||
disp.draw_tile(path[i].x,path[i].y);
|
||||
|
@ -757,10 +776,13 @@ bool unit_attack(display& disp, unit_map& units, const gamemap& map,
|
|||
int new_halo_x = unit_frame.halo_x;
|
||||
int new_halo_y = unit_frame.halo_y;
|
||||
|
||||
const std::string& unit_image_name = unit_frame.image.empty() ? attacker.image() : unit_frame.image;
|
||||
|
||||
image::locator unit_image(unit_image_name,attacker.team_rgb_range(),attacker.type().flag_rgb());
|
||||
|
||||
const std::string& unit_image_name = unit_frame.image ;
|
||||
image::locator unit_image;
|
||||
if(!unit_image_name.empty()) {
|
||||
unit_image = image::locator(unit_image_name,attacker.team_rgb_range(),attacker.type().flag_rgb());
|
||||
} else {
|
||||
unit_image = attacker.image_loc();
|
||||
}
|
||||
if(!attacker.facing_left()) {
|
||||
xoffset *= -1;
|
||||
new_halo_x *= -1;
|
||||
|
|
|
@ -961,6 +961,10 @@ unit_type::unit_type(const config& cfg, const movement_type_map& mv_types,
|
|||
for(config::child_list::const_iterator t = teleports.begin(); t != teleports.end(); ++t) {
|
||||
teleport_animations_.push_back(unit_animation(**t));
|
||||
}
|
||||
if(teleport_animations_.empty()) {
|
||||
teleport_animations_.push_back(unit_animation(image(),-20,20));
|
||||
// always have a defensive animation
|
||||
}
|
||||
const config::child_list& extra_anims = cfg_.get_children("extra_anim");
|
||||
{
|
||||
for(config::child_list::const_iterator t = extra_anims.begin(); t != extra_anims.end(); ++t) {
|
||||
|
@ -1420,10 +1424,9 @@ const defensive_animation& unit_type::defend_animation(bool hits, std::string ra
|
|||
return *options[rand()%options.size()];
|
||||
}
|
||||
|
||||
const unit_animation* unit_type::teleport_animation( ) const
|
||||
const unit_animation& unit_type::teleport_animation( ) const
|
||||
{
|
||||
if (teleport_animations_.empty()) return NULL;
|
||||
return &teleport_animations_[rand() % teleport_animations_.size()];
|
||||
return teleport_animations_[rand() % teleport_animations_.size()];
|
||||
}
|
||||
|
||||
const unit_animation* unit_type::extra_animation(std::string flag ) const
|
||||
|
|
|
@ -243,7 +243,7 @@ public:
|
|||
const std::string& race() const;
|
||||
|
||||
const defensive_animation& defend_animation(bool hits, std::string range) const;
|
||||
const unit_animation* teleport_animation() const;
|
||||
const unit_animation& teleport_animation() const;
|
||||
const unit_animation* extra_animation(std::string flag) const;
|
||||
const death_animation& die_animation(const attack_type* attack) const;
|
||||
const movement_animation& move_animation(const std::string terrain,gamemap::location::DIRECTION) const;
|
||||
|
|
Loading…
Add table
Reference in a new issue