1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Textures used by the landscape */
18
19#include <C4Include.h>
20#include <C4Texture.h>
21
22#include <C4SurfaceFile.h>
23#include <C4Group.h>
24#include <C4Game.h>
25#include <C4Config.h>
26#include <C4Components.h>
27#include <C4Application.h>
28#include <C4Material.h>
29#include <C4Landscape.h>
30#include <C4Wrappers.h>
31
32#include <format>
33
34C4Texture::C4Texture()
35{
36 Name[0] = 0;
37 Surface8 = nullptr;
38 Surface32 = nullptr;
39 Next = nullptr;
40}
41
42C4Texture::~C4Texture()
43{
44 delete Surface8;
45 delete Surface32;
46}
47
48C4TexMapEntry::C4TexMapEntry()
49 : pMaterial(nullptr), iMaterialIndex(MNone) {}
50
51void C4TexMapEntry::Clear()
52{
53 Material.Clear(); Texture.Clear();
54 iMaterialIndex = MNone;
55 pMaterial = nullptr;
56 MatPattern.Clear();
57}
58
59bool C4TexMapEntry::Create(const char *szMaterial, const char *szTexture)
60{
61 // Clear previous data
62 Clear();
63 // Save names
64 Material = szMaterial; Texture = szTexture;
65 return true;
66}
67
68bool C4TexMapEntry::Init()
69{
70 // Find material
71 iMaterialIndex = Game.Material.Get(szMaterial: Material.getData());
72 if (!MatValid(mat: iMaterialIndex))
73 {
74 DebugLog(level: spdlog::level::err, fmt: "Error initializing material {}-{}: Invalid material!", args: Material.getData(), args: Texture.getData());
75 return false;
76 }
77 pMaterial = &Game.Material.Map[iMaterialIndex];
78 // Special, hardcoded crap: change <liquid>-Smooth to <liquid>-Liquid
79 const char *szTexture = Texture.getData();
80 if (DensityLiquid(dens: pMaterial->Density))
81 if (SEqualNoCase(szStr1: szTexture, szStr2: "Smooth"))
82 szTexture = "Liquid";
83 // Find texture
84 C4Texture *sfcTexture = Game.TextureMap.GetTexture(szTexture);
85 if (!sfcTexture)
86 {
87 DebugLog(level: spdlog::level::err, fmt: "Error initializing material {}-{}: Invalid texture!", args: Material.getData(), args: Texture.getData());
88 Clear();
89 return false;
90 }
91 // Get overlay properties
92 int32_t iOverlayType = pMaterial->OverlayType;
93 bool fMono = !!(iOverlayType & C4MatOv_Monochrome);
94 int32_t iZoom = 0;
95 if (iOverlayType & C4MatOv_Exact) iZoom = 1;
96 if (iOverlayType & C4MatOv_HugeZoom) iZoom = 4;
97 // Create pattern
98 if (sfcTexture->Surface32)
99 MatPattern.Set(sfcSource: sfcTexture->Surface32, iZoom, fMonochrome: fMono);
100 else
101 MatPattern.Set(sfcSource: sfcTexture->Surface8, iZoom, fMonochrome: fMono);
102 MatPattern.SetColors(pClrs: pMaterial->Color, pAlpha: pMaterial->Alpha);
103 return true;
104}
105
106C4TextureMap::C4TextureMap()
107{
108 Default();
109}
110
111C4TextureMap::~C4TextureMap()
112{
113 Clear();
114}
115
116bool C4TextureMap::AddEntry(uint8_t byIndex, const char *szMaterial, const char *szTexture)
117{
118 // Security
119 if (byIndex <= 0 || byIndex >= C4M_MaxTexIndex)
120 return false;
121 if (!szMaterial || !szTexture)
122 return false;
123 // Set entry and initialize
124 Entry[byIndex].Create(szMaterial, szTexture);
125 if (fInitialized)
126 {
127 if (!Entry[byIndex].Init())
128 {
129 // Clear entry if it could not be initialized
130 Entry[byIndex].Clear();
131 return false;
132 }
133 // Landscape must be notified (new valid pixel clr)
134 Game.Landscape.HandleTexMapUpdate();
135 }
136 return true;
137}
138
139bool C4TextureMap::AddTexture(const char *szTexture, C4Surface *sfcSurface)
140{
141 auto texture = std::make_unique<C4Texture>();
142 SCopy(szSource: szTexture, sTarget: texture->Name, iMaxL: C4M_MaxName);
143 texture->Surface32 = sfcSurface;
144 texture->Next = FirstTexture;
145 FirstTexture = texture.release();
146 return true;
147}
148
149bool C4TextureMap::AddTexture(const char *szTexture, CSurface8 *sfcSurface)
150{
151 auto texture = std::make_unique<C4Texture>();
152 SCopy(szSource: szTexture, sTarget: texture->Name, iMaxL: C4M_MaxName);
153 texture->Surface8 = sfcSurface;
154 texture->Next = FirstTexture;
155 FirstTexture = texture.release();
156 return true;
157}
158
159void C4TextureMap::Clear()
160{
161 for (int32_t i = 1; i < C4M_MaxTexIndex; i++)
162 Entry[i].Clear();
163 C4Texture *ctex, *next2;
164 for (ctex = FirstTexture; ctex; ctex = next2)
165 {
166 next2 = ctex->Next;
167 delete ctex;
168 }
169 FirstTexture = nullptr;
170 fInitialized = false;
171}
172
173bool C4TextureMap::LoadFlags(C4Group &hGroup, const char *szEntryName, bool *pOverloadMaterials, bool *pOverloadTextures)
174{
175 // Load the file
176 StdStrBuf TexMap;
177 if (!hGroup.LoadEntryString(szEntryName, Buf&: TexMap))
178 return false;
179 // Reset flags
180 if (pOverloadMaterials) *pOverloadMaterials = false;
181 if (pOverloadTextures) *pOverloadTextures = false;
182 // Check if there are flags in there
183 for (const char *pPos = TexMap.getData(); pPos && *pPos; pPos = SSearch(szString: pPos, szIndex: "\n"))
184 {
185 // Go over newlines
186 while (*pPos == '\r' || *pPos == '\n') pPos++;
187 // Flag?
188 if (pOverloadMaterials && SEqual2(szStr1: pPos, szStr2: "OverloadMaterials"))
189 *pOverloadMaterials = true;
190 if (pOverloadTextures && SEqual2(szStr1: pPos, szStr2: "OverloadTextures"))
191 *pOverloadTextures = true;
192 }
193 // Done
194 return true;
195}
196
197int32_t C4TextureMap::LoadMap(C4Group &hGroup, const char *szEntryName, bool *pOverloadMaterials, bool *pOverloadTextures)
198{
199 char *bpMap;
200 char szLine[100 + 1];
201 int32_t cnt, iIndex, iTextures = 0;
202 // Load text file into memory
203 if (!hGroup.LoadEntry(szEntryName, lpbpBuf: &bpMap, ipSize: nullptr, iAppendZeros: 1)) return 0;
204 // Scan text buffer lines
205 for (cnt = 0; SCopySegment(fstr: bpMap, segn: cnt, tstr: szLine, sepa: 0x0A, iMaxL: 100); cnt++)
206 if ((szLine[0] != '#') && (SCharCount(cTarget: '=', szInStr: szLine) == 1))
207 {
208 SReplaceChar(str: szLine, fc: 0x0D, tc: 0x00);
209 if (Inside<int32_t>(ival: iIndex = strtol(nptr: szLine, endptr: nullptr, base: 10), lbound: 0, rbound: C4M_MaxTexIndex - 1))
210 {
211 const char *szMapping = szLine + SCharPos(cTarget: '=', szInStr: szLine) + 1;
212 StdStrBuf Material, Texture;
213 Material.CopyUntil(szString: szMapping, cUntil: '-'); Texture.Copy(pnData: SSearch(szString: szMapping, szIndex: "-"));
214 if (AddEntry(byIndex: iIndex, szMaterial: Material.getData(), szTexture: Texture.getData()))
215 iTextures++;
216 }
217 }
218 else
219 {
220 if (SEqual2(szStr1: szLine, szStr2: "OverloadMaterials")) { fOverloadMaterials = true; if (pOverloadMaterials) *pOverloadMaterials = true; }
221 if (SEqual2(szStr1: szLine, szStr2: "OverloadTextures")) { fOverloadTextures = true; if (pOverloadTextures) *pOverloadTextures = true; }
222 }
223 // Delete buffer, return entry count
224 delete[] bpMap;
225 fEntriesAdded = false;
226 return iTextures;
227}
228
229int32_t C4TextureMap::Init()
230{
231 int32_t iRemoved = 0;
232 // Initialize texture mappings
233 int32_t i;
234 for (i = 0; i < C4M_MaxTexIndex; i++)
235 if (!Entry[i].isNull())
236 if (!Entry[i].Init())
237 {
238 LogNTr(level: spdlog::level::err, fmt: "Error in TextureMap initialization at entry {}", args: static_cast<int>(i));
239 Entry[i].Clear();
240 iRemoved++;
241 }
242 fInitialized = true;
243 return iRemoved;
244}
245
246bool C4TextureMap::SaveMap(C4Group &hGroup, const char *szEntryName)
247{
248 // build file in memory
249 std::string texMapFile{"# Automatically generated texture map" LineFeed "# Contains material-texture-combinations added at runtime" LineFeed};
250 // add overload-entries
251 if (fOverloadMaterials) texMapFile += "# Import materials from global file as well" LineFeed "OverloadMaterials" LineFeed;
252 if (fOverloadTextures) texMapFile += "# Import textures from global file as well" LineFeed "OverloadTextures" LineFeed;
253 texMapFile += LineFeed;
254 // add entries
255 for (int32_t i = 0; i < C4M_MaxTexIndex; i++)
256 if (!Entry[i].isNull())
257 {
258 // compose line
259 texMapFile += std::format(fmt: "{}={}-{}" LineFeed, args&: i, args: Entry[i].GetMaterialName(), args: Entry[i].GetTextureName());
260 }
261 StdStrBuf buf{texMapFile.c_str(), texMapFile.size()};
262 // add to group
263 return hGroup.Add(szName: szEntryName, pBuffer&: buf, fChild: false, fHoldBuffer: true);
264}
265
266int32_t C4TextureMap::LoadTextures(C4Group &hGroup, C4Group *OverloadFile)
267{
268 int32_t texnum = 0;
269
270 // overload: load from other file
271 if (OverloadFile) texnum += LoadTextures(hGroup&: *OverloadFile);
272
273 char texname[256 + 1];
274 C4Surface *ctex;
275 size_t binlen;
276 // newgfx: load PNG-textures first
277 hGroup.ResetSearch();
278 while (hGroup.AccessNextEntry(C4CFN_PNGFiles, iSize: &binlen, sFileName: texname))
279 {
280 // check if it already exists in the map
281 SReplaceChar(str: texname, fc: '.', tc: 0);
282 if (GetTexture(szTexture: texname)) continue;
283 SAppend(szSource: ".png", szTarget: texname);
284 // load
285 if (ctex = GroupReadSurfacePNG(hGroup))
286 {
287 SReplaceChar(str: texname, fc: '.', tc: 0);
288 if (AddTexture(szTexture: texname, sfcSurface: ctex)) texnum++;
289 else delete ctex;
290 }
291 }
292 // Load all bitmap files from group
293 hGroup.ResetSearch();
294 CSurface8 *ctex8;
295 while (hGroup.AccessNextEntry(C4CFN_BitmapFiles, iSize: &binlen, sFileName: texname))
296 {
297 // check if it already exists in the map
298 SReplaceChar(str: texname, fc: '.', tc: 0);
299 if (GetTexture(szTexture: texname)) continue;
300 SAppend(szSource: ".bmp", szTarget: texname);
301 if (ctex8 = GroupReadSurface8(hGroup))
302 {
303 ctex8->AllowColor(iRngLo: 0, iRngHi: 2, fAllowZero: true);
304 SReplaceChar(str: texname, fc: '.', tc: 0);
305 if (AddTexture(szTexture: texname, sfcSurface: ctex8)) texnum++;
306 else delete ctex;
307 }
308 }
309
310 return texnum;
311}
312
313void C4TextureMap::MoveIndex(uint8_t byOldIndex, uint8_t byNewIndex)
314{
315 Entry[byNewIndex] = Entry[byOldIndex];
316 fEntriesAdded = true;
317}
318
319int32_t C4TextureMap::GetIndex(const char *szMaterial, const char *szTexture, bool fAddIfNotExist, const char *szErrorIfFailed)
320{
321 uint8_t byIndex;
322 // Find existing
323 for (byIndex = 1; byIndex < C4M_MaxTexIndex; byIndex++)
324 if (!Entry[byIndex].isNull())
325 if (SEqualNoCase(szStr1: Entry[byIndex].GetMaterialName(), szStr2: szMaterial))
326 if (!szTexture || SEqualNoCase(szStr1: Entry[byIndex].GetTextureName(), szStr2: szTexture))
327 return byIndex;
328 // Add new entry
329 if (fAddIfNotExist)
330 for (byIndex = 1; byIndex < C4M_MaxTexIndex; byIndex++)
331 if (Entry[byIndex].isNull())
332 {
333 if (AddEntry(byIndex, szMaterial, szTexture))
334 {
335 fEntriesAdded = true;
336 return byIndex;
337 }
338 if (szErrorIfFailed) DebugLog(level: spdlog::level::err, fmt: "Error getting MatTex {}-{} for {} from TextureMap: Init failed.", args&: szMaterial, args&: szTexture, args&: szErrorIfFailed);
339 return 0;
340 }
341 // Else, fail
342 if (szErrorIfFailed) DebugLog(level: spdlog::level::err, fmt: "Error getting MatTex {}-{} for {} from TextureMap: {}.", args&: szMaterial, args&: szTexture, args&: szErrorIfFailed, args: fAddIfNotExist ? "Map is full!" : "Entry not found.");
343 return 0;
344}
345
346int32_t C4TextureMap::GetIndexMatTex(const char *szMaterialTexture, const char *szDefaultTexture, bool fAddIfNotExist, const char *szErrorIfFailed)
347{
348 // split material/texture pair
349 StdStrBuf Material, Texture;
350 Material.CopyUntil(szString: szMaterialTexture, cUntil: '-');
351 Texture.Copy(pnData: SSearch(szString: szMaterialTexture, szIndex: "-"));
352 // texture not given or invalid?
353 int32_t iMatTex = 0;
354 if (Texture.getData())
355 if (iMatTex = GetIndex(szMaterial: Material.getData(), szTexture: Texture.getData(), fAddIfNotExist))
356 return iMatTex;
357 if (szDefaultTexture)
358 if (iMatTex = GetIndex(szMaterial: Material.getData(), szTexture: szDefaultTexture, fAddIfNotExist))
359 return iMatTex;
360 // search material
361 const auto iMaterial = Game.Material.Get(szMaterial: szMaterialTexture);
362 if (!MatValid(mat: iMaterial))
363 {
364 if (szErrorIfFailed) DebugLog(level: spdlog::level::err, fmt: "Error getting MatTex for {}: Invalid material", args&: szErrorIfFailed);
365 return 0;
366 }
367 // return default map entry
368 return Game.Material.Map[iMaterial].DefaultMatTex;
369}
370
371C4Texture *C4TextureMap::GetTexture(const char *szTexture)
372{
373 C4Texture *pTexture;
374 for (pTexture = FirstTexture; pTexture; pTexture = pTexture->Next)
375 if (SEqualNoCase(szStr1: pTexture->Name, szStr2: szTexture))
376 return pTexture;
377 return nullptr;
378}
379
380bool C4TextureMap::CheckTexture(const char *szTexture)
381{
382 C4Texture *pTexture;
383 for (pTexture = FirstTexture; pTexture; pTexture = pTexture->Next)
384 if (SEqualNoCase(szStr1: pTexture->Name, szStr2: szTexture))
385 return true;
386 return false;
387}
388
389const char *C4TextureMap::GetTexture(size_t iIndex)
390{
391 C4Texture *pTexture;
392 size_t cindex;
393 for (pTexture = FirstTexture, cindex = 0; pTexture; pTexture = pTexture->Next, cindex++)
394 if (cindex == iIndex)
395 return pTexture->Name;
396 return nullptr;
397}
398
399void C4TextureMap::Default()
400{
401 FirstTexture = nullptr;
402 fEntriesAdded = false;
403 fOverloadMaterials = false;
404 fOverloadTextures = false;
405 fInitialized = false;
406}
407
408void C4TextureMap::StoreMapPalette(uint8_t *bypPalette, C4MaterialMap &rMaterial)
409{
410 // Zero palette
411 std::fill_n(first: bypPalette, n: 256 * 3, value: 0);
412 // Sky color
413 bypPalette[0] = 192;
414 bypPalette[1] = 196;
415 bypPalette[2] = 252;
416 // Material colors by texture map entries
417 bool fSet[256]{};
418 int32_t i;
419 for (i = 0; i < C4M_MaxTexIndex; i++)
420 {
421 // Find material
422 C4Material *pMat = Entry[i].GetMaterial();
423 if (pMat)
424 {
425 bypPalette[3 * i + 0] = pMat->Color[6];
426 bypPalette[3 * i + 1] = pMat->Color[7];
427 bypPalette[3 * i + 2] = pMat->Color[8];
428 bypPalette[3 * (i + IFT) + 0] = pMat->Color[3];
429 bypPalette[3 * (i + IFT) + 1] = pMat->Color[4];
430 bypPalette[3 * (i + IFT) + 2] = pMat->Color[5];
431 fSet[i] = fSet[i + IFT] = true;
432 }
433 }
434 // Crosscheck colors, change equal palette entries
435 for (i = 0; i < 256; i++) if (fSet[i])
436 for (;;)
437 {
438 // search equal entry
439 int32_t j = 0;
440 for (; j < i; j++) if (fSet[j])
441 if (
442 bypPalette[3 * i + 0] == bypPalette[3 * j + 0] &&
443 bypPalette[3 * i + 1] == bypPalette[3 * j + 1] &&
444 bypPalette[3 * i + 2] == bypPalette[3 * j + 2])
445 break;
446 // not found? ok then
447 if (j >= i) break;
448 // change randomly
449 if (rand() < RAND_MAX / 2) bypPalette[3 * i + 0] += 3; else bypPalette[3 * i + 0] -= 3;
450 if (rand() < RAND_MAX / 2) bypPalette[3 * i + 1] += 3; else bypPalette[3 * i + 1] -= 3;
451 if (rand() < RAND_MAX / 2) bypPalette[3 * i + 2] += 3; else bypPalette[3 * i + 2] -= 3;
452 }
453}
454