1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2001, Sven2
6 * Copyright (c) 2017-2021, The LegacyClonk 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// game object lists
19
20#include <C4Include.h>
21#include <C4GameObjects.h>
22
23#include <C4Object.h>
24#include <C4ObjectCom.h>
25#include <C4Random.h>
26#include <C4SolidMask.h>
27#include <C4Network2Stats.h>
28#include <C4Game.h>
29#include <C4Wrappers.h>
30
31C4GameObjects::C4GameObjects()
32{
33 Default();
34}
35
36C4GameObjects::~C4GameObjects()
37{
38 Sectors.Clear();
39}
40
41void C4GameObjects::Default()
42{
43 ResortProc = nullptr;
44 Sectors.Clear();
45 LastUsedMarker = 0;
46}
47
48void C4GameObjects::Init(int32_t iWidth, int32_t iHeight)
49{
50 // init sectors
51 Sectors.Init(Wdt: iWidth, Hgt: iHeight);
52}
53
54bool C4GameObjects::Add(C4Object *nObj)
55{
56 // add inactive objects to the inactive list only
57 if (nObj->Status == C4OS_INACTIVE)
58 return InactiveObjects.Add(nObj, eSort: C4ObjectList::stMain);
59 // if this is a background object, add it to the list
60 if (nObj->Category & C4D_Background)
61 Game.BackObjects.Add(nObj, eSort: C4ObjectList::stMain);
62 // if this is a foreground object, add it to the list
63 if (nObj->Category & C4D_Foreground)
64 Game.ForeObjects.Add(nObj, eSort: C4ObjectList::stMain);
65 // manipulate main list
66 if (!C4ObjectList::Add(nObj, eSort: C4ObjectList::stMain))
67 return false;
68 // add to sectors
69 Sectors.Add(pObj: nObj, pMainList: this);
70 return true;
71}
72
73bool C4GameObjects::Remove(C4Object *pObj)
74{
75 // if it's an inactive object, simply remove from the inactiv elist
76 if (pObj->Status == C4OS_INACTIVE) return InactiveObjects.Remove(pObj);
77 // remove from sectors
78 Sectors.Remove(pObj);
79 // remove from backlist
80 Game.BackObjects.Remove(pObj);
81 // remove from forelist
82 Game.ForeObjects.Remove(pObj);
83 // manipulate main list
84 return C4ObjectList::Remove(pObj);
85}
86
87C4ObjectList &C4GameObjects::ObjectsAt(int ix, int iy)
88{
89 return Sectors.SectorAt(ix, iy)->ObjectShapes;
90}
91
92void C4GameObjects::CrossCheck() // Every Tick1 by ExecObjects
93{
94 C4Object *obj1, *obj2;
95 uint32_t ocf1, ocf2, focf, tocf;
96
97 // AtObject-Check: Checks for first match of obj1 at obj2
98
99 // Checks for this frame
100 focf = tocf = OCF_None;
101 // Medium level: Fight
102 if (!Tick5)
103 {
104 focf |= OCF_FightReady; tocf |= OCF_FightReady;
105 }
106 // Very low level: Incineration
107 if (!Tick35)
108 {
109 focf |= OCF_OnFire; tocf |= OCF_Inflammable;
110 }
111
112 if (focf && tocf)
113 for (C4ObjectList::iterator iter = begin(); iter != end() && (obj1 = *iter); ++iter)
114 if (obj1->Status && !obj1->Contained)
115 if (obj1->OCF & focf)
116 {
117 ocf1 = obj1->OCF; ocf2 = tocf;
118 if (obj2 = AtObject(ctx: obj1->x, cty: obj1->y, ocf&: ocf2, exclude: obj1))
119 {
120 // Incineration
121 if ((ocf1 & OCF_OnFire) && (ocf2 & OCF_Inflammable))
122 if (!Random(iRange: obj2->Def->ContactIncinerate))
123 {
124 obj2->Incinerate(iCausedBy: obj1->GetFireCausePlr(), fBlasted: false, pIncineratingObject: obj1); continue;
125 }
126 // Fight
127 if ((ocf1 & OCF_FightReady) && (ocf2 & OCF_FightReady))
128 if (Game.Players.Hostile(iPlayer1: obj1->Owner, iPlayer2: obj2->Owner))
129 {
130 // RejectFight callback
131 if (obj1->Call(PSF_RejectFight, pPars: {C4VObj(pObj: obj2)}).getBool()) continue;
132 if (obj2->Call(PSF_RejectFight, pPars: {C4VObj(pObj: obj1)}).getBool()) continue;
133 ObjectActionFight(cObj: obj1, pTarget: obj2);
134 ObjectActionFight(cObj: obj2, pTarget: obj1);
135 continue;
136 }
137 }
138 }
139
140 // Reverse area check: Checks for all obj2 at obj1
141
142 focf = tocf = OCF_None;
143 // High level: Collection, Hit
144 if (!Tick3)
145 {
146 focf |= OCF_Collection; tocf |= OCF_Carryable;
147 }
148 focf |= OCF_Alive; tocf |= OCF_HitSpeed2;
149
150 if (focf && tocf)
151 for (C4ObjectList::iterator iter = begin(); iter != end() && (obj1 = *iter); ++iter)
152 if (obj1->Status && !obj1->Contained && (obj1->OCF & focf))
153 {
154 uint32_t Marker = GetNextMarker();
155 C4LSector *pSct;
156 for (C4ObjectList *pLst = obj1->Area.FirstObjects(ppSct: &pSct); pLst; pLst = obj1->Area.NextObjects(pPrev: pLst, ppSct: &pSct))
157 for (C4ObjectList::iterator iter2 = pLst->begin(); iter2 != pLst->end() && (obj2 = *iter2); ++iter2)
158 if (obj2->Status && !obj2->Contained && (obj2 != obj1) && (obj2->OCF & tocf))
159 if (Inside<int32_t>(ival: obj2->x - (obj1->x + obj1->Shape.x), lbound: 0, rbound: obj1->Shape.Wdt - 1))
160 if (Inside<int32_t>(ival: obj2->y - (obj1->y + obj1->Shape.y), lbound: 0, rbound: obj1->Shape.Hgt - 1))
161 if (obj1->pLayer == obj2->pLayer)
162 {
163 // handle collision only once
164 if (obj2->Marker == Marker) continue;
165 obj2->Marker = Marker;
166 // Hit
167 if ((obj2->OCF & OCF_HitSpeed2) && (obj1->OCF & OCF_Alive) && (obj2->Category & C4D_Object))
168 if (!obj1->Call(PSF_QueryCatchBlow, pPars: {C4VObj(pObj: obj2)}))
169 {
170 // "realistic" hit energy
171 C4Fixed dXDir = obj2->xdir - obj1->xdir, dYDir = obj2->ydir - obj1->ydir;
172 int32_t iHitEnergy = fixtoi(x: (dXDir * dXDir + dYDir * dYDir) * obj2->Mass / 5);
173 iHitEnergy = std::max<int32_t>(a: iHitEnergy / 3, b: !!iHitEnergy); // hit energy reduced to 1/3rd, but do not drop to zero because of this division
174 obj1->DoEnergy(iChange: -iHitEnergy / 5, fExact: false, C4FxCall_EngObjHit, iCausedByPlr: obj2->Controller);
175 int tmass = std::max<int32_t>(a: obj1->Mass, b: 50);
176 if (!Tick3 || (obj1->Action.Act >= 0 && obj1->Def->ActMap[obj1->Action.Act].Procedure != DFA_FLIGHT))
177 obj1->Fling(txdir: obj2->xdir * 50 / tmass, tydir: -Abs(val: obj2->ydir / 2) * 50 / tmass, fAddSpeed: false, byPlayer: obj2->Controller);
178 obj1->Call(PSF_CatchBlow, pPars: {C4VInt(iVal: -iHitEnergy / 5),
179 C4VObj(pObj: obj2)});
180 // obj1 might have been tampered with
181 if (!obj1->Status || obj1->Contained || !(obj1->OCF & focf))
182 goto out1;
183 continue;
184 }
185 // Collection
186 if ((obj1->OCF & OCF_Collection) && (obj2->OCF & OCF_Carryable))
187 if (Inside<int32_t>(ival: obj2->x - (obj1->x + obj1->Def->Collection.x), lbound: 0, rbound: obj1->Def->Collection.Wdt - 1))
188 if (Inside<int32_t>(ival: obj2->y - (obj1->y + obj1->Def->Collection.y), lbound: 0, rbound: obj1->Def->Collection.Hgt - 1))
189 {
190 obj1->Collect(pObj: obj2);
191 // obj1 might have been tampered with
192 if (!obj1->Status || obj1->Contained || !(obj1->OCF & focf))
193 goto out1;
194 }
195 }
196 out1:;
197 }
198
199 // Contained-Check: Checks for matching Contained
200
201 // Checks for this frame
202 focf = tocf = OCF_None;
203 // Low level: Fight
204 if (!Tick10)
205 {
206 focf |= OCF_FightReady; tocf |= OCF_FightReady;
207 }
208
209 if (focf && tocf)
210 for (C4ObjectList::iterator iter = begin(); iter != end() && (obj1 = *iter); ++iter)
211 if (obj1->Status && obj1->Contained && (obj1->OCF & focf))
212 {
213 for (C4ObjectList::iterator iter2 = obj1->Contained->Contents.begin(); iter2 != end() && (obj2 = *iter2); ++iter2)
214 if (obj2->Status && obj2->Contained && (obj2 != obj1) && (obj2->OCF & tocf))
215 if (obj1->pLayer == obj2->pLayer)
216 {
217 ocf1 = obj1->OCF; ocf2 = obj2->OCF;
218 // Fight
219 if ((ocf1 & OCF_FightReady) && (ocf2 & OCF_FightReady))
220 if (Game.Players.Hostile(iPlayer1: obj1->Owner, iPlayer2: obj2->Owner))
221 {
222 ObjectActionFight(cObj: obj1, pTarget: obj2);
223 ObjectActionFight(cObj: obj2, pTarget: obj1);
224 // obj1 might have been tampered with
225 if (!obj1->Status || obj1->Contained || !(obj1->OCF & focf))
226 goto out2;
227 continue;
228 }
229 }
230 out2:;
231 }
232}
233
234C4Object *C4GameObjects::AtObject(int ctx, int cty, uint32_t &ocf, C4Object *exclude)
235{
236 uint32_t cocf;
237 C4Object *cObj; C4ObjectLink *clnk;
238
239 for (clnk = ObjectsAt(ix: ctx, iy: cty).First; clnk && (cObj = clnk->Obj); clnk = clnk->Next)
240 if (!exclude || (cObj != exclude && exclude->pLayer == cObj->pLayer)) if (cObj->Status)
241 {
242 cocf = ocf | OCF_Exclusive;
243 if (cObj->At(ctx, cty, ocf&: cocf))
244 {
245 // Search match
246 if (cocf & ocf) { ocf = cocf; return cObj; }
247 // EXCLUSIVE block
248 else return nullptr;
249 }
250 }
251 return nullptr;
252}
253
254void C4GameObjects::Synchronize()
255{
256 // synchronize unsorted objects
257 ResortUnsorted();
258 ExecuteResorts();
259 // synchronize solidmasks
260 RemoveSolidMasks();
261 PutSolidMasks();
262}
263
264C4Object *C4GameObjects::FindInternal(C4ID id)
265{
266 // search list of system objects (searches global list)
267 return ObjectsInt().Find(id);
268}
269
270C4Object *C4GameObjects::ObjectPointer(int32_t iNumber)
271{
272 // search own list
273 C4Object *pObj = C4ObjectList::ObjectPointer(iNumber);
274 if (pObj) return pObj;
275 // search deactivated
276 return InactiveObjects.ObjectPointer(iNumber);
277}
278
279std::int32_t C4GameObjects::ObjectNumber(C4Object *pObj)
280{
281 // search own list
282 if (const std::int32_t number{C4ObjectList::ObjectNumber(pObj)}; number)
283 {
284 return number;
285 }
286
287 // search deactivated
288 return InactiveObjects.ObjectNumber(pObj);
289}
290
291C4ObjectList &C4GameObjects::ObjectsInt()
292{
293 return *this;
294}
295
296void C4GameObjects::RemoveSolidMasks()
297{
298 C4ObjectLink *cLnk;
299 for (cLnk = First; cLnk; cLnk = cLnk->Next)
300 if (cLnk->Obj->Status)
301 if (cLnk->Obj->pSolidMaskData)
302 cLnk->Obj->pSolidMaskData->Remove(fCauseInstability: false, fBackupAttachment: false);
303}
304
305void C4GameObjects::PutSolidMasks()
306{
307 C4ObjectLink *cLnk;
308 for (cLnk = First; cLnk; cLnk = cLnk->Next)
309 if (cLnk->Obj->Status)
310 cLnk->Obj->UpdateSolidMask(fRestoreAttachedObjects: false);
311}
312
313void C4GameObjects::DeleteObjects()
314{
315 // delete links and objects
316 while (First)
317 {
318 C4Object *pObj = First->Obj;
319 Remove(pObj);
320 delete pObj;
321 }
322 // reset mass
323 Mass = 0;
324}
325
326void C4GameObjects::Clear(bool fClearInactive)
327{
328 DeleteObjects();
329 if (fClearInactive)
330 InactiveObjects.Clear();
331 ResortProc = nullptr;
332 LastUsedMarker = 0;
333}
334
335/* C4ObjResort */
336
337C4ObjResort::C4ObjResort()
338{
339 Category = 0;
340 OrderFunc = nullptr;
341 Next = nullptr;
342 pSortObj = pObjBefore = nullptr;
343 fSortAfter = false;
344}
345
346C4ObjResort::~C4ObjResort() {}
347
348void C4ObjResort::Execute()
349{
350 // no order func: resort given objects
351 if (!OrderFunc)
352 {
353 // no objects given?
354 if (!pSortObj || !pObjBefore) return;
355 // object to be resorted died or changed category
356 if (pSortObj->Status != C4OS_NORMAL || pSortObj->Unsorted) return;
357 // exchange
358 if (fSortAfter)
359 Game.Objects.OrderObjectAfter(pObj1: pSortObj, pObj2: pObjBefore);
360 else
361 Game.Objects.OrderObjectBefore(pObj1: pSortObj, pObj2: pObjBefore);
362 // done
363 return;
364 }
365 else if (pSortObj)
366 {
367 // sort single object
368 SortObject();
369 return;
370 }
371 // get first link to start sorting
372 C4ObjectLink *pLnk = Game.Objects.Last; if (!pLnk) return;
373 // sort all categories given; one by one (sort by category is ensured by C4ObjectList::Add)
374 for (int iCat = 1; iCat < C4D_SortLimit; iCat <<= 1)
375 if (iCat & Category)
376 {
377 // get first link of this category
378 while (!(pLnk->Obj->Status && (pLnk->Obj->Category & iCat)))
379 if (!(pLnk = pLnk->Prev))
380 // no more objects to sort: done
381 break;
382 // first link found?
383 if (pLnk)
384 {
385 // get last link of this category
386 C4ObjectLink *pNextLnk = pLnk;
387 while (!pLnk->Obj->Status || (pNextLnk->Obj->Category & iCat))
388 if (!(pNextLnk = pNextLnk->Prev))
389 // no more objects: end of list reached
390 break;
391 // get previous link, which is the last in the list of this category
392 C4ObjectLink *pLastLnk;
393 if (pNextLnk) pLastLnk = pNextLnk->Next; else pLastLnk = Game.Objects.First;
394 // get next valid (there must be at least one: pLnk; so this loop should be safe)
395 while (!pLastLnk->Obj->Status) pLastLnk = pLastLnk->Next;
396 // now sort this portion of the list
397 Sort(pFirst: pLastLnk, pLast: pLnk);
398 // start searching at end of this list next time
399 // if the end has already been reached: stop here
400 if (!(pLnk = pNextLnk)) return;
401 }
402 // continue with next category
403 }
404}
405
406void C4ObjResort::SortObject()
407{
408 // safety
409 if (pSortObj->Status != C4OS_NORMAL || pSortObj->Unsorted) return;
410 // pre-build parameters
411 C4AulParSet Pars;
412 Pars[1].Set(C4VObj(pObj: pSortObj));
413 // first, check forward in list
414 C4ObjectLink *pMoveLink = nullptr;
415 C4ObjectLink *pLnk = Game.Objects.GetLink(pObj: pSortObj);
416 C4ObjectLink *pLnkBck = pLnk;
417 C4Object *pObj2; int iResult;
418 if (!pLnk) return;
419 while (pLnk = pLnk->Next)
420 {
421 // get object
422 pObj2 = pLnk->Obj;
423 if (!pObj2->Status) continue;
424 // does the category still match?
425 if (!(pObj2->Category & pSortObj->Category)) break;
426 // perform the check
427 Pars[0].Set(C4VObj(pObj: pObj2));
428 iResult = OrderFunc->Exec(pObj: nullptr, pPars: Pars).getInt();
429 if (iResult > 0) break;
430 if (iResult < 0) pMoveLink = pLnk;
431 }
432 // check if movement has to be done
433 if (pMoveLink)
434 {
435 // move link directly after pMoveLink
436 // FIXME: Inform C4ObjectList that this is a reorder, not a remove+insert
437 // move out of current position
438 Game.Objects.RemoveLink(pLnk: pLnkBck);
439 // put into new position
440 Game.Objects.InsertLink(pLink: pLnkBck, pAfter: pMoveLink);
441 }
442 else
443 {
444 // no movement yet: check backwards in list
445 Pars[0].Set(C4VObj(pObj: pSortObj));
446 pLnk = pLnkBck;
447 while (pLnk = pLnk->Prev)
448 {
449 // get object
450 pObj2 = pLnk->Obj;
451 if (!pObj2->Status) continue;
452 // does the category still match?
453 if (!(pObj2->Category & pSortObj->Category)) break;
454 // perform the check
455 Pars[1].Set(C4VObj(pObj: pObj2));
456 iResult = OrderFunc->Exec(pObj: nullptr, pPars: Pars).getInt();
457 if (iResult > 0) break;
458 if (iResult < 0) pMoveLink = pLnk;
459 }
460 // no movement to be done? finish
461 if (!pMoveLink) return;
462 // move link directly before pMoveLink
463 // move out of current position
464 Game.Objects.RemoveLink(pLnk: pLnkBck);
465 // put into new position
466 Game.Objects.InsertLinkBefore(pLink: pLnkBck, pBefore: pMoveLink);
467 }
468 // object has been resorted: resort into area lists, too
469 Game.Objects.UpdatePosResort(pObj: pSortObj);
470 // done
471}
472
473void C4ObjResort::Sort(C4ObjectLink *pFirst, C4ObjectLink *pLast)
474{
475#ifndef NDEBUG
476 assert(Game.Objects.Sectors.CheckSort());
477#endif
478 // do a simple insertion-like sort
479 C4ObjectLink *pCurr; // current link to analyse
480 C4ObjectLink *pCurr2; // second (previous) link to analyse
481 C4ObjectLink *pNewFirst; // next link to be first
482
483 C4ObjectLink *pFirstBck = pFirst; // backup of first link
484
485 // pre-build parameters
486 C4AulParSet Pars;
487
488 // loop until there's nothing left to sort
489 while (pFirst != pLast)
490 {
491 // start from the very end of the list
492 pCurr = pNewFirst = pLast;
493 // loop the checks up to the first list item to check
494 while (pCurr != pFirst)
495 {
496 // get second check item
497 pCurr2 = pCurr->Prev;
498 while (!pCurr2->Obj->Status) pCurr2 = pCurr2->Prev;
499 // perform the check
500 Pars[0].Set(C4VObj(pObj: pCurr->Obj)); Pars[1].Set(C4VObj(pObj: pCurr2->Obj));
501 if (OrderFunc->Exec(pObj: nullptr, pPars: Pars).getInt() < 0)
502 {
503 // so there's something to be reordered: swap the links
504 // FIXME: Inform C4ObjectList about this reorder
505 C4Object *pObj = pCurr->Obj; pCurr->Obj = pCurr2->Obj; pCurr2->Obj = pObj;
506 // and readd to sector lists
507 pCurr->Obj->Unsorted = pCurr2->Obj->Unsorted = true;
508 // grow list section to scan next
509 pNewFirst = pCurr;
510 }
511 // advance in list
512 pCurr = pCurr2;
513 }
514 // reduce area to be checked
515 pFirst = pNewFirst;
516 }
517#ifndef NDEBUG
518 assert(Game.Objects.Sectors.CheckSort());
519#endif
520 // resort objects in sector lists
521 for (pCurr = pFirstBck; pCurr != pLast->Next; pCurr = pCurr->Next)
522 {
523 C4Object *pObj = pCurr->Obj;
524 if (pObj->Status && pObj->Unsorted)
525 {
526 pObj->Unsorted = false;
527 Game.Objects.UpdatePosResort(pObj);
528 }
529 }
530#ifndef NDEBUG
531 assert(Game.Objects.Sectors.CheckSort());
532#endif
533}
534
535int C4GameObjects::Load(C4Group &hGroup, bool fKeepInactive)
536{
537 // Load data component
538 StdStrBuf Source;
539 if (!hGroup.LoadEntryString(C4CFN_ScenarioObjects, Buf&: Source))
540 return 0;
541
542 // Compile
543 StdStrBuf Name = hGroup.GetFullName() + DirSep C4CFN_ScenarioObjects;
544 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(
545 TargetStruct: mkParAdapt(rObj&: *this, rPar: false),
546 SrcBuf: Source,
547 szName: Name.getData()))
548 return 0;
549
550 // Process objects
551 C4ObjectLink *cLnk;
552 C4Object *pObj;
553 bool fObjectNumberCollision = false;
554 int32_t iMaxObjectNumber = 0;
555 for (cLnk = Last; cLnk; cLnk = cLnk->Prev)
556 {
557 C4Object *pObj = cLnk->Obj;
558 // check object number collision with inactive list
559 if (fKeepInactive)
560 {
561 for (C4ObjectLink *clnk = InactiveObjects.First; clnk; clnk = clnk->Next)
562 if (clnk->Obj->Number == pObj->Number) fObjectNumberCollision = true;
563 }
564 // keep track of numbers
565 iMaxObjectNumber = std::max<long>(a: iMaxObjectNumber, b: pObj->Number);
566 // add to list of backobjects
567 if (pObj->Category & C4D_Background)
568 Game.BackObjects.Add(nObj: pObj, eSort: C4ObjectList::stMain, pLstSorted: this);
569 // add to list of foreobjects
570 if (pObj->Category & C4D_Foreground)
571 Game.ForeObjects.Add(nObj: pObj, eSort: C4ObjectList::stMain, pLstSorted: this);
572 // Unterminate end
573 }
574
575 // Denumerate pointers
576 // if object numbers collideded, numbers will be adjusted afterwards
577 // so fake inactive object list empty meanwhile
578 C4ObjectLink *pInFirst;
579 if (fObjectNumberCollision) { pInFirst = InactiveObjects.First; InactiveObjects.First = nullptr; }
580 // denumerate pointers
581 Denumerate();
582 // update object enumeration index now, because calls like UpdateTransferZone might create objects
583 Game.ObjectEnumerationIndex = (std::max)(a: Game.ObjectEnumerationIndex, b: iMaxObjectNumber);
584 // end faking and adjust object numbers
585 if (fObjectNumberCollision)
586 {
587 InactiveObjects.First = pInFirst;
588 // simply renumber all inactive objects
589 for (cLnk = InactiveObjects.First; cLnk; cLnk = cLnk->Next)
590 if ((pObj = cLnk->Obj)->Status)
591 pObj->Number = ++Game.ObjectEnumerationIndex;
592 }
593
594 // special checks:
595 // -contained/contents-consistency
596 // -StaticBack-objects zero speed
597 for (cLnk = First; cLnk; cLnk = cLnk->Next)
598 if ((pObj = cLnk->Obj)->Status)
599 {
600 // staticback must not have speed
601 if (pObj->Category & C4D_StaticBack)
602 {
603 pObj->xdir = pObj->ydir = 0;
604 }
605 // contained must be in contents list
606 if (pObj->Contained)
607 if (!pObj->Contained->Contents.GetLink(pObj))
608 {
609 DebugLog(level: spdlog::level::err, fmt: "Error in Objects.txt: Container of #{} is #{}, but not found in contents list!", args&: pObj->Number, args&: pObj->Contained->Number);
610 pObj->Contained->Contents.Add(nObj: pObj, eSort: C4ObjectList::stContents);
611 }
612 // all contents must have contained set; otherwise, remove them!
613 C4Object *pObj2;
614 for (C4ObjectLink *cLnkCont = pObj->Contents.First; cLnkCont; cLnkCont = cLnkCont->Next)
615 {
616 // check double links
617 if (pObj->Contents.GetLink(pObj: cLnkCont->Obj) != cLnkCont)
618 {
619 DebugLog(level: spdlog::level::err, fmt: "Error in Objects.txt: Double containment of #{} by #{}!", args&: cLnkCont->Obj->Number, args&: pObj->Number);
620 // this remove-call will only remove the previous (dobuled) link, so cLnkCont should be save
621 pObj->Contents.Remove(pObj: cLnkCont->Obj);
622 // contents checked already
623 continue;
624 }
625 // check contents/contained-relation
626 if ((pObj2 = cLnkCont->Obj)->Status)
627 if (pObj2->Contained != pObj)
628 {
629 DebugLog(level: spdlog::level::err, fmt: "Error in Objects.txt: Object #{} not in container #{} as referenced!", args&: pObj2->Number, args&: pObj->Number);
630 pObj2->Contained = pObj;
631 }
632 }
633 }
634 // sort out inactive objects
635 C4ObjectLink *cLnkNext;
636 for (cLnk = First; cLnk; cLnk = cLnkNext)
637 {
638 cLnkNext = cLnk->Next;
639 if (cLnk->Obj->Status == C4OS_INACTIVE)
640 {
641 if (cLnk->Prev) cLnk->Prev->Next = cLnkNext; else First = cLnkNext;
642 if (cLnkNext) cLnkNext->Prev = cLnk->Prev; else Last = cLnk->Prev;
643 if (cLnk->Prev = InactiveObjects.Last)
644 InactiveObjects.Last->Next = cLnk;
645 else
646 InactiveObjects.First = cLnk;
647 InactiveObjects.Last = cLnk; cLnk->Next = nullptr;
648 Mass -= pObj->Mass;
649 }
650 }
651
652 {
653 C4DebugRecOff DBGRECOFF; // - script callbacks that would kill DebugRec-sync for runtime start
654 // update graphics
655 UpdateGraphics(fGraphicsChanged: false);
656 // Update faces
657 UpdateFaces(bUpdateShape: false);
658 // Update ocf
659 SetOCF();
660 }
661
662 // make sure list is sorted by category - after sorting out inactives, because inactives aren't sorted into the main list
663 FixObjectOrder();
664
665 // misc updates
666 for (cLnk = First; cLnk; cLnk = cLnk->Next)
667 if ((pObj = cLnk->Obj)->Status)
668 {
669 // add to plrview
670 pObj->PlrFoWActualize();
671 // update flipdir (for old objects.txt with no flipdir defined)
672 // assigns Action.DrawDir as well
673 pObj->UpdateFlipDir();
674 }
675 // Done
676 return ObjectCount();
677}
678
679bool C4GameObjects::Save(C4Group &hGroup, bool fSaveGame, bool fSaveInactive)
680{
681 // Save to temp file
682 char szFilename[_MAX_PATH + 1]; SCopy(szSource: Config.AtTempPath(C4CFN_ScenarioObjects), sTarget: szFilename);
683 if (!Save(szFilename, fSaveGame, fSaveInactive)) return false;
684
685 // Move temp file to group
686 hGroup.Move(szFile: szFilename, szAddAs: nullptr); // check?
687 // Success
688 return true;
689}
690
691bool C4GameObjects::Save(const char *szFilename, bool fSaveGame, bool fSaveInactive)
692{
693 // Enumerate
694 Enumerate();
695 InactiveObjects.Enumerate();
696 Game.ScriptEngine.Strings.EnumStrings();
697
698 // Decompile objects to buffer
699 std::string buffer;
700 bool fSuccess = DecompileToBuf_Log<StdCompilerINIWrite>(TargetStruct: mkParAdapt(rObj&: *this, rPar1: false, rPar2: !fSaveGame), pOut: &buffer, szName: szFilename);
701
702 // Decompile inactives
703 if (fSaveInactive)
704 {
705 std::string inactiveBuffer;
706 fSuccess &= DecompileToBuf_Log<StdCompilerINIWrite>(TargetStruct: mkParAdapt(rObj&: InactiveObjects, rPar1: false, rPar2: !fSaveGame), pOut: &inactiveBuffer, szName: szFilename);
707 buffer += "\r\n";
708 buffer += std::move(inactiveBuffer);
709 }
710
711 // Denumerate
712 InactiveObjects.Denumerate();
713 Denumerate();
714
715 // Error?
716 if (!fSuccess)
717 return false;
718
719 // Write
720 return StdStrBuf{buffer.c_str(), buffer.size(), false}.SaveToFile(szFile: szFilename);
721}
722
723void C4GameObjects::UpdateScriptPointers()
724{
725 // call in sublists
726 C4ObjectList::UpdateScriptPointers();
727 InactiveObjects.UpdateScriptPointers();
728 // adjust global effects
729 if (Game.pGlobalEffects) Game.pGlobalEffects->ReAssignAllCallbackFunctions();
730}
731
732void C4GameObjects::UpdatePos(C4Object *pObj)
733{
734 // Position might have changed. Update sector lists
735 Sectors.Update(pObj, pMainList: this);
736}
737
738void C4GameObjects::UpdatePosResort(C4Object *pObj)
739{
740 // Object order for this object was changed. Readd object to sectors
741 Sectors.Remove(pObj);
742 Sectors.Add(pObj, pMainList: this);
743}
744
745bool C4GameObjects::OrderObjectBefore(C4Object *pObj1, C4Object *pObj2)
746{
747 // check that this won't screw the category sort
748 if ((pObj1->Category & C4D_SortLimit) < (pObj2->Category & C4D_SortLimit))
749 return false;
750 // reorder
751 if (!C4ObjectList::OrderObjectBefore(pObj1, pObj2))
752 return false;
753 // update area lists
754 UpdatePosResort(pObj: pObj1);
755 // done, success
756 return true;
757}
758
759bool C4GameObjects::OrderObjectAfter(C4Object *pObj1, C4Object *pObj2)
760{
761 // check that this won't screw the category sort
762 if ((pObj1->Category & C4D_SortLimit) > (pObj2->Category & C4D_SortLimit))
763 return false;
764 // reorder
765 if (!C4ObjectList::OrderObjectAfter(pObj1, pObj2))
766 return false;
767 // update area lists
768 UpdatePosResort(pObj: pObj1);
769 // done, success
770 return true;
771}
772
773void C4GameObjects::FixObjectOrder()
774{
775 // fixes the object order so it matches the global object order sorting constraints
776 C4ObjectLink *pLnk0 = First, *pLnkL = Last;
777 while (pLnk0 != pLnkL)
778 {
779 C4ObjectLink *pLnk1stUnsorted = nullptr, *pLnkLastUnsorted = nullptr, *pLnkPrev = nullptr, *pLnk;
780 C4Object *pLastWarnObj = nullptr;
781 // forward fix
782 uint32_t dwLastCategory = C4D_SortLimit;
783 for (pLnk = pLnk0; pLnk != pLnkL->Next; pLnk = pLnk->Next)
784 {
785 C4Object *pObj = pLnk->Obj;
786 if (pObj->Unsorted || !pObj->Status) continue;
787 uint32_t dwCategory = pObj->Category & C4D_SortLimit;
788 // must have exactly one SortOrder-bit set
789 if (!dwCategory)
790 {
791 DebugLog(level: spdlog::level::err, fmt: "Objects.txt: Object #{} is missing sorting category!", args: static_cast<int>(pObj->Number));
792 ++pObj->Category; dwCategory = 1;
793 }
794 else
795 {
796 uint32_t dwCat2 = dwCategory; int i = 0;
797 while (~dwCat2 & 1) { dwCat2 = dwCat2 >> 1; ++i; }
798 if (dwCat2 != 1)
799 {
800 DebugLog(level: spdlog::level::err, fmt: "Objects.txt: Object #{} has invalid sorting category {:x}!", args: static_cast<int>(pObj->Number), args: static_cast<unsigned int>(dwCategory));
801 dwCategory = (1 << i);
802 pObj->Category = (pObj->Category & ~C4D_SortLimit) | dwCategory;
803 }
804 }
805 // fix order
806 if (dwCategory > dwLastCategory)
807 {
808 // SORT ERROR! (note that pLnkPrev can't be 0)
809 if (pLnkPrev->Obj != pLastWarnObj)
810 {
811 DebugLog(level: spdlog::level::err, fmt: "Objects.txt: Wrong object order of #{}-#{}! (down)", args: static_cast<int>(pObj->Number), args: static_cast<int>(pLnkPrev->Obj->Number));
812 pLastWarnObj = pLnkPrev->Obj;
813 }
814 pLnk->Obj = pLnkPrev->Obj;
815 pLnkPrev->Obj = pObj;
816 pLnkLastUnsorted = pLnkPrev;
817 }
818 else
819 dwLastCategory = dwCategory;
820 pLnkPrev = pLnk;
821 }
822 if (!pLnkLastUnsorted) break; // done
823 pLnkL = pLnkLastUnsorted;
824 // backwards fix
825 dwLastCategory = 0;
826 for (pLnk = pLnkL; pLnk != pLnk0->Prev; pLnk = pLnk->Prev)
827 {
828 C4Object *pObj = pLnk->Obj;
829 if (pObj->Unsorted || !pObj->Status) continue;
830 uint32_t dwCategory = pObj->Category & C4D_SortLimit;
831 if (dwCategory < dwLastCategory)
832 {
833 // SORT ERROR! (note that pLnkPrev can't be 0)
834 if (pLnkPrev->Obj != pLastWarnObj)
835 {
836 DebugLog(level: spdlog::level::err, fmt: "Objects.txt: Wrong object order of #{}-#{}! (up)", args: static_cast<int>(pObj->Number), args: static_cast<int>(pLnkPrev->Obj->Number));
837 pLastWarnObj = pLnkPrev->Obj;
838 }
839 pLnk->Obj = pLnkPrev->Obj;
840 pLnkPrev->Obj = pObj;
841 pLnk1stUnsorted = pLnkPrev;
842 }
843 else
844 dwLastCategory = dwCategory;
845 pLnkPrev = pLnk;
846 }
847 if (!pLnk1stUnsorted) break; // done
848 pLnk0 = pLnk1stUnsorted;
849 }
850 // objects fixed!
851}
852
853void C4GameObjects::ResortUnsorted()
854{
855 C4ObjectLink *clnk = First; C4Object *cObj;
856 while (clnk && (cObj = clnk->Obj))
857 {
858 clnk = clnk->Next;
859 if (cObj->Unsorted)
860 {
861 // readd to main object list
862 Remove(pObj: cObj);
863 cObj->Unsorted = false;
864 if (!Add(nObj: cObj))
865 {
866 // readd failed: Better kill object to prevent leaking...
867 Game.ClearPointers(cobj: cObj);
868 delete cObj;
869 }
870 }
871 }
872}
873
874void C4GameObjects::ExecuteResorts()
875{
876 // custom object sort
877 C4ObjResort *pRes = ResortProc;
878 while (pRes)
879 {
880 C4ObjResort *pNextRes = pRes->Next;
881 pRes->Execute();
882 delete pRes;
883 pRes = pNextRes;
884 }
885 ResortProc = nullptr;
886}
887
888bool C4GameObjects::ValidateOwners()
889{
890 // validate in sublists
891 bool fSucc = true;
892 if (!C4ObjectList::ValidateOwners()) fSucc = false;
893 if (!InactiveObjects.ValidateOwners()) fSucc = false;
894 return fSucc;
895}
896
897bool C4GameObjects::AssignInfo()
898{
899 // assign in sublists
900 bool fSucc = true;
901 if (!C4ObjectList::AssignInfo()) fSucc = false;
902 if (!InactiveObjects.AssignInfo()) fSucc = false;
903 return fSucc;
904}
905
906uint32_t C4GameObjects::GetNextMarker()
907{
908 // Get a new marker.
909 uint32_t marker = ++LastUsedMarker;
910 // If all markers are exceeded, restart marker at 1 and reset all object markers to zero.
911 if (!marker)
912 {
913 C4Object *cobj; C4ObjectLink *clnk;
914 for (clnk = First; clnk && (cobj = clnk->Obj); clnk = clnk->Next)
915 cobj->Marker = 0;
916 marker = ++LastUsedMarker;
917 }
918 return marker;
919}
920