1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 1998-2000, Matthes Bender
5  * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
6  * Copyright (c) 2009-2016, The OpenClonk Team and contributors
7  *
8  * Distributed under the terms of the ISC license; see accompanying file
9  * "COPYING" for details.
10  *
11  * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12  * See accompanying file "TRADEMARK" for details.
13  *
14  * To redistribute this file separately, substitute the full license texts
15  * for the above references.
16  */
17 
18 /* Textures used by the landscape */
19 
20 #include "C4Include.h"
21 #include "landscape/C4Texture.h"
22 
23 #include "c4group/C4Components.h"
24 #include "c4group/C4Group.h"
25 #include "landscape/C4Landscape.h"
26 #include "lib/C4Random.h"
27 #include "lib/StdColors.h"
28 
C4Texture()29 C4Texture::C4Texture()
30 {
31 	Surface32=nullptr;
32 	AvgColor = 0x00000000;
33 	Next=nullptr;
34 }
35 
~C4Texture()36 C4Texture::~C4Texture()
37 {
38 	delete Surface32;
39 }
40 
C4TexMapEntry()41 C4TexMapEntry::C4TexMapEntry()
42 		: iMaterialIndex(MNone)
43 {
44 }
45 
Clear()46 void C4TexMapEntry::Clear()
47 {
48 	Material.Clear(); Texture.Clear();
49 	iMaterialIndex = MNone;
50 	pMaterial = nullptr;
51 	MatPattern.Clear();
52 }
53 
Create(const char * szMaterial,const char * szTexture)54 bool C4TexMapEntry::Create(const char *szMaterial, const char *szTexture)
55 {
56 	// Clear previous data
57 	Clear();
58 	// Save names
59 	Material = szMaterial; Texture = szTexture;
60 	return true;
61 }
62 
Init()63 bool C4TexMapEntry::Init()
64 {
65 	// Find material
66 	iMaterialIndex = ::MaterialMap.Get(Material.getData());
67 	if (!MatValid(iMaterialIndex))
68 	{
69 		DebugLogF("Error initializing material %s-%s: Invalid material!", Material.getData(), Texture.getData());
70 		return false;
71 	}
72 	pMaterial = &::MaterialMap.Map[iMaterialIndex];
73 	// Find texture
74 	StdStrBuf FirstTexture;
75 	FirstTexture.CopyUntil(Texture.getData(), '-');
76 	C4Texture * sfcTexture = ::TextureMap.GetTexture(FirstTexture.getData());
77 	if (!sfcTexture)
78 	{
79 		DebugLogF("Error initializing material %s-%s: Invalid texture!", Material.getData(), FirstTexture.getData());
80 		Clear();
81 		return false;
82 	}
83 	// Get overlay properties
84 	int32_t iOverlayType=pMaterial->OverlayType;
85 	int32_t iZoom=0;
86 	if (iOverlayType & C4MatOv_Exact) iZoom=1;
87 	if (iOverlayType & C4MatOv_HugeZoom) iZoom=4;
88 	// Create pattern
89 	MatPattern.Set(sfcTexture->Surface32, iZoom);
90 	return true;
91 }
92 
C4TextureMap()93 C4TextureMap::C4TextureMap()
94 {
95 	Order.reserve(C4M_MaxTexIndex);
96 }
97 
~C4TextureMap()98 C4TextureMap::~C4TextureMap()
99 {
100 	Clear();
101 }
102 
AddEntry(BYTE byIndex,const char * szMaterial,const char * szTexture)103 bool C4TextureMap::AddEntry(BYTE byIndex, const char *szMaterial, const char *szTexture)
104 {
105 	// Security
106 	if (byIndex <= 0 || byIndex >= C4M_MaxTexIndex)
107 		return false;
108 	// Set entry and initialize
109 	Entry[byIndex].Create(szMaterial, szTexture);
110 	if (fInitialized)
111 	{
112 		if (!Entry[byIndex].Init())
113 		{
114 			// Clear entry if it could not be initialized
115 			Entry[byIndex].Clear();
116 			return false;
117 		}
118 		// Landscape must be notified (new valid pixel clr)
119 		::Landscape.HandleTexMapUpdate();
120 	}
121 	// Add last in order list
122 	Order.push_back(byIndex);
123 	return true;
124 }
125 
AddTexture(const char * szTexture,C4Surface * sfcSurface)126 bool C4TextureMap::AddTexture(const char *szTexture, C4Surface * sfcSurface)
127 {
128 	C4Texture *pTexture;
129 	if (!(pTexture=new C4Texture)) return false;
130 	pTexture->Name.Copy(szTexture);
131 	pTexture->Surface32=sfcSurface;
132 	pTexture->Next=FirstTexture;
133 	FirstTexture=pTexture;
134 
135 	// Compute average texture color
136 	if(sfcSurface)
137 	{
138 		sfcSurface->Lock();
139 		uint32_t avg_c[4] = { 0, 0, 0, 0 };
140 		for(int32_t y = 0; y < sfcSurface->Hgt; ++y)
141 		{
142 			for(int32_t x = 0; x < sfcSurface->Wdt; ++x)
143 			{
144 				DWORD c = sfcSurface->GetPixDw(x, y, false);
145 				avg_c[0] += c & 0xff;
146 				avg_c[1] += (c >> 8) & 0xff;
147 				avg_c[2] += (c >> 16) & 0xff;
148 				avg_c[3] += (c >> 24) & 0xff;
149 			}
150 		}
151 		sfcSurface->Unlock();
152 
153 		double Size = sfcSurface->Wdt * sfcSurface->Hgt;
154 		avg_c[0] = static_cast<uint32_t>(avg_c[0] / Size + 0.5);
155 		avg_c[1] = static_cast<uint32_t>(avg_c[1] / Size + 0.5);
156 		avg_c[2] = static_cast<uint32_t>(avg_c[2] / Size + 0.5);
157 		avg_c[3] = static_cast<uint32_t>(avg_c[3] / Size + 0.5);
158 		pTexture->SetAverageColor(avg_c[0] | (avg_c[1] << 8) | (avg_c[2] << 16) | (avg_c[3] << 24));
159 	}
160 	else
161 	{
162 		pTexture->SetAverageColor(0x00000000);
163 	}
164 
165 	return true;
166 }
167 
Clear()168 void C4TextureMap::Clear()
169 {
170 	for (int32_t i = 1; i < C4M_MaxTexIndex; i++)
171 		Entry[i].Clear();
172 	C4Texture *ctex,*next2;
173 	for (ctex=FirstTexture; ctex; ctex=next2)
174 	{
175 		next2=ctex->Next;
176 		delete ctex;
177 	}
178 	FirstTexture=nullptr;
179 	fInitialized = false;
180 	fEntriesAdded = false;
181 	fOverloadMaterials = false;
182 	fOverloadTextures = false;
183 	Order.clear();
184 	Order.reserve(C4M_MaxTexIndex);
185 }
186 
LoadFlags(C4Group & hGroup,const char * szEntryName,bool * pOverloadMaterials,bool * pOverloadTextures)187 bool C4TextureMap::LoadFlags(C4Group &hGroup, const char *szEntryName, bool *pOverloadMaterials, bool *pOverloadTextures)
188 {
189 	// Load the file
190 	StdStrBuf TexMap;
191 	if (!hGroup.LoadEntryString(szEntryName, &TexMap))
192 		return false;
193 	// Reset flags
194 	if (pOverloadMaterials) *pOverloadMaterials = false;
195 	if (pOverloadTextures) *pOverloadTextures = false;
196 	// Check if there are flags in there
197 	for (const char *pPos = TexMap.getData(); pPos && *pPos; pPos = SSearch(pPos + 1, "\n"))
198 	{
199 		// Go over newlines
200 		while (*pPos == '\r' || *pPos == '\n') pPos++;
201 		// Flag?
202 		if (pOverloadMaterials && SEqual2(pPos, "OverloadMaterials"))
203 			*pOverloadMaterials = true;
204 		if (pOverloadTextures && SEqual2(pPos, "OverloadTextures"))
205 			*pOverloadTextures = true;
206 	}
207 	// Done
208 	return true;
209 }
210 
LoadMap(C4Group & hGroup,const char * szEntryName,bool * pOverloadMaterials,bool * pOverloadTextures)211 int32_t C4TextureMap::LoadMap(C4Group &hGroup, const char *szEntryName, bool *pOverloadMaterials, bool *pOverloadTextures)
212 {
213 	static std::regex line_terminator("\r?\n", static_cast<std::regex::flag_type>(std::regex_constants::optimize | std::regex_constants::ECMAScript));
214 
215 	char *bpMap;
216 	size_t map_size;
217 	int32_t iTextures = 0;
218 	// Load text file into memory
219 	if (!hGroup.LoadEntry(szEntryName,&bpMap,&map_size,1)) return 0;
220 
221 	char *begin = bpMap;
222 	char *end = begin + map_size;
223 
224 	size_t line = 1; // Counter for error messages
225 	for (auto it = std::cregex_token_iterator(begin, end, line_terminator, -1); it != std::cregex_token_iterator(); ++it, ++line)
226 	{
227 		if (it->compare("OverloadMaterials") == 0)
228 		{
229 			fOverloadMaterials = true;
230 			if (pOverloadMaterials)
231 				*pOverloadMaterials = true;
232 		}
233 		else if (it->compare("OverloadTextures") == 0)
234 		{
235 			fOverloadTextures = true;
236 			if (pOverloadTextures)
237 				*pOverloadTextures = true;
238 		}
239 		else if (it->length() == 0 || it->first[0] == '#' || std::all_of(it->first, it->second, &isspace))
240 		{
241 			// Skip empty lines, comments, and all-whitespace lines
242 			continue;
243 		}
244 		else
245 		{
246 			// This must be a texmap entry now
247 			std::string value;
248 
249 			// Read index
250 			unsigned long index;
251 			try
252 			{
253 				size_t separator;
254 				index = std::stoul(it->str(), &separator, 10);
255 				if (index >= C4M_MaxTexIndex)
256 					throw std::out_of_range("Texture index out of range");
257 				value.assign(it->first + separator + 1, it->second);
258 			}
259 			catch (std::invalid_argument &)
260 			{
261 				DebugLogF("TexMap line %u: Texture index is not numeric", static_cast<unsigned>(line));
262 				continue;
263 			}
264 			catch (std::out_of_range &)
265 			{
266 				DebugLogF("TexMap line %u: Texture index is out of range", static_cast<unsigned>(line));
267 				continue;
268 			}
269 
270 			// Split material/texture combination
271 			std::string::const_iterator separator = std::find(value.cbegin(), value.cend(), '-');
272 			if (separator == value.cend())
273 			{
274 				DebugLogF(R"(TexMap line %u: Texture name "%s" is invalid (missing "-"))", static_cast<unsigned>(line), value.c_str());
275 				continue;
276 			}
277 
278 			std::string material(value.cbegin(), separator);
279 			std::string texture(separator + 1, value.cend());
280 
281 			if (AddEntry(index, material.c_str(), texture.c_str()))
282 				++iTextures;
283 		}
284 	}
285 
286 	// Delete buffer, return entry count
287 	delete [] bpMap;
288 	fEntriesAdded=false;
289 	return iTextures;
290 }
291 
Init()292 int32_t C4TextureMap::Init()
293 {
294 	int32_t iRemoved = 0;
295 	// Initialize texture mappings
296 	int32_t i;
297 	for (i = 0; i < C4M_MaxTexIndex; i++)
298 		if (!Entry[i].isNull())
299 			if (!Entry[i].Init())
300 			{
301 				LogF("Error in TextureMap initialization at entry %d", (int) i);
302 				Entry[i].Clear();
303 				iRemoved++;
304 			}
305 	fInitialized = true;
306 	return iRemoved;
307 }
308 
SaveMap(C4Group & hGroup,const char * szEntryName)309 bool C4TextureMap::SaveMap(C4Group &hGroup, const char *szEntryName)
310 {
311 	// build file in memory
312 	StdStrBuf sTexMapFile;
313 	// add desc
314 	sTexMapFile.Append("# Automatically generated texture map" LineFeed);
315 	sTexMapFile.Append("# Contains material-texture-combinations added at runtime" LineFeed);
316 	// add overload-entries
317 	if (fOverloadMaterials) sTexMapFile.Append("# Import materials from global file as well" LineFeed "OverloadMaterials" LineFeed);
318 	if (fOverloadTextures) sTexMapFile.Append("# Import textures from global file as well" LineFeed "OverloadTextures" LineFeed);
319 	sTexMapFile.Append(LineFeed);
320 	// add entries
321 	for (auto i : Order)
322 	{
323 		if (!Entry[i].isNull())
324 		{
325 			// compose line
326 			sTexMapFile.AppendFormat("%d=%s-%s" LineFeed, i, Entry[i].GetMaterialName(), Entry[i].GetTextureName());
327 		}
328 	}
329 	// create new buffer allocated with new [], because C4Group cannot handle StdStrBuf-buffers
330 	size_t iBufSize = sTexMapFile.getLength();
331 	BYTE *pBuf = new BYTE[iBufSize];
332 	memcpy(pBuf, sTexMapFile.getData(), iBufSize);
333 	// add to group
334 	bool fSuccess = !!hGroup.Add(szEntryName, pBuf, iBufSize, false, true);
335 	if (!fSuccess) delete [] pBuf;
336 	// done
337 	return fSuccess;
338 }
339 
LoadTextures(C4Group & hGroup,C4Group * OverloadFile)340 int32_t C4TextureMap::LoadTextures(C4Group &hGroup, C4Group* OverloadFile)
341 {
342 	int32_t texnum=0;
343 	// overload: load from other file
344 	if (OverloadFile) texnum+=LoadTextures(*OverloadFile);
345 
346 	char texname[256+1];
347 	C4Surface *ctex;
348 	size_t binlen;
349 
350 	hGroup.ResetSearch();
351 	while (hGroup.AccessNextEntry("*",&binlen,texname))
352 	{
353 		// check if it already exists in the map
354 		const char *base_filename = GetFilenameOnly(texname);
355 		if (GetTexture(base_filename)) continue;
356 		// skip shape textures for now. Will be added later after all base textures have been loaded
357 		if (WildcardMatch("*" C4CFN_MaterialShapeFiles, texname)) continue;
358 		// create surface
359 		ctex = new C4Surface();
360 		if (ctex->Read(hGroup, GetExtension(texname), C4SF_MipMap))
361 		{
362 			SReplaceChar(texname,'.',0);
363 			if (AddTexture(texname,ctex)) texnum++;
364 			else delete ctex;
365 		}
366 		else
367 		{
368 			delete ctex;
369 		}
370 	}
371 
372 	// Load texture shapes
373 	hGroup.ResetSearch();
374 	while (hGroup.AccessNextEntry("*" C4CFN_MaterialShapeFiles, &binlen, texname))
375 	{
376 		// get associated texture
377 		StdStrBuf texname4shape(texname, true);
378 		texname4shape.SetLength(texname4shape.getLength() - SLen(C4CFN_MaterialShapeFiles));
379 		C4Texture *base_tex = GetTexture(texname4shape.getData());
380 		if (!base_tex || !base_tex->Surface32)
381 		{
382 			LogF("ERROR: Texture shape %s not loaded because associated texture (%s) not found or invalid.", hGroup.GetFullName().getData(), texname4shape.getData());
383 			continue;
384 		}
385 		std::unique_ptr<C4TextureShape> shape(new C4TextureShape());
386 		int32_t scaler_zoom = 4;
387 		if (!shape->Load(hGroup, texname, base_tex->Surface32->Wdt / scaler_zoom, base_tex->Surface32->Hgt / scaler_zoom))
388 		{
389 			LogF("Error loading texture shape %s.", hGroup.GetFullName().getData());
390 			continue;
391 		}
392 		base_tex->SetMaterialShape(shape.release());
393 	}
394 
395 	return texnum;
396 }
397 
HasTextures(C4Group & hGroup)398 bool C4TextureMap::HasTextures(C4Group &hGroup)
399 {
400 	return hGroup.EntryCount(C4CFN_PNGFiles) || hGroup.EntryCount(C4CFN_BitmapFiles);
401 }
402 
MoveIndex(BYTE byOldIndex,BYTE byNewIndex)403 void C4TextureMap::MoveIndex(BYTE byOldIndex, BYTE byNewIndex)
404 {
405 	if (byNewIndex == byOldIndex) return;
406 	Entry[byNewIndex] = Entry[byOldIndex];
407 	Entry[byOldIndex].Clear();
408 	auto old_entry = std::find_if(Order.begin(), Order.end(),
409 		[byOldIndex](const int32_t &entry) { return entry == byOldIndex; });
410 	if (old_entry != Order.end()) *old_entry = byNewIndex;
411 	fEntriesAdded = true;
412 }
413 
GetIndex(const char * szMaterial,const char * szTexture,bool fAddIfNotExist,const char * szErrorIfFailed)414 int32_t C4TextureMap::GetIndex(const char *szMaterial, const char *szTexture, bool fAddIfNotExist, const char *szErrorIfFailed)
415 {
416 	BYTE byIndex;
417 	// Find existing
418 	for (byIndex = 1; byIndex < C4M_MaxTexIndex; byIndex++)
419 		if (!Entry[byIndex].isNull())
420 			if (SEqualNoCase(Entry[byIndex].GetMaterialName(), szMaterial))
421 				if (!szTexture || SEqualNoCase(Entry[byIndex].GetTextureName(), szTexture))
422 					return byIndex;
423 	// Add new entry
424 	if (fAddIfNotExist)
425 		for (byIndex=1; byIndex<C4M_MaxTexIndex; byIndex++)
426 			if (Entry[byIndex].isNull())
427 			{
428 				if (AddEntry(byIndex, szMaterial, szTexture))
429 				{
430 					fEntriesAdded=true;
431 					return byIndex;
432 				}
433 				if (szErrorIfFailed) DebugLogF("Error getting MatTex %s-%s for %s from TextureMap: Init failed.", szMaterial, szTexture, szErrorIfFailed);
434 				return 0;
435 			}
436 	// Else, fail
437 	if (szErrorIfFailed) DebugLogF("Error getting MatTex %s-%s for %s from TextureMap: %s.", szMaterial, szTexture, szErrorIfFailed, fAddIfNotExist ? "Map is full!" : "Entry not found.");
438 	return 0;
439 }
440 
GetIndexMatTex(const char * szMaterialTexture,const char * szDefaultTexture,bool fAddIfNotExist,const char * szErrorIfFailed)441 int32_t C4TextureMap::GetIndexMatTex(const char *szMaterialTexture, const char *szDefaultTexture, bool fAddIfNotExist, const char *szErrorIfFailed)
442 {
443 	// split material/texture pair
444 	StdStrBuf Material, Texture;
445 	Material.CopyUntil(szMaterialTexture, '-');
446 	Texture.Copy(SSearch(szMaterialTexture, "-"));
447 	// texture not given or invalid?
448 	int32_t iMatTex = 0;
449 	if (Texture.getData())
450 		if ((iMatTex = GetIndex(Material.getData(), Texture.getData(), fAddIfNotExist)))
451 			return iMatTex;
452 	if (szDefaultTexture)
453 		if ((iMatTex = GetIndex(Material.getData(), szDefaultTexture, fAddIfNotExist)))
454 			return iMatTex;
455 	// search material
456 	long iMaterial = ::MaterialMap.Get(szMaterialTexture);
457 	if (!MatValid(iMaterial))
458 	{
459 		if (szErrorIfFailed) DebugLogF("Error getting MatTex for %s: Invalid material", szErrorIfFailed);
460 		return 0;
461 	}
462 	// return default map entry
463 	return ::MaterialMap.Map[iMaterial].DefaultMatTex;
464 }
465 
GetTexture(const char * szTexture)466 C4Texture * C4TextureMap::GetTexture(const char *szTexture)
467 {
468 	C4Texture *pTexture;
469 	for (pTexture=FirstTexture; pTexture; pTexture=pTexture->Next)
470 		if (SEqualNoCase(pTexture->Name.getData(),szTexture))
471 			return pTexture;
472 	return nullptr;
473 }
474 
GetTextureIndex(const char * szName)475 int32_t C4TextureMap::GetTextureIndex(const char *szName)
476 {
477 	C4Texture *pTexture;
478 	int32_t i=0;
479 	for (pTexture=FirstTexture; pTexture; pTexture=pTexture->Next, i++)
480 		if (SEqualNoCase(pTexture->Name.getData(),szName))
481 			return i;
482 	return -1;
483 }
484 
CheckTexture(const char * szTexture)485 bool C4TextureMap::CheckTexture(const char *szTexture)
486 {
487 	C4Texture *pTexture;
488 	for (pTexture=FirstTexture; pTexture; pTexture=pTexture->Next)
489 		if (SEqualNoCase(pTexture->Name.getData(),szTexture))
490 			return true;
491 	return false;
492 }
493 
GetTexture(int32_t iIndex)494 const char* C4TextureMap::GetTexture(int32_t iIndex)
495 {
496 	C4Texture *pTexture;
497 	int32_t cindex;
498 	for (pTexture=FirstTexture,cindex=0; pTexture; pTexture=pTexture->Next,cindex++)
499 		if (cindex==iIndex)
500 			return pTexture->Name.getData();
501 	return nullptr;
502 }
503 
DefaultBkgMatTex(BYTE fg) const504 BYTE C4TextureMap::DefaultBkgMatTex(BYTE fg) const
505 {
506 	// For the given foreground index, find the default background index
507 	// If fg is semisolid, this is tunnel.
508 	// Otherwise it is fg itself, so that tunnel and background bricks
509 	// stay the way they are.
510 	int32_t iTex = PixCol2Tex(fg);
511 	if (!iTex) return fg; // sky
512 
513 	// Get material-texture mapping
514 	const C4TexMapEntry *pTex = GetEntry(iTex);
515 	// Texmap entry does not exist
516 	if(!pTex || !pTex->GetMaterial()) return fg;
517 
518 	if(DensitySemiSolid(pTex->GetMaterial()->Density))
519 		return Mat2PixColDefault(MTunnel);
520 
521 	return fg;
522 
523 }
524 
RemoveEntry(int32_t iIndex)525 void C4TextureMap::RemoveEntry(int32_t iIndex)
526 {
527 	// remove entry from table and order vector
528 	if (Inside<int32_t>(iIndex, 1, C4M_MaxTexIndex - 1))
529 	{
530 		Entry[iIndex].Clear();
531 		auto last_entry = std::remove_if(Order.begin(), Order.end(),
532 			[iIndex](const int32_t &entry) { return entry == iIndex; });
533 		Order.erase(last_entry, Order.end());
534 	}
535 }
536 
StoreMapPalette(CStdPalette * Palette,C4MaterialMap & rMaterial)537 void C4TextureMap::StoreMapPalette(CStdPalette *Palette, C4MaterialMap &rMaterial)
538 {
539 	// Sky color
540 	Palette->Colors[0] = C4RGB(192, 196, 252);
541 	// Material colors by texture map entries
542 	bool fSet[C4M_MaxTexIndex];
543 	ZeroMem(&fSet, sizeof (fSet));
544 	int32_t i;
545 	for (i = 1; i < C4M_MaxTexIndex; i++)
546 	{
547 		// Find material
548 		DWORD dwPix;
549 		auto texture = GetTexture(Entry[i].GetTextureName());
550 		if (texture)
551 			dwPix = texture->GetAverageColor();
552 		else
553 			dwPix = Entry[i].GetPattern().PatternClr(0, 0);
554 		Palette->Colors[i] = dwPix;
555 		fSet[i] = true;
556 	}
557 	// Crosscheck colors, change equal palette entries
558 	for (i = 0; i < C4M_MaxTexIndex; i++) if (fSet[i])
559 			for (;;)
560 			{
561 				// search equal entry
562 				int32_t j = 0;
563 				for (; j < i; j++)
564 					if (fSet[j] && Palette->Colors[i] == Palette->Colors[j])
565 							break;
566 				// not found? ok then
567 				if (j >= i) break;
568 				// change randomly
569 				Palette->Colors[i] = C4RGB(
570 					UnsyncedRandom(2) ? GetRedValue(Palette->Colors[i]) + 3 : GetRedValue(Palette->Colors[i]) - 3,
571 					UnsyncedRandom(2) ? GetGreenValue(Palette->Colors[i]) + 3 : GetGreenValue(Palette->Colors[i]) - 3,
572 					UnsyncedRandom(2) ? GetBlueValue(Palette->Colors[i]) + 3 : GetBlueValue(Palette->Colors[i]) - 3);
573 			}
574 }
575 
576 C4TextureMap TextureMap;
577