@@ -173,7 +173,7 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu
173173 m_physics(nullptr ),
174174 m_geometryInfo(tt->getTemplateGeometryInfo ()),
175175 m_containedBy(nullptr ),
176- m_xferContainedByID (INVALID_ID ),
176+ m_containedByID (INVALID_ID ),
177177 m_containedByFrame(0 ),
178178 m_behaviors(nullptr ),
179179 m_body(nullptr ),
@@ -621,6 +621,22 @@ void Object::onContainedBy( Object *containedBy )
621621 clearStatus ( MAKE_OBJECT_STATUS_MASK ( OBJECT_STATUS_MASKED ) );
622622 m_containedBy = containedBy;
623623 m_containedByFrame = TheGameLogic->getFrame ();
624+
625+ #if RETAIL_COMPATIBLE_CRC
626+ // TheSuperHackers @info Set INVALID_ID if the container object was destroyed
627+ // to indicate that the pointer will become a dangling pointer in the next frame.
628+ if (containedBy && !containedBy->isDestroyed ())
629+ {
630+ m_containedByID = containedBy->getID ();
631+ }
632+ else
633+ {
634+ m_containedByID = INVALID_ID ;
635+ }
636+ #else
637+ DEBUG_ASSERTCRASH (containedBy == nullptr || !containedBy->isDestroyed (),
638+ (" Object::onContainedBy - Adding into a destroyed container" ));
639+ #endif
624640}
625641
626642// -------------------------------------------------------------------------------------------------
@@ -631,6 +647,10 @@ void Object::onRemovedFrom( Object *removedFrom )
631647 clearStatus ( MAKE_OBJECT_STATUS_MASK2 ( OBJECT_STATUS_MASKED , OBJECT_STATUS_UNSELECTABLE ) );
632648 m_containedBy = nullptr ;
633649 m_containedByFrame = 0 ;
650+
651+ #if RETAIL_COMPATIBLE_CRC
652+ m_containedByID = INVALID_ID ;
653+ #endif
634654}
635655
636656// -------------------------------------------------------------------------------------------------
@@ -681,9 +701,33 @@ void Object::onDestroy()
681701{
682702
683703 // This is the old cleanUpContain safeguard. Say goodbye so they don't try to look us up.
684- if ( m_containedBy && m_containedBy-> getContain () )
704+ if (m_containedBy )
685705 {
686- m_containedBy->getContain ()->removeFromContain ( this );
706+ #if RETAIL_COMPATIBLE_CRC
707+ if (m_containedByID == INVALID_ID )
708+ {
709+ // TheSuperHackers @bugfix Caball009 25/05/2026 Due to a potential use-after-free bug that cannot be fixed
710+ // with retail compatibility, the 'contained by' pointer of this object may point to an already destroyed object.
711+ // Avoid removing this object from the contain list, because it could crash the game,
712+ // as the begin / end iterator for STLPort and MSVC std::list implementations depends on dynamically allocated memory.
713+ DEBUG_CRASH ((" container object must be valid; this looks like use-after-free" ));
714+ }
715+ else
716+ {
717+ DEBUG_ASSERTCRASH (TheGameLogic->findObjectByID (m_containedByID) == m_containedBy,
718+ (" contained by pointer is out of sync with contained by ID" ));
719+
720+ if (ContainModuleInterface* contain = m_containedBy->getContain ())
721+ {
722+ contain->removeFromContain (this );
723+ }
724+ }
725+ #else
726+ if (ContainModuleInterface* contain = m_containedBy->getContain ())
727+ {
728+ contain->removeFromContain (this );
729+ }
730+ #endif
687731 }
688732
689733 //
@@ -3727,16 +3771,18 @@ void Object::xfer( Xfer *xfer )
37273771 // No, the contain module is just going to friend_ reach in and set this for us.
37283772 // Containers more complicated than Open (like Tunnel) can't do that. Our variable,
37293773 // our responsibility.
3774+ #if !RETAIL_COMPATIBLE_CRC
3775+ // TheSuperHackers @tweak Contained by ID is already set with retail compatibility; don't overwrite it.
37303776 if ( xfer->getXferMode () == XFER_SAVE )
37313777 {
37323778 if ( m_containedBy != nullptr )
3733- m_xferContainedByID = m_containedBy->getID ();
3779+ m_containedByID = m_containedBy->getID ();
37343780 else
3735- m_xferContainedByID = INVALID_ID ;
3781+ m_containedByID = INVALID_ID ;
37363782 }
3783+ #endif
37373784
3738-
3739- xfer->xferObjectID ( &m_xferContainedByID );
3785+ xfer->xferObjectID ( &m_containedByID );
37403786 }
37413787
37423788 // contained by frame
@@ -3961,8 +4007,8 @@ void Object::xfer( Xfer *xfer )
39614007// -------------------------------------------------------------------------------------------------
39624008void Object::loadPostProcess ()
39634009{
3964- if ( m_xferContainedByID != INVALID_ID )
3965- m_containedBy = TheGameLogic->findObjectByID (m_xferContainedByID );
4010+ if ( m_containedByID != INVALID_ID )
4011+ m_containedBy = TheGameLogic->findObjectByID (m_containedByID );
39664012 else
39674013 m_containedBy = nullptr ;
39684014
0 commit comments