1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2022, 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/* Material definitions used by the landscape */
18
19#include <C4Include.h>
20#include <C4Material.h>
21#include <C4Components.h>
22
23#include <C4Group.h>
24#include <C4Game.h>
25#include <C4Random.h>
26#include <C4ToolsDlg.h> // For C4TLS_MatSky...
27#include <C4Wrappers.h>
28#include <C4Physics.h> // For GravAccel
29
30#include <utility>
31
32// C4MaterialReaction
33struct ReactionFuncMapEntry { const char *szRFName; C4MaterialReactionFunc pFunc; };
34
35const ReactionFuncMapEntry ReactionFuncMap[] =
36{
37 { .szRFName: "Script", .pFunc: &C4MaterialMap::mrfScript },
38 { .szRFName: "Convert", .pFunc: &C4MaterialMap::mrfConvert },
39 { .szRFName: "Poof", .pFunc: &C4MaterialMap::mrfPoof },
40 { .szRFName: "Corrode", .pFunc: &C4MaterialMap::mrfCorrode },
41 { .szRFName: "Insert", .pFunc: &C4MaterialMap::mrfInsert },
42 { .szRFName: nullptr, .pFunc: &C4MaterialReaction::NoReaction }
43};
44
45void C4MaterialReaction::CompileFunc(StdCompiler *pComp)
46{
47 if (pComp->isCompiler()) pScriptFunc = nullptr;
48 // compile reaction func ptr
49 StdStrBuf sReactionFuncName;
50 int32_t i = 0; while (ReactionFuncMap[i].szRFName && (ReactionFuncMap[i].pFunc != pFunc)) ++i;
51 sReactionFuncName.Ref(pnData: ReactionFuncMap[i].szRFName);
52 pComp->Value(rStruct: mkNamingAdapt(rValue&: sReactionFuncName, szName: "Type", rDefault: StdStrBuf()));
53 i = 0; while (ReactionFuncMap[i].szRFName && !SEqual(szStr1: ReactionFuncMap[i].szRFName, szStr2: sReactionFuncName.getData())) ++i;
54 pFunc = ReactionFuncMap[i].pFunc;
55
56 // compile the rest
57 pComp->Value(rStruct: mkNamingAdapt(rValue&: TargetSpec, szName: "TargetSpec", rDefault: StdStrBuf()));
58 pComp->Value(rStruct: mkNamingAdapt(rValue&: ScriptFunc, szName: "ScriptFunc", rDefault: StdStrBuf()));
59 pComp->Value(rStruct: mkNamingAdapt(rValue&: iExecMask, szName: "ExecMask", rDefault: ~0u));
60 pComp->Value(rStruct: mkNamingAdapt(rValue&: fReverse, szName: "Reverse", rDefault: false));
61 pComp->Value(rStruct: mkNamingAdapt(rValue&: fInverseSpec, szName: "InverseSpec", rDefault: false));
62 pComp->Value(rStruct: mkNamingAdapt(rValue&: fInsertionCheck, szName: "CheckSlide", rDefault: true));
63 pComp->Value(rStruct: mkNamingAdapt(rValue&: iDepth, szName: "Depth", rDefault: 0));
64 pComp->Value(rStruct: mkNamingAdapt(rValue&: sConvertMat, szName: "ConvertMat", rDefault: StdStrBuf()));
65 pComp->Value(rStruct: mkNamingAdapt(rValue&: iCorrosionRate, szName: "CorrosionRate", rDefault: 100));
66}
67
68void C4MaterialReaction::ResolveScriptFuncs(const char *szMatName)
69{
70 // get script func for script-defined behaviour
71 if (pFunc == &C4MaterialMap::mrfScript)
72 pScriptFunc = Game.ScriptEngine.GetSFuncWarn(pIdtf: this->ScriptFunc.getData(), AccNeeded: AA_PROTECTED, WarnStr: std::format(fmt: "Material reaction of \"{}\"", args&: szMatName).c_str());
73 else
74 pScriptFunc = nullptr;
75}
76
77// C4MaterialCore
78
79C4MaterialCore::C4MaterialCore()
80{
81 Clear();
82}
83
84void C4MaterialCore::Clear()
85{
86 CustomReactionList.clear();
87 sTextureOverlay.Clear();
88 sPXSGfx.Clear();
89 sBlastShiftTo.Clear();
90 sInMatConvert.Clear();
91 sInMatConvertTo.Clear();
92 sBelowTempConvertTo.Clear();
93 sAboveTempConvertTo.Clear();
94 *Name = '\0';
95 int32_t i;
96 for (i = 0; i < C4M_ColsPerMat * 3; ++i) Color[i] = 0;
97 for (i = 0; i < C4M_ColsPerMat * 2; ++i) Alpha[i] = 0;
98 MapChunkType = 0;
99 Density = 0;
100 Friction = 0;
101 DigFree = 0;
102 BlastFree = 0;
103 Dig2Object = 0;
104 Dig2ObjectRatio = 0;
105 Dig2ObjectOnRequestOnly = 0;
106 Blast2Object = 0;
107 Blast2ObjectRatio = 0;
108 Blast2PXSRatio = 0;
109 Instable = 0;
110 MaxAirSpeed = 0;
111 MaxSlide = 0;
112 WindDrift = 0;
113 Inflammable = 0;
114 Incindiary = 0;
115 Extinguisher = 0;
116 Corrosive = 0;
117 Corrode = 0;
118 Soil = 0;
119 Placement = 0;
120 OverlayType = 0;
121 PXSGfxRt.Default();
122 PXSGfxSize = 0;
123 InMatConvertDepth = 0;
124 BelowTempConvert = 0;
125 BelowTempConvertDir = 0;
126 AboveTempConvert = 0;
127 AboveTempConvertDir = 0;
128 TempConvStrength = 0;
129 MinHeightCount = 0;
130 SplashRate = 10;
131}
132
133bool C4MaterialCore::Load(C4Group &hGroup,
134 const char *szEntryName)
135{
136 StdStrBuf Source;
137 if (!hGroup.LoadEntryString(szEntryName, Buf&: Source))
138 return false;
139 StdStrBuf Name = hGroup.GetFullName() + DirSep + szEntryName;
140 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(TargetStruct&: *this, SrcBuf: Source, szName: Name.getData()))
141 return false;
142 // adjust placement, if not specified
143 if (!Placement)
144 {
145 if (DensitySolid(dens: Density))
146 {
147 Placement = 30;
148 if (!DigFree) Placement += 20;
149 if (!BlastFree) Placement += 10;
150 if (!Dig2ObjectOnRequestOnly) Placement += 10;
151 }
152 else if (DensityLiquid(dens: Density))
153 Placement = 10;
154 else Placement = 5;
155 }
156 return true;
157}
158
159uint32_t C4MaterialCore::GetDWordColor(int32_t iIndex)
160{
161 if (iIndex < 0) return 0;
162 iIndex %= (C4M_ColsPerMat * 2);
163 int32_t iClrIndex = iIndex % C4M_ColsPerMat;
164 return RGB(r: Color[iClrIndex * 3 + 2], g: Color[iClrIndex * 3 + 1], b: Color[iClrIndex * 3]) | (Alpha[iIndex] << 24);
165}
166
167void C4MaterialCore::CompileFunc(StdCompiler *pComp)
168{
169 if (pComp->isCompiler()) Clear();
170
171 {
172 const auto name = pComp->Name(szName: "Material");
173 pComp->Value(rStruct: mkNamingAdapt(toC4CStr(Name), szName: "Name", rDefault: ""));
174 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: Color, default_: 0u), szName: "Color"));
175 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: Color, default_: 0u), szName: "ColorX", rDefault: Color));
176 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: Alpha, default_: 0u), szName: "Alpha"));
177 pComp->Value(rStruct: mkNamingAdapt(rValue: StdNullAdapt{}, szName: "ColorAnimation"));
178 pComp->Value(rStruct: mkNamingAdapt(rValue&: MapChunkType, szName: "Shape", rDefault: 0));
179 pComp->Value(rStruct: mkNamingAdapt(rValue&: Density, szName: "Density", rDefault: 0));
180 pComp->Value(rStruct: mkNamingAdapt(rValue&: Friction, szName: "Friction", rDefault: 0));
181 pComp->Value(rStruct: mkNamingAdapt(rValue&: DigFree, szName: "DigFree", rDefault: 0));
182 pComp->Value(rStruct: mkNamingAdapt(rValue&: BlastFree, szName: "BlastFree", rDefault: 0));
183 pComp->Value(rStruct: mkNamingAdapt(rValue: mkC4IDAdapt(rValue&: Blast2Object), szName: "Blast2Object", rDefault: 0));
184 pComp->Value(rStruct: mkNamingAdapt(rValue: mkC4IDAdapt(rValue&: Dig2Object), szName: "Dig2Object", rDefault: 0));
185 pComp->Value(rStruct: mkNamingAdapt(rValue&: Dig2ObjectRatio, szName: "Dig2ObjectRatio", rDefault: 0));
186 pComp->Value(rStruct: mkNamingAdapt(rValue&: Dig2ObjectOnRequestOnly, szName: "Dig2ObjectRequest", rDefault: 0));
187 pComp->Value(rStruct: mkNamingAdapt(rValue&: Blast2ObjectRatio, szName: "Blast2ObjectRatio", rDefault: 0));
188 pComp->Value(rStruct: mkNamingAdapt(rValue&: Blast2PXSRatio, szName: "Blast2PXSRatio", rDefault: 0));
189 pComp->Value(rStruct: mkNamingAdapt(rValue&: Instable, szName: "Instable", rDefault: 0));
190 pComp->Value(rStruct: mkNamingAdapt(rValue&: MaxAirSpeed, szName: "MaxAirSpeed", rDefault: 0));
191 pComp->Value(rStruct: mkNamingAdapt(rValue&: MaxSlide, szName: "MaxSlide", rDefault: 0));
192 pComp->Value(rStruct: mkNamingAdapt(rValue&: WindDrift, szName: "WindDrift", rDefault: 0));
193 pComp->Value(rStruct: mkNamingAdapt(rValue&: Inflammable, szName: "Inflammable", rDefault: 0));
194 pComp->Value(rStruct: mkNamingAdapt(rValue&: Incindiary, szName: "Incindiary", rDefault: 0));
195 pComp->Value(rStruct: mkNamingAdapt(rValue&: Corrode, szName: "Corrode", rDefault: 0));
196 pComp->Value(rStruct: mkNamingAdapt(rValue&: Corrosive, szName: "Corrosive", rDefault: 0));
197 pComp->Value(rStruct: mkNamingAdapt(rValue&: Extinguisher, szName: "Extinguisher", rDefault: 0));
198 pComp->Value(rStruct: mkNamingAdapt(rValue&: Soil, szName: "Soil", rDefault: 0));
199 pComp->Value(rStruct: mkNamingAdapt(rValue&: Placement, szName: "Placement", rDefault: 0));
200 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sTextureOverlay, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "TextureOverlay", rDefault: ""));
201 pComp->Value(rStruct: mkNamingAdapt(rValue&: OverlayType, szName: "OverlayType", rDefault: 0));
202 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sPXSGfx, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "PXSGfx", rDefault: ""));
203 pComp->Value(rStruct: mkNamingAdapt(rValue&: PXSGfxRt, szName: "PXSGfxRt", rDefault: TargetRect0));
204 pComp->Value(rStruct: mkNamingAdapt(rValue&: PXSGfxSize, szName: "PXSGfxSize", rDefault: PXSGfxRt.Wdt));
205 pComp->Value(rStruct: mkNamingAdapt(rValue&: TempConvStrength, szName: "TempConvStrength", rDefault: 0));
206 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sBlastShiftTo, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "BlastShiftTo", rDefault: ""));
207 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sInMatConvert, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "InMatConvert", rDefault: ""));
208 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sInMatConvertTo, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "InMatConvertTo", rDefault: ""));
209 pComp->Value(rStruct: mkNamingAdapt(rValue&: InMatConvertDepth, szName: "InMatConvertDepth", rDefault: 0));
210 pComp->Value(rStruct: mkNamingAdapt(rValue&: AboveTempConvert, szName: "AboveTempConvert", rDefault: 0));
211 pComp->Value(rStruct: mkNamingAdapt(rValue&: AboveTempConvertDir, szName: "AboveTempConvertDir", rDefault: 0));
212 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sAboveTempConvertTo, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "AboveTempConvertTo", rDefault: ""));
213 pComp->Value(rStruct: mkNamingAdapt(rValue&: BelowTempConvert, szName: "BelowTempConvert", rDefault: 0));
214 pComp->Value(rStruct: mkNamingAdapt(rValue&: BelowTempConvertDir, szName: "BelowTempConvertDir", rDefault: 0));
215 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sBelowTempConvertTo, rPar: StdCompiler::RCT_IdtfAllowEmpty), szName: "BelowTempConvertTo", rDefault: ""));
216 pComp->Value(rStruct: mkNamingAdapt(rValue&: MinHeightCount, szName: "MinHeightCount", rDefault: 0));
217 pComp->Value(rStruct: mkNamingAdapt(rValue&: SplashRate, szName: "SplashRate", rDefault: 10));
218 }
219
220 // material reactions
221 pComp->Value(rStruct: mkNamingAdapt(
222 rValue: mkSTLContainerAdapt(rTarget&: CustomReactionList),
223 szName: "Reaction", rDefault: std::vector<C4MaterialReaction>()));
224}
225
226// C4Material
227
228C4Material::C4Material()
229{
230 BlastShiftTo = 0;
231 InMatConvertTo = MNone;
232 BelowTempConvertTo = 0;
233 AboveTempConvertTo = 0;
234}
235
236void C4Material::UpdateScriptPointers()
237{
238 for (uint32_t i = 0; i < CustomReactionList.size(); ++i)
239 CustomReactionList[i].ResolveScriptFuncs(szMatName: Name);
240}
241
242// C4MaterialMap
243
244C4MaterialMap::C4MaterialMap() : DefReactConvert(&mrfConvert), DefReactPoof(&mrfPoof), DefReactCorrode(&mrfCorrode), DefReactIncinerate(&mrfIncinerate), DefReactInsert(&mrfInsert)
245{
246 Default();
247}
248
249C4MaterialMap::~C4MaterialMap()
250{
251 Clear();
252}
253
254void C4MaterialMap::Clear()
255{
256 delete[] Map; Map = nullptr;
257 delete[] ppReactionMap; ppReactionMap = nullptr;
258}
259
260int32_t C4MaterialMap::Load(C4Group &hGroup, C4Group *OverloadFile)
261{
262 char entryname[256 + 1];
263
264 // Determine number of materials in files
265 int32_t mat_num = hGroup.EntryCount(C4CFN_MaterialFiles);
266
267 // Allocate new map
268 C4Material *pNewMap = new C4Material[mat_num + Num];
269 if (!pNewMap) return 0;
270
271 // Load material cores to map
272 hGroup.ResetSearch(); int32_t cnt = 0;
273 while (hGroup.FindNextEntry(C4CFN_MaterialFiles, sFileName: entryname))
274 {
275 // Load mat
276 if (!pNewMap[cnt].Load(hGroup, szEntryName: entryname))
277 {
278 delete[] pNewMap; return 0;
279 }
280 // A new material?
281 if (Get(szMaterial: pNewMap[cnt].Name) == MNone)
282 cnt++;
283 }
284
285 // Take over old materials.
286 for (int32_t i = 0; i < Num; i++)
287 {
288 pNewMap[cnt + i] = Map[i];
289 }
290 delete[] Map;
291 Map = pNewMap;
292
293 // set material number
294 Num += cnt;
295
296 return cnt;
297}
298
299int32_t C4MaterialMap::Get(const char *szMaterial)
300{
301 int32_t cnt;
302 for (cnt = 0; cnt < Num; cnt++)
303 if (SEqualNoCase(szStr1: szMaterial, szStr2: Map[cnt].Name))
304 return cnt;
305 return MNone;
306}
307
308void C4MaterialMap::CrossMapMaterials() // Called after load
309{
310 // build reaction function map
311 delete[] ppReactionMap;
312 typedef C4MaterialReaction *C4MaterialReactionPtr;
313 ppReactionMap = new C4MaterialReactionPtr[(Num + 1) * (Num + 1)];
314 for (int32_t iMatPXS = -1; iMatPXS < Num; iMatPXS++)
315 {
316 C4Material *pMatPXS = (iMatPXS + 1) ? Map + iMatPXS : nullptr;
317 for (int32_t iMatLS = -1; iMatLS < Num; iMatLS++)
318 {
319 C4MaterialReaction *pReaction = nullptr;
320 C4Material *pMatLS = (iMatLS + 1) ? Map + iMatLS : nullptr;
321 // natural stuff: material conversion here?
322 if (pMatPXS && pMatPXS->sInMatConvert.getLength() && SEqualNoCase(szStr1: pMatPXS->sInMatConvert.getData(), szStr2: pMatLS ? pMatLS->Name : C4TLS_MatSky))
323 pReaction = &DefReactConvert;
324 // the rest is happening for same/higher densities only
325 else if ((MatDensity(mat: iMatPXS) <= MatDensity(mat: iMatLS)) && pMatPXS && pMatLS)
326 {
327 // incindiary vs extinguisher
328 if ((pMatPXS->Incindiary && pMatLS->Extinguisher) || (pMatPXS->Extinguisher && pMatLS->Incindiary))
329 pReaction = &DefReactPoof;
330 // incindiary vs inflammable
331 else if ((pMatPXS->Incindiary && pMatLS->Inflammable) || (pMatPXS->Inflammable && pMatLS->Incindiary))
332 pReaction = &DefReactIncinerate;
333 // corrosive vs corrode
334 else if (pMatPXS->Corrosive && pMatLS->Corrode)
335 pReaction = &DefReactCorrode;
336 // otherwise, when hitting same or higher density: Material insertion
337 else
338 pReaction = &DefReactInsert;
339 }
340 // assign the function; or nullptr for no reaction
341 SetMatReaction(iPXSMat: iMatPXS, iLSMat: iMatLS, pReact: pReaction);
342 }
343 }
344 // material-specific initialization
345 int32_t cnt;
346 for (cnt = 0; cnt < Num; cnt++)
347 {
348 C4Material *pMat = Map + cnt;
349 const char *szTextureOverlay = nullptr;
350 // newgfx: init pattern
351 if (Map[cnt].sTextureOverlay.getLength())
352 if (Game.TextureMap.GetTexture(szTexture: Map[cnt].sTextureOverlay.getData()))
353 {
354 szTextureOverlay = Map[cnt].sTextureOverlay.getData();
355 // backwards compatibility: if a pattern was specified although the no-pattern flag was set, overwrite that flag
356 if (Map[cnt].OverlayType & C4MatOv_None)
357 {
358 DebugLog(level: spdlog::level::err, fmt: "Error in overlay of material {}: Flag C4MatOv_None ignored because a custom overlay ({}) was specified!", args: +Map[cnt].Name, args&: szTextureOverlay);
359 Map[cnt].OverlayType &= ~C4MatOv_None;
360 }
361 }
362 // default to smooth
363 if (!szTextureOverlay)
364 szTextureOverlay = "Smooth";
365 // search/create entry in texmap
366 Map[cnt].DefaultMatTex = Game.TextureMap.GetIndex(szMaterial: Map[cnt].Name, szTexture: szTextureOverlay, fAddIfNotExist: true,
367 szErrorIfFailed: std::format(fmt: "DefaultMatTex of mat {}", args: +Map[cnt].Name).c_str());
368 const C4TexMapEntry *pTex = Game.TextureMap.GetEntry(iIndex: Map[cnt].DefaultMatTex);
369 if (pTex)
370 {
371 // take pattern
372 Map[cnt].MatPattern = pTex->getPattern();
373 // special zooming for overlay
374 Map[cnt].MatPattern.SetZoom((Map[cnt].OverlayType & C4MatOv_Exact) ? 1 : 2);
375 }
376 // init PXS facet
377 C4Surface *sfcTexture;
378 C4Texture *Texture;
379 if (Map[cnt].sPXSGfx.getLength())
380 if (Texture = Game.TextureMap.GetTexture(szTexture: Map[cnt].sPXSGfx.getData()))
381 if (sfcTexture = Texture->Surface32)
382 Map[cnt].PXSFace.Set(nsfc: sfcTexture, nx: Map[cnt].PXSGfxRt.x, ny: Map[cnt].PXSGfxRt.y, nwdt: Map[cnt].PXSGfxRt.Wdt, nhgt: Map[cnt].PXSGfxRt.Hgt);
383 // evaluate reactions for that material
384 for (unsigned int iRCnt = 0; iRCnt < pMat->CustomReactionList.size(); ++iRCnt)
385 {
386 C4MaterialReaction *pReact = &(pMat->CustomReactionList[iRCnt]);
387 if (pReact->sConvertMat.getLength()) pReact->iConvertMat = Get(szMaterial: pReact->sConvertMat.getData()); else pReact->iConvertMat = -1;
388 // evaluate target spec
389 int32_t tmat;
390 if (MatValid(mat: tmat = Get(szMaterial: pReact->TargetSpec.getData())))
391 {
392 // single material target
393 if (pReact->fInverseSpec)
394 {
395 for (int32_t cnt2 = -1; cnt2 < Num; cnt2++)
396 {
397 if (cnt2 != tmat)
398 {
399 SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
400 }
401 }
402 }
403 else
404 {
405 SetMatReaction(iPXSMat: cnt, iLSMat: tmat, pReact);
406 }
407 }
408 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "All"))
409 {
410 // add to all materials, including sky
411 if (!pReact->fInverseSpec) for (int32_t cnt2 = -1; cnt2 < Num; cnt2++) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
412 }
413 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Solid"))
414 {
415 // add to all solid materials
416 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
417 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (DensitySolid(dens: Map[cnt2].Density) != pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
418 }
419 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "SemiSolid"))
420 {
421 // add to all semisolid materials
422 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
423 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (DensitySemiSolid(dens: Map[cnt2].Density) != pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
424 }
425 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Background"))
426 {
427 // add to all BG materials, including sky
428 if (!pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
429 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (!Map[cnt2].Density != pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
430 }
431 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Sky"))
432 {
433 // add to sky
434 if (!pReact->fInverseSpec)
435 SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
436 else
437 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
438 }
439 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Incindiary"))
440 {
441 // add to all incendiary materials
442 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
443 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (!Map[cnt2].Incindiary == pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
444 }
445 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Extinguisher"))
446 {
447 // add to all incendiary materials
448 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
449 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (!Map[cnt2].Extinguisher == pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
450 }
451 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Inflammable"))
452 {
453 // add to all incendiary materials
454 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
455 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (!Map[cnt2].Inflammable == pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
456 }
457 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Corrosive"))
458 {
459 // add to all incendiary materials
460 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
461 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (!Map[cnt2].Corrosive == pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
462 }
463 else if (SEqualNoCase(szStr1: pReact->TargetSpec.getData(), szStr2: "Corrode"))
464 {
465 // add to all incendiary materials
466 if (pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: -1, pReact);
467 for (int32_t cnt2 = 0; cnt2 < Num; cnt2++) if (!Map[cnt2].Corrode == pReact->fInverseSpec) SetMatReaction(iPXSMat: cnt, iLSMat: cnt2, pReact);
468 }
469 }
470 }
471 // second loop (DefaultMatTex is needed by GetIndexMatTex)
472 for (cnt = 0; cnt < Num; cnt++)
473 {
474 if (Map[cnt].sBlastShiftTo.getLength())
475 Map[cnt].BlastShiftTo = Game.TextureMap.GetIndexMatTex(szMaterialTexture: Map[cnt].sBlastShiftTo.getData(), szDefaultTexture: nullptr, fAddIfNotExist: true, szErrorIfFailed: std::format(fmt: "BlastShiftTo of mat {}", args: +Map[cnt].Name).c_str());
476 if (Map[cnt].sInMatConvertTo.getLength())
477 Map[cnt].InMatConvertTo = Get(szMaterial: Map[cnt].sInMatConvertTo.getData());
478 if (Map[cnt].sBelowTempConvertTo.getLength())
479 Map[cnt].BelowTempConvertTo = Game.TextureMap.GetIndexMatTex(szMaterialTexture: Map[cnt].sBelowTempConvertTo.getData(), szDefaultTexture: nullptr, fAddIfNotExist: true, szErrorIfFailed: std::format(fmt: "BelowTempConvertTo of mat {}", args: +Map[cnt].Name).c_str());
480 if (Map[cnt].sAboveTempConvertTo.getLength())
481 Map[cnt].AboveTempConvertTo = Game.TextureMap.GetIndexMatTex(szMaterialTexture: Map[cnt].sAboveTempConvertTo.getData(), szDefaultTexture: nullptr, fAddIfNotExist: true, szErrorIfFailed: std::format(fmt: "AboveTempConvertTo of mat {}", args: +Map[cnt].Name).c_str());
482 }
483}
484
485void C4MaterialMap::SetMatReaction(int32_t iPXSMat, int32_t iLSMat, C4MaterialReaction *pReact)
486{
487 // evaluate reaction swap
488 if (pReact && pReact->fReverse) std::swap(a&: iPXSMat, b&: iLSMat);
489 // set it
490 ppReactionMap[(iLSMat + 1) * (Num + 1) + iPXSMat + 1] = pReact;
491}
492
493bool C4MaterialMap::SaveEnumeration(C4Group &hGroup)
494{
495 char *mapbuf = new char[1000];
496 mapbuf[0] = 0;
497 SAppend(szSource: "[Enumeration]", szTarget: mapbuf); SAppend(LineFeed, szTarget: mapbuf);
498 for (int32_t cnt = 0; cnt < Num; cnt++)
499 {
500 SAppend(szSource: Map[cnt].Name, szTarget: mapbuf);
501 SAppend(LineFeed, szTarget: mapbuf);
502 }
503 SAppend(EndOfFile, szTarget: mapbuf);
504 return hGroup.Add(C4CFN_MatMap, pBuffer: mapbuf, iSize: SLen(sptr: mapbuf), fChild: false, fHoldBuffer: true);
505}
506
507bool C4MaterialMap::LoadEnumeration(C4Group &hGroup)
508{
509 // Load enumeration map (from savegame), succeed if not present
510 StdStrBuf mapbuf;
511 if (!hGroup.LoadEntryString(C4CFN_MatMap, Buf&: mapbuf)) return true;
512
513 // Sort material array by enumeration map, fail if some missing
514 const char *csearch;
515 char cmatname[C4M_MaxName + 1];
516 int32_t cmat = 0;
517 if (!(csearch = SSearch(szString: mapbuf.getData(), szIndex: "[Enumeration]"))) { return false; }
518 csearch = SAdvanceSpace(szSPos: csearch);
519 while (IsIdentifier(cChar: *csearch))
520 {
521 SCopyIdentifier(szSource: csearch, sTarget: cmatname, iMaxL: C4M_MaxName);
522 if (!SortEnumeration(iMat: cmat, szMatName: cmatname))
523 {
524 // Output error message!
525 return false;
526 }
527 cmat++;
528 csearch += SLen(sptr: cmatname);
529 csearch = SAdvanceSpace(szSPos: csearch);
530 }
531
532 return true;
533}
534
535bool C4MaterialMap::SortEnumeration(int32_t iMat, const char *szMatName)
536{
537 // Not enough materials loaded
538 if (iMat >= Num) return false;
539
540 // Find requested mat
541 int32_t cmat;
542 for (cmat = iMat; cmat < Num; cmat++)
543 if (SEqual(szStr1: szMatName, szStr2: Map[cmat].Name))
544 break;
545 // Not found
546 if (cmat >= Num) return false;
547
548 // already the same?
549 if (cmat == iMat) return true;
550
551 // Move requested mat to indexed position
552 std::swap(a&: Map[iMat], b&: Map[cmat]);
553
554 return true;
555}
556
557void C4MaterialMap::Default()
558{
559 Num = 0;
560 Map = nullptr;
561 ppReactionMap = nullptr;
562}
563
564bool mrfInsertCheck(int32_t &iX, int32_t &iY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, bool *pfPosChanged)
565{
566 // always manipulating pos/speed here
567 if (pfPosChanged) *pfPosChanged = true;
568
569 // Rough contact? May splash
570 if (fYDir > itofix(x: 1))
571 if (Game.Material.Map[iPxsMat].SplashRate && !Random(iRange: Game.Material.Map[iPxsMat].SplashRate))
572 {
573 fYDir = -fYDir / 8;
574 fXDir = fXDir / 8 + FIXED100(x: Random(iRange: 200) - 100);
575 if (fYDir) return false;
576 }
577
578 // Contact: Stop
579 fYDir = 0;
580
581 // Incindiary mats smoke on contact even before doing their slide
582 if (Game.Material.Map[iPxsMat].Incindiary)
583 if (!Random(iRange: 25)) Smoke(tx: iX, ty: iY, level: 4 + Rnd3());
584
585 // Move by mat path/slide
586 int32_t iSlideX = iX, iSlideY = iY;
587 if (Game.Landscape.FindMatSlide(fx&: iSlideX, fy&: iSlideY, ydir: Sign(GravAccel), mdens: Game.Material.Map[iPxsMat].Density, mslide: Game.Material.Map[iPxsMat].MaxSlide))
588 {
589 if (iPxsMat == iLsMat)
590 {
591 iX = iSlideX; iY = iSlideY; fXDir = 0; return false;
592 }
593 // Accelerate into the direction
594 fXDir = (fXDir * 10 + Sign(val: iSlideX - iX)) / 11 + FIXED10(x: Random(iRange: 5) - 2);
595 // Slide target in range? Move there directly.
596 if (Abs(val: iX - iSlideX) <= Abs(val: fixtoi(x: fXDir)))
597 {
598 iX = iSlideX;
599 iY = iSlideY;
600 if (fYDir <= 0) fXDir = 0;
601 }
602 // Continue existance
603 return false;
604 }
605 // insertion OK
606 return true;
607}
608
609bool mrfUserCheck(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
610{
611 // check execution mask
612 if ((1 << evEvent) & ~pReaction->iExecMask) return false;
613 // do splash/slide check, if desired
614 if (pReaction->fInsertionCheck && evEvent == meePXSMove)
615 if (!mrfInsertCheck(iX, iY, fXDir, fYDir, iPxsMat, iLsMat, pfPosChanged))
616 return false;
617 // checks OK; reaction may be applied
618 return true;
619}
620
621bool C4MaterialMap::mrfConvert(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
622{
623 if (pReaction->fUserDefined) if (!mrfUserCheck(pReaction, iX, iY, iLSPosX, iLSPosY, fXDir, fYDir, iPxsMat, iLsMat, evEvent, pfPosChanged)) return false;
624 switch (evEvent)
625 {
626 case meePXSMove: // PXS movement
627 // for hardcoded stuff: only InMatConvert is Snow in Water, which does not have any collision proc
628 if (!pReaction->fUserDefined) break;
629 // user-defined conversions may also convert upon hitting materials
630
631 case meePXSPos: // PXS check before movement
632 {
633 // Check depth
634 int32_t iDepth = pReaction->fUserDefined ? pReaction->iDepth : Game.Material.Map[iPxsMat].InMatConvertDepth;
635 if (!iDepth || GBackMat(x: iX, y: iY - iDepth) == iLsMat)
636 {
637 // Convert
638 iPxsMat = pReaction->fUserDefined ? pReaction->iConvertMat : Game.Material.Map[iPxsMat].InMatConvertTo;
639 if (!MatValid(mat: iPxsMat))
640 // Convert failure (target mat not be loaded, or target may be C4TLS_MatSky): Kill Pix
641 return true;
642 // stop movement after conversion
643 fXDir = fYDir = 0;
644 if (pfPosChanged) *pfPosChanged = true;
645 }
646 }
647 break;
648
649 case meeMassMove: // MassMover-movement
650 // Conversion-transfer to PXS
651 Game.PXS.Create(mat: iPxsMat, ix: itofix(x: iX), iy: itofix(x: iY));
652 return true;
653 }
654 // not handled
655 return false;
656}
657
658bool C4MaterialMap::mrfPoof(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
659{
660 if (pReaction->fUserDefined) if (!mrfUserCheck(pReaction, iX, iY, iLSPosX, iLSPosY, fXDir, fYDir, iPxsMat, iLsMat, evEvent, pfPosChanged)) return false;
661 switch (evEvent)
662 {
663 case meeMassMove: // MassMover-movement
664 case meePXSPos: // PXS check before movement: Kill both landscape and PXS mat
665 Game.Landscape.ExtractMaterial(fx: iLSPosX, fy: iLSPosY);
666 if (!Rnd3()) Smoke(tx: iX, ty: iY, level: 3);
667 if (!Rnd3()) StartSoundEffectAt(name: "Pshshsh", x: iX, y: iY);
668 return true;
669
670 case meePXSMove: // PXS movement
671 // incindiary/extinguisher/corrosives are always same density proc; so do insertion check first
672 if (!pReaction->fUserDefined)
673 if (!mrfInsertCheck(iX, iY, fXDir, fYDir, iPxsMat, iLsMat, pfPosChanged))
674 // either splash or slide prevented interaction
675 return false;
676 // Always kill both landscape and PXS mat
677 Game.Landscape.ExtractMaterial(fx: iLSPosX, fy: iLSPosY);
678 if (!Rnd3()) Smoke(tx: iX, ty: iY, level: 3);
679 if (!Rnd3()) StartSoundEffectAt(name: "Pshshsh", x: iX, y: iY);
680 return true;
681 }
682 // not handled
683 return false;
684}
685
686bool C4MaterialMap::mrfCorrode(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
687{
688 if (pReaction->fUserDefined) if (!mrfUserCheck(pReaction, iX, iY, iLSPosX, iLSPosY, fXDir, fYDir, iPxsMat, iLsMat, evEvent, pfPosChanged)) return false;
689 switch (evEvent)
690 {
691 case meePXSPos: // PXS check before movement
692 // No corrosion - it would make acid incredibly effective
693 break;
694 case meeMassMove: // MassMover-movement
695 {
696 // evaluate corrosion percentage
697 bool fDoCorrode;
698 if (pReaction->fUserDefined)
699 fDoCorrode = (Random(iRange: 100) < pReaction->iCorrosionRate);
700 else
701 fDoCorrode = (Random(iRange: 100) < Game.Material.Map[iPxsMat].Corrosive) && (Random(iRange: 100) < Game.Material.Map[iLsMat].Corrode);
702 if (fDoCorrode)
703 {
704 ClearBackPix(tx: iLSPosX, ty: iLSPosY);
705 if (!Random(iRange: 5)) Smoke(tx: iX, ty: iY, level: 3 + Random(iRange: 3));
706 if (!Random(iRange: 20)) StartSoundEffectAt(name: "Corrode", x: iX, y: iY);
707 return true;
708 }
709 }
710 break;
711
712 case meePXSMove: // PXS movement
713 {
714 // corrodes to corrosives are always same density proc; so do insertion check first
715 if (!pReaction->fUserDefined)
716 if (!mrfInsertCheck(iX, iY, fXDir, fYDir, iPxsMat, iLsMat, pfPosChanged))
717 // either splash or slide prevented interaction
718 return false;
719 // evaluate corrosion percentage
720 bool fDoCorrode;
721 if (pReaction->fUserDefined)
722 fDoCorrode = (Random(iRange: 100) < pReaction->iCorrosionRate);
723 else
724 fDoCorrode = (Random(iRange: 100) < Game.Material.Map[iPxsMat].Corrosive) && (Random(iRange: 100) < Game.Material.Map[iLsMat].Corrode);
725 if (fDoCorrode)
726 {
727 ClearBackPix(tx: iLSPosX, ty: iLSPosY);
728 Game.Landscape.CheckInstabilityRange(tx: iLSPosX, ty: iLSPosY);
729 if (!Random(iRange: 5)) Smoke(tx: iX, ty: iY, level: 3 + Random(iRange: 3));
730 if (!Random(iRange: 20)) StartSoundEffectAt(name: "Corrode", x: iX, y: iY);
731 return true;
732 }
733 // Else: dead. Insert material here
734 Game.Landscape.InsertMaterial(mat: iPxsMat, tx: iX, ty: iY);
735 return true;
736 }
737 }
738 // not handled
739 return false;
740}
741
742bool C4MaterialMap::mrfIncinerate(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
743{
744 // not available as user reaction
745 assert(!pReaction->fUserDefined);
746 switch (evEvent)
747 {
748 case meeMassMove: // MassMover-movement
749 case meePXSPos: // PXS check before movement
750 if (Game.Landscape.Incinerate(x: iX, y: iY)) return true;
751 break;
752
753 case meePXSMove: // PXS movement
754 // incinerate to inflammables are always same density proc; so do insertion check first
755 if (!mrfInsertCheck(iX, iY, fXDir, fYDir, iPxsMat, iLsMat, pfPosChanged))
756 // either splash or slide prevented interaction
757 return false;
758 // evaluate inflammation (should always succeed)
759 if (Game.Landscape.Incinerate(x: iX, y: iY)) return true;
760 // Else: dead. Insert material here
761 Game.Landscape.InsertMaterial(mat: iPxsMat, tx: iX, ty: iY);
762 return true;
763 }
764 // not handled
765 return false;
766}
767
768bool C4MaterialMap::mrfInsert(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
769{
770 if (pReaction->fUserDefined) if (!mrfUserCheck(pReaction, iX, iY, iLSPosX, iLSPosY, fXDir, fYDir, iPxsMat, iLsMat, evEvent, pfPosChanged)) return false;
771 switch (evEvent)
772 {
773 case meePXSPos: // PXS check before movement
774 break;
775
776 case meePXSMove: // PXS movement
777 {
778 // check for bounce/slide
779 if (!pReaction->fUserDefined)
780 if (!mrfInsertCheck(iX, iY, fXDir, fYDir, iPxsMat, iLsMat, pfPosChanged))
781 // continue existing
782 return false;
783 // Else: dead. Insert material here
784 Game.Landscape.InsertMaterial(mat: iPxsMat, tx: iX, ty: iY);
785 return true;
786 }
787
788 case meeMassMove: // MassMover-movement
789 break;
790 }
791 // not handled
792 return false;
793}
794
795bool C4MaterialMap::mrfScript(C4MaterialReaction *pReaction, int32_t &iX, int32_t &iY, int32_t iLSPosX, int32_t iLSPosY, C4Fixed &fXDir, C4Fixed &fYDir, int32_t &iPxsMat, int32_t iLsMat, MaterialInteractionEvent evEvent, bool *pfPosChanged)
796{
797 // do generic checks for user-defined reactions
798 if (!mrfUserCheck(pReaction, iX, iY, iLSPosX, iLSPosY, fXDir, fYDir, iPxsMat, iLsMat, evEvent, pfPosChanged))
799 return false;
800
801 // check script func
802 if (!pReaction->pScriptFunc) return false;
803 // OK - let's call it!
804 // 0 1 2 3 4 5 6 7 8
805 int32_t iXDir1, iYDir1, iXDir2, iYDir2;
806 auto parX = C4VInt(iVal: iX), parY = C4VInt(iVal: iY), parXDir = C4VInt(iVal: iXDir1 = fixtoi(x: fXDir, prec: 100)), parYDir = C4VInt(iVal: iYDir1 = fixtoi(x: fYDir, prec: 100)), parPxsMat = C4VInt(iVal: iPxsMat);
807 const C4AulParSet pars{parX.GetRef(), parY.GetRef(), C4VInt(iVal: iLSPosX), C4VInt(iVal: iLSPosY), parXDir.GetRef(), parYDir.GetRef(), parPxsMat.GetRef(), C4VInt(iVal: iLsMat), C4VInt(iVal: evEvent)};
808 if (pReaction->pScriptFunc->Exec(pObj: nullptr, pPars: pars, fPassErrors: false))
809 {
810 // PXS shall be killed!
811 return true;
812 }
813 // PXS shall exist further: write back parameters
814 iPxsMat = parPxsMat.getInt();
815 int32_t iX2 = parX.getInt(), iY2 = parY.getInt();
816 iXDir2 = parXDir.getInt(); iYDir2 = parYDir.getInt();
817 if (iX != iX2 || iY != iY2 || iXDir1 != iXDir2 || iYDir1 != iYDir2)
818 {
819 // changes to pos/speed detected
820 if (pfPosChanged) *pfPosChanged = true;
821 iX = iX2; iY = iY2;
822 fXDir = FIXED100(x: iXDir2);
823 fYDir = FIXED100(x: iYDir2);
824 }
825 // OK; done
826 return false;
827}
828
829void C4MaterialMap::UpdateScriptPointers()
830{
831 // update in all materials
832 for (int32_t i = 0; i < Num; ++i) Map[i].UpdateScriptPointers();
833}
834