1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "mapimages.h"
4
5#include <base/log.h>
6
7#include <engine/graphics.h>
8#include <engine/map.h>
9#include <engine/storage.h>
10#include <engine/textrender.h>
11
12#include <game/client/gameclient.h>
13#include <game/generated/client_data.h>
14#include <game/layers.h>
15#include <game/localization.h>
16#include <game/mapitems.h>
17
18const char *const gs_apModEntitiesNames[] = {
19 "ddnet",
20 "ddrace",
21 "race",
22 "blockworlds",
23 "fng",
24 "vanilla",
25 "f-ddrace",
26};
27
28CMapImages::CMapImages() :
29 CMapImages(100)
30{
31}
32
33CMapImages::CMapImages(int TextureSize)
34{
35 m_Count = 0;
36 m_TextureScale = TextureSize;
37 mem_zero(block: m_aEntitiesIsLoaded, size: sizeof(m_aEntitiesIsLoaded));
38 m_SpeedupArrowIsLoaded = false;
39
40 mem_zero(block: m_aTextureUsedByTileOrQuadLayerFlag, size: sizeof(m_aTextureUsedByTileOrQuadLayerFlag));
41
42 str_copy(dst&: m_aEntitiesPath, src: "editor/entities_clear");
43
44 static_assert(std::size(gs_apModEntitiesNames) == MAP_IMAGE_MOD_TYPE_COUNT, "Mod name string count is not equal to mod type count");
45}
46
47void CMapImages::OnInit()
48{
49 InitOverlayTextures();
50
51 if(str_comp(a: g_Config.m_ClAssetsEntities, b: "default") == 0)
52 str_copy(dst&: m_aEntitiesPath, src: "editor/entities_clear");
53 else
54 {
55 str_format(buffer: m_aEntitiesPath, buffer_size: sizeof(m_aEntitiesPath), format: "assets/entities/%s", g_Config.m_ClAssetsEntities);
56 }
57}
58
59void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap)
60{
61 // unload all textures
62 for(int i = 0; i < m_Count; i++)
63 {
64 Graphics()->UnloadTexture(pIndex: &(m_aTextures[i]));
65 m_aTextureUsedByTileOrQuadLayerFlag[i] = 0;
66 }
67 m_Count = 0;
68
69 int Start;
70 pMap->GetType(Type: MAPITEMTYPE_IMAGE, pStart: &Start, pNum: &m_Count);
71
72 m_Count = clamp<int>(val: m_Count, lo: 0, hi: MAX_MAPIMAGES);
73
74 for(int g = 0; g < pLayers->NumGroups(); g++)
75 {
76 CMapItemGroup *pGroup = pLayers->GetGroup(Index: g);
77 if(!pGroup)
78 {
79 continue;
80 }
81
82 for(int l = 0; l < pGroup->m_NumLayers; l++)
83 {
84 CMapItemLayer *pLayer = pLayers->GetLayer(Index: pGroup->m_StartLayer + l);
85 if(pLayer->m_Type == LAYERTYPE_TILES)
86 {
87 CMapItemLayerTilemap *pTLayer = (CMapItemLayerTilemap *)pLayer;
88 if(pTLayer->m_Image >= 0 && pTLayer->m_Image < m_Count)
89 {
90 m_aTextureUsedByTileOrQuadLayerFlag[pTLayer->m_Image] |= 1;
91 }
92 }
93 else if(pLayer->m_Type == LAYERTYPE_QUADS)
94 {
95 CMapItemLayerQuads *pQLayer = (CMapItemLayerQuads *)pLayer;
96 if(pQLayer->m_Image >= 0 && pQLayer->m_Image < m_Count)
97 {
98 m_aTextureUsedByTileOrQuadLayerFlag[pQLayer->m_Image] |= 2;
99 }
100 }
101 }
102 }
103
104 const int TextureLoadFlag = Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
105
106 // load new textures
107 bool ShowWarning = false;
108 for(int i = 0; i < m_Count; i++)
109 {
110 const int LoadFlag = (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 1) != 0) ? TextureLoadFlag : 0) | (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 2) != 0) ? 0 : (Graphics()->HasTextureArraysSupport() ? IGraphics::TEXLOAD_NO_2D_TEXTURE : 0));
111 const CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pMap->GetItem(Index: Start + i);
112
113 const char *pName = pMap->GetDataString(Index: pImg->m_ImageName);
114 if(pName == nullptr || pName[0] == '\0')
115 {
116 if(pImg->m_External)
117 {
118 log_error("mapimages", "Failed to load map image %d: failed to load name.", i);
119 ShowWarning = true;
120 continue;
121 }
122 pName = "(error)";
123 }
124
125 if(pImg->m_Version > 1 && pImg->m_MustBe1 != 1)
126 {
127 log_error("mapimages", "Failed to load map image %d '%s': invalid map image type.", i, pName);
128 ShowWarning = true;
129 continue;
130 }
131
132 if(pImg->m_External)
133 {
134 char aPath[IO_MAX_PATH_LENGTH];
135 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "mapres/%s.png", pName);
136 m_aTextures[i] = Graphics()->LoadTexture(pFilename: aPath, StorageType: IStorage::TYPE_ALL, Flags: LoadFlag);
137 }
138 else
139 {
140 CImageInfo ImageInfo;
141 ImageInfo.m_Width = pImg->m_Width;
142 ImageInfo.m_Height = pImg->m_Height;
143 ImageInfo.m_Format = CImageInfo::FORMAT_RGBA;
144 ImageInfo.m_pData = static_cast<uint8_t *>(pMap->GetData(Index: pImg->m_ImageData));
145 char aTexName[IO_MAX_PATH_LENGTH];
146 str_format(buffer: aTexName, buffer_size: sizeof(aTexName), format: "embedded: %s", pName);
147 m_aTextures[i] = Graphics()->LoadTextureRaw(Image: ImageInfo, Flags: LoadFlag, pTexName: aTexName);
148 pMap->UnloadData(Index: pImg->m_ImageData);
149 }
150 pMap->UnloadData(Index: pImg->m_ImageName);
151 ShowWarning = ShowWarning || m_aTextures[i].IsNullTexture();
152 }
153 if(ShowWarning)
154 {
155 Client()->AddWarning(Warning: SWarning(Localize(pStr: "Some map images could not be loaded. Check the local console for details.")));
156 }
157}
158
159void CMapImages::OnMapLoad()
160{
161 IMap *pMap = Kernel()->RequestInterface<IMap>();
162 CLayers *pLayers = m_pClient->Layers();
163 OnMapLoadImpl(pLayers, pMap);
164}
165
166void CMapImages::LoadBackground(class CLayers *pLayers, class IMap *pMap)
167{
168 OnMapLoadImpl(pLayers, pMap);
169}
170
171static EMapImageModType GetEntitiesModType(const CGameInfo &GameInfo)
172{
173 if(GameInfo.m_EntitiesFDDrace)
174 return MAP_IMAGE_MOD_TYPE_FDDRACE;
175 else if(GameInfo.m_EntitiesDDNet)
176 return MAP_IMAGE_MOD_TYPE_DDNET;
177 else if(GameInfo.m_EntitiesDDRace)
178 return MAP_IMAGE_MOD_TYPE_DDRACE;
179 else if(GameInfo.m_EntitiesRace)
180 return MAP_IMAGE_MOD_TYPE_RACE;
181 else if(GameInfo.m_EntitiesBW)
182 return MAP_IMAGE_MOD_TYPE_BLOCKWORLDS;
183 else if(GameInfo.m_EntitiesFNG)
184 return MAP_IMAGE_MOD_TYPE_FNG;
185 else if(GameInfo.m_EntitiesVanilla)
186 return MAP_IMAGE_MOD_TYPE_VANILLA;
187 else
188 return MAP_IMAGE_MOD_TYPE_DDNET;
189}
190
191static bool IsValidTile(int LayerType, bool EntitiesAreMasked, EMapImageModType EntitiesModType, int TileIndex)
192{
193 if(TileIndex == TILE_AIR)
194 return false;
195 if(!EntitiesAreMasked)
196 return true;
197
198 if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || EntitiesModType == MAP_IMAGE_MOD_TYPE_DDRACE)
199 {
200 if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || TileIndex != TILE_BOOST)
201 {
202 if(LayerType == MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH &&
203 !IsValidGameTile(Index: TileIndex) &&
204 !IsValidFrontTile(Index: TileIndex) &&
205 !IsValidSpeedupTile(Index: TileIndex) &&
206 !IsValidTeleTile(Index: TileIndex) &&
207 !IsValidTuneTile(Index: TileIndex))
208 {
209 return false;
210 }
211 else if(LayerType == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH &&
212 !IsValidSwitchTile(Index: TileIndex))
213 {
214 return false;
215 }
216 }
217 }
218 else if(EntitiesModType == MAP_IMAGE_MOD_TYPE_RACE && IsCreditsTile(TileIndex))
219 {
220 return false;
221 }
222 else if(EntitiesModType == MAP_IMAGE_MOD_TYPE_FNG && IsCreditsTile(TileIndex))
223 {
224 return false;
225 }
226 else if(EntitiesModType == MAP_IMAGE_MOD_TYPE_VANILLA && IsCreditsTile(TileIndex))
227 {
228 return false;
229 }
230 return true;
231}
232
233IGraphics::CTextureHandle CMapImages::GetEntities(EMapImageEntityLayerType EntityLayerType)
234{
235 const bool EntitiesAreMasked = !GameClient()->m_GameInfo.m_DontMaskEntities;
236 const EMapImageModType EntitiesModType = GetEntitiesModType(GameInfo: GameClient()->m_GameInfo);
237
238 if(!m_aEntitiesIsLoaded[(EntitiesModType * 2) + (int)EntitiesAreMasked])
239 {
240 m_aEntitiesIsLoaded[(EntitiesModType * 2) + (int)EntitiesAreMasked] = true;
241
242 int TextureLoadFlag = 0;
243 if(Graphics()->HasTextureArraysSupport())
244 TextureLoadFlag = (Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE;
245
246 CImageInfo ImgInfo;
247 char aPath[IO_MAX_PATH_LENGTH];
248 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.png", m_aEntitiesPath, gs_apModEntitiesNames[EntitiesModType]);
249 Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
250
251 // try as single ddnet replacement
252 if(ImgInfo.m_pData == nullptr && EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET)
253 {
254 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s.png", m_aEntitiesPath);
255 Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
256 }
257
258 // try default
259 if(ImgInfo.m_pData == nullptr)
260 {
261 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "editor/entities_clear/%s.png", gs_apModEntitiesNames[EntitiesModType]);
262 Graphics()->LoadPng(Image&: ImgInfo, pFilename: aPath, StorageType: IStorage::TYPE_ALL);
263 }
264
265 if(ImgInfo.m_pData != nullptr)
266 {
267 CImageInfo BuildImageInfo;
268 BuildImageInfo.m_Width = ImgInfo.m_Width;
269 BuildImageInfo.m_Height = ImgInfo.m_Height;
270 BuildImageInfo.m_Format = ImgInfo.m_Format;
271 BuildImageInfo.m_pData = static_cast<uint8_t *>(malloc(size: BuildImageInfo.DataSize()));
272
273 // build game layer
274 for(int LayerType = 0; LayerType < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++LayerType)
275 {
276 dbg_assert(!m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][LayerType].IsValid(), "entities texture already loaded when it should not be");
277
278 // set everything transparent
279 mem_zero(block: BuildImageInfo.m_pData, size: BuildImageInfo.DataSize());
280
281 for(int i = 0; i < 256; ++i)
282 {
283 int TileIndex = i;
284 if(IsValidTile(LayerType, EntitiesAreMasked, EntitiesModType, TileIndex))
285 {
286 if(LayerType == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && TileIndex == TILE_SWITCHTIMEDOPEN)
287 {
288 TileIndex = 8;
289 }
290
291 const size_t CopyWidth = ImgInfo.m_Width / 16;
292 const size_t CopyHeight = ImgInfo.m_Height / 16;
293 const size_t OffsetX = (size_t)(TileIndex % 16) * CopyWidth;
294 const size_t OffsetY = (size_t)(TileIndex / 16) * CopyHeight;
295 Graphics()->CopyTextureBufferSub(pDestBuffer: BuildImageInfo.m_pData, SourceImage: ImgInfo, SubOffsetX: OffsetX, SubOffsetY: OffsetY, SubCopyWidth: CopyWidth, SubCopyHeight: CopyHeight);
296 }
297 }
298
299 m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][LayerType] = Graphics()->LoadTextureRaw(Image: BuildImageInfo, Flags: TextureLoadFlag, pTexName: aPath);
300 }
301
302 BuildImageInfo.Free();
303 ImgInfo.Free();
304 }
305 }
306
307 return m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][EntityLayerType];
308}
309
310IGraphics::CTextureHandle CMapImages::GetSpeedupArrow()
311{
312 if(!m_SpeedupArrowIsLoaded)
313 {
314 int TextureLoadFlag = (Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE;
315 m_SpeedupArrowTexture = Graphics()->LoadTexture(pFilename: "editor/speed_arrow_array.png", StorageType: IStorage::TYPE_ALL, Flags: TextureLoadFlag);
316 m_SpeedupArrowIsLoaded = true;
317 }
318 return m_SpeedupArrowTexture;
319}
320
321IGraphics::CTextureHandle CMapImages::GetOverlayBottom()
322{
323 return m_OverlayBottomTexture;
324}
325
326IGraphics::CTextureHandle CMapImages::GetOverlayTop()
327{
328 return m_OverlayTopTexture;
329}
330
331IGraphics::CTextureHandle CMapImages::GetOverlayCenter()
332{
333 return m_OverlayCenterTexture;
334}
335
336void CMapImages::ChangeEntitiesPath(const char *pPath)
337{
338 if(str_comp(a: pPath, b: "default") == 0)
339 str_copy(dst&: m_aEntitiesPath, src: "editor/entities_clear");
340 else
341 {
342 str_format(buffer: m_aEntitiesPath, buffer_size: sizeof(m_aEntitiesPath), format: "assets/entities/%s", pPath);
343 }
344
345 for(int ModType = 0; ModType < MAP_IMAGE_MOD_TYPE_COUNT * 2; ++ModType)
346 {
347 if(m_aEntitiesIsLoaded[ModType])
348 {
349 for(int LayerType = 0; LayerType < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++LayerType)
350 {
351 if(m_aaEntitiesTextures[ModType][LayerType].IsValid())
352 {
353 Graphics()->UnloadTexture(pIndex: &(m_aaEntitiesTextures[ModType][LayerType]));
354 }
355 m_aaEntitiesTextures[ModType][LayerType] = IGraphics::CTextureHandle();
356 }
357
358 m_aEntitiesIsLoaded[ModType] = false;
359 }
360 }
361}
362
363void CMapImages::SetTextureScale(int Scale)
364{
365 if(m_TextureScale == Scale)
366 return;
367
368 m_TextureScale = Scale;
369
370 if(Graphics() && m_OverlayCenterTexture.IsValid()) // check if component was initialized
371 {
372 // reinitialize component
373 Graphics()->UnloadTexture(pIndex: &m_OverlayBottomTexture);
374 Graphics()->UnloadTexture(pIndex: &m_OverlayTopTexture);
375 Graphics()->UnloadTexture(pIndex: &m_OverlayCenterTexture);
376
377 m_OverlayBottomTexture = IGraphics::CTextureHandle();
378 m_OverlayTopTexture = IGraphics::CTextureHandle();
379 m_OverlayCenterTexture = IGraphics::CTextureHandle();
380
381 InitOverlayTextures();
382 }
383}
384
385int CMapImages::GetTextureScale() const
386{
387 return m_TextureScale;
388}
389
390IGraphics::CTextureHandle CMapImages::UploadEntityLayerText(int TextureSize, int MaxWidth, int YOffset)
391{
392 CImageInfo TextImage;
393 TextImage.m_Width = 1024;
394 TextImage.m_Height = 1024;
395 TextImage.m_Format = CImageInfo::FORMAT_RGBA;
396 TextImage.m_pData = static_cast<uint8_t *>(calloc(nmemb: TextImage.DataSize(), size: sizeof(uint8_t)));
397
398 UpdateEntityLayerText(TextImage, TextureSize, MaxWidth, YOffset, NumbersPower: 0);
399 UpdateEntityLayerText(TextImage, TextureSize, MaxWidth, YOffset, NumbersPower: 1);
400 UpdateEntityLayerText(TextImage, TextureSize, MaxWidth, YOffset, NumbersPower: 2, MaxNumber: 255);
401
402 const int TextureLoadFlag = (Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE;
403 return Graphics()->LoadTextureRawMove(Image&: TextImage, Flags: TextureLoadFlag);
404}
405
406void CMapImages::UpdateEntityLayerText(CImageInfo &TextImage, int TextureSize, int MaxWidth, int YOffset, int NumbersPower, int MaxNumber)
407{
408 char aBuf[4];
409 int DigitsCount = NumbersPower + 1;
410
411 int CurrentNumber = std::pow(x: 10, y: NumbersPower);
412
413 if(MaxNumber == -1)
414 MaxNumber = CurrentNumber * 10 - 1;
415
416 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", CurrentNumber);
417
418 int CurrentNumberSuitableFontSize = TextRender()->AdjustFontSize(pText: aBuf, TextLength: DigitsCount, MaxSize: TextureSize, MaxWidth);
419 int UniversalSuitableFontSize = CurrentNumberSuitableFontSize * 0.92f; // should be smoothed enough to fit any digits combination
420
421 YOffset += ((TextureSize - UniversalSuitableFontSize) / 2);
422
423 for(; CurrentNumber <= MaxNumber; ++CurrentNumber)
424 {
425 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", CurrentNumber);
426
427 float x = (CurrentNumber % 16) * 64;
428 float y = (CurrentNumber / 16) * 64;
429
430 int ApproximateTextWidth = TextRender()->CalculateTextWidth(pText: aBuf, TextLength: DigitsCount, FontWidth: 0, FontSize: UniversalSuitableFontSize);
431 int XOffSet = (MaxWidth - clamp(val: ApproximateTextWidth, lo: 0, hi: MaxWidth)) / 2;
432
433 TextRender()->UploadEntityLayerText(TextImage, TexSubWidth: (TextImage.m_Width / 16) - XOffSet, TexSubHeight: (TextImage.m_Height / 16) - YOffset, pText: aBuf, Length: DigitsCount, x: x + XOffSet, y: y + YOffset, FontSize: UniversalSuitableFontSize);
434 }
435}
436
437void CMapImages::InitOverlayTextures()
438{
439 int TextureSize = 64 * m_TextureScale / 100;
440 TextureSize = clamp(val: TextureSize, lo: 2, hi: 64);
441 int TextureToVerticalCenterOffset = (64 - TextureSize) / 2; // should be used to move texture to the center of 64 pixels area
442
443 if(!m_OverlayBottomTexture.IsValid())
444 {
445 m_OverlayBottomTexture = UploadEntityLayerText(TextureSize: TextureSize / 2, MaxWidth: 64, YOffset: 32 + TextureToVerticalCenterOffset / 2);
446 }
447
448 if(!m_OverlayTopTexture.IsValid())
449 {
450 m_OverlayTopTexture = UploadEntityLayerText(TextureSize: TextureSize / 2, MaxWidth: 64, YOffset: TextureToVerticalCenterOffset / 2);
451 }
452
453 if(!m_OverlayCenterTexture.IsValid())
454 {
455 m_OverlayCenterTexture = UploadEntityLayerText(TextureSize, MaxWidth: 64, YOffset: TextureToVerticalCenterOffset);
456 }
457}
458