Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions Core/GameEngineDevice/Include/W3DDevice/GameClient/TileData.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,25 @@ typedef struct {

#define INVERTED_MASK 0x1 //AND this with TBlendTileInfo.inverted to get actual inverted state
#define FLIPPED_MASK 0x2 //AND this with TBlendTileInfo.inverted to get forced flip state (for horizontal/vertical flips).
#define TILE_PIXEL_EXTENT 64
// GeneralsX @feature mrkinglollipop 11/07/2026 Bumps base tile resolution 64->256 (4x per axis, 16x pixels).
// The mip chain grows from 6 fixed levels (32/16/8/4/2/1) to 8 chained levels
// (128/64/32/16/8/4/2/1) so doMip() still halves exactly once per level down to 1x1.
// LEGACY_TILE_PIXEL_EXTENT (see WorldHeightMap.cpp) is the pre-patch 64px grid cell
// size that unmodified base/custom-map TGAs are still packed in; the loader detects
// and nearest-neighbor upscales those on read so they keep working unmodified.
#define TILE_PIXEL_EXTENT 256
#define TILE_BYTES_PER_PIXEL 4
#define DATA_LEN_BYTES TILE_PIXEL_EXTENT*TILE_PIXEL_EXTENT*TILE_BYTES_PER_PIXEL
#define DATA_LEN_PIXELS TILE_PIXEL_EXTENT*TILE_PIXEL_EXTENT
#define TILE_PIXEL_EXTENT_MIP1 32
#define TILE_PIXEL_EXTENT_MIP2 16
#define TILE_PIXEL_EXTENT_MIP3 8
#define TILE_PIXEL_EXTENT_MIP4 4
#define TILE_PIXEL_EXTENT_MIP5 2
#define TILE_PIXEL_EXTENT_MIP6 1
#define TEXTURE_WIDTH 2048 // was 1024 jba
#define TILE_PIXEL_EXTENT_MIP1 128
#define TILE_PIXEL_EXTENT_MIP2 64
#define TILE_PIXEL_EXTENT_MIP3 32
#define TILE_PIXEL_EXTENT_MIP4 16
#define TILE_PIXEL_EXTENT_MIP5 8
#define TILE_PIXEL_EXTENT_MIP6 4
#define TILE_PIXEL_EXTENT_MIP7 2
#define TILE_PIXEL_EXTENT_MIP8 1
#define TEXTURE_WIDTH 8192 // GeneralsX @feature mrkinglollipop 11/07/2026 Bumps atlas TEXTURE_WIDTH 2048->8192 (was 2048). MoltenVK/Metal cap is 16384.

/** This class holds the bitmap data from the .tga texture files. It is used to
create the D3D texture in the game and 3d windows, and to create DIB data for the
Expand All @@ -68,13 +76,15 @@ class TileData : public RefCountClass
// Also, first byte is lower left pixel, not upper left pixel.
// so 0,0 is lower left, not upper left.
UnsignedByte m_tileData[DATA_LEN_BYTES];
/// Mipped down copies of the tile data.
UnsignedByte m_tileDataMip32[TILE_PIXEL_EXTENT_MIP1*TILE_PIXEL_EXTENT_MIP1*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip16[TILE_PIXEL_EXTENT_MIP2*TILE_PIXEL_EXTENT_MIP2*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip8[TILE_PIXEL_EXTENT_MIP3*TILE_PIXEL_EXTENT_MIP3*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip4[TILE_PIXEL_EXTENT_MIP4*TILE_PIXEL_EXTENT_MIP4*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip2[TILE_PIXEL_EXTENT_MIP5*TILE_PIXEL_EXTENT_MIP5*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip1[TILE_PIXEL_EXTENT_MIP6*TILE_PIXEL_EXTENT_MIP6*TILE_BYTES_PER_PIXEL];
/// Mipped down copies of the tile data. 8-level chain: 128/64/32/16/8/4/2/1.
UnsignedByte m_tileDataMip128[TILE_PIXEL_EXTENT_MIP1*TILE_PIXEL_EXTENT_MIP1*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip64[TILE_PIXEL_EXTENT_MIP2*TILE_PIXEL_EXTENT_MIP2*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip32[TILE_PIXEL_EXTENT_MIP3*TILE_PIXEL_EXTENT_MIP3*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip16[TILE_PIXEL_EXTENT_MIP4*TILE_PIXEL_EXTENT_MIP4*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip8[TILE_PIXEL_EXTENT_MIP5*TILE_PIXEL_EXTENT_MIP5*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip4[TILE_PIXEL_EXTENT_MIP6*TILE_PIXEL_EXTENT_MIP6*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip2[TILE_PIXEL_EXTENT_MIP7*TILE_PIXEL_EXTENT_MIP7*TILE_BYTES_PER_PIXEL];
UnsignedByte m_tileDataMip1[TILE_PIXEL_EXTENT_MIP8*TILE_PIXEL_EXTENT_MIP8*TILE_BYTES_PER_PIXEL];

public:
ICoord2D m_tileLocationInTexture;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,12 @@ class WorldHeightMap : public RefCountClass,
if ((ndx>=0) && (ndx<m_dataSize) && m_data) m_data[ndx]=height;
};
public: // Read tile utilities. jba [7/9/2003]
static Bool readTiles(InputStream *pStrm, TileData **tiles, Int numRows);
static Int countTiles(InputStream *pStrm, Bool *halfTile=nullptr);
// GeneralsX @feature mrkinglollipop 11/07/2026 Adds expectedTileCount/pIsLegacyGrid so the caller's
// INI-declared tile count can disambiguate legacy 64px-grid TGAs from native
// TILE_PIXEL_EXTENT (256px)-grid TGAs; isLegacyGrid must be forwarded from
// countTiles' result into readTiles so both use the same interpretation of the file.
static Bool readTiles(InputStream *pStrm, TileData **tiles, Int numRows, Bool isLegacyGrid=false);
static Int countTiles(InputStream *pStrm, Bool *halfTile=nullptr, Int expectedTileCount=0, Bool *pIsLegacyGrid=nullptr);

protected:
void setCliffState(Int xIndex, Int yIndex, Bool state);
Expand Down
52 changes: 34 additions & 18 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/TileData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,17 @@ TileData::TileData()

}

#define TILE_PIXEL_EXTENT_MIP1 32
#define TILE_PIXEL_EXTENT_MIP2 16
#define TILE_PIXEL_EXTENT_MIP3 8
#define TILE_PIXEL_EXTENT_MIP4 4
#define TILE_PIXEL_EXTENT_MIP5 2
#define TILE_PIXEL_EXTENT_MIP6 1
// GeneralsX @feature mrkinglollipop 11/07/2026 Bumps this local mip-extent copy to match TileData.h's
// 8-level chain (128/64/32/16/8/4/2/1); shadows the header's #defines (as it did
// before this change) -- kept in sync manually.
#define TILE_PIXEL_EXTENT_MIP1 128
#define TILE_PIXEL_EXTENT_MIP2 64
#define TILE_PIXEL_EXTENT_MIP3 32
#define TILE_PIXEL_EXTENT_MIP4 16
#define TILE_PIXEL_EXTENT_MIP5 8
#define TILE_PIXEL_EXTENT_MIP6 4
#define TILE_PIXEL_EXTENT_MIP7 2
#define TILE_PIXEL_EXTENT_MIP8 1

Bool TileData::hasRGBDataForWidth(Int width)
{
Expand All @@ -59,29 +64,40 @@ Bool TileData::hasRGBDataForWidth(Int width)
if (width == TILE_PIXEL_EXTENT_MIP4) return(true);
if (width == TILE_PIXEL_EXTENT_MIP5) return(true);
if (width == TILE_PIXEL_EXTENT_MIP6) return(true);
if (width == TILE_PIXEL_EXTENT_MIP7) return(true);
if (width == TILE_PIXEL_EXTENT_MIP8) return(true);
return(false);
}

UnsignedByte * TileData::getRGBDataForWidth(Int width)
{
// default
if (width == TILE_PIXEL_EXTENT_MIP1) return(m_tileDataMip32);
if (width == TILE_PIXEL_EXTENT_MIP2) return(m_tileDataMip16);
if (width == TILE_PIXEL_EXTENT_MIP3) return(m_tileDataMip8);
if (width == TILE_PIXEL_EXTENT_MIP4) return(m_tileDataMip4);
if (width == TILE_PIXEL_EXTENT_MIP5) return(m_tileDataMip2);
if (width == TILE_PIXEL_EXTENT_MIP6) return(m_tileDataMip1);
if (width == TILE_PIXEL_EXTENT_MIP1) return(m_tileDataMip128);
if (width == TILE_PIXEL_EXTENT_MIP2) return(m_tileDataMip64);
if (width == TILE_PIXEL_EXTENT_MIP3) return(m_tileDataMip32);
if (width == TILE_PIXEL_EXTENT_MIP4) return(m_tileDataMip16);
if (width == TILE_PIXEL_EXTENT_MIP5) return(m_tileDataMip8);
if (width == TILE_PIXEL_EXTENT_MIP6) return(m_tileDataMip4);
if (width == TILE_PIXEL_EXTENT_MIP7) return(m_tileDataMip2);
if (width == TILE_PIXEL_EXTENT_MIP8) return(m_tileDataMip1);
return(m_tileData);
}

void TileData::updateMips()
{
doMip(m_tileData, TILE_PIXEL_EXTENT, m_tileDataMip32);
doMip(m_tileDataMip32, TILE_PIXEL_EXTENT_MIP1, m_tileDataMip16);
doMip(m_tileDataMip16, TILE_PIXEL_EXTENT_MIP2, m_tileDataMip8);
doMip(m_tileDataMip8, TILE_PIXEL_EXTENT_MIP3, m_tileDataMip4);
doMip(m_tileDataMip4, TILE_PIXEL_EXTENT_MIP4, m_tileDataMip2);
doMip(m_tileDataMip2, TILE_PIXEL_EXTENT_MIP5, m_tileDataMip1);
// GeneralsX @build mrkinglollipop 11/07/2026 Chains each mip level to exactly half the width of its
// parent, so doMip's hiRow/2 addressing stays correct all the way down to 1x1.
// A naive bump of TILE_PIXEL_EXTENT without adding the 128/64 levels would make
// the first doMip() call write a 128x128 result into the old 32x32
// m_tileDataMip32 buffer -- a heap/member buffer overflow. Do not collapse levels.
doMip(m_tileData, TILE_PIXEL_EXTENT, m_tileDataMip128);
doMip(m_tileDataMip128, TILE_PIXEL_EXTENT_MIP1, m_tileDataMip64);
doMip(m_tileDataMip64, TILE_PIXEL_EXTENT_MIP2, m_tileDataMip32);
doMip(m_tileDataMip32, TILE_PIXEL_EXTENT_MIP3, m_tileDataMip16);
doMip(m_tileDataMip16, TILE_PIXEL_EXTENT_MIP4, m_tileDataMip8);
doMip(m_tileDataMip8, TILE_PIXEL_EXTENT_MIP5, m_tileDataMip4);
doMip(m_tileDataMip4, TILE_PIXEL_EXTENT_MIP6, m_tileDataMip2);
doMip(m_tileDataMip2, TILE_PIXEL_EXTENT_MIP7, m_tileDataMip1);
}


Expand Down
166 changes: 126 additions & 40 deletions Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,8 @@ void WorldHeightMap::readTexClass(TXTextureClass *texClass, TileData **tileData)
if (theFile != nullptr) {
GDIFileStream theStream(theFile);
InputStream *pStr = &theStream;
Int numTiles = WorldHeightMap::countTiles(pStr);
Bool isLegacyGrid = false;
Int numTiles = WorldHeightMap::countTiles(pStr, nullptr, texClass->numTiles, &isLegacyGrid);
theFile->seek(0, File::START);
if (numTiles >= texClass->numTiles) {
numTiles = texClass->numTiles;
Expand All @@ -1022,7 +1023,7 @@ void WorldHeightMap::readTexClass(TXTextureClass *texClass, TileData **tileData)
break;
}
}
WorldHeightMap::readTiles(pStr, tileData+texClass->firstTile, width);
WorldHeightMap::readTiles(pStr, tileData+texClass->firstTile, width, isLegacyGrid);
}
theFile->close();
}
Expand Down Expand Up @@ -1301,17 +1302,69 @@ typedef struct {



// GeneralsX @feature mrkinglollipop 11/07/2026 Adds legacy-grid detection so TGAs packed at the pre-patch
// 64px cell size keep loading now that TILE_PIXEL_EXTENT is 256 (native HD grid cell).
// Unmodified base-game and custom-map TGAs are still packed as NxN grids of
// LEGACY_TILE_PIXEL_EXTENT (64px) cells -- the classic pre-patch tile size. Without a
// fallback, a legacy file's imageWidth/TILE_PIXEL_EXTENT floors to 0 and the file is
// silently rejected (0 tiles), breaking every unmodified map/mod.
//
// Detection rule (documented per plan): try the *native* 256px-cell grid first, but
// only trust it when it alone can satisfy the texture class's INI-declared tile count
// (expectedTileCount). This resolves the one genuinely ambiguous width -- 256px, which
// is simultaneously a valid native 1x1 HD grid AND a valid legacy 4x4 grid of 64px
// cells (16 tiles) -- because the base game genuinely ships 256px-wide legacy sheets
// (width census: 32, 64, 128, 256, 640). Any other width where both interpretations
// divide evenly is treated as legacy too (matches actual on-disk content) -- this is
// the "treat legacy files as 64-grids by default" simplification. The old "halfTile"
// sub-tile case (a lone 32x32 image = half of a legacy 64px cell) keeps keying off the
// legacy half-extent (32), not a new native half-extent, so it matches the same files
// it always did.
#define LEGACY_TILE_PIXEL_EXTENT 64

static Bool ChooseTileGrid(Short imageWidth, Short imageHeight, Int expectedTileCount,
Int &outTileWidth, Int &outTileHeight, Bool &outIsLegacyGrid)
{
Int nativeW = imageWidth / TILE_PIXEL_EXTENT;
Int nativeH = imageHeight / TILE_PIXEL_EXTENT;
Bool nativeValid = (nativeW>=1 && nativeW<=10 && nativeH>=1 && nativeH<=10);

Int legacyW = imageWidth / LEGACY_TILE_PIXEL_EXTENT;
Int legacyH = imageHeight / LEGACY_TILE_PIXEL_EXTENT;
Bool legacyValid = (legacyW>=1 && legacyW<=10 && legacyH>=1 && legacyH<=10);

Bool useLegacy;
if (nativeValid && legacyValid) {
// Ambiguous width: only trust native when the class can't possibly want more
// tiles than the (smaller) native grid provides -- i.e. this really is a fresh
// HD single/few-tile replacement, not an unmodified legacy sheet.
useLegacy = !(expectedTileCount > 0 && expectedTileCount <= nativeW*nativeH);
} else if (legacyValid) {
useLegacy = true;
} else if (nativeValid) {
useLegacy = false;
} else {
return false;
}

outIsLegacyGrid = useLegacy;
outTileWidth = useLegacy ? legacyW : nativeW;
outTileHeight = useLegacy ? legacyH : nativeH;
return true;
}

/// Count how many tiles come in from a targa file.
Int WorldHeightMap::countTiles(InputStream *pStr, Bool *halfTile)
Int WorldHeightMap::countTiles(InputStream *pStr, Bool *halfTile, Int expectedTileCount, Bool *pIsLegacyGrid)
{
TTargaHeader hdr;
if (halfTile) {
*halfTile = false;
}
if (pIsLegacyGrid) {
*pIsLegacyGrid = false;
}
Int len = pStr->read(&hdr,sizeof(hdr));
if (len!=sizeof(hdr)) return(0);
Int tileWidth = hdr.imageWidth/TILE_PIXEL_EXTENT;
Int tileHeight = hdr.imageHeight/TILE_PIXEL_EXTENT;

if (hdr.colorMapType != 0) {
return(0); // we don't do indexed at this time. jba.
Expand All @@ -1322,41 +1375,65 @@ Int WorldHeightMap::countTiles(InputStream *pStr, Bool *halfTile)

if (hdr.pixelDepth < 24) return(false);
if (hdr.pixelDepth > 32) return(false);
// 3x3 gives 9,
// 2x2 gives 4,
// 1x1 gives 1,
// else 0;
if (tileWidth>10 || tileHeight>10) return(0); // don't do huge images, or bad files.
if (tileWidth>=10 && tileHeight >=10) return(100);
if (tileWidth>=9 && tileHeight >=9) return(81);
if (tileWidth>=8 && tileHeight >=8) return(64);
if (tileWidth>=7 && tileHeight >=7) return(49);
if (tileWidth>=6 && tileHeight >=6) return(36);
if (tileWidth>=5 && tileHeight >=5) return(25);
if (tileWidth>=4 && tileHeight >=4) return(16);
if (tileWidth>=3 && tileHeight >=3) return(9);
if (tileWidth>=2 && tileHeight >=2) return(4);
if (tileWidth>=1 && tileHeight >=1) return(1);
if (halfTile && hdr.imageHeight==TILE_PIXEL_EXTENT/2 && hdr.imageWidth==TILE_PIXEL_EXTENT/2) {

Int tileWidth, tileHeight;
Bool isLegacyGrid = false;
if (ChooseTileGrid(hdr.imageWidth, hdr.imageHeight, expectedTileCount, tileWidth, tileHeight, isLegacyGrid)) {
if (pIsLegacyGrid) *pIsLegacyGrid = isLegacyGrid;
// 3x3 gives 9,
// 2x2 gives 4,
// 1x1 gives 1,
// else 0;
if (tileWidth>=10 && tileHeight >=10) return(100);
if (tileWidth>=9 && tileHeight >=9) return(81);
if (tileWidth>=8 && tileHeight >=8) return(64);
if (tileWidth>=7 && tileHeight >=7) return(49);
if (tileWidth>=6 && tileHeight >=6) return(36);
if (tileWidth>=5 && tileHeight >=5) return(25);
if (tileWidth>=4 && tileHeight >=4) return(16);
if (tileWidth>=3 && tileHeight >=3) return(9);
if (tileWidth>=2 && tileHeight >=2) return(4);
if (tileWidth>=1 && tileHeight >=1) return(1);
}
// Neither native nor legacy full-cell grid fit -- last resort: a lone image half
// as wide/tall as one LEGACY cell (32x32), always legacy-scattered.
if (halfTile && hdr.imageHeight==LEGACY_TILE_PIXEL_EXTENT/2 && hdr.imageWidth==LEGACY_TILE_PIXEL_EXTENT/2) {
*halfTile = true;
if (pIsLegacyGrid) *pIsLegacyGrid = true;
return 1;
}
return(0);
}
/*Break down a .tga file into a collection of tiles. numRows * numRows total tiles.*/
Bool WorldHeightMap::readTiles(InputStream *pStr, TileData **tiles, Int numRows)
/*Break down a .tga file into a collection of tiles. numRows * numRows total tiles.
isLegacyGrid must be the interpretation countTiles() already chose for this same
file (via ChooseTileGrid) -- readTiles does not re-derive it, so the two stay
consistent even in the ambiguous-width case. */
Bool WorldHeightMap::readTiles(InputStream *pStr, TileData **tiles, Int numRows, Bool isLegacyGrid)
{
TTargaHeader hdr;
pStr->read(&hdr, sizeof(hdr));
Int tileWidth = hdr.imageWidth/TILE_PIXEL_EXTENT;
Int tileHeight = hdr.imageHeight/TILE_PIXEL_EXTENT;

if (hdr.imageHeight==TILE_PIXEL_EXTENT/2) {
// Effective source-cell pixel size for this file's grid. scale is how many times
// each decoded source pixel is nearest-neighbor replicated per axis into the
// native TILE_PIXEL_EXTENT destination cell; scale==1 (no scatter, byte-identical
// to the pre-patch loop) for native HD content.
Int srcCellExtent = isLegacyGrid ? LEGACY_TILE_PIXEL_EXTENT : TILE_PIXEL_EXTENT;

Int tileWidth = hdr.imageWidth/srcCellExtent;
Int tileHeight = hdr.imageHeight/srcCellExtent;

// Legacy "halfTile" sub-tile case: a lone image half as wide/tall as one legacy
// cell (32x32) represents a single tile; scatter it across the full native tile.
if (isLegacyGrid && hdr.imageHeight==srcCellExtent/2) {
tileHeight = 1;
}
if (hdr.imageWidth==TILE_PIXEL_EXTENT/2) {
if (isLegacyGrid && hdr.imageWidth==srcCellExtent/2) {
tileWidth = 1;
}
if (isLegacyGrid && (hdr.imageWidth==LEGACY_TILE_PIXEL_EXTENT/2 || hdr.imageHeight==LEGACY_TILE_PIXEL_EXTENT/2)) {
srcCellExtent = LEGACY_TILE_PIXEL_EXTENT/2;
}
Int scale = TILE_PIXEL_EXTENT / srcCellExtent;

if (tileWidth<numRows && tileHeight<numRows) {
return(false);
Expand All @@ -1379,7 +1456,7 @@ Bool WorldHeightMap::readTiles(InputStream *pStr, TileData **tiles, Int numRows)
int repeatCount = 0;
// Bool read = false;
Bool running = false;
for (row = 0; row < numRows*TILE_PIXEL_EXTENT; row++) {
for (row = 0; row < numRows*srcCellExtent; row++) {
for (column=0; column<hdr.imageWidth; column++) {
UnsignedByte r, g, b, a;
if (row < hdr.imageHeight) {
Expand Down Expand Up @@ -1408,18 +1485,27 @@ Bool WorldHeightMap::readTiles(InputStream *pStr, TileData **tiles, Int numRows)
} else {
r = g = b = a = 0;
}
if (column >= (numRows*TILE_PIXEL_EXTENT)) continue;
int tileNdx = (column/TILE_PIXEL_EXTENT) + numRows*(row/TILE_PIXEL_EXTENT);
int pixelNdx = (column%TILE_PIXEL_EXTENT) + TILE_PIXEL_EXTENT*(row%TILE_PIXEL_EXTENT);

UnsignedByte *pixel = tiles[tileNdx]->getDataPtr();

pixel += pixelNdx*TILE_BYTES_PER_PIXEL;
*pixel++ = b;
*pixel++ = g;
*pixel++ = r;
*pixel = a;

if (column >= (numRows*srcCellExtent)) continue;
int tileNdx = (column/srcCellExtent) + numRows*(row/srcCellExtent);
int cellCol = column % srcCellExtent;
int cellRow = row % srcCellExtent;

UnsignedByte *tileBase = tiles[tileNdx]->getDataPtr();
// Nearest-neighbor scatter: replicate this one decoded source pixel into
// its scale x scale destination block. No row lookahead needed -- the
// whole block is written immediately from the single decoded pixel.
for (int dy=0; dy<scale; dy++) {
int destRow = cellRow*scale+dy;
for (int dx=0; dx<scale; dx++) {
int destCol = cellCol*scale+dx;
int pixelNdx = destCol + TILE_PIXEL_EXTENT*destRow;
UnsignedByte *pixel = tileBase + pixelNdx*TILE_BYTES_PER_PIXEL;
pixel[0] = b;
pixel[1] = g;
pixel[2] = r;
pixel[3] = a;
}
}
}
DEBUG_ASSERTCRASH(repeatCount==0, ("Invalid tga."));
}
Expand Down
Loading