Show bounds and octrees of VoxelLodTerrain with debug drawing
This commit is contained in:
parent
e70f87e4a7
commit
d158a92f57
1
SCsub
1
SCsub
@ -20,6 +20,7 @@ files = [
|
||||
"server/*.cpp",
|
||||
"math/*.cpp",
|
||||
"edition/*.cpp",
|
||||
"editor/*.cpp",
|
||||
"editor/graph/*.cpp",
|
||||
"editor/terrain/*.cpp",
|
||||
"thirdparty/lz4/*.c"
|
||||
|
@ -69,6 +69,11 @@ void VoxelTerrainEditorPlugin::set_node(Node *node) {
|
||||
// Also moving the node around in the tree triggers exit/enter so have to listen for both.
|
||||
_node->disconnect("tree_entered", this, "_on_terrain_tree_entered");
|
||||
_node->disconnect("tree_exited", this, "_on_terrain_tree_exited");
|
||||
|
||||
VoxelLodTerrain *vlt = Object::cast_to<VoxelLodTerrain>(_node);
|
||||
if (vlt != nullptr) {
|
||||
vlt->set_show_gizmos(false);
|
||||
}
|
||||
}
|
||||
|
||||
_node = node;
|
||||
@ -76,12 +81,27 @@ void VoxelTerrainEditorPlugin::set_node(Node *node) {
|
||||
if (_node != nullptr) {
|
||||
_node->connect("tree_entered", this, "_on_terrain_tree_entered", varray(_node));
|
||||
_node->connect("tree_exited", this, "_on_terrain_tree_exited", varray(_node));
|
||||
|
||||
VoxelLodTerrain *vlt = Object::cast_to<VoxelLodTerrain>(_node);
|
||||
if (vlt != nullptr) {
|
||||
vlt->set_show_gizmos(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelTerrainEditorPlugin::make_visible(bool visible) {
|
||||
_restart_stream_button->set_visible(visible);
|
||||
// Can't use `make_visible(false)` to reset our reference to the node,
|
||||
|
||||
if (_node != nullptr) {
|
||||
VoxelLodTerrain *vlt = Object::cast_to<VoxelLodTerrain>(_node);
|
||||
if (vlt != nullptr) {
|
||||
vlt->set_show_gizmos(visible);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO There are deselection problems I cannot fix cleanly!
|
||||
|
||||
// Can't use `make_visible(false)` to reset our reference to the node or reset gizmos,
|
||||
// because of https://github.com/godotengine/godot/issues/40166
|
||||
// So we'll need to check if _node is null all over the place
|
||||
}
|
||||
@ -99,11 +119,11 @@ void VoxelTerrainEditorPlugin::_on_restart_stream_button_pressed() {
|
||||
}
|
||||
|
||||
void VoxelTerrainEditorPlugin::_on_terrain_tree_entered(Node *node) {
|
||||
// If the node exited the tree because it was deleted, signals we connected should automatically disconnect.
|
||||
_node = node;
|
||||
}
|
||||
|
||||
void VoxelTerrainEditorPlugin::_on_terrain_tree_exited(Node *node) {
|
||||
// If the node exited the tree because it was deleted, signals we connected should automatically disconnect.
|
||||
_node = nullptr;
|
||||
}
|
||||
|
||||
|
157
editor/voxel_debug.cpp
Normal file
157
editor/voxel_debug.cpp
Normal file
@ -0,0 +1,157 @@
|
||||
#include "voxel_debug.h"
|
||||
#include "../util/direct_mesh_instance.h"
|
||||
#include "../util/fixed_array.h"
|
||||
#include "../util/utility.h"
|
||||
#include <scene/resources/mesh.h>
|
||||
|
||||
namespace VoxelDebug {
|
||||
|
||||
FixedArray<Ref<Mesh>, ID_COUNT> g_wirecubes;
|
||||
bool g_finalized = false;
|
||||
|
||||
template <typename T>
|
||||
void raw_copy_to(PoolVector<T> &dst, const T *src, unsigned int count) {
|
||||
dst.resize(count);
|
||||
PoolVector<T>::Write w = dst.write();
|
||||
memcpy(w.ptr(), src, count * sizeof(T));
|
||||
}
|
||||
|
||||
static Color get_color(ColorID id) {
|
||||
switch (id) {
|
||||
case ID_VOXEL_BOUNDS:
|
||||
return Color(1, 1, 1);
|
||||
case ID_OCTREE_BOUNDS:
|
||||
return Color(0.5, 0.5, 0.5);
|
||||
default:
|
||||
CRASH_NOW_MSG("Unexpected index");
|
||||
}
|
||||
return Color();
|
||||
}
|
||||
|
||||
Ref<Mesh> get_wirecube(ColorID id) {
|
||||
CRASH_COND(g_finalized);
|
||||
|
||||
Ref<Mesh> &wirecube = g_wirecubes[id];
|
||||
|
||||
if (wirecube.is_null()) {
|
||||
const Vector3 positions_raw[] = {
|
||||
Vector3(0, 0, 0),
|
||||
Vector3(1, 0, 0),
|
||||
Vector3(1, 0, 1),
|
||||
Vector3(0, 0, 1),
|
||||
Vector3(0, 1, 0),
|
||||
Vector3(1, 1, 0),
|
||||
Vector3(1, 1, 1),
|
||||
Vector3(0, 1, 1)
|
||||
};
|
||||
PoolVector3Array positions;
|
||||
raw_copy_to(positions, positions_raw, 8);
|
||||
|
||||
Color white(1.0, 1.0, 1.0);
|
||||
PoolColorArray colors;
|
||||
colors.resize(positions.size());
|
||||
{
|
||||
PoolColorArray::Write w = colors.write();
|
||||
for (int i = 0; i < colors.size(); ++i) {
|
||||
w[i] = white;
|
||||
}
|
||||
}
|
||||
|
||||
const int indices_raw[] = {
|
||||
0, 1,
|
||||
1, 2,
|
||||
2, 3,
|
||||
3, 0,
|
||||
|
||||
4, 5,
|
||||
5, 6,
|
||||
6, 7,
|
||||
7, 4,
|
||||
|
||||
0, 4,
|
||||
1, 5,
|
||||
2, 6,
|
||||
3, 7
|
||||
};
|
||||
PoolIntArray indices;
|
||||
raw_copy_to(indices, indices_raw, 24);
|
||||
|
||||
Array arrays;
|
||||
arrays.resize(Mesh::ARRAY_MAX);
|
||||
arrays[Mesh::ARRAY_VERTEX] = positions;
|
||||
arrays[Mesh::ARRAY_COLOR] = colors;
|
||||
arrays[Mesh::ARRAY_INDEX] = indices;
|
||||
Ref<ArrayMesh> mesh = memnew(ArrayMesh);
|
||||
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_LINES, arrays);
|
||||
|
||||
Ref<SpatialMaterial> mat;
|
||||
mat.instance();
|
||||
mat->set_albedo(get_color(id));
|
||||
mat->set_flag(SpatialMaterial::FLAG_UNSHADED, true);
|
||||
mesh->surface_set_material(0, mat);
|
||||
|
||||
wirecube = mesh;
|
||||
}
|
||||
|
||||
return wirecube;
|
||||
}
|
||||
|
||||
void free_resources() {
|
||||
for (unsigned int i = 0; i < g_wirecubes.size(); ++i) {
|
||||
g_wirecubes[i].unref();
|
||||
}
|
||||
g_finalized = true;
|
||||
}
|
||||
|
||||
DebugRenderer::~DebugRenderer() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void DebugRenderer::clear() {
|
||||
for (auto it = _mesh_instances.begin(); it != _mesh_instances.end(); ++it) {
|
||||
memdelete(*it);
|
||||
}
|
||||
_mesh_instances.clear();
|
||||
}
|
||||
|
||||
void DebugRenderer::set_world(World *world) {
|
||||
_world = world;
|
||||
for (auto it = _mesh_instances.begin(); it != _mesh_instances.end(); ++it) {
|
||||
(*it)->set_world(world);
|
||||
}
|
||||
}
|
||||
|
||||
void DebugRenderer::begin() {
|
||||
CRASH_COND(_inside_block);
|
||||
CRASH_COND(_world == nullptr);
|
||||
_current = 0;
|
||||
_inside_block = true;
|
||||
}
|
||||
|
||||
void DebugRenderer::draw_box(Transform t, ColorID color) {
|
||||
DirectMeshInstance *mi;
|
||||
if (_current >= _mesh_instances.size()) {
|
||||
mi = memnew(DirectMeshInstance);
|
||||
mi->create();
|
||||
mi->set_world(_world);
|
||||
_mesh_instances.push_back(mi);
|
||||
} else {
|
||||
mi = _mesh_instances[_current];
|
||||
}
|
||||
|
||||
mi->set_mesh(get_wirecube(color));
|
||||
mi->set_transform(t);
|
||||
|
||||
++_current;
|
||||
}
|
||||
|
||||
void DebugRenderer::end() {
|
||||
CRASH_COND(!_inside_block);
|
||||
for (unsigned int i = _current; i < _mesh_instances.size(); ++i) {
|
||||
DirectMeshInstance *mi = _mesh_instances[i];
|
||||
mi->set_visible(false);
|
||||
}
|
||||
_inside_block = false;
|
||||
}
|
||||
|
||||
} // namespace VoxelDebug
|
42
editor/voxel_debug.h
Normal file
42
editor/voxel_debug.h
Normal file
@ -0,0 +1,42 @@
|
||||
#ifndef VOXEL_DEBUG_H
|
||||
#define VOXEL_DEBUG_H
|
||||
|
||||
#include <core/reference.h>
|
||||
#include <vector>
|
||||
|
||||
class Mesh;
|
||||
class DirectMeshInstance;
|
||||
class World;
|
||||
|
||||
namespace VoxelDebug {
|
||||
|
||||
enum ColorID {
|
||||
ID_VOXEL_BOUNDS = 0,
|
||||
ID_OCTREE_BOUNDS,
|
||||
ID_COUNT
|
||||
};
|
||||
|
||||
Ref<Mesh> get_wirecube(ColorID id);
|
||||
void free_resources();
|
||||
|
||||
class DebugRenderer {
|
||||
public:
|
||||
~DebugRenderer();
|
||||
|
||||
void set_world(World *world);
|
||||
|
||||
void begin();
|
||||
void draw_box(Transform t, ColorID color);
|
||||
void end();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
std::vector<DirectMeshInstance *> _mesh_instances;
|
||||
unsigned int _current = 0;
|
||||
bool _inside_block = false;
|
||||
World *_world = nullptr;
|
||||
};
|
||||
|
||||
} // namespace VoxelDebug
|
||||
|
||||
#endif // VOXEL_DEBUG_H
|
@ -695,7 +695,8 @@ float VoxelGraphRuntime::generate_single(const Vector3i &position) {
|
||||
|
||||
case VoxelGeneratorGraph::NODE_IMAGE_2D: {
|
||||
const PNodeImage2D &n = read<PNodeImage2D>(_program, pc);
|
||||
// TODO Not great, but in Godot 4.0 we won't need to lock anymore. Otherwise, need to do it in a pre-run and post-run
|
||||
// TODO Not great, but in Godot 4.0 we won't need to lock anymore.
|
||||
// Otherwise, need to do it in a pre-run and post-run
|
||||
n.p_image->lock();
|
||||
memory[n.a_out] = get_pixel_repeat(*n.p_image, memory[n.a_x], memory[n.a_y]);
|
||||
n.p_image->unlock();
|
||||
|
@ -200,8 +200,8 @@ public:
|
||||
|
||||
inline Rect3i downscaled(int step_size) const {
|
||||
Rect3i o;
|
||||
o.pos = pos.udiv(step_size);
|
||||
Vector3i max_pos = (pos + size - Vector3i(1)).udiv(step_size);
|
||||
o.pos = pos.floordiv(step_size);
|
||||
Vector3i max_pos = (pos + size - Vector3i(1)).floordiv(step_size);
|
||||
o.size = max_pos - o.pos + Vector3i(1);
|
||||
return o;
|
||||
}
|
||||
|
@ -134,10 +134,6 @@ struct Vector3i {
|
||||
::sort_min_max(a.z, b.z);
|
||||
}
|
||||
|
||||
inline Vector3i udiv(int d) const {
|
||||
return Vector3i(::udiv(x, d), ::udiv(y, d), ::udiv(z, d));
|
||||
}
|
||||
|
||||
inline Vector3i udiv(const Vector3i d) const {
|
||||
return Vector3i(::udiv(x, d.x), ::udiv(y, d.y), ::udiv(z, d.z));
|
||||
}
|
||||
|
@ -32,6 +32,10 @@
|
||||
#include "voxel_string_names.h"
|
||||
#include <core/engine.h>
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/voxel_debug.h"
|
||||
#endif
|
||||
|
||||
void register_voxel_types() {
|
||||
VoxelMemoryPool::create_singleton();
|
||||
VoxelStringNames::create_singleton();
|
||||
@ -94,8 +98,6 @@ void register_voxel_types() {
|
||||
PRINT_VERBOSE(String("Size of VoxelBlock: {0}").format(varray((int)sizeof(VoxelBlock))));
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
VoxelDebug::create_debug_box_mesh();
|
||||
|
||||
EditorPlugins::add_by_type<VoxelGraphEditorPlugin>();
|
||||
EditorPlugins::add_by_type<VoxelTerrainEditorPlugin>();
|
||||
#endif
|
||||
@ -122,7 +124,7 @@ void unregister_voxel_types() {
|
||||
// TODO No remove?
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
VoxelDebug::free_debug_box_mesh();
|
||||
VoxelDebug::free_resources();
|
||||
|
||||
// TODO Seriously, no remove?
|
||||
//EditorPlugins::remove_by_type<VoxelGraphEditorPlugin>();
|
||||
|
@ -440,6 +440,11 @@ void VoxelLodTerrain::_notification(int p_what) {
|
||||
});
|
||||
}
|
||||
}
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (is_showing_gizmos()) {
|
||||
_debug_renderer.set_world(is_visible_in_tree() ? world : nullptr);
|
||||
}
|
||||
#endif
|
||||
} break;
|
||||
|
||||
case NOTIFICATION_EXIT_WORLD: {
|
||||
@ -450,6 +455,9 @@ void VoxelLodTerrain::_notification(int p_what) {
|
||||
});
|
||||
}
|
||||
}
|
||||
#ifdef TOOLS_ENABLED
|
||||
_debug_renderer.set_world(nullptr);
|
||||
#endif
|
||||
} break;
|
||||
|
||||
case NOTIFICATION_VISIBILITY_CHANGED: {
|
||||
@ -461,6 +469,11 @@ void VoxelLodTerrain::_notification(int p_what) {
|
||||
});
|
||||
}
|
||||
}
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (is_showing_gizmos()) {
|
||||
_debug_renderer.set_world(is_visible_in_tree() ? *get_world() : nullptr);
|
||||
}
|
||||
#endif
|
||||
} break;
|
||||
|
||||
// TODO Listen for transform changes
|
||||
@ -1154,6 +1167,12 @@ void VoxelLodTerrain::_process() {
|
||||
}
|
||||
|
||||
_stats.time_process_update_responses = profiling_clock.restart();
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (is_showing_gizmos()) {
|
||||
update_gizmos();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void VoxelLodTerrain::flush_pending_lod_edits() {
|
||||
@ -1576,6 +1595,45 @@ Array VoxelLodTerrain::debug_get_octrees() const {
|
||||
return positions;
|
||||
}
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
|
||||
void VoxelLodTerrain::update_gizmos() {
|
||||
VOXEL_PROFILE_SCOPE();
|
||||
|
||||
VoxelDebug::DebugRenderer &dr = _debug_renderer;
|
||||
dr.begin();
|
||||
|
||||
const int octree_size = get_block_size() << (get_lod_count() - 1);
|
||||
for (Map<Vector3i, OctreeItem>::Element *E = _lod_octrees.front(); E; E = E->next()) {
|
||||
Transform t = get_global_transform();
|
||||
t.scale(Vector3(octree_size, octree_size, octree_size));
|
||||
t.translate(E->key().to_vec3());
|
||||
dr.draw_box(t, VoxelDebug::ID_OCTREE_BOUNDS);
|
||||
}
|
||||
|
||||
const float bounds_in_voxels_len = _bounds_in_voxels.size.length();
|
||||
if (bounds_in_voxels_len < 10000) {
|
||||
Transform t = get_global_transform();
|
||||
Vector3 margin = Vector3(1, 1, 1) * bounds_in_voxels_len * 0.0025f;
|
||||
t.scale(_bounds_in_voxels.size.to_vec3() + margin * 2.f);
|
||||
t.origin = _bounds_in_voxels.pos.to_vec3() - margin;
|
||||
dr.draw_box(t, VoxelDebug::ID_VOXEL_BOUNDS);
|
||||
}
|
||||
|
||||
dr.end();
|
||||
}
|
||||
|
||||
void VoxelLodTerrain::set_show_gizmos(bool enable) {
|
||||
_show_gizmos_enabled = enable;
|
||||
if (_show_gizmos_enabled) {
|
||||
_debug_renderer.set_world(is_visible_in_tree() ? *get_world() : nullptr);
|
||||
} else {
|
||||
_debug_renderer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Array VoxelLodTerrain::_b_debug_print_sdf_top_down(Vector3 center, Vector3 extents) const {
|
||||
Array image_array;
|
||||
image_array.resize(get_lod_count());
|
||||
|
@ -2,10 +2,15 @@
|
||||
#define VOXEL_LOD_TERRAIN_HPP
|
||||
|
||||
#include "../server/voxel_server.h"
|
||||
#include "../util/direct_mesh_instance.h"
|
||||
#include "lod_octree.h"
|
||||
#include <core/set.h>
|
||||
#include <scene/3d/spatial.h>
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "../editor/voxel_debug.h"
|
||||
#endif
|
||||
|
||||
class VoxelMap;
|
||||
class VoxelTool;
|
||||
class VoxelStream;
|
||||
@ -53,6 +58,7 @@ public:
|
||||
|
||||
unsigned int get_block_size_pow2() const;
|
||||
void set_block_size_po2(unsigned int p_block_size_po2);
|
||||
unsigned int get_block_size() const;
|
||||
|
||||
// These must be called after an edit
|
||||
void post_edit_area(Rect3i p_box);
|
||||
@ -92,6 +98,11 @@ public:
|
||||
Dictionary debug_get_block_info(Vector3 fbpos, int lod_index) const;
|
||||
Array debug_get_octrees() const;
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
void set_show_gizmos(bool enable);
|
||||
bool is_showing_gizmos() const { return _show_gizmos_enabled; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
@ -99,7 +110,6 @@ protected:
|
||||
void _process();
|
||||
|
||||
private:
|
||||
unsigned int get_block_size() const;
|
||||
Spatial *get_viewer() const;
|
||||
void immerge_block(Vector3i block_pos, int lod_index);
|
||||
|
||||
@ -132,11 +142,15 @@ private:
|
||||
AABB _b_get_voxel_bounds() const;
|
||||
Array _b_debug_print_sdf_top_down(Vector3 center, Vector3 extents) const;
|
||||
|
||||
private:
|
||||
struct OctreeItem {
|
||||
LodOctree octree;
|
||||
};
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
void update_gizmos();
|
||||
#endif
|
||||
|
||||
private:
|
||||
// This terrain type is a sparse grid of octrees.
|
||||
// Indexed by a grid coordinate whose step is the size of the highest-LOD block.
|
||||
// Not using a pointer because Map storage is stable.
|
||||
@ -188,6 +202,10 @@ private:
|
||||
unsigned int _view_distance_voxels = 512;
|
||||
|
||||
bool _run_stream_in_editor = true;
|
||||
#ifdef TOOLS_ENABLED
|
||||
bool _show_gizmos_enabled = false;
|
||||
VoxelDebug::DebugRenderer _debug_renderer;
|
||||
#endif
|
||||
|
||||
Stats _stats;
|
||||
};
|
||||
|
@ -48,7 +48,9 @@ void DirectMeshInstance::set_mesh(Ref<Mesh> mesh) {
|
||||
ERR_FAIL_COND(!_mesh_instance.is_valid());
|
||||
VisualServer &vs = *VisualServer::get_singleton();
|
||||
if (mesh.is_valid()) {
|
||||
vs.instance_set_base(_mesh_instance, mesh->get_rid());
|
||||
if (_mesh != mesh) {
|
||||
vs.instance_set_base(_mesh_instance, mesh->get_rid());
|
||||
}
|
||||
} else {
|
||||
vs.instance_set_base(_mesh_instance, RID());
|
||||
}
|
||||
|
@ -51,55 +51,3 @@ bool try_call_script(const Object *obj, StringName method_name, const Variant **
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#if TOOLS_ENABLED
|
||||
|
||||
namespace VoxelDebug {
|
||||
|
||||
Ref<Mesh> g_debug_box_mesh;
|
||||
|
||||
void create_debug_box_mesh() {
|
||||
PoolVector3Array positions;
|
||||
positions.resize(8);
|
||||
{
|
||||
PoolVector3Array::Write w = positions.write();
|
||||
for (int i = 0; i < positions.size(); ++i) {
|
||||
w[i] = Cube::g_corner_position[i];
|
||||
}
|
||||
}
|
||||
PoolIntArray indices;
|
||||
indices.resize(Cube::EDGE_COUNT * 2);
|
||||
{
|
||||
PoolIntArray::Write w = indices.write();
|
||||
int j = 0;
|
||||
for (int i = 0; i < Cube::EDGE_COUNT; ++i) {
|
||||
w[j++] = Cube::g_edge_corners[i][0];
|
||||
w[j++] = Cube::g_edge_corners[i][1];
|
||||
}
|
||||
}
|
||||
Array arrays;
|
||||
arrays.resize(Mesh::ARRAY_MAX);
|
||||
arrays[Mesh::ARRAY_VERTEX] = positions;
|
||||
arrays[Mesh::ARRAY_INDEX] = indices;
|
||||
Ref<ArrayMesh> mesh;
|
||||
mesh.instance();
|
||||
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_LINES, arrays);
|
||||
Ref<SpatialMaterial> mat;
|
||||
mat.instance();
|
||||
mat->set_albedo(Color(0, 1, 0));
|
||||
mat->set_flag(SpatialMaterial::FLAG_UNSHADED, true);
|
||||
mesh->surface_set_material(0, mat);
|
||||
g_debug_box_mesh = mesh;
|
||||
}
|
||||
|
||||
void free_debug_box_mesh() {
|
||||
g_debug_box_mesh.unref();
|
||||
}
|
||||
|
||||
Ref<Mesh> get_debug_box_mesh() {
|
||||
return g_debug_box_mesh;
|
||||
}
|
||||
|
||||
} // namespace VoxelDebug
|
||||
|
||||
#endif
|
||||
|
@ -229,12 +229,4 @@ inline bool try_call_script(const Object *obj, StringName method_name, Variant a
|
||||
return try_call_script(obj, method_name, args, 3, out_ret);
|
||||
}
|
||||
|
||||
#if TOOLS_ENABLED
|
||||
namespace VoxelDebug {
|
||||
void create_debug_box_mesh();
|
||||
void free_debug_box_mesh();
|
||||
Ref<Mesh> get_debug_box_mesh();
|
||||
} // namespace VoxelDebug
|
||||
#endif
|
||||
|
||||
#endif // HEADER_VOXEL_UTILITY_H
|
||||
|
Loading…
x
Reference in New Issue
Block a user