| 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 | |
| 34 | C4Texture::C4Texture() |
| 35 | { |
| 36 | Name[0] = 0; |
| 37 | Surface8 = nullptr; |
| 38 | Surface32 = nullptr; |
| 39 | Next = nullptr; |
| 40 | } |
| 41 | |
| 42 | C4Texture::~C4Texture() |
| 43 | { |
| 44 | delete Surface8; |
| 45 | delete Surface32; |
| 46 | } |
| 47 | |
| 48 | C4TexMapEntry::C4TexMapEntry() |
| 49 | : pMaterial(nullptr), iMaterialIndex(MNone) {} |
| 50 | |
| 51 | void C4TexMapEntry::Clear() |
| 52 | { |
| 53 | Material.Clear(); Texture.Clear(); |
| 54 | iMaterialIndex = MNone; |
| 55 | pMaterial = nullptr; |
| 56 | MatPattern.Clear(); |
| 57 | } |
| 58 | |
| 59 | bool 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 | |
| 68 | bool 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 | |
| 106 | C4TextureMap::C4TextureMap() |
| 107 | { |
| 108 | Default(); |
| 109 | } |
| 110 | |
| 111 | C4TextureMap::~C4TextureMap() |
| 112 | { |
| 113 | Clear(); |
| 114 | } |
| 115 | |
| 116 | bool 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 | |
| 139 | bool 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 | |
| 149 | bool 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 | |
| 159 | void 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 | |
| 173 | bool 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 | |
| 197 | int32_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 | |
| 229 | int32_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 | |
| 246 | bool 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 | |
| 266 | int32_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 | |
| 313 | void C4TextureMap::MoveIndex(uint8_t byOldIndex, uint8_t byNewIndex) |
| 314 | { |
| 315 | Entry[byNewIndex] = Entry[byOldIndex]; |
| 316 | fEntriesAdded = true; |
| 317 | } |
| 318 | |
| 319 | int32_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 | |
| 346 | int32_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 | |
| 371 | C4Texture *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 | |
| 380 | bool 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 | |
| 389 | const 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 | |
| 399 | void C4TextureMap::Default() |
| 400 | { |
| 401 | FirstTexture = nullptr; |
| 402 | fEntriesAdded = false; |
| 403 | fOverloadMaterials = false; |
| 404 | fOverloadTextures = false; |
| 405 | fInitialized = false; |
| 406 | } |
| 407 | |
| 408 | void 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 | |