mirror of
https://github.com/ZDoom/raze-gles.git
synced 2024-11-10 23:02:03 +00:00
- redid font translation so that it doesn't need to crush the font characters' color set to the base palette.
Right now it creates a special type of luminance translation that can operate on a true color bitmap.
This commit is contained in:
parent
9382a62aa1
commit
0bab333f36
9 changed files with 189 additions and 44 deletions
|
@ -32,7 +32,7 @@ struct FRemapTable
|
|||
PalEntry Palette[256]; // The ideal palette this maps to
|
||||
int crc32;
|
||||
int Index;
|
||||
int NumEntries; // # of elements in this table (usually 256)
|
||||
int NumEntries; // # of elements in this table (usually 256), if this is -1 it is a luminosity translation.
|
||||
bool Inactive = false; // This table is inactive and should be treated as if it was passed as NULL
|
||||
bool TwodOnly = false; // Only used for 2D rendering
|
||||
bool ForFont = false; // Mark font translations because they may require different handling than the ones for sprites-
|
||||
|
@ -70,12 +70,26 @@ inline constexpr uint32_t TRANSLATION(uint8_t a, uint32_t b)
|
|||
{
|
||||
return (a << TRANSLATION_SHIFT) | b;
|
||||
}
|
||||
inline constexpr uint32_t LuminosityTranslation(int range, uint8_t min, uint8_t max)
|
||||
{
|
||||
// ensure that the value remains positive.
|
||||
return ( (1 << 30) | ((range&0x3fff) << 16) | (min << 8) | max );
|
||||
}
|
||||
|
||||
inline constexpr bool IsLuminosityTranslation(int trans)
|
||||
{
|
||||
return trans > 0 && (trans & (1 << 30));
|
||||
}
|
||||
|
||||
inline constexpr int GetTranslationType(uint32_t trans)
|
||||
{
|
||||
assert(!IsLuminosityTranslation(trans));
|
||||
return (trans & TRANSLATIONTYPE_MASK) >> TRANSLATION_SHIFT;
|
||||
}
|
||||
|
||||
inline constexpr int GetTranslationIndex(uint32_t trans)
|
||||
{
|
||||
assert(!IsLuminosityTranslation(trans));
|
||||
return (trans & TRANSLATION_MASK);
|
||||
}
|
||||
|
||||
|
|
|
@ -458,7 +458,7 @@ void FFont::ReadSheetFont(TArray<FolderEntry> &folderdata, int width, int height
|
|||
// Move the Windows-1252 characters to their proper place.
|
||||
for (int i = 0x80; i < 0xa0; i++)
|
||||
{
|
||||
if (win1252map[i - 0x80] != i && Chars[i - minchar].TranslatedPic != nullptr && Chars[win1252map[i - 0x80] - minchar].TranslatedPic == nullptr)
|
||||
if (win1252map[i - 0x80] != i && Chars[i - minchar].OriginalPic != nullptr && Chars[win1252map[i - 0x80] - minchar].OriginalPic == nullptr)
|
||||
{
|
||||
std::swap(Chars[i - minchar], Chars[win1252map[i - 0x80] - minchar]);
|
||||
}
|
||||
|
@ -509,24 +509,24 @@ void FFont::CheckCase()
|
|||
}
|
||||
if (myislower(chr))
|
||||
{
|
||||
if (Chars[i].TranslatedPic != nullptr) lowercount++;
|
||||
if (Chars[i].OriginalPic != nullptr) lowercount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Chars[i].TranslatedPic != nullptr) uppercount++;
|
||||
if (Chars[i].OriginalPic != nullptr) uppercount++;
|
||||
}
|
||||
}
|
||||
if (lowercount == 0) return; // This is an uppercase-only font and we are done.
|
||||
|
||||
// The ß needs special treatment because it is far more likely to be supplied lowercase only, even in an uppercase font.
|
||||
if (Chars[0xdf - FirstChar].TranslatedPic != nullptr)
|
||||
if (Chars[0xdf - FirstChar].OriginalPic != nullptr)
|
||||
{
|
||||
if (LastChar < 0x1e9e)
|
||||
{
|
||||
Chars.Resize(0x1e9f - FirstChar);
|
||||
LastChar = 0x1e9e;
|
||||
}
|
||||
if (Chars[0x1e9e - FirstChar].TranslatedPic == nullptr)
|
||||
if (Chars[0x1e9e - FirstChar].OriginalPic == nullptr)
|
||||
{
|
||||
std::swap(Chars[0xdf - FirstChar], Chars[0x1e9e - FirstChar]);
|
||||
lowercount--;
|
||||
|
@ -705,7 +705,7 @@ static int compare (const void *arg1, const void *arg2)
|
|||
//
|
||||
//==========================================================================
|
||||
|
||||
int FFont::SimpleTranslation (uint32_t *colorsused, uint8_t *translation, uint8_t *reverse, TArray<double> &Luminosity)
|
||||
int FFont::SimpleTranslation (uint32_t *colorsused, uint8_t *translation, uint8_t *reverse, TArray<double> &Luminosity, int* minlum, int* maxlum)
|
||||
{
|
||||
double min, max, diver;
|
||||
int i, j;
|
||||
|
@ -744,6 +744,8 @@ int FFont::SimpleTranslation (uint32_t *colorsused, uint8_t *translation, uint8_
|
|||
{
|
||||
Luminosity[i] = (Luminosity[i] - min) * diver;
|
||||
}
|
||||
if (minlum) *minlum = int(min);
|
||||
if (maxlum) *maxlum = int(max);
|
||||
|
||||
return j;
|
||||
}
|
||||
|
@ -889,7 +891,7 @@ int FFont::GetCharCode(int code, bool needpic) const
|
|||
// regular chars turn negative when the 8th bit is set.
|
||||
code &= 255;
|
||||
}
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].OriginalPic != nullptr))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
@ -903,7 +905,7 @@ int FFont::GetCharCode(int code, bool needpic) const
|
|||
if (myislower(code))
|
||||
{
|
||||
code = upperforlower[code];
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].OriginalPic != nullptr))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
@ -912,7 +914,7 @@ int FFont::GetCharCode(int code, bool needpic) const
|
|||
while ((newcode = stripaccent(code)) != code)
|
||||
{
|
||||
code = newcode;
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].OriginalPic != nullptr))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
@ -926,7 +928,7 @@ int FFont::GetCharCode(int code, bool needpic) const
|
|||
while ((newcode = stripaccent(code)) != code)
|
||||
{
|
||||
code = newcode;
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].OriginalPic != nullptr))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
@ -944,7 +946,7 @@ int FFont::GetCharCode(int code, bool needpic) const
|
|||
while ((newcode = stripaccent(code)) != code)
|
||||
{
|
||||
code = newcode;
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].TranslatedPic != nullptr))
|
||||
if (code >= FirstChar && code <= LastChar && (!needpic || Chars[code - FirstChar].OriginalPic != nullptr))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
@ -979,7 +981,7 @@ FGameTexture *FFont::GetChar (int code, int translation, int *const width, bool
|
|||
if (code < 0) return nullptr;
|
||||
|
||||
|
||||
if ((translation == CR_UNTRANSLATED || translation == CR_UNDEFINED) && !forceremap)
|
||||
if ((translation == CR_UNTRANSLATED || translation == CR_UNDEFINED || translation >= NumTextColors) && !forceremap)
|
||||
{
|
||||
bool redirect = Chars[code].OriginalPic && Chars[code].OriginalPic != Chars[code].TranslatedPic;
|
||||
if (redirected) *redirected = redirect;
|
||||
|
@ -990,6 +992,11 @@ FGameTexture *FFont::GetChar (int code, int translation, int *const width, bool
|
|||
}
|
||||
}
|
||||
if (redirected) *redirected = false;
|
||||
if (IsLuminosityTranslation(Translations[translation]))
|
||||
{
|
||||
assert(Chars[code].OriginalPic->GetUseType() == ETextureType::FontChar);
|
||||
return Chars[code].OriginalPic;
|
||||
}
|
||||
assert(Chars[code].TranslatedPic->GetUseType() == ETextureType::FontChar);
|
||||
return Chars[code].TranslatedPic;
|
||||
}
|
||||
|
@ -1177,26 +1184,25 @@ void FFont::LoadTranslations()
|
|||
|
||||
for (unsigned int i = 0; i < count; i++)
|
||||
{
|
||||
if (Chars[i].TranslatedPic)
|
||||
if (Chars[i].OriginalPic)
|
||||
{
|
||||
FFontChar1 *pic = static_cast<FFontChar1 *>(Chars[i].TranslatedPic->GetTexture()->GetImage());
|
||||
if (pic)
|
||||
{
|
||||
pic->SetSourceRemap(nullptr); // Force the FFontChar1 to return the same pixels as the base texture
|
||||
RecordTextureColors(pic, usedcolors);
|
||||
}
|
||||
auto pic = Chars[i].OriginalPic->GetTexture()->GetImage();
|
||||
if (pic) RecordTextureColors(pic, usedcolors);
|
||||
}
|
||||
}
|
||||
|
||||
ActiveColors = SimpleTranslation (usedcolors, PatchRemap, identity, Luminosity);
|
||||
int minlum = 0, maxlum = 0;
|
||||
ActiveColors = SimpleTranslation (usedcolors, PatchRemap, identity, Luminosity, &minlum, &maxlum);
|
||||
|
||||
for (unsigned int i = 0; i < count; i++)
|
||||
// Here we can set everything to a luminosity translation.
|
||||
|
||||
// Create different translations for different color ranges
|
||||
Translations.Resize(NumTextColors);
|
||||
for (int i = 0; i < NumTextColors; i++)
|
||||
{
|
||||
if(Chars[i].TranslatedPic)
|
||||
static_cast<FFontChar1 *>(Chars[i].TranslatedPic->GetTexture()->GetImage())->SetSourceRemap(PatchRemap);
|
||||
if (i == CR_UNTRANSLATED) Translations[i] = 0;
|
||||
else Translations[i] = LuminosityTranslation(i*2, minlum, maxlum);
|
||||
}
|
||||
|
||||
BuildTranslations (Luminosity.Data(), identity, &TranslationParms[TranslationType][0], ActiveColors, nullptr);
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
|
|
|
@ -161,14 +161,10 @@ void FSpecialFont::LoadTranslations()
|
|||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
if (Chars[i].TranslatedPic)
|
||||
if (Chars[i].OriginalPic)
|
||||
{
|
||||
FFontChar1 *pic = static_cast<FFontChar1 *>(Chars[i].TranslatedPic->GetTexture()->GetImage());
|
||||
if (pic)
|
||||
{
|
||||
pic->SetSourceRemap(nullptr); // Force the FFontChar1 to return the same pixels as the base texture
|
||||
RecordTextureColors(pic, usedcolors);
|
||||
}
|
||||
auto pic = Chars[i].OriginalPic->GetTexture()->GetImage();
|
||||
if (pic) RecordTextureColors(pic, usedcolors);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -586,6 +586,107 @@ EColorRange V_FindFontColor (FName name)
|
|||
return CR_UNTRANSLATED;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
//
|
||||
// CreateLuminosityTranslationRanges
|
||||
//
|
||||
// Create universal remap ranges for hardware rendering.
|
||||
//
|
||||
//==========================================================================
|
||||
static PalEntry* paletteptr;
|
||||
|
||||
static void CreateLuminosityTranslationRanges()
|
||||
{
|
||||
paletteptr = (PalEntry*)ImageArena.Alloc(256 * ((NumTextColors * 2)) * sizeof(PalEntry));
|
||||
for (int l = 0; l < 2; l++)
|
||||
{
|
||||
auto parmstart = &TranslationParms[l][0];
|
||||
// Put the data into the image arena where it gets deleted with the rest of the texture data.
|
||||
for (int p = 0; p < NumTextColors; p++)
|
||||
{
|
||||
// Intended storage order is Range 1, variant 1 - Range 1, variant 2, Range 2, variant 1, and so on.
|
||||
// The storage of the ranges forces us to go through this differently...
|
||||
PalEntry* palette = paletteptr + p * 512 + l * 256;
|
||||
for (int v = 0; v < 256; v++)
|
||||
{
|
||||
palette[v].b = palette[v].g = palette[v].r = (uint8_t)v;
|
||||
}
|
||||
if (p != CR_UNTRANSLATED) // This table skips the untranslated entry. Do I need to say that the stored data format is garbage? >)
|
||||
{
|
||||
for (int v = 0; v < 256; v++)
|
||||
{
|
||||
// Find the color range that this luminosity value lies within.
|
||||
const TranslationParm* parms = parmstart - 1;
|
||||
do
|
||||
{
|
||||
parms++;
|
||||
if (parms->RangeStart <= v && parms->RangeEnd >= v)
|
||||
break;
|
||||
} while (parms[1].RangeStart > parms[0].RangeEnd);
|
||||
|
||||
// Linearly interpolate to find out which color this luminosity level gets.
|
||||
int rangev = ((v - parms->RangeStart) << 8) / (parms->RangeEnd - parms->RangeStart);
|
||||
int r = ((parms->Start[0] << 8) + rangev * (parms->End[0] - parms->Start[0])) >> 8; // red
|
||||
int g = ((parms->Start[1] << 8) + rangev * (parms->End[1] - parms->Start[1])) >> 8; // green
|
||||
int b = ((parms->Start[2] << 8) + rangev * (parms->End[2] - parms->Start[2])) >> 8; // blue
|
||||
palette[v].r = (uint8_t)clamp(r, 0, 255);
|
||||
palette[v].g = (uint8_t)clamp(g, 0, 255);
|
||||
palette[v].b = (uint8_t)clamp(b, 0, 255);
|
||||
}
|
||||
// Advance to the next color range.
|
||||
while (parmstart[1].RangeStart > parmstart[0].RangeEnd)
|
||||
{
|
||||
parmstart++;
|
||||
}
|
||||
parmstart++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
//
|
||||
// V_ApplyLuminosityTranslation
|
||||
//
|
||||
// Applies the translation to a bitmap for texture generation.
|
||||
//
|
||||
//==========================================================================
|
||||
|
||||
void V_ApplyLuminosityTranslation(int translation, uint8_t* pixel, int size)
|
||||
{
|
||||
int colorrange = (translation >> 16) & 0x3fff;
|
||||
if (colorrange >= NumTextColors * 2) return;
|
||||
int lum_min = (translation >> 8) & 0xff;
|
||||
int lum_max = translation & 0xff;
|
||||
int lum_range = (lum_max - lum_min + 1);
|
||||
PalEntry* remap = paletteptr + colorrange * 256;
|
||||
|
||||
for (int i = 0; i < size; i++, pixel += 4)
|
||||
{
|
||||
// we must also process the transparent pixels here to ensure proper filtering on the characters' edges.
|
||||
int gray = PalEntry(255, pixel[2], pixel[1], pixel[0]).Luminance();
|
||||
int lumadjust = (gray - lum_min) * 255 / lum_range;
|
||||
int index = clamp(lumadjust, 0, 255);
|
||||
PalEntry newcol = remap[index];
|
||||
// extend the range if we find colors outside what initial analysis provided.
|
||||
if (gray < lum_min)
|
||||
{
|
||||
newcol.r = newcol.r * gray / lum_min;
|
||||
newcol.g = newcol.g * gray / lum_min;
|
||||
newcol.b = newcol.b * gray / lum_min;
|
||||
}
|
||||
else if (gray > lum_max)
|
||||
{
|
||||
newcol.r = clamp(newcol.r * gray / lum_max, 0, 255);
|
||||
newcol.g = clamp(newcol.g * gray / lum_max, 0, 255);
|
||||
newcol.b = clamp(newcol.b * gray / lum_max, 0, 255);
|
||||
}
|
||||
pixel[0] = newcol.b;
|
||||
pixel[1] = newcol.g;
|
||||
pixel[2] = newcol.r;
|
||||
}
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
//
|
||||
// V_LogColorFromColorRange
|
||||
|
@ -676,6 +777,7 @@ EColorRange V_ParseFontColor (const uint8_t *&color_value, int normalcolor, int
|
|||
|
||||
void V_InitFonts()
|
||||
{
|
||||
CreateLuminosityTranslationRanges();
|
||||
V_InitCustomFonts();
|
||||
|
||||
FFont *CreateHexLumpFont(const char *fontname, int lump);
|
||||
|
|
|
@ -142,7 +142,7 @@ protected:
|
|||
void FixXMoves();
|
||||
|
||||
static int SimpleTranslation (uint32_t *colorsused, uint8_t *translation,
|
||||
uint8_t *identity, TArray<double> &Luminosity);
|
||||
uint8_t *identity, TArray<double> &Luminosity, int* minlum = nullptr, int* maxlum = nullptr);
|
||||
|
||||
void ReadSheetFont(TArray<FolderEntry> &folderdata, int width, int height, const DVector2 &Scale);
|
||||
|
||||
|
@ -193,5 +193,6 @@ FFont *V_GetFont(const char *fontname, const char *fontlumpname = nullptr);
|
|||
void V_InitFontColors();
|
||||
char* CleanseString(char* str);
|
||||
void V_LoadTranslations();
|
||||
class FBitmap;
|
||||
|
||||
|
||||
|
|
|
@ -198,6 +198,11 @@ public:
|
|||
return Pitch;
|
||||
}
|
||||
|
||||
int GetBufferSize() const
|
||||
{
|
||||
return Pitch * Height;
|
||||
}
|
||||
|
||||
const uint8_t *GetPixels() const
|
||||
{
|
||||
return data;
|
||||
|
|
|
@ -65,8 +65,16 @@ private:
|
|||
// This is needed for allowing the client to allocate slots that aren't matched to a palette, e.g. Build's indexed variants.
|
||||
if (translation >= 0)
|
||||
{
|
||||
auto remap = GPalette.TranslationToTable(translation);
|
||||
translation = remap == nullptr ? 0 : remap->Index;
|
||||
if (!IsLuminosityTranslation(translation))
|
||||
{
|
||||
auto remap = GPalette.TranslationToTable(translation);
|
||||
translation = remap == nullptr ? 0 : remap->Index;
|
||||
}
|
||||
else
|
||||
{
|
||||
// only needs to preserve the color range plus an identifier for marking this a luminosity translation.
|
||||
translation = ((translation >> 16) & 0x3fff) | 0xff0000;
|
||||
}
|
||||
}
|
||||
else translation &= ~0x7fffffff;
|
||||
|
||||
|
|
|
@ -321,6 +321,7 @@ bool FTexture::ProcessData(unsigned char* buffer, int w, int h, bool ispatch)
|
|||
// Initializes the buffer for the texture data
|
||||
//
|
||||
//===========================================================================
|
||||
void V_ApplyLuminosityTranslation(int translation, uint8_t *buffer, int size);
|
||||
|
||||
FTextureBuffer FTexture::CreateTexBuffer(int translation, int flags)
|
||||
{
|
||||
|
@ -356,7 +357,7 @@ FTextureBuffer FTexture::CreateTexBuffer(int translation, int flags)
|
|||
buffer = new unsigned char[W * (H + 1) * 4];
|
||||
memset(buffer, 0, W * (H + 1) * 4);
|
||||
|
||||
auto remap = translation <= 0 ? nullptr : GPalette.TranslationToTable(translation);
|
||||
auto remap = translation <= 0 || IsLuminosityTranslation(translation) ? nullptr : GPalette.TranslationToTable(translation);
|
||||
if (remap && remap->Inactive) remap = nullptr;
|
||||
if (remap) translation = remap->Index;
|
||||
FBitmap bmp(buffer, W * 4, W, H);
|
||||
|
@ -364,6 +365,10 @@ FTextureBuffer FTexture::CreateTexBuffer(int translation, int flags)
|
|||
int trans;
|
||||
auto Pixels = GetBgraBitmap(remap ? remap->Palette : nullptr, &trans);
|
||||
bmp.Blit(exx, exx, Pixels);
|
||||
if (IsLuminosityTranslation(translation))
|
||||
{
|
||||
V_ApplyLuminosityTranslation(translation, buffer, W * H);
|
||||
}
|
||||
|
||||
if (remap == nullptr)
|
||||
{
|
||||
|
|
|
@ -331,21 +331,29 @@ bool PickTexture(FRenderState *state, FGameTexture* tex, int paletteid, TextureP
|
|||
{
|
||||
if (!tex->isValid() || tex->GetTexelWidth() <= 0 || tex->GetTexelHeight() <= 0) return false;
|
||||
|
||||
int usepalette = paletteid == 0? 0 : GetTranslationType(paletteid) - Translation_Remap;
|
||||
int usepalswap = GetTranslationIndex(paletteid);
|
||||
int usepalette = 0, useremap = 0;
|
||||
if (!IsLuminosityTranslation(paletteid))
|
||||
{
|
||||
usepalette = paletteid == 0 ? 0 : GetTranslationType(paletteid) - Translation_Remap;
|
||||
useremap = GetTranslationIndex(paletteid);
|
||||
}
|
||||
bool foggy = state && (state->GetFogColor() & 0xffffff);
|
||||
int TextureType = hw_int_useindexedcolortextures && !foggy? TT_INDEXED : TT_TRUECOLOR;
|
||||
|
||||
pick.translation = paletteid;
|
||||
pick.basepalTint = 0xffffff;
|
||||
|
||||
auto& h = lookups.tables[usepalswap];
|
||||
auto& h = lookups.tables[useremap];
|
||||
bool applytint = false;
|
||||
// Canvas textures must be treated like hightile replacements in the following code.
|
||||
|
||||
int hipalswap = usepalette >= 0 ? usepalswap : 0;
|
||||
int hipalswap = usepalette >= 0 ? useremap : 0;
|
||||
auto rep = (hw_hightile && !(h.tintFlags & TINTF_ALWAYSUSEART)) ? FindReplacement(tex->GetID(), hipalswap, false) : nullptr;
|
||||
if (rep || tex->GetTexture()->isHardwareCanvas())
|
||||
if (IsLuminosityTranslation(paletteid))
|
||||
{
|
||||
// For a luminosity translation we only want the plain texture as-is.
|
||||
}
|
||||
else if (rep || tex->GetTexture()->isHardwareCanvas())
|
||||
{
|
||||
if (usepalette > 0)
|
||||
{
|
||||
|
@ -371,9 +379,9 @@ bool PickTexture(FRenderState *state, FGameTexture* tex, int paletteid, TextureP
|
|||
if (h.tintFlags & (TINTF_ALWAYSUSEART | TINTF_USEONART))
|
||||
{
|
||||
applytint = true;
|
||||
if (!(h.tintFlags & TINTF_APPLYOVERPALSWAP)) usepalswap = 0;
|
||||
if (!(h.tintFlags & TINTF_APPLYOVERPALSWAP)) useremap = 0;
|
||||
}
|
||||
pick.translation = paletteid == 0? 0 : TRANSLATION(usepalette + Translation_Remap, usepalswap);
|
||||
pick.translation = paletteid == 0? 0 : TRANSLATION(usepalette + Translation_Remap, useremap);
|
||||
}
|
||||
else pick.translation |= 0x80000000;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue