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