mirror of
https://github.com/luanti-org/luanti.git
synced 2025-12-17 04:25:26 +01:00
Refactor parts of CGUITTFont
This commit is contained in:
@@ -35,7 +35,6 @@
|
||||
|
||||
#include "irr_ptr.h"
|
||||
#include "log.h"
|
||||
#include "filesys.h"
|
||||
#include "debug.h"
|
||||
#include "IGUIEnvironment.h"
|
||||
|
||||
@@ -46,9 +45,8 @@
|
||||
namespace gui
|
||||
{
|
||||
|
||||
std::map<io::path, SGUITTFace*> SGUITTFace::faces;
|
||||
FT_Library SGUITTFace::freetype_library = nullptr;
|
||||
std::size_t SGUITTFace::n_faces = 0;
|
||||
size_t SGUITTFace::n_faces = 0;
|
||||
|
||||
FT_Library SGUITTFace::getFreeTypeLibrary()
|
||||
{
|
||||
@@ -85,54 +83,28 @@ SGUITTFace* SGUITTFace::createFace(std::string &&buffer)
|
||||
auto ft = getFreeTypeLibrary();
|
||||
if (!ft)
|
||||
return nullptr;
|
||||
return (FT_New_Memory_Face(ft,
|
||||
bool ok = FT_New_Memory_Face(ft,
|
||||
reinterpret_cast<const FT_Byte*>(face->face_buffer.data()),
|
||||
face->face_buffer.size(), 0, &face->face))
|
||||
? nullptr : face.release();
|
||||
face->face_buffer.size(), 0, &face->face) == 0;
|
||||
return ok ? face.release() : nullptr;
|
||||
}
|
||||
|
||||
SGUITTFace* SGUITTFace::loadFace(const io::path &filename)
|
||||
{
|
||||
auto it = faces.find(filename);
|
||||
if (it != faces.end()) {
|
||||
it->second->grab();
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::string buffer;
|
||||
if (!fs::ReadFile(filename.c_str(), buffer, true)) {
|
||||
errorstream << "CGUITTFont: Reading file " << filename.c_str() << " failed." << std::endl;
|
||||
irr_ptr<SGUITTFace> face(new SGUITTFace(""));
|
||||
auto ft = getFreeTypeLibrary();
|
||||
if (!ft)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *face = SGUITTFace::createFace(std::move(buffer));
|
||||
if (!face) {
|
||||
errorstream << "CGUITTFont: FT_New_Memory_Face failed." << std::endl;
|
||||
return nullptr;
|
||||
}
|
||||
faces.emplace(filename, face);
|
||||
return face;
|
||||
}
|
||||
|
||||
void SGUITTFace::dropFilename()
|
||||
{
|
||||
if (!filename.has_value())
|
||||
return;
|
||||
|
||||
auto it = faces.find(*filename);
|
||||
if (it == faces.end())
|
||||
return;
|
||||
|
||||
SGUITTFace* f = it->second;
|
||||
// Drop our face. If this was the last face, the destructor will clean up.
|
||||
if (f->drop())
|
||||
faces.erase(*filename);
|
||||
// Prefer FT_New_Face because it doesn't require loading everything
|
||||
// to memory.
|
||||
bool ok = FT_New_Face(ft, filename.c_str(), 0, &face->face) == 0;
|
||||
return ok ? face.release() : nullptr;
|
||||
}
|
||||
|
||||
video::IImage* SGUITTGlyph::createGlyphImage(const FT_Bitmap& bits, video::IVideoDriver* driver) const
|
||||
{
|
||||
// Make sure our casts to s32 in the loops below will not cause problems
|
||||
if ((s32)bits.rows < 0 || (s32)bits.width < 0)
|
||||
if (bits.rows > INT32_MAX || bits.width > INT32_MAX)
|
||||
FATAL_ERROR("Insane font glyph size");
|
||||
|
||||
// Determine what our texture size should be.
|
||||
@@ -140,8 +112,8 @@ video::IImage* SGUITTGlyph::createGlyphImage(const FT_Bitmap& bits, video::IVide
|
||||
core::dimension2du d(bits.width + 1, bits.rows + 1);
|
||||
core::dimension2du texture_size;
|
||||
|
||||
// Create and load our image now.
|
||||
video::IImage* image = 0;
|
||||
// Turn bitmap into an image
|
||||
video::IImage *image = nullptr;
|
||||
switch (bits.pixel_mode)
|
||||
{
|
||||
case FT_PIXEL_MODE_MONO:
|
||||
@@ -233,44 +205,89 @@ void SGUITTGlyph::preload(u32 char_index, FT_Face face, CGUITTFont *parent, u32
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate slot from page
|
||||
glyph_page = parent->getLastGlyphPageIndex();
|
||||
u32 texture_side_length = page->texture->getOriginalSize().Width;
|
||||
core::vector2di page_position(
|
||||
(page->used_slots % (texture_side_length / font_size)) * font_size,
|
||||
(page->used_slots / (texture_side_length / font_size)) * font_size
|
||||
);
|
||||
);
|
||||
source_rect.UpperLeftCorner = page_position;
|
||||
source_rect.LowerRightCorner = core::vector2di(page_position.X + bits.width, page_position.Y + bits.rows);
|
||||
|
||||
page->dirty = true;
|
||||
++page->used_slots;
|
||||
--page->available_slots;
|
||||
|
||||
// We grab the glyph bitmap here so the data won't be removed when the next glyph is loaded.
|
||||
surface = createGlyphImage(bits, parent->getDriver());
|
||||
// createGlyphImage can now be called, the next preload() call will however
|
||||
// invalidate the data in `bits`.
|
||||
}
|
||||
|
||||
void SGUITTGlyph::unload()
|
||||
{
|
||||
if (surface)
|
||||
{
|
||||
surface->drop();
|
||||
surface = 0;
|
||||
}
|
||||
// reset isLoaded to false
|
||||
source_rect = core::recti();
|
||||
}
|
||||
|
||||
bool CGUITTGlyphPage::createPageTexture(const u8 pixel_mode,
|
||||
const core::dimension2du texture_size)
|
||||
{
|
||||
if (texture)
|
||||
return false;
|
||||
|
||||
bool flgmip = driver->getTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS);
|
||||
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);
|
||||
bool flgcpy = driver->getTextureCreationFlag(video::ETCF_ALLOW_MEMORY_COPY);
|
||||
driver->setTextureCreationFlag(video::ETCF_ALLOW_MEMORY_COPY, true);
|
||||
|
||||
// Create texture
|
||||
switch (pixel_mode) {
|
||||
case FT_PIXEL_MODE_MONO:
|
||||
texture = driver->addTexture(texture_size, name, video::ECF_A1R5G5B5);
|
||||
break;
|
||||
case FT_PIXEL_MODE_GRAY:
|
||||
default:
|
||||
texture = driver->addTexture(texture_size, name, video::ECF_A8R8G8B8);
|
||||
break;
|
||||
}
|
||||
|
||||
// Restore texture creation flags
|
||||
driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, flgmip);
|
||||
driver->setTextureCreationFlag(video::ETCF_ALLOW_MEMORY_COPY, flgcpy);
|
||||
|
||||
return texture ? true : false;
|
||||
}
|
||||
|
||||
//! Updates the texture atlas with new glyphs.
|
||||
void CGUITTGlyphPage::updateTexture()
|
||||
{
|
||||
if (!isDirty())
|
||||
return;
|
||||
|
||||
void *ptr = texture->lock();
|
||||
if (!ptr)
|
||||
return;
|
||||
|
||||
video::ECOLOR_FORMAT format = texture->getColorFormat();
|
||||
core::dimension2du size = texture->getOriginalSize();
|
||||
video::IImage* pageholder = driver->createImageFromData(format, size, ptr, true, false);
|
||||
|
||||
for (auto &it : glyph_to_be_paged)
|
||||
it.surface->copyTo(pageholder, it.glyph->source_rect.UpperLeftCorner);
|
||||
|
||||
pageholder->drop();
|
||||
texture->unlock();
|
||||
glyph_to_be_paged.clear();
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
|
||||
CGUITTFont* CGUITTFont::createTTFont(IGUIEnvironment *env,
|
||||
CGUITTFont *CGUITTFont::createTTFont(IGUIEnvironment *env,
|
||||
SGUITTFace *face, u32 size, bool antialias,
|
||||
bool transparency, u32 shadow, u32 shadow_alpha)
|
||||
bool preload, u32 shadow, u32 shadow_alpha)
|
||||
{
|
||||
CGUITTFont* font = new CGUITTFont(env);
|
||||
bool ret = font->load(face, size, antialias, transparency);
|
||||
if (!ret)
|
||||
{
|
||||
CGUITTFont *font = new CGUITTFont(env);
|
||||
bool ret = font->load(face, size, antialias, true, preload);
|
||||
if (!ret) {
|
||||
font->drop();
|
||||
return 0;
|
||||
}
|
||||
@@ -284,9 +301,9 @@ CGUITTFont* CGUITTFont::createTTFont(IGUIEnvironment *env,
|
||||
//////////////////////
|
||||
|
||||
//! Constructor.
|
||||
CGUITTFont::CGUITTFont(IGUIEnvironment *env)
|
||||
: use_monochrome(false), use_transparency(true), use_hinting(true), use_auto_hinting(true),
|
||||
batch_load_size(1)
|
||||
CGUITTFont::CGUITTFont(IGUIEnvironment *env) :
|
||||
use_monochrome(false), use_hinting(true), use_auto_hinting(true),
|
||||
batch_load_size(1)
|
||||
{
|
||||
|
||||
if (env) {
|
||||
@@ -300,16 +317,16 @@ batch_load_size(1)
|
||||
setInvisibleCharacters(L" ");
|
||||
}
|
||||
|
||||
bool CGUITTFont::load(SGUITTFace *face, const u32 size, const bool antialias, const bool transparency)
|
||||
bool CGUITTFont::load(SGUITTFace *face, const u32 size, const bool antialias,
|
||||
const bool transparency, const bool preload)
|
||||
{
|
||||
if (!Driver || size == 0 || !face)
|
||||
return false;
|
||||
|
||||
this->size = size;
|
||||
|
||||
// Update the font loading flags when the font is first loaded.
|
||||
// Update the font loading flags when the font is first loaded
|
||||
this->use_monochrome = !antialias;
|
||||
this->use_transparency = transparency;
|
||||
update_load_flags();
|
||||
|
||||
// Store our face.
|
||||
@@ -327,11 +344,13 @@ bool CGUITTFont::load(SGUITTFace *face, const u32 size, const bool antialias, co
|
||||
Glyphs.clear();
|
||||
Glyphs.set_used(tt_face->num_glyphs);
|
||||
|
||||
// Cache the first 127 ascii characters.
|
||||
u32 old_size = batch_load_size;
|
||||
batch_load_size = 127;
|
||||
getGlyphIndexByChar((char32_t)0);
|
||||
batch_load_size = old_size;
|
||||
// Cache the first 127 ASCII characters
|
||||
if (preload) {
|
||||
u32 old_size = batch_load_size;
|
||||
batch_load_size = 127;
|
||||
getGlyphIndexByChar(U' '); // char needs to exist, so pick space
|
||||
batch_load_size = old_size;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -366,31 +385,27 @@ void CGUITTFont::update_glyph_pages() const
|
||||
{
|
||||
for (u32 i = 0; i != Glyph_Pages.size(); ++i)
|
||||
{
|
||||
if (Glyph_Pages[i]->dirty)
|
||||
if (Glyph_Pages[i]->isDirty())
|
||||
Glyph_Pages[i]->updateTexture();
|
||||
}
|
||||
}
|
||||
|
||||
CGUITTGlyphPage* CGUITTFont::getLastGlyphPage() const
|
||||
{
|
||||
CGUITTGlyphPage* page = 0;
|
||||
if (Glyph_Pages.empty())
|
||||
return 0;
|
||||
else
|
||||
{
|
||||
page = Glyph_Pages[getLastGlyphPageIndex()];
|
||||
if (page->available_slots == 0)
|
||||
page = 0;
|
||||
}
|
||||
return nullptr;
|
||||
CGUITTGlyphPage *page = Glyph_Pages[getLastGlyphPageIndex()];
|
||||
if (page->available_slots == 0)
|
||||
return nullptr;
|
||||
return page;
|
||||
}
|
||||
|
||||
CGUITTGlyphPage* CGUITTFont::createGlyphPage(const u8 pixel_mode)
|
||||
{
|
||||
CGUITTGlyphPage* page = 0;
|
||||
CGUITTGlyphPage *page = nullptr;
|
||||
|
||||
// Name of our page.
|
||||
io::path name("TTFontGlyphPage_");
|
||||
io::path name("glyph_");
|
||||
name += tt_face->family_name;
|
||||
name += ".";
|
||||
name += tt_face->style_name;
|
||||
@@ -403,19 +418,15 @@ CGUITTGlyphPage* CGUITTFont::createGlyphPage(const u8 pixel_mode)
|
||||
page = new CGUITTGlyphPage(Driver, name);
|
||||
|
||||
// Determine our maximum texture size.
|
||||
// If we keep getting 0, set it to 1024x1024, as that number is pretty safe.
|
||||
core::dimension2du max_texture_size = max_page_texture_size;
|
||||
if (max_texture_size.Width == 0 || max_texture_size.Height == 0)
|
||||
max_texture_size = Driver->getMaxTextureSize();
|
||||
if (max_texture_size.Width == 0 || max_texture_size.Height == 0)
|
||||
max_texture_size = core::dimension2du(1024, 1024);
|
||||
core::dimension2du max_texture_size = Driver->getMaxTextureSize();
|
||||
|
||||
// We want to try to put at least 144 glyphs on a single texture.
|
||||
// We want to try to put at least 180 glyphs on a single texture.
|
||||
// magic number = floor(texture_size / sqrt(180))
|
||||
core::dimension2du page_texture_size;
|
||||
if (size <= 21) page_texture_size = core::dimension2du(256, 256);
|
||||
else if (size <= 42) page_texture_size = core::dimension2du(512, 512);
|
||||
else if (size <= 84) page_texture_size = core::dimension2du(1024, 1024);
|
||||
else if (size <= 168) page_texture_size = core::dimension2du(2048, 2048);
|
||||
if (size <= 19) page_texture_size = core::dimension2du(256, 256);
|
||||
else if (size <= 38) page_texture_size = core::dimension2du(512, 512);
|
||||
else if (size <= 76) page_texture_size = core::dimension2du(1024, 1024);
|
||||
else if (size <= 152) page_texture_size = core::dimension2du(2048, 2048);
|
||||
else page_texture_size = core::dimension2du(4096, 4096);
|
||||
|
||||
if (page_texture_size.Width > max_texture_size.Width || page_texture_size.Height > max_texture_size.Height)
|
||||
@@ -428,21 +439,12 @@ CGUITTGlyphPage* CGUITTFont::createGlyphPage(const u8 pixel_mode)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (page)
|
||||
{
|
||||
// Determine the number of glyph slots on the page and add it to the list of pages.
|
||||
page->available_slots = (page_texture_size.Width / size) * (page_texture_size.Height / size);
|
||||
Glyph_Pages.push_back(page);
|
||||
}
|
||||
// Determine the number of glyph slots on the page and add it to the list of pages
|
||||
page->available_slots = (page_texture_size.Width / size) * (page_texture_size.Height / size);
|
||||
Glyph_Pages.push_back(page);
|
||||
return page;
|
||||
}
|
||||
|
||||
void CGUITTFont::setTransparency(const bool flag)
|
||||
{
|
||||
use_transparency = flag;
|
||||
reset_images();
|
||||
}
|
||||
|
||||
void CGUITTFont::setMonochrome(const bool flag)
|
||||
{
|
||||
use_monochrome = flag;
|
||||
@@ -467,6 +469,7 @@ void CGUITTFont::draw(const core::stringw& text, const core::rect<s32>& position
|
||||
void CGUITTFont::draw(const EnrichedString &text, const core::rect<s32>& position, bool hcenter, bool vcenter, const core::rect<s32>* clip)
|
||||
{
|
||||
const auto &colors = text.getColors();
|
||||
constexpr video::SColor fallback_color(255, 255, 255, 255); // if colors is too short
|
||||
|
||||
if (!Driver)
|
||||
return;
|
||||
@@ -499,9 +502,6 @@ void CGUITTFont::draw(const EnrichedString &text, const core::rect<s32>& positio
|
||||
const std::u32string utext = convertWCharToU32String(text.c_str());
|
||||
const u32 lineHeight = getLineHeight();
|
||||
|
||||
// Key: Glyph page index. Value: Arrays relevant for rendering
|
||||
std::map<u32, CGUITTGlyphPage*> Render_Map;
|
||||
|
||||
// Start parsing characters.
|
||||
// The same logic is applied to `CGUITTFont::getDimension`
|
||||
char32_t previousChar = 0;
|
||||
@@ -538,7 +538,7 @@ void CGUITTFont::draw(const EnrichedString &text, const core::rect<s32>& positio
|
||||
const u32 width = getWidthFromCharacter(currentChar);
|
||||
|
||||
// Skip whitespace characters
|
||||
if (InvisibleChars.find_first_of(currentChar) != std::u32string::npos)
|
||||
if (InvisibleChars.find(currentChar) != std::u32string::npos)
|
||||
goto skip_invisible;
|
||||
|
||||
if (clip) {
|
||||
@@ -568,14 +568,10 @@ void CGUITTFont::draw(const EnrichedString &text, const core::rect<s32>& positio
|
||||
offset += getKerning(currentChar, previousChar);
|
||||
|
||||
// Determine rendering information.
|
||||
CGUITTGlyphPage* const page = Glyph_Pages[glyph->glyph_page];
|
||||
page->render_positions.push_back(core::position2di(offset.X + offx, offset.Y + offy));
|
||||
CGUITTGlyphPage *const page = Glyph_Pages[glyph->glyph_page];
|
||||
page->render_positions.emplace_back(offset.X + offx, offset.Y + offy);
|
||||
page->render_source_rects.push_back(glyph->source_rect);
|
||||
if (i < colors.size())
|
||||
page->render_colors.push_back(colors[i]);
|
||||
else
|
||||
page->render_colors.push_back(video::SColor(255,255,255,255));
|
||||
Render_Map[glyph->glyph_page] = page;
|
||||
page->render_colors.push_back(i < colors.size() ? colors[i] : fallback_color);
|
||||
}
|
||||
else if (fallback)
|
||||
{
|
||||
@@ -587,7 +583,7 @@ void CGUITTFont::draw(const EnrichedString &text, const core::rect<s32>& positio
|
||||
|
||||
fallback->draw(core::stringw(l1),
|
||||
core::rect<s32>({offset.X-1, offset.Y-1}, position.LowerRightCorner), // ???
|
||||
i < colors.size() ? colors[i] : video::SColor(255, 255, 255, 255),
|
||||
i < colors.size() ? colors[i] : fallback_color,
|
||||
false, false, clip);
|
||||
}
|
||||
|
||||
@@ -600,9 +596,14 @@ skip_invisible:
|
||||
update_glyph_pages();
|
||||
core::array<core::vector2di> tmp_positions;
|
||||
core::array<core::recti> tmp_source_rects;
|
||||
for (const auto &it : Render_Map)
|
||||
{
|
||||
CGUITTGlyphPage *page = it.second;
|
||||
for (u32 page_i = 0; page_i < Glyph_Pages.size(); ++page_i) {
|
||||
CGUITTGlyphPage *page = Glyph_Pages[page_i];
|
||||
|
||||
if (page->render_positions.empty())
|
||||
continue;
|
||||
|
||||
assert(page->render_positions.size() == page->render_colors.size());
|
||||
assert(page->render_positions.size() == page->render_source_rects.size());
|
||||
|
||||
// render runs of matching color in batch
|
||||
video::SColor colprev;
|
||||
@@ -616,9 +617,6 @@ skip_invisible:
|
||||
tmp_source_rects.set_data(&page->render_source_rects[ibegin], i - ibegin);
|
||||
--i;
|
||||
|
||||
if (!use_transparency)
|
||||
colprev.color |= 0xff000000;
|
||||
|
||||
if (shadow_offset) {
|
||||
for (size_t i = 0; i < tmp_positions.size(); ++i)
|
||||
tmp_positions[i] += core::vector2di(shadow_offset, shadow_offset);
|
||||
@@ -746,13 +744,14 @@ u32 CGUITTFont::getGlyphIndexByChar(char32_t c) const
|
||||
return 0;
|
||||
|
||||
// If our glyph is already loaded, don't bother doing any batch loading code.
|
||||
if (glyph != 0 && Glyphs[glyph - 1].isLoaded())
|
||||
if (Glyphs[glyph - 1].isLoaded())
|
||||
return glyph;
|
||||
|
||||
// Determine our batch loading positions.
|
||||
u32 half_size = (batch_load_size / 2);
|
||||
u32 start_pos = 0;
|
||||
if (c > half_size) start_pos = c - half_size;
|
||||
if (c > half_size)
|
||||
start_pos = c - half_size;
|
||||
u32 end_pos = start_pos + batch_load_size;
|
||||
|
||||
// Load all our characters.
|
||||
@@ -769,7 +768,8 @@ u32 CGUITTFont::getGlyphIndexByChar(char32_t c) const
|
||||
{
|
||||
auto *this2 = const_cast<CGUITTFont*>(this); // oh well
|
||||
glyph.preload(char_index, tt_face, this2, size, load_flags);
|
||||
Glyph_Pages[glyph.glyph_page]->pushGlyphToBePaged(&glyph);
|
||||
auto *surface = glyph.createGlyphImage(tt_face->glyph->bitmap, Driver);
|
||||
Glyph_Pages[glyph.glyph_page]->pushGlyphToBePaged(&glyph, surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,51 +876,6 @@ void CGUITTFont::setInvisibleCharacters(const wchar_t *s)
|
||||
InvisibleChars = convertWCharToU32String(s);
|
||||
}
|
||||
|
||||
video::IImage* CGUITTFont::createTextureFromChar(const char32_t& ch)
|
||||
{
|
||||
// This character allows us to print something to the screen for unknown, unrecognizable, or
|
||||
// unrepresentable characters. See Unicode spec.
|
||||
const char32_t UTF_REPLACEMENT_CHARACTER = 0xFFFD;
|
||||
|
||||
u32 n = getGlyphIndexByChar(ch);
|
||||
if (n == 0)
|
||||
n = getGlyphIndexByChar(UTF_REPLACEMENT_CHARACTER);
|
||||
|
||||
const SGUITTGlyph& glyph = Glyphs[n-1];
|
||||
CGUITTGlyphPage* page = Glyph_Pages[glyph.glyph_page];
|
||||
|
||||
if (page->dirty)
|
||||
page->updateTexture();
|
||||
|
||||
video::ITexture* tex = page->texture;
|
||||
|
||||
// Acquire a read-only lock of the corresponding page texture.
|
||||
void* ptr = tex->lock(video::ETLM_READ_ONLY);
|
||||
if (!ptr)
|
||||
return nullptr;
|
||||
|
||||
video::ECOLOR_FORMAT format = tex->getColorFormat();
|
||||
core::dimension2du tex_size = tex->getOriginalSize();
|
||||
video::IImage* pageholder = Driver->createImageFromData(format, tex_size, ptr, true, false);
|
||||
|
||||
// Copy the image data out of the page texture.
|
||||
core::dimension2du glyph_size(glyph.source_rect.getSize());
|
||||
video::IImage* image = Driver->createImage(format, glyph_size);
|
||||
pageholder->copyTo(image, core::position2di(0, 0), glyph.source_rect);
|
||||
|
||||
tex->unlock();
|
||||
pageholder->drop();
|
||||
return image;
|
||||
}
|
||||
|
||||
video::ITexture* CGUITTFont::getPageTextureByIndex(const u32& page_index) const
|
||||
{
|
||||
if (page_index < Glyph_Pages.size())
|
||||
return Glyph_Pages[page_index]->texture;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::u32string CGUITTFont::convertWCharToU32String(const wchar_t* const charArray) const
|
||||
{
|
||||
static_assert(sizeof(wchar_t) == 2 || sizeof(wchar_t) == 4, "unexpected wchar size");
|
||||
|
||||
Reference in New Issue
Block a user