Tangible serialization and design improvements
This commit is contained in:
@@ -6,7 +6,6 @@ AnimStep::AnimStep() {}
|
|||||||
AnimStep::~AnimStep() {}
|
AnimStep::~AnimStep() {}
|
||||||
|
|
||||||
AnimQueue::AnimQueue() {
|
AnimQueue::AnimQueue() {
|
||||||
id_ = 0;
|
|
||||||
size_limit_ = 10; // Default size limit.
|
size_limit_ = 10; // Default size limit.
|
||||||
clear_steps();
|
clear_steps();
|
||||||
}
|
}
|
||||||
@@ -204,7 +203,6 @@ bool AnimQueue::valid() {
|
|||||||
|
|
||||||
void AnimQueue::serialize(StreamBuffer *sb) {
|
void AnimQueue::serialize(StreamBuffer *sb) {
|
||||||
assert(valid()); // can't serialize an invalid animqueue.
|
assert(valid()); // can't serialize an invalid animqueue.
|
||||||
sb->write_int64(id_);
|
|
||||||
sb->write_int32(size_limit_);
|
sb->write_int32(size_limit_);
|
||||||
sb->write_size(steps_.size());
|
sb->write_size(steps_.size());
|
||||||
for (const AnimStep &step : steps_) {
|
for (const AnimStep &step : steps_) {
|
||||||
@@ -229,7 +227,6 @@ void AnimQueue::serialize(StreamBuffer *sb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AnimQueue::deserialize(StreamBuffer *sb) {
|
void AnimQueue::deserialize(StreamBuffer *sb) {
|
||||||
id_ = sb->read_int64();
|
|
||||||
size_limit_ = sb->read_int32();
|
size_limit_ = sb->read_int32();
|
||||||
size_t nsteps = sb->read_size();
|
size_t nsteps = sb->read_size();
|
||||||
steps_.clear();
|
steps_.clear();
|
||||||
@@ -330,7 +327,6 @@ LuaDefine(unittests_animqueue, "c") {
|
|||||||
LuaAssert(L, aq.valid());
|
LuaAssert(L, aq.valid());
|
||||||
|
|
||||||
// Test serialization and deserialization.
|
// Test serialization and deserialization.
|
||||||
aq.set_id(123);
|
|
||||||
aq.set_size_limit(5);
|
aq.set_size_limit(5);
|
||||||
aq.clear_steps();
|
aq.clear_steps();
|
||||||
aq.add(12345, "walk");
|
aq.add(12345, "walk");
|
||||||
@@ -342,7 +338,6 @@ LuaDefine(unittests_animqueue, "c") {
|
|||||||
aq.serialize(&sb);
|
aq.serialize(&sb);
|
||||||
aqds.deserialize(&sb);
|
aqds.deserialize(&sb);
|
||||||
|
|
||||||
LuaAssert(L, aqds.get_id() == 123);
|
|
||||||
LuaAssert(L, aqds.size_limit() == 5);
|
LuaAssert(L, aqds.size_limit() == 5);
|
||||||
LuaAssert(L, aqds.size() == 4);
|
LuaAssert(L, aqds.size() == 4);
|
||||||
|
|
||||||
|
|||||||
@@ -75,15 +75,12 @@ public:
|
|||||||
|
|
||||||
class AnimQueue {
|
class AnimQueue {
|
||||||
private:
|
private:
|
||||||
int64_t id_;
|
|
||||||
int32_t size_limit_;
|
int32_t size_limit_;
|
||||||
std::deque<AnimStep> steps_;
|
std::deque<AnimStep> steps_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AnimQueue();
|
AnimQueue();
|
||||||
|
|
||||||
int64_t get_id() const { return id_; }
|
|
||||||
void set_id(int64_t id) { id_ = id; }
|
|
||||||
const AnimStep &nth(int n) const { return steps_[n]; }
|
const AnimStep &nth(int n) const { return steps_[n]; }
|
||||||
size_t size() const { return steps_.size(); }
|
size_t size() const { return steps_.size(); }
|
||||||
int32_t size_limit() const { return size_limit_; }
|
int32_t size_limit() const { return size_limit_; }
|
||||||
|
|||||||
@@ -104,24 +104,36 @@ void IdGlobalPool::deserialize(StreamBuffer *sb) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IdPlayerPool::IdPlayerPool(IdGlobalPool *gp) {
|
IdPlayerPool::IdPlayerPool(IdGlobalPool *g) {
|
||||||
global_ = gp;
|
global_ = g;
|
||||||
|
fifo_enabled_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IdPlayerPool::~IdPlayerPool() {
|
IdPlayerPool::~IdPlayerPool() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IdPlayerPool::enable_fifo() {
|
||||||
|
fifo_enabled_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IdPlayerPool::disable_fifo() {
|
||||||
|
unqueue();
|
||||||
|
fifo_enabled_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
void IdPlayerPool::purge() {
|
void IdPlayerPool::purge() {
|
||||||
ranges_.clear();
|
ranges_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void IdPlayerPool::refill() {
|
void IdPlayerPool::refill() {
|
||||||
|
if (fifo_enabled_) {
|
||||||
while (int(ranges_.size()) < global_->queue_fill()) {
|
while (int(ranges_.size()) < global_->queue_fill()) {
|
||||||
int64_t batch = global_->get_batch();
|
int64_t batch = global_->get_batch();
|
||||||
if (batch == 0) break;
|
if (batch == 0) break;
|
||||||
ranges_.push_back(batch);
|
ranges_.push_back(batch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void IdPlayerPool::unqueue() {
|
void IdPlayerPool::unqueue() {
|
||||||
while (!ranges_.empty()) {
|
while (!ranges_.empty()) {
|
||||||
@@ -131,7 +143,8 @@ void IdPlayerPool::unqueue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int64_t IdPlayerPool::get_batch() {
|
int64_t IdPlayerPool::get_batch() {
|
||||||
while (int(ranges_.size()) < global_->queue_fill() + 1) {
|
int fill = fifo_enabled_ ? global_->queue_fill() : 0;
|
||||||
|
while (int(ranges_.size()) < fill + 1) {
|
||||||
int64_t batch = global_->get_batch();
|
int64_t batch = global_->get_batch();
|
||||||
if (batch == 0) break;
|
if (batch == 0) break;
|
||||||
ranges_.push_back(batch);
|
ranges_.push_back(batch);
|
||||||
@@ -155,6 +168,7 @@ void IdPlayerPool::prepare_thread(lua_State *L) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void IdPlayerPool::serialize(StreamBuffer *sb) {
|
void IdPlayerPool::serialize(StreamBuffer *sb) {
|
||||||
|
sb->write_bool(fifo_enabled_);
|
||||||
sb->write_size(ranges_.size());
|
sb->write_size(ranges_.size());
|
||||||
for (int64_t batch : ranges_) {
|
for (int64_t batch : ranges_) {
|
||||||
sb->write_int64(batch);
|
sb->write_int64(batch);
|
||||||
@@ -162,6 +176,7 @@ void IdPlayerPool::serialize(StreamBuffer *sb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void IdPlayerPool::deserialize(StreamBuffer *sb) {
|
void IdPlayerPool::deserialize(StreamBuffer *sb) {
|
||||||
|
fifo_enabled_ = sb->read_bool();
|
||||||
size_t ranges_size = sb->read_size();
|
size_t ranges_size = sb->read_size();
|
||||||
ranges_.resize(ranges_size);
|
ranges_.resize(ranges_size);
|
||||||
for (int i=0; i < int(ranges_size); i++) {
|
for (int i=0; i < int(ranges_size); i++) {
|
||||||
@@ -230,6 +245,7 @@ LuaDefine(unittests_idalloc, "c") {
|
|||||||
|
|
||||||
// In the synchronous model, refill should do nothing.
|
// In the synchronous model, refill should do nothing.
|
||||||
pp.purge();
|
pp.purge();
|
||||||
|
pp.enable_fifo();
|
||||||
gp.init_synch(3);
|
gp.init_synch(3);
|
||||||
pp.refill();
|
pp.refill();
|
||||||
LuaAssert(L, pp.size() == 0);
|
LuaAssert(L, pp.size() == 0);
|
||||||
@@ -237,8 +253,20 @@ LuaDefine(unittests_idalloc, "c") {
|
|||||||
LuaAssert(L, pp.size() == 0);
|
LuaAssert(L, pp.size() == 0);
|
||||||
LuaAssert(L, pp.get_batch() == 0);
|
LuaAssert(L, pp.get_batch() == 0);
|
||||||
|
|
||||||
// Test refill from master.
|
// In the master model, with fifo disabled. Fifo should remain
|
||||||
|
// empty, but batches should be returned.
|
||||||
pp.purge();
|
pp.purge();
|
||||||
|
pp.disable_fifo();
|
||||||
|
gp.init_master(3);
|
||||||
|
pp.refill();
|
||||||
|
LuaAssert(L, pp.size() == 0);
|
||||||
|
LuaAssert(L, pp.get_batch() == nthbatch(0));
|
||||||
|
LuaAssert(L, pp.size() == 0);
|
||||||
|
LuaAssert(L, pp.get_batch() == nthbatch(1));
|
||||||
|
|
||||||
|
// Test refill from master (with enabled fifo).
|
||||||
|
pp.purge();
|
||||||
|
pp.enable_fifo();
|
||||||
gp.init_master(3);
|
gp.init_master(3);
|
||||||
pp.refill();
|
pp.refill();
|
||||||
LuaAssert(L, ranges_equal(pp.ranges_, nthbatch(0), nthbatch(1), nthbatch(2)));
|
LuaAssert(L, ranges_equal(pp.ranges_, nthbatch(0), nthbatch(1), nthbatch(2)));
|
||||||
@@ -258,6 +286,7 @@ LuaDefine(unittests_idalloc, "c") {
|
|||||||
|
|
||||||
// Try preparing a thread and salvaging a thread.
|
// Try preparing a thread and salvaging a thread.
|
||||||
pp.purge();
|
pp.purge();
|
||||||
|
pp.enable_fifo();
|
||||||
gp.init_master(3);
|
gp.init_master(3);
|
||||||
lua_setnextid(L, 0);
|
lua_setnextid(L, 0);
|
||||||
pp.prepare_thread(L);
|
pp.prepare_thread(L);
|
||||||
@@ -300,11 +329,14 @@ LuaDefine(unittests_idalloc, "c") {
|
|||||||
gpds.init_synch(5);
|
gpds.init_synch(5);
|
||||||
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
LuaAssert(L, gp.get_batch() == nthbatch(0));
|
||||||
pp.purge();
|
pp.purge();
|
||||||
|
pp.enable_fifo();
|
||||||
pp.refill();
|
pp.refill();
|
||||||
|
LuaAssert(L, pp.fifo_enabled());
|
||||||
LuaAssert(L, pp.size() == 3);
|
LuaAssert(L, pp.size() == 3);
|
||||||
ppds.purge();
|
ppds.purge();
|
||||||
pp.serialize(&sb);
|
pp.serialize(&sb);
|
||||||
ppds.deserialize(&sb);
|
ppds.deserialize(&sb);
|
||||||
|
LuaAssert(L, ppds.fifo_enabled());
|
||||||
LuaAssert(L, ppds.size() == 3);
|
LuaAssert(L, ppds.size() == 3);
|
||||||
LuaAssert(L, ppds.get_batch() == nthbatch(1));
|
LuaAssert(L, ppds.get_batch() == nthbatch(1));
|
||||||
LuaAssert(L, ppds.get_batch() == nthbatch(2));
|
LuaAssert(L, ppds.get_batch() == nthbatch(2));
|
||||||
|
|||||||
@@ -128,22 +128,32 @@ public:
|
|||||||
class IdPlayerPool {
|
class IdPlayerPool {
|
||||||
private:
|
private:
|
||||||
IdGlobalPool *global_;
|
IdGlobalPool *global_;
|
||||||
|
bool fifo_enabled_;
|
||||||
std::deque<int64_t> ranges_;
|
std::deque<int64_t> ranges_;
|
||||||
friend int unittests_idalloc(lua_State *L);
|
friend int unittests_idalloc(lua_State *L);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Construct a player pool.
|
// Construct a player pool.
|
||||||
// The Player pool stores a pointer to the global pool.
|
//
|
||||||
IdPlayerPool(IdGlobalPool *igp);
|
// The fifo is initially in the disabled state. In the disabled state, the
|
||||||
|
// fifo is not kept filled. You can still use the allocator when the fifo is
|
||||||
|
// disabled - doing so just fetches a batch from the global pool.
|
||||||
|
//
|
||||||
|
IdPlayerPool(IdGlobalPool *g);
|
||||||
~IdPlayerPool();
|
~IdPlayerPool();
|
||||||
|
|
||||||
// Refill the fifo queue of batches from the global pool.
|
// Enable or disable the fifo.
|
||||||
void refill();
|
void enable_fifo();
|
||||||
|
void disable_fifo();
|
||||||
|
bool fifo_enabled() { return fifo_enabled_; }
|
||||||
|
|
||||||
// Return all batches to the global pool. Leave the fifo empty.
|
// Return all batches from the fifo to the global pool.
|
||||||
void unqueue();
|
void unqueue();
|
||||||
|
|
||||||
// Discard all batches. This is only for unit testing.
|
// Refill the fifo of batches from the global pool.
|
||||||
|
void refill();
|
||||||
|
|
||||||
|
// Discard all batches in the fifo. This is only for unit testing.
|
||||||
void purge();
|
void purge();
|
||||||
|
|
||||||
// Get a batch from the fifo. Also refills the fifo.
|
// Get a batch from the fifo. Also refills the fifo.
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ public:
|
|||||||
void write_uint32(uint32_t v) { write_int32(v); }
|
void write_uint32(uint32_t v) { write_int32(v); }
|
||||||
void write_uint64(uint64_t v) { write_int64(v); }
|
void write_uint64(uint64_t v) { write_int64(v); }
|
||||||
void write_size(size_t sz) { write_int64(sz); }
|
void write_size(size_t sz) { write_int64(sz); }
|
||||||
|
void write_bool(bool b) { write_int8(b ? 1 : 0); }
|
||||||
|
|
||||||
// Write strings or blocks of bytes into the buffer.
|
// Write strings or blocks of bytes into the buffer.
|
||||||
void write_string(const std::string &s);
|
void write_string(const std::string &s);
|
||||||
@@ -347,6 +348,7 @@ public:
|
|||||||
uint16_t read_uint16() { return read_int16(); }
|
uint16_t read_uint16() { return read_int16(); }
|
||||||
uint32_t read_uint32() { return read_int32(); }
|
uint32_t read_uint32() { return read_int32(); }
|
||||||
uint64_t read_uint64() { return read_int64(); }
|
uint64_t read_uint64() { return read_int64(); }
|
||||||
|
bool read_bool() { return read_int8(); }
|
||||||
|
|
||||||
// May throw StreamEof or StreamCorruption.
|
// May throw StreamEof or StreamCorruption.
|
||||||
size_t read_size();
|
size_t read_size();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
#include "traceback.hpp"
|
#include "traceback.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
Tangible::Tangible() : world_(nullptr) {
|
Tangible::Tangible(World *w, int64_t id) : world_(w), id_player_pool_(&w->id_global_pool_) {
|
||||||
|
plane_item_.set_id(id);
|
||||||
|
w->plane_map_.track(&plane_item_);
|
||||||
}
|
}
|
||||||
|
|
||||||
World::~World() {
|
World::~World() {
|
||||||
@@ -43,9 +45,23 @@ void Tangible::update_plane_item() {
|
|||||||
plane_item_.set_pos(plane, xyz.x, xyz.y, xyz.z);
|
plane_item_.set_pos(plane, xyz.x, xyz.y, xyz.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Tangible::serialize(StreamBuffer *sb) {
|
||||||
|
sb->write_int64(id());
|
||||||
|
anim_queue_.serialize(sb);
|
||||||
|
id_player_pool_.serialize(sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tangible::deserialize(StreamBuffer *sb) {
|
||||||
|
int64_t id = sb->read_int64();
|
||||||
|
plane_item_.set_id(id);
|
||||||
|
anim_queue_.deserialize(sb);
|
||||||
|
id_player_pool_.deserialize(sb);
|
||||||
|
update_plane_item();
|
||||||
|
}
|
||||||
|
|
||||||
void Tangible::be_a_player() {
|
void Tangible::be_a_player() {
|
||||||
if (id_player_pool_ == nullptr) {
|
if (!id_player_pool_.fifo_enabled()) {
|
||||||
id_player_pool_.reset(new IdPlayerPool(&world_->id_global_pool_));
|
id_player_pool_.enable_fifo();
|
||||||
|
|
||||||
anim_queue_.add(world_->id_global_pool_.get_one(), "");
|
anim_queue_.add(world_->id_global_pool_.get_one(), "");
|
||||||
anim_queue_.set_graphic("player");
|
anim_queue_.set_graphic("player");
|
||||||
@@ -55,7 +71,7 @@ void Tangible::be_a_player() {
|
|||||||
|
|
||||||
LS.makeclass(classtab, "player");
|
LS.makeclass(classtab, "player");
|
||||||
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
LS.rawget(tangibles, LuaRegistry, "tangibles");
|
||||||
LS.rawget(place, tangibles, anim_queue_.get_id());
|
LS.rawget(place, tangibles, id());
|
||||||
LS.getmetatable(mt, place);
|
LS.getmetatable(mt, place);
|
||||||
LS.rawset(mt, "__index", classtab);
|
LS.rawset(mt, "__index", classtab);
|
||||||
LS.result();
|
LS.result();
|
||||||
@@ -80,7 +96,7 @@ Tangible *World::tangible_get(int64_t id) {
|
|||||||
if (iter == tangibles_.end()) {
|
if (iter == tangibles_.end()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
} else {
|
} else {
|
||||||
return &iter->second;
|
return iter->second.get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,12 +138,9 @@ Tangible *World::tangible_make(lua_State *L, int64_t id, bool pushdb) {
|
|||||||
if (id == 0) id = id_global_pool_.alloc_id_for_thread(L);
|
if (id == 0) id = id_global_pool_.alloc_id_for_thread(L);
|
||||||
|
|
||||||
// Create the C++ part of the structure.
|
// Create the C++ part of the structure.
|
||||||
Tangible *t = &tangibles_[id];
|
std::unique_ptr<Tangible> &t = tangibles_[id];
|
||||||
assert(t->world_ == nullptr);
|
assert (t == nullptr);
|
||||||
t->world_ = this;
|
t.reset(new Tangible(this, id));
|
||||||
t->plane_item_.set_id(id);
|
|
||||||
t->anim_queue_.set_id(id);
|
|
||||||
plane_map_.track(&t->plane_item_);
|
|
||||||
|
|
||||||
// Create the tangible's database and metatable.
|
// Create the tangible's database and metatable.
|
||||||
LS.set(database, LuaNewTable);
|
LS.set(database, LuaNewTable);
|
||||||
@@ -146,7 +159,7 @@ Tangible *World::tangible_make(lua_State *L, int64_t id, bool pushdb) {
|
|||||||
|
|
||||||
LS.result();
|
LS.result();
|
||||||
if (!pushdb) lua_pop(L, 1);
|
if (!pushdb) lua_pop(L, 1);
|
||||||
return t;
|
return t.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
void World::store_global_pointer(lua_State *L, World *v) {
|
void World::store_global_pointer(lua_State *L, World *v) {
|
||||||
@@ -227,7 +240,7 @@ void World::invoke_plan(int64_t actor_id, int64_t place_id, const std::string &a
|
|||||||
|
|
||||||
// Get an ID batch for the thread, and take one for the thread itself.
|
// Get an ID batch for the thread, and take one for the thread itself.
|
||||||
Tangible *tactor = tangible_get(actor_id);
|
Tangible *tactor = tangible_get(actor_id);
|
||||||
int64_t id_batch = tactor->id_player_pool_->get_batch();
|
int64_t id_batch = tactor->id_player_pool_.get_batch();
|
||||||
int64_t tid = id_batch++;
|
int64_t tid = id_batch++;
|
||||||
|
|
||||||
// Set up for Lua manipulation.
|
// Set up for Lua manipulation.
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ class World;
|
|||||||
|
|
||||||
class Tangible {
|
class Tangible {
|
||||||
public:
|
public:
|
||||||
// Simple constructor initializes everything to null.
|
|
||||||
//
|
|
||||||
Tangible();
|
|
||||||
|
|
||||||
// Always points back to the world model.
|
// Always points back to the world model.
|
||||||
World *world_;
|
World *world_;
|
||||||
|
|
||||||
@@ -33,17 +29,36 @@ public:
|
|||||||
// Plane Item.
|
// Plane Item.
|
||||||
//
|
//
|
||||||
// The PlaneItem also contains this tangible's ID.
|
// The PlaneItem also contains this tangible's ID.
|
||||||
// To move this Tangible, update the anim_queue first, then update
|
// To move this PlaneItem, update the anim_queue first, then call
|
||||||
// this plane_item_ from the anim_queue.
|
// update_plane_item, which copies the data from the anim_queue.
|
||||||
|
//
|
||||||
PlaneItem plane_item_;
|
PlaneItem plane_item_;
|
||||||
|
|
||||||
// Player ID pool
|
// Player ID pool
|
||||||
//
|
//
|
||||||
// Note: this is only allocated if this Tangible is a player.
|
// This is present in every tangible, whether a player or not.
|
||||||
std::unique_ptr<IdPlayerPool> id_player_pool_;
|
// However, the fifo is only enabled in logged-in players.
|
||||||
|
//
|
||||||
|
IdPlayerPool id_player_pool_;
|
||||||
|
|
||||||
|
// constructor.
|
||||||
|
//
|
||||||
|
Tangible(World *w, int64_t id);
|
||||||
|
|
||||||
|
// Get the ID
|
||||||
|
//
|
||||||
|
int64_t id() { return plane_item_.id(); }
|
||||||
void be_a_player();
|
void be_a_player();
|
||||||
void update_plane_item();
|
void update_plane_item();
|
||||||
|
|
||||||
|
// Serialize and deserialize
|
||||||
|
//
|
||||||
|
// PlaneItem is not serialized except the ID. The deserialize routine
|
||||||
|
// rebuilds the PlaneItem from the AnimQueue. World pointer is not
|
||||||
|
// serialized.
|
||||||
|
//
|
||||||
|
void serialize(StreamBuffer *sb);
|
||||||
|
void deserialize(StreamBuffer *sb);
|
||||||
};
|
};
|
||||||
|
|
||||||
class World {
|
class World {
|
||||||
@@ -60,7 +75,7 @@ public:
|
|||||||
//
|
//
|
||||||
SourceDB source_db_;
|
SourceDB source_db_;
|
||||||
PlaneMap plane_map_;
|
PlaneMap plane_map_;
|
||||||
std::unordered_map<int64_t, Tangible> tangibles_;
|
std::unordered_map<int64_t, std::unique_ptr<Tangible>> tangibles_;
|
||||||
|
|
||||||
// Thread schedule: must include every thread, except
|
// Thread schedule: must include every thread, except
|
||||||
// for the one currently-executing thread.
|
// for the one currently-executing thread.
|
||||||
|
|||||||
Reference in New Issue
Block a user