mirror of
https://github.com/Relintai/pandemonium_engine.git
synced 2025-02-11 10:30:06 +01:00
Implemented chunk material invalidation support for TerrainLibraryMergerPCM.
This commit is contained in:
parent
e11e75e5ea
commit
2024a33ee9
@ -425,7 +425,7 @@ void TerrainWorldEditor::isolevel_brush_draw(const Vector3 &p_world_position) {
|
||||
}
|
||||
}
|
||||
|
||||
_world->set_voxels_at_world_data_position(draw_data, _isolevel_brush_channel, true, _isolevel_brush_allow_create_chunks);
|
||||
_world->set_voxels_at_world_data_position(draw_data, _isolevel_brush_channel, true, _isolevel_brush_allow_create_chunks, false);
|
||||
}
|
||||
|
||||
void TerrainWorldEditor::paint_brush_draw(const Vector3 &p_world_position) {
|
||||
@ -471,7 +471,7 @@ void TerrainWorldEditor::paint_brush_draw(const Vector3 &p_world_position) {
|
||||
}
|
||||
}
|
||||
|
||||
_world->set_voxels_at_world_data_position(draw_data, _paint_brush_channel, true, _paint_brush_allow_create_chunks);
|
||||
_world->set_voxels_at_world_data_position(draw_data, _paint_brush_channel, true, _paint_brush_allow_create_chunks, true);
|
||||
}
|
||||
|
||||
void TerrainWorldEditor::edit(TerrainWorld *p_world) {
|
||||
|
@ -69,6 +69,12 @@ void TerrainLibraryMergerPCM::_material_cache_get_key(Ref<TerrainChunk> chunk) {
|
||||
return;
|
||||
}
|
||||
|
||||
int old_key = 0;
|
||||
|
||||
if (chunk->material_cache_key_has()) {
|
||||
old_key = chunk->material_cache_key_get();
|
||||
}
|
||||
|
||||
Vector<uint8_t> surfaces;
|
||||
|
||||
uint32_t size = chunk->get_data_size();
|
||||
@ -98,6 +104,10 @@ void TerrainLibraryMergerPCM::_material_cache_get_key(Ref<TerrainChunk> chunk) {
|
||||
chunk->material_cache_key_set(0);
|
||||
chunk->material_cache_key_has_set(false);
|
||||
|
||||
if (old_key != 0) {
|
||||
material_cache_unref(old_key);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -111,9 +121,18 @@ void TerrainLibraryMergerPCM::_material_cache_get_key(Ref<TerrainChunk> chunk) {
|
||||
|
||||
int hash = static_cast<int>(hstr.hash());
|
||||
|
||||
if (old_key != 0 && old_key == hash) {
|
||||
chunk->material_cache_key_invalid_set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
chunk->material_cache_key_set(hash);
|
||||
chunk->material_cache_key_has_set(true);
|
||||
|
||||
if (old_key != 0) {
|
||||
material_cache_unref(old_key);
|
||||
}
|
||||
|
||||
_material_cache_mutex.lock();
|
||||
|
||||
if (_material_cache.has(hash)) {
|
||||
@ -198,7 +217,7 @@ void TerrainLibraryMergerPCM::_material_cache_unref(const int key) {
|
||||
// This is needed, because when duplicating materials the RenderingServer apparently
|
||||
// needs synchronization with the main thread. So if _material_cache_unref holds the mutex
|
||||
// and is duplicating the materials, trying to get the lock from the main thread will deadlock
|
||||
// the game. This can happen when chungs are spawned and despawned really fast.
|
||||
// the game. This can happen when chunks are spawned and despawned really fast.
|
||||
// E.g. when flying around in the editor.
|
||||
MessageQueue::get_singleton()->push_call(this, "_material_cache_unref", key, 1);
|
||||
return;
|
||||
@ -237,6 +256,12 @@ void TerrainLibraryMergerPCM::_liquid_material_cache_get_key(Ref<TerrainChunk> c
|
||||
return;
|
||||
}
|
||||
|
||||
int old_key = 0;
|
||||
|
||||
if (chunk->liquid_material_cache_key_has()) {
|
||||
old_key = chunk->liquid_material_cache_key_get();
|
||||
}
|
||||
|
||||
Vector<uint8_t> surfaces;
|
||||
|
||||
uint32_t size = chunk->get_data_size();
|
||||
@ -266,6 +291,10 @@ void TerrainLibraryMergerPCM::_liquid_material_cache_get_key(Ref<TerrainChunk> c
|
||||
chunk->liquid_material_cache_key_set(0);
|
||||
chunk->liquid_material_cache_key_has_set(false);
|
||||
|
||||
if (old_key != 0) {
|
||||
liquid_material_cache_unref(old_key);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -279,9 +308,18 @@ void TerrainLibraryMergerPCM::_liquid_material_cache_get_key(Ref<TerrainChunk> c
|
||||
|
||||
int hash = static_cast<int>(hstr.hash());
|
||||
|
||||
if (old_key != 0 && old_key == hash) {
|
||||
chunk->liquid_material_cache_key_invalid_set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
chunk->liquid_material_cache_key_set(hash);
|
||||
chunk->liquid_material_cache_key_has_set(true);
|
||||
|
||||
if (old_key != 0) {
|
||||
liquid_material_cache_unref(old_key);
|
||||
}
|
||||
|
||||
_liquid_material_cache_mutex.lock();
|
||||
|
||||
if (_liquid_material_cache.has(hash)) {
|
||||
@ -397,6 +435,12 @@ void TerrainLibraryMergerPCM::_liquid_material_cache_unref(const int key) {
|
||||
|
||||
//Props
|
||||
void TerrainLibraryMergerPCM::_prop_material_cache_get_key(Ref<TerrainChunk> chunk) {
|
||||
int old_key = 0;
|
||||
|
||||
if (chunk->prop_material_cache_key_has()) {
|
||||
old_key = chunk->prop_material_cache_key_get();
|
||||
}
|
||||
|
||||
Vector<uint64_t> props;
|
||||
|
||||
/*
|
||||
@ -454,6 +498,10 @@ void TerrainLibraryMergerPCM::_prop_material_cache_get_key(Ref<TerrainChunk> chu
|
||||
chunk->prop_material_cache_key_set(0);
|
||||
chunk->prop_material_cache_key_has_set(false);
|
||||
|
||||
if (old_key != 0) {
|
||||
prop_material_cache_unref(old_key);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -467,9 +515,18 @@ void TerrainLibraryMergerPCM::_prop_material_cache_get_key(Ref<TerrainChunk> chu
|
||||
|
||||
int hash = static_cast<int>(hstr.hash());
|
||||
|
||||
if (old_key != 0 && old_key == hash) {
|
||||
chunk->prop_material_cache_key_invalid_set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
chunk->prop_material_cache_key_set(hash);
|
||||
chunk->prop_material_cache_key_has_set(true);
|
||||
|
||||
if (old_key != 0) {
|
||||
prop_material_cache_unref(old_key);
|
||||
}
|
||||
|
||||
_prop_material_cache_mutex.lock();
|
||||
|
||||
if (_prop_material_cache.has(hash)) {
|
||||
|
@ -354,7 +354,7 @@ void TerrainPropJob::phase_setup() {
|
||||
}
|
||||
|
||||
if (library->supports_caching()) {
|
||||
if (!_chunk->prop_material_cache_key_has()) {
|
||||
if (!_chunk->prop_material_cache_key_has() || _chunk->prop_material_cache_key_invalid_get()) {
|
||||
library->prop_material_cache_get_key(_chunk);
|
||||
|
||||
if (!_chunk->prop_material_cache_key_has()) {
|
||||
@ -366,13 +366,6 @@ void TerrainPropJob::phase_setup() {
|
||||
|
||||
Ref<TerrainMaterialCache> cache = library->prop_material_cache_get(_chunk->prop_material_cache_key_get());
|
||||
|
||||
//Note: without threadpool and threading none of this can happen, as cache will get initialized the first time a thread requests it!
|
||||
while (!cache->get_initialized()) {
|
||||
//Means it's currently merging the atlases on a different thread.
|
||||
//Let's just wait
|
||||
OS::get_singleton()->delay_usec(100);
|
||||
}
|
||||
|
||||
#ifdef MODULE_MESH_DATA_RESOURCE_ENABLED
|
||||
for (int i = 0; i < _chunk->mesh_data_resource_get_count(); ++i) {
|
||||
Ref<Texture> tex = _chunk->mesh_data_resource_get_texture(i);
|
||||
|
@ -113,34 +113,12 @@ void TerrainTerrainJob::phase_library_setup() {
|
||||
}
|
||||
|
||||
if (lib->supports_caching()) {
|
||||
if (!_chunk->material_cache_key_has()) {
|
||||
if (!_chunk->material_cache_key_has() || _chunk->material_cache_key_invalid_get()) {
|
||||
lib->material_cache_get_key(_chunk);
|
||||
} else {
|
||||
Ref<TerrainMaterialCache> cache = lib->material_cache_get(_chunk->material_cache_key_get());
|
||||
|
||||
if (cache.is_valid()) {
|
||||
//Note: without threadpool and threading none of this can happen, as cache will get initialized the first time a thread requests it!
|
||||
while (!cache->get_initialized()) {
|
||||
//Means it's currently merging the atlases on a different thread.
|
||||
//Let's just wait
|
||||
OS::get_singleton()->delay_usec(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_chunk->liquid_material_cache_key_has()) {
|
||||
if (!_chunk->liquid_material_cache_key_has() || _chunk->liquid_material_cache_key_invalid_get()) {
|
||||
lib->liquid_material_cache_get_key(_chunk);
|
||||
} else {
|
||||
Ref<TerrainMaterialCache> cache = lib->liquid_material_cache_get(_chunk->liquid_material_cache_key_get());
|
||||
|
||||
if (cache.is_valid()) {
|
||||
//Note: without threadpool and threading none of this can happen, as cache will get initialized the first time a thread requests it!
|
||||
while (!cache->get_initialized()) {
|
||||
//Means it's currently merging the atlases on a different thread.
|
||||
//Let's just wait
|
||||
OS::get_singleton()->delay_usec(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,6 +200,8 @@ int TerrainChunk::material_cache_key_get() const {
|
||||
}
|
||||
void TerrainChunk::material_cache_key_set(const int value) {
|
||||
_material_cache_key = value;
|
||||
|
||||
_material_cache_key_invalid = false;
|
||||
}
|
||||
|
||||
bool TerrainChunk::material_cache_key_has() const {
|
||||
@ -209,11 +211,20 @@ void TerrainChunk::material_cache_key_has_set(const bool value) {
|
||||
_material_cache_key_has = value;
|
||||
}
|
||||
|
||||
bool TerrainChunk::material_cache_key_invalid_get() const {
|
||||
return _material_cache_key_invalid;
|
||||
}
|
||||
void TerrainChunk::material_cache_key_invalid_set(const bool value) {
|
||||
_material_cache_key_invalid = value;
|
||||
}
|
||||
|
||||
int TerrainChunk::liquid_material_cache_key_get() const {
|
||||
return _liquid_material_cache_key;
|
||||
}
|
||||
void TerrainChunk::liquid_material_cache_key_set(const int value) {
|
||||
_liquid_material_cache_key = value;
|
||||
|
||||
_liquid_material_cache_key_invalid = false;
|
||||
}
|
||||
|
||||
bool TerrainChunk::liquid_material_cache_key_has() const {
|
||||
@ -223,11 +234,20 @@ void TerrainChunk::liquid_material_cache_key_has_set(const bool value) {
|
||||
_liquid_material_cache_key_has = value;
|
||||
}
|
||||
|
||||
bool TerrainChunk::liquid_material_cache_key_invalid_get() const {
|
||||
return _liquid_material_cache_key_invalid;
|
||||
}
|
||||
void TerrainChunk::liquid_material_cache_key_invalid_set(const bool value) {
|
||||
_liquid_material_cache_key_invalid = value;
|
||||
}
|
||||
|
||||
int TerrainChunk::prop_material_cache_key_get() const {
|
||||
return _prop_material_cache_key;
|
||||
}
|
||||
void TerrainChunk::prop_material_cache_key_set(const int value) {
|
||||
_prop_material_cache_key = value;
|
||||
|
||||
_prop_material_cache_key_invalid = false;
|
||||
}
|
||||
|
||||
bool TerrainChunk::prop_material_cache_key_has() const {
|
||||
@ -237,6 +257,13 @@ void TerrainChunk::prop_material_cache_key_has_set(const bool value) {
|
||||
_prop_material_cache_key_has = value;
|
||||
}
|
||||
|
||||
bool TerrainChunk::prop_material_cache_key_invalid_get() const {
|
||||
return _prop_material_cache_key_invalid;
|
||||
}
|
||||
void TerrainChunk::prop_material_cache_key_invalid_set(const bool value) {
|
||||
_prop_material_cache_key_invalid = value;
|
||||
}
|
||||
|
||||
Ref<TerrainLibrary> TerrainChunk::get_library() {
|
||||
return _library;
|
||||
}
|
||||
@ -1150,12 +1177,15 @@ TerrainChunk::TerrainChunk() {
|
||||
|
||||
_material_cache_key = 0;
|
||||
_material_cache_key_has = false;
|
||||
_material_cache_key_invalid = false;
|
||||
|
||||
_liquid_material_cache_key = 0;
|
||||
_liquid_material_cache_key_has = false;
|
||||
_liquid_material_cache_key_invalid = false;
|
||||
|
||||
_prop_material_cache_key = 0;
|
||||
_prop_material_cache_key_has = false;
|
||||
_prop_material_cache_key_invalid = false;
|
||||
|
||||
_current_job = -1;
|
||||
|
||||
@ -1464,6 +1494,10 @@ void TerrainChunk::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("material_cache_key_has_set"), &TerrainChunk::material_cache_key_has_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "material_cache_key_has", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "material_cache_key_has_set", "material_cache_key_has_get");
|
||||
|
||||
ClassDB::bind_method(D_METHOD("material_cache_key_invalid_get"), &TerrainChunk::material_cache_key_invalid_get);
|
||||
ClassDB::bind_method(D_METHOD("material_cache_key_invalid_set"), &TerrainChunk::material_cache_key_invalid_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "material_cache_key_invalid", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "material_cache_key_invalid_set", "material_cache_key_invalid_get");
|
||||
|
||||
ClassDB::bind_method(D_METHOD("liquid_material_cache_key_get"), &TerrainChunk::liquid_material_cache_key_get);
|
||||
ClassDB::bind_method(D_METHOD("liquid_material_cache_key_set"), &TerrainChunk::liquid_material_cache_key_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "liquid_material_cache_key", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "liquid_material_cache_key_set", "liquid_material_cache_key_get");
|
||||
@ -1472,6 +1506,10 @@ void TerrainChunk::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("liquid_material_cache_key_has_set"), &TerrainChunk::liquid_material_cache_key_has_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "liquid_material_cache_key_has", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "liquid_material_cache_key_has_set", "liquid_material_cache_key_has_get");
|
||||
|
||||
ClassDB::bind_method(D_METHOD("liquid_material_cache_key_invalid_get"), &TerrainChunk::liquid_material_cache_key_invalid_get);
|
||||
ClassDB::bind_method(D_METHOD("liquid_material_cache_key_invalid_set"), &TerrainChunk::liquid_material_cache_key_invalid_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "liquid_material_cache_key_invalid", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "liquid_material_cache_key_invalid_set", "liquid_material_cache_key_invalid_get");
|
||||
|
||||
ClassDB::bind_method(D_METHOD("prop_material_cache_key_get"), &TerrainChunk::prop_material_cache_key_get);
|
||||
ClassDB::bind_method(D_METHOD("prop_material_cache_key_set"), &TerrainChunk::prop_material_cache_key_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "prop_material_cache_key", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "prop_material_cache_key_set", "prop_material_cache_key_get");
|
||||
@ -1480,6 +1518,10 @@ void TerrainChunk::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("prop_material_cache_key_has_set"), &TerrainChunk::prop_material_cache_key_has_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "prop_material_cache_key_has", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "prop_material_cache_key_has_set", "prop_material_cache_key_has_get");
|
||||
|
||||
ClassDB::bind_method(D_METHOD("prop_material_cache_key_invalid_get"), &TerrainChunk::prop_material_cache_key_invalid_get);
|
||||
ClassDB::bind_method(D_METHOD("prop_material_cache_key_invalid_set"), &TerrainChunk::prop_material_cache_key_invalid_set);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "prop_material_cache_key_invalid", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "prop_material_cache_key_invalid_set", "prop_material_cache_key_invalid_get");
|
||||
|
||||
ClassDB::bind_method(D_METHOD("get_library"), &TerrainChunk::get_library);
|
||||
ClassDB::bind_method(D_METHOD("set_library", "value"), &TerrainChunk::set_library);
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "library", PROPERTY_HINT_RESOURCE_TYPE, "TerrainLibrary"), "set_library", "get_library");
|
||||
|
@ -140,18 +140,27 @@ public:
|
||||
bool material_cache_key_has() const;
|
||||
void material_cache_key_has_set(const bool value);
|
||||
|
||||
bool material_cache_key_invalid_get() const;
|
||||
void material_cache_key_invalid_set(const bool value);
|
||||
|
||||
int liquid_material_cache_key_get() const;
|
||||
void liquid_material_cache_key_set(const int value);
|
||||
|
||||
bool liquid_material_cache_key_has() const;
|
||||
void liquid_material_cache_key_has_set(const bool value);
|
||||
|
||||
bool liquid_material_cache_key_invalid_get() const;
|
||||
void liquid_material_cache_key_invalid_set(const bool value);
|
||||
|
||||
int prop_material_cache_key_get() const;
|
||||
void prop_material_cache_key_set(const int value);
|
||||
|
||||
bool prop_material_cache_key_has() const;
|
||||
void prop_material_cache_key_has_set(const bool value);
|
||||
|
||||
bool prop_material_cache_key_invalid_get() const;
|
||||
void prop_material_cache_key_invalid_set(const bool value);
|
||||
|
||||
Ref<TerrainLibrary> get_library();
|
||||
void set_library(const Ref<TerrainLibrary> &value);
|
||||
|
||||
@ -381,12 +390,15 @@ protected:
|
||||
|
||||
int _material_cache_key;
|
||||
bool _material_cache_key_has;
|
||||
bool _material_cache_key_invalid;
|
||||
|
||||
int _liquid_material_cache_key;
|
||||
bool _liquid_material_cache_key_has;
|
||||
bool _liquid_material_cache_key_invalid;
|
||||
|
||||
int _prop_material_cache_key;
|
||||
bool _prop_material_cache_key_has;
|
||||
bool _prop_material_cache_key_invalid;
|
||||
|
||||
float _world_height;
|
||||
|
||||
|
@ -1037,7 +1037,7 @@ Ref<TerrainChunk> TerrainWorld::get_or_create_chunk_at_world_data_position(const
|
||||
return chunk_get_or_create(x, z);
|
||||
}
|
||||
|
||||
void TerrainWorld::set_voxels_at_world_data_position(const Array &p_data, const int p_channel_index, const bool p_immediate_build, const bool p_allow_creating_chunks) {
|
||||
void TerrainWorld::set_voxels_at_world_data_position(const Array &p_data, const int p_channel_index, const bool p_immediate_build, const bool p_allow_creating_chunks, const bool p_invalidate_texture_caches) {
|
||||
ERR_FAIL_COND(p_data.size() % 2 != 0);
|
||||
|
||||
// TODO rework this so it works directly with ints.
|
||||
@ -1143,6 +1143,11 @@ void TerrainWorld::set_voxels_at_world_data_position(const Array &p_data, const
|
||||
for (HashSet<Ref<TerrainChunk>>::Iterator iter = chunks_to_rebuild.begin(); iter.valid(); iter.next()) {
|
||||
Ref<TerrainChunk> chunk = iter.key();
|
||||
|
||||
if (p_invalidate_texture_caches) {
|
||||
chunk->material_cache_key_invalid_set(true);
|
||||
chunk->liquid_material_cache_key_invalid_set(true);
|
||||
}
|
||||
|
||||
if (p_immediate_build) {
|
||||
chunk->build_immediate();
|
||||
} else {
|
||||
@ -1527,7 +1532,7 @@ void TerrainWorld::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_voxel_at_world_data_position", "world_data_position", "data", "channel_index", "rebuild", "allow_creating_chunks "), &TerrainWorld::set_voxel_at_world_data_position, DEFVAL(true), DEFVAL(true));
|
||||
ClassDB::bind_method(D_METHOD("get_chunk_at_world_data_position", "world_data_position"), &TerrainWorld::get_chunk_at_world_data_position);
|
||||
ClassDB::bind_method(D_METHOD("get_or_create_chunk_at_world_data_position", "world_data_position"), &TerrainWorld::get_or_create_chunk_at_world_data_position);
|
||||
ClassDB::bind_method(D_METHOD("set_voxels_at_world_data_position", "data", "channel_index", "immediate_build", "allow_creating_chunks"), &TerrainWorld::set_voxels_at_world_data_position, DEFVAL(false), DEFVAL(true));
|
||||
ClassDB::bind_method(D_METHOD("set_voxels_at_world_data_position", "data", "channel_index", "immediate_build", "allow_creating_chunks", "invalidate_texture_caches"), &TerrainWorld::set_voxels_at_world_data_position, DEFVAL(false), DEFVAL(true), DEFVAL(true));
|
||||
|
||||
BIND_VMETHOD(MethodInfo(PropertyInfo(Variant::INT, "ret"), "_get_channel_index_info", PropertyInfo(Variant::INT, "channel_type", PROPERTY_HINT_ENUM, BINDING_STRING_CHANNEL_TYPE_INFO)));
|
||||
|
||||
|
@ -198,7 +198,7 @@ public:
|
||||
void set_voxel_at_world_data_position(const Vector2i &world_data_position, const uint8_t data, const int channel_index, const bool p_immediate_build = true, const bool allow_creating_chunks = true);
|
||||
Ref<TerrainChunk> get_chunk_at_world_data_position(const Vector2i &world_data_position);
|
||||
Ref<TerrainChunk> get_or_create_chunk_at_world_data_position(const Vector2i &world_data_position);
|
||||
void set_voxels_at_world_data_position(const Array &p_data, const int p_channel_index, const bool p_immediate_build = false, const bool p_allow_creating_chunks = true);
|
||||
void set_voxels_at_world_data_position(const Array &p_data, const int p_channel_index, const bool p_immediate_build = false, const bool p_allow_creating_chunks = true, const bool p_invalidate_texture_caches = true);
|
||||
|
||||
int get_channel_index_info(const ChannelTypeInfo channel_type);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user