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