1 //**************************************************************************
3 //** ## ## ## ## ## #### #### ### ###
4 //** ## ## ## ## ## ## ## ## ## ## #### ####
5 //** ## ## ## ## ## ## ## ## ## ## ## ## ## ##
6 //** ## ## ######## ## ## ## ## ## ## ## ### ##
7 //** ### ## ## ### ## ## ## ## ## ##
8 //** # ## ## # #### #### ## ##
10 //** Copyright (C) 1999-2006 Jānis Legzdiņš
11 //** Copyright (C) 2018-2023 Ketmar Dark
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.
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.
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/>.
25 //**************************************************************************
26 #include "../gamedefs.h"
28 #include "p_levelinfo.h"
31 # include "../drawer.h"
35 static VCvarB
dbg_pobj_unstuck_verbose("dbg_pobj_unstuck_verbose", false, "Verbose 3d pobj unstuck code?", CVAR_PreInit
|CVAR_Hidden
|CVAR_NoShadow
);
43 AFF_NOTHING_ZERO
= 0u,
46 struct SavedEntityData
{
50 unsigned aflags
; // AFF_xxx flags, used in movement
51 TVec spot
; // used in rotator
54 static TArray
<SavedEntityData
> poAffectedEnitities
;
57 // ////////////////////////////////////////////////////////////////////////// //
59 TVec uv
; // unstuck vector
60 TVec unorm
; // unstuck normal
67 //==========================================================================
71 //==========================================================================
72 static int entityDataCompareZ (const void *a
, const void *b
, void *) noexcept
{
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;
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
96 mobj
->Create2DBox(mybbox
);
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;
109 if (ld
->backsector
&& (ld
->flags
&ML_TWOSIDED
)) {
110 // two-sided line, check if we can step up
112 for (unsigned ff
= 0; ff
< 2; ++ff
) {
113 const sector_t
*sec
= (ff
? ld
->backsector
: ld
->frontsector
);
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
) {
121 const float cz
= sec
->ceiling
.GetPointZClamped(mobj
->Origin
);
122 if (mobj
->Origin
.z
+mobj
->Height
> cz
) {
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
;
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
);
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;
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
);
223 if (csdist
> 0.0f
) continue;
225 uv
= ld
->normal
*(-csdist
);
228 if (csdist
< 0.0f
) continue;
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]));
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();
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
);
271 if (!foundVector
) return false; // oops
274 if (wasIntersect
) return (uvlist
.length() > 0);
279 //==========================================================================
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
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
;
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;
320 //==========================================================================
322 // DoUnstuckByAverage
324 // returns `false` if can't
326 //==========================================================================
327 static bool DoUnstuckByAverage (TArray
<UnstuckInfo
> &uvlist
, VEntity
*mobj
) {
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
339 // calculate distance to move away
341 mobj
->Create2DBox(mybbox
);
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
);
352 if (!isFiniteF(tm0
)) {
353 if (!isFiniteF(tm1
)) continue;
357 if (isFiniteF(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
370 mobj
->Origin
= origOrigin
+nsum
*ndist
;
372 for (auto &&nfo
: uvlist
) {
374 ok
= !mobj
->Check3DPObjLineBlocked(nfo
.line
->pobj(), nfo
.line
);
379 ok
= mobj
->CheckRelPosition(tmtrace
, mobj
->Origin
, /*noPickups*/true, /*ignoreMonsters*/true, /*ignorePlayers*/true);
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
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
;
400 for (auto &&nfo
: uvlist
) {
402 ok
= !mobj
->Check3DPObjLineBlocked(nfo
.line
->pobj(), nfo
.line
);
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());
414 const float sqdist
= (origOrigin
-mobj
->Origin
).length2DSquared();
416 goodPos
= mobj
->Origin
;
417 if (sqdist
<= 0.1f
*0.1f
) break;
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());
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
);
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);
457 mobj
->Origin
= origOrigin
;
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
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());
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
) {
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());
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
519 if (uvlist
.length() == 0) continue; // not stuck in this pobj
521 if (!DoUnstuckByAverage(uvlist
, mobj
)) {
523 if (!checkCrushMObj(po
, mobj
, true)) return false; // blocked
524 wasMove
= false; // get away, we're done with this mobj
530 edata
.aflags
|= AFF_MOVE
;
531 } // polyobject link loop
534 // wasn't moved, so no need to check for "still stuck"
535 doFinalCheck
= false;
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
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());
570 if (!checkCrushMObj(po
, mobj
, true)) return false; // blocked
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;
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);
600 // blocked, abort horizontal movement, and try again
601 //FIXME: this can break stacking and such
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);
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
;
617 // link it back, so pobj checker will do its work
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());
630 //FIXME: process 3d floors
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());
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
;
649 // cannot step up, rollback horizontal movement
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);
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
;
661 // link it back, so pobj checker will do its work
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());
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
)) {
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?
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;
751 float ndz
= (other
->Origin
.z
+other
->Height
)-mobj
->Origin
.z
;
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;
774 float ndz
= (mobj
->Origin
.z
+mobj
->Height
)-other
->Origin
.z
;
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
805 const float dz
= mobj
->Origin
.z
-edata
.Origin
.z
;
806 if (dz
== 0.0f
) continue; // just in case, for horizontal movement
809 ProcessStackingAffectedDownFrom(mobj
, eidx
-1);
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
)) {
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
)) {
859 po
->savedFloor
= po
->pofloor
;
860 po
->savedCeiling
= po
->poceiling
;
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;
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();
874 edata
.Origin
= mobj
->Origin
;
875 edata
.LastMoveOrigin
= mobj
->LastMoveOrigin
;
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
) {
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
) {
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
;
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
)) {
939 const TVec
delta(po
->startSpot
.x
+x
, po
->startSpot
.y
+y
);
940 const float an
= AngleMod(po
->angle
);
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
) {
949 //**vptr = (*origPts)+delta;
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
;
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?
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
);
992 // check height-blocking objects
993 // note that `Link3DPolyobjMObjs()` collected all touching mobjs in `poRoughEntityList` for us
994 for (VEntity
*mobj
: poRoughEntityList
) {
996 if (!mobj
->PObjNeedPositionCheck()) continue;
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
1014 line_t
**lineList
= po
->lines
;
1015 for (int count
= po
->numlines
; count
; --count
, ++lineList
) {
1016 if (!mobj
->CheckPObjLineBlocked(po
, *lineList
)) continue;
1021 if (!checkCrushMObj(po
, mobj
, false)) {
1026 const float pz1
= po
->poceiling
.maxz
;
1027 if (mz0
>= pz1
) continue;
1028 if (IsBBox2DTouching3DPolyObj(po
, mobjbb2d
)) {
1029 if (!checkCrushMObj(po
, mobj
, true)) {
1038 // if blocked, unlink back
1040 for (po
= pofirst
; po
; po
= (skipLink
? nullptr : po
->polink
)) {
1047 // restore position if blocked
1050 // undo polyobject movement, and link them back
1051 for (po
= pofirst
; po
; po
= (skipLink
? nullptr : po
->polink
)) {
1053 po
->pofloor
= po
->savedFloor
;
1054 po
->poceiling
= po
->savedCeiling
;
1057 TVec
*prevPts
= po
->prevPts
;
1058 TVec
**vptr
= po
->segPts
;
1059 for (int f
= po
->segPtsCount
; f
--; ++vptr
, ++prevPts
) **vptr
= *prevPts
;
1061 OffsetPolyobjFlats(po
, 0.0f
, true);
1062 LinkPolyobj(po
, false); // do not 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();
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
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
) {
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
1112 for (po
= pofirst
; po
; po
= (skipLink
? nullptr : po
->polink
)) {
1113 Renderer
->PObjModified(po
);
1114 Renderer
->InvalidatePObjLMaps(po
);
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
);
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());
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();
1172 edata
.Origin
= mobj
->Origin
;
1173 edata
.LastMoveOrigin
= mobj
->LastMoveOrigin
;
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
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
;
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();
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
) {
1261 po
->savedFloor
= po
->pofloor
;
1262 po
->savedCeiling
= po
->poceiling
;
1264 // save the previous point
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
;
1282 bool blocked
= false;
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
);
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
1308 for (po
= pofirst
; po
; po
= (skipLink
? nullptr : po
->polink
)) {
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
;
1318 LinkPolyobj(po
, false); // do not 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();
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
);
1339 po
->pofloor
.BaseAngle
= AngleMod(po
->pofloor
.BaseAngle
+angle
);
1340 po
->poceiling
.BaseAngle
= AngleMod(po
->poceiling
.BaseAngle
+angle
);
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);
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
);
1370 Link3DPolyobjMObjs(pofirst
, skipLink
);
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
1388 for (po
= pofirst
; po
; po
= (skipLink
? nullptr : po
->polink
)) {
1389 Renderer
->PObjModified(po
);
1390 Renderer
->InvalidatePObjLMaps(po
);