ddnet/src/game/client/components/mapimages.cpp
Robert Müller dde45f7a40 Add CImageInfo::PixelSize function, use enum EImageFormat
Use `enum EImageFormat` type for image format literals and variables.

Add `PixelSize` function to get the number of bytes/color channels per pixel for a specified image format.

Remove unused store format argument of texture loading functions. All textures are automatically being stored as RGBA, so the argument was unused. Also remove the therefore unused `FORMAT_AUTO`.

Rename variables consistently to `PixelSize` and use `size_t`, instead of mixing different names like `BPP` and `ColorChannelCount`.

Validate image format loaded from maps using `CImageInfo::ImageFormatFromInt`. Add `FORMAT_ERROR` to represent invalid formats.

Remove redundant `PixelSize` parameter from graphics backends and commands, which can be derived from the texture format.

Fix memory leak when RGB image data is being converted to RGBA format when saving map in editor.
2023-09-03 20:40:28 +02:00

493 lines
16 KiB
C++

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <engine/graphics.h>
#include <engine/map.h>
#include <engine/storage.h>
#include <engine/textrender.h>
#include <game/generated/client_data.h>
#include <game/mapitems.h>
#include <game/layers.h>
#include "mapimages.h"
#include <game/client/gameclient.h>
const char *const gs_apModEntitiesNames[] = {
"ddnet",
"ddrace",
"race",
"blockworlds",
"fng",
"vanilla",
"f-ddrace",
};
CMapImages::CMapImages() :
CMapImages(100)
{
}
CMapImages::CMapImages(int TextureSize)
{
m_Count = 0;
m_TextureScale = TextureSize;
mem_zero(m_aEntitiesIsLoaded, sizeof(m_aEntitiesIsLoaded));
m_SpeedupArrowIsLoaded = false;
mem_zero(m_aTextureUsedByTileOrQuadLayerFlag, sizeof(m_aTextureUsedByTileOrQuadLayerFlag));
str_copy(m_aEntitiesPath, "editor/entities_clear");
static_assert(std::size(gs_apModEntitiesNames) == MAP_IMAGE_MOD_TYPE_COUNT, "Mod name string count is not equal to mod type count");
}
void CMapImages::OnInit()
{
InitOverlayTextures();
if(str_comp(g_Config.m_ClAssetsEntities, "default") == 0)
str_copy(m_aEntitiesPath, "editor/entities_clear");
else
{
str_format(m_aEntitiesPath, sizeof(m_aEntitiesPath), "assets/entities/%s", g_Config.m_ClAssetsEntities);
}
}
void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap)
{
// unload all textures
for(int i = 0; i < m_Count; i++)
{
Graphics()->UnloadTexture(&(m_aTextures[i]));
m_aTextureUsedByTileOrQuadLayerFlag[i] = 0;
}
m_Count = 0;
int Start;
pMap->GetType(MAPITEMTYPE_IMAGE, &Start, &m_Count);
m_Count = clamp(m_Count, 0, 64);
for(int g = 0; g < pLayers->NumGroups(); g++)
{
CMapItemGroup *pGroup = pLayers->GetGroup(g);
if(!pGroup)
{
continue;
}
for(int l = 0; l < pGroup->m_NumLayers; l++)
{
CMapItemLayer *pLayer = pLayers->GetLayer(pGroup->m_StartLayer + l);
if(pLayer->m_Type == LAYERTYPE_TILES)
{
CMapItemLayerTilemap *pTLayer = (CMapItemLayerTilemap *)pLayer;
if(pTLayer->m_Image != -1 && pTLayer->m_Image < (int)(std::size(m_aTextures)))
{
m_aTextureUsedByTileOrQuadLayerFlag[pTLayer->m_Image] |= 1;
}
}
else if(pLayer->m_Type == LAYERTYPE_QUADS)
{
CMapItemLayerQuads *pQLayer = (CMapItemLayerQuads *)pLayer;
if(pQLayer->m_Image != -1 && pQLayer->m_Image < (int)(std::size(m_aTextures)))
{
m_aTextureUsedByTileOrQuadLayerFlag[pQLayer->m_Image] |= 2;
}
}
}
}
int TextureLoadFlag = Graphics()->HasTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
// load new textures
for(int i = 0; i < m_Count; i++)
{
int LoadFlag = (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 1) != 0) ? TextureLoadFlag : 0) | (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 2) != 0) ? 0 : (Graphics()->IsTileBufferingEnabled() ? IGraphics::TEXLOAD_NO_2D_TEXTURE : 0));
const CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pMap->GetItem(Start + i);
const CImageInfo::EImageFormat Format = pImg->m_Version < CMapItemImage_v2::CURRENT_VERSION ? CImageInfo::FORMAT_RGBA : CImageInfo::ImageFormatFromInt(pImg->m_Format);
if(pImg->m_External)
{
char aPath[IO_MAX_PATH_LENGTH];
char *pName = (char *)pMap->GetData(pImg->m_ImageName);
str_format(aPath, sizeof(aPath), "mapres/%s.png", pName);
m_aTextures[i] = Graphics()->LoadTexture(aPath, IStorage::TYPE_ALL, LoadFlag);
pMap->UnloadData(pImg->m_ImageName);
}
else if(Format != CImageInfo::FORMAT_RGBA)
{
m_aTextures[i] = Graphics()->InvalidTexture();
pMap->UnloadData(pImg->m_ImageName);
}
else
{
void *pData = pMap->GetData(pImg->m_ImageData);
char *pName = (char *)pMap->GetData(pImg->m_ImageName);
char aTexName[128];
str_format(aTexName, sizeof(aTexName), "%s %s", "embedded:", pName);
m_aTextures[i] = Graphics()->LoadTextureRaw(pImg->m_Width, pImg->m_Height, Format, pData, LoadFlag, aTexName);
pMap->UnloadData(pImg->m_ImageName);
pMap->UnloadData(pImg->m_ImageData);
}
}
}
void CMapImages::OnMapLoad()
{
IMap *pMap = Kernel()->RequestInterface<IMap>();
CLayers *pLayers = m_pClient->Layers();
OnMapLoadImpl(pLayers, pMap);
}
void CMapImages::LoadBackground(class CLayers *pLayers, class IMap *pMap)
{
OnMapLoadImpl(pLayers, pMap);
}
bool CMapImages::HasFrontLayer(EMapImageModType ModType)
{
return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE;
}
bool CMapImages::HasSpeedupLayer(EMapImageModType ModType)
{
return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE;
}
bool CMapImages::HasSwitchLayer(EMapImageModType ModType)
{
return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE;
}
bool CMapImages::HasTeleLayer(EMapImageModType ModType)
{
return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE;
}
bool CMapImages::HasTuneLayer(EMapImageModType ModType)
{
return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE;
}
IGraphics::CTextureHandle CMapImages::GetEntities(EMapImageEntityLayerType EntityLayerType)
{
EMapImageModType EntitiesModType = MAP_IMAGE_MOD_TYPE_DDNET;
bool EntitiesAreMasked = !GameClient()->m_GameInfo.m_DontMaskEntities;
if(GameClient()->m_GameInfo.m_EntitiesFDDrace)
EntitiesModType = MAP_IMAGE_MOD_TYPE_FDDRACE;
else if(GameClient()->m_GameInfo.m_EntitiesDDNet)
EntitiesModType = MAP_IMAGE_MOD_TYPE_DDNET;
else if(GameClient()->m_GameInfo.m_EntitiesDDRace)
EntitiesModType = MAP_IMAGE_MOD_TYPE_DDRACE;
else if(GameClient()->m_GameInfo.m_EntitiesRace)
EntitiesModType = MAP_IMAGE_MOD_TYPE_RACE;
else if(GameClient()->m_GameInfo.m_EntitiesBW)
EntitiesModType = MAP_IMAGE_MOD_TYPE_BLOCKWORLDS;
else if(GameClient()->m_GameInfo.m_EntitiesFNG)
EntitiesModType = MAP_IMAGE_MOD_TYPE_FNG;
else if(GameClient()->m_GameInfo.m_EntitiesVanilla)
EntitiesModType = MAP_IMAGE_MOD_TYPE_VANILLA;
if(!m_aEntitiesIsLoaded[(EntitiesModType * 2) + (int)EntitiesAreMasked])
{
m_aEntitiesIsLoaded[(EntitiesModType * 2) + (int)EntitiesAreMasked] = true;
// any mod that does not mask, will get all layers unmasked
bool WasUnknown = !EntitiesAreMasked;
char aPath[64];
str_format(aPath, sizeof(aPath), "%s/%s.png", m_aEntitiesPath, gs_apModEntitiesNames[EntitiesModType]);
bool GameTypeHasFrontLayer = HasFrontLayer(EntitiesModType) || WasUnknown;
bool GameTypeHasSpeedupLayer = HasSpeedupLayer(EntitiesModType) || WasUnknown;
bool GameTypeHasSwitchLayer = HasSwitchLayer(EntitiesModType) || WasUnknown;
bool GameTypeHasTeleLayer = HasTeleLayer(EntitiesModType) || WasUnknown;
bool GameTypeHasTuneLayer = HasTuneLayer(EntitiesModType) || WasUnknown;
int TextureLoadFlag = 0;
if(Graphics()->IsTileBufferingEnabled())
TextureLoadFlag = (Graphics()->HasTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE;
CImageInfo ImgInfo;
bool ImagePNGLoaded = false;
if(Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL))
ImagePNGLoaded = true;
else
{
bool TryDefault = true;
// try as single ddnet replacement
if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET)
{
str_format(aPath, sizeof(aPath), "%s.png", m_aEntitiesPath);
if(Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL))
{
ImagePNGLoaded = true;
TryDefault = false;
}
}
if(!ImagePNGLoaded && TryDefault)
{
// try default
str_format(aPath, sizeof(aPath), "editor/entities_clear/%s.png", gs_apModEntitiesNames[EntitiesModType]);
if(Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL))
{
ImagePNGLoaded = true;
}
}
}
if(ImagePNGLoaded && ImgInfo.m_Width > 0 && ImgInfo.m_Height > 0)
{
const size_t PixelSize = ImgInfo.PixelSize();
const size_t BuildImageSize = (size_t)ImgInfo.m_Width * ImgInfo.m_Height * PixelSize;
uint8_t *pTmpImgData = (uint8_t *)ImgInfo.m_pData;
uint8_t *pBuildImgData = (uint8_t *)malloc(BuildImageSize);
// build game layer
for(int n = 0; n < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++n)
{
bool BuildThisLayer = true;
if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH && !GameTypeHasFrontLayer &&
!GameTypeHasSpeedupLayer && !GameTypeHasTeleLayer && !GameTypeHasTuneLayer)
BuildThisLayer = false;
else if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && !GameTypeHasSwitchLayer)
BuildThisLayer = false;
dbg_assert(!m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][n].IsValid(), "entities texture already loaded when it should not be");
if(BuildThisLayer)
{
// set everything transparent
mem_zero(pBuildImgData, BuildImageSize);
for(int i = 0; i < 256; ++i)
{
bool ValidTile = i != 0;
int TileIndex = i;
if(EntitiesAreMasked)
{
if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || EntitiesModType == MAP_IMAGE_MOD_TYPE_DDRACE)
{
if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || TileIndex != TILE_BOOST)
{
if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH && !IsValidGameTile((int)TileIndex) && !IsValidFrontTile((int)TileIndex) && !IsValidSpeedupTile((int)TileIndex) &&
!IsValidTeleTile((int)TileIndex) && !IsValidTuneTile((int)TileIndex))
ValidTile = false;
else if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH)
{
if(!IsValidSwitchTile((int)TileIndex))
ValidTile = false;
}
}
}
else if((EntitiesModType == MAP_IMAGE_MOD_TYPE_RACE) && IsCreditsTile((int)TileIndex))
{
ValidTile = false;
}
else if((EntitiesModType == MAP_IMAGE_MOD_TYPE_FNG) && IsCreditsTile((int)TileIndex))
{
ValidTile = false;
}
else if((EntitiesModType == MAP_IMAGE_MOD_TYPE_VANILLA) && IsCreditsTile((int)TileIndex))
{
ValidTile = false;
}
}
if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || EntitiesModType == MAP_IMAGE_MOD_TYPE_DDRACE)
{
if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && TileIndex == TILE_SWITCHTIMEDOPEN)
TileIndex = 8;
}
int X = TileIndex % 16;
int Y = TileIndex / 16;
int CopyWidth = ImgInfo.m_Width / 16;
int CopyHeight = ImgInfo.m_Height / 16;
if(ValidTile)
{
Graphics()->CopyTextureBufferSub(pBuildImgData, pTmpImgData, ImgInfo.m_Width, ImgInfo.m_Height, PixelSize, (size_t)X * CopyWidth, (size_t)Y * CopyHeight, CopyWidth, CopyHeight);
}
}
m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][n] = Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, pBuildImgData, TextureLoadFlag, aPath);
}
else
{
if(!m_TransparentTexture.IsValid())
{
// set everything transparent
mem_zero(pBuildImgData, BuildImageSize);
m_TransparentTexture = Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, pBuildImgData, TextureLoadFlag, aPath);
}
m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][n] = m_TransparentTexture;
}
}
free(pBuildImgData);
Graphics()->FreePNG(&ImgInfo);
}
}
return m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][EntityLayerType];
}
IGraphics::CTextureHandle CMapImages::GetSpeedupArrow()
{
if(!m_SpeedupArrowIsLoaded)
{
int TextureLoadFlag = (Graphics()->HasTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE_SINGLE_LAYER : IGraphics::TEXLOAD_TO_3D_TEXTURE_SINGLE_LAYER) | IGraphics::TEXLOAD_NO_2D_TEXTURE;
m_SpeedupArrowTexture = Graphics()->LoadTexture(g_pData->m_aImages[IMAGE_SPEEDUP_ARROW].m_pFilename, IStorage::TYPE_ALL, TextureLoadFlag);
m_SpeedupArrowIsLoaded = true;
}
return m_SpeedupArrowTexture;
}
IGraphics::CTextureHandle CMapImages::GetOverlayBottom()
{
return m_OverlayBottomTexture;
}
IGraphics::CTextureHandle CMapImages::GetOverlayTop()
{
return m_OverlayTopTexture;
}
IGraphics::CTextureHandle CMapImages::GetOverlayCenter()
{
return m_OverlayCenterTexture;
}
void CMapImages::ChangeEntitiesPath(const char *pPath)
{
if(str_comp(pPath, "default") == 0)
str_copy(m_aEntitiesPath, "editor/entities_clear");
else
{
str_format(m_aEntitiesPath, sizeof(m_aEntitiesPath), "assets/entities/%s", pPath);
}
for(int i = 0; i < MAP_IMAGE_MOD_TYPE_COUNT * 2; ++i)
{
if(m_aEntitiesIsLoaded[i])
{
for(int n = 0; n < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++n)
{
if(m_aaEntitiesTextures[i][n].IsValid())
Graphics()->UnloadTexture(&(m_aaEntitiesTextures[i][n]));
m_aaEntitiesTextures[i][n] = IGraphics::CTextureHandle();
}
m_aEntitiesIsLoaded[i] = false;
}
}
}
void CMapImages::SetTextureScale(int Scale)
{
if(m_TextureScale == Scale)
return;
m_TextureScale = Scale;
if(Graphics() && m_OverlayCenterTexture.IsValid()) // check if component was initialized
{
// reinitialize component
Graphics()->UnloadTexture(&m_OverlayBottomTexture);
Graphics()->UnloadTexture(&m_OverlayTopTexture);
Graphics()->UnloadTexture(&m_OverlayCenterTexture);
m_OverlayBottomTexture = IGraphics::CTextureHandle();
m_OverlayTopTexture = IGraphics::CTextureHandle();
m_OverlayCenterTexture = IGraphics::CTextureHandle();
InitOverlayTextures();
}
}
int CMapImages::GetTextureScale()
{
return m_TextureScale;
}
IGraphics::CTextureHandle CMapImages::UploadEntityLayerText(int TextureSize, int MaxWidth, int YOffset)
{
const size_t Width = 1024;
const size_t Height = 1024;
const size_t PixelSize = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA);
void *pMem = calloc(Width * Height * PixelSize, 1);
UpdateEntityLayerText(pMem, PixelSize, Width, Height, TextureSize, MaxWidth, YOffset, 0);
UpdateEntityLayerText(pMem, PixelSize, Width, Height, TextureSize, MaxWidth, YOffset, 1);
UpdateEntityLayerText(pMem, PixelSize, Width, Height, TextureSize, MaxWidth, YOffset, 2, 255);
const int TextureLoadFlag = (Graphics()->HasTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE;
IGraphics::CTextureHandle Texture = Graphics()->LoadTextureRaw(Width, Height, CImageInfo::FORMAT_RGBA, pMem, TextureLoadFlag);
free(pMem);
return Texture;
}
void CMapImages::UpdateEntityLayerText(void *pTexBuffer, size_t PixelSize, size_t TexWidth, size_t TexHeight, int TextureSize, int MaxWidth, int YOffset, int NumbersPower, int MaxNumber)
{
char aBuf[4];
int DigitsCount = NumbersPower + 1;
int CurrentNumber = std::pow(10, NumbersPower);
if(MaxNumber == -1)
MaxNumber = CurrentNumber * 10 - 1;
str_from_int(CurrentNumber, aBuf);
int CurrentNumberSuitableFontSize = TextRender()->AdjustFontSize(aBuf, DigitsCount, TextureSize, MaxWidth);
int UniversalSuitableFontSize = CurrentNumberSuitableFontSize * 0.92f; // should be smoothed enough to fit any digits combination
YOffset += ((TextureSize - UniversalSuitableFontSize) / 2);
for(; CurrentNumber <= MaxNumber; ++CurrentNumber)
{
str_from_int(CurrentNumber, aBuf);
float x = (CurrentNumber % 16) * 64;
float y = (CurrentNumber / 16) * 64;
int ApproximateTextWidth = TextRender()->CalculateTextWidth(aBuf, DigitsCount, 0, UniversalSuitableFontSize);
int XOffSet = (MaxWidth - clamp(ApproximateTextWidth, 0, MaxWidth)) / 2;
TextRender()->UploadEntityLayerText(pTexBuffer, PixelSize, TexWidth, TexHeight, (TexWidth / 16) - XOffSet, (TexHeight / 16) - YOffset, aBuf, DigitsCount, x + XOffSet, y + YOffset, UniversalSuitableFontSize);
}
}
void CMapImages::InitOverlayTextures()
{
int TextureSize = 64 * m_TextureScale / 100;
TextureSize = clamp(TextureSize, 2, 64);
int TextureToVerticalCenterOffset = (64 - TextureSize) / 2; // should be used to move texture to the center of 64 pixels area
if(!m_OverlayBottomTexture.IsValid())
{
m_OverlayBottomTexture = UploadEntityLayerText(TextureSize / 2, 64, 32 + TextureToVerticalCenterOffset / 2);
}
if(!m_OverlayTopTexture.IsValid())
{
m_OverlayTopTexture = UploadEntityLayerText(TextureSize / 2, 64, TextureToVerticalCenterOffset / 2);
}
if(!m_OverlayCenterTexture.IsValid())
{
m_OverlayCenterTexture = UploadEntityLayerText(TextureSize, 64, TextureToVerticalCenterOffset);
}
}