render: added "r_sprite_lighting_type" variable to control sprite lighting with light...
[k8vavoom.git] / source / psim / p_polyobj_movement.cpp
blobe5de5c8ea488ae9679b7ddb805c1de33f9137a76
1 //**************************************************************************
2 //**
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
9 //**
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
12 //**
13 //** This program is free software: you can redistribute it and/or modify
14 //** it under the terms of the GNU General Public License as published by
15 //** the Free Software Foundation, version 3 of the License ONLY.
16 //**
17 //** This program is distributed in the hope that it will be useful,
18 //** but WITHOUT ANY WARRANTY; without even the implied warranty of
19 //** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 //** GNU General Public License for more details.
21 //**
22 //** You should have received a copy of the GNU General Public License
23 //** along with this program. If not, see <http://www.gnu.org/licenses/>.
24 //**
25 //**************************************************************************
26 #include "../gamedefs.h"
27 #include "p_entity.h"
28 #include "p_levelinfo.h"
29 #include "p_decal.h"
30 #ifdef CLIENT
31 # include "../drawer.h"
32 #endif
35 static VCvarB dbg_pobj_unstuck_verbose("dbg_pobj_unstuck_verbose", false, "Verbose 3d pobj unstuck code?", CVAR_PreInit|CVAR_Hidden|CVAR_NoShadow);
38 enum {
39 AFF_VERT = 1u<<0,
40 AFF_MOVE = 1u<<1,
41 AFF_STUCK = 1u<<2,
43 AFF_NOTHING_ZERO = 0u,
46 struct SavedEntityData {
47 VEntity *mobj;
48 TVec Origin;
49 TVec LastMoveOrigin;
50 unsigned aflags; // AFF_xxx flags, used in movement
51 TVec spot; // used in rotator
54 static TArray<SavedEntityData> poAffectedEnitities;
57 // ////////////////////////////////////////////////////////////////////////// //
58 struct UnstuckInfo {
59 TVec uv; // unstuck vector
60 TVec unorm; // unstuck normal
61 bool blockedByOther;
62 bool normFlipped;
63 const line_t *line;
67 //==========================================================================
69 // entityDataCompareZ
71 //==========================================================================
72 static int entityDataCompareZ (const void *a, const void *b, void *) noexcept {
73 if (a == b) return 0;
74 VEntity *ea = ((const SavedEntityData *)a)->mobj;
75 VEntity *eb = ((const SavedEntityData *)b)->mobj;
76 if (ea->Origin.z < eb->Origin.z) return -1;
77 if (ea->Origin.z > eb->Origin.z) return +1;
78 return 0;
82 //==========================================================================
84 // CalcMapUnstuckVector
86 // collect walls we may stuck in
88 // returns `false` if stuck, and cannot unstuck
90 //==========================================================================
91 static void CalcMapUnstuckVector (TArray<UnstuckInfo> &uvlist, VEntity *mobj) {
92 // ignore object collisions for now
93 uvlist.reset();
94 float mybbox[4];
96 mobj->Create2DBox(mybbox);
98 // collect 1s walls
100 VLevel *XLevel = mobj->XLevel;
101 DeclareMakeBlockMapCoordsBBox2D(mybbox, xl, yl, xh, yh);
102 for (int bx = xl; bx <= xh; ++bx) {
103 for (int by = yl; by <= yh; ++by) {
104 for (auto &&it : XLevel->allBlockLines(bx, by)) {
105 line_t *ld = it.line();
106 if (ld->pobj()) continue; // ignore polyobject lines (why?)
107 if (!mobj->IsRealBlockingLine(ld)) continue;
108 int side = 0;
109 if (ld->backsector && (ld->flags&ML_TWOSIDED)) {
110 // two-sided line, check if we can step up
111 bool doAdd = false;
112 for (unsigned ff = 0; ff < 2; ++ff) {
113 const sector_t *sec = (ff ? ld->backsector : ld->frontsector);
114 vassert(sec);
115 const float fz = sec->floor.GetPointZClamped(mobj->Origin);
116 const float zdiff = fz-mobj->Origin.z;
117 if (zdiff > 0.0f && zdiff > mobj->MaxStepHeight) {
118 doAdd = true;
119 break;
121 const float cz = sec->ceiling.GetPointZClamped(mobj->Origin);
122 if (mobj->Origin.z+mobj->Height > cz) {
123 doAdd = true;
124 break;
127 if (!doAdd) continue;
128 side = ld->PointOnSide(mobj->Origin);
130 if (dbg_pobj_unstuck_verbose.asBool()) {
131 GCon->Logf(NAME_Debug, "**** mobj %s:%u, MAP LDEF #%d", mobj->GetClass()->GetName(), mobj->GetUniqueId(), (int)(ptrdiff_t)(ld-&mobj->XLevel->Lines[0]));
133 UnstuckInfo &nfo = uvlist.alloc();
134 nfo.uv = TVec(0.0f, 0.0f); // unused
135 nfo.unorm = (side ? -ld->normal : ld->normal);
136 nfo.blockedByOther = false; // unused
137 nfo.normFlipped = side;
138 nfo.line = ld;
146 //==========================================================================
148 // CalcPolyUnstuckVector
150 // calculate horizontal "unstuck vector" (delta to move)
151 // for the given 3d polyobject
153 // returns `false` if stuck, and cannot unstuck
155 // returns `true` if not stuck, or can unstuck
156 // in this case, check `uvlist.length()`, if it is zero, then we aren't stuck
158 // this is wrong, but is still better than stucking
160 //==========================================================================
161 static bool CalcPolyUnstuckVector (TArray<UnstuckInfo> &uvlist, polyobj_t *po, VEntity *mobj) {
162 uvlist.resetNoDtor();
163 //if (!po || !mobj || !po->posector) return false;
165 const float rad = mobj->GetMoveRadius();
166 if (rad <= 0.0f || mobj->Height <= 0.0f) return true; // just in case
167 const float radext = rad+0.2f;
169 const TVec mobjOrigOrigin = mobj->Origin;
170 const TVec orig2d(mobjOrigOrigin.x, mobjOrigOrigin.y);
171 float bbox2d[4];
172 mobj->Create2DBox(bbox2d);
173 bbox2d[BOX2D_TOP] = orig2d.y+rad;
174 bbox2d[BOX2D_BOTTOM] = orig2d.y-rad;
175 bbox2d[BOX2D_RIGHT] = orig2d.x+rad;
176 bbox2d[BOX2D_LEFT] = orig2d.x-rad;
178 // if no "valid" sides to unstuck found, but has some "invalid" ones, try "invalid" sides
179 bool wasIntersect = false;
180 const float mobjz0 = mobj->Origin.z;
181 //const float mobjz1 = mobjz0+mobj->Height;
182 const float ptopz = po->poceiling.minz;
184 const float crmult[4][2] = {
185 { -1.0f, -1.0f }, // bottom-left
186 { +1.0f, -1.0f }, // bottom-right
187 { -1.0f, +1.0f }, // top-left
188 { +1.0f, +1.0f }, // top-right
191 const char *crnames[4] = { "bottom-left", "bottom-right", "top-left", "top-right" };
193 for (auto &&lit : po->LineFirst()) {
194 const line_t *ld = lit.line();
196 // if we are above the polyobject, check for blocking top texture
197 if (!mobj->Check3DPObjLineBlocked(po, ld)) continue;
198 wasIntersect = true;
200 const float orgsdist = ld->PointDistance(orig2d);
202 // if this is blocking toptex, we need to check side
203 // otherwise, we cannot be "inside", and should move out of the front side of the wall
204 bool badSide = false;
205 if ((ld->flags&ML_CLIP_MIDTEX) != 0 && mobjz0 >= ptopz) {
206 badSide = (orgsdist < 0.0f);
209 if (dbg_pobj_unstuck_verbose.asBool()) {
210 GCon->Logf(NAME_Debug, "mobj '%s': going to unstuck from pobj %d, line #%d, orgsdist=%g; badSide=%d",
211 mobj->GetClass()->GetName(), po->tag, (int)(ptrdiff_t)(ld-&mobj->XLevel->Lines[0]), orgsdist, (int)badSide);
214 // check 4 corners, find the shortest "unstuck" distance
215 bool foundVector = false;
216 for (unsigned cridx = 0u; cridx < 4u; ++cridx) {
217 TVec corner = orig2d;
218 corner += TVec(radext*crmult[cridx][0], radext*crmult[cridx][1], 0.0f);
219 float csdist = ld->PointDistance(corner);
220 TVec uv;
222 if (!badSide) {
223 if (csdist > 0.0f) continue;
224 csdist -= 0.01f;
225 uv = ld->normal*(-csdist);
226 } else {
227 // 3d midtex
228 if (csdist < 0.0f) continue;
229 csdist += 0.01f;
231 uv = ld->normal*(-csdist);
233 // check if we'll stuck in some other pobj line
234 bool stuckOther = false;
236 mobj->Origin = mobjOrigOrigin+uv;
237 for (auto &&lxx : po->LineFirst()) {
238 const line_t *lnx = lxx.line();
239 if (lnx == ld) continue;
240 if (!mobj->Check3DPObjLineBlocked(po, lnx)) continue;
241 if (dbg_pobj_unstuck_verbose.asBool()) {
242 GCon->Logf(NAME_Debug, "mobj '%s': going to unstuck from pobj %d, line #%d, corner %u (%s); stuck in line #%d",
243 mobj->GetClass()->GetName(), po->tag, (int)(ptrdiff_t)(ld-&mobj->XLevel->Lines[0]), cridx, crnames[cridx],
244 (int)(ptrdiff_t)(lnx-&mobj->XLevel->Lines[0]));
246 stuckOther = true;
247 break;
249 mobj->Origin = mobjOrigOrigin;
252 if (dbg_pobj_unstuck_verbose.asBool()) {
253 GCon->Logf(NAME_Debug, "mobj '%s': going to unstuck from pobj %d, line #%d, corner %u (%s); orig2d=(%g,%g); rad=%g (%g); cridx=%u; csdist=%g; uvec=(%g,%g); ulen=%g; unorm=(%g,%g)",
254 mobj->GetClass()->GetName(), po->tag, (int)(ptrdiff_t)(ld-&mobj->XLevel->Lines[0]), cridx, crnames[cridx],
255 orig2d.x, orig2d.y, rad, radext, cridx, csdist, uv.x, uv.y, uv.length2D(),
256 ld->normal.x*(badSide ? -1.0f : 1.0f), ld->normal.y*(badSide ? -1.0f : 1.0f));
259 // append to the list
261 UnstuckInfo &nfo = uvlist.alloc();
262 nfo.uv = uv;
263 nfo.unorm = (badSide ? -ld->normal : ld->normal); // pobj sides points to outside
264 nfo.blockedByOther = stuckOther;
265 nfo.normFlipped = badSide;
266 nfo.line = (foundVector ? nullptr : ld);
268 foundVector = true;
271 if (!foundVector) return false; // oops
274 if (wasIntersect) return (uvlist.length() > 0);
275 return true;
279 //==========================================================================
281 // checkCrushMObj
283 // crush mobj; returns `false` if still blocked
285 //==========================================================================
286 static bool checkCrushMObj (polyobj_t *po, VEntity *mobj, bool vertical, TVec thrustDir=TVec(0.0f, 0.0f)) {
287 if (!mobj || mobj->IsGoingToDie()) return true; // not blocked
288 // for solids, `PolyThrustMobj()` will do the job, otherwise call `PolyCrushMobj()`
289 if (mobj->EntityFlags&VEntity::EF_Solid) {
290 // `PolyThrustMobj()` returns `false` if mobj was killed
291 // vertical with 3d pobj means "crush instantly"
292 if (mobj->Level->eventPolyThrustMobj(mobj, thrustDir, po, vertical)) return false; // blocked
293 } else {
294 // non-solid object can't stop us
295 mobj->Level->eventPolyCrushMobj(mobj, po);
297 // just in case: blocked if not crushed
298 return !(mobj->PObjNeedPositionCheck() && mobj->Health > 0 && mobj->Height > 0.0f && mobj->GetMoveRadius() > 0.0f);
302 //==========================================================================
304 // unstuckVectorCompare
306 //==========================================================================
307 static int unstuckVectorCompare (const void *aa, const void *bb, void */*udata*/) {
308 if (aa == bb) return 0;
309 const UnstuckInfo *a = (const UnstuckInfo *)aa;
310 const UnstuckInfo *b = (const UnstuckInfo *)bb;
311 // sort by length
312 const float alensq = a->uv.length2DSquared();
313 const float blensq = b->uv.length2DSquared();
314 if (alensq < blensq) return -1;
315 if (alensq > blensq) return +1;
316 return 0;
320 //==========================================================================
322 // DoUnstuckByAverage
324 // returns `false` if can't
326 //==========================================================================
327 static bool DoUnstuckByAverage (TArray<UnstuckInfo> &uvlist, VEntity *mobj) {
328 tmtrace_t tmtrace;
329 const TVec origOrigin = mobj->Origin;
331 // try to unstick by average vector
332 TVec nsum(0.0f, 0.0f, 0.0f);
333 for (auto &&nfo : uvlist) if (nfo.line) nsum += nfo.unorm;
334 nsum.z = 0.0f; // just in case
335 if (nsum.isValid() && !nsum.isZero2D()) {
336 nsum = nsum.normalise();
337 // good unstuck normal
338 #if 0
339 // calculate distance to move away
340 float mybbox[4];
341 mobj->Create2DBox(mybbox);
342 float ndist = 0.0f;
343 for (auto &&nfo : uvlist) {
344 if (!nfo.line) continue;
345 const bool blocked = mobj->Check3DPObjLineBlocked(nfo.line->pobj(), nfo.line);
346 if (!blocked) continue;
347 const TVec p0 = nfo.line->get2DBBoxRejectPoint(mybbox);
348 const TVec p1 = nfo.line->get2DBBoxAcceptPoint(mybbox);
349 const float tm0 = nfo.line->LineIntersectTime(p0, p0+nsum);
350 float tm1 = nfo.line->LineIntersectTime(p1, p1+nsum);
351 float tm;
352 if (!isFiniteF(tm0)) {
353 if (!isFiniteF(tm1)) continue;
354 tm = fabsf(tm1);
355 } else {
356 tm = fabsf(tm0);
357 if (isFiniteF(tm1)) {
358 tm1 = fabsf(tm1);
359 if (tm < tm1) tm = tm1;
362 if (tm > ndist) ndist = tm;
364 if (dbg_pobj_unstuck_verbose.asBool()) {
365 GCon->Logf(NAME_Debug, "+++ %s: trying by normal (%g,%g,%g); ndist=%g", mobj->GetClass()->GetName(), nsum.x, nsum.y, nsum.z, ndist);
367 // try calculated distance
368 if (ndist > 0.0f) {
369 ndist += 0.02f;
370 mobj->Origin = origOrigin+nsum*ndist;
371 bool ok = true;
372 for (auto &&nfo : uvlist) {
373 if (nfo.line) {
374 ok = !mobj->Check3DPObjLineBlocked(nfo.line->pobj(), nfo.line);
375 if (!ok) break;
378 if (ok) {
379 ok = mobj->CheckRelPosition(tmtrace, mobj->Origin, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
381 if (ok) return true;
383 #endif
385 // try to move by 1/3 of radius
386 const float maxdist = mobj->GetMoveRadius()/3.0f;
387 TVec goodPos(0.0f, 0.0f, 0.0f);
388 bool goodPosFound = false;
389 if (maxdist > 0.0f) {
390 //FIXME: use binary search for now
391 float maxlow = 0.0f;
392 float maxhigh = maxdist;
393 if (dbg_pobj_unstuck_verbose.asBool()) {
394 GCon->Logf(NAME_Debug, "%s: trying by normal (%g,%g,%g); maxdist=%g", mobj->GetClass()->GetName(), nsum.x, nsum.y, nsum.z, maxhigh);
396 while (maxlow < maxhigh && maxhigh-maxlow > 0.001f) {
397 float maxmid = (maxlow+maxhigh)*0.5f;
398 mobj->Origin = origOrigin+nsum*maxmid;
399 bool ok = true;
400 for (auto &&nfo : uvlist) {
401 if (nfo.line) {
402 ok = !mobj->Check3DPObjLineBlocked(nfo.line->pobj(), nfo.line);
403 if (!ok) break;
406 if (ok) {
407 ok = mobj->CheckRelPosition(tmtrace, mobj->Origin, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
409 if (dbg_pobj_unstuck_verbose.asBool()) {
410 GCon->Logf(NAME_Debug, " ok=%d; maxmid=%g; dist=%g", (int)ok, maxmid, (origOrigin-mobj->Origin).length2D());
412 if (ok) {
413 // not blocked
414 const float sqdist = (origOrigin-mobj->Origin).length2DSquared();
415 goodPosFound = true;
416 goodPos = mobj->Origin;
417 if (sqdist <= 0.1f*0.1f) break;
418 // shrink
419 maxhigh = maxmid;
420 } else {
421 // blocked
422 maxlow = maxmid;
425 if (goodPosFound) {
426 if (dbg_pobj_unstuck_verbose.asBool()) {
427 GCon->Logf(NAME_Debug, "%s: found by normal (%g,%g,%g); dist=%g", mobj->GetClass()->GetName(), nsum.x, nsum.y, nsum.z, (origOrigin-goodPos).length2D());
429 return true;
434 // try non-average
435 // sort unstuck vectors by distance
436 smsort_r(uvlist.ptr(), uvlist.length(), sizeof(UnstuckInfo), &unstuckVectorCompare, nullptr);
438 // try each unstuck vector
439 //bool wasAtLeastOneGood = false;
440 for (auto &&nfo : uvlist) {
441 if (nfo.blockedByOther) continue; // bad line
442 const TVec uv = nfo.uv;
443 vassert(uv.isValid());
444 if (uv.isZero2D()) continue; // just in case
445 //wasAtLeastOneGood = true;
446 if (dbg_pobj_unstuck_verbose.asBool()) {
447 GCon->Logf(NAME_Debug, "mobj '%s' unstuck move: (%g,%g,%g)", mobj->GetClass()->GetName(), uv.x, uv.y, uv.z);
449 // need to move
450 //TODO: move any touching objects too
451 mobj->Origin = origOrigin+uv;
452 const bool ok = mobj->CheckRelPosition(tmtrace, mobj->Origin, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
453 if (ok) return true;
456 // restore
457 mobj->Origin = origOrigin;
458 return false;
462 //==========================================================================
464 // UnstuckFromRotatedPObj
466 // returns `false` if movement was blocked
468 //==========================================================================
469 static bool UnstuckFromRotatedPObj (VLevel *Level, polyobj_t *pofirst, bool skipLink) noexcept {
470 if (pofirst && !pofirst->posector) pofirst = nullptr; // don't do this for non-3d pobjs
472 // ok, it's not blocked; now try to unstuck
473 if (!pofirst) return true; // ok to move
475 if (poAffectedEnitities.length() == 0) return true; // still ok to move
477 tmtrace_t tmtrace;
478 TArray<UnstuckInfo> uvlist;
480 // check each affected entity against all pobj lines
481 for (auto &&edata : poAffectedEnitities) {
482 // if it already was stuck, ignore it
483 if (edata.aflags&AFF_STUCK) {
484 if (dbg_pobj_unstuck_verbose.asBool()) {
485 GCon->Logf(NAME_Debug, "mobj '%s' already stuck", edata.mobj->GetClass()->GetName());
487 continue;
489 VEntity *mobj = edata.mobj;
490 if (!mobj || !mobj->PObjNeedPositionCheck()) continue;
492 bool doFinalCheck = true;
493 // try to unstuck 3 times
494 for (int trycount = 3; trycount > 0; --trycount) {
495 doFinalCheck = true;
496 bool wasMove = false;
497 for (polyobj_t *po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
498 if (!po || !po->posector) continue;
499 // reject only polyobjects that are above us
500 // (otherwise we may miss blockig top texture)
501 if (mobj->Origin.z+max2(0.0f, mobj->Height) <= po->pofloor.minz) continue;
502 // if pobj has no top-blocking textures, it can be skipped if we're above it
503 if ((po->PolyFlags&polyobj_t::PF_HasTopBlocking) == 0) {
504 if (mobj->Origin.z >= po->poceiling.minz) continue;
507 const bool canUnstuck = CalcPolyUnstuckVector(uvlist, po, mobj);
508 if (dbg_pobj_unstuck_verbose.asBool()) {
509 GCon->Logf(NAME_Debug, "**** mobj %s:%u, pobj %d, triesleft=%d: canUnstuck=%d; uvlist.length=%d", mobj->GetClass()->GetName(), mobj->GetUniqueId(), po->tag, trycount, (int)canUnstuck, uvlist.length());
512 if (!canUnstuck) {
513 // oops, blocked, and can't unstuck, crush
514 if (!checkCrushMObj(po, mobj, true)) return false; // blocked
515 wasMove = false; // get away, we're done with this mobj
516 break;
519 if (uvlist.length() == 0) continue; // not stuck in this pobj
521 if (!DoUnstuckByAverage(uvlist, mobj)) {
522 // totally stuck
523 if (!checkCrushMObj(po, mobj, true)) return false; // blocked
524 wasMove = false; // get away, we're done with this mobj
525 break;
528 // moved
529 wasMove = true;
530 edata.aflags |= AFF_MOVE;
531 } // polyobject link loop
533 if (!wasMove) {
534 // wasn't moved, so no need to check for "still stuck"
535 doFinalCheck = false;
536 break;
538 } // unstuck try loop
540 // check if we stuck in any map walls
541 if (!mobj->IsGoingToDie() && mobj->PObjNeedPositionCheck()) {
542 CalcMapUnstuckVector(uvlist, mobj);
543 if (uvlist.length()) {
544 if (dbg_pobj_unstuck_verbose.asBool()) {
545 GCon->Logf(NAME_Debug, "mobj '%s': map unstuck", edata.mobj->GetClass()->GetName());
547 if (!DoUnstuckByAverage(uvlist, mobj)) return false; // blocked
551 // if we got here, it means that either unstucking is failed, or we're ok
552 // if `doFinalCheck` flag is set, we should be stuck
553 // check it again, just to be sure
554 if (doFinalCheck) {
555 for (polyobj_t *po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
556 // reject only polyobjects that are above us
557 // (otherwise we may miss blockig top texture)
558 if (mobj->Origin.z+max2(0.0f, mobj->Height) <= po->pofloor.minz) continue;
559 // if pobj has no top-blocking textures, it can be skipped if we're above it
560 if ((po->PolyFlags&polyobj_t::PF_HasTopBlocking) == 0) {
561 if (mobj->Origin.z >= po->poceiling.minz) continue;
563 const bool canUnstuck = CalcPolyUnstuckVector(uvlist, po, mobj);
564 // if we can't find unstuck direction, or found at least one, it means that we're stuck
565 if (!canUnstuck || uvlist.length() > 0) {
566 if (dbg_pobj_unstuck_verbose.asBool()) {
567 GCon->Logf(NAME_Debug, "mobj '%s' STILL BLOCKED", edata.mobj->GetClass()->GetName());
569 // still blocked
570 if (!checkCrushMObj(po, mobj, true)) return false; // blocked
576 // ok to move
577 return true;
581 //==========================================================================
583 // CheckAffectedMObjPositions
585 // returns `true` if movement was blocked
587 //==========================================================================
588 static bool CheckAffectedMObjPositions (const polyobj_t *pofirst) noexcept {
589 if (poAffectedEnitities.length() == 0) return false;
590 (void)pofirst;
592 tmtrace_t tmtrace;
593 for (auto &&edata : poAffectedEnitities) {
594 if (!edata.aflags) continue;
595 VEntity *mobj = edata.mobj;
596 if (!mobj->PObjNeedPositionCheck()) continue;
597 // all effected objects were unlinked from the world at this stage
598 bool ok = mobj->CheckRelPosition(tmtrace, mobj->Origin, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
599 if (!ok) {
600 // blocked, abort horizontal movement, and try again
601 //FIXME: this can break stacking and such
602 #if 0
603 if (edata.aflags&AFF_MOVE) {
604 if (dbg_pobj_unstuck_verbose.asBool()) {
605 GCon->Logf(NAME_Debug, "pobj #%d: HCHECK(0) for %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
607 TVec oo = edata.Origin;
608 oo.z = mobj->Origin.z;
609 ok = mobj->CheckRelPosition(tmtrace, oo, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
610 if (ok) {
611 // undo horizontal movement, and allow normal pobj check to crash the object
612 if (dbg_pobj_unstuck_verbose.asBool()) {
613 GCon->Logf(NAME_Debug, "pobj #%d: HABORT(0) for %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
615 mobj->LastMoveOrigin.z += mobj->Origin.z-oo.z;
616 mobj->Origin = oo;
617 // link it back, so pobj checker will do its work
618 edata.aflags = 0;
619 mobj->LinkToWorld();
620 continue;
623 #endif
624 // alas, blocked
625 if (dbg_pobj_unstuck_verbose.asBool()) {
626 GCon->Logf(NAME_Debug, "pobj #%d: blocked(0) by %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
628 return true;
630 //FIXME: process 3d floors
631 // check ceiling
632 if (mobj->Origin.z+mobj->Height > tmtrace.CeilingZ) {
633 // alas, height block
634 if (dbg_pobj_unstuck_verbose.asBool()) {
635 GCon->Logf(NAME_Debug, "pobj #%d: blocked(1) by %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
637 return true;
639 // fix z if we can step up
640 const float zdiff = tmtrace.FloorZ-mobj->Origin.z;
641 //GCon->Logf(NAME_Debug, "%s(%u): zdiff=%g; z=%g; FloorZ=%g; tr.FloorZ=%g", mobj->GetClass()->GetName(), mobj->GetUniqueId(), zdiff, mobj->Origin.z, mobj->FloorZ, tmtrace.FloorZ);
642 if (zdiff <= 0.0f) continue;
643 if (zdiff <= mobj->MaxStepHeight) {
644 // ok, we can step up
645 // let physics engine fix it
646 if (!mobj->IsPlayer()) mobj->Origin.z = tmtrace.FloorZ;
647 continue;
649 // cannot step up, rollback horizontal movement
650 #if 0
651 if (edata.aflags&AFF_MOVE) {
652 //GCon->Logf(NAME_Debug, "pobj #%d: HCHECK(1) for %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
653 TVec oo = edata.Origin;
654 oo.z = mobj->Origin.z;
655 ok = mobj->CheckRelPosition(tmtrace, oo, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
656 if (ok) {
657 // undo horizontal movement, and allow normal pobj check to crash the object
658 //GCon->Logf(NAME_Debug, "pobj #%d: HABORT(1) for %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
659 mobj->LastMoveOrigin.z += mobj->Origin.z-oo.z;
660 mobj->Origin = oo;
661 // link it back, so pobj checker will do its work
662 edata.aflags = 0;
663 mobj->LinkToWorld();
664 continue;
667 #endif
668 // alas, blocked
669 if (dbg_pobj_unstuck_verbose.asBool()) {
670 GCon->Logf(NAME_Debug, "pobj #%d: blocked(2) by %s(%u)", pofirst->tag, mobj->GetClass()->GetName(), mobj->GetUniqueId());
672 return true;
675 // done, no blocks
676 return false;
680 //==========================================================================
682 // VLevel::PolyCheckMobjLineBlocking
684 //==========================================================================
685 bool VLevel::PolyCheckMobjLineBlocking (const line_t *ld, polyobj_t *po, bool /*rotation*/) {
686 // check one extra block, as usual
687 int top = MapBlock(ld->bbox2d[BOX2D_TOP]-BlockMapOrgY)+1;
688 int bottom = MapBlock(ld->bbox2d[BOX2D_BOTTOM]-BlockMapOrgY)-1;
689 int left = MapBlock(ld->bbox2d[BOX2D_LEFT]-BlockMapOrgX)-1;
690 int right = MapBlock(ld->bbox2d[BOX2D_RIGHT]-BlockMapOrgX)+1;
692 if (top < 0 || right < 0 || bottom >= BlockMapHeight || left >= BlockMapWidth) return false;
694 if (bottom < 0) bottom = 0;
695 if (top >= BlockMapHeight) top = BlockMapHeight-1;
696 if (left < 0) left = 0;
697 if (right >= BlockMapWidth) right = BlockMapWidth-1;
699 bool blocked = false;
701 for (int by = bottom*BlockMapWidth; by <= top*BlockMapWidth; by += BlockMapWidth) {
702 for (int bx = left; bx <= right; ++bx) {
703 for (VEntity *mobj = BlockLinks[by+bx]; mobj; mobj = mobj->BlockMapNext) {
704 if (mobj->IsGoingToDie()) continue;
705 if (!mobj->PObjNeedPositionCheck()) continue;
706 if (!mobj->CheckPObjLineBlocked(po, ld)) continue;
708 if (dbg_pobj_unstuck_verbose.asBool()) {
709 GCon->Logf(NAME_Debug, "mobj '%s': blocked by pobj %d, line #%d", mobj->GetClass()->GetName(), po->tag, (int)(ptrdiff_t)(ld-&mobj->XLevel->Lines[0]));
711 if (!checkCrushMObj(po, mobj, false, ld->normal)) {
712 blocked = true;
713 break;
719 return blocked;
723 //==========================================================================
725 // VLevel::PolyCheckMobjBlocked
727 //==========================================================================
728 bool VLevel::PolyCheckMobjBlocked (polyobj_t *po, bool rotation) {
729 if (!po || po->numlines == 0) return false;
730 bool blocked = false;
731 line_t **lineList = po->lines;
732 for (int count = po->numlines; count; --count, ++lineList) {
733 if (PolyCheckMobjLineBlocking(*lineList, po, rotation)) blocked = true; //k8: break here?
735 return blocked;
739 //==========================================================================
741 // ProcessStackingAffectedDownFrom
743 // FIXME:this should be optimized!
745 //==========================================================================
746 static void ProcessStackingAffectedDownFrom (VEntity *mobj, int idx) {
747 for (; idx >= 0; --idx) {
748 VEntity *other = poAffectedEnitities.ptr()[idx].mobj; // no need to do range checking
749 if (!mobj->CollisionWithOther(other)) continue;
750 // move other down
751 float ndz = (other->Origin.z+other->Height)-mobj->Origin.z;
752 vassert(ndz > 0.0f);
753 other->Origin.z -= ndz;
754 poAffectedEnitities.ptr()[idx].aflags |= AFF_VERT;
755 ProcessStackingAffectedDownFrom(other, idx-1);
760 //==========================================================================
762 // ProcessStackingAffectedUpFrom
764 // FIXME:this should be optimized!
766 //==========================================================================
767 static void ProcessStackingAffectedUpFrom (VEntity *mobj, int idx, const unsigned addflg) {
768 const int len = poAffectedEnitities.length();
769 for (; idx < len; ++idx) {
770 VEntity *other = poAffectedEnitities.ptr()[idx].mobj; // no need to do range checking
771 if (other->Origin.z >= mobj->Origin.z+mobj->Height) break; // no more
772 if (!mobj->CollisionWithOther(other)) continue;
773 // move other up
774 float ndz = (mobj->Origin.z+mobj->Height)-other->Origin.z;
775 vassert(ndz > 0.0f);
776 other->Origin.z += ndz;
777 poAffectedEnitities.ptr()[idx].aflags |= AFF_VERT|addflg;
778 ProcessStackingAffectedUpFrom(other, idx+1, addflg);
783 //==========================================================================
785 // ProcessStackingAffected
787 // if there was some vertical movement, check for stacking
788 // this can be done faster, but currently i'm ok with this code
789 // this doesn't check vertical bounds
791 // `poAffectedEnitities` should contain the list for checking
793 //==========================================================================
794 static void ProcessStackingAffected () {
795 // sort by z position
796 smsort_r(poAffectedEnitities.ptr(), poAffectedEnitities.length(), sizeof(SavedEntityData), &entityDataCompareZ, nullptr);
797 const int len = poAffectedEnitities.length();
798 for (int eidx = 0; eidx < len; ++eidx) {
799 SavedEntityData &edata = poAffectedEnitities.ptr()[eidx]; // no need to do range checking
800 VEntity *mobj = edata.mobj;
801 if ((edata.aflags&AFF_VERT) == 0) continue; // not moved
802 if ((mobj->EntityFlags&(VEntity::EF_ColideWithThings|VEntity::EF_Solid)) != (VEntity::EF_ColideWithThings|VEntity::EF_Solid)) continue; // cannot collide with things
803 if (mobj->GetMoveRadius() <= 0.0f || mobj->Height <= 0.0f) continue; // cannot collide with things
804 // z delta
805 const float dz = mobj->Origin.z-edata.Origin.z;
806 if (dz == 0.0f) continue; // just in case, for horizontal movement
807 if (dz < 0.0f) {
808 // moved down
809 ProcessStackingAffectedDownFrom(mobj, eidx-1);
810 } else {
811 ProcessStackingAffectedUpFrom(mobj, eidx+1, (edata.aflags&AFF_MOVE));
818 //**************************************************************************
820 // polyobject movement code
822 //**************************************************************************
824 //==========================================================================
826 // VLevel::MovePolyobj
828 //==========================================================================
829 bool VLevel::MovePolyobj (int num, float x, float y, float z, unsigned flags) {
830 const bool forcedMove = (flags&POFLAG_FORCED); // forced move is like teleport, it will not affect objects
831 const bool skipLink = (flags&POFLAG_NOLINK);
833 polyobj_t *po = GetPolyobj(num);
834 if (!po) { GCon->Logf(NAME_Error, "MovePolyobj: Invalid pobj #%d", num); return false; }
836 if (!po->posector) z = 0.0f;
838 polyobj_t *pofirst = po;
840 const bool verticalMove = (!forcedMove && z != 0.0f);
841 const bool horizMove = (!forcedMove && (x != 0.0f || y != 0.0f));
842 const bool docarry = (!forcedMove && horizMove && pofirst->posector && !(pofirst->PolyFlags&polyobj_t::PF_NoCarry));
843 bool flatsSaved = false;
845 // collect `poAffectedEnitities`, and save planes
846 // but do this only if we have to
847 // only for 3d pobjs that either can carry objects, or moving vertically
848 // (because vertical move always pushing objects)
849 // "affected entities" are entities that collides with world, can be interacted, and touching our polyobjects
850 // world linker sets "touching sector" list for us, so no need to check coordinates or bounding boxes here
851 poAffectedEnitities.resetNoDtor();
852 if (!forcedMove && pofirst->posector && (docarry || verticalMove)) {
853 flatsSaved = true;
854 IncrementValidCount();
855 const int visCount = validcount;
856 //GCon->Logf(NAME_Debug, "=== pobj #%d ===", pofirst->tag);
857 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
858 // save flats
859 po->savedFloor = po->pofloor;
860 po->savedCeiling = po->poceiling;
861 // collect objects
862 for (msecnode_t *n = po->posector->TouchingThingList; n; n = n->SNext) {
863 VEntity *mobj = n->Thing;
864 if (mobj->ValidCount == visCount) continue;
865 mobj->ValidCount = visCount;
866 if (mobj->IsGoingToDie()) continue;
867 // check flags
868 if ((mobj->EntityFlags&VEntity::EF_ColideWithWorld) == 0) continue;
869 if ((mobj->FlagsEx&(VEntity::EFEX_NoInteraction|VEntity::EFEX_NoTickGrav)) == VEntity::EFEX_NoInteraction) continue;
870 //FIXME: ignore everything that cannot possibly be affected?
871 //GCon->Logf(NAME_Debug, " %s(%u): z=%g (poz1=%g); sector=%p; basesector=%p", mobj->GetClass()->GetName(), mobj->GetUniqueId(), mobj->Origin.z, pofirst->poceiling.maxz, mobj->Sector, mobj->BaseSector);
872 SavedEntityData &edata = poAffectedEnitities.alloc();
873 edata.mobj = mobj;
874 edata.Origin = mobj->Origin;
875 edata.LastMoveOrigin = mobj->LastMoveOrigin;
876 edata.aflags = 0;
880 // setup "affected" flags, calculate new z
881 //const bool verticalMoveUp = (verticalMove && z > 0.0f);
882 bool wasVertMovement = false; // set only if some solid thing was moved
883 for (auto &&edata : poAffectedEnitities) {
884 VEntity *mobj = edata.mobj;
885 // reset valid count, so we can avoid incrementing it
886 mobj->ValidCount = 0;
887 // check for vertical movement
888 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
889 const float oldz = mobj->Origin.z;
890 const float pofz = po->poceiling.maxz;
891 // fix "affected" flag
892 // vertical move goes up, and the platform goes through the object?
893 if (verticalMove && oldz > pofz && oldz < pofz+z) {
894 // yeah, carry it, but only if the platform is moving up
895 // if the platform is moving down, it should be blocked, or the object should be crushed
896 //FIXME: move down if possible instead of crush -- check this later
897 /*if (verticalMoveUp)*/ {
898 edata.aflags = AFF_VERT|(docarry ? AFF_MOVE : AFF_NOTHING_ZERO);
899 mobj->Origin.z = pofz+z;
900 if (!wasVertMovement) {
901 wasVertMovement =
902 (mobj->EntityFlags&(VEntity::EF_ColideWithThings|VEntity::EF_Solid)) == (VEntity::EF_ColideWithThings|VEntity::EF_Solid) &&
903 mobj->GetMoveRadius() > 0.0f && mobj->Height > 0.0f;
906 } else if (oldz == pofz) {
907 // the object is standing on the pobj ceiling (i.e. floor ;-)
908 // always carry, and always move with the pobj, even if it is flying
909 edata.aflags = AFF_VERT|(docarry ? AFF_MOVE : AFF_NOTHING_ZERO);
910 mobj->Origin.z = pofz+z;
911 if (!wasVertMovement) {
912 wasVertMovement =
913 (mobj->EntityFlags&(VEntity::EF_ColideWithThings|VEntity::EF_Solid)) == (VEntity::EF_ColideWithThings|VEntity::EF_Solid) &&
914 mobj->GetMoveRadius() > 0.0f && mobj->Height > 0.0f;
920 // if there was some vertical movement, check for stacking
921 if (wasVertMovement) ProcessStackingAffected();
923 // now unlink all affected objects, because we'll do "move and check" later
924 // also, move objects horizontally, because why not
925 const TVec xymove(x, y);
926 for (auto &&edata : poAffectedEnitities) {
927 VEntity *mobj = edata.mobj;
928 if (edata.aflags) {
929 mobj->UnlinkFromWorld();
930 if (edata.aflags&AFF_MOVE) mobj->Origin += xymove;
935 const bool dovmove = (z != 0.0f); // cannot use `verticalMove` here
936 // unlink and move all linked polyobjects
937 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
938 UnlinkPolyobj(po);
939 const TVec delta(po->startSpot.x+x, po->startSpot.y+y);
940 const float an = AngleMod(po->angle);
941 float s, c;
942 msincos(an, &s, &c);
943 // save previous points, move horizontally
944 const TVec *origPts = po->originalPts;
945 TVec *prevPts = po->prevPts;
946 TVec **vptr = po->segPts;
947 for (int f = po->segPtsCount; f--; ++vptr, ++origPts, ++prevPts) {
948 *prevPts = **vptr;
949 //**vptr = (*origPts)+delta;
950 TVec np(*origPts);
951 // get the original X and Y values
952 const float tr_x = np.x;
953 const float tr_y = np.y;
954 // calculate the new X and Y values
955 np.x = (tr_x*c-tr_y*s)+delta.x;
956 np.y = (tr_y*c+tr_x*s)+delta.y;
957 **vptr = np;
959 UpdatePolySegs(po);
960 if (dovmove) OffsetPolyobjFlats(po, z, false);
963 // now check if movement is blocked by any affected object
964 // we have to do it after we unlinked all pobjs
965 bool blocked = false;
967 if (!forcedMove && pofirst->posector) {
968 if (!blocked) blocked = !UnstuckFromRotatedPObj(this, pofirst, skipLink);
969 if (!blocked) blocked = CheckAffectedMObjPositions(pofirst);
970 if (dbg_pobj_unstuck_verbose.asBool()) {
971 GCon->Logf(NAME_Debug, "pobj #%d: MOVEMENT: blocked=%d", pofirst->tag, (int)blocked);
975 if (!forcedMove && !blocked) {
976 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
977 blocked = PolyCheckMobjBlocked(po, false);
978 if (dbg_pobj_unstuck_verbose.asBool()) {
979 GCon->Logf(NAME_Debug, "pobj #%d: MOVEMENT(1): blocked=%d", po->tag, (int)blocked);
981 if (blocked) break; // process all?
985 bool linked = false;
987 // if not blocked, relink polyobject temporarily, and check vertical hits
988 if (!blocked && !forcedMove && pofirst->posector) {
989 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) LinkPolyobj(po, false); // don't relink mobjs
990 Link3DPolyobjMObjs(pofirst, skipLink);
991 linked = true;
992 // check height-blocking objects
993 // note that `Link3DPolyobjMObjs()` collected all touching mobjs in `poRoughEntityList` for us
994 for (VEntity *mobj : poRoughEntityList) {
995 // check flags
996 if (!mobj->PObjNeedPositionCheck()) continue;
997 float mobjbb2d[4];
998 mobj->Create2DBox(mobjbb2d);
999 const float mz0 = mobj->Origin.z;
1000 const float mz1 = mz0+mobj->Height;
1001 // check all polyobjects
1002 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1003 //if (!IsAABBInside2DBBox(mobj->Origin.x, mobj->Origin.y, mobj->GetMoveRadius(), po->bbox2d)) continue;
1004 const float pz0 = po->pofloor.minz;
1005 if (mz1 <= pz0) continue;
1006 // if pobj has no top-blocking textures, it can be skipped if we're above it
1007 if ((po->PolyFlags&polyobj_t::PF_HasTopBlocking) == 0) {
1008 const float pz1 = po->poceiling.maxz;
1009 if (mz0 >= pz1) continue;
1011 if (!Are2DBBoxesOverlap(mobjbb2d, po->bbox2d)) continue;
1012 // possible vertical intersection, check pobj lines
1013 bool blk = false;
1014 line_t **lineList = po->lines;
1015 for (int count = po->numlines; count; --count, ++lineList) {
1016 if (!mobj->CheckPObjLineBlocked(po, *lineList)) continue;
1017 blk = true;
1018 break;
1020 if (blk) {
1021 if (!checkCrushMObj(po, mobj, false)) {
1022 blocked = true;
1023 break;
1025 } else {
1026 const float pz1 = po->poceiling.maxz;
1027 if (mz0 >= pz1) continue;
1028 if (IsBBox2DTouching3DPolyObj(po, mobjbb2d)) {
1029 if (!checkCrushMObj(po, mobj, true)) {
1030 blocked = true;
1031 break;
1036 if (blocked) break;
1038 // if blocked, unlink back
1039 if (blocked) {
1040 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1041 UnlinkPolyobj(po);
1043 linked = false;
1047 // restore position if blocked
1048 if (blocked) {
1049 vassert(!linked);
1050 // undo polyobject movement, and link them back
1051 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1052 if (flatsSaved) {
1053 po->pofloor = po->savedFloor;
1054 po->poceiling = po->savedCeiling;
1056 // restore points
1057 TVec *prevPts = po->prevPts;
1058 TVec **vptr = po->segPts;
1059 for (int f = po->segPtsCount; f--; ++vptr, ++prevPts) **vptr = *prevPts;
1060 UpdatePolySegs(po);
1061 OffsetPolyobjFlats(po, 0.0f, true);
1062 LinkPolyobj(po, false); // do not relink mobjs
1064 // relink mobjs
1065 Link3DPolyobjMObjs(pofirst, skipLink);
1066 // restore and relink all mobjs back
1067 for (auto &&edata : poAffectedEnitities) {
1068 VEntity *mobj = edata.mobj;
1069 if (!mobj->IsGoingToDie() && edata.aflags) {
1070 mobj->LastMoveOrigin = edata.LastMoveOrigin;
1071 mobj->Origin = edata.Origin;
1072 mobj->LinkToWorld();
1075 return false;
1078 // succesfull move, fix startspot
1079 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1080 po->startSpot.x += x;
1081 po->startSpot.y += y;
1082 po->startSpot.z += z;
1083 OffsetPolyobjFlats(po, 0.0f, true);
1084 if (!linked) LinkPolyobj(po, false); // do not relink mobjs
1085 // move decals
1086 if (!forcedMove && po->Is3D() && subsectorDecalList) {
1087 for (subsector_t *posub = po->GetSector()->subsectors; posub; posub = posub->seclink) {
1088 const unsigned psnum = (unsigned)(ptrdiff_t)(posub-&Subsectors[0]);
1089 VDecalList *lst = &subsectorDecalList[psnum];
1090 for (decal_t *dc = lst->head; dc; dc = dc->subnext) {
1091 dc->worldx += x;
1092 dc->worldy += y;
1097 // relink mobjs
1098 Link3DPolyobjMObjs(pofirst, skipLink);
1100 // relink all mobjs back
1101 for (auto &&edata : poAffectedEnitities) {
1102 VEntity *mobj = edata.mobj;
1103 if (!mobj->IsGoingToDie() && edata.aflags) {
1104 mobj->LastMoveOrigin += mobj->Origin-edata.Origin;
1105 mobj->LinkToWorld();
1109 // notify renderer that this polyobject was moved
1110 #ifdef CLIENT
1111 if (Renderer) {
1112 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1113 Renderer->PObjModified(po);
1114 Renderer->InvalidatePObjLMaps(po);
1117 #endif
1119 return true;
1124 //**************************************************************************
1126 // polyobject rotation code
1128 //**************************************************************************
1130 //==========================================================================
1132 // VLevel::RotatePolyobj
1134 //==========================================================================
1135 bool VLevel::RotatePolyobj (int num, float angle, unsigned flags) {
1136 const bool forcedMove = (flags&POFLAG_FORCED);
1137 const bool skipLink = (flags&POFLAG_NOLINK);
1138 const bool indRot = (flags&POFLAG_INDROT);
1140 // get the polyobject
1141 polyobj_t *po = GetPolyobj(num);
1142 if (!po) { GCon->Logf(NAME_Error, "RotatePolyobj: Invalid pobj #%d", num); return false; }
1144 polyobj_t *pofirst = po;
1146 const bool docarry = (!forcedMove && pofirst->posector && !(po->PolyFlags&polyobj_t::PF_NoCarry));
1147 const bool doangle = (!forcedMove && pofirst->posector && !(po->PolyFlags&polyobj_t::PF_NoAngleChange));
1148 const bool flatsSaved = (!forcedMove && pofirst->posector);
1150 float s, c;
1152 // collect objects we need to move/rotate
1153 poAffectedEnitities.resetNoDtor();
1154 if (!forcedMove && pofirst->posector && (docarry || doangle)) {
1155 IncrementValidCount();
1156 const int visCount = validcount;
1157 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1158 //const float pz1 = po->poceiling.maxz;
1159 for (msecnode_t *n = po->posector->TouchingThingList; n; n = n->SNext) {
1160 VEntity *mobj = n->Thing;
1161 if (mobj->ValidCount == visCount) continue;
1162 mobj->ValidCount = visCount;
1163 if (mobj->IsGoingToDie()) continue;
1164 if (dbg_pobj_unstuck_verbose.asBool()) {
1165 GCon->Logf(NAME_Debug, "ROTATION: pobj #%d: checking mobj '%s'", po->tag, mobj->GetClass()->GetName());
1167 // check flags
1168 if ((mobj->EntityFlags&VEntity::EF_ColideWithWorld) == 0) continue;
1169 if ((mobj->FlagsEx&(VEntity::EFEX_NoInteraction|VEntity::EFEX_NoTickGrav)) == VEntity::EFEX_NoInteraction) continue;
1170 SavedEntityData &edata = poAffectedEnitities.alloc();
1171 edata.mobj = mobj;
1172 edata.Origin = mobj->Origin;
1173 edata.LastMoveOrigin = mobj->LastMoveOrigin;
1174 edata.aflags = 0;
1178 // flag objects we should carry
1179 for (auto &&edata : poAffectedEnitities) {
1180 VEntity *mobj = edata.mobj;
1181 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1182 // check if it is initially stuck
1183 #if 1
1184 if ((edata.aflags&AFF_STUCK) == 0 && mobj->PObjNeedPositionCheck()) {
1185 for (auto &&lxx : po->LineFirst()) {
1186 const line_t *ld = lxx.line();
1187 if (mobj->CheckPObjLineBlocked(po, ld)) {
1188 edata.aflags |= AFF_STUCK;
1189 break;
1193 #endif
1194 if ((edata.aflags&AFF_MOVE) == 0) {
1195 const float pz1 = po->poceiling.maxz;
1196 if (mobj->Origin.z != pz1) continue;
1197 // we need to remember rotation point
1198 //FIXME: this will glitch with objects standing on some multipart platforms
1199 edata.spot = (indRot ? po->startSpot : pofirst->startSpot); // mobj->Sector->ownpobj
1200 edata.aflags |= AFF_MOVE;
1201 if (dbg_pobj_unstuck_verbose.asBool()) {
1202 GCon->Logf(NAME_Debug, "ROTATION: pobj #%d: mobj '%s' is affected", po->tag, mobj->GetClass()->GetName());
1208 // now unlink all affected objects, because we'll do "move and check" later
1209 // also, rotate the objects
1210 msincos(AngleMod(angle), &s, &c);
1211 for (auto &&edata : poAffectedEnitities) {
1212 if ((edata.aflags&AFF_MOVE) == 0) continue; // no need to move it
1213 VEntity *mobj = edata.mobj;
1214 mobj->UnlinkFromWorld();
1215 if (docarry) {
1216 const float ssx = edata.spot.x;
1217 const float ssy = edata.spot.y;
1218 // rotate around polyobject spot point
1219 const float xc = mobj->Origin.x-ssx;
1220 const float yc = mobj->Origin.y-ssy;
1221 //GCon->Logf(NAME_Debug, "%s(%u): oldrelpos=(%g,%g)", mobj->GetClass()->GetName(), mobj->GetUniqueId(), xc, yc);
1222 // calculate the new X and Y values
1223 const float nx = (xc*c-yc*s);
1224 const float ny = (yc*c+xc*s);
1225 //GCon->Logf(NAME_Debug, "%s(%u): newrelpos=(%g,%g)", mobj->GetClass()->GetName(), mobj->GetUniqueId(), nx, ny);
1226 const float dx = (nx+ssx)-mobj->Origin.x;
1227 const float dy = (ny+ssy)-mobj->Origin.y;
1228 if (dx != 0.0f || dy != 0.0f) {
1229 mobj->Origin.x += dx;
1230 mobj->Origin.y += dy;
1231 //edata.aflags |= AFF_MOVE;
1237 // rotate all polyobjects
1238 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1239 /*if (IsForServer())*/ UnlinkPolyobj(po);
1240 po->origStartSpot = po->startSpot;
1241 if (!indRot && po != pofirst) {
1242 // rotate this object's starting spot around the main object starting spot
1243 msincos(angle, &s, &c);
1244 const float sp_x = po->startSpot.x-pofirst->startSpot.x;
1245 const float sp_y = po->startSpot.y-pofirst->startSpot.y;
1246 // calculate the new X and Y values
1247 const float nx = (sp_x*c-sp_y*s)+pofirst->startSpot.x;
1248 const float ny = (sp_y*c+sp_x*s)+pofirst->startSpot.y;
1249 po->startSpot.x = nx;
1250 po->startSpot.y = ny;
1252 const float ssx = po->startSpot.x;
1253 const float ssy = po->startSpot.y;
1254 const float an = AngleMod(po->angle+angle);
1255 msincos(an, &s, &c);
1256 const TVec *origPts = po->originalPts;
1257 TVec *prevPts = po->prevPts;
1258 TVec **vptr = po->segPts;
1259 for (int f = po->segPtsCount; f--; ++vptr, ++prevPts, ++origPts) {
1260 if (flatsSaved) {
1261 po->savedFloor = po->pofloor;
1262 po->savedCeiling = po->poceiling;
1264 // save the previous point
1265 *prevPts = **vptr;
1266 // get the original X and Y values
1267 float tr_x = origPts->x;
1268 float tr_y = origPts->y;
1270 if (!indRot && po != pofirst) {
1271 tr_x += po->startSpot.x-ssx;
1272 tr_y += po->startSpot.y-ssy;
1275 // calculate the new X and Y values
1276 (*vptr)->x = (tr_x*c-tr_y*s)+ssx;
1277 (*vptr)->y = (tr_y*c+tr_x*s)+ssy;
1279 UpdatePolySegs(po);
1282 bool blocked = false;
1284 if (!forcedMove) {
1285 // now check if movement is blocked by any affected object
1286 // we have to do it after we unlinked all pobjs
1287 if (!forcedMove && pofirst->posector) {
1288 if (!blocked) blocked = !UnstuckFromRotatedPObj(this, pofirst, skipLink);
1289 if (!blocked) blocked = CheckAffectedMObjPositions(pofirst);
1290 if (dbg_pobj_unstuck_verbose.asBool()) {
1291 GCon->Logf(NAME_Debug, "pobj #%d: ROTATION: blocked=%d", pofirst->tag, (int)blocked);
1294 if (!blocked) {
1295 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1296 blocked = PolyCheckMobjBlocked(po, true);
1297 if (dbg_pobj_unstuck_verbose.asBool()) {
1298 GCon->Logf(NAME_Debug, "pobj #%d: ROTATION(1): blocked=%d", po->tag, (int)blocked);
1300 if (blocked) break; // process all?
1305 // if we are blocked then restore the previous points
1306 if (blocked) {
1307 // restore points
1308 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1309 if (flatsSaved) {
1310 po->pofloor = po->savedFloor;
1311 po->poceiling = po->savedCeiling;
1313 po->startSpot = po->origStartSpot;
1314 TVec *prevPts = po->prevPts;
1315 TVec **vptr = po->segPts;
1316 for (int f = po->segPtsCount; f--; ++vptr, ++prevPts) **vptr = *prevPts;
1317 UpdatePolySegs(po);
1318 LinkPolyobj(po, false); // do not relink mobjs
1320 // relink mobjs
1321 Link3DPolyobjMObjs(pofirst, skipLink);
1322 // restore and relink all mobjs back
1323 for (auto &&edata : poAffectedEnitities) {
1324 VEntity *mobj = edata.mobj;
1325 if (!mobj->IsGoingToDie()) {
1326 mobj->LastMoveOrigin = edata.LastMoveOrigin;
1327 mobj->Origin = edata.Origin;
1328 mobj->LinkToWorld();
1331 return false;
1334 // not blocked, fix angles and floors, rotate decals
1335 // also, fix starting spots for non-independent rotation
1336 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1337 po->angle = AngleMod(po->angle+angle);
1338 if (flatsSaved) {
1339 po->pofloor.BaseAngle = AngleMod(po->pofloor.BaseAngle+angle);
1340 po->poceiling.BaseAngle = AngleMod(po->poceiling.BaseAngle+angle);
1341 } else {
1342 // on loading, our baseangle is not set
1343 //FIXME: wrong with initially rotated floors
1344 po->pofloor.BaseAngle = po->poceiling.BaseAngle = AngleMod(po->angle);
1346 OffsetPolyobjFlats(po, 0.0f, true);
1347 LinkPolyobj(po, false);
1348 // move decals
1349 if (!forcedMove && po->Is3D() && subsectorDecalList) {
1350 for (subsector_t *posub = po->GetSector()->subsectors; posub; posub = posub->seclink) {
1351 const unsigned psnum = (unsigned)(ptrdiff_t)(posub-&Subsectors[0]);
1352 VDecalList *lst = &subsectorDecalList[psnum];
1353 if (!lst->head) continue;
1354 msincos(AngleMod(angle), &s, &c);
1355 const float ssx = (indRot ? po->startSpot.x : pofirst->startSpot.x);
1356 const float ssy = (indRot ? po->startSpot.y : pofirst->startSpot.y);
1357 for (decal_t *dc = lst->head; dc; dc = dc->subnext) {
1358 const float xc = dc->worldx-ssx;
1359 const float yc = dc->worldy-ssy;
1360 const float nx = (xc*c-yc*s);
1361 const float ny = (yc*c+xc*s);
1362 dc->worldx = nx+ssx;
1363 dc->worldy = ny+ssy;
1364 dc->angle = AngleMod(dc->angle+angle);
1369 // relink mobjs
1370 Link3DPolyobjMObjs(pofirst, skipLink);
1372 // relink things
1373 if (!forcedMove && poAffectedEnitities.length()) {
1374 for (auto &&edata : poAffectedEnitities) {
1375 VEntity *mobj = edata.mobj;
1376 if (mobj->IsGoingToDie()) continue;
1377 if ((edata.aflags&AFF_MOVE) != 0) {
1378 if (doangle) mobj->Level->eventPolyRotateMobj(mobj, angle);
1380 mobj->LastMoveOrigin += mobj->Origin-edata.Origin;
1381 mobj->LinkToWorld();
1385 // notify renderer that this polyobject was moved
1386 #ifdef CLIENT
1387 if (Renderer) {
1388 for (po = pofirst; po; po = (skipLink ? nullptr : po->polink)) {
1389 Renderer->PObjModified(po);
1390 Renderer->InvalidatePObjLMaps(po);
1393 #endif
1395 return true;