mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2025-05-18 19:31:17 +03:00
texture viewer
This commit is contained in:
parent
08c8d07d91
commit
5f84617567
@ -47,6 +47,9 @@ public class App
|
|||||||
serviceCollection.AddSingleton(type);
|
serviceCollection.AddSingleton(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton(openGl);
|
||||||
|
serviceCollection.AddSingleton(window);
|
||||||
|
|
||||||
serviceCollection.AddSingleton(new NResExplorerViewModel());
|
serviceCollection.AddSingleton(new NResExplorerViewModel());
|
||||||
serviceCollection.AddSingleton(new TexmExplorerViewModel());
|
serviceCollection.AddSingleton(new TexmExplorerViewModel());
|
||||||
|
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
using ImGuiNET;
|
using System.Numerics;
|
||||||
|
using ImGuiNET;
|
||||||
using NResUI.Abstractions;
|
using NResUI.Abstractions;
|
||||||
using NResUI.Models;
|
using NResUI.Models;
|
||||||
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace NResUI.ImGuiUI;
|
namespace NResUI.ImGuiUI;
|
||||||
|
|
||||||
public class TexmExplorer : IImGuiPanel
|
public class TexmExplorer : IImGuiPanel
|
||||||
{
|
{
|
||||||
private readonly TexmExplorerViewModel _viewModel;
|
private readonly TexmExplorerViewModel _viewModel;
|
||||||
|
private readonly GL _openGl;
|
||||||
|
|
||||||
public TexmExplorer(TexmExplorerViewModel viewModel)
|
public TexmExplorer(TexmExplorerViewModel viewModel, GL openGl)
|
||||||
{
|
{
|
||||||
_viewModel = viewModel;
|
_viewModel = viewModel;
|
||||||
|
_openGl = openGl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnImGuiRender()
|
public void OnImGuiRender()
|
||||||
@ -49,7 +53,7 @@ public class TexmExplorer : IImGuiPanel
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.Text(_viewModel.TexmFile.Header.Stride.ToString());
|
ImGui.Text(_viewModel.TexmFile.Header.Stride.ToString());
|
||||||
|
|
||||||
ImGui.Text("Magic1: ");
|
ImGui.Text("Magic1 (possibly ddsCaps): ");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.Text(_viewModel.TexmFile.Header.Magic1.ToString());
|
ImGui.Text(_viewModel.TexmFile.Header.Magic1.ToString());
|
||||||
|
|
||||||
@ -64,6 +68,45 @@ public class TexmExplorer : IImGuiPanel
|
|||||||
ImGui.Text("IsIndexed: ");
|
ImGui.Text("IsIndexed: ");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.Text(_viewModel.TexmFile.IsIndexed.ToString());
|
ImGui.Text(_viewModel.TexmFile.IsIndexed.ToString());
|
||||||
|
|
||||||
|
ImGui.Checkbox("Включить чёрный фон", ref _viewModel.IsBlackBgEnabled);
|
||||||
|
ImGui.Checkbox("Включить белый фон", ref _viewModel.IsWhiteBgEnabled);
|
||||||
|
|
||||||
|
if (_viewModel.IsWhiteBgEnabled && _viewModel.IsBlackBgEnabled)
|
||||||
|
{
|
||||||
|
_viewModel.IsBlackBgEnabled = false;
|
||||||
|
_viewModel.IsWhiteBgEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewModel.GenerateGlTextures(_openGl);
|
||||||
|
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
for (var index = 0; index < _viewModel.GlTextures.Count; index++)
|
||||||
|
{
|
||||||
|
var glTexture = _viewModel.GlTextures[index];
|
||||||
|
var screenPos = ImGui.GetCursorScreenPos();
|
||||||
|
if (_viewModel.IsBlackBgEnabled)
|
||||||
|
{
|
||||||
|
drawList.AddRectFilled(screenPos, screenPos + new Vector2(glTexture.Width, glTexture.Height), 0xFF000000);
|
||||||
|
}
|
||||||
|
else if (_viewModel.IsWhiteBgEnabled)
|
||||||
|
{
|
||||||
|
drawList.AddRectFilled(screenPos, screenPos + new Vector2(glTexture.Width, glTexture.Height), 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Image((IntPtr) glTexture.GlTexture, new Vector2(glTexture.Width, glTexture.Height));
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
var mousePos = ImGui.GetMousePos();
|
||||||
|
var relativePos = mousePos - screenPos;
|
||||||
|
|
||||||
|
ImGui.Text("Hovering over: ");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Text(relativePos.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using TexmLib;
|
using Silk.NET.OpenGL;
|
||||||
|
using TexmLib;
|
||||||
|
|
||||||
namespace NResUI.Models;
|
namespace NResUI.Models;
|
||||||
|
|
||||||
@ -8,9 +9,15 @@ public class TexmExplorerViewModel
|
|||||||
public string? Error { get; set; }
|
public string? Error { get; set; }
|
||||||
|
|
||||||
public TexmFile? TexmFile { get; set; }
|
public TexmFile? TexmFile { get; set; }
|
||||||
|
|
||||||
public string? Path { get; set; }
|
public string? Path { get; set; }
|
||||||
|
public List<OpenGlTexture> GlTextures { get; set; } = [];
|
||||||
|
|
||||||
|
private bool _glTexturesDirty = false;
|
||||||
|
public bool IsWhiteBgEnabled;
|
||||||
|
|
||||||
|
public bool IsBlackBgEnabled;
|
||||||
|
|
||||||
public void SetParseResult(TexmParseResult result, string path)
|
public void SetParseResult(TexmParseResult result, string path)
|
||||||
{
|
{
|
||||||
Error = result.Error;
|
Error = result.Error;
|
||||||
@ -22,6 +29,38 @@ public class TexmExplorerViewModel
|
|||||||
|
|
||||||
TexmFile = result.TexmFile;
|
TexmFile = result.TexmFile;
|
||||||
Path = path;
|
Path = path;
|
||||||
|
_glTexturesDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сгенерировать OpenGL текстуры из всех мипмапов Texm файла
|
||||||
|
/// </summary>
|
||||||
|
public void GenerateGlTextures(GL gl)
|
||||||
|
{
|
||||||
|
if (_glTexturesDirty && TexmFile is not null)
|
||||||
|
{
|
||||||
|
foreach (var glTexture in GlTextures)
|
||||||
|
{
|
||||||
|
glTexture.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
GlTextures.Clear();
|
||||||
|
|
||||||
|
for (var i = 0; i < TexmFile!.Header.MipmapCount; i++)
|
||||||
|
{
|
||||||
|
var bytes = TexmFile.GetRgba32BytesFromMipmap(i, out var width, out var height);
|
||||||
|
|
||||||
|
var glTexture = new OpenGlTexture(
|
||||||
|
gl,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bytes
|
||||||
|
);
|
||||||
|
|
||||||
|
GlTextures.Add(glTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
_glTexturesDirty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
152
NResUI/OpenGlTexture.cs
Normal file
152
NResUI/OpenGlTexture.cs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
|
namespace NResUI
|
||||||
|
{
|
||||||
|
public enum TextureCoordinate
|
||||||
|
{
|
||||||
|
S = TextureParameterName.TextureWrapS,
|
||||||
|
T = TextureParameterName.TextureWrapT,
|
||||||
|
R = TextureParameterName.TextureWrapR
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OpenGlTexture : IDisposable
|
||||||
|
{
|
||||||
|
public const SizedInternalFormat Srgb8Alpha8 = (SizedInternalFormat)GLEnum.Srgb8Alpha8;
|
||||||
|
public const SizedInternalFormat Rgb32F = (SizedInternalFormat)GLEnum.Rgb32f;
|
||||||
|
|
||||||
|
public const GLEnum MaxTextureMaxAnisotropy = (GLEnum)0x84FF;
|
||||||
|
|
||||||
|
public static float? MaxAniso;
|
||||||
|
private readonly GL _gl;
|
||||||
|
public readonly string Name;
|
||||||
|
public readonly uint GlTexture;
|
||||||
|
public readonly uint Width, Height;
|
||||||
|
public readonly uint MipmapLevels;
|
||||||
|
public readonly SizedInternalFormat InternalFormat;
|
||||||
|
|
||||||
|
public OpenGlTexture(GL gl, int width, int height, IntPtr data, bool generateMipmaps = false, bool srgb = false)
|
||||||
|
{
|
||||||
|
_gl = gl;
|
||||||
|
MaxAniso ??= gl.GetFloat(MaxTextureMaxAnisotropy);
|
||||||
|
Width = (uint)width;
|
||||||
|
Height = (uint)height;
|
||||||
|
InternalFormat = srgb ? Srgb8Alpha8 : SizedInternalFormat.Rgba8;
|
||||||
|
MipmapLevels = (uint)(generateMipmaps == false ? 1 : (int)Math.Floor(Math.Log(Math.Max(Width, Height), 2)));
|
||||||
|
|
||||||
|
GlTexture = _gl.GenTexture();
|
||||||
|
Bind();
|
||||||
|
|
||||||
|
_gl.TexStorage2D(GLEnum.Texture2D, MipmapLevels, InternalFormat, Width, Height);
|
||||||
|
_gl.TexSubImage2D(GLEnum.Texture2D, 0, 0, 0, Width, Height, PixelFormat.Bgra, PixelType.UnsignedByte, data);
|
||||||
|
|
||||||
|
if (generateMipmaps) _gl.GenerateTextureMipmap(GlTexture);
|
||||||
|
|
||||||
|
SetWrap(TextureCoordinate.S, TextureWrapMode.Repeat);
|
||||||
|
SetWrap(TextureCoordinate.T, TextureWrapMode.Repeat);
|
||||||
|
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLevel, MipmapLevels - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenGlTexture(GL gl, int width, int height, byte[] data, bool generateMipmaps = false, bool srgb = false)
|
||||||
|
{
|
||||||
|
_gl = gl;
|
||||||
|
MaxAniso ??= gl.GetFloat(MaxTextureMaxAnisotropy);
|
||||||
|
Width = (uint)width;
|
||||||
|
Height = (uint)height;
|
||||||
|
InternalFormat = srgb ? Srgb8Alpha8 : SizedInternalFormat.Rgba8;
|
||||||
|
MipmapLevels = (uint)(generateMipmaps == false ? 1 : (int)Math.Floor(Math.Log(Math.Max(Width, Height), 2)));
|
||||||
|
|
||||||
|
GlTexture = _gl.GenTexture();
|
||||||
|
Bind();
|
||||||
|
|
||||||
|
_gl.TexStorage2D(GLEnum.Texture2D, MipmapLevels, InternalFormat, Width, Height);
|
||||||
|
_gl.TexSubImage2D(GLEnum.Texture2D, 0, 0, 0, Width, Height, PixelFormat.Bgra, PixelType.UnsignedByte, Unsafe.AsRef(ref data[0]));
|
||||||
|
|
||||||
|
if (generateMipmaps) _gl.GenerateTextureMipmap(GlTexture);
|
||||||
|
|
||||||
|
SetWrap(TextureCoordinate.S, TextureWrapMode.Repeat);
|
||||||
|
SetWrap(TextureCoordinate.T, TextureWrapMode.Repeat);
|
||||||
|
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLevel, MipmapLevels - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenGlTexture(GL gl, int width, int height, byte[] data, PixelFormat pixelFormat, bool generateMipmaps = false, bool srgb = false)
|
||||||
|
{
|
||||||
|
_gl = gl;
|
||||||
|
MaxAniso ??= gl.GetFloat(MaxTextureMaxAnisotropy);
|
||||||
|
Width = (uint)width;
|
||||||
|
Height = (uint)height;
|
||||||
|
InternalFormat = srgb ? Srgb8Alpha8 : SizedInternalFormat.Rgba8;
|
||||||
|
MipmapLevels = (uint)(generateMipmaps == false ? 1 : (int)Math.Floor(Math.Log(Math.Max(Width, Height), 2)));
|
||||||
|
|
||||||
|
GlTexture = _gl.GenTexture();
|
||||||
|
Bind();
|
||||||
|
|
||||||
|
_gl.TexStorage2D(GLEnum.Texture2D, MipmapLevels, InternalFormat, Width, Height);
|
||||||
|
_gl.TexSubImage2D(GLEnum.Texture2D, 0, 0, 0, Width, Height, pixelFormat, PixelType.UnsignedByte, Unsafe.AsRef(ref data[0]));
|
||||||
|
|
||||||
|
if (generateMipmaps) _gl.GenerateTextureMipmap(GlTexture);
|
||||||
|
|
||||||
|
SetWrap(TextureCoordinate.S, TextureWrapMode.Repeat);
|
||||||
|
SetWrap(TextureCoordinate.T, TextureWrapMode.Repeat);
|
||||||
|
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLevel, MipmapLevels - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateData(byte[] data, PixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
Bind();
|
||||||
|
|
||||||
|
_gl.TexSubImage2D(GLEnum.Texture2D, 0, 0, 0, Width, Height, pixelFormat, PixelType.UnsignedByte, Unsafe.AsRef(ref data[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] DownloadData()
|
||||||
|
{
|
||||||
|
Bind();
|
||||||
|
|
||||||
|
byte[] data = new byte[Width * Height * 4];
|
||||||
|
_gl.GetTexImage(TextureTarget.Texture2D, 0, PixelFormat.Rgba, PixelType.UnsignedByte, out Unsafe.AsRef(ref data[0]));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Bind()
|
||||||
|
{
|
||||||
|
_gl.BindTexture(GLEnum.Texture2D, GlTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMinFilter(TextureMinFilter filter)
|
||||||
|
{
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinFilter, (int)filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMagFilter(TextureMagFilter filter)
|
||||||
|
{
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMagFilter, (int)filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public void SetAnisotropy(float level)
|
||||||
|
// {
|
||||||
|
// const TextureParameterName textureMaxAnisotropy = (TextureParameterName)0x84FE;
|
||||||
|
// _gl.TexParameter(GLEnum.Texture2D, (GLEnum)textureMaxAnisotropy, Util.Clamp(level, 1, MaxAniso.GetValueOrDefault()));
|
||||||
|
// }
|
||||||
|
|
||||||
|
public void SetLod(int @base, int min, int max)
|
||||||
|
{
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, @base);
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, min);
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetWrap(TextureCoordinate coord, TextureWrapMode mode)
|
||||||
|
{
|
||||||
|
_gl.TexParameterI(GLEnum.Texture2D, (TextureParameterName)coord, (int)mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_gl.DeleteTexture(GlTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -114,6 +114,26 @@ public class TexmFile
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] GetRgba32BytesFromMipmap(int index, out int mipWidth, out int mipHeight)
|
||||||
|
{
|
||||||
|
var mipmapBytes = MipmapBytes[index];
|
||||||
|
|
||||||
|
mipWidth = Header.Width / (int) Math.Pow(2, index);
|
||||||
|
mipHeight = Header.Height / (int) Math.Pow(2, index);
|
||||||
|
|
||||||
|
if (IsIndexed)
|
||||||
|
{
|
||||||
|
return ReinterpretIndexedMipmap(mipmapBytes, LookupColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReinterpretMipmapBytesAsRgba32(
|
||||||
|
mipmapBytes,
|
||||||
|
mipWidth,
|
||||||
|
mipHeight,
|
||||||
|
Header.Format
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] ReinterpretIndexedMipmap(byte[] bytes, byte[] lookupColors)
|
private byte[] ReinterpretIndexedMipmap(byte[] bytes, byte[] lookupColors)
|
||||||
{
|
{
|
||||||
var span = bytes.AsSpan();
|
var span = bytes.AsSpan();
|
||||||
@ -123,15 +143,15 @@ public class TexmFile
|
|||||||
{
|
{
|
||||||
var index = span[i];
|
var index = span[i];
|
||||||
|
|
||||||
var a = lookupColors[index * 4 + 0];
|
var r = lookupColors[index * 4 + 0];
|
||||||
var r = lookupColors[index * 4 + 1];
|
var g = lookupColors[index * 4 + 1];
|
||||||
var g = lookupColors[index * 4 + 2];
|
var b = lookupColors[index * 4 + 2];
|
||||||
var b = lookupColors[index * 4 + 3];
|
var a = lookupColors[index * 4 + 3];
|
||||||
|
|
||||||
result[i * 4 + 0] = r;
|
result[i * 4 + 0] = r;
|
||||||
result[i * 4 + 1] = g;
|
result[i * 4 + 1] = g;
|
||||||
result[i * 4 + 2] = b;
|
result[i * 4 + 2] = b;
|
||||||
result[i * 4 + 3] = a;
|
result[i * 4 + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -161,8 +181,8 @@ public class TexmFile
|
|||||||
var rawPixel = span.Slice(i, 2);
|
var rawPixel = span.Slice(i, 2);
|
||||||
|
|
||||||
var r = (byte)(((rawPixel[0] >> 3) & 0b11111) / 32 * 255);
|
var r = (byte)(((rawPixel[0] >> 3) & 0b11111) / 32 * 255);
|
||||||
var g = (byte)(((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111) / 64 * 255);
|
var b = (byte)(((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111) / 64 * 255);
|
||||||
var b = (byte)((rawPixel[1] & 0b11111) / 32 * 255);
|
var g = (byte)((rawPixel[1] & 0b11111) / 32 * 255);
|
||||||
|
|
||||||
result[i / 2 * 4 + 0] = r;
|
result[i / 2 * 4 + 0] = r;
|
||||||
result[i / 2 * 4 + 1] = g;
|
result[i / 2 * 4 + 1] = g;
|
||||||
@ -182,10 +202,10 @@ public class TexmFile
|
|||||||
{
|
{
|
||||||
var rawPixel = span.Slice(i, 2);
|
var rawPixel = span.Slice(i, 2);
|
||||||
|
|
||||||
var a = (byte)((float)((rawPixel[0] >> 4) & 0b1111) / 16 * 255);
|
var r = (byte)((float)((rawPixel[0] >> 4) & 0b1111) * 17);
|
||||||
var r = (byte)((float)((rawPixel[0] >> 0) & 0b1111) / 16 * 255);
|
var g = (byte)((float)((rawPixel[0] >> 0) & 0b1111) * 17);
|
||||||
var g = (byte)((float)((rawPixel[1] >> 4) & 0b1111) / 16 * 255);
|
var b = (byte)((float)((rawPixel[1] >> 4) & 0b1111) * 17);
|
||||||
var b = (byte)((float)((rawPixel[1] >> 0) & 0b1111) / 16 * 255);
|
var a = (byte)((float)((rawPixel[1] >> 0) & 0b1111) * 17);
|
||||||
|
|
||||||
result[i / 2 * 4 + 0] = r;
|
result[i / 2 * 4 + 0] = r;
|
||||||
result[i / 2 * 4 + 1] = g;
|
result[i / 2 * 4 + 1] = g;
|
||||||
@ -205,14 +225,14 @@ public class TexmFile
|
|||||||
{
|
{
|
||||||
var rawPixel = span.Slice(i, 4);
|
var rawPixel = span.Slice(i, 4);
|
||||||
|
|
||||||
var x = rawPixel[0];
|
var r = rawPixel[0];
|
||||||
var y = rawPixel[1];
|
var g = rawPixel[1];
|
||||||
var z = rawPixel[2];
|
var b = rawPixel[2];
|
||||||
var w = rawPixel[3];
|
var w = rawPixel[3];
|
||||||
|
|
||||||
result[i + 0] = y;
|
result[i + 0] = r;
|
||||||
result[i + 1] = z;
|
result[i + 1] = g;
|
||||||
result[i + 2] = w;
|
result[i + 2] = b;
|
||||||
result[i + 3] = 255;
|
result[i + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,10 +248,10 @@ public class TexmFile
|
|||||||
{
|
{
|
||||||
var rawPixel = span.Slice(i, 4);
|
var rawPixel = span.Slice(i, 4);
|
||||||
|
|
||||||
var a = rawPixel[0];
|
var b = rawPixel[0];
|
||||||
var r = rawPixel[1];
|
var g = rawPixel[1];
|
||||||
var g = rawPixel[2];
|
var r = rawPixel[2];
|
||||||
var b = rawPixel[3];
|
var a = rawPixel[3];
|
||||||
|
|
||||||
result[i + 0] = r;
|
result[i + 0] = r;
|
||||||
result[i + 1] = g;
|
result[i + 1] = g;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user