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// C4AulFun-based effects assigned to an object
18/* Also contains some helper functions for various landscape effects */
19
20#include <C4Include.h>
21
22#include <C4Object.h>
23#include <C4Random.h>
24#include <C4Log.h>
25#include <C4Game.h>
26#include <C4Wrappers.h>
27
28#include <format>
29#include <numbers>
30
31void C4Effect::AssignCallbackFunctions()
32{
33 C4AulScript *pSrcScript = GetCallbackScript();
34 // compose function names and search them
35 pFnStart = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxStart, args: +Name).c_str());
36 pFnStop = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxStop, args: +Name).c_str());
37 pFnTimer = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxTimer, args: +Name).c_str());
38 pFnEffect = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxEffect, args: +Name).c_str());
39 pFnDamage = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxDamage, args: +Name).c_str());
40}
41
42C4AulScript *C4Effect::GetCallbackScript()
43{
44 // def script or global only?
45 C4AulScript *pSrcScript; C4Def *pDef;
46 if (pCommandTarget)
47 {
48 pSrcScript = &pCommandTarget->Def->Script;
49 // overwrite ID for sync safety in runtime join
50 idCommandTarget = pCommandTarget->id;
51 }
52 else if (idCommandTarget && (pDef = Game.Defs.ID2Def(id: idCommandTarget)))
53 pSrcScript = &pDef->Script;
54 else
55 pSrcScript = &Game.ScriptEngine;
56 return pSrcScript;
57}
58
59C4Effect::C4Effect(C4Object *pForObj, const char *szName, int32_t iPrio, int32_t iTimerIntervall, C4Object *pCmdTarget, C4ID idCmdTarget, const C4Value &rVal1, const C4Value &rVal2, const C4Value &rVal3, const C4Value &rVal4, bool fDoCalls, int32_t &riStoredAsNumber, bool passErrors)
60 : EffectVars(0)
61{
62 C4Effect *pPrev, *pCheck;
63 // assign values
64 SCopy(szSource: szName, sTarget: Name, iMaxL: C4MaxDefString);
65 iPriority = 0; // effect is not yet valid; some callbacks to other effects are done before
66 riStoredAsNumber = 0;
67 iIntervall = iTimerIntervall;
68 iTime = 0;
69 pCommandTarget = pCmdTarget;
70 pCommandTarget.Enumerate();
71 idCommandTarget = idCmdTarget;
72 AssignCallbackFunctions();
73 // get effect target
74 C4Effect **ppEffectList = pForObj ? &pForObj->pEffects : &Game.pGlobalEffects;
75 // assign a unique number for that object
76 iNumber = 1;
77 for (pCheck = *ppEffectList; pCheck; pCheck = pCheck->pNext)
78 if (pCheck->iNumber >= iNumber) iNumber = pCheck->iNumber + 1;
79 // register into object
80 pPrev = *ppEffectList;
81 if (pPrev && Abs(val: pPrev->iPriority) < iPrio)
82 {
83 while (pCheck = pPrev->pNext)
84 if (Abs(val: pCheck->iPriority) >= iPrio) break; else pPrev = pCheck;
85 // insert after previous
86 pNext = pPrev->pNext;
87 pPrev->pNext = this;
88 }
89 else
90 {
91 // insert as first effect
92 pNext = *ppEffectList;
93 *ppEffectList = this;
94 }
95 // no calls to be done: finished here
96 if (!fDoCalls) return;
97 // ask all effects with higher priority first - except for prio 1 effects, which are considered out of the priority call chain (as per doc)
98 try
99 {
100 bool fRemoveUpper = (iPrio != 1);
101 // note that apart from denying the creation of this effect, higher priority effects may also remove themselves
102 // or do other things with the effect list
103 // (which does not quite make sense, because the effect might be denied by another effect)
104 // so the priority is assigned after this call, marking this effect dead before it's definitely valid
105 if (fRemoveUpper && pNext)
106 {
107 int32_t iResult = pNext->Check(pForObj, szCheckEffect: Name, iPrio, iTimer: iIntervall, rVal1, rVal2, rVal3, rVal4, passErrors);
108 if (iResult)
109 {
110 // effect denied (iResult = -1), added to an effect (iResult = Number of that effect)
111 // or added to an effect that destroyed itself (iResult = -2)
112 if (iResult != C4Fx_Effect_Deny) riStoredAsNumber = iResult;
113 // effect is still marked dead
114 return;
115 }
116 }
117 // init effect
118 // higher-priority effects must be deactivated temporarily, and then reactivated regarding the new effect
119 // higher-level effects should not be inserted during the process of removing or adding a lower-level effect
120 // because that would cause a wrong initialization order
121 // (hardly ever causing trouble, however...)
122 C4Effect *pLastRemovedEffect = nullptr;
123 if (fRemoveUpper && pNext && pFnStart)
124 TempRemoveUpperEffects(pObj: pForObj, fTempRemoveThis: false, ppLastRemovedEffect: &pLastRemovedEffect);
125 // bad things may happen
126 if (pForObj && !pForObj->Status) return; // this will be invalid!
127 iPriority = iPrio; // validate effect now
128 if (pFnStart)
129 if (pFnStart->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj: pForObj), C4VInt(iVal: iNumber), C4VInt(iVal: 0), rVal1, rVal2, rVal3, rVal4}, fPassErrors: true, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Start_Deny)
130 // the effect denied to start: assume it hasn't, and mark it dead
131 SetDead();
132 if (fRemoveUpper && pNext && pFnStart)
133 TempReaddUpperEffects(pObj: pForObj, pLastReaddEffect: pLastRemovedEffect);
134 if (pForObj && !pForObj->Status) return; // this will be invalid!
135 // this effect has been created; hand back the number
136 riStoredAsNumber = iNumber;
137 }
138 catch (...)
139 {
140 if (*ppEffectList == this)
141 {
142 *ppEffectList = pNext;
143 }
144 else
145 {
146 pPrev->pNext = pNext;
147 }
148 pNext = nullptr;
149 throw;
150 }
151}
152
153C4Effect::C4Effect(StdCompiler *pComp) : EffectVars(0)
154{
155 // defaults
156 iNumber = iPriority = iTime = iIntervall = 0;
157 pNext = nullptr;
158 // compile
159 pComp->Value(rStruct&: *this);
160}
161
162C4Effect::~C4Effect()
163{
164 // del following effects (not recursively)
165 C4Effect *pEffect;
166 while (pEffect = pNext)
167 {
168 pNext = pEffect->pNext;
169 pEffect->pNext = nullptr;
170 delete pEffect;
171 }
172}
173
174void C4Effect::EnumeratePointers()
175{
176 // enum in all effects
177 C4Effect *pEff = this;
178 do
179 {
180 // command target
181 pEff->pCommandTarget.Enumerate();
182 // effect var denumeration: not necessary, because this is done while saving
183 } while (pEff = pEff->pNext);
184}
185
186void C4Effect::DenumeratePointers()
187{
188 // denum in all effects
189 C4Effect *pEff = this;
190 do
191 {
192 // command target
193 pEff->pCommandTarget.Denumerate();
194 // variable pointers
195 pEff->EffectVars.DenumeratePointers();
196 // assign any callback functions
197 pEff->AssignCallbackFunctions();
198 } while (pEff = pEff->pNext);
199}
200
201void C4Effect::ClearPointers(C4Object *pObj)
202{
203 // clear pointers in all effects
204 C4Effect *pEff = this;
205 do
206 // command target lost: effect dead w/o callback
207 if (pEff->pCommandTarget == pObj)
208 {
209 pEff->SetDead();
210 pEff->pCommandTarget = nullptr;
211 }
212 while (pEff = pEff->pNext);
213}
214
215C4Effect *C4Effect::Get(const char *szName, int32_t iIndex, int32_t iMaxPriority)
216{
217 // safety
218 if (!szName) return nullptr;
219 // check all effects
220 C4Effect *pEff = this;
221 do
222 {
223 // skip dead
224 if (pEff->IsDead()) continue;
225 // skip effects with too high priority
226 if (iMaxPriority && pEff->iPriority > iMaxPriority) continue;
227 // wildcard compare name
228 const char *szEffectName = pEff->Name;
229 if (!SWildcardMatchEx(szString: szEffectName, szWildcard: szName)) continue;
230 // effect name matches
231 // check index
232 if (iIndex--) continue;
233 // effect found
234 return pEff;
235 } while (pEff = pEff->pNext);
236 // nothing found
237 return nullptr;
238}
239
240C4Effect *C4Effect::Get(int32_t iNumber, bool fIncludeDead, int32_t iMaxPriority)
241{
242 // check all effects
243 C4Effect *pEff = this;
244 do
245 if (pEff->iNumber == iNumber)
246 {
247 if (!pEff->IsDead() || fIncludeDead)
248 if (!iMaxPriority || pEff->iPriority <= iMaxPriority)
249 return pEff;
250 // effect found but denied
251 return nullptr;
252 }
253 while (pEff = pEff->pNext);
254 // nothing found
255 return nullptr;
256}
257
258int32_t C4Effect::GetCount(const char *szMask, int32_t iMaxPriority)
259{
260 // count all matching effects
261 int32_t iCnt = 0; C4Effect *pEff = this;
262 do if (!pEff->IsDead())
263 if (!szMask || SWildcardMatchEx(szString: pEff->Name, szWildcard: szMask))
264 if (!iMaxPriority || pEff->iPriority <= iMaxPriority)
265 ++iCnt;
266 while (pEff = pEff->pNext);
267 // return count
268 return iCnt;
269}
270
271int32_t C4Effect::Check(C4Object *pForObj, const char *szCheckEffect, int32_t iPrio, int32_t iTimer, const C4Value &rVal1, const C4Value &rVal2, const C4Value &rVal3, const C4Value &rVal4, bool passErrors)
272{
273 // priority=1: always OK; no callbacks
274 if (iPrio == 1) return 0;
275 // check this and other effects
276 C4Effect *pAddToEffect = nullptr; bool fDoTempCallsForAdd = false;
277 C4Effect *pLastRemovedEffect = nullptr;
278 for (C4Effect *pCheck = this; pCheck; pCheck = pCheck->pNext)
279 {
280 if (!pCheck->IsDead() && pCheck->pFnEffect && pCheck->iPriority >= iPrio)
281 {
282 int32_t iResult = pCheck->pFnEffect->Exec(pObj: pCheck->pCommandTarget, pPars: {C4VString(strString: szCheckEffect), C4VObj(pObj: pForObj), C4VInt(iVal: pCheck->iNumber), C4Value(), rVal1, rVal2, rVal3, rVal4}, fPassErrors: passErrors, nonStrict3WarnConversionOnly: true).getInt();
283 if (iResult == C4Fx_Effect_Deny)
284 // effect denied
285 return C4Fx_Effect_Deny;
286 // add to other effect
287 if (iResult == C4Fx_Effect_Annul || iResult == C4Fx_Effect_AnnulCalls)
288 {
289 pAddToEffect = pCheck;
290 fDoTempCallsForAdd = (iResult == C4Fx_Effect_AnnulCalls);
291 }
292 }
293 }
294 // adding to other effect?
295 if (pAddToEffect)
296 {
297 // do temp remove calls if desired
298 if (pAddToEffect->pNext && fDoTempCallsForAdd)
299 pAddToEffect->TempRemoveUpperEffects(pObj: pForObj, fTempRemoveThis: false, ppLastRemovedEffect: &pLastRemovedEffect);
300 C4Value Par1 = C4VString(strString: szCheckEffect), Par2 = C4VInt(iVal: iTimer);
301 int32_t iResult = pAddToEffect->DoCall(pObj: pForObj, PSFS_FxAdd, rVal1: Par1, rVal2: Par2, rVal3: rVal1, rVal4: rVal2, rVal5: rVal3, rVal6: rVal4).getInt();
302 // do temp readd calls if desired
303 if (pAddToEffect->pNext && fDoTempCallsForAdd)
304 pAddToEffect->TempReaddUpperEffects(pObj: pForObj, pLastReaddEffect: pLastRemovedEffect);
305 // effect removed by this call?
306 if (iResult == C4Fx_Start_Deny)
307 {
308 pAddToEffect->Kill(pObj: pForObj);
309 return C4Fx_Effect_Annul;
310 }
311 else
312 // other effect is the target effect number
313 return pAddToEffect->iNumber;
314 }
315 // added to no effect and not denied
316 return 0;
317}
318
319void C4Effect::Execute(C4Object *pObj)
320{
321 // get effect list
322 C4Effect **ppEffectList = pObj ? &pObj->pEffects : &Game.pGlobalEffects;
323 // execute all effects not marked as dead
324 C4Effect *pEffect = this, **ppPrevEffect = ppEffectList;
325 do
326 {
327 // effect dead?
328 if (pEffect->IsDead())
329 {
330 // delete it, then
331 C4Effect *pNextEffect = pEffect->pNext;
332 pEffect->pNext = nullptr;
333 delete pEffect;
334 // next effect
335 *ppPrevEffect = pEffect = pNextEffect;
336 }
337 else
338 {
339 // execute effect: time elapsed
340 ++pEffect->iTime;
341 // check timer execution
342 if (pEffect->iIntervall && !(pEffect->iTime % pEffect->iIntervall))
343 if (pEffect->pFnTimer)
344 {
345 if (pEffect->pFnTimer->Exec(pObj: pEffect->pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: pEffect->iNumber), C4VInt(iVal: pEffect->iTime)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Execute_Kill)
346 {
347 // safety: this class got deleted!
348 if (pObj && !pObj->Status) return;
349 // timer function decided to finish it
350 pEffect->Kill(pObj);
351 }
352 // safety: this class got deleted!
353 if (pObj && !pObj->Status) return;
354 }
355 else
356 // no timer function: mark dead after time elapsed
357 pEffect->Kill(pObj);
358 // next effect
359 ppPrevEffect = &pEffect->pNext;
360 pEffect = pEffect->pNext;
361 }
362 } while (pEffect);
363}
364
365void C4Effect::Kill(C4Object *pObj)
366{
367 const auto deletionTracker = TrackDeletion();
368 // active?
369 C4Effect *pLastRemovedEffect = nullptr;
370 if (IsActive())
371 {
372 // then temp remove all higher priority effects
373 TempRemoveUpperEffects(pObj, fTempRemoveThis: false, ppLastRemovedEffect: &pLastRemovedEffect);
374 }
375 else
376 {
377 // otherwise: temp reactivate before real removal
378 // this happens only if a lower priority effect removes an upper priority effect in its add- or removal-call
379 if (pFnStart && iPriority != 1)
380 {
381 pFnStart->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), C4VInt(C4FxCall_TempAddForRemoval)}, fPassErrors: false, nonStrict3WarnConversionOnly: true);
382 if (deletionTracker.IsDeleted())
383 {
384 return;
385 }
386 }
387 }
388 // remove this effect
389 int32_t iPrevPrio = iPriority; SetDead();
390 if (pFnStop)
391 {
392 if (pFnStop->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Stop_Deny)
393 {
394 // effect denied to be removed: recover
395 iPriority = iPrevPrio;
396 }
397
398 if (deletionTracker.IsDeleted())
399 {
400 return;
401 }
402 }
403 // reactivate other effects
404 TempReaddUpperEffects(pObj, pLastReaddEffect: pLastRemovedEffect);
405}
406
407void C4Effect::ClearAll(C4Object *pObj, int32_t iClearFlag)
408{
409 // simply remove access all effects recursively, and do removal calls
410 // this does not regard lower-level effects being added in the removal calls,
411 // because this could hang the engine with poorly coded effects
412 if (pNext) pNext->ClearAll(pObj, iClearFlag);
413 if ((pObj && !pObj->Status) || IsDead()) return;
414 int32_t iPrevPrio = iPriority;
415 SetDead();
416 if (pFnStop)
417 if (pFnStop->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), C4VInt(iVal: iClearFlag)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Stop_Deny)
418 {
419 // this stop-callback might have deleted the object and then denied its own removal
420 // must not modify self in this case...
421 if (pObj && !pObj->Status) return;
422 // effect denied to be removed: recover it
423 iPriority = iPrevPrio;
424 }
425}
426
427void C4Effect::DoDamage(C4Object *pObj, int32_t &riDamage, int32_t iDamageType, int32_t iCausePlr)
428{
429 // ask all effects for damage adjustments
430 C4Effect *pEff = this;
431 do
432 {
433 if (!pEff->IsDead() && pEff->pFnDamage)
434 riDamage = pEff->pFnDamage->Exec(pObj: pEff->pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: pEff->iNumber), C4VInt(iVal: riDamage), C4VInt(iVal: iDamageType), C4VInt(iVal: iCausePlr)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt();
435 if (pObj && !pObj->Status) return;
436 } while ((pEff = pEff->pNext) && riDamage);
437}
438
439C4Value C4Effect::DoCall(C4Object *pObj, const char *szFn, const C4Value &rVal1, const C4Value &rVal2, const C4Value &rVal3, const C4Value &rVal4, const C4Value &rVal5, const C4Value &rVal6, const C4Value &rVal7, bool passErrors, bool convertNilToIntBool)
440{
441 // def script or global only?
442 C4AulScript *pSrcScript; C4Def *pDef;
443 if (pCommandTarget)
444 {
445 pSrcScript = &pCommandTarget->Def->Script;
446 // overwrite ID for sync safety in runtime join
447 idCommandTarget = pCommandTarget->id;
448 }
449 else if (idCommandTarget && (pDef = Game.Defs.ID2Def(id: idCommandTarget)))
450 pSrcScript = &pDef->Script;
451 else
452 pSrcScript = &Game.ScriptEngine;
453 // call it
454 C4AulFunc *pFn = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxCustom, args: +Name, args&: szFn).c_str());
455 if (!pFn) return C4Value();
456 return pFn->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), rVal1, rVal2, rVal3, rVal4, rVal5, rVal6, rVal7}, fPassErrors: passErrors, nonStrict3WarnConversionOnly: true, convertNilToIntBool);
457}
458
459void C4Effect::OnObjectChangedDef(C4Object *pObj)
460{
461 // safety
462 if (!pObj) return;
463 // check all effects for reassignment
464 C4Effect *pCheck = this;
465 while (pCheck)
466 {
467 if (pCheck->pCommandTarget == pObj)
468 pCheck->ReAssignCallbackFunctions();
469 pCheck = pCheck->pNext;
470 }
471}
472
473void C4Effect::TempRemoveUpperEffects(C4Object *pObj, bool fTempRemoveThis, C4Effect **ppLastRemovedEffect)
474{
475 if (pObj && !pObj->Status) return; // this will be invalid!
476 // priority=1: no callbacks
477 if (iPriority == 1) return;
478 // remove from high to low priority
479 // recursive implementation...
480 C4Effect *pEff = pNext;
481 while (pEff) if (pEff->IsActive()) break; else pEff = pEff->pNext;
482 // temp remove active effects with higher priority
483 if (pEff) pEff->TempRemoveUpperEffects(pObj, fTempRemoveThis: true, ppLastRemovedEffect);
484 // temp remove this
485 if (fTempRemoveThis)
486 {
487 FlipActive();
488 // temp callbacks only for higher priority effects
489 if (pFnStop && iPriority != 1) pFnStop->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), C4VInt(C4FxCall_Temp), C4VBool(fVal: true)}, fPassErrors: false, nonStrict3WarnConversionOnly: true);
490 if (!*ppLastRemovedEffect) *ppLastRemovedEffect = this;
491 }
492}
493
494void C4Effect::TempReaddUpperEffects(C4Object *pObj, C4Effect *pLastReaddEffect)
495{
496 // nothing to do? - this will also happen if TempRemoveUpperEffects did nothing due to priority==1
497 if (!pLastReaddEffect) return;
498 if (pObj && !pObj->Status) return; // this will be invalid!
499 // simply activate all following, inactive effects
500 for (C4Effect *pEff = pNext; pEff; pEff = pEff->pNext)
501 {
502 if (pEff->IsInactiveAndNotDead())
503 {
504 pEff->FlipActive();
505 if (pEff->pFnStart && pEff->iPriority != 1) pEff->pFnStart->Exec(pObj: pEff->pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: pEff->iNumber), C4VInt(C4FxCall_Temp)}, fPassErrors: false, nonStrict3WarnConversionOnly: true);
506 }
507 // done?
508 if (pEff == pLastReaddEffect) break;
509 }
510}
511
512void C4Effect::CompileFunc(StdCompiler *pComp)
513{
514 // read name
515 pComp->Value(mkStringAdaptMI(Name));
516 pComp->Separator(eSep: StdCompiler::SEP_START); // '('
517 // read number
518 pComp->Value(rInt&: iNumber); pComp->Separator();
519 // read priority
520 pComp->Value(rInt&: iPriority); pComp->Separator();
521 // read time and intervall
522 pComp->Value(rInt&: iTime); pComp->Separator();
523 pComp->Value(rInt&: iIntervall); pComp->Separator();
524 // read object number
525 pComp->Value(rStruct&: pCommandTarget); pComp->Separator();
526 // read ID
527 pComp->Value(rStruct: mkC4IDAdapt(rValue&: idCommandTarget));
528 pComp->Separator(eSep: StdCompiler::SEP_END); // ')'
529 // read variables
530 if (pComp->isCompiler() || EffectVars.GetSize() > 0)
531 if (pComp->Separator(eSep: StdCompiler::SEP_START2)) // '['
532 {
533 pComp->Value(rStruct&: EffectVars);
534 pComp->Separator(eSep: StdCompiler::SEP_END2); // ']'
535 }
536 else
537 EffectVars.Reset();
538 // is there a next effect?
539 bool fNext = !!pNext;
540 if (pComp->hasNaming())
541 {
542 if (fNext || pComp->isCompiler())
543 fNext = pComp->Separator();
544 }
545 else
546 pComp->Value(rBool&: fNext);
547 if (!fNext) return;
548 // read next
549 pComp->Value(rStruct: mkPtrAdapt(rpObj&: pNext, fAllowNull: false));
550 // denumeration and callback assignment will be done later
551}
552
553// Internal fire effect
554
555C4Value &FxFireVarMode (C4Effect *pEffect) { return pEffect->EffectVars[0]; }
556C4Value &FxFireVarCausedBy (C4Effect *pEffect) { return pEffect->EffectVars[1]; }
557C4Value &FxFireVarBlasted (C4Effect *pEffect) { return pEffect->EffectVars[2]; }
558C4Value &FxFireVarIncineratingObj(C4Effect *pEffect) { return pEffect->EffectVars[3]; }
559
560int32_t FnFxFireStart(C4AulContext *ctx, C4Object *pObj, int32_t iNumber, int32_t iTemp, int32_t iCausedBy, bool fBlasted, C4Object *pIncineratingObject)
561{
562 // safety
563 if (!pObj) return -1;
564 // temp readd
565 if (iTemp) { pObj->SetOnFire(true); return 1; }
566 // fail if already on fire
567 if (pObj->GetOnFire()) return -1;
568 // get associated effect
569 C4Effect *pEffect;
570 if (!(pEffect = pObj->pEffects)) return -1;
571 if (!(pEffect = pEffect->Get(iNumber, fIncludeDead: true))) return -1;
572 // structures must eject contents now, because DoCon is not guaranteed to be executed!
573 // In extinguishing material
574 bool fFireCaused = true;
575 int32_t iMat;
576 if (MatValid(mat: iMat = GBackMat(x: pObj->x, y: pObj->y)))
577 if (Game.Material.Map[iMat].Extinguisher)
578 {
579 // blasts should changedef in water, too!
580 if (fBlasted) if (pObj->Def->BurnTurnTo != C4ID_None) pObj->ChangeDef(idNew: pObj->Def->BurnTurnTo);
581 // no fire caused
582 fFireCaused = false;
583 }
584 // BurnTurnTo
585 if (fFireCaused) if (pObj->Def->BurnTurnTo != C4ID_None) pObj->ChangeDef(idNew: pObj->Def->BurnTurnTo);
586 // eject contents
587 C4Object *cobj;
588 if (!pObj->Def->IncompleteActivity && !pObj->Def->NoBurnDecay)
589 while (cobj = pObj->Contents.GetObject())
590 {
591 cobj->Controller = iCausedBy; // update controller, so incinerating a hut full of flints attributes the damage to the incinerator
592 if (pObj->Contained) cobj->Enter(pTarget: pObj->Contained);
593 else cobj->Exit(iX: cobj->x, iY: cobj->y);
594 }
595 // Detach attached objects
596 cobj = nullptr;
597 if (!pObj->Def->IncompleteActivity && !pObj->Def->NoBurnDecay)
598 while (cobj = Game.FindObject(id: 0, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: nullptr, pActionTarget: pObj, pExclude: nullptr, pContainer: nullptr, iOwner: ANY_OWNER, pFindNext: cobj))
599 if ((cobj->Action.Act > ActIdle) && (cobj->Def->ActMap[cobj->Action.Act].Procedure == DFA_ATTACH))
600 cobj->SetAction(iAct: ActIdle);
601 // fire caused?
602 if (!fFireCaused)
603 {
604 // if object was blasted but not incinerated (i.e., inside extinguisher)
605 // do a script callback
606 if (fBlasted) pObj->Call(PSF_IncinerationEx, pPars: {C4VInt(iVal: iCausedBy)});
607 return -1;
608 }
609 // determine fire appearance
610 int32_t iFireMode;
611 if (!(iFireMode = pObj->Call(PSF_FireMode).getInt()))
612 {
613 // set default fire modes
614 uint32_t dwCat = pObj->Category;
615 if (dwCat & (C4D_Living | C4D_StaticBack)) // Tiere, Bäume
616 iFireMode = C4Fx_FireMode_LivingVeg;
617 else if (dwCat & (C4D_Structure | C4D_Vehicle)) // Gebäude und Fahrzeuge sind unten meist kantig
618 iFireMode = C4Fx_FireMode_StructVeh;
619 else
620 iFireMode = C4Fx_FireMode_Object;
621 }
622 else if (!Inside<int32_t>(ival: iFireMode, lbound: 1, C4Fx_FireMode_Last))
623 {
624 DebugLog(level: spdlog::level::warn, fmt: "FireMode {} of object {} ({}) is invalid!", args&: iFireMode, args: pObj->GetName(), args: pObj->Def->GetName());
625 iFireMode = C4Fx_FireMode_Object;
626 }
627 // store causes in effect vars
628 FxFireVarMode(pEffect).SetInt(iFireMode);
629 FxFireVarCausedBy(pEffect).SetInt(iCausedBy); // used in C4Object::GetFireCause and timer!
630 FxFireVarBlasted(pEffect).SetBool(fBlasted);
631 FxFireVarIncineratingObj(pEffect).SetObject(pIncineratingObject);
632 // Set values
633 pObj->SetOnFire(true);
634 pObj->FirePhase = Random(MaxFirePhase);
635 if (pObj->Shape.Wdt * pObj->Shape.Hgt > 500) StartSoundEffect(name: "Inflame", loop: false, volume: 100, obj: pObj);
636 if (pObj->Def->Mass >= 100) StartSoundEffect(name: "Fire", loop: true, volume: 100, obj: pObj);
637 // Engine script call
638 pObj->Call(PSF_Incineration, pPars: {C4VInt(iVal: iCausedBy)});
639 // Done, success
640 return C4Fx_OK;
641}
642
643int32_t FnFxFireTimer(C4AulContext *ctx, C4Object *pObj, int32_t iNumber, int32_t iTime)
644{
645 // safety
646 if (!pObj) return C4Fx_Execute_Kill;
647
648 // get cause
649 int32_t iCausedByPlr = NO_OWNER; C4Effect *pEffect;
650 if (pEffect = pObj->pEffects)
651 if (pEffect = pEffect->Get(iNumber, fIncludeDead: true))
652 {
653 iCausedByPlr = FxFireVarCausedBy(pEffect).getInt();
654 if (!ValidPlr(plr: iCausedByPlr)) iCausedByPlr = NO_OWNER;
655 }
656
657 // causes on object
658 pObj->ExecFire(iIndex: iNumber, iCausedByPlr);
659
660 // special effects only if loaded
661 if (!Game.Particles.IsFireParticleLoaded()) return C4Fx_OK;
662
663 // get effect: May be nullptr after object fire execution, in which case the fire has been extinguished
664 if (!pObj->GetOnFire()) return C4Fx_Execute_Kill;
665 if (!(pEffect = pObj->pEffects)) return C4Fx_Execute_Kill;
666 if (!(pEffect = pEffect->Get(iNumber, fIncludeDead: true))) return C4Fx_Execute_Kill;
667
668 /* Fire execution behaviour transferred from script (FIRE) */
669
670 // get fire mode
671 int32_t iFireMode = FxFireVarMode(pEffect).getInt();
672
673 // special effects only each four frames, except for objects (e.g.: Projectiles)
674 if (iTime % 4 && iFireMode != C4Fx_FireMode_Object) return C4Fx_OK;
675
676 // no gfx for contained
677 if (pObj->Contained) return C4Fx_OK;
678
679 // some constant effect parameters for this object
680 int32_t iWidth = std::max<int32_t>(a: pObj->Def->Shape.Wdt, b: 1),
681 iHeight = pObj->Def->Shape.Hgt,
682 iYOff = iHeight / 2 - pObj->Def->Shape.FireTop;
683
684 int32_t iCount = int32_t(sqrt(x: double(iWidth * iHeight)) / 4); // Number of particles per execution
685 const int32_t iBaseParticleSize = 30; // With of particles in pixels/10, w/o add of values below
686 const int32_t iParticleSizeDiff = 10; // Size variation among particles
687 const int32_t iRelParticleSize = 12; // Influence of object size on particle size
688
689 // some varying effect parameters
690 int32_t iX = pObj->x, iY = pObj->y;
691 int32_t iXDir, iYDir, iCon, iWdtCon, iA, iSize;
692
693 // get remainign size (%)
694 iCon = iWdtCon = std::max<int32_t>(a: (100 * pObj->GetCon()) / FullCon, b: 1);
695 if (!pObj->Def->GrowthType)
696 // fixed width for not-stretched-objects
697 if (iWdtCon < 100) iWdtCon = 100;
698
699 // regard non-center object offsets
700 iX += pObj->Shape.x + pObj->Shape.Wdt / 2;
701 iY += pObj->Shape.y + pObj->Shape.Hgt / 2;
702
703 // apply rotation
704 float fRot[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
705 if (pObj->r && pObj->Def->Rotateable)
706 {
707 fRot[0] = cosf(x: static_cast<float>(pObj->r * std::numbers::pi_v<float> / 180.0));
708 fRot[1] = -sinf(x: static_cast<float>(pObj->r * std::numbers::pi_v<float> / 180.0));
709 fRot[2] = -fRot[1];
710 fRot[3] = fRot[0];
711 // rotated objects usually better burn from the center
712 if (iYOff > 0) iYOff = 0;
713 }
714
715 // Adjust particle number by con
716 iCount = (std::max)(a: 2, b: iCount * iWdtCon / 100);
717
718 // calc base for particle size parameter
719 iA = static_cast<int32_t>(sqrt(x: sqrt(x: double(iWidth * iHeight)) * (iCon + 20) / 120) * iRelParticleSize);
720
721 // create a double set of particles; first quarter normal (Fire); remaining three quarters additive (Fire2)
722 for (int32_t i = 0; i < iCount * 2; ++i)
723 {
724 // calc actual size to be used in this frame
725 // Using Random instead of SafeRandom would be safe here
726 // However, since it's just affecting particles there's no need to use synchronized random values
727 iSize = SafeRandom(range: iParticleSizeDiff + 1) + iBaseParticleSize - iParticleSizeDiff / 2 - 1 + iA;
728
729 // get particle target list
730 C4ParticleList *pParticleList = SafeRandom(range: 4) ? &(pObj->BackParticles) : &(pObj->FrontParticles);
731
732 // get particle def and color
733 C4ParticleDef *pPartDef; uint32_t dwClr;
734 if (i < iCount / 2)
735 {
736 dwClr = 0x32004000 + ((SafeRandom(range: 59) + 196) << 16);
737 pPartDef = Game.Particles.pFire1;
738 }
739 else
740 {
741 dwClr = 0xffffff;
742 pPartDef = Game.Particles.pFire2;
743 }
744 if (iFireMode == C4Fx_FireMode_Object) dwClr += 0x62000000;
745
746 // get particle creation pos...
747 int32_t iRandX = SafeRandom(range: iWidth + 1) - iWidth / 2 - 1;
748
749 int32_t iPx = iRandX * iWdtCon / 100;
750 int32_t iPy = iYOff * iCon / 100;
751 if (iFireMode == C4Fx_FireMode_LivingVeg) iPy -= iPx * iPx * 100 / iWidth / iWdtCon; // parable form particle pos on livings
752
753 // ...and movement speed
754 if (iFireMode != C4Fx_FireMode_Object)
755 {
756 // ...for normal fire proc
757 iXDir = iRandX * iCon / 400 - (iPx / 3) - int32_t(fixtof(x: pObj->xdir) * 3);
758 iYDir = -SafeRandom(range: 15 + iHeight * iCon / 300) - 1 - int32_t(fixtof(x: pObj->ydir) * 3);
759 }
760 else
761 {
762 // ...for objects
763 iXDir = -int32_t(fixtof(x: pObj->xdir) * 3);
764 iYDir = -int32_t(fixtof(x: pObj->ydir) * 3);
765 if (!iYDir) iYDir = -SafeRandom(range: 13 + iHeight / 4) - 1;
766 }
767
768 // OK; create it!
769 Game.Particles.Create(pOfDef: pPartDef, x: float(iX) + fRot[0] * iPx + fRot[1] * iPy, y: float(iY) + fRot[2] * iPx + fRot[3] * iPy, xdir: iXDir / 10.0f, ydir: iYDir / 10.0f, a: iSize / 10.0f, b: dwClr, pPxList: pParticleList, pObj);
770 }
771
772 return C4Fx_OK;
773}
774
775int32_t FnFxFireStop(C4AulContext *ctx, C4Object *pObj, int32_t iNumber, int32_t iReason, bool fTemp)
776{
777 // safety
778 if (!pObj) return false;
779 // only if real removal is done
780 if (fTemp)
781 {
782 // but fake being not on fire, so higher-priority effects get the status right
783 pObj->SetOnFire(false);
784 return true;
785 }
786 // alter OnFire-flag
787 pObj->SetOnFire(false);
788 // stop sound
789 if (pObj->Def->Mass >= 100) StopSoundEffect(name: "Fire", obj: pObj);
790 // done, success
791 return true;
792}
793
794C4String *FnFxFireInfo(C4AulContext *ctx, C4Object *pObj, int32_t iNumber)
795{
796 return new C4String(LoadResStr(id: C4ResStrTableKey::IDS_OBJ_BURNS), &Game.ScriptEngine.Strings);
797}
798
799// Some other, internal effects
800
801void Splash(int32_t tx, int32_t ty, int32_t amt, C4Object *pByObj)
802{
803 // Splash only if there is free space above
804 if (GBackSemiSolid(x: tx, y: ty - 15)) return;
805 // get back mat
806 int32_t iMat = GBackMat(x: tx, y: ty);
807 // check liquid
808 if (MatValid(mat: iMat))
809 if (DensityLiquid(dens: Game.Material.Map[iMat].Density) && Game.Material.Map[iMat].Instable)
810 {
811 int32_t sy = ty;
812 while (GBackLiquid(x: tx, y: sy) && sy > ty - 20 && sy >= 0) sy--;
813 // Splash bubbles and liquid
814 for (int32_t cnt = 0; cnt < amt; cnt++)
815 {
816 // force argument evaluation order
817 const auto r2 = Random(iRange: 16);
818 const auto r1 = Random(iRange: 16);
819 BubbleOut(tx: tx + r1 - 8, ty: ty + r2 - 6);
820 if (GBackLiquid(x: tx, y: ty) && !GBackSemiSolid(x: tx, y: sy))
821 {
822 // force argument evaluation order
823 const auto r2 = FIXED100(x: -Random(iRange: 200));
824 const auto r1 = FIXED100(x: Random(iRange: 151) - 75);
825 Game.PXS.Create(mat: Game.Landscape.ExtractMaterial(fx: tx, fy: ty),
826 ix: itofix(x: tx), iy: itofix(x: sy),
827 ixdir: r1,
828 iydir: r2);
829 }
830 }
831 }
832 // Splash sound
833 if (amt >= 20)
834 StartSoundEffect(name: "Splash2", loop: false, volume: 100, obj: pByObj);
835 else if (amt > 1) StartSoundEffect(name: "Splash1", loop: false, volume: 100, obj: pByObj);
836}
837
838int32_t GetSmokeLevel()
839{
840 // Network active: enforce fixed smoke level
841 if (Game.Control.SyncMode())
842 return 150;
843 // User-defined smoke level
844 return Config.Graphics.SmokeLevel;
845}
846
847void BubbleOut(int32_t tx, int32_t ty)
848{
849 // No bubbles from nowhere
850 if (!GBackSemiSolid(x: tx, y: ty)) return;
851 // User-defined smoke level
852 int32_t SmokeLevel = GetSmokeLevel();
853 // Enough bubbles out there already
854 if (Game.Objects.ObjectCount(id: C4Id(str: "FXU1")) >= SmokeLevel) return;
855 // Create bubble
856 Game.CreateObject(type: C4Id(str: "FXU1"), pCreator: nullptr, owner: NO_OWNER, x: tx, y: ty);
857}
858
859void Smoke(int32_t tx, int32_t ty, int32_t level, uint32_t dwClr)
860{
861 if (Game.Particles.pSmoke)
862 {
863 Game.Particles.Create(pOfDef: Game.Particles.pSmoke, x: float(tx), y: float(ty) - level / 2, xdir: 0.0f, ydir: 0.0f, a: float(level), b: dwClr);
864 return;
865 }
866 // User-defined smoke level
867 int32_t SmokeLevel = GetSmokeLevel();
868 // Enough smoke out there already
869 if (Game.Objects.ObjectCount(id: C4Id(str: "FXS1")) >= SmokeLevel) return;
870 // Create smoke
871 level = BoundBy<int32_t>(bval: level, lbound: 3, rbound: 32);
872 C4Object *pObj;
873 if (pObj = Game.CreateObjectConstruction(type: C4Id(str: "FXS1"), pCreator: nullptr, owner: NO_OWNER, ctx: tx, bty: ty, con: FullCon * level / 32))
874 pObj->Call(PSF_Activate);
875}
876
877void Explosion(int32_t tx, int32_t ty, int32_t level, C4Object *inobj, int32_t iCausedBy, C4Object *pByObj, C4ID idEffect, const char *szEffect)
878{
879 int32_t grade = BoundBy(bval: (level / 10) - 1, lbound: 1, rbound: 3);
880 // Sound
881 StartSoundEffect(name: std::format(fmt: "Blast{}", args: '0' + grade).c_str(), loop: false, volume: 100, obj: pByObj);
882 // Check blast containment
883 C4Object *container = inobj;
884 while (container && !container->Def->ContainBlast) container = container->Contained;
885 // Uncontained blast effects
886 if (!container)
887 {
888 // Incinerate landscape
889 if (!Game.Landscape.Incinerate(x: tx, y: ty))
890 if (!Game.Landscape.Incinerate(x: tx, y: ty - 10))
891 if (!Game.Landscape.Incinerate(x: tx - 5, y: ty - 5))
892 Game.Landscape.Incinerate(x: tx + 5, y: ty - 5);
893 // Create blast object or particle
894 C4Object *pBlast;
895 C4ParticleDef *pPrtDef = Game.Particles.pBlast;
896 // particle override
897 if (szEffect)
898 {
899 C4ParticleDef *pPrtDef2 = Game.Particles.GetDef(szName: szEffect);
900 if (pPrtDef2) pPrtDef = pPrtDef2;
901 }
902 else if (idEffect) pPrtDef = nullptr;
903 // create particle
904 if (pPrtDef)
905 {
906 Game.Particles.Create(pOfDef: pPrtDef, x: static_cast<float>(tx), y: static_cast<float>(ty), xdir: 0.0f, ydir: 0.0f, a: static_cast<float>(level), b: 0);
907 if (SEqual2(szStr1: pPrtDef->Name.getData(), szStr2: "Blast"))
908 Game.Particles.Cast(pOfDef: Game.Particles.pFSpark, iAmount: level / 5 + 1, x: static_cast<float>(tx), y: static_cast<float>(ty), level, a0: level / 2 + 1.0f, b0: 0x00ef0000, a1: level + 1.0f, b1: 0xffff1010);
909 }
910 else if (pBlast = Game.CreateObjectConstruction(type: idEffect ? idEffect : C4Id(str: "FXB1"), pCreator: pByObj, owner: iCausedBy, ctx: tx, bty: ty + level, con: FullCon * level / 20))
911 pBlast->Call(PSF_Activate);
912 }
913 // Blast objects
914 Game.BlastObjects(tx, ty, level, inobj, iCausedBy, pByObj);
915 if (container != inobj) Game.BlastObjects(tx, ty, level, inobj: container, iCausedBy, pByObj);
916 if (!container)
917 {
918 // Blast free landscape. After blasting objects so newly mined materials don't get flinged
919 Game.Landscape.BlastFree(tx, ty, rad: level, grade, iByPlayer: iCausedBy);
920 }
921}
922