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/* That which fills the world with life */
18
19#include <C4Include.h>
20#include <C4Object.h>
21#include <C4Version.h>
22
23#include <C4ObjectInfo.h>
24#include <C4Physics.h>
25#include <C4ObjectCom.h>
26#include <C4Command.h>
27#include <C4Viewport.h>
28#ifdef DEBUGREC
29#include <C4Record.h>
30#endif
31#include <C4SolidMask.h>
32#include <C4Random.h>
33#include <C4Wrappers.h>
34#include <C4Player.h>
35#include <C4ObjectMenu.h>
36
37#include <cstring>
38#include <format>
39#include <limits>
40#include <utility>
41
42void DrawVertex(C4Facet &cgo, int32_t tx, int32_t ty, int32_t col, int32_t contact)
43{
44 if (Inside<int32_t>(ival: tx, lbound: 1, rbound: cgo.Wdt - 2) && Inside<int32_t>(ival: ty, lbound: 1, rbound: cgo.Hgt - 2))
45 {
46 Application.DDraw->DrawHorizontalLine(sfcDest: cgo.Surface, x1: cgo.X + tx - 1, x2: cgo.X + tx + 1, y: cgo.Y + ty, col);
47 Application.DDraw->DrawVerticalLine(sfcDest: cgo.Surface, x: cgo.X + tx, y1: cgo.Y + ty - 1, y2: cgo.Y + ty + 1, col);
48 if (contact) Application.DDraw->DrawFrame(sfcDest: cgo.Surface, x1: cgo.X + tx - 2, y1: cgo.Y + ty - 2, x2: cgo.X + tx + 2, y2: cgo.Y + ty + 2, col: CWhite);
49 }
50}
51
52void C4Action::SetBridgeData(int32_t iBridgeTime, bool fMoveClonk, bool fWall, int32_t iBridgeMaterial)
53{
54 // validity
55 iBridgeMaterial = std::min<int32_t>(a: iBridgeMaterial, b: Game.Material.Num - 1);
56 if (iBridgeMaterial < 0) iBridgeMaterial = 0xff;
57 iBridgeTime = BoundBy<int32_t>(bval: iBridgeTime, lbound: 0, rbound: 0xffff);
58 // mask in this->Data
59 Data = (uint32_t(iBridgeTime) << 16) + (uint32_t(fMoveClonk) << 8) + (uint32_t(fWall) << 9) + iBridgeMaterial;
60}
61
62void C4Action::GetBridgeData(int32_t &riBridgeTime, bool &rfMoveClonk, bool &rfWall, int32_t &riBridgeMaterial)
63{
64 // mask from this->Data
65 uint32_t uiData = Data;
66 riBridgeTime = uiData >> 16;
67 rfMoveClonk = !!(uiData & 0x100);
68 rfWall = !!(uiData & 0x200);
69 riBridgeMaterial = (uiData & 0xff);
70 if (riBridgeMaterial == 0xff) riBridgeMaterial = -1;
71}
72
73C4Object::C4Object()
74{
75 Default();
76}
77
78void C4Object::Default()
79{
80 id = C4ID_None;
81 Number = -1;
82 Status = 1;
83 nInfo.Clear();
84 RemovalDelay = 0;
85 Owner = NO_OWNER;
86 Controller = NO_OWNER;
87 LastEnergyLossCausePlayer = NO_OWNER;
88 Category = 0;
89 x = y = r = 0;
90 motion_x = motion_y = 0;
91 NoCollectDelay = 0;
92 Base = NO_OWNER;
93 Con = 0;
94 Mass = OwnMass = 0;
95 Damage = 0;
96 Energy = 0;
97 MagicEnergy = 0;
98 Alive = 0;
99 Breath = 0;
100 FirePhase = 0;
101 InMat = MNone;
102 Color = 0;
103 ViewEnergy = 0;
104 Local.Reset();
105 PlrViewRange = 0;
106 fix_x = fix_y = fix_r = 0;
107 xdir = ydir = rdir = 0;
108 Mobile = 0;
109 Select = 0;
110 Unsorted = false;
111 Initializing = false;
112 OnFire = 0;
113 InLiquid = 0;
114 EntranceStatus = 0;
115 Audible = -1;
116 NeedEnergy = 0;
117 Timer = 0;
118 t_contact = 0;
119 OCF = 0;
120 Action.Default();
121 Shape.Default();
122 fOwnVertices = 0;
123 Contents.Default();
124 Component.Clear();
125 SolidMask.Default();
126 PictureRect.Default();
127 Def = nullptr;
128 Info = nullptr;
129 Command = nullptr;
130 Contained = nullptr;
131 TopFace.Default();
132 Menu = nullptr;
133 PhysicalTemporary = false;
134 TemporaryPhysical.Default();
135 MaterialContents.fill(u: 0);
136 Visibility = VIS_All;
137 LocalNamed.Reset();
138 Marker = 0;
139 ColorMod = BlitMode = 0;
140 CrewDisabled = false;
141 pLayer = nullptr;
142 pSolidMaskData = nullptr;
143 pGraphics = nullptr;
144 pDrawTransform = nullptr;
145 pEffects = nullptr;
146 FirstRef = nullptr;
147 pGfxOverlay = nullptr;
148 iLastAttachMovementFrame = -1;
149}
150
151bool C4Object::Init(C4Def *pDef, C4Object *pCreator,
152 int32_t iOwner, C4ObjectInfo *pInfo,
153 int32_t nx, int32_t ny, int32_t nr,
154 C4Fixed nxdir, C4Fixed nydir, C4Fixed nrdir, int32_t iController)
155{
156 // currently initializing
157 Initializing = true;
158
159 // Def & basics
160 id = pDef->id;
161 Owner = iOwner;
162 if (iController > NO_OWNER) Controller = iController; else Controller = iOwner;
163 LastEnergyLossCausePlayer = NO_OWNER;
164 Info = pInfo;
165 Def = pDef;
166 Category = Def->Category;
167 Def->Count++;
168 if (pCreator) pLayer = pCreator->pLayer;
169
170 // graphics
171 pGraphics = &Def->Graphics;
172 BlitMode = Def->BlitMode;
173
174 // Position
175 if (!Def->Rotateable) { nr = 0; nrdir = 0; }
176 x = nx; y = ny; r = nr;
177 fix_x = itofix(x);
178 fix_y = itofix(x: y);
179 fix_r = itofix(x: r);
180 xdir = nxdir; ydir = nydir; rdir = nrdir;
181
182 // Initial mobility
183 if (Category != C4D_StaticBack)
184 if (!!xdir || !!ydir || !!rdir)
185 Mobile = 1;
186
187 // Mass
188 Mass = std::max<int32_t>(a: Def->Mass * Con / FullCon, b: 1);
189
190 // Life, energy, breath
191 if (Category & C4D_Living) Alive = 1;
192 if (Alive) Energy = GetPhysical()->Energy;
193 Breath = GetPhysical()->Breath;
194
195 // Components
196 Component = Def->Component;
197 ComponentConCutoff();
198
199 // Color
200 if (Def->ColorByOwner)
201 if (ValidPlr(plr: Owner))
202 Color = Game.Players.Get(iPlayer: Owner)->ColorDw;
203
204 // Shape & face
205 Shape = Def->Shape;
206 SolidMask = Def->SolidMask;
207 CheckSolidMaskRect();
208 UpdateGraphics(fGraphicsChanged: false);
209 UpdateFace(bUpdateShape: true);
210
211 // Initial audibility
212 Audible = -1;
213
214 // Initial OCF
215 SetOCF();
216
217 // local named vars
218 LocalNamed.SetNameList(&pDef->Script.LocalNamed);
219
220 // finished initializing
221 Initializing = false;
222
223 return true;
224}
225
226C4Object::~C4Object()
227{
228 Clear();
229
230#ifndef NDEBUG
231 // debug: mustn't be listed in any list now
232 assert(!Game.Objects.ObjectNumber(this));
233 assert(!Game.Objects.InactiveObjects.ObjectNumber(this));
234 Game.Objects.Sectors.AssertObjectNotInList(this);
235#endif
236}
237
238void C4Object::AssignRemoval(bool fExitContents)
239{
240 // check status
241 if (!Status) return;
242#ifdef DEBUGREC
243 C4RCCreateObj rc;
244 rc.id = Def->id;
245 rc.oei = Number;
246 rc.x = x; rc.y = y; rc.ownr = Owner;
247 AddDbgRec(RCT_DsObj, &rc, sizeof(rc));
248#endif
249 // Destruction call in container
250 if (Contained)
251 {
252 Contained->Call(PSF_ContentsDestruction, pPars: {C4VObj(pObj: this)});
253 if (!Status) return;
254 }
255 // Destruction call
256 Call(PSF_Destruction);
257 // Destruction-callback might have deleted the object already
258 if (!Status) return;
259 // remove all effects (extinguishes as well)
260 if (pEffects)
261 {
262 pEffects->ClearAll(pObj: this, C4FxCall_RemoveClear);
263 // Effect-callback might actually have deleted the object already
264 if (!Status) return;
265 }
266 // ...or just deleted the effects
267 delete pEffects;
268 pEffects = nullptr;
269 // remove particles
270 if (FrontParticles) FrontParticles.Clear();
271 if (BackParticles) BackParticles.Clear();
272 // Action idle
273 SetAction(iAct: ActIdle);
274 // Object system operation
275 if (Status == C4OS_INACTIVE)
276 {
277 // object was inactive: activate first, then delete
278 Game.Objects.InactiveObjects.Remove(pObj: this);
279 Status = C4OS_NORMAL;
280 Game.Objects.Add(nObj: this);
281 }
282 Status = 0;
283 // count decrease
284 Def->Count--;
285 // Kill contents
286 C4Object *cobj; C4ObjectLink *clnk;
287 while ((clnk = Contents.First) && (cobj = clnk->Obj))
288 {
289 if (fExitContents)
290 cobj->Exit(iX: x, iY: y);
291 else
292 {
293 Contents.Remove(pObj: cobj);
294 cobj->AssignRemoval();
295 }
296 }
297 // remove from container *after* contents have been removed!
298 C4Object *pCont;
299 if (pCont = Contained)
300 {
301 pCont->Contents.Remove(pObj: this);
302 pCont->UpdateMass();
303 pCont->SetOCF();
304 Contained = nullptr;
305 }
306 // Object info
307 if (Info) Info->Retire();
308 Info = nullptr;
309 // Object system operation
310 while (FirstRef) FirstRef->Set0();
311 Game.ClearPointers(cobj: this);
312 ClearCommands();
313 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
314 delete pSolidMaskData;
315 pSolidMaskData = nullptr;
316 SolidMask.Wdt = 0;
317 RemovalDelay = 2;
318}
319
320void C4Object::UpdateShape(bool bUpdateVertices)
321{
322 // Line shape independent
323 if (Def->Line) return;
324
325 // Copy shape from def
326 Shape.CopyFrom(rFrom: Def->Shape, bCpyVertices: bUpdateVertices, fCopyVerticesFromSelf: !!fOwnVertices);
327
328 // Construction zoom
329 if (Con != FullCon)
330 if (Def->GrowthType)
331 Shape.Stretch(iPercent: Con * 100 / FullCon, bUpdateVertices);
332 else
333 Shape.Jolt(iPercent: Con * 100 / FullCon, bUpdateVertices);
334
335 // Rotation
336 if (Def->Rotateable)
337 if (r != 0)
338 Shape.Rotate(iAngle: r, bUpdateVertices);
339
340 // covered area changed? to be on the save side, update pos
341 UpdatePos();
342}
343
344void C4Object::UpdatePos()
345{
346 // get new area covered
347 // do *NOT* do this while initializing, because object cannot be sorted by main list
348 if (!Initializing && Status == C4OS_NORMAL)
349 {
350 Game.Objects.UpdatePos(pObj: this);
351 Audible = -1; // outdated, needs to be recalculated if needed
352 }
353}
354
355void C4Object::UpdateFace(bool bUpdateShape, bool fTemp)
356{
357 // Update shape - NOT for temp call, because temnp calls are done in drawing routine
358 // must not change sync relevant data here (although the shape and pos *should* be updated at that time anyway,
359 // because a runtime join would desync otherwise)
360 if (!fTemp) { if (bUpdateShape) UpdateShape(); else UpdatePos(); }
361
362 // SolidMask
363 if (!fTemp) UpdateSolidMask(fRestoreAttachedObjects: false);
364
365 // Null defaults
366 TopFace.Default();
367
368 // newgfx: TopFace only
369 if (Con >= FullCon || Def->GrowthType)
370 if (!Def->Rotateable || (r == 0))
371 if (Def->TopFace.Wdt > 0) // Fullcon & no rotation
372 TopFace.Set(nsfc: GetGraphics()->GetBitmap(dwClr: Color),
373 nx: Def->TopFace.x, ny: Def->TopFace.y,
374 nwdt: Def->TopFace.Wdt, nhgt: Def->TopFace.Hgt);
375
376 // Active face
377 UpdateActionFace();
378}
379
380void C4Object::UpdateGraphics(bool fGraphicsChanged, bool fTemp)
381{
382 // check color
383 if (!fTemp) if (!pGraphics->IsColorByOwner()) Color = 0;
384 // new grafics: update face + solidmask
385 if (fGraphicsChanged)
386 {
387 // update solid
388 if (pSolidMaskData && !fTemp)
389 {
390 // remove if put
391 pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
392 // delete
393 delete pSolidMaskData; pSolidMaskData = nullptr;
394 // ensure SolidMask-rect lies within new graphics-rect
395 CheckSolidMaskRect();
396 }
397 // update face - this also puts any SolidMask
398 UpdateFace(bUpdateShape: false);
399 }
400}
401
402void C4Object::UpdateFlipDir()
403{
404 int32_t iFlipDir;
405 // We're active
406 if (Action.Act > ActIdle)
407 // Get flipdir value from action
408 if (iFlipDir = Def->ActMap[Action.Act].FlipDir)
409 // Action dir is in flipdir range
410 if (Action.Dir >= iFlipDir)
411 {
412 // Calculate flipped drawing dir (from the flipdir direction going backwards)
413 Action.DrawDir = (iFlipDir - 1 - (Action.Dir - iFlipDir));
414 // Set draw transform, creating one if necessary
415 if (pDrawTransform)
416 pDrawTransform->SetFlipDir(-1);
417 else
418 pDrawTransform = new C4DrawTransform(-1);
419 // Done setting flipdir
420 return;
421 }
422 // No flipdir necessary
423 Action.DrawDir = Action.Dir;
424 // Draw transform present?
425 if (pDrawTransform)
426 {
427 // reset flip dir
428 pDrawTransform->SetFlipDir(1);
429 // if it's identity now, remove the matrix
430 if (pDrawTransform->IsIdentity())
431 {
432 delete pDrawTransform;
433 pDrawTransform = nullptr;
434 }
435 }
436}
437
438void C4Object::DrawFace(C4FacetEx &cgo, int32_t cgoX, int32_t cgoY, int32_t iPhaseX, int32_t iPhaseY)
439{
440 const int swdt = Def->Shape.Wdt;
441 const int shgt = Def->Shape.Hgt;
442 // Grow Type Display
443 float fx = static_cast<float>(swdt * iPhaseX);
444 float fy = static_cast<float>(shgt * iPhaseY);
445 float fwdt = static_cast<float>(swdt);
446 float fhgt = static_cast<float>(shgt);
447
448 float tx = static_cast<float>(cgoX + (Shape.Wdt - swdt * Con / FullCon) / 2);
449 float ty = static_cast<float>(cgoY + (Shape.Hgt - shgt * Con / FullCon) / 2);
450 float twdt = static_cast<float>(swdt * Con / FullCon);
451 float thgt = static_cast<float>(shgt * Con / FullCon);
452
453 // Construction Type Display
454 if (!Def->GrowthType)
455 {
456 tx = static_cast<float>(cgoX) + (Shape.Wdt - swdt) / 2.0f;
457 twdt = static_cast<float>(swdt);
458 fy += static_cast<float>(shgt * std::max<int32_t>(a: FullCon - Con, b: 0) / FullCon);
459 fhgt = static_cast<float>(std::min<int32_t>(a: shgt * Con / FullCon, b: shgt));
460 }
461
462 const float scale{GetGraphics()->pDef->Scale};
463
464 fx *= scale;
465 fy *= scale;
466 fwdt *= scale;
467 fhgt *= scale;
468
469 // Straight
470 if ((!Def->Rotateable || (r == 0)) && !pDrawTransform)
471 {
472 lpDDraw->Blit(sfcSource: GetGraphics()->GetBitmap(dwClr: Color),
473 fx, fy, fwdt, fhgt,
474 sfcTarget: cgo.Surface, tx, ty, twdt, thgt,
475 fSrcColKey: true, pTransform: nullptr);
476 }
477 // Rotated or transformed
478 else
479 {
480 C4DrawTransform rot;
481 if (pDrawTransform)
482 {
483 rot.SetTransformAt(rCopy&: *pDrawTransform, iOffX: cgoX + Shape.Wdt / 2.0f, iOffY: cgoY + Shape.Hgt / 2.0f);
484 if (r) rot.Rotate(iAngle: r * 100, fOffX: cgoX + Shape.Wdt / 2.0f, fOffY: cgoY + Shape.Hgt / 2.0f);
485 }
486 else
487 {
488 rot.SetRotate(iAngle: r * 100, fOffX: cgoX + Shape.Wdt / 2.0f, fOffY: cgoY + Shape.Hgt / 2.0f);
489 }
490 lpDDraw->Blit(sfcSource: GetGraphics()->GetBitmap(dwClr: Color),
491 fx, fy, fwdt, fhgt,
492 sfcTarget: cgo.Surface, tx, ty, twdt, thgt,
493 fSrcColKey: true, pTransform: &rot);
494 }
495}
496
497void C4Object::UpdateMass()
498{
499 Mass = std::max<int32_t>(a: (Def->Mass + OwnMass) * Con / FullCon, b: 1);
500 if (!Def->NoComponentMass) Mass += Contents.Mass;
501 if (Contained)
502 {
503 Contained->Contents.MassCount();
504 Contained->UpdateMass();
505 }
506}
507
508void C4Object::ComponentConCutoff()
509{
510 // this is not ideal, since it does not know about custom builder components
511 int32_t cnt;
512 for (cnt = 0; Component.GetID(index: cnt); cnt++)
513 Component.SetCount(index: cnt,
514 count: std::min<int32_t>(a: Component.GetCount(index: cnt), b: Def->Component.GetCount(index: cnt) * Con / FullCon));
515}
516
517void C4Object::ComponentConGain()
518{
519 // this is not ideal, since it does not know about custom builder components
520 int32_t cnt;
521 for (cnt = 0; Component.GetID(index: cnt); cnt++)
522 Component.SetCount(index: cnt,
523 count: std::max<int32_t>(a: Component.GetCount(index: cnt), b: Def->Component.GetCount(index: cnt) * Con / FullCon));
524}
525
526void C4Object::SetOCF()
527{
528#ifdef DEBUGREC_OCF
529 uint32_t dwOCFOld = OCF;
530#endif
531 // Update the object character flag according to the object's current situation
532 C4Fixed cspeed = GetSpeed();
533#ifndef NDEBUG
534 if (Contained && !Game.Objects.ObjectNumber(Contained))
535 {
536 LogNTr(spdlog::level::warn, "Contained in wild object {}!", static_cast<void *>(Contained.Object()));
537 }
538 else if (Contained && !Contained->Status)
539 {
540 LogNTr(spdlog::level::warn, "Warning: contained in deleted object {} ({})!", static_cast<void *>(Contained.Object()), Contained->GetName());
541 }
542#endif
543 if (Contained)
544 InMat = Contained->Def->ClosedContainer ? MNone : Contained->InMat;
545 else
546 InMat = GBackMat(x, y);
547 // OCF_Normal: The OCF is never zero
548 OCF = OCF_Normal;
549 // OCF_Construct: Can be built outside
550 if (Def->Constructable && (Con < FullCon)
551 && (r == 0) && !OnFire)
552 OCF |= OCF_Construct;
553 // OCF_Grab: Can be pushed
554 if (Def->Grab && !(Category & C4D_StaticBack))
555 OCF |= OCF_Grab;
556 // OCF_Carryable: Can be picked up
557 if (Def->Carryable)
558 OCF |= OCF_Carryable;
559 // OCF_OnFire: Is burning
560 if (OnFire)
561 OCF |= OCF_OnFire;
562 // OCF_Inflammable: Is not burning and is inflammable
563 if (!OnFire && Def->ContactIncinerate > 0)
564 // Is not a dead living
565 if (!(Category & C4D_Living) || Alive)
566 OCF |= OCF_Inflammable;
567 // OCF_FullCon: Is fully completed/grown
568 if (Con >= FullCon)
569 OCF |= OCF_FullCon;
570 // OCF_Chop: Can be chopped
571 uint32_t cocf = OCF_Exclusive;
572 if (Def->Chopable)
573 if (Category & C4D_StaticBack) // Must be static back: this excludes trees that have already been chopped
574 if (!Game.Objects.AtObject(ctx: x, cty: y, ocf&: cocf)) // Can only be chopped if the center is not blocked by an exclusive object
575 OCF |= OCF_Chop;
576 // OCF_Rotate: Can be rotated
577 if (Def->Rotateable)
578 // Don't rotate minimum (invisible) construction sites
579 if (Con > 100)
580 OCF |= OCF_Rotate;
581 // OCF_Exclusive: No action through this, no construction in front of this
582 if (Def->Exclusive)
583 OCF |= OCF_Exclusive;
584 // OCF_Entrance: Can currently be entered/activated
585 if ((Def->Entrance.Wdt > 0) && (Def->Entrance.Hgt > 0))
586 if ((OCF & OCF_FullCon) && ((Def->RotatedEntrance == 1) || (r <= Def->RotatedEntrance)))
587 OCF |= OCF_Entrance;
588 // HitSpeeds
589 if (cspeed >= HitSpeed1) OCF |= OCF_HitSpeed1;
590 if (cspeed >= HitSpeed2) OCF |= OCF_HitSpeed2;
591 if (cspeed >= HitSpeed3) OCF |= OCF_HitSpeed3;
592 if (cspeed >= HitSpeed4) OCF |= OCF_HitSpeed4;
593 // OCF_Collection
594 if ((OCF & OCF_FullCon) || Def->IncompleteActivity)
595 if ((Def->Collection.Wdt > 0) && (Def->Collection.Hgt > 0))
596 if (!Def->CollectionLimit || (Contents.ObjectCount() < Def->CollectionLimit))
597 if ((Action.Act <= ActIdle) || (!Def->ActMap[Action.Act].Disabled))
598 if (NoCollectDelay == 0)
599 OCF |= OCF_Collection;
600 // OCF_Living
601 if (Category & C4D_Living)
602 {
603 OCF |= OCF_Living;
604 if (Alive) OCF |= OCF_Alive;
605 }
606 // OCF_FightReady
607 if (OCF & OCF_Alive)
608 if ((Action.Act <= ActIdle) || (!Def->ActMap[Action.Act].Disabled))
609 if (!Def->NoFight)
610 OCF |= OCF_FightReady;
611 // OCF_LineConstruct
612 if (OCF & OCF_FullCon)
613 if (Def->LineConnect & ~C4D_EnergyHolder)
614 OCF |= OCF_LineConstruct;
615 // OCF_Prey
616 if (Def->Prey)
617 if (Alive)
618 OCF |= OCF_Prey;
619 // OCF_CrewMember
620 if (Def->CrewMember)
621 if (Alive)
622 OCF |= OCF_CrewMember;
623 // OCF_AttractLightning
624 if (Def->AttractLightning)
625 if (OCF & OCF_FullCon)
626 OCF |= OCF_AttractLightning;
627 // OCF_NotContained
628 if (!Contained)
629 OCF |= OCF_NotContained;
630 // OCF_Edible
631 if (Def->Edible)
632 OCF |= OCF_Edible;
633 // OCF_InLiquid
634 if (InLiquid)
635 if (!Contained)
636 OCF |= OCF_InLiquid;
637 // OCF_InSolid
638 if (!Contained)
639 if (GBackSolid(x, y))
640 OCF |= OCF_InSolid;
641 // OCF_InFree
642 if (!Contained)
643 if (!GBackSemiSolid(x, y: y - 1))
644 OCF |= OCF_InFree;
645 // OCF_Available
646 if (!Contained || (Contained->Def->GrabPutGet & C4D_Grab_Get) || (Contained->OCF & OCF_Entrance))
647 if (!GBackSemiSolid(x, y: y - 1) || (!GBackSolid(x, y: y - 1) && !GBackSemiSolid(x, y: y - 8)))
648 OCF |= OCF_Available;
649 // OCF_PowerConsumer
650 if (Def->LineConnect & C4D_Power_Consumer)
651 if (OCF & OCF_FullCon)
652 OCF |= OCF_PowerConsumer;
653 // OCF_PowerSupply
654 if ((Def->LineConnect & C4D_Power_Generator)
655 || ((Def->LineConnect & C4D_Power_Output) && (Energy > 0)))
656 if (OCF & OCF_FullCon)
657 OCF |= OCF_PowerSupply;
658 // OCF_Container
659 if ((Def->GrabPutGet & C4D_Grab_Put) || (Def->GrabPutGet & C4D_Grab_Get) || (OCF & OCF_Entrance))
660 OCF |= OCF_Container;
661#ifdef DEBUGREC_OCF
662 assert(!dwOCFOld || ((dwOCFOld & OCF_Carryable) == (OCF & OCF_Carryable)));
663 C4RCOCF rc = { dwOCFOld, OCF, false };
664 AddDbgRec(RCT_OCF, &rc, sizeof(rc));
665#endif
666}
667
668void C4Object::UpdateOCF()
669{
670#ifdef DEBUGREC_OCF
671 uint32_t dwOCFOld = OCF;
672#endif
673 // Update the object character flag according to the object's current situation
674 C4Fixed cspeed = GetSpeed();
675#ifndef NDEBUG
676 if (Contained && !Game.Objects.ObjectNumber(Contained))
677 {
678 LogNTr(spdlog::level::warn, "contained in wild object {}!", static_cast<void *>(Contained.Object()));
679 }
680 else if (Contained && !Contained->Status)
681 {
682 LogNTr(spdlog::level::warn, "contained in deleted object {} ({})!", static_cast<void *>(Contained.Object()), Contained->GetName());
683 }
684#endif
685 if (Contained)
686 InMat = Contained->Def->ClosedContainer ? MNone : Contained->InMat;
687 else
688 InMat = GBackMat(x, y);
689 // Keep the bits that only have to be updated with SetOCF (def, category, con, alive, onfire)
690 OCF = OCF & (OCF_Normal | OCF_Carryable | OCF_Exclusive | OCF_Edible | OCF_Grab | OCF_FullCon
691 /*| OCF_Chop - now updated regularly, see below */
692 | OCF_Rotate | OCF_OnFire | OCF_Inflammable | OCF_Living | OCF_Alive
693 | OCF_LineConstruct | OCF_Prey | OCF_CrewMember | OCF_AttractLightning
694 | OCF_PowerConsumer);
695 // OCF_Construct: Can be built outside
696 if (Def->Constructable && (Con < FullCon)
697 && (r == 0) && !OnFire)
698 OCF |= OCF_Construct;
699 // OCF_Entrance: Can currently be entered/activated
700 if ((Def->Entrance.Wdt > 0) && (Def->Entrance.Hgt > 0))
701 if ((OCF & OCF_FullCon) && ((Def->RotatedEntrance == 1) || (r <= Def->RotatedEntrance)))
702 OCF |= OCF_Entrance;
703 // OCF_Chop: Can be chopped
704 uint32_t cocf = OCF_Exclusive;
705 if (Def->Chopable)
706 if (Category & C4D_StaticBack) // Must be static back: this excludes trees that have already been chopped
707 if (!Game.Objects.AtObject(ctx: x, cty: y, ocf&: cocf)) // Can only be chopped if the center is not blocked by an exclusive object
708 OCF |= OCF_Chop;
709 // HitSpeeds
710 if (cspeed >= HitSpeed1) OCF |= OCF_HitSpeed1;
711 if (cspeed >= HitSpeed2) OCF |= OCF_HitSpeed2;
712 if (cspeed >= HitSpeed3) OCF |= OCF_HitSpeed3;
713 if (cspeed >= HitSpeed4) OCF |= OCF_HitSpeed4;
714 // OCF_Collection
715 if ((OCF & OCF_FullCon) || Def->IncompleteActivity)
716 if ((Def->Collection.Wdt > 0) && (Def->Collection.Hgt > 0))
717 if (!Def->CollectionLimit || (Contents.ObjectCount() < Def->CollectionLimit))
718 if ((Action.Act <= ActIdle) || (!Def->ActMap[Action.Act].Disabled))
719 if (NoCollectDelay == 0)
720 OCF |= OCF_Collection;
721 // OCF_FightReady
722 if (OCF & OCF_Alive)
723 if ((Action.Act <= ActIdle) || (!Def->ActMap[Action.Act].Disabled))
724 if (!Def->NoFight)
725 OCF |= OCF_FightReady;
726 // OCF_NotContained
727 if (!Contained)
728 OCF |= OCF_NotContained;
729 // OCF_InLiquid
730 if (InLiquid)
731 if (!Contained)
732 OCF |= OCF_InLiquid;
733 // OCF_InSolid
734 if (!Contained)
735 if (GBackSolid(x, y))
736 OCF |= OCF_InSolid;
737 // OCF_InFree
738 if (!Contained)
739 if (!GBackSemiSolid(x, y: y - 1))
740 OCF |= OCF_InFree;
741 // OCF_Available
742 if (!Contained || (Contained->Def->GrabPutGet & C4D_Grab_Get) || (Contained->OCF & OCF_Entrance))
743 if (!GBackSemiSolid(x, y: y - 1) || (!GBackSolid(x, y: y - 1) && !GBackSemiSolid(x, y: y - 8)))
744 OCF |= OCF_Available;
745 // OCF_PowerSupply
746 if ((Def->LineConnect & C4D_Power_Generator)
747 || ((Def->LineConnect & C4D_Power_Output) && (Energy > 0)))
748 if (OCF & OCF_FullCon)
749 OCF |= OCF_PowerSupply;
750 // OCF_Container
751 if ((Def->GrabPutGet & C4D_Grab_Put) || (Def->GrabPutGet & C4D_Grab_Get) || (OCF & OCF_Entrance))
752 OCF |= OCF_Container;
753#ifdef DEBUGREC_OCF
754 C4RCOCF rc = { dwOCFOld, OCF, true };
755 AddDbgRec(RCT_OCF, &rc, sizeof(rc));
756#endif
757#ifndef NDEBUG
758 DEBUGREC_OFF
759 uint32_t updateOCF = OCF;
760 SetOCF();
761 assert(updateOCF == OCF);
762 DEBUGREC_ON
763#endif
764}
765
766bool C4Object::ExecFire(int32_t iFireNumber, int32_t iCausedByPlr)
767{
768 // Fire Phase
769 FirePhase++; if (FirePhase >= MaxFirePhase) FirePhase = 0;
770 // Extinguish in base
771 if (!Tick5)
772 if (Category & C4D_Living)
773 if (Contained && ValidPlr(plr: Contained->Base))
774 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Extinguish)
775 Extinguish(iFireNumber);
776 // Decay
777 if (!Def->NoBurnDecay)
778 DoCon(iChange: -100);
779 // Damage
780 if (!Tick10) if (!Def->NoBurnDamage) DoDamage(iLevel: +2, iCausedByPlr, C4FxCall_DmgFire);
781 // Energy
782 if (!Tick5) DoEnergy(iChange: -1, fExact: false, C4FxCall_EngFire, iCausedByPlr);
783 // Effects
784 int32_t smoke_level = 2 * Shape.Wdt / 3;
785 int32_t smoke_rate = Def->SmokeRate;
786 if (smoke_rate)
787 {
788 smoke_rate = 50 * smoke_level / smoke_rate;
789 if (!((Game.FrameCounter + (Number * 7)) % std::max<int32_t>(a: smoke_rate, b: 3)) || (Abs(val: xdir) > 2))
790 Smoke(tx: x, ty: y, level: smoke_level);
791 }
792 // Background Effects
793 if (!Tick5)
794 {
795 int32_t mat;
796 if (MatValid(mat: mat = GBackMat(x, y)))
797 {
798 // Extinguish
799 if (Game.Material.Map[mat].Extinguisher)
800 {
801 Extinguish(iFireNumber); if (GBackLiquid(x, y)) StartSoundEffect(name: "Pshshsh", loop: false, volume: 100, obj: this);
802 }
803 // Inflame
804 if (!Random(iRange: 3))
805 Game.Landscape.Incinerate(x, y);
806 }
807 }
808
809 return true;
810}
811
812bool C4Object::BuyEnergy()
813{
814 C4Player *pPlr = Game.Players.Get(iPlayer: Base); if (!pPlr) return false;
815 if (!GetPhysical()->Energy) return false;
816 if (pPlr->Eliminated) return false;
817 if (pPlr->Wealth < Game.C4S.Game.Realism.BaseRegenerateEnergyPrice) return false;
818 pPlr->DoWealth(change: -Game.C4S.Game.Realism.BaseRegenerateEnergyPrice);
819 DoEnergy(iChange: +100, fExact: false, C4FxCall_EngBaseRefresh, iCausedByPlr: Owner);
820 return true;
821}
822
823bool C4Object::ExecLife()
824{
825 // Growth
826 if (!Tick35)
827 // Growth specified by definition
828 if (Def->Growth)
829 // Alive livings && trees only
830 if (((Category & C4D_Living) && Alive)
831 || (Category & C4D_StaticBack))
832 // Not burning
833 if (!OnFire)
834 // Not complete yet
835 if (Con < FullCon)
836 // Grow
837 DoCon(iChange: Def->Growth * 100);
838
839 // Energy reload in base
840 int32_t transfer;
841 if (!Tick3) if (Alive)
842 if (Contained && ValidPlr(plr: Contained->Base))
843 if (!Hostile(plr1: Owner, plr2: Contained->Base))
844 if (Energy < GetPhysical()->Energy)
845 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_RegenerateEnergy)
846 {
847 if (Contained->Energy <= 0) Contained->BuyEnergy();
848 transfer = std::min<int32_t>(a: std::min<int32_t>(a: 2 * C4MaxPhysical / 100, b: Contained->Energy), b: GetPhysical()->Energy - Energy);
849 if (transfer)
850 {
851 Contained->DoEnergy(iChange: -transfer, fExact: true, C4FxCall_EngBaseRefresh, iCausedByPlr: Contained->Owner);
852 DoEnergy(iChange: transfer, fExact: true, C4FxCall_EngBaseRefresh, iCausedByPlr: Contained->Owner);
853 }
854 }
855
856 // Magic reload
857 if (!Tick3) if (Alive)
858 if (Contained)
859 if (!Hostile(plr1: Owner, plr2: Contained->Owner))
860 if (MagicEnergy < GetPhysical()->Magic)
861 {
862 transfer = std::min<int32_t>(a: std::min<int32_t>(a: 2 * MagicPhysicalFactor, b: Contained->MagicEnergy), b: GetPhysical()->Magic - MagicEnergy) / MagicPhysicalFactor;
863 if (transfer)
864 {
865 // do energy transfer via script, so it can be overloaded by No-Magic-Energy-rule
866 // always use global func instead of local to save double search
867 C4AulFunc *pMagicEnergyFn = Game.ScriptEngine.GetFuncRecursive(PSF_DoMagicEnergy);
868 if (pMagicEnergyFn) // should always be true
869 {
870 if (pMagicEnergyFn->Exec(pObj: nullptr, pPars: {C4VInt(iVal: -transfer), C4VObj(pObj: Contained)}))
871 {
872 pMagicEnergyFn->Exec(pObj: nullptr, pPars: {C4VInt(iVal: +transfer), C4VObj(pObj: this)});
873 }
874 }
875 }
876 }
877
878 // Breathing
879 if (!Tick5)
880 if (Alive && !Def->NoBreath)
881 {
882 // Supply check
883 bool Breathe = false;
884 // Forcefields are breathable.
885 if (GBackMat(x, y: y + Shape.y / 2) == MVehic)
886 {
887 Breathe = true;
888 }
889 else if (GetPhysical()->BreatheWater)
890 {
891 if (GBackMat(x, y) == MWater) Breathe = true;
892 }
893 else
894 {
895 if (!GBackSemiSolid(x, y: y + Shape.y / 2)) Breathe = true;
896 }
897 if (Contained) Breathe = true;
898 // No supply
899 if (!Breathe)
900 {
901 // Reduce breath, then energy, bubble
902 // Asphyxiation cause is last energy loss cause player, so kill tracing works when player is pushed into liquid
903 if (Breath > 0) Breath = (std::max)(a: Breath - 2 * C4MaxPhysical / 100, b: 0);
904 else DoEnergy(iChange: -1, fExact: false, C4FxCall_EngAsphyxiation, iCausedByPlr: LastEnergyLossCausePlayer);
905 BubbleOut(tx: x + Random(iRange: 5) - 2, ty: y + Shape.y / 2);
906 ViewEnergy = C4ViewDelay;
907 // Physical training
908 TrainPhysical(mpiOffset: &C4PhysicalInfo::Breath, iTrainBy: 2, iMaxTrain: C4MaxPhysical);
909 }
910 // Supply
911 else
912 {
913 // Take breath
914 int32_t takebreath = GetPhysical()->Breath - Breath;
915 if (takebreath > GetPhysical()->Breath / 2)
916 Call(PSF_DeepBreath);
917 Breath += takebreath;
918 }
919 }
920
921 // Corrosion energy loss
922 if (!Tick10)
923 if (Alive)
924 if (InMat != MNone)
925 if (Game.Material.Map[InMat].Corrosive)
926 if (!GetPhysical()->CorrosionResist)
927 // Inflict corrision damage by last energy loss cause player, so tumbling enemies into an acid lake attribute kills properly
928 DoEnergy(iChange: -Game.Material.Map[InMat].Corrosive / 15, fExact: false, C4FxCall_EngCorrosion, iCausedByPlr: LastEnergyLossCausePlayer);
929
930 // InMat incineration
931 if (!Tick10)
932 if (InMat != MNone)
933 if (Game.Material.Map[InMat].Incindiary)
934 if (Def->ContactIncinerate)
935 // Inflict fire by last energy loss cause player, so tumbling enemies into a lava lake attribute kills properly
936 Incinerate(iCausedBy: LastEnergyLossCausePlayer);
937
938 // Nonlife normal energy loss
939 if (!Tick10) if (Energy)
940 if (!(Category & C4D_Living))
941 if (!ValidPlr(plr: Base) || (~Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_RegenerateEnergy))
942 // don't loose if assigned as Energy-holder
943 if (!(Def->LineConnect & C4D_EnergyHolder))
944 DoEnergy(iChange: -1, fExact: false, C4FxCall_EngStruct, iCausedByPlr: NO_OWNER);
945
946 // birthday
947 if (!Tick255)
948 if (Alive)
949 if (Info)
950 {
951 int32_t iPlayingTime = Info->TotalPlayingTime + (Game.Time - Info->InActionTime);
952
953 int32_t iNewAge = iPlayingTime / 3600 / 5;
954
955 if (Info->Age != iNewAge)
956 {
957 // message
958 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_BIRTHDAY, args: GetName(), args&: iNewAge).c_str(), pTarget: this);
959 StartSoundEffect(name: "Trumpet", loop: false, volume: 100, obj: this);
960 }
961
962 Info->Age = iNewAge;
963 }
964
965 return true;
966}
967
968void C4Object::AutoSellContents()
969{
970 C4Player *pPlr = Game.Players.Get(iPlayer: Base); if (!pPlr) return;
971
972 for (auto outerIt = std::begin(cont&: Contents); outerIt != std::end(cont&: Contents); )
973 {
974 const auto &obj = *outerIt;
975 ++outerIt;
976 if (obj && obj->Status)
977 {
978 for (auto innerIt = std::begin(cont&: obj->Contents); innerIt != std::end(cont&: obj->Contents); )
979 {
980 const auto &contents = *innerIt;
981 ++innerIt;
982 if (contents && contents->Status && contents->Def->BaseAutoSell && pPlr->CanSell(obj: contents))
983 {
984 contents->Exit();
985 pPlr->Sell2Home(tobj: contents);
986 }
987 }
988
989 if (obj->Def->BaseAutoSell && pPlr->CanSell(obj))
990 {
991 obj->Exit();
992 pPlr->Sell2Home(tobj: obj);
993 }
994 }
995 }
996}
997
998void C4Object::ExecBase()
999{
1000 C4Object *flag;
1001
1002 // New base assignment by flag (no old base removal)
1003 if (!Tick10)
1004 if (Def->CanBeBase) if (!ValidPlr(plr: Base))
1005 if (flag = Contents.Find(id: C4ID_Flag))
1006 if (ValidPlr(plr: flag->Owner) && (flag->Owner != Base))
1007 {
1008 // Attach new flag
1009 flag->Exit();
1010 flag->SetActionByName(szActName: "FlyBase", pTarget: this);
1011 // Assign new base
1012 Base = flag->Owner;
1013 Contents.CloseMenus();
1014 StartSoundEffect(name: "Trumpet", loop: false, volume: 100, obj: this);
1015 SetOwner(flag->Owner);
1016 }
1017
1018 // Base execution
1019 if (!Tick35)
1020 if (ValidPlr(plr: Base))
1021 {
1022 // Auto sell contents
1023 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_AutoSellContents) AutoSellContents();
1024 // Lost flag?
1025 if (!Game.FindObject(id: C4ID_Flag, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: "FlyBase", pActionTarget: this))
1026 {
1027 Base = NO_OWNER;
1028 Contents.CloseMenus();
1029 }
1030 }
1031
1032 // Environmental action
1033 if (!Tick35)
1034 {
1035 // Structures dig free snow
1036 if ((Category & C4D_Structure) && !(Game.Rules & C4RULE_StructuresSnowIn))
1037 if (r == 0)
1038 {
1039 Game.Landscape.DigFreeMat(tx: x + Shape.x, ty: y + Shape.y, wdt: Shape.Wdt, hgt: Shape.Hgt, mat: MSnow);
1040 Game.Landscape.DigFreeMat(tx: x + Shape.x, ty: y + Shape.y, wdt: Shape.Wdt, hgt: Shape.Hgt, mat: Game.Material.Get(szMaterial: "FlyAshes"));
1041 }
1042 }
1043}
1044
1045void C4Object::Execute()
1046{
1047#ifdef DEBUGREC
1048 // record debug
1049 C4RCExecObj rc;
1050 rc.Number = Number;
1051 rc.id = Def->id;
1052 rc.fx = fix_x;
1053 rc.fy = fix_y;
1054 rc.fr = fix_r;
1055 AddDbgRec(RCT_ExecObj, &rc, sizeof(rc));
1056#endif
1057 // OCF
1058 UpdateOCF();
1059 // Command
1060 ExecuteCommand();
1061 // Action
1062 // need not check status, because dead objects have lost their action
1063 ExecAction();
1064 // commands and actions are likely to have removed the object, and movement
1065 // *must not* be executed for dead objects (SolidMask-errors)
1066 if (!Status) return;
1067 // Movement
1068 ExecMovement();
1069 if (!Status) return;
1070 // particles
1071 if (BackParticles) BackParticles.Exec(pObj: this);
1072 if (FrontParticles) FrontParticles.Exec(pObj: this);
1073 // effects
1074 if (pEffects)
1075 {
1076 pEffects->Execute(pObj: this);
1077 if (!Status) return;
1078 }
1079 // Life
1080 ExecLife();
1081 // Base
1082 ExecBase();
1083 // Timer
1084 Timer++;
1085 if (Timer >= Def->Timer)
1086 {
1087 Timer = 0;
1088 // TimerCall
1089 if (Def->TimerCall) Def->TimerCall->Exec(pObj: this);
1090 }
1091 // Menu
1092 if (Menu) Menu->Execute();
1093 // View delays
1094 if (ViewEnergy > 0) ViewEnergy--;
1095}
1096
1097bool C4Object::At(int32_t ctx, int32_t cty)
1098{
1099 if (Status) if (!Contained) if (Def)
1100 if (Inside<int32_t>(ival: cty - (y + Shape.y - addtop()), lbound: 0, rbound: Shape.Hgt - 1 + addtop()))
1101 if (Inside<int32_t>(ival: ctx - (x + Shape.x), lbound: 0, rbound: Shape.Wdt - 1))
1102 return true;
1103 return false;
1104}
1105
1106bool C4Object::At(int32_t ctx, int32_t cty, uint32_t &ocf)
1107{
1108 if (Status) if (!Contained) if (Def)
1109 if (OCF & ocf)
1110 if (Inside<int32_t>(ival: cty - (y + Shape.y - addtop()), lbound: 0, rbound: Shape.Hgt - 1 + addtop()))
1111 if (Inside<int32_t>(ival: ctx - (x + Shape.x), lbound: 0, rbound: Shape.Wdt - 1))
1112 {
1113 // Set ocf return value
1114 GetOCFForPos(ctx, cty, ocf);
1115 return true;
1116 }
1117
1118 return false;
1119}
1120
1121void C4Object::GetOCFForPos(int32_t ctx, int32_t cty, uint32_t &ocf)
1122{
1123 uint32_t rocf = OCF;
1124 // Verify entrance area OCF return
1125 if (rocf & OCF_Entrance)
1126 if (!Inside<int32_t>(ival: cty - (y + Def->Entrance.y), lbound: 0, rbound: Def->Entrance.Hgt - 1)
1127 || !Inside<int32_t>(ival: ctx - (x + Def->Entrance.x), lbound: 0, rbound: Def->Entrance.Wdt - 1))
1128 rocf &= (~OCF_Entrance);
1129 // Verify collection area OCF return
1130 if (rocf & OCF_Collection)
1131 if (!Inside<int32_t>(ival: cty - (y + Def->Collection.y), lbound: 0, rbound: Def->Collection.Hgt - 1)
1132 || !Inside<int32_t>(ival: ctx - (x + Def->Collection.x), lbound: 0, rbound: Def->Collection.Wdt - 1))
1133 rocf &= (~OCF_Collection);
1134 ocf = rocf;
1135}
1136
1137void C4Object::AssignDeath(bool fForced)
1138{
1139 C4Object *thing;
1140 // Alive objects only
1141 if (!Alive) return;
1142 // clear all effects
1143 // do not delete effects afterwards, because they might have denied removal
1144 // set alive-flag before, so objects know what's up
1145 // and prevent recursive death-calls this way
1146 // get death causing player before doing effect calls, because those might meddle around with the flags
1147 int32_t iDeathCausingPlayer = LastEnergyLossCausePlayer;
1148 Alive = 0;
1149 if (pEffects) pEffects->ClearAll(pObj: this, C4FxCall_RemoveDeath);
1150 // if the object is alive again, abort here if the kill is not forced
1151 if (Alive && !fForced) return;
1152 // Action
1153 SetActionByName(szActName: "Dead");
1154 // Values
1155 Select = 0;
1156 Alive = 0;
1157 ClearCommands();
1158 if (Info)
1159 {
1160 Info->HasDied = true;
1161 ++Info->DeathCount;
1162 Info->Retire();
1163 }
1164 // Lose contents
1165 while (thing = Contents.GetObject()) thing->Exit(iX: thing->x, iY: thing->y);
1166 // Remove from crew/cursor/view
1167 C4Player *pPlr = Game.Players.Get(iPlayer: Owner);
1168 if (pPlr) pPlr->ClearPointers(tptr: this, fDeath: true);
1169 // ensure objects that won't be affected by dead-plrview-decay are handled properly
1170 if (!pPlr || !(Category & C4D_Living) || !pPlr->FoWViewObjs.IsContained(pObj: this))
1171 SetPlrViewRange(0);
1172 // Engine script call
1173 Call(PSF_Death, pPars: {C4VInt(iVal: iDeathCausingPlayer)});
1174 // Update OCF. Done here because previously it would have been done in the next frame
1175 // Whats worse: Having the OCF change because of some unrelated script-call like
1176 // SetCategory, or slightly breaking compatibility?
1177 SetOCF();
1178}
1179
1180bool C4Object::ChangeDef(C4ID idNew)
1181{
1182 // Get new definition
1183 C4Def *pDef = C4Id2Def(id: idNew);
1184 if (!pDef) return false;
1185 // Containment storage
1186 C4Object *pContainer = Contained;
1187 // Exit container (no Ejection/Departure)
1188 if (Contained) Exit(iX: 0, iY: 0, iR: 0, iXDir: Fix0, iYDir: Fix0, iRDir: Fix0, fCalls: false);
1189 // Pre change resets
1190 SetAction(iAct: ActIdle);
1191 Action.Act = ActIdle; // Enforce ActIdle because SetAction may have failed due to NoOtherAction
1192 SetDir(0); // will drop any outdated flipdir
1193 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
1194 delete pSolidMaskData; pSolidMaskData = nullptr;
1195 Def->Count--;
1196 // Def change
1197 Def = pDef;
1198 id = pDef->id;
1199 Def->Count++;
1200 LocalNamed.SetNameList(&pDef->Script.LocalNamed);
1201 // new def: Needs to be resorted
1202 Unsorted = true;
1203 // graphics change
1204 pGraphics = &pDef->Graphics;
1205 // blit mode adjustment
1206 if (!(BlitMode & C4GFXBLIT_CUSTOM)) BlitMode = Def->BlitMode;
1207 // an object may have newly become an ColorByOwner-object
1208 // if it had been ColorByOwner, but is not now, this will be caught in UpdateGraphics()
1209 if (!Color && ValidPlr(plr: Owner))
1210 Color = Game.Players.Get(iPlayer: Owner)->ColorDw;
1211 if (!Def->Rotateable) { r = 0; fix_r = rdir = Fix0; }
1212 // Reset solid mask
1213 SolidMask = Def->SolidMask;
1214 // Post change updates
1215 UpdateGraphics(fGraphicsChanged: true);
1216 UpdateMass();
1217 UpdateFace(bUpdateShape: true);
1218 SetOCF();
1219 // Any effect callbacks to this object might need to reinitialize their target functions
1220 // This is ugly, because every effect there is must be updated...
1221 if (Game.pGlobalEffects) Game.pGlobalEffects->OnObjectChangedDef(pObj: this);
1222 for (C4ObjectLink *pLnk = Game.Objects.First; pLnk; pLnk = pLnk->Next)
1223 if (pLnk->Obj->pEffects) pLnk->Obj->pEffects->OnObjectChangedDef(pObj: this);
1224 // Containment (no Entrance)
1225 if (pContainer) Enter(pTarget: pContainer, fCalls: false);
1226 // Done
1227 return true;
1228}
1229
1230bool C4Object::Incinerate(int32_t iCausedBy, bool fBlasted, C4Object *pIncineratingObject)
1231{
1232 // Already on fire
1233 if (OnFire) return false;
1234 // Dead living don't burn
1235 if ((Category & C4D_Living) && !Alive) return false;
1236 // add effect
1237 int32_t iEffNumber;
1238 C4Value Par1 = C4VInt(iVal: iCausedBy), Par2 = C4VBool(fVal: !!fBlasted), Par3 = C4VObj(pObj: pIncineratingObject), Par4;
1239 new C4Effect(this, C4Fx_Fire, C4Fx_FirePriority, C4Fx_FireTimer, nullptr, 0, Par1, Par2, Par3, Par4, true, iEffNumber);
1240 return !!iEffNumber;
1241}
1242
1243bool C4Object::Extinguish(int32_t iFireNumber)
1244{
1245 // any effects?
1246 if (!pEffects) return false;
1247 // fire number known: extinguish that fire
1248 C4Effect *pEffFire;
1249 if (iFireNumber)
1250 {
1251 pEffFire = pEffects->Get(iNumber: iFireNumber, fIncludeDead: false);
1252 if (!pEffFire) return false;
1253 pEffFire->Kill(pObj: this);
1254 }
1255 else
1256 {
1257 // otherwise, kill all fires
1258 // (keep checking from beginning of pEffects, as Kill might delete or change effects)
1259 int32_t iFiresKilled = 0;
1260 while (pEffects && (pEffFire = pEffects->Get(C4Fx_AnyFire)))
1261 {
1262 while (pEffFire && WildcardMatch(C4Fx_Internal, szFName2: pEffFire->Name))
1263 {
1264 pEffFire = pEffFire->pNext;
1265 if (pEffFire) pEffFire = pEffFire->Get(C4Fx_AnyFire);
1266 }
1267 if (!pEffFire) break;
1268 pEffFire->Kill(pObj: this);
1269 ++iFiresKilled;
1270 }
1271 if (!iFiresKilled) return false;
1272 }
1273 // done, success
1274 return true;
1275}
1276
1277void C4Object::DoDamage(int32_t iChange, int32_t iCausedBy, int32_t iCause)
1278{
1279 // non-living: ask effects first
1280 if (pEffects && !Alive)
1281 {
1282 pEffects->DoDamage(pObj: this, riDamage&: iChange, iDamageType: iCause, iCausePlr: iCausedBy);
1283 if (!iChange) return;
1284 }
1285 // Change value
1286 Damage = std::max<int32_t>(a: Damage + iChange, b: 0);
1287 // Engine script call
1288 Call(PSF_Damage, pPars: {C4VInt(iVal: iChange), C4VInt(iVal: iCausedBy)});
1289}
1290
1291// returns x * y, but returns std::numeric_limits<T>::min() or std::numeric_limits<T>::max() in case of a negative or positive overflow respectively
1292template<typename T>
1293constexpr T clampedMultiplication(T x, T y)
1294{
1295 if (y == 0) return 0;
1296
1297 if (y < 0)
1298 {
1299 if (x < 0)
1300 {
1301 x *= -1;
1302 y *= -1;
1303 }
1304 else
1305 {
1306 std::swap(x, y);
1307 }
1308 }
1309 constexpr auto min = std::numeric_limits<T>::min();
1310 constexpr auto max = std::numeric_limits<T>::max();
1311 if (x < 0 && x < min / y) // would give negative overflow, so make it the most negative possible
1312 {
1313 return min;
1314 }
1315 else if (x > 0 && x > max / y) // would give positive overflow, so make it the most positive possible
1316 {
1317 return max;
1318 }
1319 else
1320 {
1321 return x * y;
1322 }
1323}
1324
1325// same as above, but calculating x + y instead
1326template<typename T>
1327constexpr T clampedAddition(T x, T y)
1328{
1329 constexpr auto min = std::numeric_limits<T>::min();
1330 constexpr auto max = std::numeric_limits<T>::max();
1331 if (x < 0 && y < min - x) // would give negative overflow, so make it the most negative possible
1332 {
1333 return min;
1334 }
1335 else if (x > 0 && y > max - x) // would give positive overflow, so make it the most positive possible
1336 {
1337 return max;
1338 }
1339 else
1340 {
1341 return x + y;
1342 }
1343}
1344
1345void C4Object::DoEnergy(int32_t iChange, bool fExact, int32_t iCause, int32_t iCausedByPlr)
1346{
1347 // iChange 100% = Physical 100000
1348 if (!fExact) iChange = clampedMultiplication(x: C4MaxPhysical / 100, y: iChange);
1349 // Was zero?
1350 bool fWasZero = (Energy == 0);
1351 // Mark last damage causing player to trace kills. Always update on C4FxCall_EngObjHit even if iChange==0
1352 // so low mass objects also correctly set the killer
1353 if (iChange < 0 || iCause == C4FxCall_EngObjHit) UpdatLastEnergyLossCause(iNewCausePlr: iCausedByPlr);
1354 // Living things: ask effects for change first
1355 if (pEffects && Alive)
1356 {
1357 pEffects->DoDamage(pObj: this, riDamage&: iChange, iDamageType: iCause, iCausePlr: iCausedByPlr);
1358 if (!iChange) return;
1359 }
1360 // Do change
1361 Energy = BoundBy<int32_t>(bval: clampedAddition(x: Energy, y: iChange), lbound: 0, rbound: GetPhysical()->Energy);
1362 // Alive and energy reduced to zero: death
1363 if (Alive) if (Energy == 0) if (!fWasZero) AssignDeath(fForced: false);
1364 // View change
1365 ViewEnergy = C4ViewDelay;
1366}
1367
1368void C4Object::UpdatLastEnergyLossCause(int32_t iNewCausePlr)
1369{
1370 // Mark last damage causing player to trace kills
1371 // do not regard self-administered damage if there was a previous damage causing player, because that would steal kills
1372 // if people tumble themselves via stop-stop-(left/right)-throw while falling into teh abyss
1373 if (iNewCausePlr != Controller || LastEnergyLossCausePlayer < 0)
1374 {
1375 LastEnergyLossCausePlayer = iNewCausePlr;
1376 }
1377}
1378
1379void C4Object::DoBreath(int32_t iChange)
1380{
1381 // iChange 100% = Physical 100000
1382 iChange = clampedMultiplication(x: C4MaxPhysical / 100, y: iChange);
1383 // Do change
1384 Breath = BoundBy<int32_t>(bval: clampedAddition(x: Breath, y: iChange), lbound: 0, rbound: GetPhysical()->Breath);
1385 // View change
1386 ViewEnergy = C4ViewDelay;
1387}
1388
1389void C4Object::Blast(int32_t iLevel, int32_t iCausedBy)
1390{
1391 // Damage
1392 DoDamage(iChange: iLevel, iCausedBy, C4FxCall_DmgBlast);
1393 // Energy (alive objects)
1394 if (Alive) DoEnergy(iChange: -iLevel / 3, fExact: false, C4FxCall_EngBlast, iCausedByPlr: iCausedBy);
1395 // Incinerate
1396 if (Def->BlastIncinerate)
1397 if (Damage >= Def->BlastIncinerate)
1398 Incinerate(iCausedBy, fBlasted: true);
1399}
1400
1401void C4Object::DoCon(int32_t iChange, bool fInitial, bool fNoComponentChange)
1402{
1403 int32_t iStepSize = FullCon / 100;
1404 int32_t lRHgt = Shape.Hgt, lRy = Shape.y;
1405 int32_t iLastStep = Con / iStepSize;
1406 int32_t strgt_con_b = y + Shape.y + Shape.Hgt;
1407 bool fWasFull = (Con >= FullCon);
1408
1409 // Change con
1410 if (Def->Oversize)
1411 Con = std::max<int32_t>(a: Con + iChange, b: 0);
1412 else
1413 Con = BoundBy<int32_t>(bval: Con + iChange, lbound: 0, rbound: FullCon);
1414 int32_t iStepDiff = Con / iStepSize - iLastStep;
1415
1416 // Update OCF
1417 SetOCF();
1418
1419 // If step changed or limit reached or degraded from full: update mass, face, components, etc.
1420 if (iStepDiff || (Con >= FullCon) || (Con == 0) || (fWasFull && (Con < FullCon)))
1421 {
1422 // Mass
1423 UpdateMass();
1424 // Decay from full remove mask before face is changed
1425 if (fWasFull && (Con < FullCon))
1426 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
1427 // Face
1428 UpdateFace(bUpdateShape: true);
1429 // component update
1430 if (!fNoComponentChange)
1431 {
1432 // Decay: reduce components
1433 if (iChange < 0)
1434 ComponentConCutoff();
1435 // Growth: gain components
1436 else
1437 ComponentConGain();
1438 }
1439 // Unfullcon
1440 if (Con < FullCon)
1441 {
1442 // Lose contents
1443 if (!Def->IncompleteActivity)
1444 {
1445 C4Object *cobj;
1446 while (cobj = Contents.GetObject())
1447 if (Contained) cobj->Enter(pTarget: Contained);
1448 else cobj->Exit(iX: cobj->x, iY: cobj->y);
1449 }
1450 // No energy need
1451 NeedEnergy = 0;
1452 }
1453 // Decay from full stop action
1454 if (fWasFull && (Con < FullCon))
1455 if (!Def->IncompleteActivity)
1456 SetAction(iAct: ActIdle);
1457 }
1458 else
1459 // set first position
1460 if (fInitial) UpdatePos();
1461
1462 // Straight Con bottom y-adjust
1463 if (!r || fInitial)
1464 {
1465 if ((Shape.Hgt != lRHgt) || (Shape.y != lRy))
1466 {
1467 y = strgt_con_b - Shape.Hgt - Shape.y;
1468 UpdatePos(); UpdateSolidMask(fRestoreAttachedObjects: false);
1469 }
1470 }
1471 else if (Category & C4D_Structure) if (iStepDiff > 0)
1472 {
1473 // even rotated buildings need to be moved upwards
1474 // but by con difference, because with keep-bottom-method, they might still be sinking
1475 // besides, moving the building up may often stabilize it
1476 y -= ((iLastStep + iStepDiff) * Def->Shape.Hgt / 100) - (iLastStep * Def->Shape.Hgt / 100);
1477 UpdatePos(); UpdateSolidMask(fRestoreAttachedObjects: false);
1478 }
1479 // Completion (after bottom y-adjust for correct position)
1480 if (!fWasFull && (Con >= FullCon))
1481 {
1482 Call(PSF_Completion);
1483 Call(PSF_Initialize);
1484 }
1485
1486 // Con Zero Removal
1487 if (Con <= 0)
1488 AssignRemoval();
1489}
1490
1491void C4Object::DoExperience(int32_t change)
1492{
1493 const int32_t MaxExperience = 100000000;
1494
1495 if (!Info) return;
1496
1497 Info->Experience = BoundBy<int32_t>(bval: Info->Experience + change, lbound: 0, rbound: MaxExperience);
1498
1499 // Promotion check
1500 if (Info->Experience < MaxExperience)
1501 if (Info->Experience >= Game.Rank.Experience(iRank: Info->Rank + 1))
1502 Promote(torank: Info->Rank + 1, fForceRankName: false);
1503}
1504
1505bool C4Object::Exit(int32_t iX, int32_t iY, int32_t iR, C4Fixed iXDir, C4Fixed iYDir, C4Fixed iRDir, bool fCalls)
1506{
1507 // 1. Exit the current container.
1508 // 2. Update Contents of container object and set Contained to nullptr.
1509 // 3. Set offset position/motion if desired.
1510 // 4. Call Ejection for container and Departure for object.
1511
1512 // Not contained
1513 C4Object *pContainer = Contained;
1514 if (!pContainer) return false;
1515 // Remove object from container
1516 pContainer->Contents.Remove(pObj: this);
1517 pContainer->UpdateMass();
1518 pContainer->SetOCF();
1519 // No container
1520 Contained = nullptr;
1521 // Position/motion
1522 BoundsCheck(ctcox&: iX, ctcoy&: iY);
1523 x = iX; y = iY; r = iR;
1524 fix_x = itofix(x); fix_y = itofix(x: y); fix_r = itofix(x: r);
1525 xdir = iXDir; ydir = iYDir; rdir = iRDir;
1526 // Misc updates
1527 Mobile = 1;
1528 InLiquid = 0;
1529 CloseMenu(fForce: true);
1530 UpdateFace(bUpdateShape: true);
1531 SetOCF();
1532 // Engine calls
1533 if (fCalls) pContainer->Call(PSF_Ejection, pPars: {C4VObj(pObj: this)});
1534 if (fCalls) Call(PSF_Departure, pPars: {C4VObj(pObj: pContainer)});
1535 // Success (if the obj wasn't "re-entered" by script)
1536 return !Contained;
1537}
1538
1539bool C4Object::Enter(C4Object *pTarget, bool fCalls, bool fCopyMotion, bool *pfRejectCollect)
1540{
1541 // 0. Query entrance and collection
1542 // 1. Exit if contained.
1543 // 2. Set new container.
1544 // 3. Update Contents and mass of the new container.
1545 // 4. Call collection for container
1546 // 5. Call entrance for object.
1547
1548 // No target or target is self
1549 if (!pTarget || (pTarget == this)) return false;
1550 // check if entrance is allowed
1551 if (Call(PSF_RejectEntrance, pPars: {C4VObj(pObj: pTarget)})) return false;
1552 // check if we end up in an endless container-recursion
1553 for (C4Object *pCnt = pTarget->Contained; pCnt; pCnt = pCnt->Contained)
1554 if (pCnt == this) return false;
1555 // Check RejectCollect, if desired
1556 if (pfRejectCollect)
1557 {
1558 if (pTarget->Call(PSF_RejectCollection, pPars: {C4VID(idVal: Def->id), C4VObj(pObj: this)}))
1559 {
1560 *pfRejectCollect = true;
1561 return false;
1562 }
1563 *pfRejectCollect = false;
1564 }
1565 // Exit if contained
1566 if (Contained) if (!Exit(iX: x, iY: y)) return false;
1567 if (Contained || !Status || !pTarget->Status) return false;
1568 // Failsafe updates
1569 CloseMenu(fForce: true);
1570 SetOCF();
1571 // Set container
1572 Contained = pTarget;
1573 // Enter
1574 if (!Contained->Contents.Add(nObj: this, eSort: C4ObjectList::stContents))
1575 {
1576 Contained = nullptr;
1577 return false;
1578 }
1579 // Assume that the new container controls this object, if it cannot control itself (i.e.: Alive)
1580 // So it can be traced back who caused the damage, if a projectile hits its target
1581 if (!(Alive && (Category & C4D_Living)))
1582 Controller = pTarget->Controller;
1583 // Misc updates
1584 // motion must be copied immediately, so the position will be correct when OCF is set, and
1585 // OCF_Available will be set for newly bought items, even if 50/50 is solid in the landscape
1586 // however, the motion must be preserved sometimes to keep flags like OCF_HitSpeed upon collection
1587 if (fCopyMotion)
1588 {
1589 // remove any solidmask before copying the motion...
1590 UpdateSolidMask(fRestoreAttachedObjects: false);
1591 CopyMotion(from: Contained);
1592 }
1593 SetOCF();
1594 UpdateFace(bUpdateShape: true);
1595 // Update container
1596 Contained->UpdateMass();
1597 Contained->SetOCF();
1598 // Collection call
1599 if (fCalls) pTarget->Call(PSF_Collection2, pPars: {C4VObj(pObj: this)});
1600 if (!Contained || !Contained->Status || !pTarget->Status) return true;
1601 // Entrance call
1602 if (fCalls) Call(PSF_Entrance, pPars: {C4VObj(pObj: Contained)});
1603 if (!Contained || !Contained->Status || !pTarget->Status) return true;
1604 // Base auto sell contents
1605 if (ValidPlr(plr: Contained->Base))
1606 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_AutoSellContents)
1607 Contained->AutoSellContents();
1608 // Success
1609 return true;
1610}
1611
1612void C4Object::Fling(C4Fixed txdir, C4Fixed tydir, bool fAddSpeed, int32_t iCausedBy)
1613{
1614 // trace indirect killers
1615 if (OCF & OCF_Alive) UpdatLastEnergyLossCause(iNewCausePlr: iCausedBy); else if (!Contained) Controller = iCausedBy;
1616 // fling object
1617 if (fAddSpeed) { txdir += xdir / 2; tydir += ydir / 2; }
1618 if (!ObjectActionTumble(cObj: this, dir: (txdir < 0), xdir: txdir, ydir: tydir))
1619 if (!ObjectActionJump(cObj: this, xdir: txdir, ydir: tydir, fByCom: false))
1620 {
1621 xdir = txdir; ydir = tydir;
1622 Mobile = 1;
1623 Action.t_attach &= ~CNAT_Bottom;
1624 }
1625}
1626
1627bool C4Object::ActivateEntrance(int32_t by_plr, C4Object *by_obj)
1628{
1629 // Hostile: no entrance to base
1630 if (Hostile(plr1: by_plr, plr2: Base) && (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_RejectEntrance))
1631 {
1632 if (ValidPlr(plr: Owner))
1633 {
1634 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_HOSTILENOENTRANCE, args: Game.Players.Get(iPlayer: Owner)->GetName()).c_str(), pTarget: this);
1635 }
1636 return false;
1637 }
1638 // Try entrance activation
1639 if (OCF & OCF_Entrance)
1640 if (Call(PSF_ActivateEntrance, pPars: {C4VObj(pObj: by_obj)}))
1641 return true;
1642 // Failure
1643 return false;
1644}
1645
1646void C4Object::Explode(int32_t iLevel, C4ID idEffect, const char *szEffect)
1647{
1648 // Called by FnExplode only
1649 C4Object *pContainer = Contained;
1650 int32_t iCausedBy = Controller;
1651 AssignRemoval();
1652 Explosion(tx: x, ty: y, level: iLevel, inobj: pContainer, iCausedBy, pByObj: this, idEffect, szEffect);
1653}
1654
1655bool C4Object::Build(int32_t iLevel, C4Object *pBuilder)
1656{
1657 int32_t cnt;
1658 C4ID NeededMaterial;
1659 int32_t NeededMaterialCount = 0;
1660 C4Object *pMaterial;
1661
1662 // Invalid or complete: no build
1663 if (!Status || !Def || (Con >= FullCon)) return false;
1664
1665 // Material check (if rule set or any other than structure or castle-part)
1666 bool fNeedMaterial = (Game.Rules & C4RULE_ConstructionNeedsMaterial) || !(Category & (C4D_Structure | C4D_StaticBack));
1667 if (fNeedMaterial)
1668 {
1669 // Determine needed components (may be overloaded)
1670 C4IDList NeededComponents;
1671 Def->GetComponents(pOutList: &NeededComponents, pObjInstance: nullptr, pBuilder);
1672
1673 // Grab any needed components from builder
1674 C4ID idMat;
1675 for (cnt = 0; idMat = NeededComponents.GetID(index: cnt); cnt++)
1676 if (Component.GetIDCount(id: idMat) < NeededComponents.GetCount(index: cnt))
1677 if ((pMaterial = pBuilder->Contents.Find(id: idMat)))
1678 if (!pMaterial->OnFire) if (pMaterial->OCF & OCF_FullCon)
1679 {
1680 Component.SetIDCount(id: idMat, count: Component.GetIDCount(id: idMat) + 1, addNewID: true);
1681 pBuilder->Contents.Remove(pObj: pMaterial);
1682 pMaterial->AssignRemoval();
1683 }
1684 // Grab any needed components from container
1685 if (Contained)
1686 for (cnt = 0; idMat = NeededComponents.GetID(index: cnt); cnt++)
1687 if (Component.GetIDCount(id: idMat) < NeededComponents.GetCount(index: cnt))
1688 if ((pMaterial = Contained->Contents.Find(id: idMat)))
1689 if (!pMaterial->OnFire) if (pMaterial->OCF & OCF_FullCon)
1690 {
1691 Component.SetIDCount(id: idMat, count: Component.GetIDCount(id: idMat) + 1, addNewID: true);
1692 Contained->Contents.Remove(pObj: pMaterial);
1693 pMaterial->AssignRemoval();
1694 }
1695 // Check for needed components at current con
1696 for (cnt = 0; idMat = NeededComponents.GetID(index: cnt); cnt++)
1697 if (NeededComponents.GetCount(index: cnt) != 0)
1698 if ((100 * Component.GetIDCount(id: idMat) / NeededComponents.GetCount(index: cnt)) < (100 * Con / FullCon))
1699 {
1700 NeededMaterial = NeededComponents.GetID(index: cnt);
1701 NeededMaterialCount = NeededComponents.GetCount(index: cnt) - Component.GetCount(index: cnt);
1702 break;
1703 }
1704 }
1705
1706 // Needs components
1707 if (NeededMaterialCount)
1708 {
1709 // BuildNeedsMaterial call to builder script...
1710 if (!pBuilder->Call(PSF_BuildNeedsMaterial, pPars: {C4VID(idVal: NeededMaterial), C4VInt(iVal: NeededMaterialCount)}))
1711 {
1712 // Builder is a crew member...
1713 if (pBuilder->OCF & OCF_CrewMember)
1714 // ...tell builder to acquire the material
1715 pBuilder->AddCommand(iCommand: C4CMD_Acquire, pTarget: nullptr, iTx: 0, iTy: 0, iUpdateInterval: 50, pTarget2: nullptr, fInitEvaluation: true, iData: NeededMaterial, fAppend: false, iRetries: 1);
1716 // ...game message if not overloaded
1717 Game.Messages.New(iType: C4GM_Target, Text: StdStrBuf{GetNeededMatStr(pBuilder).c_str()}, pTarget: pBuilder, iPlayer: pBuilder->Controller);
1718 }
1719 // Still in need: done/fail
1720 return false;
1721 }
1722
1723 // Do con (mass- and builder-relative)
1724 int32_t iBuildSpeed = 100; C4PhysicalInfo *pPhys;
1725 if (pBuilder) if (pPhys = pBuilder->GetPhysical())
1726 {
1727 iBuildSpeed = pPhys->CanConstruct;
1728 if (!iBuildSpeed)
1729 {
1730 // shouldn't even have gotten here. Looks like the Clonk lost the ability to build recently
1731 return false;
1732 }
1733 if (iBuildSpeed <= 1) iBuildSpeed = 100;
1734 }
1735 DoCon(iChange: iLevel * iBuildSpeed * 150 / (Def->Mass ? Def->Mass : 1), fInitial: false, fNoComponentChange: fNeedMaterial);
1736
1737 // TurnTo
1738 if (Def->BuildTurnTo != C4ID_None)
1739 ChangeDef(idNew: Def->BuildTurnTo);
1740
1741 // Repair
1742 Damage = 0;
1743
1744 // Done/success
1745 return true;
1746}
1747
1748bool C4Object::Chop(C4Object *pByObject)
1749{
1750 // Valid check
1751 if (!Status || !Def || Contained || !(OCF & OCF_Chop))
1752 return false;
1753 // Chop
1754 if (!Tick10) DoDamage(iChange: +10, iCausedBy: pByObject ? pByObject->Owner : NO_OWNER, C4FxCall_DmgChop);
1755 return true;
1756}
1757
1758bool C4Object::Push(C4Fixed txdir, C4Fixed dforce, bool fStraighten)
1759{
1760 // Valid check
1761 if (!Status || !Def || Contained || !(OCF & OCF_Grab)) return false;
1762 // Grabbing okay, no pushing
1763 if (Def->Grab == 2) return true;
1764 // Mobilization check (pre-mobilization zero)
1765 if (!Mobile)
1766 {
1767 xdir = ydir = Fix0; fix_x = itofix(x); fix_y = itofix(x: y);
1768 }
1769 // General pushing force vs. object mass
1770 dforce = dforce * 100 / Mass;
1771 // Set dir
1772 if (xdir < 0) SetDir(DIR_Left);
1773 if (xdir > 0) SetDir(DIR_Right);
1774 // Work towards txdir
1775 if (Abs(val: xdir - txdir) <= dforce) // Close-enough-set
1776 {
1777 xdir = txdir;
1778 }
1779 else // Work towards
1780 {
1781 if (xdir < txdir) xdir += dforce;
1782 if (xdir > txdir) xdir -= dforce;
1783 }
1784 // Straighten
1785 if (fStraighten)
1786 if (Inside<int32_t>(ival: r, lbound: -StableRange, rbound: +StableRange))
1787 {
1788 rdir = 0; // cheap way out
1789 }
1790 else
1791 {
1792 if (r > 0) { if (rdir > -RotateAccel) rdir -= dforce; }
1793 else { if (rdir < +RotateAccel) rdir += dforce; }
1794 }
1795
1796 // Mobilization check
1797 if (!!xdir || !!ydir || !!rdir) Mobile = 1;
1798
1799 // Stuck check
1800 if (!Tick35) if (txdir) if (!Def->NoHorizontalMove)
1801 if (ContactCheck(atx: x, aty: y)) // Resets t_contact
1802 {
1803 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_STUCK, args: GetName()).c_str(), pTarget: this);
1804 Call(PSF_Stuck);
1805 }
1806
1807 return true;
1808}
1809
1810bool C4Object::Lift(C4Fixed tydir, C4Fixed dforce)
1811{
1812 // Valid check
1813 if (!Status || !Def || Contained) return false;
1814 // Mobilization check
1815 if (!Mobile)
1816 {
1817 xdir = ydir = Fix0; fix_x = itofix(x); fix_y = itofix(x: y); Mobile = 1;
1818 }
1819 // General pushing force vs. object mass
1820 dforce = dforce * 100 / Mass;
1821 // If close enough, set tydir
1822 if (Abs(val: tydir - ydir) <= Abs(val: dforce))
1823 ydir = tydir;
1824 else // Work towards tydir
1825 {
1826 if (ydir < tydir) ydir += dforce;
1827 if (ydir > tydir) ydir -= dforce;
1828 }
1829 // Stuck check
1830 if (tydir != -GravAccel)
1831 if (ContactCheck(atx: x, aty: y)) // Resets t_contact
1832 {
1833 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_STUCK, args: GetName()).c_str(), pTarget: this);
1834 Call(PSF_Stuck);
1835 }
1836 return true;
1837}
1838
1839C4Object *C4Object::CreateContents(C4ID n_id)
1840{
1841 C4Object *nobj;
1842 if (!(nobj = Game.CreateObject(type: n_id, pCreator: this, owner: Owner))) return nullptr;
1843 if (!nobj->Enter(pTarget: this)) { nobj->AssignRemoval(); return nullptr; }
1844 return nobj;
1845}
1846
1847bool C4Object::CreateContentsByList(C4IDList &idlist)
1848{
1849 int32_t cnt, cnt2;
1850 for (cnt = 0; idlist.GetID(index: cnt); cnt++)
1851 for (cnt2 = 0; cnt2 < idlist.GetCount(index: cnt); cnt2++)
1852 if (!CreateContents(n_id: idlist.GetID(index: cnt)))
1853 return false;
1854 return true;
1855}
1856
1857bool C4Object::ActivateMenu(int32_t iMenu, int32_t iMenuSelect,
1858 int32_t iMenuData, int32_t iMenuPosition,
1859 C4Object *pTarget)
1860{
1861 // Variables
1862 C4FacetExSurface fctSymbol;
1863 std::string caption;
1864 std::string command;
1865 int32_t cnt, iCount;
1866 C4Def *pDef;
1867 C4Player *pPlayer;
1868 C4IDList ListItems;
1869 // Close any other menu
1870 if (Menu && Menu->IsActive()) if (!Menu->TryClose(fOK: true, fControl: false)) return false;
1871 // Create menu
1872 if (!Menu) Menu = new C4ObjectMenu; else Menu->ClearItems(fResetSelection: true);
1873 // Open menu
1874 switch (iMenu)
1875 {
1876 case C4MN_Activate:
1877 // No target specified: use own container as target
1878 if (!pTarget) if (!(pTarget = Contained)) break;
1879 // Opening contents menu blocked by RejectContents
1880 if (pTarget->Call(PSF_RejectContents)) return false;
1881 // Create symbol
1882 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize);
1883 pTarget->Def->Draw(cgo&: fctSymbol, fSelected: false, iColor: pTarget->Color, pObj: pTarget);
1884 caption = LoadResStr(id: C4ResStrTableKey::IDS_OBJ_EMPTY, args: pTarget->GetName());
1885 // Init
1886 Menu->Init(fctSymbol, szEmpty: caption.c_str(), pObject: this, iExtra: C4MN_Extra_None, iExtraData: 0, iId: iMenu);
1887 Menu->SetPermanent(true);
1888 Menu->SetRefillObject(pTarget);
1889 // Success
1890 return true;
1891
1892 case C4MN_Buy:
1893 // No target specified: container is base
1894 if (!pTarget) if (!(pTarget = Contained)) break;
1895 // Create symbol
1896 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize);
1897 DrawMenuSymbol(iMenu: C4MN_Buy, cgo&: fctSymbol, iOwner: pTarget->Owner, cObj: pTarget);
1898 // Init menu
1899 Menu->Init(fctSymbol, szEmpty: LoadResStr(id: C4ResStrTableKey::IDS_PLR_NOBUY), pObject: this, iExtra: C4MN_Extra_Value, iExtraData: 0, iId: iMenu);
1900 Menu->SetPermanent(true);
1901 Menu->SetRefillObject(pTarget);
1902 // Success
1903 return true;
1904
1905 case C4MN_Sell:
1906 // No target specified: container is base
1907 if (!pTarget) if (!(pTarget = Contained)) break;
1908 // Create symbol & init
1909 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize);
1910 DrawMenuSymbol(iMenu: C4MN_Sell, cgo&: fctSymbol, iOwner: pTarget->Owner, cObj: pTarget);
1911 caption = LoadResStr(id: C4ResStrTableKey::IDS_OBJ_EMPTY, args: pTarget->GetName());
1912 Menu->Init(fctSymbol, szEmpty: caption.c_str(), pObject: this, iExtra: C4MN_Extra_Value, iExtraData: 0, iId: iMenu);
1913 Menu->SetPermanent(true);
1914 Menu->SetRefillObject(pTarget);
1915 // Success
1916 return true;
1917
1918 case C4MN_Get:
1919 case C4MN_Contents:
1920 // No target specified
1921 if (!pTarget) break;
1922 // Opening contents menu blocked by RejectContents
1923 if (pTarget->Call(PSF_RejectContents)) return false;
1924 // Create symbol & init
1925 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize);
1926 pTarget->Def->Draw(cgo&: fctSymbol, fSelected: false, iColor: pTarget->Color, pObj: pTarget);
1927 caption = LoadResStr(id: C4ResStrTableKey::IDS_OBJ_EMPTY, args: pTarget->GetName());
1928 Menu->Init(fctSymbol, szEmpty: caption.c_str(), pObject: this, iExtra: C4MN_Extra_None, iExtraData: 0, iId: iMenu);
1929 Menu->SetPermanent(true);
1930 Menu->SetRefillObject(pTarget);
1931 // Success
1932 return true;
1933
1934 case C4MN_Context:
1935 {
1936 // Target by parameter
1937 if (!pTarget) break;
1938
1939 // Create symbol & init menu
1940 pPlayer = Game.Players.Get(iPlayer: pTarget->Owner);
1941 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize);
1942 pTarget->Def->Draw(cgo&: fctSymbol, fSelected: false, iColor: pTarget->Color, pObj: pTarget);
1943 Menu->Init(fctSymbol, szEmpty: pTarget->GetName(), pObject: this, iExtra: C4MN_Extra_None, iExtraData: 0, iId: iMenu, iStyle: C4MN_Style_Context);
1944
1945 Menu->SetPermanent(iMenuData);
1946 Menu->SetRefillObject(pTarget);
1947
1948 // Preselect
1949 Menu->SetSelection(iSelection: iMenuSelect, fAdjustPosition: false, fDoCalls: true);
1950 Menu->SetPosition(iMenuPosition);
1951 }
1952 // Success
1953 return true;
1954
1955 case C4MN_Construction:
1956 // Check valid player
1957 if (!(pPlayer = Game.Players.Get(iPlayer: Owner))) break;
1958 // Create symbol
1959 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize);
1960 DrawMenuSymbol(iMenu: C4MN_Construction, cgo&: fctSymbol, iOwner: -1, cObj: nullptr);
1961 // Init menu
1962 Menu->Init(fctSymbol, szEmpty: LoadResStr(id: C4ResStrTableKey::IDS_PLR_NOBKNOW, args: pPlayer->GetName()).c_str(), pObject: this, iExtra: C4MN_Extra_Components, iExtraData: 0, iId: iMenu);
1963 // Add player's structure build knowledge
1964 for (cnt = 0; pDef = C4Id2Def(id: pPlayer->Knowledge.GetID(rDefs&: Game.Defs, dwCategory: C4D_Structure, index: cnt, ipCount: &iCount)); cnt++)
1965 {
1966 // Caption
1967 caption = LoadResStr(id: C4ResStrTableKey::IDS_MENU_CONSTRUCT, args: pDef->GetName());
1968 // Picture
1969 pDef->Picture2Facet(cgo&: fctSymbol);
1970 // Command
1971 command = std::format(fmt: "SetCommand(this, \"Construct\",,0,0,,{})", args: C4IdText(id: pDef->id));
1972 // Add menu item
1973 Menu->AddRefSym(szCaption: caption.c_str(), fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pDef->GetDesc(), idID: pDef->id);
1974 }
1975 // Preselect
1976 Menu->SetSelection(iSelection: iMenuSelect, fAdjustPosition: false, fDoCalls: true);
1977 Menu->SetPosition(iMenuPosition);
1978 // Success
1979 return true;
1980
1981 case C4MN_Info:
1982 // Target by parameter
1983 if (!pTarget) break;
1984 pPlayer = Game.Players.Get(iPlayer: pTarget->Owner);
1985 // Create symbol & init menu
1986 fctSymbol.Create(iWdt: C4SymbolSize, iHgt: C4SymbolSize); GfxR->fctOKCancel.Draw(cgo&: fctSymbol, fAspect: true, iPhaseX: 0, iPhaseY: 1);
1987 Menu->Init(fctSymbol, szEmpty: pTarget->GetName(), pObject: this, iExtra: C4MN_Extra_None, iExtraData: 0, iId: iMenu, iStyle: C4MN_Style_Info);
1988 Menu->SetPermanent(true);
1989 Menu->SetAlignment(C4MN_Align_Free);
1990 C4Viewport *pViewport = Game.GraphicsSystem.GetViewport(iPlayer: Owner); // Hackhackhack!!!
1991 if (pViewport) Menu->SetLocation(iX: pTarget->x + pTarget->Shape.x + pTarget->Shape.Wdt + 10 - pViewport->ViewX, iY: pTarget->y + pTarget->Shape.y - pViewport->ViewY);
1992
1993 const auto pictureSize = Application.GetScale() * C4PictureSize;
1994
1995 // Add info item
1996 fctSymbol.Create(iWdt: pictureSize, iHgt: pictureSize); pTarget->Def->Draw(cgo&: fctSymbol, fSelected: false, iColor: pTarget->Color, pObj: pTarget);
1997 Menu->Add(szCaption: pTarget->GetName(), fctSymbol, szCommand: "", iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pTarget->GetInfoString().getData());
1998 fctSymbol.Default();
1999 // Success
2000 return true;
2001 }
2002 // Invalid menu identification
2003 CloseMenu(fForce: true);
2004 return false;
2005}
2006
2007bool C4Object::CloseMenu(bool fForce)
2008{
2009 if (Menu)
2010 {
2011 if (Menu->IsActive()) if (!Menu->TryClose(fOK: fForce, fControl: false)) return false;
2012 if (!Menu->IsCloseQuerying()) { delete Menu; Menu = nullptr; } // protect menu deletion from recursive menu operation calls
2013 }
2014 return true;
2015}
2016
2017void C4Object::AutoContextMenu(int32_t iMenuSelect)
2018{
2019 // Auto Context Menu - the "new structure menus"
2020 // No command set and no menu open
2021 if (!Command && !(Menu && Menu->IsActive()))
2022 // In a container with AutoContextMenu
2023 if (Contained && Contained->Def->AutoContextMenu)
2024 // Crew members only
2025 if (OCF & OCF_CrewMember)
2026 {
2027 // Player has AutoContextMenus enabled
2028 C4Player *pPlayer = Game.Players.Get(iPlayer: Controller);
2029 if (pPlayer && pPlayer->AutoContextMenu)
2030 {
2031 // Open context menu for structure
2032 ActivateMenu(iMenu: C4MN_Context, iMenuSelect, iMenuData: 1, iMenuPosition: 0, pTarget: Contained);
2033 // Closing the menu exits the building (all selected clonks)
2034 Menu->SetCloseCommand("PlayerObjectCommand(GetOwner(), \"Exit\") && ExecuteCommand()");
2035 }
2036 }
2037}
2038
2039uint8_t C4Object::GetArea(int32_t &aX, int32_t &aY, int32_t &aWdt, int32_t &aHgt)
2040{
2041 if (!Status || !Def) return 0;
2042 aX = x + Shape.x; aY = y + Shape.y;
2043 aWdt = Shape.Wdt; aHgt = Shape.Hgt;
2044 return 1;
2045}
2046
2047uint8_t C4Object::GetEntranceArea(int32_t &aX, int32_t &aY, int32_t &aWdt, int32_t &aHgt)
2048{
2049 if (!Status || !Def) return 0;
2050 // Return actual entrance
2051 if (OCF & OCF_Entrance)
2052 {
2053 aX = x + Def->Entrance.x;
2054 aY = y + Def->Entrance.y;
2055 aWdt = Def->Entrance.Wdt;
2056 aHgt = Def->Entrance.Hgt;
2057 }
2058 // Return object center
2059 else
2060 {
2061 aX = x; aY = y;
2062 aWdt = 0; aHgt = 0;
2063 }
2064 // Done
2065 return 1;
2066}
2067
2068C4Fixed C4Object::GetSpeed()
2069{
2070 C4Fixed cobjspd = Fix0;
2071 if (xdir < 0) cobjspd -= xdir; else cobjspd += xdir;
2072 if (ydir < 0) cobjspd -= ydir; else cobjspd += ydir;
2073 return cobjspd;
2074}
2075
2076const char *C4Object::GetName()
2077{
2078 if (!CustomName.empty()) return CustomName.c_str();
2079 if (Info) return Info->Name;
2080 return Def->Name.getData();
2081}
2082
2083void C4Object::SetName(const char *NewName)
2084{
2085 if (!NewName)
2086 CustomName.clear();
2087 else
2088 CustomName = NewName;
2089}
2090
2091int32_t C4Object::GetValue(C4Object *pInBase, int32_t iForPlayer)
2092{
2093 int32_t iValue;
2094
2095 // value by script?
2096 if (C4AulScriptFunc *f = Def->Script.SFn_CalcValue)
2097 iValue = f->Exec(pObj: this, pPars: {C4VObj(pObj: pInBase), C4VInt(iVal: iForPlayer)}).getInt();
2098 else
2099 {
2100 // get value of def
2101 // Caution: Do not pass pInBase here, because the def base value is to be queried
2102 // - and not the value if you had to buy the object in this particular base
2103 iValue = Def->GetValue(pInBase: nullptr, iBuyPlayer: iForPlayer);
2104 }
2105 // Con percentage
2106 iValue = iValue * Con / FullCon;
2107 // value adjustments buy base function
2108 if (pInBase)
2109 {
2110 C4AulFunc *pFn;
2111 if (pFn = pInBase->Def->Script.GetSFunc(PSF_CalcSellValue, AccNeeded: AA_PROTECTED))
2112 iValue = pFn->Exec(pObj: pInBase, pPars: {C4VObj(pObj: this), C4VInt(iVal: iValue)}).getInt();
2113 }
2114 // Return value
2115 return iValue;
2116}
2117
2118C4PhysicalInfo *C4Object::GetPhysical(bool fPermanent)
2119{
2120 // Temporary physical
2121 if (PhysicalTemporary && !fPermanent) return &TemporaryPhysical;
2122 // Info physical: Available only if there's an info and it should be used
2123 if (Info)
2124 if (!Game.Parameters.UseFairCrew)
2125 return &(Info->Physical);
2126 else if (Info->pDef)
2127 return Info->pDef->GetFairCrewPhysicals();
2128 else
2129 // shouldn't really happen, but who knows.
2130 // Maybe some time it will be possible to have crew infos that aren't tied to a specific definition
2131 return Def->GetFairCrewPhysicals();
2132 // Definition physical
2133 return &(Def->Physical);
2134}
2135
2136bool C4Object::TrainPhysical(C4PhysicalInfo::Offset mpiOffset, int32_t iTrainBy, int32_t iMaxTrain)
2137{
2138 int i = 0;
2139 // Train temp
2140 if (PhysicalTemporary) { TemporaryPhysical.Train(mpiOffset, iTrainBy, iMaxTrain); ++i; }
2141 // train permanent, if existent
2142 // this also trains if fair crew is used!
2143 if (Info) { Info->Physical.Train(mpiOffset, iTrainBy, iMaxTrain); ++i; }
2144 // return whether anything was trained
2145 return !!i;
2146}
2147
2148bool C4Object::Promote(int32_t torank, bool fForceRankName)
2149{
2150 if (!Info) return false;
2151 // get rank system
2152 C4Def *pUseDef = C4Id2Def(id: Info->id);
2153 C4RankSystem *pRankSys;
2154 if (pUseDef && pUseDef->pRankNames)
2155 pRankSys = pUseDef->pRankNames;
2156 else
2157 pRankSys = &Game.Rank;
2158 // always promote info
2159 Info->Promote(iRank: torank, rRanks&: *pRankSys, fForceRankName);
2160 // silent update?
2161 if (!pRankSys->GetRankName(iRank: torank, fReturnLastIfOver: false)) return false;
2162 GameMsgObject(szText: LoadResStr(id: C4ResStrTableKey::IDS_OBJ_PROMOTION, args: GetName(), args: Info->sRankName.getData()).c_str(), pTarget: this);
2163 StartSoundEffect(name: "Trumpet", loop: 0, volume: 100, obj: this);
2164 return true;
2165}
2166
2167void C4Object::ClearPointers(C4Object *pObj)
2168{
2169 // effects
2170 if (pEffects) pEffects->ClearPointers(pObj);
2171 // contents/contained: not necessary, because it's done in AssignRemoval and StatusDeactivate
2172 // Action targets
2173 if (Action.Target == pObj) Action.Target = nullptr;
2174 if (Action.Target2 == pObj) Action.Target2 = nullptr;
2175 // Commands
2176 C4Command *cCom;
2177 for (cCom = Command; cCom; cCom = cCom->Next)
2178 cCom->ClearPointers(pObj);
2179 // Menu
2180 if (Menu) Menu->ClearPointers(pObj);
2181 // Layer
2182 if (pLayer == pObj) pLayer = nullptr;
2183 // gfx overlays
2184 if (pGfxOverlay)
2185 {
2186 C4GraphicsOverlay *pNextGfxOvrl = pGfxOverlay, *pGfxOvrl;
2187 while (pGfxOvrl = pNextGfxOvrl)
2188 {
2189 pNextGfxOvrl = pGfxOvrl->GetNext();
2190 if (pGfxOvrl->GetOverlayObject() == pObj)
2191 // overlay relying on deleted object: Delete!
2192 RemoveGraphicsOverlay(iOverlayID: pGfxOvrl->GetID());
2193 }
2194 }
2195}
2196
2197C4Value C4Object::Call(const char *szFunctionCall, const C4AulParSet &pPars, bool fPassError, bool convertNilToIntBool)
2198{
2199 if (!Status || !Def || !szFunctionCall[0]) return C4VNull;
2200 return Def->Script.ObjectCall(pCaller: this, pObj: this, szFunction: szFunctionCall, pPars, fPassError, convertNilToIntBool);
2201}
2202
2203bool C4Object::SetPhase(int32_t iPhase)
2204{
2205 if (Action.Act <= ActIdle) return false;
2206 C4ActionDef *actdef = &(Def->ActMap[Action.Act]);
2207 Action.Phase = BoundBy<int32_t>(bval: iPhase, lbound: 0, rbound: actdef->Length);
2208 return true;
2209}
2210
2211void C4Object::Draw(C4FacetEx &cgo, int32_t iByPlayer, DrawMode eDrawMode)
2212{
2213 C4Facet ccgo;
2214
2215 // Status
2216 if (!Status || !Def) return;
2217
2218 // visible?
2219 if (Visibility || pLayer) if (!IsVisible(iForPlr: iByPlayer, fAsOverlay: !!eDrawMode)) return;
2220
2221 // Line
2222 if (Def->Line) { DrawLine(cgo); return; }
2223
2224 // background particles (bounds not checked)
2225 if (BackParticles && !Contained && eDrawMode != ODM_BaseOnly) BackParticles.Draw(cgo, pObj: this);
2226
2227 // Object output position
2228 int32_t cotx = cgo.TargetX, coty = cgo.TargetY; if (eDrawMode != ODM_Overlay) TargetPos(riTx&: cotx, riTy&: coty, fctViewport: cgo);
2229 int32_t cox = x + Shape.x - cotx, coy = y + Shape.y - coty;
2230
2231 bool fYStretchObject = false;
2232 if (Action.Act > ActIdle)
2233 if (Def->ActMap[Action.Act].FacetTargetStretch)
2234 fYStretchObject = true;
2235
2236 // Set audibility - only for parallax objects, others calculate it on demand
2237 if (!eDrawMode && (Category & C4D_Parallax)) SetAudibilityAt(cgo, iX: x, iY: y);
2238
2239 // Output boundary
2240 if (!fYStretchObject && !eDrawMode)
2241 if (Action.Act > ActIdle && !r && !Def->ActMap[Action.Act].FacetBase && Con <= FullCon)
2242 {
2243 // active
2244 if (!Inside(ival: cox + Action.FacetX, lbound: 1 - Action.Facet.Wdt, rbound: cgo.Wdt)
2245 || (!Inside(ival: coy + Action.FacetY, lbound: 1 - Action.Facet.Hgt, rbound: cgo.Hgt)))
2246 {
2247 if (FrontParticles && !Contained) FrontParticles.Draw(cgo, pObj: this); return;
2248 }
2249 }
2250 else
2251 // idle
2252 if (!Inside(ival: cox, lbound: 1 - Shape.Wdt, rbound: cgo.Wdt)
2253 || (!Inside(ival: coy, lbound: 1 - Shape.Hgt, rbound: cgo.Hgt)))
2254 {
2255 if (FrontParticles && !Contained) FrontParticles.Draw(cgo, pObj: this); return;
2256 }
2257
2258 // ensure correct color is set
2259 if (GetGraphics()->BitmapClr) GetGraphics()->BitmapClr->SetClr(Color);
2260
2261 // Debug Display
2262 if (Game.GraphicsSystem.ShowCommand && !eDrawMode)
2263 {
2264 C4Command *pCom;
2265 int32_t ccx = x, ccy = y;
2266 int32_t x1, y1, x2, y2;
2267 std::string command;
2268 std::string commands;
2269 int32_t iMoveTos = 0;
2270 for (pCom = Command; pCom; pCom = pCom->Next)
2271 {
2272 switch (pCom->Command)
2273 {
2274 case C4CMD_MoveTo:
2275 // Angle
2276 int32_t iAngle; iAngle = Angle(iX1: ccx, iY1: ccy, iX2: pCom->Tx._getInt(), iY2: pCom->Ty); while (iAngle > 180) iAngle -= 360;
2277 // Path
2278 x1 = ccx - cotx; y1 = ccy - coty;
2279 x2 = pCom->Tx._getInt() - cotx; y2 = pCom->Ty - coty;
2280 Application.DDraw->DrawLine(sfcTarget: cgo.Surface, x1: cgo.X + x1, y1: cgo.Y + y1, x2: cgo.X + x2, y2: cgo.Y + y2, byCol: CRed);
2281 Application.DDraw->DrawFrame(sfcDest: cgo.Surface, x1: cgo.X + x2 - 1, y1: cgo.Y + y2 - 1, x2: cgo.X + x2 + 1, y2: cgo.Y + y2 + 1, col: CRed);
2282 if (x1 > x2) std::swap(a&: x1, b&: x2); if (y1 > y2) std::swap(a&: y1, b&: y2);
2283 ccx = pCom->Tx._getInt(); ccy = pCom->Ty;
2284 // Message
2285 iMoveTos++;
2286 command.clear();
2287 break;
2288 case C4CMD_Put:
2289 command = std::format(fmt: "{} {} to {}", args: CommandName(iCommand: pCom->Command), args: pCom->Target2 ? pCom->Target2->GetName() : pCom->Data ? C4IdText(id: pCom->Data) : "Content", args: pCom->Target ? pCom->Target->GetName() : "");
2290 break;
2291 case C4CMD_Buy: case C4CMD_Sell:
2292 command = std::format(fmt: "{} {} at {}", args: CommandName(iCommand: pCom->Command), args: C4IdText(id: pCom->Data), args: pCom->Target ? pCom->Target->GetName() : "closest base");
2293 break;
2294 case C4CMD_Acquire:
2295 command = std::format(fmt: "{} {}", args: CommandName(iCommand: pCom->Command), args: pCom->Target ? pCom->Target->GetName() : "");
2296 break;
2297 case C4CMD_Call:
2298 command = std::format(fmt: "{} {} in {}", args: CommandName(iCommand: pCom->Command), args&: pCom->Text, args: pCom->Target ? pCom->Target->GetName() : "(null)");
2299 break;
2300 case C4CMD_Construct:
2301 C4Def *pDef; pDef = C4Id2Def(id: pCom->Data);
2302 command = std::format(fmt: "{} {}", args: CommandName(iCommand: pCom->Command), args: pDef ? pDef->GetName() : "");
2303 break;
2304 case C4CMD_None:
2305 command.clear();
2306 break;
2307 case C4CMD_Transfer:
2308 // Path
2309 x1 = ccx - cotx; y1 = ccy - coty;
2310 x2 = pCom->Tx._getInt() - cotx; y2 = pCom->Ty - coty;
2311 Application.DDraw->DrawLine(sfcTarget: cgo.Surface, x1: cgo.X + x1, y1: cgo.Y + y1, x2: cgo.X + x2, y2: cgo.Y + y2, byCol: CGreen);
2312 Application.DDraw->DrawFrame(sfcDest: cgo.Surface, x1: cgo.X + x2 - 1, y1: cgo.Y + y2 - 1, x2: cgo.X + x2 + 1, y2: cgo.Y + y2 + 1, col: CGreen);
2313 if (x1 > x2) std::swap(a&: x1, b&: x2); if (y1 > y2) std::swap(a&: y1, b&: y2);
2314 ccx = pCom->Tx._getInt(); ccy = pCom->Ty;
2315 // Message
2316 command = std::format(fmt: "{} {}", args: CommandName(iCommand: pCom->Command), args: pCom->Target ? pCom->Target->GetName() : "");
2317 break;
2318 default:
2319 command = std::format(fmt: "{} {}", args: CommandName(iCommand: pCom->Command), args: pCom->Target ? pCom->Target->GetName() : "");
2320 break;
2321 }
2322 // Compose command stack message
2323 if (!command.empty())
2324 {
2325 // End MoveTo stack first
2326 if (iMoveTos) { commands += std::format(fmt: "|{}x MoveTo", args: std::exchange(obj&: iMoveTos, new_val: 0)); }
2327 // Current message
2328 commands += '|';
2329 if (pCom->Finished) commands += "<i>";
2330 commands += std::move(command);
2331 if (pCom->Finished) commands += "</i>";
2332 }
2333 }
2334 // Open MoveTo stack
2335 if (iMoveTos) { commands += std::format(fmt: "|{}x MoveTo", args: std::exchange(obj&: iMoveTos, new_val: 0)); }
2336 // Draw message
2337 int32_t cmwdt, cmhgt; Game.GraphicsResource.FontRegular.GetTextExtent(szText: commands.c_str(), rsx&: cmwdt, rsy&: cmhgt, fCheckMarkup: true);
2338 Application.DDraw->TextOut(szText: commands.c_str(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + cox - Shape.x, iTy: cgo.Y + coy - 10 - cmhgt, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ACenter);
2339 }
2340
2341 // Don't draw (show solidmask)
2342 if (Game.GraphicsSystem.ShowSolidMask)
2343 if (SolidMask.Wdt)
2344 {
2345 // no need to draw it, because the 8bit-surface will be shown
2346 return;
2347 }
2348
2349 // Contained check
2350 if (Contained && !eDrawMode) return;
2351
2352 // Visibility inside FoW
2353 bool fOldClrModEnabled = !!(Category & C4D_IgnoreFoW);
2354 if (fOldClrModEnabled)
2355 {
2356 fOldClrModEnabled = lpDDraw->GetClrModMapEnabled();
2357 lpDDraw->SetClrModMapEnabled(false);
2358 }
2359
2360 // Fire facet - always draw, even if particles are drawn as well
2361 if (OnFire) if (eDrawMode != ODM_BaseOnly)
2362 {
2363 C4Facet fgo;
2364 // Straight: Full Shape.Rect on fire
2365 if (r == 0)
2366 {
2367 fgo.Set(nsfc: cgo.Surface, nx: cgo.X + cox, ny: cgo.Y + coy,
2368 nwdt: Shape.Wdt, nhgt: Shape.Hgt - Shape.FireTop);
2369 }
2370 // Rotated: Reduced fire rect
2371 else
2372 {
2373 C4Rect fr;
2374 Shape.GetVertexOutline(rRect&: fr);
2375 fgo.Set(nsfc: cgo.Surface,
2376 nx: cgo.X + cox - Shape.x + fr.x,
2377 ny: cgo.Y + coy - Shape.y + fr.y,
2378 nwdt: fr.Wdt, nhgt: fr.Hgt);
2379 }
2380 Game.GraphicsResource.fctFire.Draw(cgo&: fgo, fAspect: false, iPhaseX: FirePhase);
2381 }
2382
2383 // color modulation (including construction sign...)
2384 if (ColorMod || BlitMode) if (!eDrawMode) PrepareDrawing();
2385
2386 // Not active or rotated: BaseFace only
2387 if ((Action.Act <= ActIdle))
2388 {
2389 DrawFace(cgo, cgoX: cgo.X + cox, cgoY: cgo.Y + coy);
2390 }
2391
2392 // Active
2393 else
2394 {
2395 // FacetBase
2396 if (Def->ActMap[Action.Act].FacetBase)
2397 DrawFace(cgo, cgoX: cgo.X + cox, cgoY: cgo.Y + coy, iPhaseX: 0, iPhaseY: Action.DrawDir);
2398
2399 // Facet
2400 if (Action.Facet.Surface)
2401 {
2402 // Special: stretched action facet
2403 if (Def->ActMap[Action.Act].FacetTargetStretch)
2404 {
2405 if (Action.Target)
2406 Action.Facet.DrawX(sfcTarget: cgo.Surface,
2407 iX: cgo.X + cox + Action.FacetX,
2408 iY: cgo.Y + coy + Action.FacetY,
2409 iWdt: Action.Facet.Wdt,
2410 iHgt: (Action.Target->y + Action.Target->Shape.y) - (y + Shape.y + Action.FacetY),
2411 iPhaseX: 0, iPhaseY: 0, scale: GetGraphics()->pDef->Scale);
2412 }
2413 // Regular action facet
2414 else
2415 {
2416 // Calculate graphics phase index
2417 int32_t iPhase = Action.Phase;
2418 if (Def->ActMap[Action.Act].Reverse) iPhase = Def->ActMap[Action.Act].Length - 1 - Action.Phase;
2419 if (r != 0 && Def->Rotateable)
2420 {
2421 // newgfx: draw rotated actions as well
2422 if (Def->GrowthType || Con == FullCon)
2423 {
2424 // rotate midpoint of action facet around center of shape
2425 // combine with existing transform if necessary
2426 C4DrawTransform rot;
2427 if (pDrawTransform)
2428 {
2429 rot.SetTransformAt(rCopy&: *pDrawTransform, iOffX: cgo.X + cox + float(Shape.Wdt) / 2, iOffY: cgo.Y + coy + float(Shape.Hgt) / 2);
2430 rot.Rotate(iAngle: r * 100, fOffX: float(cgo.X + cox + Shape.Wdt / 2), fOffY: float(cgo.Y + coy + Shape.Hgt / 2));
2431 }
2432 else
2433 rot.SetRotate(iAngle: r * 100, fOffX: float(cgo.X + cox + Shape.Wdt / 2), fOffY: float(cgo.Y + coy + Shape.Hgt / 2));
2434 // draw stretched towards shape center with transform
2435 Action.Facet.DrawXT(sfcTarget: cgo.Surface,
2436 iX: (Def->Shape.x + Action.FacetX) * Con / FullCon + cgo.X + cox - Shape.x,
2437 iY: (Def->Shape.y + Action.FacetY) * Con / FullCon + cgo.Y + coy - Shape.y,
2438 iWdt: Action.Facet.Wdt * Con / FullCon, iHgt: Action.Facet.Hgt * Con / FullCon,
2439 iPhaseX: iPhase, iPhaseY: Action.DrawDir, pTransform: &rot,
2440 noScalingCorrection: false, scale: GetGraphics()->pDef->Scale);
2441 }
2442 else
2443 {
2444 // incomplete constructions do not show actions
2445 DrawFace(cgo, cgoX: cgo.X + cox, cgoY: cgo.Y + coy);
2446 }
2447 }
2448 else
2449 {
2450 // Exact fullcon
2451 if (Con == FullCon)
2452 Action.Facet.DrawT(sfcTarget: cgo.Surface,
2453 iX: cgo.X + cox + Action.FacetX,
2454 iY: cgo.Y + coy + Action.FacetY,
2455 iPhaseX: iPhase, iPhaseY: Action.DrawDir,
2456 pTransform: pDrawTransform ? &C4DrawTransform(*pDrawTransform, static_cast<float>(Shape.Wdt) / 2 + cgo.X + cox, static_cast<float>(Shape.Hgt) / 2 + cgo.Y + coy) : nullptr,
2457 noScalingCorrection: false, scale: GetGraphics()->pDef->Scale);
2458 // Growth strechted
2459 else
2460 Action.Facet.DrawXT(sfcTarget: cgo.Surface,
2461 iX: cgo.X + cox, iY: cgo.Y + coy,
2462 iWdt: Shape.Wdt, iHgt: Shape.Hgt,
2463 iPhaseX: iPhase, iPhaseY: Action.DrawDir,
2464 pTransform: pDrawTransform ? &C4DrawTransform(*pDrawTransform, static_cast<float>(Shape.Wdt) / 2 + cgo.X + cox, static_cast<float>(Shape.Hgt) / 2 + cgo.Y + coy) : nullptr,
2465 noScalingCorrection: false, scale: GetGraphics()->pDef->Scale);
2466 }
2467 }
2468 }
2469 }
2470
2471 // end of color modulation
2472 if (ColorMod || BlitMode) if (!eDrawMode) FinishedDrawing();
2473
2474 // draw overlays - after blit mode changes, because overlay gfx set their own
2475 if (pGfxOverlay) if (eDrawMode != ODM_BaseOnly)
2476 for (C4GraphicsOverlay *pGfxOvrl = pGfxOverlay; pGfxOvrl; pGfxOvrl = pGfxOvrl->GetNext())
2477 if (!pGfxOvrl->IsPicture())
2478 pGfxOvrl->Draw(cgo, pForObj: this, iByPlayer);
2479
2480 // local particles in front of the object
2481 if (FrontParticles) if (eDrawMode != ODM_BaseOnly) FrontParticles.Draw(cgo, pObj: this);
2482
2483 // Select Mark
2484 if (Select)
2485 if (eDrawMode != ODM_BaseOnly)
2486 if (ValidPlr(plr: Owner))
2487 if (Owner == iByPlayer)
2488 if (Game.Players.Get(iPlayer: Owner)->SelectFlash)
2489 DrawSelectMark(cgo);
2490
2491 // Energy shortage
2492 if (NeedEnergy) if (Tick35 > 12) if (eDrawMode != ODM_BaseOnly)
2493 {
2494 C4Facet &fctEnergy = Game.GraphicsResource.fctEnergy;
2495 int32_t tx = cox + Shape.Wdt / 2 - fctEnergy.Wdt / 2, ty = coy - fctEnergy.Hgt - 5;
2496 fctEnergy.Draw(sfcTarget: cgo.Surface, iX: cgo.X + tx, iY: cgo.Y + ty);
2497 }
2498
2499 // Debug Display
2500 if (Game.GraphicsSystem.ShowVertices) if (eDrawMode != ODM_BaseOnly)
2501 {
2502 int32_t cnt;
2503 if (Shape.VtxNum > 1)
2504 for (cnt = 0; cnt < Shape.VtxNum; cnt++)
2505 {
2506 DrawVertex(cgo,
2507 tx: cox - Shape.x + Shape.VtxX[cnt],
2508 ty: coy - Shape.y + Shape.VtxY[cnt],
2509 col: (Shape.VtxCNAT[cnt] & CNAT_NoCollision) ? CBlue : (Mobile ? CRed : CYellow),
2510 contact: Shape.VtxContactCNAT[cnt]);
2511 }
2512 }
2513
2514 if (Game.GraphicsSystem.ShowEntrance) if (eDrawMode != ODM_BaseOnly)
2515 {
2516 if (OCF & OCF_Entrance)
2517 Application.DDraw->DrawFrame(sfcDest: cgo.Surface, x1: cgo.X + cox - Shape.x + Def->Entrance.x,
2518 y1: cgo.Y + coy - Shape.y + Def->Entrance.y,
2519 x2: cgo.X + cox - Shape.x + Def->Entrance.x + Def->Entrance.Wdt - 1,
2520 y2: cgo.Y + coy - Shape.y + Def->Entrance.y + Def->Entrance.Hgt - 1,
2521 col: CBlue);
2522 if (OCF & OCF_Collection)
2523 Application.DDraw->DrawFrame(sfcDest: cgo.Surface, x1: cgo.X + cox - Shape.x + Def->Collection.x,
2524 y1: cgo.Y + coy - Shape.y + Def->Collection.y,
2525 x2: cgo.X + cox - Shape.x + Def->Collection.x + Def->Collection.Wdt - 1,
2526 y2: cgo.Y + coy - Shape.y + Def->Collection.y + Def->Collection.Hgt - 1,
2527 col: CRed);
2528 }
2529
2530 if (Game.GraphicsSystem.ShowAction) if (eDrawMode != ODM_BaseOnly)
2531 {
2532 if (Action.Act > ActIdle)
2533 {
2534 const std::string message{std::format(fmt: "{} ({})", args: +Def->ActMap[Action.Act].Name, args&: Action.Phase)};
2535 int32_t cmwdt, cmhgt;
2536 Game.GraphicsResource.FontRegular.GetTextExtent(szText: message.c_str(), rsx&: cmwdt, rsy&: cmhgt, fCheckMarkup: true);
2537 Application.DDraw->TextOut(szText: message.c_str(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + cox - Shape.x, iTy: cgo.Y + coy - cmhgt, dwFCol: InLiquid ? 0xfa0000FF : CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ACenter);
2538 }
2539 }
2540
2541 // Restore visibility inside FoW
2542 if (fOldClrModEnabled) lpDDraw->SetClrModMapEnabled(fOldClrModEnabled);
2543}
2544
2545void C4Object::DrawTopFace(C4FacetEx &cgo, int32_t iByPlayer, DrawMode eDrawMode)
2546{
2547 // Status
2548 if (!Status || !Def) return;
2549 // visible?
2550 if (Visibility) if (!IsVisible(iForPlr: iByPlayer, fAsOverlay: eDrawMode == ODM_Overlay)) return;
2551 // target pos (parallax)
2552 int32_t cotx = cgo.TargetX, coty = cgo.TargetY; if (eDrawMode != ODM_Overlay) TargetPos(riTx&: cotx, riTy&: coty, fctViewport: cgo);
2553 // Clonk name
2554 // Name of Owner/Clonk (only when Crew Member; never in films)
2555 if (OCF & OCF_CrewMember) if ((Config.Graphics.ShowCrewNames || Config.Graphics.ShowCrewCNames) && (!Game.C4S.Head.Film || !Game.C4S.Head.Replay)) if (!eDrawMode)
2556 if (Owner != iByPlayer && !Contained)
2557 {
2558 // inside screen range?
2559 if (!Inside(ival: x + Shape.x - cotx, lbound: 1 - Shape.Wdt, rbound: cgo.Wdt)
2560 || !Inside(ival: y + Shape.y - coty, lbound: 1 - Shape.Hgt, rbound: cgo.Hgt)) return;
2561 // get player
2562 C4Player *pOwner = Game.Players.Get(iPlayer: Owner);
2563 if (pOwner) if (!Hostile(plr1: Owner, plr2: iByPlayer)) if (!pOwner->IsInvisible())
2564 {
2565 int32_t X = x;
2566 int32_t Y = y - Def->Shape.Hgt / 2 - 20;
2567 // compose string
2568 char szText[C4GM_MaxText + 1];
2569 if (Config.Graphics.ShowCrewNames)
2570 if (Config.Graphics.ShowCrewCNames)
2571 FormatWithNull(buf&: szText, fmt: "{} ({})", args: GetName(), args: pOwner->GetName());
2572 else
2573 SCopy(szSource: pOwner->GetName(), sTarget: szText);
2574 else
2575 SCopy(szSource: GetName(), sTarget: szText);
2576 // Word wrap to cgo width
2577 int32_t iCharWdt, dummy; Game.GraphicsResource.FontRegular.GetTextExtent(szText: "m", rsx&: iCharWdt, rsy&: dummy, fCheckMarkup: false);
2578 int32_t iMaxLine = std::max<int32_t>(a: cgo.Wdt / iCharWdt, b: 20);
2579 SWordWrap(szText, cSpace: ' ', cSepa: '|', iMaxLine);
2580 // Adjust position by output boundaries
2581 int32_t iTX, iTY, iTWdt, iTHgt;
2582 Game.GraphicsResource.FontRegular.GetTextExtent(szText, rsx&: iTWdt, rsy&: iTHgt, fCheckMarkup: true);
2583 iTX = BoundBy<int32_t>(bval: X - cotx, lbound: iTWdt / 2, rbound: cgo.Wdt - iTWdt / 2);
2584 iTY = BoundBy<int32_t>(bval: Y - coty - iTHgt, lbound: 0, rbound: cgo.Hgt - iTHgt);
2585 // Draw
2586 Application.DDraw->TextOut(szText, rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, iTx: cgo.X + iTX, iTy: cgo.Y + iTY,
2587 dwFCol: pOwner->ColorDw | 0x7f000000, byForm: ACenter);
2588 }
2589 }
2590 // TopFace
2591 if (!(TopFace.Surface || (OCF & OCF_Construct))) return;
2592 // Output position
2593 int32_t cox, coy;
2594 cox = x + Shape.x - cotx;
2595 coy = y + Shape.y - coty;
2596 // Output bounds check
2597 if (!Inside(ival: cox, lbound: 1 - Shape.Wdt, rbound: cgo.Wdt)
2598 || !Inside(ival: coy, lbound: 1 - Shape.Hgt, rbound: cgo.Hgt))
2599 return;
2600 // Don't draw (show solidmask)
2601 if (Game.GraphicsSystem.ShowSolidMask)
2602 if (SolidMask.Wdt)
2603 return;
2604 // Contained
2605 if (Contained) if (eDrawMode != ODM_Overlay) return;
2606 // Construction sign
2607 if (OCF & OCF_Construct) if (r == 0) if (eDrawMode != ODM_BaseOnly)
2608 {
2609 C4Facet &fctConSign = Game.GraphicsResource.fctConstruction;
2610 fctConSign.Draw(sfcTarget: cgo.Surface, iX: cgo.X + cox, iY: cgo.Y + coy + Shape.Hgt - fctConSign.Hgt);
2611 }
2612 // FacetTopFace: Override TopFace.x/y
2613 if ((Action.Act > ActIdle) && Def->ActMap[Action.Act].FacetTopFace)
2614 {
2615 C4ActionDef *actdef = &Def->ActMap[Action.Act];
2616 int32_t iPhase = Action.Phase;
2617 if (actdef->Reverse) iPhase = actdef->Length - 1 - Action.Phase;
2618 TopFace.X = actdef->Facet.x + Def->TopFace.x + actdef->Facet.Wdt * iPhase;
2619 TopFace.Y = actdef->Facet.y + Def->TopFace.y + actdef->Facet.Hgt * Action.DrawDir;
2620 }
2621 // ensure correct color is set
2622 if (GetGraphics()->BitmapClr) GetGraphics()->BitmapClr->SetClr(Color);
2623 // color modulation (including construction sign...)
2624 if (!eDrawMode) PrepareDrawing();
2625 // Draw top face bitmap
2626 if (Con != FullCon && Def->GrowthType)
2627 // stretched
2628 TopFace.DrawXT(sfcTarget: cgo.Surface,
2629 iX: cgo.X + cox + Def->TopFace.tx * Con / FullCon,
2630 iY: cgo.Y + coy + Def->TopFace.ty * Con / FullCon,
2631 iWdt: TopFace.Wdt * Con / FullCon,
2632 iHgt: TopFace.Hgt * Con / FullCon,
2633 iPhaseX: 0, iPhaseY: 0,
2634 pTransform: pDrawTransform ? &C4DrawTransform(*pDrawTransform, cgo.X + cox + float(Shape.Wdt) / 2, cgo.Y + coy + float(Shape.Hgt) / 2) : nullptr,
2635 noScalingCorrection: false, scale: GetGraphics()->pDef->Scale);
2636 else
2637 // normal
2638 TopFace.DrawT(sfcTarget: cgo.Surface,
2639 iX: cgo.X + cox + Def->TopFace.tx,
2640 iY: cgo.Y + coy + Def->TopFace.ty,
2641 iPhaseX: 0, iPhaseY: 0,
2642 pTransform: pDrawTransform ? &C4DrawTransform(*pDrawTransform, cgo.X + cox + float(Shape.Wdt) / 2, cgo.Y + coy + float(Shape.Hgt) / 2) : nullptr,
2643 noScalingCorrection: false, scale: GetGraphics()->pDef->Scale);
2644 // end of color modulation
2645 if (!eDrawMode) FinishedDrawing();
2646}
2647
2648void C4Object::DrawLine(C4FacetEx &cgo)
2649{
2650 // Audibility
2651 SetAudibilityAt(cgo, iX: Shape.VtxX[0], iY: Shape.VtxY[0]);
2652 SetAudibilityAt(cgo, iX: Shape.VtxX[Shape.VtxNum - 1], iY: Shape.VtxY[Shape.VtxNum - 1]);
2653 // additive mode?
2654 PrepareDrawing();
2655 // Draw line segments
2656 for (int32_t vtx = 0; vtx + 1 < Shape.VtxNum; vtx++)
2657 switch (Def->Line)
2658 {
2659 case C4D_Line_Power:
2660 cgo.DrawLine(iX1: Shape.VtxX[vtx], iY1: Shape.VtxY[vtx],
2661 iX2: Shape.VtxX[vtx + 1], iY2: Shape.VtxY[vtx + 1],
2662 bCol1: 68, bCol2: 26);
2663 break;
2664 case C4D_Line_Source: case C4D_Line_Drain:
2665 cgo.DrawLine(iX1: Shape.VtxX[vtx], iY1: Shape.VtxY[vtx],
2666 iX2: Shape.VtxX[vtx + 1], iY2: Shape.VtxY[vtx + 1],
2667 bCol1: 23, bCol2: 26);
2668 break;
2669 case C4D_Line_Lightning:
2670 cgo.DrawBolt(iX1: Shape.VtxX[vtx], iY1: Shape.VtxY[vtx],
2671 iX2: Shape.VtxX[vtx + 1], iY2: Shape.VtxY[vtx + 1],
2672 bCol: CWhite, bCol2: CWhite);
2673 break;
2674 case C4D_Line_Rope:
2675 cgo.DrawLine(iX1: Shape.VtxX[vtx], iY1: Shape.VtxY[vtx],
2676 iX2: Shape.VtxX[vtx + 1], iY2: Shape.VtxY[vtx + 1],
2677 bCol1: 65, bCol2: 65);
2678 break;
2679 case C4D_Line_Vertex:
2680 case C4D_Line_Colored:
2681 cgo.DrawLine(iX1: Shape.VtxX[vtx], iY1: Shape.VtxY[vtx],
2682 iX2: Shape.VtxX[vtx + 1], iY2: Shape.VtxY[vtx + 1],
2683 bCol1: uint8_t(Local[0].getInt()), bCol2: uint8_t(Local[1].getInt()));
2684 break;
2685 }
2686 // reset blit mode
2687 FinishedDrawing();
2688}
2689
2690void C4Object::DrawEnergy(C4Facet &cgo)
2691{
2692 cgo.DrawEnergyLevelEx(iLevel: Energy, iRange: GetPhysical()->Energy, gfx: Game.GraphicsResource.fctEnergyBars, bar_idx: 0);
2693}
2694
2695void C4Object::DrawMagicEnergy(C4Facet &cgo)
2696{
2697 // draw in units of MagicPhysicalFactor, so you can get a full magic energy bar by script even if partial magic energy training is not fulfilled
2698 cgo.DrawEnergyLevelEx(iLevel: MagicEnergy / MagicPhysicalFactor, iRange: GetPhysical()->Magic / MagicPhysicalFactor, gfx: Game.GraphicsResource.fctEnergyBars, bar_idx: 1);
2699}
2700
2701void C4Object::DrawBreath(C4Facet &cgo)
2702{
2703 cgo.DrawEnergyLevelEx(iLevel: Breath, iRange: GetPhysical()->Breath, gfx: Game.GraphicsResource.fctEnergyBars, bar_idx: 2);
2704}
2705
2706void C4Object::CompileFunc(StdCompiler *pComp)
2707{
2708 bool fCompiler = pComp->isCompiler();
2709 if (fCompiler)
2710 Clear();
2711
2712 // Compile ID, search definition
2713 pComp->Value(rStruct: mkNamingAdapt(rValue: mkC4IDAdapt(rValue&: id), szName: "id", rDefault: C4ID_None));
2714 if (fCompiler)
2715 {
2716 Def = Game.Defs.ID2Def(id);
2717 if (!Def)
2718 {
2719 pComp->excNotFound(message: LoadResStr(id: C4ResStrTableKey::IDS_PRC_UNDEFINEDOBJECT, args: C4IdText(id))); return;
2720 }
2721 }
2722
2723 // Write the name only if the object has an individual name, use def name as default for reading.
2724 // (Info may overwrite later, see C4Player::MakeCrewMember)
2725 if (pComp->isCompiler())
2726 {
2727 pComp->Value(rStruct: mkNamingAdapt(rValue&: CustomName, szName: "Name", rDefault: std::string{}));
2728 }
2729 else if (!CustomName.empty())
2730 // Write the name only if the object has an individual name
2731 // 2do: And what about binary compilers?
2732 pComp->Value(rStruct: mkNamingAdapt(rValue&: CustomName, szName: "Name"));
2733
2734 pComp->Value(rStruct: mkNamingAdapt(rValue&: Number, szName: "Number", rDefault: -1));
2735 pComp->Value(rStruct: mkNamingAdapt(rValue&: Status, szName: "Status", rDefault: 1));
2736 pComp->Value(rStruct: mkNamingAdapt(toC4CStrBuf(nInfo), szName: "Info", rDefault: ""));
2737 pComp->Value(rStruct: mkNamingAdapt(rValue&: Owner, szName: "Owner", rDefault: NO_OWNER));
2738 pComp->Value(rStruct: mkNamingAdapt(rValue&: Timer, szName: "Timer", rDefault: 0));
2739 pComp->Value(rStruct: mkNamingAdapt(rValue&: Controller, szName: "Controller", rDefault: NO_OWNER));
2740 pComp->Value(rStruct: mkNamingAdapt(rValue&: LastEnergyLossCausePlayer, szName: "LastEngLossPlr", rDefault: NO_OWNER));
2741 pComp->Value(rStruct: mkNamingAdapt(rValue&: Category, szName: "Category", rDefault: 0));
2742 pComp->Value(rStruct: mkNamingAdapt(rValue&: x, szName: "X", rDefault: 0));
2743 pComp->Value(rStruct: mkNamingAdapt(rValue&: y, szName: "Y", rDefault: 0));
2744 pComp->Value(rStruct: mkNamingAdapt(rValue&: r, szName: "Rotation", rDefault: 0));
2745 pComp->Value(rStruct: mkNamingAdapt(rValue&: motion_x, szName: "MotionX", rDefault: 0));
2746 pComp->Value(rStruct: mkNamingAdapt(rValue&: motion_y, szName: "MotionY", rDefault: 0));
2747 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLastAttachMovementFrame, szName: "LastSolidAtchFrame", rDefault: -1));
2748 pComp->Value(rStruct: mkNamingAdapt(rValue&: NoCollectDelay, szName: "NoCollectDelay", rDefault: 0));
2749 pComp->Value(rStruct: mkNamingAdapt(rValue&: Base, szName: "Base", rDefault: NO_OWNER));
2750 pComp->Value(rStruct: mkNamingAdapt(rValue&: Con, szName: "Size", rDefault: 0));
2751 pComp->Value(rStruct: mkNamingAdapt(rValue&: OwnMass, szName: "OwnMass", rDefault: 0));
2752 pComp->Value(rStruct: mkNamingAdapt(rValue&: Mass, szName: "Mass", rDefault: 0));
2753 pComp->Value(rStruct: mkNamingAdapt(rValue&: Damage, szName: "Damage", rDefault: 0));
2754 pComp->Value(rStruct: mkNamingAdapt(rValue&: Energy, szName: "Energy", rDefault: 0));
2755 pComp->Value(rStruct: mkNamingAdapt(rValue&: MagicEnergy, szName: "MagicEnergy", rDefault: 0));
2756 pComp->Value(rStruct: mkNamingAdapt(rValue&: Alive, szName: "Alive", rDefault: false));
2757 pComp->Value(rStruct: mkNamingAdapt(rValue&: Breath, szName: "Breath", rDefault: 0));
2758 pComp->Value(rStruct: mkNamingAdapt(rValue&: FirePhase, szName: "FirePhase", rDefault: 0));
2759 pComp->Value(rStruct: mkNamingAdapt(rValue&: Color, szName: "Color", rDefault: 0u)); // TODO: Convert
2760 pComp->Value(rStruct: mkNamingAdapt(rValue&: Color, szName: "ColorDw", rDefault: 0u));
2761 pComp->Value(rStruct: mkNamingAdapt(rValue&: Local, szName: "Locals", rDefault: C4ValueList()));
2762 pComp->Value(rStruct: mkNamingAdapt(rValue&: fix_x, szName: "FixX", rDefault: 0));
2763 pComp->Value(rStruct: mkNamingAdapt(rValue&: fix_y, szName: "FixY", rDefault: 0));
2764 pComp->Value(rStruct: mkNamingAdapt(rValue&: fix_r, szName: "FixR", rDefault: 0));
2765 pComp->Value(rStruct: mkNamingAdapt(rValue&: xdir, szName: "XDir", rDefault: 0));
2766 pComp->Value(rStruct: mkNamingAdapt(rValue&: ydir, szName: "YDir", rDefault: 0));
2767 pComp->Value(rStruct: mkNamingAdapt(rValue&: rdir, szName: "RDir", rDefault: 0));
2768 pComp->Value(rStruct: mkParAdapt(rObj&: Shape, rPar: true));
2769 pComp->Value(rStruct: mkNamingAdapt(rValue&: fOwnVertices, szName: "OwnVertices", rDefault: false));
2770 pComp->Value(rStruct: mkNamingAdapt(rValue&: SolidMask, szName: "SolidMask", rDefault: Def->SolidMask));
2771 pComp->Value(rStruct: mkNamingAdapt(rValue&: PictureRect, szName: "Picture"));
2772 pComp->Value(rStruct: mkNamingAdapt(rValue&: Mobile, szName: "Mobile", rDefault: false));
2773 pComp->Value(rStruct: mkNamingAdapt(rValue&: Select, szName: "Selected", rDefault: false));
2774 pComp->Value(rStruct: mkNamingAdapt(rValue&: OnFire, szName: "OnFire", rDefault: false));
2775 pComp->Value(rStruct: mkNamingAdapt(rValue&: InLiquid, szName: "InLiquid", rDefault: false));
2776 pComp->Value(rStruct: mkNamingAdapt(rValue&: EntranceStatus, szName: "EntranceStatus", rDefault: false));
2777 pComp->Value(rStruct: mkNamingAdapt(rValue&: PhysicalTemporary, szName: "PhysicalTemporary", rDefault: false));
2778 pComp->Value(rStruct: mkNamingAdapt(rValue&: NeedEnergy, szName: "NeedEnergy", rDefault: false));
2779 pComp->Value(rStruct: mkNamingAdapt(rValue&: OCF, szName: "OCF", rDefault: 0u));
2780 pComp->Value(rStruct&: Action);
2781 pComp->Value(rStruct: mkNamingAdapt(rValue&: Contained, szName: "Contained", rDefault: C4EnumeratedObjectPtr{}));
2782 pComp->Value(rStruct: mkNamingAdapt(rValue&: Action.Target, szName: "ActionTarget1", rDefault: C4EnumeratedObjectPtr{}));
2783 pComp->Value(rStruct: mkNamingAdapt(rValue&: Action.Target2, szName: "ActionTarget2", rDefault: C4EnumeratedObjectPtr{}));
2784 pComp->Value(rStruct: mkNamingAdapt(rValue&: Component, szName: "Component"));
2785 pComp->Value(rStruct: mkNamingAdapt(rValue&: Contents, szName: "Contents"));
2786 pComp->Value(rStruct: mkNamingAdapt(rValue&: PlrViewRange, szName: "PlrViewRange", rDefault: 0));
2787 pComp->Value(rStruct: mkNamingAdapt(rValue&: Visibility, szName: "Visibility", VIS_All));
2788 pComp->Value(rStruct: mkNamingAdapt(rValue&: LocalNamed, szName: "LocalNamed"));
2789 pComp->Value(rStruct: mkNamingAdapt(rValue&: ColorMod, szName: "ColorMod", rDefault: 0u));
2790 pComp->Value(rStruct: mkNamingAdapt(rValue&: BlitMode, szName: "BlitMode", rDefault: 0u));
2791 pComp->Value(rStruct: mkNamingAdapt(rValue&: CrewDisabled, szName: "CrewDisabled", rDefault: false));
2792 pComp->Value(rStruct: mkNamingAdapt(rValue&: pLayer, szName: "Layer", rDefault: C4EnumeratedObjectPtr{}));
2793 pComp->Value(rStruct: mkNamingAdapt(rValue: C4DefGraphicsAdapt(pGraphics), szName: "Graphics", rDefault: &Def->Graphics));
2794 pComp->Value(rStruct: mkNamingPtrAdapt(rpObj&: pDrawTransform, szNaming: "DrawTransform"));
2795 pComp->Value(rStruct: mkNamingPtrAdapt(rpObj&: pEffects, szNaming: "Effects"));
2796 pComp->Value(rStruct: mkNamingAdapt(rValue: C4GraphicsOverlayListAdapt(pGfxOverlay), szName: "GfxOverlay", rDefault: nullptr));
2797
2798 if (PhysicalTemporary)
2799 {
2800 pComp->FollowName(szName: "Physical");
2801 pComp->Value(rStruct&: TemporaryPhysical);
2802 }
2803
2804 // Commands
2805 if (pComp->FollowName(szName: "Commands"))
2806 if (fCompiler)
2807 {
2808 C4Command *pCmd = nullptr;
2809 for (int i = 1; ; i++)
2810 {
2811 // Every command has its own naming environment
2812 const std::string naming{std::format(fmt: "Command{}", args&: i)};
2813 pComp->Value(rStruct: mkNamingPtrAdapt(rpObj&: pCmd ? pCmd->Next : Command, szNaming: naming.c_str()));
2814 // Last command?
2815 pCmd = (pCmd ? pCmd->Next : Command);
2816 if (!pCmd)
2817 break;
2818 pCmd->cObj = this;
2819 }
2820 }
2821 else
2822 {
2823 C4Command *pCmd = Command;
2824 for (int i = 1; pCmd; i++, pCmd = pCmd->Next)
2825 {
2826 const std::string naming{std::format(fmt: "Command{}", args&: i)};
2827 pComp->Value(rStruct: mkNamingAdapt(rValue&: *pCmd, szName: naming.c_str()));
2828 }
2829 }
2830
2831 // Compiling? Do initialization.
2832 if (fCompiler)
2833 {
2834 // add to def count
2835 Def->Count++;
2836
2837 // set local variable names
2838 LocalNamed.SetNameList(&Def->Script.LocalNamed);
2839
2840 // Set action (override running data)
2841 int32_t iTime = Action.Time;
2842 int32_t iPhase = Action.Phase;
2843 int32_t iPhaseDelay = Action.PhaseDelay;
2844 if (SetActionByName(szActName: Action.Name, pTarget: nullptr, pTarget2: nullptr, iCalls: false))
2845 {
2846 Action.Time = iTime;
2847 Action.Phase = iPhase; // No checking for valid phase
2848 Action.PhaseDelay = iPhaseDelay;
2849 }
2850
2851 // if on fire but no effect is present (old-style savegames), re-incinerate
2852 int32_t iFireNumber;
2853 C4Value Par1, Par2, Par3, Par4;
2854 if (OnFire && !pEffects) new C4Effect(this, C4Fx_Fire, C4Fx_FirePriority, C4Fx_FireTimer, nullptr, 0, Par1, Par2, Par3, Par4, false, iFireNumber);
2855
2856 // blit mode not assigned? use definition default then
2857 if (!BlitMode) BlitMode = Def->BlitMode;
2858
2859 // object needs to be resorted? May happen if there's unsorted objects in savegame
2860 if (Unsorted) Game.fResortAnyObject = true;
2861
2862 // initial OCF update
2863 SetOCF();
2864 }
2865}
2866
2867void C4Object::EnumeratePointers()
2868{
2869 EnumerateObjectPtrs(args&: Contained, args&: Action.Target, args&: Action.Target2, args&: pLayer);
2870
2871 // Info by name
2872 if (Info) nInfo = Info->Name; else nInfo.Clear();
2873
2874 // Commands
2875 for (C4Command *pCom = Command; pCom; pCom = pCom->Next)
2876 pCom->EnumeratePointers();
2877
2878 // effects
2879 if (pEffects) pEffects->EnumeratePointers();
2880
2881 // gfx overlays
2882 if (pGfxOverlay)
2883 for (C4GraphicsOverlay *pGfxOvrl = pGfxOverlay; pGfxOvrl; pGfxOvrl = pGfxOvrl->GetNext())
2884 pGfxOvrl->EnumeratePointers();
2885}
2886
2887void C4Object::DenumeratePointers()
2888{
2889 DenumerateObjectPtrs(args&: Contained, args&: Action.Target, args&: Action.Target2, args&: pLayer);
2890
2891 // Post-compile object list
2892 Contents.DenumerateRead();
2893
2894 // Local variable pointers
2895 Local.DenumeratePointers();
2896 LocalNamed.DenumeratePointers();
2897
2898 // Commands
2899 for (C4Command *pCom = Command; pCom; pCom = pCom->Next)
2900 pCom->DenumeratePointers();
2901
2902 // effects
2903 if (pEffects) pEffects->DenumeratePointers();
2904
2905 // gfx overlays
2906 if (pGfxOverlay)
2907 for (C4GraphicsOverlay *pGfxOvrl = pGfxOverlay; pGfxOvrl; pGfxOvrl = pGfxOvrl->GetNext())
2908 pGfxOvrl->DenumeratePointers();
2909}
2910
2911bool DrawCommandQuery(int32_t controller, C4ScriptHost &scripthost, int32_t *mask, int com)
2912{
2913 int method = scripthost.GetControlMethod(com, first: mask[0], second: mask[1]);
2914 C4Player *player = Game.Players.Get(iPlayer: controller);
2915 if (!player) return false;
2916
2917 switch (method)
2918 {
2919 case C4AUL_ControlMethod_All: return true;
2920 case C4AUL_ControlMethod_None: return false;
2921 case C4AUL_ControlMethod_Classic: return !player->ControlStyle;
2922 case C4AUL_ControlMethod_JumpAndRun: return !!player->ControlStyle;
2923 default: return false;
2924 }
2925}
2926
2927void C4Object::DrawCommands(C4Facet &cgoBottom, C4Facet &cgoSide, C4RegionList *pRegions)
2928{
2929 int32_t cnt;
2930 C4Facet ccgo, ccgo2;
2931 C4Object *tObj;
2932 int32_t iDFA = GetProcedure();
2933 bool fContainedDownOverride = false;
2934 bool fContainedLeftOverride = false; // carlo
2935 bool fContainedRightOverride = false; // carlo
2936 bool fContentsActivationOverride = false;
2937
2938 // Active menu (does not consider owner's active player menu)
2939 if (Menu && Menu->IsActive()) return;
2940
2941 uint32_t ocf = OCF_Construct;
2942 if (Action.ComDir == COMD_Stop && iDFA == DFA_WALK && (tObj = Game.Objects.AtObject(ctx: x, cty: y, ocf, exclude: this)))
2943 {
2944 int32_t com = COM_Down_D;
2945 if (Game.Players.Get(iPlayer: Controller)->ControlStyle) com = COM_Down;
2946
2947 tObj->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: com, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_BUILD, args: tObj->GetName()).c_str(), pfctImage: &ccgo);
2948 tObj->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: tObj->Color, pObj: tObj);
2949 Game.GraphicsResource.fctBuild.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true);
2950 }
2951
2952 // Grab target control (control flag)
2953 if (iDFA == DFA_PUSH && Action.Target)
2954 {
2955 for (cnt = ComOrderNum - 1; cnt >= 0; cnt--)
2956 if (DrawCommandQuery(controller: Controller, scripthost&: Action.Target->Def->Script, mask: Action.Target->Def->Script.ControlMethod, com: cnt))
2957 {
2958 Action.Target->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, PSF_Control, iCom: ComOrder(iCom: cnt), pRegions, iPlayer: Owner);
2959 }
2960 else if (ComOrder(iCom: cnt) == COM_Down_D)
2961 {
2962 // Let Go
2963 Action.Target->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Down_D, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_UNGRAB, args: Action.Target->GetName()).c_str(), pfctImage: &ccgo);
2964 Action.Target->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: Action.Target->Color, pObj: Action.Target);
2965 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 6);
2966 }
2967 else if (ComOrder(iCom: cnt) == COM_Throw)
2968 {
2969 // Put
2970 if ((tObj = Contents.GetObject()) && (Action.Target->Def->GrabPutGet & C4D_Grab_Put))
2971 {
2972 Action.Target->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Throw, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_PUT, args: tObj->GetName(), args: Action.Target->GetName()).c_str(), pfctImage: &ccgo);
2973 tObj->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: tObj->Color, pObj: tObj);
2974 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 0);
2975 }
2976 // Get
2977 else if (Action.Target->Contents.ListIDCount(dwCategory: C4D_Get) && (Action.Target->Def->GrabPutGet & C4D_Grab_Get))
2978 {
2979 Action.Target->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Throw, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_GET, args: Action.Target->GetName()).c_str(), pfctImage: &ccgo);
2980 Action.Target->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: Action.Target->Color, pObj: Action.Target);
2981 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 1);
2982 }
2983 }
2984 }
2985
2986 // Contained control (contained control flag)
2987 if (Contained)
2988 {
2989 for (cnt = ComOrderNum - 1; cnt >= 0; cnt--)
2990 if (DrawCommandQuery(controller: Controller, scripthost&: Contained->Def->Script, mask: Contained->Def->Script.ContainedControlMethod, com: cnt))
2991 {
2992 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, PSF_ContainedControl, iCom: ComOrder(iCom: cnt), pRegions, iPlayer: Owner);
2993 // Contained control down overrides contained exit control
2994 if (Com2Control(iCom: ComOrder(iCom: cnt)) == CON_Down) fContainedDownOverride = true;
2995 // carlo - Contained controls left/right override contained Take, Take2 controls
2996 if (Com2Control(iCom: ComOrder(iCom: cnt)) == CON_Left) fContainedLeftOverride = true;
2997 if (Com2Control(iCom: ComOrder(iCom: cnt)) == CON_Right) fContainedRightOverride = true;
2998 }
2999 // Contained exit
3000 if (!fContainedDownOverride)
3001 {
3002 DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Down, pRegions, iPlayer: Owner,
3003 szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_EXIT), pfctImage: &ccgo);
3004 Game.GraphicsResource.fctExit.Draw(cgo&: ccgo);
3005 }
3006 // Contained base commands
3007 if (ValidPlr(plr: Contained->Base))
3008 {
3009 // Sell
3010 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Sell)
3011 {
3012 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Dig, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_SELL), pfctImage: &ccgo);
3013 DrawMenuSymbol(iMenu: C4MN_Sell, cgo&: ccgo, iOwner: Contained->Base, cObj: Contained);
3014 }
3015 // Buy
3016 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Buy)
3017 {
3018 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Up, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_BUY), pfctImage: &ccgo);
3019 DrawMenuSymbol(iMenu: C4MN_Buy, cgo&: ccgo, iOwner: Contained->Base, cObj: Contained);
3020 }
3021 }
3022 // Contained put & activate
3023 // carlo
3024 int32_t nContents = Contained->Contents.ListIDCount(dwCategory: C4D_Get);
3025 if (nContents)
3026 {
3027 // carlo: Direct get ("Take2")
3028 if (!fContainedRightOverride)
3029 {
3030 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Right, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_GET, args: Contained->GetName()).c_str(), pfctImage: &ccgo);
3031 Contained->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: Contained->Color, pObj: Contained);
3032 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 1);
3033 }
3034 // carlo: Get ("Take")
3035 if (!fContainedLeftOverride)
3036 {
3037 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Left, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_ACTIVATEFROM, args: Contained->GetName()).c_str(), pfctImage: &ccgo);
3038 Contained->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: Contained->Color, pObj: Contained);
3039 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 0);
3040 }
3041 }
3042 if (tObj = Contents.GetObject())
3043 {
3044 // Put
3045 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Throw, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_PUT, args: tObj->GetName(), args: Contained->GetName()).c_str(), pfctImage: &ccgo);
3046 tObj->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: tObj->Color, pObj: tObj);
3047 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 0);
3048 }
3049 else if (nContents)
3050 {
3051 // Get
3052 Contained->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, szFunctionFormat: nullptr, iCom: COM_Throw, pRegions, iPlayer: Owner, szDesc: LoadResStr(id: C4ResStrTableKey::IDS_CON_ACTIVATEFROM, args: Contained->GetName()).c_str(), pfctImage: &ccgo);
3053 Contained->Def->Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top), fSelected: false, iColor: Contained->Color, pObj: Contained);
3054 Game.GraphicsResource.fctHand.Draw(cgo&: ccgo2 = ccgo.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom), fAspect: true, iPhaseX: 0);
3055 }
3056 }
3057
3058 // Contents activation (activation control flag)
3059 if (!Contained)
3060 {
3061 if ((iDFA == DFA_WALK) || (iDFA == DFA_SWIM) || (iDFA == DFA_DIG))
3062 if (tObj = Contents.GetObject())
3063 if (DrawCommandQuery(controller: Controller, scripthost&: tObj->Def->Script, mask: tObj->Def->Script.ActivationControlMethod, com: COM_Dig_D))
3064 {
3065 tObj->DrawCommand(cgoBar&: cgoBottom, iAlign: C4FCT_Right, PSF_Activate, iCom: COM_Dig_D, pRegions, iPlayer: Owner);
3066 // Flag override self-activation
3067 fContentsActivationOverride = true;
3068 }
3069
3070 // Self activation (activation control flag)
3071 if (!fContentsActivationOverride)
3072 if ((iDFA == DFA_WALK) || (iDFA == DFA_SWIM) || (iDFA == DFA_FLOAT))
3073 if (DrawCommandQuery(controller: Controller, scripthost&: Def->Script, mask: Def->Script.ActivationControlMethod, com: COM_Dig_D))
3074 DrawCommand(cgoBar&: cgoSide, iAlign: C4FCT_Bottom | C4FCT_Half, PSF_Activate, iCom: COM_Dig_D, pRegions, iPlayer: Owner);
3075 }
3076
3077 // Self special control (control flag)
3078 for (cnt = 6; cnt < ComOrderNum; cnt++)
3079 {
3080 // Hardcoded com order indexes for COM_Specials
3081 if (cnt == 8) cnt = 14; if (cnt == 16) cnt = 22;
3082 // Control in control flag?
3083 if (DrawCommandQuery(controller: Controller, scripthost&: Def->Script, mask: Def->Script.ControlMethod, com: cnt))
3084 DrawCommand(cgoBar&: cgoSide, iAlign: C4FCT_Bottom | C4FCT_Half, PSF_Control, iCom: ComOrder(iCom: cnt), pRegions, iPlayer: Owner);
3085 }
3086}
3087
3088void C4Object::DrawPicture(C4Facet &cgo, bool fSelected, C4RegionList *pRegions)
3089{
3090 // Draw def picture with object color
3091 Def->Draw(cgo, fSelected, iColor: Color, pObj: this);
3092 // Region
3093 if (pRegions) pRegions->Add(iX: cgo.X, iY: cgo.Y, iWdt: cgo.Wdt, iHgt: cgo.Hgt, szCaption: GetName(), iCom: COM_None, pTarget: this);
3094}
3095
3096void C4Object::Picture2Facet(C4FacetExSurface &cgo)
3097{
3098 // set picture rect to facet
3099 C4Rect fctPicRect = PictureRect;
3100 if (!fctPicRect.Wdt) fctPicRect = Def->PictureRect;
3101 const auto scaledRect = fctPicRect.Scaled(scale: Def->Scale);
3102 C4Facet fctPicture{GetGraphics()->GetBitmap(dwClr: Color), scaledRect.x, scaledRect.y, scaledRect.Wdt, scaledRect.Hgt};
3103
3104 // use direct facet w/o own data if possible
3105 if (!ColorMod && BlitMode == C4GFXBLIT_NORMAL && !pGfxOverlay)
3106 {
3107 cgo.Set(fctPicture);
3108 return;
3109 }
3110
3111 // otherwise, draw to picture facet
3112 if (!cgo.Create(iWdt: cgo.Wdt, iHgt: cgo.Hgt)) return;
3113
3114 // specific object color?
3115 PrepareDrawing();
3116
3117 // draw picture itself
3118 fctPicture.Draw(cgo, fAspect: true);
3119
3120 // draw overlays
3121 if (pGfxOverlay)
3122 for (C4GraphicsOverlay *pGfxOvrl = pGfxOverlay; pGfxOvrl; pGfxOvrl = pGfxOvrl->GetNext())
3123 if (pGfxOvrl->IsPicture())
3124 pGfxOvrl->DrawPicture(cgo, pForObj: this);
3125
3126 // done; reset drawing states
3127 FinishedDrawing();
3128}
3129
3130bool C4Object::ValidateOwner()
3131{
3132 // Check owner and controller
3133 if (!ValidPlr(plr: Owner)) Owner = NO_OWNER;
3134 if (!ValidPlr(plr: Base)) Base = NO_OWNER;
3135 if (!ValidPlr(plr: Controller)) Controller = NO_OWNER;
3136 // Color is not reset any more, because many scripts change colors to non-owner-colors these days
3137 // Additionally, player colors are now guarantueed to remain the same in savegame resumes
3138 return true;
3139}
3140
3141bool C4Object::AssignInfo()
3142{
3143 if (Info || !ValidPlr(plr: Owner)) return false;
3144 // In crew list?
3145 C4Player *pPlr = Game.Players.Get(iPlayer: Owner);
3146 if (pPlr->Crew.GetLink(pObj: this))
3147 {
3148 // Register with player
3149 if (!Game.Players.Get(iPlayer: Owner)->MakeCrewMember(pObj: this, fForceInfo: true, fDoCalls: false))
3150 pPlr->Crew.Remove(pObj: this);
3151 return true;
3152 }
3153 // Info set, but not in crew list, so
3154 // a) The savegame is old-style (without crew list)
3155 // or b) The clonk is dead
3156 // or c) The clonk belongs to a script player that's restored without Game.txt
3157 else if (nInfo.getLength())
3158 {
3159 if (!Game.Players.Get(iPlayer: Owner)->MakeCrewMember(pObj: this, fForceInfo: true, fDoCalls: false))
3160 return false;
3161 // Dead and gone (info flags, remove from crew/cursor)
3162 if (!Alive)
3163 {
3164 Info->HasDied = true;
3165 if (ValidPlr(plr: Owner)) Game.Players.Get(iPlayer: Owner)->ClearPointers(tptr: this, fDeath: true);
3166 }
3167 return true;
3168 }
3169 return false;
3170}
3171
3172bool C4Object::AssignPlrViewRange()
3173{
3174 // no range?
3175 if (!PlrViewRange) return true;
3176 // add to FoW-repellers
3177 PlrFoWActualize();
3178 // success
3179 return true;
3180}
3181
3182void C4Object::ClearInfo(C4ObjectInfo *pInfo)
3183{
3184 if (Info == pInfo) Info = nullptr;
3185}
3186
3187void C4Object::Clear()
3188{
3189 delete pEffects; pEffects = nullptr;
3190 if (FrontParticles) FrontParticles.Clear();
3191 if (BackParticles) BackParticles.Clear();
3192 delete pSolidMaskData; pSolidMaskData = nullptr;
3193 delete Menu; Menu = nullptr;
3194 MaterialContents.fill(u: 0);
3195 // clear commands!
3196 C4Command *pCom, *pNext;
3197 for (pCom = Command; pCom; pCom = pNext)
3198 {
3199 pNext = pCom->Next; delete pCom; pCom = pNext;
3200 }
3201 delete pDrawTransform; pDrawTransform = nullptr;
3202 delete pGfxOverlay; pGfxOverlay = nullptr;
3203 while (FirstRef) FirstRef->Set0();
3204}
3205
3206bool C4Object::ContainedControl(uint8_t byCom)
3207{
3208 // Check
3209 if (!Contained) return false;
3210 // Check if object is about to exit; if so, return
3211 // dunno, maybe I should check all the commands, not just the first one?
3212 if ((byCom == COM_Left || byCom == COM_Right) && Command)
3213 if (Command->Command == C4CMD_Exit)
3214 // hack: in structures only; not in vehicles
3215 // they might have a pending Exit-command due to a down-control
3216 if (Contained->Category & C4D_Structure)
3217 return false; // or true? Currently it doesn't matter.
3218 // get script function if defined
3219 C4AulFunc *sf = Contained->Def->Script.GetSFunc(pIdtf: std::format(PSF_ContainedControl, args: ComName(iCom: byCom)).c_str());
3220 // in old versions, do hardcoded actions first (until gwe3)
3221 // new objects may overload them
3222 C4Def *pCDef = Contained->Def;
3223 bool fCallSfEarly = CompareVersion(iVer1: pCDef->rC4XVer[0], iVer2: pCDef->rC4XVer[1], iVer3: pCDef->rC4XVer[2], iVer4: pCDef->rC4XVer[3], iVerBuild: 0, iRVer1: 4, iRVer2: 9, iRVer3: 1, iRVer4: 3) >= 0;
3224 bool result = false;
3225 C4Player *pPlr = Game.Players.Get(iPlayer: Controller);
3226 if (fCallSfEarly)
3227 {
3228 if (sf && sf->Exec(pObj: Contained, pPars: {C4VObj(pObj: this)})) result = true;
3229 // AutoStopControl: Also notify container about controlupdate
3230 // Note Contained may be nulled now due to ContainedControl call
3231 if (Contained && !(byCom & (COM_Single | COM_Double)) && pPlr->ControlStyle)
3232 {
3233 int32_t PressedComs = pPlr->PressedComs;
3234 Contained->Call(PSF_ContainedControlUpdate, pPars: {C4VObj(pObj: this), C4VInt(iVal: Coms2ComDir(iComs: PressedComs)),
3235 C4VBool(fVal: !!(PressedComs & (1 << COM_Dig))), C4VBool(fVal: !!(PressedComs & (1 << COM_Throw)))});
3236 }
3237 }
3238 if (result) return true;
3239
3240 // hardcoded actions
3241 switch (byCom)
3242 {
3243 case COM_Down:
3244 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Exit);
3245 break;
3246 case COM_Throw_D:
3247 // avoid breaking objects with non-default behavior on ContainedThrow
3248 if (Contained->Def->Script.GetSFunc(pIdtf: std::format(PSF_ContainedControl, args: ComName(iCom: COM_Throw)).c_str()))
3249 {
3250 break;
3251 }
3252 [[fallthrough]];
3253 case COM_Throw:
3254 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw) && ExecuteCommand();
3255 break;
3256 case COM_Up:
3257 if (ValidPlr(plr: Contained->Base))
3258 if (!Hostile(plr1: Owner, plr2: Contained->Base))
3259 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Buy)
3260 ActivateMenu(iMenu: C4MN_Buy);
3261 break;
3262 case COM_Dig:
3263 if (ValidPlr(plr: Contained->Base))
3264 if (!Hostile(plr1: Owner, plr2: Contained->Base))
3265 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Sell)
3266 ActivateMenu(iMenu: C4MN_Sell);
3267 break;
3268 }
3269 // Call container script if defined for old versions
3270 if (!fCallSfEarly)
3271 {
3272 if (sf) sf->Exec(pObj: Contained, pPars: {C4VObj(pObj: this)});
3273 if (Contained && !(byCom & (COM_Single | COM_Double)) && pPlr->ControlStyle)
3274 {
3275 int32_t PressedComs = pPlr->PressedComs;
3276 Contained->Call(PSF_ContainedControlUpdate, pPars: {C4VObj(pObj: this), C4VInt(iVal: Coms2ComDir(iComs: PressedComs)),
3277 C4VBool(fVal: !!(PressedComs & (1 << COM_Dig))), C4VBool(fVal: !!(PressedComs & (1 << COM_Throw)))});
3278 }
3279 }
3280 // Take/Take2
3281 if (!sf || fCallSfEarly) switch (byCom)
3282 {
3283 case COM_Left:
3284 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Take);
3285 break;
3286 case COM_Right:
3287 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Take2);
3288 break;
3289 }
3290 // Success
3291 return true;
3292}
3293
3294bool C4Object::CallControl(C4Player *pPlr, uint8_t byCom, const C4AulParSet &pPars)
3295{
3296 assert(pPlr);
3297
3298 bool result = static_cast<bool>(Call(szFunctionCall: std::format(PSF_Control, args: ComName(iCom: byCom)).c_str(), pPars));
3299
3300 // Call ControlUpdate when using Jump'n'Run control
3301 if (pPlr->ControlStyle)
3302 {
3303 int32_t PressedComs = pPlr->PressedComs;
3304 Call(PSF_ControlUpdate, pPars: {pPars[0]._getBool() ? pPars[0] : C4VObj(pObj: this),
3305 C4VInt(iVal: Coms2ComDir(iComs: PressedComs)),
3306 C4VBool(fVal: !!(PressedComs & (1 << COM_Dig))),
3307 C4VBool(fVal: !!(PressedComs & (1 << COM_Throw))),
3308 C4VBool(fVal: !!(PressedComs & (1 << COM_Special))),
3309 C4VBool(fVal: !!(PressedComs & (1 << COM_Special2)))});
3310 }
3311 return result;
3312}
3313
3314void C4Object::DirectCom(uint8_t byCom, int32_t iData) // By player ObjectCom
3315{
3316#ifdef DEBUGREC_OBJCOM
3317 C4RCObjectCom rc = { byCom, iData, Number };
3318 AddDbgRec(RCT_ObjCom, &rc, sizeof(C4RCObjectCom));
3319#endif
3320
3321 // Wether this is a KeyRelease-event
3322 bool IsRelease = Inside(ival: byCom, lbound: COM_ReleaseFirst, rbound: COM_ReleaseLast);
3323 const auto plainCom = (IsRelease ? (byCom - 16) : (byCom & ~(COM_Single | COM_Double)));
3324 bool isCursor = Inside<int>(ival: plainCom, lbound: COM_CursorFirst, rbound: COM_CursorLast);
3325
3326 // we only want the script callbacks for cursor controls
3327 if (isCursor)
3328 {
3329 if (C4Player *pController = Game.Players.Get(iPlayer: Controller))
3330 {
3331 CallControl(pPlr: pController, byCom);
3332 }
3333 return;
3334 }
3335
3336 // COM_Special and COM_Contents specifically bypass the menu and always go to the object
3337 bool fBypassMenu = ((plainCom == COM_Special) || (byCom == COM_Contents));
3338
3339 // Menu control
3340 if (!fBypassMenu)
3341 if (Menu && Menu->Control(byCom, iData)) return;
3342
3343 // Ignore any menu com leftover in control queue from closed menu
3344 if (Inside(ival: byCom, lbound: COM_MenuNavigation1, rbound: COM_MenuNavigation2)) return;
3345
3346 // Decrease NoCollectDelay
3347 if (!(byCom & COM_Single) && !(byCom & COM_Double) && !IsRelease)
3348 if (NoCollectDelay > 0)
3349 NoCollectDelay--;
3350
3351 // COM_Contents contents shift (data is target number (not ID!))
3352 // contents shift must always be done to container object, which is not necessarily this
3353 if (byCom == COM_Contents)
3354 {
3355 C4Object *pTarget = Game.Objects.SafeObjectPointer(iNumber: iData);
3356 if (pTarget && pTarget->Contained)
3357 pTarget->Contained->DirectComContents(pTarget, fDoCalls: true);
3358 return;
3359 }
3360
3361 // Contained control (except specials)
3362 if (Contained)
3363 if (plainCom != COM_Special && plainCom != COM_Special2 && byCom != COM_WheelUp && byCom != COM_WheelDown)
3364 {
3365 Contained->Controller = Controller; ContainedControl(byCom); return;
3366 }
3367
3368 // Regular DirectCom clears commands
3369 if (!(byCom & COM_Single) && !(byCom & COM_Double) && !IsRelease)
3370 ClearCommands();
3371
3372 // Object script override
3373 C4Player *pController;
3374 if (pController = Game.Players.Get(iPlayer: Controller))
3375 if (CallControl(pPlr: pController, byCom))
3376 return;
3377
3378 // direct wheel control
3379 if (byCom == COM_WheelUp || byCom == COM_WheelDown)
3380 // scroll contents
3381 {
3382 ShiftContents(fShiftBack: byCom == COM_WheelUp, fDoCalls: true); return;
3383 }
3384
3385 // The Player updates Controller before calling this, so trust Players.Get will return it
3386 if (pController && pController->ControlStyle)
3387 {
3388 AutoStopDirectCom(byCom, iData);
3389 return;
3390 }
3391
3392 // Control by procedure
3393 switch (GetProcedure())
3394 {
3395 case DFA_WALK:
3396 switch (byCom)
3397 {
3398 case COM_Left: ObjectComMovement(cObj: this, COMD_Left); break;
3399 case COM_Right: ObjectComMovement(cObj: this, COMD_Right); break;
3400 case COM_Down: ObjectComMovement(cObj: this, COMD_Stop); break;
3401 case COM_Up: ObjectComUp(cObj: this); break;
3402 case COM_Down_D: ObjectComDownDouble(cObj: this); break;
3403 case COM_Dig_S:
3404 if (ObjectComDig(cObj: this))
3405 {
3406 Action.ComDir = (Action.Dir == DIR_Right) ? COMD_DownRight : COMD_DownLeft;
3407 }
3408 break;
3409 case COM_Dig_D: ObjectComDigDouble(cObj: this); break;
3410 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw); break;
3411 }
3412 break;
3413
3414 case DFA_FLIGHT: case DFA_KNEEL: case DFA_THROW:
3415 switch (byCom)
3416 {
3417 case COM_Left: ObjectComMovement(cObj: this, COMD_Left); break;
3418 case COM_Right: ObjectComMovement(cObj: this, COMD_Right); break;
3419 case COM_Down: ObjectComMovement(cObj: this, COMD_Stop); break;
3420 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw); break;
3421 }
3422 break;
3423
3424 case DFA_SCALE:
3425 switch (byCom)
3426 {
3427 case COM_Left:
3428 if (Action.Dir == DIR_Left) ObjectComMovement(cObj: this, COMD_Stop);
3429 else { ObjectComMovement(cObj: this, COMD_Left); ObjectComLetGo(cObj: this, xdirf: -1); }
3430 break;
3431 case COM_Right:
3432 if (Action.Dir == DIR_Right) ObjectComMovement(cObj: this, COMD_Stop);
3433 else { ObjectComMovement(cObj: this, COMD_Right); ObjectComLetGo(cObj: this, xdirf: +1); }
3434 break;
3435 case COM_Up: ObjectComMovement(cObj: this, COMD_Up); break;
3436 case COM_Down: ObjectComMovement(cObj: this, COMD_Down); break;
3437 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3438 }
3439 break;
3440
3441 case DFA_HANGLE:
3442 switch (byCom)
3443 {
3444 case COM_Left: ObjectComMovement(cObj: this, COMD_Left); break;
3445 case COM_Right: ObjectComMovement(cObj: this, COMD_Right); break;
3446 case COM_Up: ObjectComMovement(cObj: this, COMD_Stop); break;
3447 case COM_Down: ObjectComLetGo(cObj: this, xdirf: 0); break;
3448 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3449 }
3450 break;
3451
3452 case DFA_DIG:
3453 switch (byCom)
3454 {
3455 case COM_Left: if (Inside<int32_t>(ival: Action.ComDir, COMD_UpRight, COMD_Left)) Action.ComDir++; break;
3456 case COM_Right: if (Inside<int32_t>(ival: Action.ComDir, COMD_Right, COMD_UpLeft)) Action.ComDir--; break;
3457 case COM_Down: ObjectComStop(cObj: this); break;
3458 case COM_Dig_D: ObjectComDigDouble(cObj: this); break;
3459 case COM_Dig_S: Action.Data = (!Action.Data); break; // Dig mat 2 object request
3460 }
3461 break;
3462
3463 case DFA_SWIM:
3464 switch (byCom)
3465 {
3466 case COM_Left: ObjectComMovement(cObj: this, COMD_Left); break;
3467 case COM_Right: ObjectComMovement(cObj: this, COMD_Right); break;
3468 case COM_Up:
3469 ObjectComMovement(cObj: this, COMD_Up);
3470 ObjectComUp(cObj: this); break;
3471 case COM_Down: ObjectComMovement(cObj: this, COMD_Down); break;
3472 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3473 case COM_Dig_D: ObjectComDigDouble(cObj: this); break;
3474 }
3475 break;
3476
3477 case DFA_BRIDGE: case DFA_BUILD: case DFA_CHOP:
3478 switch (byCom)
3479 {
3480 case COM_Down: ObjectComStop(cObj: this); break;
3481 }
3482 break;
3483
3484 case DFA_FIGHT:
3485 switch (byCom)
3486 {
3487 case COM_Left: ObjectComMovement(cObj: this, COMD_Left); break;
3488 case COM_Right: ObjectComMovement(cObj: this, COMD_Right); break;
3489 case COM_Down: ObjectComStop(cObj: this); break;
3490 }
3491 break;
3492
3493 case DFA_PUSH:
3494 {
3495 bool fGrabControlOverload = false;
3496 if (Action.Target)
3497 {
3498 // Make sure controller is up-to-date, so if e.g. multiple people push a catapult the controller is correct for whoever issued the ControlThrow
3499 Action.Target->Controller = Controller;
3500 // New grab-control model: objects version 4.95 or higher (CE)
3501 // may overload control of grabbing clonks
3502 C4Def *pTDef = Action.Target->Def;
3503 if (CompareVersion(iVer1: pTDef->rC4XVer[0], iVer2: pTDef->rC4XVer[1], iVer3: pTDef->rC4XVer[2], iVer4: pTDef->rC4XVer[3], iVerBuild: 0, iRVer1: 4, iRVer2: 9, iRVer3: 5, iRVer4: 0) >= 0)
3504 fGrabControlOverload = true;
3505 }
3506 // Call object control first in case it overloads
3507 if (fGrabControlOverload)
3508 if (Action.Target)
3509 if (Action.Target->CallControl(pPlr: pController, byCom, pPars: {C4VObj(pObj: this)}))
3510 return;
3511 // Clonk direct control
3512 switch (byCom)
3513 {
3514 case COM_Left: ObjectComMovement(cObj: this, COMD_Left); break;
3515 case COM_Right: ObjectComMovement(cObj: this, COMD_Right); break;
3516 case COM_Up:
3517 // Target -> enter
3518 if (ObjectComEnter(cObj: Action.Target))
3519 ObjectComMovement(cObj: this, COMD_Stop);
3520 // Else, comdir up for target straightening
3521 else
3522 ObjectComMovement(cObj: this, COMD_Up);
3523 break;
3524 case COM_Down: ObjectComMovement(cObj: this, COMD_Stop); break;
3525 case COM_Down_D: ObjectComUnGrab(cObj: this); break;
3526 case COM_Throw_D:
3527 // avoid breaking objects with non-default behavior on ControlThrow
3528 if (!fGrabControlOverload || !Action.Target || Action.Target->Def->Script.GetSFunc(pIdtf: std::format(PSF_Control, args: ComName(iCom: COM_Throw)).c_str()))
3529 {
3530 break;
3531 }
3532 [[fallthrough]];
3533 case COM_Throw:
3534 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw);
3535 break;
3536 }
3537 // Action target call control late for old objects
3538 if (!fGrabControlOverload)
3539 if (Action.Target)
3540 Action.Target->CallControl(pPlr: pController, byCom, pPars: {C4VObj(pObj: this)});
3541 break;
3542 }
3543 }
3544}
3545
3546void C4Object::AutoStopDirectCom(uint8_t byCom, int32_t iData) // By DirecCom
3547{
3548 C4Player *pPlayer = Game.Players.Get(iPlayer: Controller);
3549 // Control by procedure
3550 switch (GetProcedure())
3551 {
3552 case DFA_WALK:
3553 switch (byCom)
3554 {
3555 case COM_Up: ObjectComUp(cObj: this); break;
3556 case COM_Down:
3557 // inhibit controldownsingle on freshly grabbed objects
3558 if (ObjectComDownDouble(cObj: this))
3559 pPlayer->LastCom = COM_None;
3560 break;
3561 case COM_Dig_S: ObjectComDig(cObj: this); break;
3562 case COM_Dig_D: ObjectComDigDouble(cObj: this); break;
3563 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw); break;
3564 default: AutoStopUpdateComDir();
3565 }
3566 break;
3567
3568 case DFA_FLIGHT:
3569 switch (byCom)
3570 {
3571 case COM_Throw:
3572 // Drop when pressing left, right or down
3573 if (pPlayer->PressedComs & ((1 << COM_Left) | (1 << COM_Right) | (1 << COM_Down)))
3574 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop);
3575 else
3576 // This will fail, but whatever.
3577 PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw);
3578 break;
3579 default: AutoStopUpdateComDir();
3580 }
3581 break;
3582
3583 case DFA_KNEEL: case DFA_THROW:
3584 switch (byCom)
3585 {
3586 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Throw); break;
3587 default: AutoStopUpdateComDir();
3588 }
3589 break;
3590
3591 case DFA_SCALE:
3592 switch (byCom)
3593 {
3594 case COM_Left:
3595 if (Action.Dir == DIR_Right) ObjectComLetGo(cObj: this, xdirf: -1);
3596 else AutoStopUpdateComDir();
3597 break;
3598 case COM_Right:
3599 if (Action.Dir == DIR_Left) ObjectComLetGo(cObj: this, xdirf: +1);
3600 else AutoStopUpdateComDir();
3601 break;
3602 case COM_Dig: ObjectComLetGo(cObj: this, xdirf: (Action.Dir == DIR_Left) ? +1 : -1);
3603 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3604 default: AutoStopUpdateComDir();
3605 }
3606 break;
3607
3608 case DFA_HANGLE:
3609 switch (byCom)
3610 {
3611 case COM_Down: ObjectComLetGo(cObj: this, xdirf: 0); break;
3612 case COM_Dig: ObjectComLetGo(cObj: this, xdirf: 0); break;
3613 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3614 default: AutoStopUpdateComDir();
3615 }
3616 break;
3617
3618 case DFA_DIG:
3619 switch (byCom)
3620 {
3621 // Dig mat 2 object request
3622 case COM_Throw: case COM_Dig: Action.Data = (!Action.Data); break;
3623 default: AutoStopUpdateComDir();
3624 }
3625 break;
3626
3627 case DFA_SWIM:
3628 switch (byCom)
3629 {
3630 case COM_Up:
3631 AutoStopUpdateComDir();
3632 ObjectComUp(cObj: this);
3633 break;
3634 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3635 case COM_Dig_D: ObjectComDigDouble(cObj: this); break;
3636 default: AutoStopUpdateComDir();
3637 }
3638 break;
3639
3640 case DFA_BRIDGE: case DFA_BUILD: case DFA_CHOP:
3641 switch (byCom)
3642 {
3643 case COM_Down: ObjectComStop(cObj: this); break;
3644 }
3645 break;
3646
3647 case DFA_FIGHT:
3648 switch (byCom)
3649 {
3650 case COM_Down: ObjectComStop(cObj: this); break;
3651 default: AutoStopUpdateComDir();
3652 }
3653 break;
3654
3655 case DFA_PUSH:
3656 {
3657 bool fGrabControlOverload = false;
3658 if (Action.Target)
3659 {
3660 // New grab-control model: objects version 4.95 or higher (CE)
3661 // may overload control of grabbing clonks
3662 C4Def *pTDef = Action.Target->Def;
3663 if (CompareVersion(iVer1: pTDef->rC4XVer[0], iVer2: pTDef->rC4XVer[1], iVer3: pTDef->rC4XVer[2], iVer4: pTDef->rC4XVer[3], iVerBuild: 0, iRVer1: 4, iRVer2: 9, iRVer3: 5, iRVer4: 0) >= 0)
3664 fGrabControlOverload = true;
3665 // Call object control first in case it overloads
3666 if (fGrabControlOverload)
3667 {
3668 if (Action.Target->CallControl(pPlr: pPlayer, byCom, pPars: {C4VObj(pObj: this)}))
3669 {
3670 return;
3671 }
3672 }
3673 }
3674 // Clonk direct control
3675 switch (byCom)
3676 {
3677 case COM_Up:
3678 // Target -> enter
3679 if (ObjectComEnter(cObj: Action.Target))
3680 ObjectComMovement(cObj: this, COMD_Stop);
3681 // Else, comdir up for target straightening
3682 else
3683 AutoStopUpdateComDir();
3684 break;
3685 case COM_Down:
3686 // FIXME: replace constants
3687 // ComOrder(3) is COM_Down, ComOrder(11) is COM_Down_S and ComOrder(19) is COM_Down_D
3688 if (Action.Target
3689 && !DrawCommandQuery(controller: Controller, scripthost&: Action.Target->Def->Script, mask: Action.Target->Def->Script.ControlMethod, com: 3)
3690 && !DrawCommandQuery(controller: Controller, scripthost&: Action.Target->Def->Script, mask: Action.Target->Def->Script.ControlMethod, com: 11)
3691 && !DrawCommandQuery(controller: Controller, scripthost&: Action.Target->Def->Script, mask: Action.Target->Def->Script.ControlMethod, com: 19))
3692 {
3693 ObjectComUnGrab(cObj: this);
3694 }
3695 break;
3696 case COM_Down_D: ObjectComUnGrab(cObj: this); break;
3697 case COM_Throw_D:
3698 // avoid breaking objects with non-default behavior on ControlThrow
3699 if (!fGrabControlOverload || !Action.Target || Action.Target->Def->Script.GetSFunc(pIdtf: std::format(PSF_Control, args: ComName(iCom: COM_Throw)).c_str()))
3700 {
3701 break;
3702 }
3703 [[fallthrough]];
3704 case COM_Throw: PlayerObjectCommand(plr: Owner, cmdf: C4CMD_Drop); break;
3705 default:
3706 AutoStopUpdateComDir();
3707 }
3708 // Action target call control late for old objects
3709 if (!fGrabControlOverload && Action.Target)
3710 Action.Target->CallControl(pPlr: pPlayer, byCom, pPars: {C4VObj(pObj: this)});
3711 break;
3712 }
3713 }
3714}
3715
3716void C4Object::AutoStopUpdateComDir()
3717{
3718 C4Player *pPlr = Game.Players.Get(iPlayer: Controller);
3719 if (!pPlr || pPlr->Cursor != this) return;
3720 int32_t NewComDir = Coms2ComDir(iComs: pPlr->PressedComs);
3721 if (Action.ComDir == NewComDir) return;
3722 if (NewComDir == COMD_Stop && GetProcedure() == DFA_DIG)
3723 {
3724 ObjectComStop(cObj: this);
3725 return;
3726 }
3727 ObjectComMovement(cObj: this, iComDir: NewComDir);
3728}
3729
3730bool C4Object::MenuCommand(const char *szCommand)
3731{
3732 // Native script execution
3733 if (!Def || !Status) return false;
3734 return static_cast<bool>(Def->Script.DirectExec(pObj: this, szScript: szCommand, szContext: "MenuCommand", fPassErrors: false, Strict: Def->Script.Strict));
3735}
3736
3737C4Object *C4Object::ComposeContents(C4ID id)
3738{
3739 int32_t cnt, cnt2;
3740 C4ID c_id;
3741 bool fInsufficient = false;
3742 C4Object *pObj;
3743 C4ID idNeeded = C4ID_None;
3744 int32_t iNeeded = 0;
3745 // Get def
3746 C4Def *pDef = C4Id2Def(id); if (!pDef) return nullptr;
3747 // get needed contents
3748 C4IDList NeededComponents;
3749 pDef->GetComponents(pOutList: &NeededComponents, pObjInstance: nullptr, pBuilder: this);
3750 // Check for sufficient components
3751 std::string needs{LoadResStr(id: C4ResStrTableKey::IDS_CON_BUILDMATNEED, args: pDef->GetName())};
3752 for (cnt = 0; c_id = NeededComponents.GetID(index: cnt); cnt++)
3753 if (NeededComponents.GetCount(index: cnt) > Contents.ObjectCount(id: c_id))
3754 {
3755 needs += std::format(fmt: "|{}x {}", args: NeededComponents.GetCount(index: cnt) - Contents.ObjectCount(id: c_id), args: C4Id2Def(id: c_id) ? C4Id2Def(id: c_id)->GetName() : C4IdText(id: c_id));
3756 if (!idNeeded) { idNeeded = c_id; iNeeded = NeededComponents.GetCount(index: cnt) - Contents.ObjectCount(id: c_id); }
3757 fInsufficient = true;
3758 }
3759 // Insufficient
3760 if (fInsufficient)
3761 {
3762 // BuildNeedsMaterial call to object...
3763 if (!Call(PSF_BuildNeedsMaterial, pPars: {C4VID(idVal: idNeeded), C4VInt(iVal: iNeeded)}))
3764 // ...game message if not overloaded
3765 GameMsgObject(szText: needs.c_str(), pTarget: this);
3766 // Return
3767 return nullptr;
3768 }
3769 // Remove components
3770 for (cnt = 0; c_id = NeededComponents.GetID(index: cnt); cnt++)
3771 for (cnt2 = 0; cnt2 < NeededComponents.GetCount(index: cnt); cnt2++)
3772 if (!(pObj = Contents.Find(id: c_id)))
3773 return nullptr;
3774 else
3775 pObj->AssignRemoval();
3776 // Create composed object
3777 // the object is created with default components instead of builder components
3778 // this is done because some objects (e.g. arrow packs) will set custom components during initialization, which should not be overriden
3779 return CreateContents(n_id: id);
3780}
3781
3782void C4Object::SetSolidMask(int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt, int32_t iTX, int32_t iTY)
3783{
3784 // remove osld
3785 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
3786 delete pSolidMaskData; pSolidMaskData = nullptr;
3787 // set new data
3788 SolidMask.Set(iX, iY, iWdt, iHgt, iTX, iTY);
3789 // re-put if valid
3790 if (CheckSolidMaskRect()) UpdateSolidMask(fRestoreAttachedObjects: false);
3791}
3792
3793bool C4Object::CheckSolidMaskRect()
3794{
3795 // check NewGfx only, because invalid SolidMask-rects are OK in OldGfx
3796 // the bounds-check is done in CStdDDraw::GetPixel()
3797 C4Surface *sfcGraphics = GetGraphics()->GetBitmap();
3798 SolidMask.Set(iX: std::max<int32_t>(a: SolidMask.x, b: 0), iY: std::max<int32_t>(a: SolidMask.y, b: 0), iWdt: std::min<int32_t>(a: SolidMask.Wdt, b: sfcGraphics->Wdt - SolidMask.x), iHgt: std::min<int32_t>(a: SolidMask.Hgt, b: sfcGraphics->Hgt - SolidMask.y), iTX: SolidMask.tx, iTY: SolidMask.ty);
3799 if (SolidMask.Hgt <= 0) SolidMask.Wdt = 0;
3800 return SolidMask.Wdt > 0;
3801}
3802
3803void C4Object::SyncClearance()
3804{
3805 // Misc. no-save safeties
3806 Action.t_attach = CNAT_None;
3807 InMat = MNone;
3808 t_contact = 0;
3809 // Fixed point values - precision reduction
3810 fix_x = itofix(x);
3811 fix_y = itofix(x: y);
3812 fix_r = itofix(x: r);
3813 // Update OCF
3814 SetOCF();
3815 // Menu
3816 CloseMenu(fForce: true);
3817 // Material contents
3818 MaterialContents.fill(u: 0);
3819 // reset speed of staticback-objects
3820 if (Category & C4D_StaticBack)
3821 {
3822 xdir = ydir = 0;
3823 }
3824}
3825
3826void C4Object::DrawSelectMark(C4FacetEx &cgo)
3827{
3828 // Status
3829 if (!Status) return;
3830 // No select marks in film playback
3831 if (Game.C4S.Head.Film && Game.C4S.Head.Replay) return;
3832 // target pos (parallax)
3833 int32_t cotx = cgo.TargetX, coty = cgo.TargetY; TargetPos(riTx&: cotx, riTy&: coty, fctViewport: cgo);
3834 // Output boundary
3835 if (!Inside<int32_t>(ival: x - cotx, lbound: 0, rbound: cgo.Wdt - 1)
3836 || !Inside<int32_t>(ival: y - coty, lbound: 0, rbound: cgo.Hgt - 1)) return;
3837 // Draw select marks
3838 int32_t cox = x + Shape.x - cotx + cgo.X - 2;
3839 int32_t coy = y + Shape.y - coty + cgo.Y - 2;
3840 GfxR->fctSelectMark.Draw(sfcTarget: cgo.Surface, iX: cox, iY: coy, iPhaseX: 0);
3841 GfxR->fctSelectMark.Draw(sfcTarget: cgo.Surface, iX: cox + Shape.Wdt, iY: coy, iPhaseX: 1);
3842 GfxR->fctSelectMark.Draw(sfcTarget: cgo.Surface, iX: cox, iY: coy + Shape.Hgt, iPhaseX: 2);
3843 GfxR->fctSelectMark.Draw(sfcTarget: cgo.Surface, iX: cox + Shape.Wdt, iY: coy + Shape.Hgt, iPhaseX: 3);
3844}
3845
3846void C4Object::ClearCommands()
3847{
3848 C4Command *pNext;
3849 while (Command)
3850 {
3851 pNext = Command->Next;
3852 if (!Command->iExec)
3853 delete Command;
3854 else
3855 Command->iExec = 2;
3856 Command = pNext;
3857 }
3858}
3859
3860void C4Object::ClearCommand(C4Command *pUntil)
3861{
3862 C4Command *pCom, *pNext;
3863 for (pCom = Command; pCom; pCom = pNext)
3864 {
3865 // Last one to clear
3866 if (pCom == pUntil) pNext = nullptr;
3867 // Next one to clear after this
3868 else pNext = pCom->Next;
3869 Command = pCom->Next;
3870 if (!pCom->iExec)
3871 delete pCom;
3872 else
3873 pCom->iExec = 2;
3874 }
3875}
3876
3877bool C4Object::AddCommand(int32_t iCommand, C4Object *pTarget, C4Value iTx, int32_t iTy,
3878 int32_t iUpdateInterval, C4Object *pTarget2,
3879 bool fInitEvaluation, int32_t iData, bool fAppend,
3880 int32_t iRetries, const char *szText, int32_t iBaseMode)
3881{
3882 // Command stack size safety
3883 const int32_t MaxCommandStack = 35;
3884 C4Command *pCom, *pLast; int32_t iCommands;
3885 for (pCom = Command, iCommands = 0; pCom; pCom = pCom->Next, iCommands++);
3886 if (iCommands >= MaxCommandStack) return false;
3887 // Valid command safety
3888 if (!Inside(ival: iCommand, lbound: C4CMD_First, rbound: C4CMD_Last)) return false;
3889 // Allocate and set new command
3890 pCom = new C4Command;
3891 pCom->Set(iCommand, pObj: this, pTarget, iTx, iTy, pTarget2, iData,
3892 iUpdateInterval, fEvaluated: !fInitEvaluation, iRetries, szText, iBaseMode);
3893 // Append to bottom of stack
3894 if (fAppend)
3895 {
3896 for (pLast = Command; pLast && pLast->Next; pLast = pLast->Next);
3897 if (pLast) pLast->Next = pCom;
3898 else Command = pCom;
3899 }
3900 // Add to top of command stack
3901 else
3902 {
3903 pCom->Next = Command;
3904 Command = pCom;
3905 }
3906 // Success
3907 return true;
3908}
3909
3910void C4Object::SetCommand(int32_t iCommand, C4Object *pTarget, C4Value iTx, int32_t iTy,
3911 C4Object *pTarget2, bool fControl, int32_t iData,
3912 int32_t iRetries, const char *szText)
3913{
3914 // Decrease NoCollectDelay
3915 if (NoCollectDelay > 0) NoCollectDelay--;
3916 // Clear stack
3917 ClearCommands();
3918 // Close menu
3919 if (fControl)
3920 if (!CloseMenu(fForce: false)) return;
3921 // Script overload
3922 if (fControl)
3923 if (Call(PSF_ControlCommand, pPars: {C4VString(strString: CommandName(iCommand)),
3924 C4VObj(pObj: pTarget),
3925 iTx,
3926 C4VInt(iVal: iTy),
3927 C4VObj(pObj: pTarget2),
3928 C4VInt(iVal: iData)}))
3929 return;
3930 // Inside vehicle control overload
3931 if (Contained)
3932 if (Contained->Def->VehicleControl & C4D_VehicleControl_Inside)
3933 {
3934 Contained->Controller = Controller;
3935 if (Contained->Call(PSF_ControlCommand, pPars: {C4VString(strString: CommandName(iCommand)),
3936 C4VObj(pObj: pTarget),
3937 iTx,
3938 C4VInt(iVal: iTy),
3939 C4VObj(pObj: pTarget2),
3940 C4VInt(iVal: iData),
3941 C4VObj(pObj: this)}))
3942 return;
3943 }
3944 // Outside vehicle control overload
3945 if (GetProcedure() == DFA_PUSH)
3946 if (Action.Target) if (Action.Target->Def->VehicleControl & C4D_VehicleControl_Outside)
3947 {
3948 Action.Target->Controller = Controller;
3949 if (Action.Target->Call(PSF_ControlCommand, pPars: {C4VString(strString: CommandName(iCommand)),
3950 C4VObj(pObj: pTarget),
3951 iTx,
3952 C4VInt(iVal: iTy),
3953 C4VObj(pObj: pTarget2),
3954 C4VInt(iVal: iData)}))
3955 return;
3956 }
3957 // Add new command
3958 AddCommand(iCommand, pTarget, iTx, iTy, iUpdateInterval: 0, pTarget2, fInitEvaluation: true, iData, fAppend: false, iRetries, szText, iBaseMode: C4CMD_Mode_Base);
3959}
3960
3961C4Command *C4Object::FindCommand(int32_t iCommandType)
3962{
3963 // seek all commands
3964 for (C4Command *pCom = Command; pCom; pCom = pCom->Next)
3965 if (pCom->Command == iCommandType) return pCom;
3966 // nothing found
3967 return nullptr;
3968}
3969
3970bool C4Object::ExecuteCommand()
3971{
3972 // Execute first command
3973 if (Command) Command->Execute();
3974 // Command finished: engine call
3975 if (Command && Command->Finished)
3976 Call(PSF_ControlCommandFinished, pPars: {C4VString(strString: CommandName(iCommand: Command->Command)), C4VObj(pObj: Command->Target), Command->Tx, C4VInt(iVal: Command->Ty), C4VObj(pObj: Command->Target2), C4Value(Command->Data, C4V_Any)});
3977 // Clear finished commands
3978 while (Command && Command->Finished) ClearCommand(pUntil: Command);
3979 // Done
3980 return true;
3981}
3982
3983void C4Object::AddMaterialContents(int32_t iMaterial, int32_t iAmount)
3984{
3985 // Add amount
3986 if (!Inside<int32_t>(ival: iMaterial, lbound: 0, rbound: C4MaxMaterial)) return;
3987 MaterialContents[iMaterial] += iAmount;
3988}
3989
3990void C4Object::DigOutMaterialCast(bool fRequest)
3991{
3992 // Check material contents for sufficient object cast amounts
3993 for (int32_t iMaterial = 0; iMaterial < Game.Material.Num; iMaterial++)
3994 if (MaterialContents[iMaterial])
3995 if (Game.Material.Map[iMaterial].Dig2Object != C4ID_None)
3996 if (Game.Material.Map[iMaterial].Dig2ObjectRatio != 0)
3997 if (fRequest || !Game.Material.Map[iMaterial].Dig2ObjectOnRequestOnly)
3998 if (MaterialContents[iMaterial] >= Game.Material.Map[iMaterial].Dig2ObjectRatio)
3999 {
4000 Game.CreateObject(type: Game.Material.Map[iMaterial].Dig2Object, pCreator: this, owner: NO_OWNER, x, y: y + Shape.y + Shape.Hgt, r: Random(iRange: 360));
4001 MaterialContents[iMaterial] = 0;
4002 }
4003}
4004
4005void C4Object::DrawCommand(C4Facet &cgoBar, int32_t iAlign, const char *szFunctionFormat,
4006 int32_t iCom, C4RegionList *pRegions, int32_t iPlayer,
4007 const char *szDesc, C4Facet *pfctImage)
4008{
4009 const char *cpDesc = szDesc;
4010 C4ID idDescImage = id;
4011 C4Def *pDescImageDef = nullptr;
4012 int32_t iDescImagePhase = 0;
4013 C4Facet cgoLeft, cgoRight;
4014 bool fFlash = false;
4015
4016 // Flash
4017 C4Player *pPlr;
4018 if (pPlr = Game.Players.Get(iPlayer: Owner))
4019 if (iCom == pPlr->FlashCom)
4020 fFlash = true;
4021
4022 // Get desc from def script function desc
4023 if (szFunctionFormat)
4024 cpDesc = Def->Script.GetControlDesc(szFunctionFormat, iCom, pidImage: &idDescImage, piImagePhase: &iDescImagePhase);
4025
4026 // Image def by id
4027 if (idDescImage && idDescImage != C4ID_Contents)
4028 pDescImageDef = C4Id2Def(id: idDescImage);
4029
4030 // Symbol sections
4031 cgoRight = cgoBar.TruncateSection(iAlign);
4032 if (!cgoRight.Wdt) return;
4033 if (iAlign & C4FCT_Bottom) cgoLeft = cgoRight.TruncateSection(iAlign: C4FCT_Left);
4034 else cgoLeft = cgoBar.TruncateSection(iAlign);
4035 if (!cgoLeft.Wdt) return;
4036
4037 // image drawn by caller
4038 if (pfctImage)
4039 *pfctImage = cgoRight;
4040 // Specified def
4041 else if (pDescImageDef)
4042 pDescImageDef->Draw(cgo&: cgoRight, fSelected: false, iColor: Color, pObj: nullptr, iPhaseX: iDescImagePhase); // ...use specified color, but not object.
4043 // Contents image
4044 else if (idDescImage == C4ID_Contents)
4045 {
4046 // contents object
4047 C4Object *pContents = Contents.GetObject();
4048 if (pContents)
4049 pContents->DrawPicture(cgo&: cgoRight);
4050 else
4051 DrawPicture(cgo&: cgoRight);
4052 }
4053 // Picture
4054 else
4055 DrawPicture(cgo&: cgoRight);
4056
4057 // Command
4058 if (!fFlash || Tick35 > 15)
4059 DrawCommandKey(cgo&: cgoLeft, iCom, fPressed: false,
4060 szText: Config.Graphics.ShowCommandKeys ? PlrControlKeyName(iPlayer, iControl: Com2Control(iCom), fShort: true).c_str() : nullptr);
4061
4062 // Region (both symbols)
4063 if (pRegions)
4064 pRegions->Add(iX: cgoLeft.X, iY: cgoLeft.Y, iWdt: cgoLeft.Wdt * 2, iHgt: cgoLeft.Hgt, szCaption: cpDesc ? cpDesc : GetName(), iCom);
4065}
4066
4067void C4Object::Resort()
4068{
4069 // Flag resort
4070 Unsorted = true;
4071 Game.fResortAnyObject = true;
4072 // Must not immediately resort - link change/removal would crash Game::ExecObjects
4073}
4074
4075bool C4Object::SetAction(int32_t iAct, C4Object *pTarget, C4Object *pTarget2, int32_t iCalls, bool fForce)
4076{
4077 int32_t iLastAction = Action.Act;
4078 int32_t iLastPhase = Action.Phase;
4079 C4ActionDef *pAction;
4080
4081 // Def lost actmap: idle (safety)
4082 if (!Def->ActMap) iLastAction = ActIdle;
4083
4084 // No other action
4085 if (iLastAction > ActIdle)
4086 if (Def->ActMap[iLastAction].NoOtherAction && !fForce)
4087 if (iAct != iLastAction)
4088 return false;
4089
4090 // Invalid action
4091 if (Def && !Inside<int32_t>(ival: iAct, lbound: ActIdle, rbound: Def->ActNum - 1))
4092 return false;
4093
4094 // Stop previous act sound
4095 if (iLastAction > ActIdle)
4096 if (iAct != iLastAction)
4097 if (Def->ActMap[iLastAction].Sound[0])
4098 StopSoundEffect(name: Def->ActMap[iLastAction].Sound, obj: this);
4099
4100 // Unfullcon objects no action
4101 if (Con < FullCon)
4102 if (!Def->IncompleteActivity)
4103 iAct = ActIdle;
4104
4105 // Reset action time on change
4106 if (iAct != iLastAction)
4107 {
4108 Action.Time = 0;
4109 // reset action data if procedure is changed
4110 if (((iAct > ActIdle) ? Def->ActMap[iAct].Procedure : DFA_NONE)
4111 != ((iLastAction > ActIdle) ? Def->ActMap[iLastAction].Procedure : DFA_NONE))
4112 Action.Data = 0;
4113 }
4114
4115 // Set new action
4116 Action.Act = iAct;
4117 std::fill(first: Action.Name, last: std::end(arr&: Action.Name), value: '\0');
4118 if (Action.Act > ActIdle) SCopy(szSource: Def->ActMap[Action.Act].Name, sTarget: Action.Name);
4119 Action.Phase = Action.PhaseDelay = 0;
4120
4121 // Set target if specified
4122 if (pTarget) Action.Target = pTarget;
4123 if (pTarget2) Action.Target2 = pTarget2;
4124
4125 // Set Action Facet
4126 UpdateActionFace();
4127
4128 // update flipdir
4129 if (((iLastAction > ActIdle) ? Def->ActMap[iLastAction].FlipDir : 0)
4130 != ((iAct > ActIdle) ? Def->ActMap[iAct].FlipDir : 0)) UpdateFlipDir();
4131
4132 // Start act sound
4133 if (Action.Act > ActIdle)
4134 if (Action.Act != iLastAction)
4135 if (Def->ActMap[Action.Act].Sound[0])
4136 StartSoundEffect(name: Def->ActMap[Action.Act].Sound, loop: +1, volume: 100, obj: this);
4137
4138 // Reset OCF
4139 SetOCF();
4140
4141 // Reset fixed position...
4142 fix_x = itofix(x); fix_y = itofix(x: y);
4143
4144 // issue calls
4145
4146 // Execute StartCall
4147 if (iCalls & SAC_StartCall)
4148 if (Action.Act > ActIdle)
4149 {
4150 pAction = &(Def->ActMap[Action.Act]);
4151 if (pAction->StartCall)
4152 {
4153 C4Def *pOldDef = Def;
4154 pAction->StartCall->Exec(pObj: this);
4155 // abort exeution if def changed
4156 if (Def != pOldDef || !Status) return true;
4157 }
4158 }
4159
4160 // Execute EndCall
4161 if (iCalls & SAC_EndCall && !fForce)
4162 if (iLastAction > ActIdle)
4163 {
4164 pAction = &(Def->ActMap[iLastAction]);
4165 if (pAction->EndCall)
4166 {
4167 C4Def *pOldDef = Def;
4168 pAction->EndCall->Exec(pObj: this);
4169 // abort exeution if def changed
4170 if (Def != pOldDef || !Status) return true;
4171 }
4172 }
4173
4174 // Execute AbortCall
4175 if (iCalls & SAC_AbortCall && !fForce)
4176 if (iLastAction > ActIdle)
4177 {
4178 pAction = &(Def->ActMap[iLastAction]);
4179 if (pAction->AbortCall)
4180 {
4181 C4Def *pOldDef = Def;
4182 pAction->AbortCall->Exec(pObj: this, pPars: {C4VInt(iVal: iLastPhase)});
4183 // abort exeution if def changed
4184 if (Def != pOldDef || !Status) return true;
4185 }
4186 }
4187
4188 return true;
4189}
4190
4191void C4Object::UpdateActionFace()
4192{
4193 // Default: no action face
4194 Action.Facet.Default();
4195 // Active: get action facet from action definition
4196 if (Action.Act > ActIdle)
4197 {
4198 C4ActionDef *pAction = &(Def->ActMap[Action.Act]);
4199 if (pAction->Facet.Wdt > 0)
4200 {
4201 Action.Facet.Set(nsfc: GetGraphics()->GetBitmap(dwClr: Color), nx: pAction->Facet.x, ny: pAction->Facet.y, nwdt: pAction->Facet.Wdt, nhgt: pAction->Facet.Hgt);
4202 Action.FacetX = pAction->Facet.tx;
4203 Action.FacetY = pAction->Facet.ty;
4204 }
4205 }
4206}
4207
4208bool C4Object::SetActionByName(const char *szActName,
4209 C4Object *pTarget, C4Object *pTarget2,
4210 int32_t iCalls, bool fForce)
4211{
4212 int32_t cnt;
4213 // Check for ActIdle passed by name
4214 if (SEqual(szStr1: szActName, szStr2: "ActIdle") || SEqual(szStr1: szActName, szStr2: "Idle"))
4215 return SetAction(iAct: ActIdle, pTarget: nullptr, pTarget2: nullptr, iCalls: SAC_StartCall | SAC_AbortCall, fForce);
4216 // Find act in ActMap of object
4217 for (cnt = 0; cnt < Def->ActNum; cnt++)
4218 if (SEqual(szStr1: szActName, szStr2: Def->ActMap[cnt].Name))
4219 return SetAction(iAct: cnt, pTarget, pTarget2, iCalls, fForce);
4220 return false;
4221}
4222
4223void C4Object::SetDir(int32_t iDir)
4224{
4225 // Not active
4226 if (Action.Act <= ActIdle) return;
4227 // Invalid direction
4228 if (!Inside<int32_t>(ival: iDir, lbound: 0, rbound: Def->ActMap[Action.Act].Directions - 1)) return;
4229 // Execute turn action
4230 C4ActionDef *pAction = &(Def->ActMap[Action.Act]);
4231 if (iDir != Action.Dir)
4232 if (pAction->TurnAction[0])
4233 {
4234 SetActionByName(szActName: pAction->TurnAction);
4235 }
4236 // Set dir
4237 Action.Dir = iDir;
4238 // update by flipdir?
4239 if (Def->ActMap[Action.Act].FlipDir)
4240 UpdateFlipDir();
4241 else
4242 Action.DrawDir = iDir;
4243}
4244
4245int32_t C4Object::GetProcedure()
4246{
4247 if (Action.Act <= ActIdle) return DFA_NONE;
4248 return Def->ActMap[Action.Act].Procedure;
4249}
4250
4251void GrabLost(C4Object *cObj)
4252{
4253 // Grab lost script call on target (quite hacky stuff...)
4254 cObj->Action.Target->Call(PSF_GrabLost);
4255 // Clear commands down to first PushTo (if any) in command stack
4256 for (C4Command *pCom = cObj->Command; pCom; pCom = pCom->Next)
4257 if (pCom->Next && pCom->Next->Command == C4CMD_PushTo)
4258 {
4259 cObj->ClearCommand(pUntil: pCom);
4260 break;
4261 }
4262}
4263
4264void DoGravity(C4Object *cobj, bool fFloatFriction = true);
4265
4266void C4Object::NoAttachAction()
4267{
4268 // Active objects
4269 if (Action.Act > ActIdle)
4270 {
4271 int32_t iProcedure = GetProcedure();
4272 // Scaling upwards: corner scale
4273 if (iProcedure == DFA_SCALE && Action.ComDir != COMD_Stop && ComDirLike(iComDir: Action.ComDir, COMD_Up))
4274 if (ObjectActionCornerScale(cObj: this)) return;
4275 if (iProcedure == DFA_SCALE && Action.ComDir == COMD_Left && Action.Dir == DIR_Left)
4276 if (ObjectActionCornerScale(cObj: this)) return;
4277 if (iProcedure == DFA_SCALE && Action.ComDir == COMD_Right && Action.Dir == DIR_Right)
4278 if (ObjectActionCornerScale(cObj: this)) return;
4279 // Scaling and stopped: fall off to side (avoid zuppel)
4280 if ((iProcedure == DFA_SCALE) && (Action.ComDir == COMD_Stop))
4281 {
4282 if (Action.Dir == DIR_Left)
4283 {
4284 if (ObjectActionJump(cObj: this, xdir: itofix(x: 1), ydir: Fix0, fByCom: false)) return;
4285 }
4286 else
4287 {
4288 if (ObjectActionJump(cObj: this, xdir: itofix(x: -1), ydir: Fix0, fByCom: false)) return;
4289 }
4290 }
4291 // Pushing: grab loss
4292 if (iProcedure == DFA_PUSH) GrabLost(cObj: this);
4293 // Fighting: Set last energy loss player for kill tracing (pushing people off a cliff)
4294 else if (iProcedure == DFA_FIGHT && Action.Target) LastEnergyLossCausePlayer = Action.Target->Controller;
4295 // Else jump
4296 ObjectActionJump(cObj: this, xdir, ydir, fByCom: false);
4297 }
4298 // Inactive objects, simple mobile natural gravity
4299 else
4300 {
4301 DoGravity(cobj: this);
4302 Mobile = 1;
4303 }
4304}
4305
4306bool ContactVtxCNAT(C4Object *cobj, uint8_t cnat_dir);
4307
4308void C4Object::ContactAction()
4309{
4310 // Take certain action on contact. Evaluate t_contact-CNAT and Procedure.
4311 C4Fixed last_xdir;
4312
4313 int32_t iDir;
4314 C4PhysicalInfo *pPhysical = GetPhysical();
4315
4316 // Determine Procedure
4317 if (Action.Act <= ActIdle) return;
4318 int32_t iProcedure = Def->ActMap[Action.Act].Procedure;
4319 int32_t fDisabled = Def->ActMap[Action.Act].Disabled;
4320
4321 // Hit Bottom
4322 if (t_contact & CNAT_Bottom)
4323 switch (iProcedure)
4324 {
4325 case DFA_FLIGHT:
4326 if (ydir < 0) break;
4327 // Jump: FlatHit / HardHit / Walk
4328 if ((OCF & OCF_HitSpeed4) || fDisabled)
4329 if (ObjectActionFlat(cObj: this, dir: Action.Dir)) return;
4330 if (OCF & OCF_HitSpeed3)
4331 if (ObjectActionKneel(cObj: this)) return;
4332 // Walk, but keep horizontal momentum (momentum is reset
4333 // by ObjectActionWalk) to avoid walk-jump-flipflop on
4334 // sideways corner hit if initial walk acceleration is
4335 // not enough to reach the next pixel for attachment.
4336 // Urks, all those special cases...
4337 last_xdir = xdir;
4338 ObjectActionWalk(cObj: this);
4339 xdir = last_xdir;
4340 return;
4341 case DFA_SCALE:
4342 // Scale up: try corner scale
4343 if (!ComDirLike(iComDir: Action.ComDir, COMD_Down))
4344 {
4345 if (ObjectActionCornerScale(cObj: this)) return;
4346 return;
4347 }
4348 // Any other: Stand
4349 ObjectActionStand(cObj: this);
4350 return;
4351 case DFA_DIG:
4352 // Redirect downleft/downright
4353 if (Action.ComDir == COMD_DownLeft)
4354 {
4355 Action.ComDir = COMD_Left; break;
4356 }
4357 if (Action.ComDir == COMD_DownRight)
4358 {
4359 Action.ComDir = COMD_Right; break;
4360 }
4361 // Else stop
4362 ObjectComStopDig(cObj: this);
4363 return;
4364 case DFA_SWIM:
4365 // Try corner scale out
4366 if (!GBackLiquid(x, y: y - 1))
4367 if (ObjectActionCornerScale(cObj: this)) return;
4368 return;
4369 }
4370
4371 // Hit Ceiling
4372 if (t_contact & CNAT_Top)
4373 switch (iProcedure)
4374 {
4375 case DFA_WALK:
4376 // Walk: Stop
4377 ObjectActionStand(cObj: this); return;
4378 case DFA_SCALE:
4379 // Scale: Try hangle, else stop if going upward
4380 if (ComDirLike(iComDir: Action.ComDir, COMD_Up))
4381 {
4382 if (pPhysical->CanHangle)
4383 {
4384 iDir = DIR_Left;
4385 if (Action.Dir == DIR_Left) { iDir = DIR_Right; }
4386 ObjectActionHangle(cObj: this, dir: iDir); return;
4387 }
4388 else
4389 Action.ComDir = COMD_Stop;
4390 }
4391 break;
4392 case DFA_FLIGHT:
4393 // Jump: Try hangle, else bounce off
4394 // High Speed Flight: Tumble
4395 if ((OCF & OCF_HitSpeed3) || fDisabled)
4396 {
4397 ObjectActionTumble(cObj: this, dir: Action.Dir, xdir: Fix0, ydir: Fix0); break;
4398 }
4399 if (pPhysical->CanHangle)
4400 {
4401 ObjectActionHangle(cObj: this, dir: Action.Dir); return;
4402 }
4403 break;
4404 case DFA_DIG:
4405 // Dig: Stop
4406 ObjectComStopDig(cObj: this); return;
4407 case DFA_HANGLE:
4408 Action.ComDir = COMD_Stop;
4409 break;
4410 }
4411
4412 // Hit Left Wall
4413 if (t_contact & CNAT_Left)
4414 {
4415 switch (iProcedure)
4416 {
4417 case DFA_FLIGHT:
4418 // High Speed Flight: Tumble
4419 if ((OCF & OCF_HitSpeed3) || fDisabled)
4420 {
4421 ObjectActionTumble(cObj: this, DIR_Left, xdir: FIXED100(x: +150), ydir: Fix0); break;
4422 }
4423 // Else
4424 else if (pPhysical->CanScale)
4425 {
4426 ObjectActionScale(cObj: this, DIR_Left); return;
4427 }
4428 break;
4429 case DFA_WALK:
4430 // Walk: Try scale, else stop
4431 if (ComDirLike(iComDir: Action.ComDir, COMD_Left))
4432 {
4433 if (pPhysical->CanScale)
4434 {
4435 ObjectActionScale(cObj: this, DIR_Left); return;
4436 }
4437 // Else stop
4438 Action.ComDir = COMD_Stop;
4439 }
4440 // Heading away from solid
4441 if (ComDirLike(iComDir: Action.ComDir, COMD_Right))
4442 {
4443 // Slide off
4444 ObjectActionJump(cObj: this, xdir: xdir / 2, ydir, fByCom: false);
4445 }
4446 return;
4447 case DFA_SWIM:
4448 // Try scale
4449 if (ComDirLike(iComDir: Action.ComDir, COMD_Left))
4450 if (pPhysical->CanScale)
4451 {
4452 ObjectActionScale(cObj: this, DIR_Left); return;
4453 }
4454 // Try corner scale out
4455 if (ObjectActionCornerScale(cObj: this)) return;
4456 return;
4457 case DFA_HANGLE:
4458 // Hangle: Try scale, else stop
4459 if (pPhysical->CanScale)
4460 if (ObjectActionScale(cObj: this, DIR_Left))
4461 return;
4462 Action.ComDir = COMD_Stop;
4463 return;
4464 case DFA_DIG:
4465 // Dig: Stop
4466 ObjectComStopDig(cObj: this);
4467 return;
4468 }
4469 }
4470
4471 // Hit Right Wall
4472 if (t_contact & CNAT_Right)
4473 {
4474 switch (iProcedure)
4475 {
4476 case DFA_FLIGHT:
4477 // High Speed Flight: Tumble
4478 if ((OCF & OCF_HitSpeed3) || fDisabled)
4479 {
4480 ObjectActionTumble(cObj: this, DIR_Right, xdir: FIXED100(x: -150), ydir: Fix0); break;
4481 }
4482 // Else Scale
4483 else if (pPhysical->CanScale)
4484 {
4485 ObjectActionScale(cObj: this, DIR_Right); return;
4486 }
4487 break;
4488 case DFA_WALK:
4489 // Walk: Try scale, else stop
4490 if (ComDirLike(iComDir: Action.ComDir, COMD_Right))
4491 {
4492 if (pPhysical->CanScale)
4493 {
4494 ObjectActionScale(cObj: this, DIR_Right); return;
4495 }
4496 Action.ComDir = COMD_Stop;
4497 }
4498 // Heading away from solid
4499 if (ComDirLike(iComDir: Action.ComDir, COMD_Left))
4500 {
4501 // Slide off
4502 ObjectActionJump(cObj: this, xdir: xdir / 2, ydir, fByCom: false);
4503 }
4504 return;
4505 case DFA_SWIM:
4506 // Try scale
4507 if (ComDirLike(iComDir: Action.ComDir, COMD_Right))
4508 if (pPhysical->CanScale)
4509 {
4510 ObjectActionScale(cObj: this, DIR_Right); return;
4511 }
4512 // Try corner scale out
4513 if (ObjectActionCornerScale(cObj: this)) return;
4514 // Skip to enable walk out
4515 return;
4516 case DFA_HANGLE:
4517 // Hangle: Try scale, else stop
4518 if (pPhysical->CanScale)
4519 if (ObjectActionScale(cObj: this, DIR_Right))
4520 return;
4521 Action.ComDir = COMD_Stop;
4522 return;
4523 case DFA_DIG:
4524 // Dig: Stop
4525 ObjectComStopDig(cObj: this);
4526 return;
4527 }
4528 }
4529
4530 // Unresolved Cases
4531
4532 // Flight stuck
4533 if (iProcedure == DFA_FLIGHT)
4534 {
4535 // Enforce slide free (might slide through tiny holes this way)
4536 if (!ydir)
4537 {
4538 bool fAllowDown = !(t_contact & CNAT_Bottom);
4539 if (t_contact & CNAT_Right)
4540 {
4541 ForcePosition(tx: x - 1, ty: y + fAllowDown);
4542 xdir = ydir = 0;
4543 }
4544 if (t_contact & CNAT_Left)
4545 {
4546 ForcePosition(tx: x + 1, ty: y + fAllowDown);
4547 xdir = ydir = 0;
4548 }
4549 }
4550 if (!xdir)
4551 {
4552 if (t_contact & CNAT_Top)
4553 {
4554 ForcePosition(tx: x, ty: y + 1);
4555 xdir = ydir = 0;
4556 }
4557 }
4558 }
4559}
4560
4561void Towards(C4Fixed &val, C4Fixed target, C4Fixed step)
4562{
4563 if (val == target) return;
4564 if (Abs(val: val - target) <= step) { val = target; return; }
4565 if (val < target) val += step; else val -= step;
4566}
4567
4568bool DoBridge(C4Object *clk)
4569{
4570 int32_t iBridgeTime; bool fMoveClonk, fWall; int32_t iBridgeMaterial;
4571 clk->Action.GetBridgeData(riBridgeTime&: iBridgeTime, rfMoveClonk&: fMoveClonk, rfWall&: fWall, riBridgeMaterial&: iBridgeMaterial);
4572 if (!iBridgeTime) iBridgeTime = 100; // default bridge time
4573 if (clk->Action.Time >= iBridgeTime) { ObjectActionStand(cObj: clk); return false; }
4574 // get bridge advancement
4575 int32_t dtp;
4576 if (fWall) switch (clk->Action.ComDir)
4577 {
4578 case COMD_Left: case COMD_Right: dtp = 4; fMoveClonk = false; break; // vertical wall: default 25 pixels
4579 case COMD_UpLeft: case COMD_UpRight: dtp = 5; fMoveClonk = false; break; // diagonal roof over Clonk: default 20 pixels up and 20 pixels side (28 pixels - optimized to close tunnels completely)
4580 case COMD_Up: dtp = 5; break; // horizontal roof over Clonk
4581 default: return true; // bridge procedure just for show
4582 }
4583 else switch (clk->Action.ComDir)
4584 {
4585 case COMD_Left: case COMD_Right: dtp = 5; break; // horizontal bridges: default 20 pixels
4586 case COMD_Up: dtp = 4; break; // vertical bridges: default 25 pixels (same as
4587 case COMD_UpLeft: case COMD_UpRight: dtp = 6; break; // diagonal bridges: default 16 pixels up and 16 pixels side (23 pixels)
4588 default: return true; // bridge procedure just for show
4589 }
4590 if (clk->Action.Time % dtp) return true; // no advancement in this frame
4591 // get target pos for Clonk and bridge
4592 int32_t cx = clk->x, cy = clk->y, cw = clk->Shape.Wdt, ch = clk->Shape.Hgt;
4593 int32_t tx = cx, ty = cy + ch / 2;
4594 int32_t dt;
4595 if (fMoveClonk) dt = 0; else dt = clk->Action.Time / dtp;
4596 if (fWall) switch (clk->Action.ComDir)
4597 {
4598 case COMD_Left: tx -= cw / 2; ty += -dt; break;
4599 case COMD_Right: tx += cw / 2; ty += -dt; break;
4600 case COMD_Up:
4601 {
4602 int32_t x0;
4603 if (fMoveClonk) x0 = -3; else x0 = (iBridgeTime / dtp) / -2;
4604 tx += (x0 + dt) * ((clk->Action.Dir == DIR_Right) * 2 - 1); cx += ((clk->Action.Dir == DIR_Right) * 2 - 1); ty -= ch + 3; break;
4605 }
4606 case COMD_UpLeft: tx -= -4 + dt; ty += -ch - 7 + dt; break;
4607 case COMD_UpRight: tx += -4 + dt; ty += -ch - 7 + dt; break;
4608 }
4609 else switch (clk->Action.ComDir)
4610 {
4611 case COMD_Left: tx += -2 - dt; --cx; break;
4612 case COMD_Right: tx += +2 + dt; ++cx; break;
4613 case COMD_Up: tx += (-cw / 2 + (cw - 1) * (clk->Action.Dir == DIR_Right)) * (!fMoveClonk); ty += -dt - fMoveClonk; --cy; break;
4614 case COMD_UpLeft: tx += -5 - dt + fMoveClonk * 3; ty += 2 - dt - fMoveClonk * 3; --cx; --cy; break;
4615 case COMD_UpRight: tx += +5 + dt - fMoveClonk * 2; ty += 2 - dt - fMoveClonk * 3; ++cx; --cy; break;
4616 }
4617 // check if Clonk movement is posible
4618 if (fMoveClonk)
4619 {
4620 int32_t cx2 = cx, cy2 = cy;
4621 if (clk->Shape.CheckContact(cx: cx2, cy: cy2 - 1))
4622 {
4623 // Clonk would collide here: Change to nonmoving Clonk mode and redo bridging
4624 iBridgeTime -= clk->Action.Time;
4625 clk->Action.Time = 0;
4626 if (fWall && clk->Action.ComDir == COMD_Up)
4627 {
4628 // special for roof above Clonk: The nonmoving roof is started at bridgelength before the Clonkl
4629 // so, when interrupted, an action time halfway through the action must be set
4630 clk->Action.Time = iBridgeTime;
4631 iBridgeTime += iBridgeTime;
4632 }
4633 clk->Action.SetBridgeData(iBridgeTime, fMoveClonk: false, fWall, iBridgeMaterial);
4634 return DoBridge(clk);
4635 }
4636 }
4637 // draw bridge into landscape
4638 Game.Landscape.DrawMaterialRect(mat: iBridgeMaterial, tx: tx - 2, ty, wdt: 4, hgt: 3);
4639 // Move Clonk
4640 if (fMoveClonk) clk->MovePosition(dx: cx - clk->x, dy: cy - clk->y);
4641 return true;
4642}
4643
4644void DoGravity(C4Object *cobj, bool fFloatFriction)
4645{
4646 // Floatation in liquids
4647 if (cobj->InLiquid && cobj->Def->Float)
4648 {
4649 cobj->ydir -= FloatAccel;
4650 if (cobj->ydir < FloatAccel * -10) cobj->ydir = FloatAccel * -10;
4651 if (fFloatFriction)
4652 {
4653 if (cobj->xdir < -FloatFriction) cobj->xdir += FloatFriction;
4654 if (cobj->xdir > +FloatFriction) cobj->xdir -= FloatFriction;
4655 if (cobj->rdir < -FloatFriction) cobj->rdir += FloatFriction;
4656 if (cobj->rdir > +FloatFriction) cobj->rdir -= FloatFriction;
4657 }
4658 if (!GBackLiquid(x: cobj->x, y: cobj->y - 1 + cobj->Def->Float * cobj->GetCon() / FullCon - 1))
4659 if (cobj->ydir < 0) cobj->ydir = 0;
4660 }
4661 // Free fall gravity
4662 else if (~cobj->Category & C4D_StaticBack)
4663 cobj->ydir += GravAccel;
4664}
4665
4666void StopActionDelayCommand(C4Object *cobj)
4667{
4668 ObjectComStop(cObj: cobj);
4669 cobj->AddCommand(iCommand: C4CMD_Wait, pTarget: nullptr, iTx: 0, iTy: 0, iUpdateInterval: 50);
4670}
4671
4672bool ReduceLineSegments(C4Shape &rShape, bool fAlternate)
4673{
4674 // try if line could go by a path directly when skipping on evertex. If fAlternate is true, try by skipping two vertices
4675 for (int32_t cnt = 0; cnt + 2 + fAlternate < rShape.VtxNum; cnt++)
4676 if (PathFree(x1: rShape.VtxX[cnt], y1: rShape.VtxY[cnt],
4677 x2: rShape.VtxX[cnt + 2 + fAlternate], y2: rShape.VtxY[cnt + 2 + fAlternate]))
4678 {
4679 if (fAlternate) rShape.RemoveVertex(iPos: cnt + 2);
4680 rShape.RemoveVertex(iPos: cnt + 1);
4681 return true;
4682 }
4683 return false;
4684}
4685
4686void C4Object::ExecAction()
4687{
4688 Action.t_attach = CNAT_None;
4689 uint32_t ocf;
4690 C4Fixed iTXDir;
4691 C4Fixed lftspeed, tydir;
4692 int32_t iTargetX;
4693 int32_t iPushRange, iPushDistance;
4694
4695 // Standard phase advance
4696 int32_t iPhaseAdvance = 1;
4697
4698 // Upright attachment check
4699 if (!Mobile)
4700 if (Def->UprightAttach)
4701 if (Inside<int32_t>(ival: r, lbound: -StableRange, rbound: +StableRange))
4702 {
4703 Action.t_attach |= Def->UprightAttach;
4704 Mobile = 1;
4705 }
4706
4707 // Idle objects do natural gravity only
4708 if (Action.Act <= ActIdle)
4709 {
4710 if (Mobile) DoGravity(cobj: this);
4711 return;
4712 }
4713
4714 // No IncompleteActivity? Reset action
4715 if (!(OCF & OCF_FullCon) && !Def->IncompleteActivity)
4716 {
4717 SetAction(iAct: ActIdle); return;
4718 }
4719
4720 // Determine ActDef & Physical Info
4721 C4ActionDef *pAction = &(Def->ActMap[Action.Act]);
4722 C4PhysicalInfo *pPhysical = GetPhysical();
4723 C4Fixed lLimit;
4724 C4Fixed fWalk, fMove;
4725 int32_t smpx, smpy;
4726
4727 // Energy usage
4728 if (Game.Rules & C4RULE_StructuresNeedEnergy)
4729 if (pAction->EnergyUsage)
4730 if (pAction->EnergyUsage <= Energy)
4731 {
4732 Energy -= pAction->EnergyUsage;
4733 // No general DoEnergy-Process
4734 NeedEnergy = 0;
4735 }
4736 // Insufficient energy for action: same as idle
4737 else
4738 {
4739 NeedEnergy = 1;
4740 if (Mobile) DoGravity(cobj: this);
4741 return;
4742 }
4743
4744 // Action time advance
4745 Action.Time++;
4746
4747 // InLiquidAction check
4748 if (InLiquid)
4749 if (pAction->InLiquidAction[0])
4750 {
4751 SetActionByName(szActName: pAction->InLiquidAction); return;
4752 }
4753
4754 // assign extra action attachment (CNAT_MultiAttach)
4755 // regular attachment values cannot be set for backwards compatibility reasons
4756 // this parameter had always been ignored for actions using an internal procedure,
4757 // but is for some obscure reasons set in the KneelDown-actions of the golems
4758 Action.t_attach |= (pAction->Attach & CNAT_MultiAttach);
4759
4760 // if an object is in controllable state, so it can be assumed that if it dies later because of NO_OWNER's cause,
4761 // it has been its own fault and not the fault of the last one who threw a flint on it
4762 // do not reset for burning objects to make sure the killer is set correctly if they fall out of the map while burning
4763 // also keep state when swimming, so you get kills for pushing people into the lava/acid/shark lake
4764 if (!pAction->Disabled && pAction->Procedure != DFA_FLIGHT && pAction->Procedure != DFA_SWIM && !OnFire)
4765 LastEnergyLossCausePlayer = NO_OWNER;
4766
4767 // Handle Default Action Procedure: evaluates Procedure and Action.ComDir
4768 // Update xdir,ydir,Action.Dir,attachment,iPhaseAdvance
4769 switch (pAction->Procedure)
4770 {
4771 case DFA_WALK:
4772 lLimit = ValByPhysical(iPercent: 280, iPhysical: pPhysical->Walk);
4773 switch (Action.ComDir)
4774 {
4775 case COMD_Left: case COMD_UpLeft: case COMD_DownLeft:
4776 xdir -= WalkAccel; if (xdir < -lLimit) xdir = -lLimit;
4777 break;
4778 case COMD_Right: case COMD_UpRight: case COMD_DownRight:
4779 xdir += WalkAccel; if (xdir > +lLimit) xdir = +lLimit;
4780 break;
4781 case COMD_Stop: case COMD_Up: case COMD_Down:
4782 if (xdir < 0) xdir += WalkAccel;
4783 if (xdir > 0) xdir -= WalkAccel;
4784 if ((xdir > -WalkAccel) && (xdir < +WalkAccel)) xdir = 0;
4785 break;
4786 }
4787 iPhaseAdvance = 0;
4788 if (xdir < 0) { iPhaseAdvance = -fixtoi(x: xdir * 10); SetDir(DIR_Left); }
4789 if (xdir > 0) { iPhaseAdvance = +fixtoi(x: xdir * 10); SetDir(DIR_Right); }
4790 Action.t_attach |= CNAT_Bottom;
4791 Mobile = 1;
4792 // object is rotateable? adjust to ground, if in horizontal movement or not attached to the center vertex
4793 if (Def->Rotateable && Shape.AttachMat != MNone && (!!xdir || Def->Shape.VtxX[Shape.iAttachVtx]))
4794 AdjustWalkRotation(iRangeX: 20, iRangeY: 20, iSpeed: 100);
4795 else
4796 rdir = 0;
4797 break;
4798
4799 case DFA_KNEEL:
4800 ydir = 0;
4801 Action.t_attach |= CNAT_Bottom;
4802 Mobile = 1;
4803 break;
4804
4805 case DFA_SCALE:
4806 {
4807 lLimit = ValByPhysical(iPercent: 200, iPhysical: pPhysical->Scale);
4808
4809 // Physical training
4810 if (!Tick5)
4811 if (Abs(val: ydir) == lLimit)
4812 TrainPhysical(mpiOffset: &C4PhysicalInfo::Scale, iTrainBy: 1, iMaxTrain: C4MaxPhysical);
4813 int ComDir = Action.ComDir;
4814 if (Action.Dir == DIR_Left && ComDir == COMD_Left)
4815 ComDir = COMD_Up;
4816 else if (Action.Dir == DIR_Right && ComDir == COMD_Right)
4817 ComDir = COMD_Up;
4818 switch (ComDir)
4819 {
4820 case COMD_Up: case COMD_UpRight: case COMD_UpLeft:
4821 ydir -= WalkAccel; if (ydir < -lLimit) ydir = -lLimit; break;
4822 case COMD_Down: case COMD_DownRight: case COMD_DownLeft:
4823 ydir += WalkAccel; if (ydir > +lLimit) ydir = +lLimit; break;
4824 case COMD_Left: case COMD_Right: case COMD_Stop:
4825 if (ydir < 0) ydir += WalkAccel;
4826 if (ydir > 0) ydir -= WalkAccel;
4827 if ((ydir > -WalkAccel) && (ydir < +WalkAccel)) ydir = 0;
4828 break;
4829 }
4830 iPhaseAdvance = 0;
4831 if (ydir < 0) iPhaseAdvance = -fixtoi(x: ydir * 14);
4832 if (ydir > 0) iPhaseAdvance = +fixtoi(x: ydir * 14);
4833 xdir = 0;
4834 if (Action.Dir == DIR_Left) Action.t_attach |= CNAT_Left;
4835 if (Action.Dir == DIR_Right) Action.t_attach |= CNAT_Right;
4836 Mobile = 1;
4837 break;
4838 }
4839
4840 case DFA_HANGLE:
4841 lLimit = ValByPhysical(iPercent: 160, iPhysical: pPhysical->Hangle);
4842
4843 // Physical training
4844 if (!Tick5)
4845 if (Abs(val: xdir) == lLimit)
4846 TrainPhysical(mpiOffset: &C4PhysicalInfo::Hangle, iTrainBy: 1, iMaxTrain: C4MaxPhysical);
4847
4848 switch (Action.ComDir)
4849 {
4850 case COMD_Left: case COMD_UpLeft: case COMD_DownLeft:
4851 xdir -= WalkAccel; if (xdir < -lLimit) xdir = -lLimit;
4852 break;
4853 case COMD_Right: case COMD_UpRight: case COMD_DownRight:
4854 xdir += WalkAccel; if (xdir > +lLimit) xdir = +lLimit;
4855 break;
4856 case COMD_Up:
4857 xdir += (Action.Dir == DIR_Left) ? -WalkAccel : WalkAccel;
4858 if (xdir < -lLimit) xdir = -lLimit;
4859 if (xdir > +lLimit) xdir = +lLimit;
4860 break;
4861 case COMD_Stop: case COMD_Down:
4862 if (xdir < 0) xdir += WalkAccel;
4863 if (xdir > 0) xdir -= WalkAccel;
4864 if ((xdir > -WalkAccel) && (xdir < +WalkAccel)) xdir = 0;
4865 break;
4866 }
4867 iPhaseAdvance = 0;
4868 if (xdir < 0) { iPhaseAdvance = -fixtoi(x: xdir * 10); SetDir(DIR_Left); }
4869 if (xdir > 0) { iPhaseAdvance = +fixtoi(x: xdir * 10); SetDir(DIR_Right); }
4870 ydir = 0;
4871 Action.t_attach |= CNAT_Top;
4872 Mobile = 1;
4873 break;
4874
4875 case DFA_FLIGHT:
4876 // Contained: fall out (one try only)
4877 if (!Tick10)
4878 if (Contained)
4879 {
4880 StopActionDelayCommand(cobj: this);
4881 SetCommand(iCommand: C4CMD_Exit);
4882 }
4883 // Gravity/mobile
4884 DoGravity(cobj: this);
4885 Mobile = 1;
4886 break;
4887
4888 case DFA_DIG:
4889 smpx = x; smpy = y;
4890 if (!Shape.Attach(cx&: smpx, cy&: smpy, cnat_pos: CNAT_Bottom))
4891 {
4892 ObjectComStopDig(cObj: this); return;
4893 }
4894 lLimit = ValByPhysical(iPercent: 125, iPhysical: pPhysical->Dig);
4895 iPhaseAdvance = fixtoi(x: lLimit * 40);
4896 switch (Action.ComDir)
4897 {
4898 case COMD_Up:
4899 ydir = -lLimit / 2;
4900 if (Action.Dir == DIR_Left) xdir = -lLimit;
4901 else xdir = +lLimit;
4902 break;
4903 case COMD_UpLeft: xdir = -lLimit; ydir = -lLimit / 2; break;
4904 case COMD_Left: xdir = -lLimit; ydir = 0; break;
4905 case COMD_DownLeft: xdir = -lLimit; ydir = +lLimit; break;
4906 case COMD_Down: xdir = 0; ydir = +lLimit; break;
4907 case COMD_DownRight: xdir = +lLimit; ydir = +lLimit; break;
4908 case COMD_Right: xdir = +lLimit; ydir = 0; break;
4909 case COMD_UpRight: xdir = +lLimit; ydir = -lLimit / 2; break;
4910 case COMD_Stop:
4911 xdir = 0; ydir = 0;
4912 iPhaseAdvance = 0;
4913 break;
4914 }
4915 if (xdir < 0) SetDir(DIR_Left); else if (xdir > 0) SetDir(DIR_Right);
4916 Action.t_attach = CNAT_None;
4917 Mobile = 1;
4918 break;
4919
4920 case DFA_SWIM:
4921 lLimit = ValByPhysical(iPercent: 160, iPhysical: pPhysical->Swim);
4922
4923 // Physical training
4924 if (!Tick10)
4925 if (Abs(val: xdir) == lLimit)
4926 TrainPhysical(mpiOffset: &C4PhysicalInfo::Swim, iTrainBy: 1, iMaxTrain: C4MaxPhysical);
4927
4928 // ComDir changes xdir/ydir
4929 switch (Action.ComDir)
4930 {
4931 case COMD_Up: ydir -= SwimAccel; break;
4932 case COMD_UpRight: ydir -= SwimAccel; xdir += SwimAccel; break;
4933 case COMD_Right: xdir += SwimAccel; break;
4934 case COMD_DownRight: ydir += SwimAccel; xdir += SwimAccel; break;
4935 case COMD_Down: ydir += SwimAccel; break;
4936 case COMD_DownLeft: ydir += SwimAccel; xdir -= SwimAccel; break;
4937 case COMD_Left: xdir -= SwimAccel; break;
4938 case COMD_UpLeft: ydir -= SwimAccel; xdir -= SwimAccel; break;
4939 case COMD_Stop:
4940 if (xdir < 0) xdir += SwimAccel;
4941 if (xdir > 0) xdir -= SwimAccel;
4942 if ((xdir > -SwimAccel) && (xdir < +SwimAccel)) xdir = 0;
4943 if (ydir < 0) ydir += SwimAccel;
4944 if (ydir > 0) ydir -= SwimAccel;
4945 if ((ydir > -SwimAccel) && (ydir < +SwimAccel)) ydir = 0;
4946 break;
4947 }
4948
4949 // Out of liquid check
4950 if (!InLiquid)
4951 {
4952 // Just above liquid: move down
4953 if (GBackLiquid(x, y: y + 1 + Def->Float * Con / FullCon - 1)) ydir = +SwimAccel;
4954 // Free fall: walk
4955 else { ObjectActionWalk(cObj: this); return; }
4956 }
4957
4958 // xdir/ydir bounds
4959 if (ydir < -lLimit) ydir = -lLimit; if (ydir > +lLimit) ydir = +lLimit;
4960 if (xdir > +lLimit) xdir = +lLimit; if (xdir < -lLimit) xdir = -lLimit;
4961 // Surface dir bound
4962 if (!GBackLiquid(x, y: y - 1 + Def->Float * Con / FullCon - 1)) if (ydir < 0) ydir = 0;
4963 // Dir, Phase, Attach
4964 if (xdir < 0) SetDir(DIR_Left);
4965 if (xdir > 0) SetDir(DIR_Right);
4966 iPhaseAdvance = fixtoi(x: lLimit * 10);
4967 Action.t_attach = CNAT_None;
4968 Mobile = 1;
4969
4970 break;
4971
4972 case DFA_THROW:
4973 ydir = 0; xdir = 0;
4974 Action.t_attach |= CNAT_Bottom;
4975 Mobile = 1;
4976 break;
4977
4978 case DFA_BRIDGE:
4979 {
4980 if (!DoBridge(clk: this)) return;
4981 switch (Action.ComDir)
4982 {
4983 case COMD_Left: case COMD_UpLeft: SetDir(DIR_Left); break;
4984 case COMD_Right: case COMD_UpRight: SetDir(DIR_Right); break;
4985 }
4986 ydir = 0; xdir = 0;
4987 Action.t_attach |= CNAT_Bottom;
4988 Mobile = 1;
4989 }
4990 break;
4991
4992 case DFA_BUILD:
4993 // Woa, structures can build without target
4994 if ((Category & C4D_Structure) || (Category & C4D_StaticBack))
4995 if (!Action.Target) break;
4996 // No target
4997 if (!Action.Target) { ObjectComStop(cObj: this); return; }
4998 // Target internal: container needs to support by own DFA_BUILD
4999 if (Action.Target->Contained)
5000 if ((Action.Target->Contained->GetProcedure() != DFA_BUILD)
5001 || (Action.Target->Contained->NeedEnergy))
5002 return;
5003 // Build speed
5004 int32_t iLevel;
5005 // Clonk-standard
5006 iLevel = 10;
5007 // Internal builds slower
5008 if (Action.Target->Contained) iLevel = 1;
5009 // Out of target area: stop
5010 if (!Inside<int32_t>(ival: x - (Action.Target->x + Action.Target->Shape.x), lbound: 0, rbound: Action.Target->Shape.Wdt)
5011 || !Inside<int32_t>(ival: y - (Action.Target->y + Action.Target->Shape.y), lbound: -16, rbound: Action.Target->Shape.Hgt + 16))
5012 {
5013 ObjectComStop(cObj: this); return;
5014 }
5015 // Build target
5016 if (!Action.Target->Build(iLevel, pBuilder: this))
5017 {
5018 // Cannot build because target is complete (or removed, ugh): we're done
5019 if (!Action.Target || Action.Target->Con >= FullCon)
5020 {
5021 // Stop
5022 ObjectComStop(cObj: this);
5023 // Exit target if internal
5024 if (Action.Target) if (Action.Target->Contained == this)
5025 Action.Target->SetCommand(iCommand: C4CMD_Exit);
5026 }
5027 // Cannot build because target needs material (assumeably)
5028 else
5029 {
5030 // Stop
5031 ObjectComStop(cObj: this);
5032 }
5033 return;
5034 }
5035 xdir = ydir = 0;
5036 Action.t_attach |= CNAT_Bottom;
5037 Mobile = 1;
5038 break;
5039
5040 case DFA_PUSH:
5041 // No target
5042 if (!Action.Target) { StopActionDelayCommand(cobj: this); return; }
5043 // Inside target
5044 if (Contained == Action.Target) { StopActionDelayCommand(cobj: this); return; }
5045 // Target pushing force
5046 bool fStraighten;
5047 iTXDir = 0; fStraighten = false;
5048 lLimit = ValByPhysical(iPercent: 280, iPhysical: pPhysical->Walk);
5049 switch (Action.ComDir)
5050 {
5051 case COMD_Left: case COMD_DownLeft: iTXDir = -lLimit; break;
5052 case COMD_UpLeft: fStraighten = 1; iTXDir = -lLimit; break;
5053 case COMD_Right: case COMD_DownRight: iTXDir = +lLimit; break;
5054 case COMD_UpRight: fStraighten = 1; iTXDir = +lLimit; break;
5055 case COMD_Up: fStraighten = 1; break;
5056 case COMD_Stop: case COMD_Down: iTXDir = 0; break;
5057 }
5058 // Push object
5059 if (!Action.Target->Push(txdir: iTXDir, dforce: ValByPhysical(iPercent: 250, iPhysical: pPhysical->Push), fStraighten))
5060 {
5061 StopActionDelayCommand(cobj: this); return;
5062 }
5063 // Set target controller
5064 Action.Target->Controller = Controller;
5065 // ObjectAction got hold check
5066 iPushDistance = (std::max)(a: Shape.Wdt / 2 - 8, b: 0);
5067 iPushRange = iPushDistance + 10;
5068 int32_t sax, say, sawdt, sahgt;
5069 Action.Target->GetArea(aX&: sax, aY&: say, aWdt&: sawdt, aHgt&: sahgt);
5070 // Object lost
5071 if (!Inside(ival: x - sax, lbound: -iPushRange, rbound: sawdt - 1 + iPushRange)
5072 || !Inside(ival: y - say, lbound: -iPushRange, rbound: sahgt - 1 + iPushRange))
5073 {
5074 // Wait command (why, anyway?)
5075 StopActionDelayCommand(cobj: this);
5076 // Grab lost action
5077 GrabLost(cObj: this);
5078 // Done
5079 return;
5080 }
5081 // Follow object (full xdir reset)
5082 // Vertical follow: If object moves out at top, assume it's being pushed upwards and the Clonk must run after it
5083 if (y - iPushDistance > say + sahgt && iTXDir) { if (iTXDir > 0) sax += sawdt / 2; sawdt /= 2; }
5084 // Horizontal follow
5085 iTargetX = BoundBy(bval: x, lbound: sax - iPushDistance, rbound: sax + sawdt - 1 + iPushDistance);
5086 if (x == iTargetX) xdir = 0;
5087 else { if (x < iTargetX) xdir = +lLimit; if (x > iTargetX) xdir = -lLimit; }
5088 // Phase by XDir
5089 if (xdir < 0) { iPhaseAdvance = -fixtoi(x: xdir * 10); SetDir(DIR_Left); }
5090 if (xdir > 0) { iPhaseAdvance = +fixtoi(x: xdir * 10); SetDir(DIR_Right); }
5091 // No YDir
5092 ydir = 0;
5093 // Attachment
5094 Action.t_attach |= CNAT_Bottom;
5095 // Mobile
5096 Mobile = 1;
5097 break;
5098
5099 case DFA_PULL:
5100 // No target
5101 if (!Action.Target) { StopActionDelayCommand(cobj: this); return; }
5102 // Inside target
5103 if (Contained == Action.Target) { StopActionDelayCommand(cobj: this); return; }
5104 // Target contained
5105 if (Action.Target->Contained) { StopActionDelayCommand(cobj: this); return; }
5106
5107 int32_t iPullDistance;
5108 int32_t iPullX;
5109
5110 iPullDistance = Action.Target->Shape.Wdt / 2 + Shape.Wdt / 2;
5111
5112 iTargetX = x;
5113 if (Action.ComDir == COMD_Right) iTargetX = Action.Target->x + iPullDistance;
5114 if (Action.ComDir == COMD_Left) iTargetX = Action.Target->x - iPullDistance;
5115
5116 iPullX = Action.Target->x;
5117 if (Action.ComDir == COMD_Right) iPullX = x - iPullDistance;
5118 if (Action.ComDir == COMD_Left) iPullX = x + iPullDistance;
5119
5120 fWalk = ValByPhysical(iPercent: 280, iPhysical: pPhysical->Walk);
5121
5122 fMove = 0;
5123 if (Action.ComDir == COMD_Right) fMove = +fWalk;
5124 if (Action.ComDir == COMD_Left) fMove = -fWalk;
5125
5126 iTXDir = fMove + fWalk * BoundBy<int32_t>(bval: iPullX - Action.Target->x, lbound: -10, rbound: +10) / 10;
5127
5128 // Push object
5129 if (!Action.Target->Push(txdir: iTXDir, dforce: ValByPhysical(iPercent: 250, iPhysical: pPhysical->Push), fStraighten: false))
5130 {
5131 StopActionDelayCommand(cobj: this); return;
5132 }
5133 // Set target controller
5134 Action.Target->Controller = Controller;
5135
5136 // Train pulling: com dir transfer
5137 if ((Action.Target->GetProcedure() == DFA_WALK)
5138 || (Action.Target->GetProcedure() == DFA_PULL))
5139 {
5140 Action.Target->Action.ComDir = COMD_Stop;
5141 if (iTXDir < 0) Action.Target->Action.ComDir = COMD_Left;
5142 if (iTXDir > 0) Action.Target->Action.ComDir = COMD_Right;
5143 }
5144
5145 // Pulling range
5146 iPushDistance = (std::max)(a: Shape.Wdt / 2 - 8, b: 0);
5147 iPushRange = iPushDistance + 20;
5148 Action.Target->GetArea(aX&: sax, aY&: say, aWdt&: sawdt, aHgt&: sahgt);
5149 // Object lost
5150 if (!Inside(ival: x - sax, lbound: -iPushRange, rbound: sawdt - 1 + iPushRange)
5151 || !Inside(ival: y - say, lbound: -iPushRange, rbound: sahgt - 1 + iPushRange))
5152 {
5153 // Wait command (why, anyway?)
5154 StopActionDelayCommand(cobj: this);
5155 // Grab lost action
5156 GrabLost(cObj: this);
5157 // Lose target
5158 Action.Target = nullptr;
5159 // Done
5160 return;
5161 }
5162
5163 // Move to pulling position
5164 xdir = fMove + fWalk * BoundBy<int32_t>(bval: iTargetX - x, lbound: -10, rbound: +10) / 10;
5165
5166 // Phase by XDir
5167 iPhaseAdvance = 0;
5168 if (xdir < 0) { iPhaseAdvance = -fixtoi(x: xdir * 10); SetDir(DIR_Left); }
5169 if (xdir > 0) { iPhaseAdvance = +fixtoi(x: xdir * 10); SetDir(DIR_Right); }
5170 // No YDir
5171 ydir = 0;
5172 // Attachment
5173 Action.t_attach |= CNAT_Bottom;
5174 // Mobile
5175 Mobile = 1;
5176
5177 break;
5178
5179 case DFA_CHOP:
5180 // Valid check
5181 if (!Action.Target) { ObjectActionStand(cObj: this); return; }
5182 // Chop
5183 if (!Tick3)
5184 if (!Action.Target->Chop(pByObject: this))
5185 {
5186 ObjectActionStand(cObj: this); return;
5187 }
5188 // Valid check (again, target might have been destroyed)
5189 if (!Action.Target) { ObjectActionStand(cObj: this); return; }
5190 // AtObject check
5191 ocf = OCF_Chop;
5192 if (!Action.Target->At(ctx: x, cty: y, ocf)) { ObjectActionStand(cObj: this); return; }
5193 // Position
5194 SetDir((x > Action.Target->x) ? DIR_Left : DIR_Right);
5195 xdir = ydir = 0;
5196 Action.t_attach |= CNAT_Bottom;
5197 Mobile = 1;
5198 break;
5199
5200 case DFA_FIGHT:
5201 // Valid check
5202 if (!Action.Target || (Action.Target->GetProcedure() != DFA_FIGHT))
5203 {
5204 ObjectActionStand(cObj: this); return;
5205 }
5206
5207 // Fighting through doors only if doors open
5208 if (Action.Target->Contained != Contained)
5209 if ((Contained && !Contained->EntranceStatus) || (Action.Target->Contained && !Action.Target->Contained->EntranceStatus))
5210 {
5211 ObjectActionStand(cObj: this); return;
5212 }
5213
5214 // Physical training
5215 if (!Tick5)
5216 TrainPhysical(mpiOffset: &C4PhysicalInfo::Fight, iTrainBy: 1, iMaxTrain: C4MaxPhysical);
5217
5218 // Direction
5219 if (Action.Target->x > x) SetDir(DIR_Right);
5220 if (Action.Target->x < x) SetDir(DIR_Left);
5221 // Position
5222 iTargetX = x;
5223 if (Action.Dir == DIR_Left) iTargetX = Action.Target->x + Action.Target->Shape.Wdt / 2 + 2;
5224 if (Action.Dir == DIR_Right) iTargetX = Action.Target->x - Action.Target->Shape.Wdt / 2 - 2;
5225 lLimit = ValByPhysical(iPercent: 95, iPhysical: pPhysical->Walk);
5226 if (x == iTargetX) Towards(val&: xdir, target: Fix0, step: lLimit);
5227 if (x < iTargetX) Towards(val&: xdir, target: +lLimit, step: lLimit);
5228 if (x > iTargetX) Towards(val&: xdir, target: -lLimit, step: lLimit);
5229 // Distance check
5230 if ((Abs(val: x - Action.Target->x) > Shape.Wdt)
5231 || (Abs(val: y - Action.Target->y) > Shape.Wdt))
5232 {
5233 ObjectActionStand(cObj: this); return;
5234 }
5235 // Other
5236 Action.t_attach |= CNAT_Bottom;
5237 ydir = 0;
5238 Mobile = 1;
5239 // Experience
5240 if (!Tick35) DoExperience(change: +2);
5241 break;
5242
5243 case DFA_LIFT:
5244 // Valid check
5245 if (!Action.Target) { SetAction(iAct: ActIdle); return; }
5246 // Target lifting force
5247 lftspeed = itofix(x: 2); tydir = 0;
5248 switch (Action.ComDir)
5249 {
5250 case COMD_Up: tydir = -lftspeed; break;
5251 case COMD_Stop: tydir = -GravAccel; break;
5252 case COMD_Down: tydir = +lftspeed; break;
5253 }
5254 // Lift object
5255 if (!Action.Target->Lift(tydir, dforce: FIXED100(x: 50)))
5256 {
5257 SetAction(iAct: ActIdle); return;
5258 }
5259 // Check LiftTop
5260 if (Def->LiftTop)
5261 if (Action.Target->y <= (y + Def->LiftTop))
5262 if (Action.ComDir == COMD_Up)
5263 Call(PSF_LiftTop);
5264 // General
5265 DoGravity(cobj: this);
5266 break;
5267
5268 case DFA_FLOAT:
5269 // Float speed
5270 lLimit = FIXED100(x: pPhysical->Float);
5271 // ComDir changes xdir/ydir
5272 switch (Action.ComDir)
5273 {
5274 case COMD_Up: ydir -= FloatAccel; break;
5275 case COMD_Down: ydir += FloatAccel; break;
5276 case COMD_Right: xdir += FloatAccel; break;
5277 case COMD_Left: xdir -= FloatAccel; break;
5278 case COMD_UpRight: ydir -= FloatAccel; xdir += FloatAccel; break;
5279 case COMD_DownRight: ydir += FloatAccel; xdir += FloatAccel; break;
5280 case COMD_DownLeft: ydir += FloatAccel; xdir -= FloatAccel; break;
5281 case COMD_UpLeft: ydir -= FloatAccel; xdir -= FloatAccel; break;
5282 }
5283 // xdir/ydir bounds
5284 if (ydir < -lLimit) ydir = -lLimit; if (ydir > +lLimit) ydir = +lLimit;
5285 if (xdir > +lLimit) xdir = +lLimit; if (xdir < -lLimit) xdir = -lLimit;
5286 Mobile = 1;
5287 break;
5288
5289 // ATTACH: Force position to target object
5290 // own vertex index is determined by high-order byte of action data
5291 // target vertex index is determined by low-order byte of action data
5292 case DFA_ATTACH:
5293 // No target
5294 if (!Action.Target)
5295 {
5296 if (Status)
5297 {
5298 SetAction(iAct: ActIdle);
5299 Call(PSF_AttachTargetLost);
5300 }
5301 return;
5302 }
5303
5304 // Target incomplete and no incomplete activity
5305 if (!(Action.Target->OCF & OCF_FullCon))
5306 if (!Action.Target->Def->IncompleteActivity)
5307 {
5308 SetAction(iAct: ActIdle); return;
5309 }
5310
5311 // Force containment
5312 if (Action.Target->Contained != Contained)
5313 {
5314 if (Action.Target->Contained)
5315 Enter(pTarget: Action.Target->Contained);
5316 else
5317 Exit(iX: x, iY: y, iR: r);
5318 // Target might be lost in Enter/Exit callback
5319 if (!Action.Target)
5320 {
5321 if (Status)
5322 {
5323 SetAction(iAct: ActIdle);
5324 Call(PSF_AttachTargetLost);
5325 }
5326 return;
5327 }
5328 }
5329
5330 // Force position
5331 ForcePosition(tx: Action.Target->x + Action.Target->Shape.VtxX[Action.Data & 255]
5332 - Shape.VtxX[Action.Data >> 8],
5333 ty: Action.Target->y + Action.Target->Shape.VtxY[Action.Data & 255]
5334 - Shape.VtxY[Action.Data >> 8]);
5335 // must zero motion...
5336 xdir = ydir = 0;
5337
5338 break;
5339
5340 case DFA_CONNECT:
5341 bool fBroke;
5342 fBroke = false;
5343 int32_t iConnectX, iConnectY;
5344
5345 // Line destruction check: Target missing or incomplete
5346 if (!Action.Target || (Action.Target->Con < FullCon)) fBroke = true;
5347 if (!Action.Target2 || (Action.Target2->Con < FullCon)) fBroke = true;
5348 if (fBroke)
5349 {
5350 Call(PSF_LineBreak, pPars: {C4VBool(fVal: true)});
5351 AssignRemoval();
5352 return;
5353 }
5354
5355 // Movement by Target
5356 if (Action.Target)
5357 {
5358 // Connect to vertex
5359 if (Def->Line == C4D_Line_Vertex)
5360 {
5361 iConnectX = Action.Target->x + Action.Target->Shape.GetVertexX(iVertex: Local[2].getInt());
5362 iConnectY = Action.Target->y + Action.Target->Shape.GetVertexY(iVertex: Local[2].getInt());
5363 }
5364 // Connect to bottom center
5365 else
5366 {
5367 iConnectX = Action.Target->x;
5368 iConnectY = Action.Target->y + Action.Target->Shape.Hgt / 4;
5369 }
5370 if ((iConnectX != Shape.VtxX[0]) || (iConnectY != Shape.VtxY[0]))
5371 {
5372 // Regular wrapping line
5373 if (Def->LineIntersect == 0)
5374 if (!Shape.LineConnect(tx: iConnectX, ty: iConnectY, cvtx: 0, ld: +1,
5375 oldx: Shape.VtxX[0], oldy: Shape.VtxY[0])) fBroke = true;
5376 // No-intersection line
5377 if (Def->LineIntersect == 1)
5378 {
5379 Shape.VtxX[0] = iConnectX; Shape.VtxY[0] = iConnectY;
5380 }
5381 }
5382 }
5383 // Movement by Target2
5384 if (Action.Target2)
5385 {
5386 // Connect to vertex
5387 if (Def->Line == C4D_Line_Vertex)
5388 {
5389 iConnectX = Action.Target2->x + Action.Target2->Shape.GetVertexX(iVertex: Local[3].getInt());
5390 iConnectY = Action.Target2->y + Action.Target2->Shape.GetVertexY(iVertex: Local[3].getInt());
5391 }
5392 // Connect to bottom center
5393 else
5394 {
5395 iConnectX = Action.Target2->x;
5396 iConnectY = Action.Target2->y + Action.Target2->Shape.Hgt / 4;
5397 }
5398 if ((iConnectX != Shape.VtxX[Shape.VtxNum - 1]) || (iConnectY != Shape.VtxY[Shape.VtxNum - 1]))
5399 {
5400 // Regular wrapping line
5401 if (Def->LineIntersect == 0)
5402 if (!Shape.LineConnect(tx: iConnectX, ty: iConnectY, cvtx: Shape.VtxNum - 1, ld: -1,
5403 oldx: Shape.VtxX[Shape.VtxNum - 1], oldy: Shape.VtxY[Shape.VtxNum - 1])) fBroke = true;
5404 // No-intersection line
5405 if (Def->LineIntersect == 1)
5406 {
5407 Shape.VtxX[Shape.VtxNum - 1] = iConnectX; Shape.VtxY[Shape.VtxNum - 1] = iConnectY;
5408 }
5409 }
5410 }
5411
5412 // Line fBroke
5413 if (fBroke)
5414 {
5415 Call(PSF_LineBreak);
5416 AssignRemoval();
5417 return;
5418 }
5419
5420 // Reduce line segments
5421 if (!Tick35)
5422 ReduceLineSegments(rShape&: Shape, fAlternate: !Tick2);
5423
5424 break;
5425
5426 default:
5427 // Attach
5428 if (pAction->Attach)
5429 {
5430 Action.t_attach |= pAction->Attach;
5431 xdir = ydir = 0;
5432 Mobile = 1;
5433 }
5434 // Free gravity
5435 else
5436 DoGravity(cobj: this);
5437 break;
5438 }
5439
5440 // Phase Advance (zero delay means no phase advance)
5441 if (pAction->Delay)
5442 {
5443 Action.PhaseDelay += iPhaseAdvance;
5444 if (Action.PhaseDelay >= pAction->Delay)
5445 {
5446 // Advance Phase
5447 Action.PhaseDelay = 0;
5448 Action.Phase += pAction->Step;
5449 // Phase call
5450 if (pAction->PhaseCall)
5451 {
5452 pAction->PhaseCall->Exec(pObj: this);
5453 }
5454 // Phase end
5455 if (Action.Phase >= pAction->Length)
5456 {
5457 // set new action if it's not Hold
5458 if (pAction->NextAction == ActHold)
5459 Action.Phase = pAction->Length - 1;
5460 else
5461 // Set new action
5462 SetAction(iAct: pAction->NextAction, pTarget: nullptr, pTarget2: nullptr, iCalls: SAC_StartCall | SAC_EndCall);
5463 }
5464 }
5465 }
5466
5467 return;
5468}
5469
5470bool C4Object::SetOwner(int32_t iOwner)
5471{
5472 C4Player *pPlr;
5473 // Check valid owner
5474 if (!(ValidPlr(plr: iOwner) || iOwner == NO_OWNER)) return false;
5475 // always set color, even if no owner-change is done
5476 if (iOwner != NO_OWNER)
5477 if (GetGraphics()->IsColorByOwner())
5478 {
5479 Color = Game.Players.Get(iPlayer: iOwner)->ColorDw;
5480 UpdateFace(bUpdateShape: false);
5481 }
5482 // no change?
5483 if (Owner == iOwner) return true;
5484 // remove old owner view
5485 if (ValidPlr(plr: Owner))
5486 {
5487 pPlr = Game.Players.Get(iPlayer: Owner);
5488 while (pPlr->FoWViewObjs.Remove(pObj: this));
5489 }
5490 else
5491 for (pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
5492 while (pPlr->FoWViewObjs.Remove(pObj: this));
5493 // set new owner
5494 int32_t iOldOwner = Owner;
5495 Owner = iOwner;
5496 if (Owner != NO_OWNER)
5497 // add to plr view
5498 PlrFoWActualize();
5499 // this automatically updates controller
5500 Controller = Owner;
5501 // if this is a flag flying on a base, the base must be updated
5502 if (id == C4ID_Flag) if (SEqual(szStr1: Action.Name, szStr2: "FlyBase")) if (Action.Target && Action.Target->Status)
5503 if (Action.Target->Base == iOldOwner)
5504 {
5505 Action.Target->Base = Owner;
5506 }
5507 // script callback
5508 Call(PSF_OnOwnerChanged, pPars: {C4VInt(iVal: Owner), C4VInt(iVal: iOldOwner)});
5509 // done
5510 return true;
5511}
5512
5513bool C4Object::SetPlrViewRange(int32_t iToRange)
5514{
5515 // set new range
5516 PlrViewRange = iToRange;
5517 // resort into player's FoW-repeller-list
5518 PlrFoWActualize();
5519 // success
5520 return true;
5521}
5522
5523void C4Object::PlrFoWActualize()
5524{
5525 C4Player *pPlr;
5526 // single owner?
5527 if (ValidPlr(plr: Owner))
5528 {
5529 // single player's FoW-list
5530 pPlr = Game.Players.Get(iPlayer: Owner);
5531 while (pPlr->FoWViewObjs.Remove(pObj: this));
5532 if (PlrViewRange) pPlr->FoWViewObjs.Add(nObj: this, eSort: C4ObjectList::stNone);
5533 }
5534 // no owner?
5535 else
5536 {
5537 // all players!
5538 for (pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
5539 {
5540 while (pPlr->FoWViewObjs.Remove(pObj: this));
5541 if (PlrViewRange) pPlr->FoWViewObjs.Add(nObj: this, eSort: C4ObjectList::stNone);
5542 }
5543 }
5544}
5545
5546void C4Object::SetAudibilityAt(C4FacetEx &cgo, int32_t iX, int32_t iY)
5547{
5548 if (Category & C4D_Parallax)
5549 {
5550 Audible = 0;
5551 // target pos (parallax)
5552 int32_t cotx = cgo.TargetX, coty = cgo.TargetY; TargetPos(riTx&: cotx, riTy&: coty, fctViewport: cgo);
5553 Audible = std::max<int32_t>(a: Audible, b: BoundBy(bval: 100 - 100 * Distance(iX1: cotx + cgo.Wdt / 2, iY1: coty + cgo.Hgt / 2, iX2: iX, iY2: iY) / C4SoundSystem::AudibilityRadius, lbound: 0, rbound: 100));
5554 AudiblePan = BoundBy(bval: AudiblePan + (iX - (cotx + cgo.Wdt / 2)) / 5, lbound: -100, rbound: 100);
5555 }
5556 else
5557 {
5558 Audible = Game.GraphicsSystem.GetAudibility(iX, iY, iPan: &AudiblePan);
5559 }
5560}
5561
5562int32_t C4Object::GetAudibility()
5563{
5564 if (Audible == -1)
5565 {
5566 Audible = Game.GraphicsSystem.GetAudibility(iX: x, iY: y, iPan: &AudiblePan);
5567 }
5568 return Audible;
5569}
5570
5571int32_t C4Object::GetAudiblePan()
5572{
5573 GetAudibility();
5574 return AudiblePan;
5575}
5576
5577bool C4Object::IsVisible(int32_t iForPlr, bool fAsOverlay)
5578{
5579 bool fDraw;
5580 // check overlay
5581 if (Visibility & VIS_OverlayOnly)
5582 {
5583 if (!fAsOverlay) return false;
5584 if (Visibility == VIS_OverlayOnly) return true;
5585 }
5586 // check layer
5587 if (pLayer && pLayer != this && !fAsOverlay)
5588 {
5589 fDraw = pLayer->IsVisible(iForPlr, fAsOverlay: false);
5590 if (pLayer->Visibility & VIS_LayerToggle) fDraw = !fDraw;
5591 if (!fDraw) return false;
5592 }
5593 // no flags set?
5594 if (!Visibility) return true;
5595 // check visibility
5596 fDraw = false;
5597 if (Visibility & VIS_Owner) fDraw = fDraw || (iForPlr == Owner);
5598 if (iForPlr != NO_OWNER)
5599 {
5600 // check all
5601 if (Visibility & VIS_Allies) fDraw = fDraw || (iForPlr != Owner && !Hostile(plr1: iForPlr, plr2: Owner));
5602 if (Visibility & VIS_Enemies) fDraw = fDraw || (iForPlr != Owner && Hostile(plr1: iForPlr, plr2: Owner));
5603 if (Visibility & VIS_Local) fDraw = fDraw || (Local[iForPlr / 32].getInt() & (1 << (iForPlr % 32)));
5604 }
5605 else fDraw = fDraw || (Visibility & VIS_God);
5606 return fDraw;
5607}
5608
5609bool C4Object::IsInLiquidCheck()
5610{
5611 return GBackLiquid(x, y: y + Def->Float * Con / FullCon - 1);
5612}
5613
5614void C4Object::SetRotation(int32_t nr)
5615{
5616 while (nr < 0) nr += 360; nr %= 360;
5617 // remove solid mask
5618 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
5619 // set rotation
5620 r = nr;
5621 fix_r = itofix(x: nr);
5622 // Update face
5623 UpdateFace(bUpdateShape: true);
5624}
5625
5626void C4Object::PrepareDrawing()
5627{
5628 // color modulation
5629 if (ColorMod || (BlitMode & (C4GFXBLIT_MOD2 | C4GFXBLIT_CLRSFC_MOD2))) Application.DDraw->ActivateBlitModulation(dwWithClr: ColorMod);
5630 // other blit modes
5631 Application.DDraw->SetBlitMode(BlitMode);
5632}
5633
5634void C4Object::FinishedDrawing()
5635{
5636 // color modulation
5637 Application.DDraw->DeactivateBlitModulation();
5638 // extra blitting flags
5639 Application.DDraw->ResetBlitMode();
5640}
5641
5642void C4Object::UpdateSolidMask(bool fRestoreAttachedObjects)
5643{
5644 // solidmask doesn't make sense with non-existent objects
5645 // (the solidmask has already been destroyed in AssignRemoval -
5646 // do not reset it!)
5647 if (!Status) return;
5648 // Determine necessity, update cSolidMask, put or remove mask
5649 // Mask if enabled, fullcon, no rotation, not contained
5650 if (SolidMask.Wdt > 0)
5651 if (Con >= FullCon)
5652 if (!Contained)
5653 if (!r || Def->RotatedSolidmasks)
5654 {
5655 // Recheck and put mask
5656 if (!pSolidMaskData)
5657 {
5658 pSolidMaskData = new C4SolidMask(this);
5659 }
5660 else
5661 pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
5662 pSolidMaskData->Put(fCauseInstability: true, pClipRect: nullptr, fRestoreAttachment: fRestoreAttachedObjects);
5663 return;
5664 }
5665 // Otherwise, remove and destroy mask
5666 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: false);
5667 delete pSolidMaskData; pSolidMaskData = nullptr;
5668}
5669
5670bool C4Object::Collect(C4Object *pObj)
5671{
5672 // Special: attached Flag may not be collectable
5673 if (pObj->Def->id == C4ID_Flag)
5674 if (!(Game.Rules & C4RULE_FlagRemoveable))
5675 if (pObj->Action.Act > ActIdle)
5676 if (SEqual(szStr1: pObj->Def->ActMap[pObj->Action.Act].Name, szStr2: "FlyBase"))
5677 return false;
5678 // Object enter container
5679 bool fRejectCollect;
5680 if (!pObj->Enter(pTarget: this, fCalls: true, fCopyMotion: false, pfRejectCollect: &fRejectCollect))
5681 return false;
5682 // Cancel attach (hacky)
5683 ObjectComCancelAttach(cObj: pObj);
5684 // Container Collection call
5685 Call(PSF_Collection, pPars: {C4VObj(pObj)});
5686 // Object Hit call
5687 if (pObj->Status && pObj->OCF & OCF_HitSpeed1) pObj->Call(PSF_Hit);
5688 if (pObj->Status && pObj->OCF & OCF_HitSpeed2) pObj->Call(PSF_Hit2);
5689 if (pObj->Status && pObj->OCF & OCF_HitSpeed3) pObj->Call(PSF_Hit3);
5690 // post-copy the motion of the new container
5691 if (pObj->Contained == this) pObj->CopyMotion(from: this);
5692 // done, success
5693 return true;
5694}
5695
5696bool C4Object::GrabInfo(C4Object *pFrom)
5697{
5698 // safety
5699 if (!pFrom) return false; if (!Status || !pFrom->Status) return false;
5700 // even more safety (own info: success)
5701 if (pFrom == this) return true;
5702 // only if other object has info
5703 if (!pFrom->Info) return false;
5704 // clear own info object
5705 if (Info)
5706 {
5707 Info->Retire();
5708 ClearInfo(pInfo: Info);
5709 }
5710 // remove objects from any owning crews
5711 Game.Players.ClearPointers(pObj: pFrom);
5712 Game.Players.ClearPointers(pObj: this);
5713 // set info
5714 Info = pFrom->Info; pFrom->ClearInfo(pInfo: pFrom->Info);
5715 // retire from old crew
5716 Info->Retire();
5717 // set death status
5718 Info->HasDied = !Alive;
5719 // if alive, recruit to new crew
5720 if (Alive) Info->Recruit();
5721 // make new crew member
5722 C4Player *pPlr = Game.Players.Get(iPlayer: Owner);
5723 if (pPlr) pPlr->MakeCrewMember(pObj: this);
5724 // done, success
5725 return true;
5726}
5727
5728bool C4Object::ShiftContents(bool fShiftBack, bool fDoCalls)
5729{
5730 // get current object
5731 C4Object *c_obj = Contents.GetObject();
5732 if (!c_obj) return false;
5733 // get next/previous
5734 C4ObjectLink *pLnk = fShiftBack ? (Contents.Last) : (Contents.First->Next);
5735 for (;;)
5736 {
5737 // end reached without success
5738 if (!pLnk) return false;
5739 // check object
5740 C4Object *pObj = pLnk->Obj;
5741 if (pObj->Status)
5742 if (!c_obj->CanConcatPictureWith(pOtherObject: pObj))
5743 {
5744 // object different: shift to this
5745 DirectComContents(pTarget: pObj, fDoCalls: !!fDoCalls);
5746 return true;
5747 }
5748 // next/prev item
5749 pLnk = fShiftBack ? (pLnk->Prev) : (pLnk->Next);
5750 }
5751 // not reached
5752}
5753
5754void C4Object::DirectComContents(C4Object *pTarget, bool fDoCalls)
5755{
5756 // safety
5757 if (!pTarget || !pTarget->Status || pTarget->Contained != this) return;
5758 // Desired object already at front?
5759 if (Contents.GetObject() == pTarget) return;
5760 // select object via script?
5761 if (fDoCalls)
5762 if (Call(szFunctionCall: "~ControlContents", pPars: {C4VID(idVal: pTarget->id)}))
5763 return;
5764 // default action
5765 if (!(Contents.ShiftContents(pNewFirst: pTarget))) return;
5766 // Selection sound
5767 if (fDoCalls) if (!Contents.GetObject()->Call(szFunctionCall: "~Selection", pPars: {C4VObj(pObj: this)})) StartSoundEffect(name: "Grab", loop: false, volume: 100, obj: this);
5768 // update menu with the new item in "put" entry
5769 if (Menu && Menu->IsActive() && Menu->IsContextMenu())
5770 {
5771 Menu->Refill();
5772 }
5773 // Done
5774 return;
5775}
5776
5777void C4Object::ApplyParallaxity(int32_t &riTx, int32_t &riTy, const C4Facet &fctViewport)
5778{
5779 // parallaxity by locals
5780 // special: Negative positions with parallaxity 0 mean HUD elements positioned to the right/bottom
5781 int iParX = Local[0].getInt(), iParY = Local[1].getInt();
5782 if (!iParX && x < 0)
5783 riTx = -fctViewport.Wdt;
5784 else
5785 riTx = riTx * iParX / 100;
5786 if (!iParY && y < 0)
5787 riTy = -fctViewport.Hgt;
5788 else
5789 riTy = riTy * iParY / 100;
5790}
5791
5792bool C4Object::DoSelect(bool fCursor)
5793{
5794 // selection allowed?
5795 if (CrewDisabled) return true;
5796 // select
5797 if (!fCursor) Select = 1;
5798 // do callback
5799 Call(PSF_CrewSelection, pPars: {C4VBool(fVal: false), C4VBool(fVal: fCursor)});
5800 // done
5801 return true;
5802}
5803
5804void C4Object::UnSelect(bool fCursor)
5805{
5806 // unselect
5807 if (!fCursor) Select = 0;
5808 // do callback
5809 Call(PSF_CrewSelection, pPars: {C4VBool(fVal: true), C4VBool(fVal: fCursor)});
5810}
5811
5812void C4Object::GetViewPosPar(int32_t &riX, int32_t &riY, int32_t tx, int32_t ty, const C4Facet &fctViewport)
5813{
5814 int iParX = Local[0].getInt(), iParY = Local[1].getInt();
5815 // get drawing pos, then subtract original target pos to get drawing pos on landscape
5816 if (!iParX && x < 0)
5817 // HUD element at right viewport pos
5818 riX = x + tx + fctViewport.Wdt;
5819 else
5820 // regular parallaxity
5821 riX = x - (tx * (iParX - 100) / 100);
5822 if (!iParY && y < 0)
5823 // HUD element at bottom viewport pos
5824 riY = y + ty + fctViewport.Hgt;
5825 else
5826 // regular parallaxity
5827 riY = y - (ty * (iParY - 100) / 100);
5828}
5829
5830bool C4Object::PutAwayUnusedObject(C4Object *pToMakeRoomForObject)
5831{
5832 // get unused object
5833 C4Object *pUnusedObject;
5834 C4AulFunc *pFnObj2Drop;
5835 if (pFnObj2Drop = Def->Script.GetSFunc(PSF_GetObject2Drop))
5836 pUnusedObject = pFnObj2Drop->Exec(pObj: this, pPars: pToMakeRoomForObject ? C4AulParSet{C4VObj(pObj: pToMakeRoomForObject)} : C4AulParSet{}).getObj();
5837 else
5838 {
5839 // is there any unused object to put away?
5840 if (!Contents.Last) return false;
5841 // defaultly, it's the last object in the list
5842 // (contents list cannot have invalid status-objects)
5843 pUnusedObject = Contents.Last->Obj;
5844 }
5845 // no object to put away? fail
5846 if (!pUnusedObject) return false;
5847 // grabbing something?
5848 bool fPushing = (GetProcedure() == DFA_PUSH);
5849 if (fPushing)
5850 // try to put it in there
5851 if (ObjectComPut(cObj: this, pTarget: Action.Target, pThing: pUnusedObject))
5852 return true;
5853 // in container? put in there
5854 if (Contained)
5855 {
5856 // try to put it in directly
5857 // note that this works too, if an object is grabbed inside the container
5858 if (ObjectComPut(cObj: this, pTarget: Contained, pThing: pUnusedObject))
5859 return true;
5860 // now putting didn't work - drop it outside
5861 AddCommand(iCommand: C4CMD_Drop, pTarget: pUnusedObject);
5862 AddCommand(iCommand: C4CMD_Exit);
5863 return true;
5864 }
5865 else
5866 // if uncontained, simply try to drop it
5867 // if this doesn't work, it won't ever
5868 return !!ObjectComDrop(cObj: this, pThing: pUnusedObject);
5869}
5870
5871bool C4Object::SetGraphics(const char *szGraphicsName, C4Def *pSourceDef)
5872{
5873 // safety
5874 if (!Status) return false;
5875 // default def
5876 if (!pSourceDef) pSourceDef = Def;
5877 // get graphics
5878 C4DefGraphics *pGrp = pSourceDef->Graphics.Get(szGrpName: szGraphicsName);
5879 if (!pGrp) return false;
5880 // no change? (no updates need to be done, then)
5881 if (pGraphics == pGrp) return true;
5882 // set new graphics
5883 pGraphics = pGrp;
5884 // update Color, SolidMask, etc.
5885 UpdateGraphics(fGraphicsChanged: true);
5886 // success
5887 return true;
5888}
5889
5890bool C4Object::SetGraphics(C4DefGraphics *pNewGfx, bool fTemp)
5891{
5892 // safety
5893 if (!pNewGfx) return false;
5894 // set it and update related stuff
5895 pGraphics = pNewGfx;
5896 UpdateGraphics(fGraphicsChanged: true, fTemp);
5897 return true;
5898}
5899
5900C4GraphicsOverlay *C4Object::GetGraphicsOverlay(int32_t iForID, bool fCreate)
5901{
5902 // search in list until ID is found or passed
5903 C4GraphicsOverlay *pOverlay = pGfxOverlay, *pPrevOverlay = nullptr;
5904 while (pOverlay && pOverlay->GetID() < iForID) { pPrevOverlay = pOverlay; pOverlay = pOverlay->GetNext(); }
5905 // exact match found?
5906 if (pOverlay && pOverlay->GetID() == iForID) return pOverlay;
5907 // ID has been passed: Create new if desired
5908 if (!fCreate) return nullptr;
5909 C4GraphicsOverlay *pNewOverlay = new C4GraphicsOverlay();
5910 pNewOverlay->SetID(iForID);
5911 pNewOverlay->SetNext(pOverlay);
5912 if (pPrevOverlay) pPrevOverlay->SetNext(pNewOverlay); else pGfxOverlay = pNewOverlay;
5913 // return newly created overlay
5914 return pNewOverlay;
5915}
5916
5917bool C4Object::RemoveGraphicsOverlay(int32_t iOverlayID)
5918{
5919 // search in list until ID is found or passed
5920 C4GraphicsOverlay *pOverlay = pGfxOverlay, *pPrevOverlay = nullptr;
5921 while (pOverlay && pOverlay->GetID() < iOverlayID) { pPrevOverlay = pOverlay; pOverlay = pOverlay->GetNext(); }
5922 // exact match found?
5923 if (pOverlay && pOverlay->GetID() == iOverlayID)
5924 {
5925 // remove it
5926 if (pPrevOverlay) pPrevOverlay->SetNext(pOverlay->GetNext()); else pGfxOverlay = pOverlay->GetNext();
5927 pOverlay->SetNext(nullptr); // prevents deletion of following overlays
5928 delete pOverlay;
5929 // removed
5930 return true;
5931 }
5932 // no match found
5933 return false;
5934}
5935
5936bool C4Object::HasGraphicsOverlayRecursion(const C4Object *pCheckObj) const
5937{
5938 C4Object *pGfxOvrlObj;
5939 if (pGfxOverlay)
5940 for (C4GraphicsOverlay *pGfxOvrl = pGfxOverlay; pGfxOvrl; pGfxOvrl = pGfxOvrl->GetNext())
5941 if (pGfxOvrlObj = pGfxOvrl->GetOverlayObject())
5942 {
5943 if (pGfxOvrlObj == pCheckObj) return true;
5944 if (pGfxOvrlObj->HasGraphicsOverlayRecursion(pCheckObj)) return true;
5945 }
5946 return false;
5947}
5948
5949bool C4Object::StatusActivate()
5950{
5951 // readd to main list
5952 Game.Objects.InactiveObjects.Remove(pObj: this);
5953 Status = C4OS_NORMAL;
5954 Game.Objects.Add(nObj: this);
5955 // update some values
5956 UpdateGraphics(fGraphicsChanged: false);
5957 UpdateFace(bUpdateShape: true);
5958 UpdatePos();
5959 Call(PSF_UpdateTransferZone);
5960 // done, success
5961 return true;
5962}
5963
5964bool C4Object::StatusDeactivate(bool fClearPointers)
5965{
5966 // clear particles
5967 if (FrontParticles) FrontParticles.Clear();
5968 if (BackParticles) BackParticles.Clear();
5969 // put into inactive list
5970 Game.Objects.Remove(pObj: this);
5971 Status = C4OS_INACTIVE;
5972 Game.Objects.InactiveObjects.Add(nObj: this, eSort: C4ObjectList::stMain);
5973 // if desired, clear game pointers
5974 if (fClearPointers)
5975 {
5976 // in this case, the object must also exit any container, and any contained objects must be exited
5977 ClearContentsAndContained();
5978 Game.ClearPointers(cobj: this);
5979 }
5980 else
5981 {
5982 // always clear transfer
5983 Game.TransferZones.ClearPointers(pObj: this);
5984 }
5985 // done, success
5986 return true;
5987}
5988
5989void C4Object::ClearContentsAndContained(bool fDoCalls)
5990{
5991 // exit contents from container
5992 for (auto it = Contents.begin(); it != std::default_sentinel; ++it)
5993 {
5994 (*it)->Exit(iX: x, iY: y, iR: 0, iXDir: Fix0, iYDir: Fix0, iRDir: Fix0, fCalls: fDoCalls);
5995 }
5996
5997 // remove from container *after* contents have been removed!
5998 if (Contained) Exit(iX: x, iY: y, iR: 0, iXDir: Fix0, iYDir: Fix0, iRDir: Fix0, fCalls: fDoCalls);
5999}
6000
6001bool C4Object::AdjustWalkRotation(int32_t iRangeX, int32_t iRangeY, int32_t iSpeed)
6002{
6003 int32_t iDestAngle;
6004 // attachment at middle (bottom) vertex?
6005 if (Shape.iAttachVtx < 0 || !Def->Shape.VtxX[Shape.iAttachVtx])
6006 {
6007 // evaluate floor around attachment pos
6008 int32_t iSolidLeft = 0, iSolidRight = 0;
6009 // left
6010 int32_t iXCheck = Shape.iAttachX - iRangeX;
6011 if (GBackSolid(x: iXCheck, y: Shape.iAttachY))
6012 {
6013 // up
6014 while (--iSolidLeft > -iRangeY)
6015 if (GBackSolid(x: iXCheck, y: Shape.iAttachY + iSolidLeft))
6016 {
6017 ++iSolidLeft; break;
6018 }
6019 }
6020 else
6021 // down
6022 while (++iSolidLeft < iRangeY)
6023 if (GBackSolid(x: iXCheck, y: Shape.iAttachY + iSolidLeft))
6024 {
6025 --iSolidLeft; break;
6026 }
6027 // right
6028 iXCheck += 2 * iRangeX;
6029 if (GBackSolid(x: iXCheck, y: Shape.iAttachY))
6030 {
6031 // up
6032 while (--iSolidRight > -iRangeY)
6033 if (GBackSolid(x: iXCheck, y: Shape.iAttachY + iSolidRight))
6034 {
6035 ++iSolidRight; break;
6036 }
6037 }
6038 else
6039 // down
6040 while (++iSolidRight < iRangeY)
6041 if (GBackSolid(x: iXCheck, y: Shape.iAttachY + iSolidRight))
6042 {
6043 --iSolidRight; break;
6044 }
6045 // calculate destination angle
6046 // 100% accurate for large values of Pi ;)
6047 iDestAngle = (iSolidRight - iSolidLeft) * (35 / std::max<int32_t>(a: iRangeX, b: 1));
6048 }
6049 else
6050 {
6051 // attachment at other than horizontal middle vertex: get your feet to the ground!
6052 // rotate target to large angle is OK, because rotation will stop once the real
6053 // bottom vertex hits solid ground
6054 if (Shape.VtxX[Shape.iAttachVtx] > 0)
6055 iDestAngle = -50;
6056 else
6057 iDestAngle = 50;
6058 }
6059 // move to destination angle
6060 if (Abs(val: iDestAngle - r) > 2)
6061 {
6062 rdir = itofix(x: BoundBy<int32_t>(bval: iDestAngle - r, lbound: -15, rbound: +15));
6063 rdir /= (10000 / iSpeed);
6064 }
6065 else rdir = 0;
6066 // done, success
6067 return true;
6068}
6069
6070void C4Object::UpdateInLiquid()
6071{
6072 // InLiquid check
6073 if (IsInLiquidCheck()) // In Liquid
6074 {
6075 if (!InLiquid) // Enter liquid
6076 {
6077 if (OCF & OCF_HitSpeed2) if (Mass > 3)
6078 Splash(tx: x, ty: y + 1, amt: (std::min)(a: Shape.Wdt * Shape.Hgt / 10, b: 20), pByObj: this);
6079 InLiquid = 1;
6080 }
6081 }
6082 else // Out of liquid
6083 {
6084 if (InLiquid) // Leave liquid
6085 InLiquid = 0;
6086 }
6087}
6088
6089void C4Object::AddRef(C4Value *pRef)
6090{
6091 pRef->NextRef = FirstRef;
6092 FirstRef = pRef;
6093}
6094
6095void C4Object::DelRef(const C4Value *pRef, C4Value *pNextRef)
6096{
6097 // References to objects never have HasBaseArray set
6098 if (pRef == FirstRef)
6099 FirstRef = pNextRef;
6100 else
6101 {
6102 C4Value *pVal = FirstRef;
6103 while (pVal->NextRef && pVal->NextRef != pRef)
6104 pVal = pVal->NextRef;
6105 assert(pVal->NextRef);
6106 pVal->NextRef = pNextRef;
6107 }
6108}
6109
6110StdStrBuf C4Object::GetInfoString()
6111{
6112 StdStrBuf sResult;
6113 // no info for invalid objects
6114 if (!Status) return sResult;
6115 // first part always description
6116 sResult.Copy(pnData: Def->GetDesc());
6117 // go through all effects and add their desc
6118 for (C4Effect *pEff = pEffects; pEff; pEff = pEff->pNext)
6119 {
6120 C4Value vInfo = pEff->DoCall(pObj: this, PSFS_FxInfo);
6121 if (!vInfo) continue;
6122 // debug: warn for wrong return types
6123 if (vInfo.GetType() != C4V_String)
6124 DebugLog(level: spdlog::level::warn, fmt: "Effect {}({}) on object {} (#{}) returned wrong info type {}.", args: +pEff->Name, args&: pEff->iNumber, args: Def->GetName(), args&: Number, args: std::to_underlying(value: vInfo.GetType()));
6125 // get string val
6126 C4String *psInfo = vInfo.getStr(); const char *szEffInfo;
6127 if (psInfo && (szEffInfo = psInfo->Data.getData()))
6128 if (*szEffInfo)
6129 {
6130 // OK; this effect has a desc. Add it!
6131 if (sResult.getLength()) sResult.AppendChar(cChar: '|');
6132 sResult.Append(pnData: szEffInfo);
6133 }
6134 }
6135 // done
6136 return sResult;
6137}
6138
6139void C4Object::GrabContents(C4Object *pFrom)
6140{
6141 // create a temp list of all objects and transfer it
6142 // this prevents nasty deadlocks caused by RejectEntrance-scripts
6143 C4ObjectList tmpList; tmpList.Copy(rList: pFrom->Contents);
6144 C4ObjectLink *cLnk;
6145 for (cLnk = tmpList.First; cLnk; cLnk = cLnk->Next)
6146 if (cLnk->Obj->Status)
6147 cLnk->Obj->Enter(pTarget: this);
6148}
6149
6150bool C4Object::CanConcatPictureWith(C4Object *pOtherObject)
6151{
6152 // check current definition ID
6153 if (id != pOtherObject->id) return false;
6154 // def overwrite of stack conditions
6155 int32_t allow_picture_stack = Def->AllowPictureStack;
6156 if (!(allow_picture_stack & APS_Color))
6157 {
6158 // check color if ColorByOwner (flags)
6159 if (Color != pOtherObject->Color && Def->ColorByOwner) return false;
6160 // check modulation
6161 if (ColorMod != pOtherObject->ColorMod) return false;
6162 if (BlitMode != pOtherObject->BlitMode) return false;
6163 }
6164 if (!(allow_picture_stack & APS_Graphics))
6165 {
6166 // check graphics
6167 if (pGraphics != pOtherObject->pGraphics) return false;
6168 // check any own picture rect
6169 if (PictureRect != pOtherObject->PictureRect) return false;
6170 }
6171 if (!(allow_picture_stack & APS_Name))
6172 {
6173 // check name, so zagabar's sandwiches don't stack
6174 if (GetName() != pOtherObject->GetName() && std::strcmp(s1: GetName(), s2: pOtherObject->GetName()) != 0) return false;
6175 }
6176 if (!(allow_picture_stack & APS_Overlay))
6177 {
6178 // check overlay graphics
6179 for (C4GraphicsOverlay *pOwnOverlay = pGfxOverlay; pOwnOverlay; pOwnOverlay = pOwnOverlay->GetNext())
6180 if (pOwnOverlay->IsPicture())
6181 {
6182 C4GraphicsOverlay *pOtherOverlay = pOtherObject->GetGraphicsOverlay(iForID: pOwnOverlay->GetID(), fCreate: false);
6183 if (!pOtherOverlay || !(*pOtherOverlay == *pOwnOverlay)) return false;
6184 }
6185 for (C4GraphicsOverlay *pOtherOverlay = pOtherObject->pGfxOverlay; pOtherOverlay; pOtherOverlay = pOtherOverlay->GetNext())
6186 if (pOtherOverlay->IsPicture())
6187 if (!GetGraphicsOverlay(iForID: pOtherOverlay->GetID(), fCreate: false)) return false;
6188 }
6189 // concat OK
6190 return true;
6191}
6192
6193int32_t C4Object::GetFireCausePlr()
6194{
6195 // get fire effect
6196 if (!pEffects) return NO_OWNER;
6197 C4Effect *pFire = pEffects->Get(C4Fx_Fire);
6198 if (!pFire) return NO_OWNER;
6199 // get causing player
6200 int32_t iFireCausePlr = FxFireVarCausedBy(pEffect: pFire).getInt();
6201 // return if valid
6202 if (ValidPlr(plr: iFireCausePlr)) return iFireCausePlr; else return NO_OWNER;
6203}
6204
6205void C4Object::UpdateScriptPointers()
6206{
6207 if (pEffects)
6208 pEffects->ReAssignAllCallbackFunctions();
6209}
6210
6211std::string C4Object::GetNeededMatStr(C4Object *pBuilder)
6212{
6213 C4Def *pComponent;
6214 int32_t cnt, ncnt;
6215 std::string neededMats;
6216
6217 C4IDList NeededComponents;
6218 Def->GetComponents(pOutList: &NeededComponents, pObjInstance: nullptr, pBuilder);
6219
6220 C4ID idComponent;
6221
6222 for (cnt = 0; idComponent = NeededComponents.GetID(index: cnt); cnt++)
6223 {
6224 if (NeededComponents.GetCount(index: cnt) != 0)
6225 {
6226 ncnt = NeededComponents.GetCount(index: cnt) - Component.GetIDCount(id: idComponent);
6227 if (ncnt > 0)
6228 {
6229 neededMats += std::format(fmt: "|{}x {}", args&: ncnt, args: (pComponent = C4Id2Def(id: idComponent)) ? pComponent->GetName() : C4IdText(id: idComponent));
6230 }
6231 }
6232 }
6233
6234 if (!neededMats.empty())
6235 {
6236 return LoadResStr(id: C4ResStrTableKey::IDS_CON_BUILDMATNEED, args: GetName()).append(str: std::move(neededMats));
6237 }
6238 else
6239 {
6240 return LoadResStr(id: C4ResStrTableKey::IDS_CON_BUILDMATNONE, args: GetName());
6241 }
6242}
6243
6244bool C4Object::IsPlayerObject(int32_t iPlayerNumber)
6245{
6246 bool fAnyPlr = (iPlayerNumber == NO_OWNER);
6247 // if an owner is specified: only owned objects
6248 if (fAnyPlr && !ValidPlr(plr: Owner)) return false;
6249 // and crew objects
6250 if (fAnyPlr || Owner == iPlayerNumber)
6251 {
6252 // flags are player objects
6253 if (id == C4ID_Flag) return true;
6254
6255 C4Player *pOwner = Game.Players.Get(iPlayer: Owner);
6256 if (pOwner)
6257 {
6258 if (pOwner && pOwner->Crew.IsContained(pObj: this)) return true;
6259 }
6260 else
6261 {
6262 // Do not force that the owner exists because the function must work for unjoined players (savegame resume)
6263 if (Category & C4D_CrewMember)
6264 return true;
6265 }
6266 }
6267 // otherwise, not a player object
6268 return false;
6269}
6270
6271bool C4Object::IsUserPlayerObject()
6272{
6273 // must be a player object at all
6274 if (!IsPlayerObject()) return false;
6275 // and the owner must not be a script player
6276 C4Player *pOwner = Game.Players.Get(iPlayer: Owner);
6277 if (!pOwner || pOwner->GetType() != C4PT_User) return false;
6278 // otherwise, it's a user playeer object
6279 return true;
6280}
6281