mirror of
https://github.com/nillerusr/source-engine.git
synced 2026-08-08 01:39:36 +00:00
1
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
//========= Copyright Valve Corporation, All rights reserved. ============//
|
||||
//
|
||||
// Purpose:
|
||||
//
|
||||
// $NoKeywords: $
|
||||
//=============================================================================//
|
||||
|
||||
#if defined ( WIN32 ) && !defined( _X360 )
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined( OSX )
|
||||
#include <Carbon/Carbon.h>
|
||||
#elif defined( LINUX )
|
||||
//#error
|
||||
#elif defined( _X360 )
|
||||
#else
|
||||
#error
|
||||
#endif
|
||||
#include "FontTextureCache.h"
|
||||
#include "MatSystemSurface.h"
|
||||
#include <vgui_surfacelib/BitmapFont.h>
|
||||
#include <vgui/IVGui.h>
|
||||
#include <vgui_controls/Controls.h>
|
||||
#include "bitmap/imageformat.h"
|
||||
#include "vtf/vtf.h"
|
||||
#include "materialsystem/imaterialvar.h"
|
||||
#include "materialsystem/itexture.h"
|
||||
#include "tier1/KeyValues.h"
|
||||
#include "tier1/utlbuffer.h"
|
||||
#include "pixelwriter.h"
|
||||
#include "tier0/icommandline.h"
|
||||
|
||||
// memdbgon must be the last include file in a .cpp file!!!
|
||||
#include "tier0/memdbgon.h"
|
||||
|
||||
extern CMatSystemSurface g_MatSystemSurface;
|
||||
static int g_FontRenderBoundingBoxes = -1;
|
||||
|
||||
#define TEXTURE_PAGE_WIDTH 256
|
||||
#define TEXTURE_PAGE_HEIGHT 256
|
||||
|
||||
// row size
|
||||
int CFontTextureCache::s_pFontPageSize[FONT_PAGE_SIZE_COUNT] =
|
||||
{
|
||||
16,
|
||||
32,
|
||||
64,
|
||||
128,
|
||||
256,
|
||||
};
|
||||
|
||||
static bool g_mat_texture_outline_fonts = false;
|
||||
CON_COMMAND( mat_texture_outline_fonts, "Outline fonts textures." )
|
||||
{
|
||||
g_mat_texture_outline_fonts = !g_mat_texture_outline_fonts;
|
||||
Msg( "mat_texture_outline_fonts: %d\n", g_mat_texture_outline_fonts );
|
||||
g_MatSystemSurface.ResetFontCaches();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Constructor
|
||||
//-----------------------------------------------------------------------------
|
||||
CFontTextureCache::CFontTextureCache()
|
||||
: m_CharCache(0, 256, CacheEntryLessFunc)
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Destructor
|
||||
//-----------------------------------------------------------------------------
|
||||
CFontTextureCache::~CFontTextureCache()
|
||||
{
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: Resets the cache
|
||||
//-----------------------------------------------------------------------------
|
||||
void CFontTextureCache::Clear()
|
||||
{
|
||||
// remove all existing data
|
||||
m_CharCache.RemoveAll();
|
||||
m_PageList.RemoveAll();
|
||||
|
||||
// reinitialize
|
||||
CacheEntry_t listHead = { 0, 0 };
|
||||
m_LRUListHeadIndex = m_CharCache.Insert(listHead);
|
||||
|
||||
m_CharCache[m_LRUListHeadIndex].nextEntry = m_LRUListHeadIndex;
|
||||
m_CharCache[m_LRUListHeadIndex].prevEntry = m_LRUListHeadIndex;
|
||||
|
||||
for (int i = 0; i < FONT_PAGE_SIZE_COUNT; ++i)
|
||||
{
|
||||
m_pCurrPage[i] = -1;
|
||||
}
|
||||
m_FontPages.SetLessFunc( DefLessFunc( vgui::HFont ) );
|
||||
m_FontPages.RemoveAll();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: comparison function for cache entries
|
||||
//-----------------------------------------------------------------------------
|
||||
bool CFontTextureCache::CacheEntryLessFunc(CacheEntry_t const &lhs, CacheEntry_t const &rhs)
|
||||
{
|
||||
if (lhs.font < rhs.font)
|
||||
return true;
|
||||
else if (lhs.font > rhs.font)
|
||||
return false;
|
||||
|
||||
return (lhs.wch < rhs.wch);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: returns the texture info for the given char & font
|
||||
//-----------------------------------------------------------------------------
|
||||
bool CFontTextureCache::GetTextureForChar( vgui::HFont font, vgui::FontDrawType_t type, wchar_t wch, int *textureID, float **texCoords )
|
||||
{
|
||||
// Ask for just one character
|
||||
return GetTextureForChars( font, type, &wch, textureID, texCoords, 1 );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: returns the texture info for the given chars & font
|
||||
//-----------------------------------------------------------------------------
|
||||
bool CFontTextureCache::GetTextureForChars( vgui::HFont font, vgui::FontDrawType_t type, const wchar_t *wch, int *textureID, float **texCoords, int numChars )
|
||||
{
|
||||
Assert( wch && textureID && texCoords );
|
||||
Assert( numChars >= 1 );
|
||||
|
||||
if ( type == vgui::FONT_DRAW_DEFAULT )
|
||||
{
|
||||
type = g_MatSystemSurface.IsFontAdditive( font ) ? vgui::FONT_DRAW_ADDITIVE : vgui::FONT_DRAW_NONADDITIVE;
|
||||
}
|
||||
|
||||
int typePage = (int)type - 1;
|
||||
typePage = clamp( typePage, 0, (int)vgui::FONT_DRAW_TYPE_COUNT - 1 );
|
||||
|
||||
if ( FontManager().IsBitmapFont( font ) )
|
||||
{
|
||||
const int MAX_BITMAP_CHARS = 256;
|
||||
if ( numChars > MAX_BITMAP_CHARS )
|
||||
{
|
||||
// Increase MAX_BITMAP_CHARS
|
||||
Assert( 0 );
|
||||
return false;
|
||||
}
|
||||
|
||||
for ( int i = 0; i < numChars; i++ )
|
||||
{
|
||||
static float sTexCoords[ 4*MAX_BITMAP_CHARS ];
|
||||
CBitmapFont *pWinFont;
|
||||
float left, top, right, bottom;
|
||||
int index;
|
||||
Page_t *pPage;
|
||||
|
||||
pWinFont = reinterpret_cast< CBitmapFont* >( FontManager().GetFontForChar( font, wch[i] ) );
|
||||
if ( !pWinFont )
|
||||
{
|
||||
// bad font handle
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the texture coords
|
||||
pWinFont->GetCharCoords( wch[i], &left, &top, &right, &bottom );
|
||||
sTexCoords[i*4 + 0] = left;
|
||||
sTexCoords[i*4 + 1] = top;
|
||||
sTexCoords[i*4 + 2] = right;
|
||||
sTexCoords[i*4 + 3] = bottom;
|
||||
|
||||
// find font handle in our list of ready pages
|
||||
index = m_FontPages.Find( font );
|
||||
if ( index == m_FontPages.InvalidIndex() )
|
||||
{
|
||||
// not found, create the texture id and its materials
|
||||
index = m_FontPages.Insert( font );
|
||||
pPage = &m_FontPages.Element( index );
|
||||
|
||||
for (int type = 0; type < FONT_DRAW_TYPE_COUNT; ++type )
|
||||
{
|
||||
pPage->textureID[type] = g_MatSystemSurface.CreateNewTextureID( false );
|
||||
}
|
||||
CreateFontMaterials( *pPage, pWinFont->GetTexturePage(), true );
|
||||
}
|
||||
|
||||
texCoords[i] = &(sTexCoords[ i*4 ]);
|
||||
textureID[i] = m_FontPages.Element( index ).textureID[typePage];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
struct newPageEntry_t
|
||||
{
|
||||
int page; // The font page a new character will go in
|
||||
int drawX; // X location within the font page
|
||||
int drawY; // Y location within the font page
|
||||
};
|
||||
|
||||
// Determine how many characters need to have their texture generated
|
||||
int numNewChars = 0;
|
||||
int maxNewCharTexels = 0;
|
||||
int totalNewCharTexels = 0;
|
||||
newChar_t *newChars = (newChar_t *)_alloca( numChars*sizeof( newChar_t ) );
|
||||
newPageEntry_t *newEntries = (newPageEntry_t *)_alloca( numChars*sizeof( newPageEntry_t ) );
|
||||
|
||||
font_t *winFont = FontManager().GetFontForChar( font, wch[0] );
|
||||
if ( !winFont )
|
||||
return false;
|
||||
|
||||
for ( int i = 0; i < numChars; i++ )
|
||||
{
|
||||
CacheEntry_t cacheItem;
|
||||
cacheItem.font = font;
|
||||
cacheItem.wch = wch[i];
|
||||
HCacheEntry cacheHandle = m_CharCache.Find( cacheItem );
|
||||
if ( ! m_CharCache.IsValidIndex( cacheHandle ) )
|
||||
{
|
||||
// All characters must come out of the same font
|
||||
if ( winFont != FontManager().GetFontForChar( font, wch[i] ) )
|
||||
return false;
|
||||
|
||||
// get the char details
|
||||
int a, b, c;
|
||||
winFont->GetCharABCWidths( wch[i], a, b, c );
|
||||
int fontWide = max( b, 1 );
|
||||
int fontTall = max( winFont->GetHeight(), 1 );
|
||||
if ( winFont->GetUnderlined() )
|
||||
{
|
||||
fontWide += ( a + c );
|
||||
}
|
||||
|
||||
// Get a texture to render into
|
||||
int page, drawX, drawY, twide, ttall;
|
||||
if ( !AllocatePageForChar( fontWide, fontTall, page, drawX, drawY, twide, ttall ) )
|
||||
return false;
|
||||
|
||||
// accumulate data to pass to GetCharsRGBA below
|
||||
newEntries[ numNewChars ].page = page;
|
||||
newEntries[ numNewChars ].drawX = drawX;
|
||||
newEntries[ numNewChars ].drawY = drawY;
|
||||
newChars[ numNewChars ].wch = wch[i];
|
||||
newChars[ numNewChars ].fontWide = fontWide;
|
||||
newChars[ numNewChars ].fontTall = fontTall;
|
||||
newChars[ numNewChars ].offset = 4*totalNewCharTexels;
|
||||
totalNewCharTexels += fontWide*fontTall;
|
||||
maxNewCharTexels = max( maxNewCharTexels, fontWide*fontTall );
|
||||
numNewChars++;
|
||||
|
||||
// set the cache info
|
||||
cacheItem.page = page;
|
||||
|
||||
// the 0.5 texel offset is done in CMatSystemTexture::SetMaterial() / CMatSystemSurface::StartDrawing()
|
||||
double adjust = 0.0f;
|
||||
|
||||
cacheItem.texCoords[0] = (float)( (double)drawX / ((double)twide + adjust) );
|
||||
cacheItem.texCoords[1] = (float)( (double)drawY / ((double)ttall + adjust) );
|
||||
cacheItem.texCoords[2] = (float)( (double)(drawX + fontWide) / (double)twide );
|
||||
cacheItem.texCoords[3] = (float)( (double)(drawY + fontTall) / (double)ttall );
|
||||
|
||||
m_CharCache.Insert(cacheItem);
|
||||
cacheHandle = m_CharCache.Find( cacheItem );
|
||||
Assert( m_CharCache.IsValidIndex( cacheHandle ) );
|
||||
}
|
||||
|
||||
int page = m_CharCache[cacheHandle].page;
|
||||
textureID[i] = m_PageList[page].textureID[typePage];
|
||||
texCoords[i] = m_CharCache[cacheHandle].texCoords;
|
||||
}
|
||||
|
||||
// Generate texture data for all newly-encountered characters
|
||||
if ( numNewChars > 0 )
|
||||
{
|
||||
|
||||
#ifdef _X360
|
||||
if ( numNewChars > 1 )
|
||||
{
|
||||
MEM_ALLOC_CREDIT();
|
||||
|
||||
// Use the 360 fast path that generates multiple characters at once
|
||||
int newCharDataSize = totalNewCharTexels*4;
|
||||
CUtlBuffer newCharData( newCharDataSize, newCharDataSize, 0 );
|
||||
unsigned char *pRGBA = (unsigned char *)newCharData.Base();
|
||||
winFont->GetCharsRGBA( newChars, numNewChars, pRGBA );
|
||||
|
||||
// Copy the data into our font pages
|
||||
for ( int i = 0; i < numNewChars; i++ )
|
||||
{
|
||||
newChar_t & newChar = newChars[i];
|
||||
newPageEntry_t & newEntry = newEntries[i];
|
||||
|
||||
// upload the new sub texture
|
||||
// NOTE: both textureIDs reference the same ITexture, so we're ok
|
||||
g_MatSystemSurface.DrawSetTexture( m_PageList[newEntry.page].textureID[typePage] );
|
||||
unsigned char *characterRGBA = pRGBA + newChar.offset;
|
||||
g_MatSystemSurface.DrawSetSubTextureRGBA( m_PageList[newEntry.page].textureID[typePage], newEntry.drawX, newEntry.drawY, characterRGBA, newChar.fontWide, newChar.fontTall );
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
// create a buffer for new characters to be rendered into
|
||||
int nByteCount = maxNewCharTexels * 4;
|
||||
unsigned char *pRGBA = (unsigned char *)_alloca( nByteCount );
|
||||
|
||||
// Generate characters individually
|
||||
for ( int i = 0; i < numNewChars; i++ )
|
||||
{
|
||||
newChar_t & newChar = newChars[i];
|
||||
newPageEntry_t & newEntry = newEntries[i];
|
||||
|
||||
// render the character into the buffer
|
||||
Q_memset( pRGBA, 0, nByteCount );
|
||||
|
||||
winFont->GetCharRGBA( newChar.wch, newChar.fontWide, newChar.fontTall, pRGBA );
|
||||
|
||||
if ( g_mat_texture_outline_fonts )
|
||||
{
|
||||
int width = newChar.fontWide;
|
||||
int height = newChar.fontTall;
|
||||
|
||||
CPixelWriter pixelWriter;
|
||||
pixelWriter.SetPixelMemory( IMAGE_FORMAT_RGBA8888, pRGBA, width * sizeof( BGRA8888_t ) );
|
||||
for( int x = 0; x < width; x++ )
|
||||
{
|
||||
pixelWriter.Seek( x, 0 );
|
||||
pixelWriter.WritePixel( 255, 0, 255, 255 );
|
||||
pixelWriter.Seek( x, height - 1 );
|
||||
pixelWriter.WritePixel( 255, 0, 255, 255 );
|
||||
}
|
||||
for( int y = 0; y < height; y++ )
|
||||
{
|
||||
if ( y < 4 || y > height - 4 )
|
||||
{
|
||||
pixelWriter.Seek( 0, y );
|
||||
pixelWriter.WritePixel( 255, 0, 255, 255 );
|
||||
pixelWriter.Seek( width - 1, y );
|
||||
pixelWriter.WritePixel( 255, 0, 255, 255 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// upload the new sub texture
|
||||
// NOTE: both textureIDs reference the same ITexture, so we're ok)
|
||||
g_MatSystemSurface.DrawSetTexture( m_PageList[newEntry.page].textureID[typePage] );
|
||||
g_MatSystemSurface.DrawSetSubTextureRGBA( m_PageList[newEntry.page].textureID[typePage], newEntry.drawX, newEntry.drawY, pRGBA, newChar.fontWide, newChar.fontTall );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Creates font materials
|
||||
//-----------------------------------------------------------------------------
|
||||
void CFontTextureCache::CreateFontMaterials( Page_t &page, ITexture *pFontTexture, bool bitmapFont )
|
||||
{
|
||||
// The normal material
|
||||
KeyValues *pVMTKeyValues = new KeyValues( "UnlitGeneric" );
|
||||
pVMTKeyValues->SetInt( "$vertexcolor", 1 );
|
||||
pVMTKeyValues->SetInt( "$vertexalpha", 1 );
|
||||
pVMTKeyValues->SetInt( "$ignorez", 1 );
|
||||
pVMTKeyValues->SetInt( "$no_fullbright", 1 );
|
||||
pVMTKeyValues->SetInt( "$translucent", 1 );
|
||||
pVMTKeyValues->SetString( "$basetexture", pFontTexture->GetName() );
|
||||
IMaterial *pMaterial = g_pMaterialSystem->CreateMaterial( "__fontpage", pVMTKeyValues );
|
||||
pMaterial->Refresh();
|
||||
|
||||
int typePageNonAdditive = (int)vgui::FONT_DRAW_NONADDITIVE-1;
|
||||
g_MatSystemSurface.DrawSetTextureMaterial( page.textureID[typePageNonAdditive], pMaterial );
|
||||
pMaterial->DecrementReferenceCount();
|
||||
|
||||
// The additive material
|
||||
pVMTKeyValues = new KeyValues( "UnlitGeneric" );
|
||||
pVMTKeyValues->SetInt( "$vertexcolor", 1 );
|
||||
pVMTKeyValues->SetInt( "$vertexalpha", 1 );
|
||||
pVMTKeyValues->SetInt( "$ignorez", 1 );
|
||||
pVMTKeyValues->SetInt( "$no_fullbright", 1 );
|
||||
pVMTKeyValues->SetInt( "$translucent", 1 );
|
||||
pVMTKeyValues->SetInt( "$additive", 1 );
|
||||
pVMTKeyValues->SetString( "$basetexture", pFontTexture->GetName() );
|
||||
pMaterial = g_pMaterialSystem->CreateMaterial( "__fontpage_additive", pVMTKeyValues );
|
||||
pMaterial->Refresh();
|
||||
|
||||
int typePageAdditive = (int)vgui::FONT_DRAW_ADDITIVE-1;
|
||||
if ( bitmapFont )
|
||||
{
|
||||
g_MatSystemSurface.DrawSetTextureMaterial( page.textureID[typePageAdditive], pMaterial );
|
||||
}
|
||||
else
|
||||
{
|
||||
g_MatSystemSurface.ReferenceProceduralMaterial( page.textureID[typePageAdditive], page.textureID[typePageNonAdditive], pMaterial );
|
||||
}
|
||||
pMaterial->DecrementReferenceCount();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Computes the page size given a character height
|
||||
//-----------------------------------------------------------------------------
|
||||
int CFontTextureCache::ComputePageType( int charTall ) const
|
||||
{
|
||||
for (int i = 0; i < FONT_PAGE_SIZE_COUNT; ++i)
|
||||
{
|
||||
if ( charTall < s_pFontPageSize[i] )
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Purpose: allocates a new page for a given character
|
||||
//-----------------------------------------------------------------------------
|
||||
bool CFontTextureCache::AllocatePageForChar(int charWide, int charTall, int &pageIndex, int &drawX, int &drawY, int &twide, int &ttall)
|
||||
{
|
||||
// see if there is room in the last page for this character
|
||||
int nPageType = ComputePageType( charTall );
|
||||
if ( nPageType < 0 )
|
||||
{
|
||||
Assert( !"Font is too tall for texture cache of glyphs\n" );
|
||||
return false;
|
||||
}
|
||||
|
||||
pageIndex = m_pCurrPage[nPageType];
|
||||
|
||||
int nNextX = 0;
|
||||
bool bNeedsNewPage = true;
|
||||
if ( pageIndex > -1 )
|
||||
{
|
||||
Page_t &page = m_PageList[ pageIndex ];
|
||||
|
||||
nNextX = page.nextX + charWide;
|
||||
|
||||
// make sure we have room on the current line of the texture page
|
||||
if ( nNextX > page.wide )
|
||||
{
|
||||
// move down a line
|
||||
page.nextX = 0;
|
||||
nNextX = charWide;
|
||||
page.nextY += page.tallestCharOnLine;
|
||||
page.tallestCharOnLine = charTall;
|
||||
}
|
||||
page.tallestCharOnLine = max( page.tallestCharOnLine, (short)charTall );
|
||||
|
||||
bNeedsNewPage = ((page.nextY + page.tallestCharOnLine) > page.tall);
|
||||
}
|
||||
|
||||
if ( bNeedsNewPage )
|
||||
{
|
||||
// allocate a new page
|
||||
pageIndex = m_PageList.AddToTail();
|
||||
Page_t &newPage = m_PageList[pageIndex];
|
||||
m_pCurrPage[nPageType] = pageIndex;
|
||||
|
||||
for (int i = 0; i < FONT_DRAW_TYPE_COUNT; ++i )
|
||||
{
|
||||
newPage.textureID[i] = g_MatSystemSurface.CreateNewTextureID( true );
|
||||
}
|
||||
|
||||
newPage.maxFontHeight = s_pFontPageSize[nPageType];
|
||||
newPage.wide = TEXTURE_PAGE_WIDTH;
|
||||
newPage.tall = TEXTURE_PAGE_HEIGHT;
|
||||
newPage.nextX = 0;
|
||||
newPage.nextY = 0;
|
||||
newPage.tallestCharOnLine = charTall;
|
||||
|
||||
nNextX = charWide;
|
||||
|
||||
static int nFontPageId = 0;
|
||||
char pTextureName[64];
|
||||
Q_snprintf( pTextureName, 64, "__font_page_%d", nFontPageId );
|
||||
++nFontPageId;
|
||||
|
||||
MEM_ALLOC_CREDIT();
|
||||
ITexture *pTexture = g_pMaterialSystem->CreateProceduralTexture(
|
||||
pTextureName,
|
||||
TEXTURE_GROUP_VGUI,
|
||||
newPage.wide,
|
||||
newPage.tall,
|
||||
IMAGE_FORMAT_RGBA8888,
|
||||
TEXTUREFLAGS_POINTSAMPLE | TEXTUREFLAGS_CLAMPS | TEXTUREFLAGS_CLAMPT |
|
||||
TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_NOLOD | TEXTUREFLAGS_PROCEDURAL | TEXTUREFLAGS_SINGLECOPY );
|
||||
|
||||
CreateFontMaterials( newPage, pTexture );
|
||||
|
||||
pTexture->DecrementReferenceCount();
|
||||
|
||||
if ( IsPC() || !IsDebug() )
|
||||
{
|
||||
// clear the texture from the inital checkerboard to black
|
||||
// allocate for 32bpp format
|
||||
int nByteCount = TEXTURE_PAGE_WIDTH * TEXTURE_PAGE_HEIGHT * 4;
|
||||
unsigned char *pRGBA = (unsigned char *)_alloca( nByteCount );
|
||||
Q_memset( pRGBA, 0, nByteCount );
|
||||
|
||||
int typePageNonAdditive = (int)(vgui::FONT_DRAW_NONADDITIVE)-1;
|
||||
g_MatSystemSurface.DrawSetTextureRGBA( newPage.textureID[typePageNonAdditive], pRGBA, newPage.wide, newPage.tall, false, false );
|
||||
}
|
||||
}
|
||||
|
||||
// output the position
|
||||
Page_t &page = m_PageList[ pageIndex ];
|
||||
drawX = page.nextX;
|
||||
drawY = page.nextY;
|
||||
twide = page.wide;
|
||||
ttall = page.tall;
|
||||
|
||||
// Update the next position to draw in
|
||||
page.nextX = nNextX + 1;
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user