From 84041f3491318e3d6f018b056e456a37e47b8458 Mon Sep 17 00:00:00 2001
From: D-AIRY <admin@ds-servers.com>
Date: Sun, 6 Apr 2025 01:56:46 +0300
Subject: [PATCH] phys_door_rotating introduced

---
 proj/sxgame/vs2013/sxgame.vcxproj             |   6 +
 proj/sxgame/vs2013/sxgame.vcxproj.filters     |  18 +
 proj/sxphysics/vs2013/sxphysics.vcxproj       |   3 +
 .../vs2013/sxphysics.vcxproj.filters          |   9 +
 sdks/bullet3                                  |   2 +-
 source/game/BaseAmmo.cpp                      |  13 +-
 source/game/BaseCharacter.cpp                 |   2 +-
 source/game/BaseDoor.cpp                      | 403 ++++++++++++++++++
 source/game/BaseDoor.h                        |  95 +++++
 source/game/DogholeMovementController.cpp     |   4 +-
 .../game/NarrowPassageMovementController.cpp  |   4 +-
 source/game/PhysDoor.cpp                      | 196 +++++++++
 source/game/PhysDoor.h                        |  46 ++
 source/game/PropDoor.cpp                      | 374 +---------------
 source/game/PropDoor.h                        |  72 +---
 source/game/PropDoorRotating.cpp              |  52 +++
 source/game/PropDoorRotating.h                |  40 ++
 source/physics/Constraint.cpp                 |  62 +++
 source/physics/Constraint.h                   | 110 +++++
 source/physics/PhyWorld.cpp                   |  32 ++
 source/physics/PhyWorld.h                     |   8 +-
 source/physics/Physics.cpp                    |   6 +
 source/physics/Physics.h                      |   2 +
 source/terrax/ProxyObject.cpp                 |   1 +
 source/xcommon/physics/IXConstraint.h         |  46 ++
 source/xcommon/physics/IXPhysics.h            |   5 +
 source/xcsg/BrushMesh.cpp                     |   5 +
 source/xcsg/BrushMesh.h                       |   2 +
 source/xcsg/EditorObject.cpp                  |   5 +-
 29 files changed, 1182 insertions(+), 441 deletions(-)
 create mode 100644 source/game/BaseDoor.cpp
 create mode 100644 source/game/BaseDoor.h
 create mode 100644 source/game/PhysDoor.cpp
 create mode 100644 source/game/PhysDoor.h
 create mode 100644 source/game/PropDoorRotating.cpp
 create mode 100644 source/game/PropDoorRotating.h
 create mode 100644 source/physics/Constraint.cpp
 create mode 100644 source/physics/Constraint.h
 create mode 100644 source/xcommon/physics/IXConstraint.h

diff --git a/proj/sxgame/vs2013/sxgame.vcxproj b/proj/sxgame/vs2013/sxgame.vcxproj
index 719b07cf1..ec55f76d6 100644
--- a/proj/sxgame/vs2013/sxgame.vcxproj
+++ b/proj/sxgame/vs2013/sxgame.vcxproj
@@ -178,6 +178,7 @@
     <ClCompile Include="..\..\..\source\common\file_utils.cpp" />
     <ClCompile Include="..\..\..\source\common\guid.cpp" />
     <ClCompile Include="..\..\..\source\common\string_utils.cpp" />
+    <ClCompile Include="..\..\..\source\game\BaseDoor.cpp" />
     <ClCompile Include="..\..\..\source\game\BaseHandle.cpp" />
     <ClCompile Include="..\..\..\source\game\BaseLight.cpp" />
     <ClCompile Include="..\..\..\source\game\BaseMag.cpp" />
@@ -227,11 +228,13 @@
     <ClCompile Include="..\..\..\source\game\NPCBase.cpp" />
     <ClCompile Include="..\..\..\source\game\NPCZombie.cpp" />
     <ClCompile Include="..\..\..\source\game\PathCorner.cpp" />
+    <ClCompile Include="..\..\..\source\game\PhysDoor.cpp" />
     <ClCompile Include="..\..\..\source\game\PointChangelevel.cpp" />
     <ClCompile Include="..\..\..\source\game\PropBreakable.cpp" />
     <ClCompile Include="..\..\..\source\game\PropButton.cpp" />
     <ClCompile Include="..\..\..\source\game\PropDebris.cpp" />
     <ClCompile Include="..\..\..\source\game\PropDoor.cpp" />
+    <ClCompile Include="..\..\..\source\game\PropDoorRotating.cpp" />
     <ClCompile Include="..\..\..\source\game\PropDynamic.cpp" />
     <ClCompile Include="..\..\..\source\game\PropStatic.cpp" />
     <ClCompile Include="..\..\..\source\game\proptable.cpp" />
@@ -270,6 +273,7 @@
     <ClInclude Include="..\..\..\source\common\AAString.h" />
     <ClInclude Include="..\..\..\source\common\file_utils.h" />
     <ClInclude Include="..\..\..\source\common\string_utils.h" />
+    <ClInclude Include="..\..\..\source\game\BaseDoor.h" />
     <ClInclude Include="..\..\..\source\game\BaseHandle.h" />
     <ClInclude Include="..\..\..\source\game\BaseLight.h" />
     <ClInclude Include="..\..\..\source\game\Baseline.h" />
@@ -312,12 +316,14 @@
     <ClInclude Include="..\..\..\source\game\LogicRelay.h" />
     <ClInclude Include="..\..\..\source\game\LogicStringbuilder.h" />
     <ClInclude Include="..\..\..\source\game\NarrowPassageMovementController.h" />
+    <ClInclude Include="..\..\..\source\game\PhysDoor.h" />
     <ClInclude Include="..\..\..\source\game\physics_util.h" />
     <ClInclude Include="..\..\..\source\game\PointChangelevel.h" />
     <ClInclude Include="..\..\..\source\game\PropBreakable.h" />
     <ClInclude Include="..\..\..\source\game\PropButton.h" />
     <ClInclude Include="..\..\..\source\game\PropDebris.h" />
     <ClInclude Include="..\..\..\source\game\PropDoor.h" />
+    <ClInclude Include="..\..\..\source\game\PropDoorRotating.h" />
     <ClInclude Include="..\..\..\source\game\PropDynamic.h" />
     <ClInclude Include="..\..\..\source\game\PropStatic.h" />
     <ClInclude Include="..\..\..\source\game\Random.h" />
diff --git a/proj/sxgame/vs2013/sxgame.vcxproj.filters b/proj/sxgame/vs2013/sxgame.vcxproj.filters
index f3634cd06..d71df317e 100644
--- a/proj/sxgame/vs2013/sxgame.vcxproj.filters
+++ b/proj/sxgame/vs2013/sxgame.vcxproj.filters
@@ -390,6 +390,15 @@
     <ClCompile Include="..\..\..\source\game\InfoOverlay.cpp">
       <Filter>Source Files\ents\info</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\source\game\BaseDoor.cpp">
+      <Filter>Source Files\ents\props</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\game\PropDoorRotating.cpp">
+      <Filter>Source Files\ents\props</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\game\PhysDoor.cpp">
+      <Filter>Source Files\ents\props</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\source\game\sxgame.h">
@@ -683,6 +692,15 @@
     <ClInclude Include="..\..\..\source\game\InfoOverlay.h">
       <Filter>Header Files\ents\info</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\source\game\BaseDoor.h">
+      <Filter>Header Files\ents\props</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\game\PropDoorRotating.h">
+      <Filter>Header Files\ents\props</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\game\PhysDoor.h">
+      <Filter>Header Files\ents\props</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\..\source\game\sxgame.rc">
diff --git a/proj/sxphysics/vs2013/sxphysics.vcxproj b/proj/sxphysics/vs2013/sxphysics.vcxproj
index e327650bb..5702d4f0b 100644
--- a/proj/sxphysics/vs2013/sxphysics.vcxproj
+++ b/proj/sxphysics/vs2013/sxphysics.vcxproj
@@ -178,6 +178,7 @@
     <ClCompile Include="..\..\..\source\physics\CharacterController.cpp" />
     <ClCompile Include="..\..\..\source\physics\CollisionObject.cpp" />
     <ClCompile Include="..\..\..\source\physics\CollisionShape.cpp" />
+    <ClCompile Include="..\..\..\source\physics\Constraint.cpp" />
     <ClCompile Include="..\..\..\source\physics\MutationObserver.cpp" />
     <ClCompile Include="..\..\..\source\physics\Physics.cpp" />
     <ClCompile Include="..\..\..\source\physics\PhyWorld.cpp" />
@@ -190,6 +191,7 @@
     <ClInclude Include="..\..\..\source\physics\CharacterController.h" />
     <ClInclude Include="..\..\..\source\physics\CollisionObject.h" />
     <ClInclude Include="..\..\..\source\physics\CollisionShape.h" />
+    <ClInclude Include="..\..\..\source\physics\Constraint.h" />
     <ClInclude Include="..\..\..\source\physics\MutationObserver.h" />
     <ClInclude Include="..\..\..\source\physics\PhyWorld.h" />
     <ClInclude Include="..\..\..\source\physics\sxphysics.h" />
@@ -197,6 +199,7 @@
     <ClInclude Include="..\..\..\source\xcommon\physics\IXCharacterController.h" />
     <ClInclude Include="..\..\..\source\xcommon\physics\IXCollisionObject.h" />
     <ClInclude Include="..\..\..\source\xcommon\physics\IXCollisionShape.h" />
+    <ClInclude Include="..\..\..\source\xcommon\physics\IXConstraint.h" />
     <ClInclude Include="..\..\..\source\xcommon\physics\IXMutationObserver.h" />
     <ClInclude Include="..\..\..\source\xcommon\physics\IXPhysics.h" />
   </ItemGroup>
diff --git a/proj/sxphysics/vs2013/sxphysics.vcxproj.filters b/proj/sxphysics/vs2013/sxphysics.vcxproj.filters
index cb4b3c015..517aac226 100644
--- a/proj/sxphysics/vs2013/sxphysics.vcxproj.filters
+++ b/proj/sxphysics/vs2013/sxphysics.vcxproj.filters
@@ -45,6 +45,9 @@
     <ClCompile Include="..\..\..\source\physics\CharacterController.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\source\physics\Constraint.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\source\physics\sxphysics.h">
@@ -92,5 +95,11 @@
     <ClInclude Include="..\..\..\source\xcommon\physics\IXMutationObserver.h">
       <Filter>Header Files\xcommon</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\source\xcommon\physics\IXConstraint.h">
+      <Filter>Header Files\xcommon</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\physics\Constraint.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/sdks/bullet3 b/sdks/bullet3
index 0af01e52b..3770e985d 160000
--- a/sdks/bullet3
+++ b/sdks/bullet3
@@ -1 +1 @@
-Subproject commit 0af01e52bc9053b53d344d52c7258d4e287d718d
+Subproject commit 3770e985d18911de4ab0898ba65a2a4545baf7bb
diff --git a/source/game/BaseAmmo.cpp b/source/game/BaseAmmo.cpp
index 35abac1ee..3265d1272 100644
--- a/source/game/BaseAmmo.cpp
+++ b/source/game/BaseAmmo.cpp
@@ -211,11 +211,16 @@ void CBaseAmmo::fire(const float3 &_vStart, const float3 &_vDir, CBaseCharacter
 			//shoot decal
 			//SXDecals_ShootDecal(DECAL_TYPE_CONCRETE, BTVEC_F3(cb.m_hitPointWorld[0]), BTVEC_F3(cb.m_hitNormalWorld[0]));
 			//SPE_EffectPlayByName("fire", &BTVEC_F3(cb.m_hitPointWorld), &BTVEC_F3(cb.m_hitNormalWorld));
-			/*if(!cb.m_collisionObject->isStaticOrKinematicObject())
+			if(!cb.m_aResults[0].pCollisionObject->isStaticOrKinematic())
 			{
-			((btRigidBody*)cb.m_collisionObject)->applyCentralImpulse(F3_BTVEC(vDir * 10.0f));
-			cb.m_collisionObject->activate();
-			}*/
+				IXRigidBody *pBody = cb.m_aResults[0].pCollisionObject->asRigidBody();
+				if(pBody)
+				{
+					pBody->applyImpulse(vDir * 10.0f, cb.m_aResults[0].vHitPoint);
+				}
+				//((btRigidBody*)cb.m_collisionObject)->applyCentralImpulse(F3_BTVEC(vDir * 10.0f));
+				//cb.m_collisionObject->activate();
+			}
 			//g_pTracer->lineTo(BTVEC_F3(cb.m_hitPointWorld[0]) + SMVector3Normalize(BTVEC_F3(cb.m_hitNormalWorld[0])) * 0.1f, 0.0f);
 		}
 		else
diff --git a/source/game/BaseCharacter.cpp b/source/game/BaseCharacter.cpp
index 65d093d77..9d7f9cb95 100644
--- a/source/game/BaseCharacter.cpp
+++ b/source/game/BaseCharacter.cpp
@@ -78,7 +78,7 @@ void CBaseCharacter::onPostLoad()
 	// Для парашютистов, например, предельная скорость составляет от 190 км/ч при максимальном сопротивлении воздуха, когда они падают плашмя, раскинув руки, до 240 км/ч при нырянии «рыбкой» или «солдатиком».
 	m_pCharacter->setFallSpeed(53.0f);
 	//m_pCharacter->setFallSpeed(30.0f);
-	m_pCharacter->setMaxPenetrationDepth(0.1f);
+	m_pCharacter->setMaxPenetrationDepth(0.0f/*0.1f*/);
 	//m_pGhostObject->setWorldTransform(startTransform);
 
 	GetPhysWorld()->addCollisionObject(m_pGhostObject, CG_CHARACTER, CG_ALL & ~(CG_DEBRIS | CG_HITBOX | CG_WATER));
diff --git a/source/game/BaseDoor.cpp b/source/game/BaseDoor.cpp
new file mode 100644
index 000000000..9d1c35b2d
--- /dev/null
+++ b/source/game/BaseDoor.cpp
@@ -0,0 +1,403 @@
+#include "BaseDoor.h"
+
+/*! \skydocent prop_door
+Дверь
+*/
+
+BEGIN_PROPTABLE(CBaseDoor)
+	//! Открыть
+	DEFINE_INPUT(inputOpen, "open", "Open", PDF_NONE)
+	//! Закрыть
+	DEFINE_INPUT(inputClose, "close", "Close", PDF_NONE)
+	//! Заблокировать
+	DEFINE_INPUT(inputLock, "lock", "Lock", PDF_NONE)
+	//! Разблокировать
+	DEFINE_INPUT(inputUnlock, "unlock", "Unlock", PDF_NONE)
+	//! Переключить
+	DEFINE_INPUT(inputToggle, "toggle", "Toggle", PDF_NONE)
+
+	//! При начале закрытия
+	DEFINE_OUTPUT(m_onClose, "OnClose", "On close")
+	//! При завершении закрытия
+	DEFINE_OUTPUT(m_onClosed, "OnClosed", "On closed")
+	//! При начале открытия
+	DEFINE_OUTPUT(m_onOpen, "OnOpen", "On open")
+	//! При завершении открытия
+	DEFINE_OUTPUT(m_onOpened, "OnOpened", "On opened")
+	//! При попытке использовать заблокированную
+	DEFINE_OUTPUT(m_onUseLocked, "OnUseLocked", "On use locked")
+
+	//! Время до автозакрытия
+	DEFINE_FIELD_FLOAT(m_fAutoCloseTime, PDFF_NONE, "autoclose_time", "Autoclose time", EDITOR_TIMEFIELD)
+	//! Повреждение при блокировке
+	DEFINE_FIELD_FLOAT(m_fBlockDamage, PDFF_NONE, "block_damage", "Block damage", EDITOR_TEXTFIELD)
+
+	DEFINE_FIELD_STRING(m_szSndClose, PDFF_NONE, "snd_close", "Close sound", EDITOR_FILEFIELD)
+		FILE_OPTION("Select sound", "ogg")
+		EDITOR_FILE_END()
+	DEFINE_FIELD_STRING(m_szSndOpen, PDFF_NONE, "snd_open", "Open sound", EDITOR_FILEFIELD)
+		FILE_OPTION("Select sound", "ogg")
+		EDITOR_FILE_END()
+	DEFINE_FIELD_STRING(m_szSndLocked, PDFF_NONE, "snd_locked", "Locked sound", EDITOR_FILEFIELD)
+		FILE_OPTION("Select sound", "ogg")
+		EDITOR_FILE_END()
+
+	//! Изначально заблокирована
+	DEFINE_FLAG(DOOR_START_LOCKED, "Start locked")
+	//! Запрет открытия игроком
+	DEFINE_FLAG(DOOR_NO_USE, "Disable player USE")
+	//! Изначально открыта
+	DEFINE_FLAG(DOOR_START_OPENED, "Start opened")
+	//! Автозакрытие по таймеру
+	DEFINE_FLAG(DOOR_AUTOCLOSE, "Autoclose")
+	//! Форсированное закрытие двери
+	DEFINE_FLAG(DOOR_FORCE, "Force close")
+END_PROPTABLE()
+
+REGISTER_ENTITY_NOLISTING(CBaseDoor, base_door, REC_MODEL_FIELD("model"));
+
+CBaseDoor::~CBaseDoor()
+{
+	releasePhysics();
+
+	mem_release(m_pSndClose);
+	mem_release(m_pSndOpen);
+	mem_release(m_pSndLocked);
+}
+
+void CBaseDoor::onPostLoad()
+{
+	m_isLocked = (getFlags() & DOOR_START_LOCKED) != 0;
+	m_bState = (getFlags() & DOOR_START_OPENED) != 0;
+
+	BaseClass::onPostLoad();
+
+	IXSoundSystem *pSound = (IXSoundSystem*)(Core_GetIXCore()->getPluginManager()->getInterface(IXSOUNDSYSTEM_GUID));
+	if(pSound)
+	{
+		IXSoundLayer *pGameLayer = pSound->findLayer("xGame");
+
+		if(m_szSndClose[0])
+		{
+			m_pSndClose = pGameLayer->newSoundPlayer(m_szSndClose, SOUND_SPACE_3D);
+		}
+		if(m_szSndOpen[0])
+		{
+			m_pSndOpen = pGameLayer->newSoundPlayer(m_szSndOpen, SOUND_SPACE_3D);
+		}
+		if(m_szSndLocked[0])
+		{
+			m_pSndLocked = pGameLayer->newSoundPlayer(m_szSndLocked, SOUND_SPACE_3D);
+		}
+	}
+}
+
+void CBaseDoor::updatePositionFrac(float fPos)
+{
+	m_fPositionFrac = fPos;
+
+	float2 p1(0.0f, 0.0f);
+	float2 p2(0.42f, 0.0f);
+	float2 p3(0.58f, 1.0f);
+	float2 p4(1.0f, 1.0f);
+	float t = m_fPositionFrac;
+	float2 p = powf(1.0f - t, 3.0f) * p1 + 3.0f * powf(1.0f - t, 2.0f) * t * p2 + 3.0f * (1.0f - t) * powf(t, 2.0f) * p3 + powf(t, 3.0f) * p4;
+	t = p.y;
+
+	updatePos(t);
+}
+
+void CBaseDoor::setMoveTime(float fTime)
+{
+	m_fMoveTime = fTime;
+}
+
+void CBaseDoor::inputOpen(inputdata_t * pInputdata)
+{
+	if(m_isLocked)
+	{
+		FIRE_OUTPUT(m_onUseLocked, pInputdata->pInflictor);
+
+		SAFE_CALL(m_pSndLocked, setWorldPos, getPos());
+		SAFE_CALL(m_pSndLocked, play);
+		return;
+	}
+	if(m_bState)
+	{
+		return;
+	}
+	m_bState = true;
+	if(m_isClosing)
+	{
+		stop();
+	}
+
+	m_pInflictor = pInputdata->pInflictor;
+	FIRE_OUTPUT(m_onOpen, pInputdata->pInflictor);
+
+	SAFE_CALL(m_pSndOpen, setWorldPos, getPos());
+	SAFE_CALL(m_pSndOpen, play);
+
+	m_isOpening = true;
+	m_idThinkInterval = SET_INTERVAL(think, 0);
+}
+
+void CBaseDoor::inputClose(inputdata_t * pInputdata)
+{
+	if(m_isLocked)
+	{
+		FIRE_OUTPUT(m_onUseLocked, pInputdata->pInflictor);
+
+		SAFE_CALL(m_pSndLocked, setWorldPos, getPos());
+		SAFE_CALL(m_pSndLocked, play);
+		return;
+	}
+	if(!m_bState)
+	{
+		return;
+	}
+	m_bState = false;
+	if(m_isOpening)
+	{
+		stop();
+	}
+
+	if(ID_VALID(m_idAutoCloseTimeout))
+	{
+		CLEAR_TIMEOUT(m_idAutoCloseTimeout);
+		m_idAutoCloseTimeout = -1;
+	}
+
+	m_pInflictor = pInputdata->pInflictor;
+	FIRE_OUTPUT(m_onClose, pInputdata->pInflictor);
+
+	SAFE_CALL(m_pSndClose, setWorldPos, getPos());
+	SAFE_CALL(m_pSndClose, play);
+
+	m_isClosing = true;
+	m_idThinkInterval = SET_INTERVAL(think, 0);
+}
+
+void CBaseDoor::inputToggle(inputdata_t * pInputdata)
+{
+	if(m_bState)
+	{
+		inputClose(pInputdata);
+	}
+	else
+	{
+		inputOpen(pInputdata);
+	}
+}
+void CBaseDoor::inputLock(inputdata_t * pInputdata)
+{
+	m_isLocked = true;
+}
+
+void CBaseDoor::inputUnlock(inputdata_t * pInputdata)
+{
+	m_isLocked = false;
+}
+
+void CBaseDoor::onUse(CBaseEntity *pUser)
+{
+	if(getFlags() & DOOR_NO_USE)
+	{
+		return;
+	}
+
+	inputdata_t inputdata;
+	memset(&inputdata, 0, sizeof(inputdata_t));
+	inputdata.pActivator = pUser;
+	inputdata.pInflictor = pUser;
+	inputdata.type = PDF_NONE;
+
+	if(getFlags() & DOOR_AUTOCLOSE)
+	{
+		inputOpen(&inputdata);
+	}
+	else
+	{
+		inputToggle(&inputdata);
+	}
+
+	BaseClass::onUse(pUser);
+}
+
+void CBaseDoor::createPhysBody()
+{
+	if(m_pCollideShape)
+	{
+		float3 vPos = getPos();
+		SMQuaternion qRot = getOrient();
+
+		GetPhysics()->newGhostObject(&m_pGhostObject, true);
+		m_pGhostObject->setPosition(vPos);
+		m_pGhostObject->setRotation(qRot);
+		m_pGhostObject->setCollisionShape(m_pCollideShape);
+		m_pGhostObject->setCollisionFlags(XCF_KINEMATIC_OBJECT);
+		m_pGhostObject->setUserPointer(this);
+		m_pGhostObject->setUserTypeId(1);
+		
+		GetPhysWorld()->addCollisionObject(m_pGhostObject, CG_DOOR, CG_CHARACTER | CG_DEFAULT);
+
+		//m_pGhostObject->activate();
+		//m_pGhostObject->setActivationState(DISABLE_DEACTIVATION);
+	}
+}
+void CBaseDoor::removePhysBody()
+{
+	if(m_pGhostObject)
+	{
+		GetPhysWorld()->removeCollisionObject(m_pGhostObject);
+		mem_release(m_pGhostObject);
+	}
+
+	BaseClass::removePhysBody();
+}
+
+void CBaseDoor::think(float fDT)
+{
+	testPenetration();
+
+	if(m_isOpening)
+	{
+		m_fPositionFrac += fDT / m_fMoveTime;
+		if(m_fPositionFrac >= 1.0f)
+		{
+			m_fPositionFrac = 1.0f;
+			FIRE_OUTPUT(m_onOpened, m_pInflictor);
+			stop();
+
+			if(getFlags() & DOOR_AUTOCLOSE)
+			{
+				m_idAutoCloseTimeout = SET_TIMEOUT(close, m_fAutoCloseTime);
+			}
+		}
+	}
+	else if(m_isClosing)
+	{
+		m_fPositionFrac -= fDT / m_fMoveTime;
+		if(m_fPositionFrac <= 0.0f)
+		{
+			m_fPositionFrac = 0.0f;
+			FIRE_OUTPUT(m_onClosed, m_pInflictor);
+			stop();
+		}
+	}
+
+	updatePositionFrac(m_fPositionFrac);
+}
+
+void CBaseDoor::stop()
+{
+	if(ID_VALID(m_idThinkInterval))
+	{
+		CLEAR_INTERVAL(m_idThinkInterval);
+		m_idThinkInterval = -1;
+	}
+	m_isOpening = false;
+	m_isClosing = false;
+
+	//@TODO: Stop sound
+}
+
+void CBaseDoor::setPos(const float3 & pos)
+{
+	BaseClass::setPos(pos);
+	if(m_pGhostObject)
+	{
+		m_pGhostObject->setPosition(pos);
+		//m_pGhostObject->setInterpolationLinearVelocity(btVector3(-0.02f, 0.0f, 0.0f));
+	}
+}
+
+bool CBaseDoor::testPenetration()
+{
+	bool hasContact = false;
+
+	for(UINT i = 0, l = m_pGhostObject->getOverlappingPairCount(); i < l; ++i)
+	{
+
+		IXCollisionPair *collisionPair = m_pGhostObject->getOverlappingPair(i);
+		
+		IXCollisionObject* obj0 = collisionPair->getObject0();
+		IXCollisionObject* obj1 = collisionPair->getObject1();
+
+		if((obj0 && !obj0->hasContactResponse()) || (obj1 && !obj1->hasContactResponse()))
+			continue;
+
+		if(!needsCollision(obj0, obj1))
+			continue;
+
+		for(UINT j = 0, jl = collisionPair->getContactManifoldCount(); j < jl; ++j)
+		{
+			IXContactManifold *manifold = collisionPair->getContactManifold(j);
+			//btScalar directionSign = manifold->getBody0() == m_pGhostObject ? btScalar(-1.0) : btScalar(1.0);
+			for(UINT p = 0, pl = manifold->getContactCount(); p<pl; ++p)
+			{
+				const IXContactManifoldPoint *pt = manifold->getContact(p);
+
+				float dist = pt->getDistance();
+
+				if(dist < -DOOR_MAX_PENETRATION_DEPTH)
+				{
+					hasContact = true;
+					const IXCollisionObject *pObject = (obj0 == m_pGhostObject) ? obj1 : obj0;
+
+					if(pObject->getUserPointer() && pObject->getUserTypeId() == 1)
+					{
+						CBaseEntity *pEnt = (CBaseEntity*)pObject->getUserPointer();
+						if(pEnt)
+						{
+							CTakeDamageInfo takeDamageInfo(this, m_fBlockDamage);
+							takeDamageInfo.m_pInflictor = this;
+
+							pEnt->dispatchDamage(takeDamageInfo);
+						}
+					}
+				}
+			}
+
+			//manifold->clearManifold();
+		}
+	}
+
+	if(hasContact)
+	{
+		if(!(getFlags() & DOOR_FORCE))
+		{
+			inputdata_t inputdata;
+			memset(&inputdata, 0, sizeof(inputdata_t));
+			inputdata.pActivator = this;
+			inputdata.pInflictor = m_pInflictor;
+			inputdata.type = PDF_NONE;
+			inputToggle(&inputdata);
+			return(false);
+		}
+		else
+		{
+			return(true);
+		}
+	}
+	return(false);
+}
+
+bool CBaseDoor::needsCollision(const IXCollisionObject *body0, const IXCollisionObject *body1)
+{
+	bool collides = (body0->getFilterGroup() & body1->getFilterMask()) != 0;
+	collides = collides && (body1->getFilterGroup() & body0->getFilterMask());
+	return(collides);
+}
+
+void CBaseDoor::close(float fDT)
+{
+	m_idAutoCloseTimeout = -1;
+
+	inputdata_t inputdata;
+	memset(&inputdata, 0, sizeof(inputdata_t));
+	inputdata.pActivator = this;
+	inputdata.pInflictor = m_pInflictor;
+	inputdata.type = PDF_NONE;
+	inputClose(&inputdata);
+}
diff --git a/source/game/BaseDoor.h b/source/game/BaseDoor.h
new file mode 100644
index 000000000..3ceec7ad6
--- /dev/null
+++ b/source/game/BaseDoor.h
@@ -0,0 +1,95 @@
+/*!
+\file
+Дверь
+*/
+
+#ifndef __BASE_DOOR_H
+#define __BASE_DOOR_H
+
+#include "PropDynamic.h"
+
+#define DOOR_START_LOCKED ENT_FLAG_0
+#define DOOR_NO_USE ENT_FLAG_1
+#define DOOR_START_OPENED ENT_FLAG_2
+#define DOOR_AUTOCLOSE ENT_FLAG_3
+#define DOOR_FORCE ENT_FLAG_4
+
+#define DOOR_MAX_PENETRATION_DEPTH 0.16f
+
+/*! Дверь
+\ingroup cbaseanimating
+*/
+class CBaseDoor: public CPropDynamic
+{
+	DECLARE_CLASS(CBaseDoor, CPropDynamic);
+	DECLARE_PROPTABLE();
+public:
+	DECLARE_TRIVIAL_CONSTRUCTOR();
+	~CBaseDoor();
+
+	void onPostLoad() override;
+
+	void onUse(CBaseEntity *pUser);
+
+	//! Устанавливает положение в мире
+	void setPos(const float3 & pos);
+
+protected:
+	virtual void updatePos(float fPosNormalized) {};
+
+	void updatePositionFrac(float fPos);
+	void setMoveTime(float fTime);
+
+	void inputOpen(inputdata_t * pInputdata);
+	void inputClose(inputdata_t * pInputdata);
+	void inputLock(inputdata_t * pInputdata);
+	void inputUnlock(inputdata_t * pInputdata);
+	void inputToggle(inputdata_t * pInputdata);
+
+	void think(float fDT);
+	ID m_idThinkInterval = -1;
+	ID m_idAutoCloseTimeout = -1;
+
+	void stop();
+
+	bool testPenetration();
+	bool needsCollision(const IXCollisionObject *body0, const IXCollisionObject *body1);
+
+	output_t m_onClose;
+	output_t m_onClosed;
+	output_t m_onOpen;
+	output_t m_onOpened;
+	output_t m_onUseLocked;
+
+	void createPhysBody() override;
+	void removePhysBody() override;
+
+	float m_fAutoCloseTime = 0.0f;
+	float m_fBlockDamage = 100.0f;
+
+	const char * m_szSndClose;
+	IXSoundPlayer *m_pSndClose = NULL;
+	const char * m_szSndOpen;
+	IXSoundPlayer *m_pSndOpen = NULL;
+	const char * m_szSndLocked;
+	IXSoundPlayer *m_pSndLocked = NULL;
+
+	bool m_isLocked = false;
+
+	bool m_bState = false;
+
+	IXGhostObject *m_pGhostObject = NULL;
+
+private:
+	float m_fPositionFrac = 0.0f;
+	float m_fMoveTime = 1.0f;
+
+	bool m_isOpening = false;
+	bool m_isClosing = false;
+
+	CBaseEntity *m_pInflictor = NULL;
+
+	void close(float fDT);
+};
+
+#endif
diff --git a/source/game/DogholeMovementController.cpp b/source/game/DogholeMovementController.cpp
index a4292c0a8..74c4417d4 100644
--- a/source/game/DogholeMovementController.cpp
+++ b/source/game/DogholeMovementController.cpp
@@ -63,12 +63,12 @@ void CDogholeMovementController::handleMove(const float3 &vDir)
 
 void CDogholeMovementController::handleJump()
 {
-	m_bWillDismount = true;
+	//m_bWillDismount = true;
 }
 
 bool CDogholeMovementController::handleUse()
 {
-	m_bWillDismount = true;
+	//m_bWillDismount = true;
 	return(true);
 }
 
diff --git a/source/game/NarrowPassageMovementController.cpp b/source/game/NarrowPassageMovementController.cpp
index d4b9cce1e..7fbf2ce92 100644
--- a/source/game/NarrowPassageMovementController.cpp
+++ b/source/game/NarrowPassageMovementController.cpp
@@ -60,12 +60,12 @@ void CNarrowPassageMovementController::handleMove(const float3 &vDir)
 
 void CNarrowPassageMovementController::handleJump()
 {
-	m_bWillDismount = true;
+	//m_bWillDismount = true;
 }
 
 bool CNarrowPassageMovementController::handleUse()
 {
-	m_bWillDismount = true;
+	//m_bWillDismount = true;
 	return(true);
 }
 
diff --git a/source/game/PhysDoor.cpp b/source/game/PhysDoor.cpp
new file mode 100644
index 000000000..f45584f01
--- /dev/null
+++ b/source/game/PhysDoor.cpp
@@ -0,0 +1,196 @@
+#include "PhysDoor.h"
+
+/*! \skydocent phys_door
+Физическая дверЪ
+*/
+
+BEGIN_PROPTABLE(CPhysDoor)
+	DEFINE_FIELD_VECTORFN(m_vPivot, PDFF_USE_GIZMO, "door_pivot", "Pivot", setPivot, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "y")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+	
+	DEFINE_FIELD_VECTORFN(m_vBaseDir, PDFF_USE_GIZMO, "door_base_dir", "Base dir", setBaseDir, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "y")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+	
+	DEFINE_FIELD_VECTORFN(m_vLimitDir0, PDFF_USE_GIZMO, "door_limit_dir0", "Limit dir 0", setLimit0Dir, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "y")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+
+	DEFINE_FIELD_VECTORFN(m_vLimitDir1, PDFF_USE_GIZMO, "door_limit_dir1", "Limit dir 1", setLimit1Dir, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "y")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+END_PROPTABLE()
+
+REGISTER_ENTITY(CPhysDoor, phys_door, REC_MODEL_FIELD("model"));
+
+void CPhysDoor::createPhysBody()
+{
+	setCollisionGroup(CG_DOOR, CG_STATIC_MASK);
+	BaseClass::createPhysBody();
+
+	XHingeConstraintBodyDesc desc = {};
+	desc.pBody = m_pRigidBody;
+	desc.vAxis = getOrient() * float3_t(0.0f, 1.0f, 0.0f);
+	desc.vPivot = getOrient() * m_vPivot;
+	GetPhysics()->newHingeConstraint(&m_pHingeConstraint, &desc);
+	//m_pHingeConstraint->setLimit(-SM_PIDIV2, SM_PIDIV2);
+	updateLimits();
+	//m_pHingeConstraint->setLimit(-100.0f, 100.0f);
+	GetPhysWorld()->addConstraint(m_pHingeConstraint);
+}
+void CPhysDoor::removePhysBody()
+{
+	GetPhysWorld()->removeConstraint(m_pHingeConstraint);
+	mem_release(m_pHingeConstraint);
+	BaseClass::removePhysBody();
+}
+
+void CPhysDoor::setPivot(const float3 &vPivot)
+{
+	m_vPivot = vPivot;
+	if(m_pRigidBody)
+	{
+		removePhysBody();
+		createPhysBody();
+	}
+}
+
+void CPhysDoor::setBaseDir(const float3 &vDir)
+{
+	m_vBaseDir = vDir;
+
+	updateLimits();
+}
+
+void CPhysDoor::setLimit0Dir(const float3 &vDir)
+{
+	m_vLimitDir0 = vDir;
+
+	updateLimits();
+}
+
+void CPhysDoor::setLimit1Dir(const float3 &vDir)
+{
+	m_vLimitDir1 = vDir;
+
+	updateLimits();
+}
+
+void CPhysDoor::updateLimits()
+{
+	if(m_pHingeConstraint)
+	{
+		float3 vDir0 = SMVector3Normalize(m_vLimitDir0 - m_vPivot);
+		float3 vDir1 = SMVector3Normalize(m_vLimitDir1 - m_vPivot);
+		float3 vBase = SMVector3Normalize(m_vBaseDir - m_vPivot);
+
+		float3 vUp = getOrient() * float3(0.0f, 1.0f, 0.0f);
+
+		float fAngle0 = SMRightAngleBetweenVectors(vBase, vDir0, vUp);
+		float fAngle1 = SMRightAngleBetweenVectors(vBase, vDir1, vUp);
+
+		float fLim0;
+		float fLim1;
+
+		if(fAngle0 < fAngle1)
+		{
+			fLim0 = -(SM_2PI - fAngle1);
+			fLim1 = fAngle0;
+		}
+		else
+		{
+			fLim0 = -(SM_2PI - fAngle0);
+			fLim1 = fAngle1;
+		}
+		m_pHingeConstraint->setLimit(fLim0, fLim1);
+		LogInfo("setLimit(%f, %f)\n", fLim0, fLim1);
+
+
+
+
+
+		/*
+		float fLim0 = safe_acosf(SMVector3Dot(vDir0, vBase));
+		float fLim1 = safe_acosf(SMVector3Dot(vDir1, vBase));
+		//float fLim = safe_acosf(SMVector3Dot(SMVector3Normalize(m_vLimitDir0 - m_vPivot), SMVector3Normalize(m_vLimitDir1 - m_vPivot)));
+		//m_pHingeConstraint->setLimit(-fLim * 0.5f, fLim * 0.5f);
+		//LogInfo("setLimit(%f, %f)\n", -fLim * 0.5f, fLim * 0.5f);
+		m_pHingeConstraint->setLimit(-fLim0, fLim1);*/
+	}
+}
+
+void CPhysDoor::renderEditor(bool is3D, bool bRenderSelection, IXGizmoRenderer *pRenderer)
+{
+	if(pRenderer && bRenderSelection)
+	{
+		const float4 c_vLineColor = bRenderSelection ? float4(1.0f, 0.0f, 0.0f, 1.0f) : float4(1.0f, 0.0f, 1.0f, 1.0f);
+		pRenderer->setColor(c_vLineColor);
+		pRenderer->setLineWidth(is3D ? 0.02f : 1.0f);
+
+		float3 vDir0 = SMVector3Normalize(m_vLimitDir0 - m_vPivot);
+		float3 vDir1 = SMVector3Normalize(m_vLimitDir1 - m_vPivot);
+		float3 vBase = SMVector3Normalize(m_vBaseDir - m_vPivot);
+
+		float3 vBasePos = getPos() + getOrient() * m_vPivot;
+		pRenderer->jumpTo(vBasePos + getOrient() * vDir0);
+		pRenderer->lineTo(vBasePos);
+		pRenderer->lineTo(vBasePos + getOrient() * vDir1);
+
+		pRenderer->setColor(float4(1.0f, 1.0f, 0.0f, 1.0f));
+		pRenderer->jumpTo(vBasePos);
+		pRenderer->lineTo(vBasePos + getOrient() * vBase);
+
+		const float c_fStep = SMToRadian(5.0f);
+		/*
+		float fLim0 = safe_acosf(SMVector3Dot(vDir0, vBase));
+		float fLim1 = safe_acosf(SMVector3Dot(vDir1, vBase));
+
+		UINT uSteps = (UINT)(fLim0 / c_fStep);
+		if(!uSteps)
+		{
+			uSteps = 1;
+		}
+		float fStep = fLim0 / (float)uSteps;
+		float3 vVec = getOrient() * vBase;
+		pRenderer->jumpTo(vBasePos + vVec);
+		for(UINT i = 0; i < uSteps; ++i)
+		{
+			pRenderer->lineTo(vBasePos + SMQuaternion(getOrient() * float3(0.0f, 1.0f, 0.0f), (float)(i + 1) * fStep) * vVec);
+		}*/
+
+		float3 vUp = getOrient() * float3(0.0f, 1.0f, 0.0f);
+
+		float fAngleDir = SMRightAngleBetweenVectors(vDir0, vDir1, vUp);
+		float fAngleBase = SMRightAngleBetweenVectors(vDir0, vBase, vUp);
+
+		float3 vVec = vDir1;
+		if(fAngleBase > fAngleDir)
+		{
+			fAngleDir = SM_2PI - fAngleDir;
+			vVec = vDir0;
+		}
+		vVec = getOrient() * vVec;
+
+		UINT uSteps = (UINT)(fAngleDir / c_fStep);
+		if(!uSteps)
+		{
+			uSteps = 1;
+		}
+
+		float fStep = fAngleDir / (float)uSteps;
+		pRenderer->jumpTo(vBasePos + vVec);
+		for(UINT i = 0; i < uSteps; ++i)
+		{
+			pRenderer->lineTo(vBasePos + SMQuaternion(vUp, (float)(i + 1) * fStep) * vVec);
+		}
+	}
+}
diff --git a/source/game/PhysDoor.h b/source/game/PhysDoor.h
new file mode 100644
index 000000000..f4c782ed1
--- /dev/null
+++ b/source/game/PhysDoor.h
@@ -0,0 +1,46 @@
+/*!
+\file
+Модель
+*/
+
+#ifndef __PHYS_DOOR_H
+#define __PHYS_DOOR_H
+
+#include "PropDynamic.h"
+
+/*! Модель
+\ingroup cbaseanimating
+*/
+class CPhysDoor: public CPropDynamic
+{
+	DECLARE_CLASS(CPhysDoor, CPropDynamic);
+	DECLARE_PROPTABLE();
+public:
+	DECLARE_TRIVIAL_CONSTRUCTOR();
+
+	void setPivot(const float3 &vPivot);
+
+	void renderEditor(bool is3D, bool bRenderSelection, IXGizmoRenderer *pRenderer) override;
+
+protected:
+	void createPhysBody() override;
+	void removePhysBody() override;
+
+private:
+	IXHingeConstraint *m_pHingeConstraint = NULL;
+
+	float3_t m_vPivot;
+
+	float3_t m_vBaseDir = float3_t(0.0f, 0.0f, 1.0f);
+	float3_t m_vLimitDir0 = float3_t(-1.0f, 0.0f, 0.0f);
+	float3_t m_vLimitDir1 = float3_t(1.0f, 0.0f, 0.0f);
+
+private:
+	void setBaseDir(const float3 &vDir);
+	void setLimit0Dir(const float3 &vDir);
+	void setLimit1Dir(const float3 &vDir);
+
+	void updateLimits();
+};
+
+#endif
diff --git a/source/game/PropDoor.cpp b/source/game/PropDoor.cpp
index c2c693de2..985a34ab1 100644
--- a/source/game/PropDoor.cpp
+++ b/source/game/PropDoor.cpp
@@ -11,224 +11,24 @@ See the license in LICENSE
 */
 
 BEGIN_PROPTABLE(CPropDoor)
-	//! Открыть
-	DEFINE_INPUT(inputOpen, "open", "Open", PDF_NONE)
-	//! Закрыть
-	DEFINE_INPUT(inputClose, "close", "Close", PDF_NONE)
-	//! Заблокировать
-	DEFINE_INPUT(inputLock, "lock", "Lock", PDF_NONE)
-	//! Разблокировать
-	DEFINE_INPUT(inputUnlock, "unlock", "Unlock", PDF_NONE)
-	//! Переключить
-	DEFINE_INPUT(inputToggle, "toggle", "Toggle", PDF_NONE)
-
-	//! При начале закрытия
-	DEFINE_OUTPUT(m_onClose, "OnClose", "On close")
-	//! При завершении закрытия
-	DEFINE_OUTPUT(m_onClosed, "OnClosed", "On closed")
-	//! При начале открытия
-	DEFINE_OUTPUT(m_onOpen, "OnOpen", "On open")
-	//! При завершении открытия
-	DEFINE_OUTPUT(m_onOpened, "OnOpened", "On opened")
-	//! При попытке использовать заблокированную
-	DEFINE_OUTPUT(m_onUseLocked, "OnUseLocked", "On use locked")
-
-	//! Время до автозакрытия
-	DEFINE_FIELD_FLOAT(m_fAutoCloseTime, PDFF_NONE, "autoclose_time", "Autoclose time", EDITOR_TIMEFIELD)
-	//! Повреждение при блокировке
-	DEFINE_FIELD_FLOAT(m_fBlockDamage, PDFF_NONE, "block_damage", "Block damage", EDITOR_TEXTFIELD)
 	//! Величина смещение (0-авто)
 	DEFINE_FIELD_FLOAT(m_fDistanceOverride, PDFF_NONE, "distance_override", "Distance override", EDITOR_TEXTFIELD)
 	//! Скорость, м/с
-	DEFINE_FIELD_FLOAT(m_fSpeed, PDFF_NONE, "speed", "Speed", EDITOR_TEXTFIELD)
+	DEFINE_FIELD_FLOATFN(m_fSpeed, PDFF_NONE, "speed", "Speed", setSpeed, EDITOR_TEXTFIELD)
 	//! Направление открытия
 	DEFINE_FIELD_ANGLES(m_qAngle, PDFF_NONE, "open_angle", "Open angle", EDITOR_ANGLES)
-
-	DEFINE_FIELD_STRING(m_szSndClose, PDFF_NONE, "snd_close", "Close sound", EDITOR_FILEFIELD)
-		FILE_OPTION("Select sound", "ogg")
-		EDITOR_FILE_END()
-	DEFINE_FIELD_STRING(m_szSndOpen, PDFF_NONE, "snd_open", "Open sound", EDITOR_FILEFIELD)
-		FILE_OPTION("Select sound", "ogg")
-		EDITOR_FILE_END()
-	DEFINE_FIELD_STRING(m_szSndLocked, PDFF_NONE, "snd_locked", "Locked sound", EDITOR_FILEFIELD)
-		FILE_OPTION("Select sound", "ogg")
-		EDITOR_FILE_END()
-
-	//! Изначально заблокирована
-	DEFINE_FLAG(DOOR_START_LOCKED, "Start locked")
-	//! Запрет открытия игроком
-	DEFINE_FLAG(DOOR_NO_USE, "Disable player USE")
-	//! Изначально открыта
-	DEFINE_FLAG(DOOR_START_OPENED, "Start opened")
-	//! Автозакрытие по таймеру
-	DEFINE_FLAG(DOOR_AUTOCLOSE, "Autoclose")
-	//! Форсированное закрытие двери
-	DEFINE_FLAG(DOOR_FORCE, "Force close")
 END_PROPTABLE()
 
 REGISTER_ENTITY(CPropDoor, prop_door, REC_MODEL_FIELD("model"));
 
-CPropDoor::~CPropDoor()
-{
-	releasePhysics();
-
-	mem_release(m_pSndClose);
-	mem_release(m_pSndOpen);
-	mem_release(m_pSndLocked);
-}
-
-void CPropDoor::onPostLoad()
-{
-	m_isLocked = (getFlags() & DOOR_START_LOCKED) != 0;
-	m_bState = (getFlags() & DOOR_START_OPENED) != 0;
-
-	BaseClass::onPostLoad();
-
-	IXSoundSystem *pSound = (IXSoundSystem*)(Core_GetIXCore()->getPluginManager()->getInterface(IXSOUNDSYSTEM_GUID));
-	if(pSound)
-	{
-		IXSoundLayer *pGameLayer = pSound->findLayer("xGame");
-
-		if(m_szSndClose[0])
-		{
-			m_pSndClose = pGameLayer->newSoundPlayer(m_szSndClose, SOUND_SPACE_3D);
-		}
-		if(m_szSndOpen[0])
-		{
-			m_pSndOpen = pGameLayer->newSoundPlayer(m_szSndOpen, SOUND_SPACE_3D);
-		}
-		if(m_szSndLocked[0])
-		{
-			m_pSndLocked = pGameLayer->newSoundPlayer(m_szSndLocked, SOUND_SPACE_3D);
-		}
-	}
-}
-
-void CPropDoor::inputOpen(inputdata_t * pInputdata)
-{
-	if(m_isLocked)
-	{
-		FIRE_OUTPUT(m_onUseLocked, pInputdata->pInflictor);
-
-		SAFE_CALL(m_pSndLocked, setWorldPos, getPos());
-		SAFE_CALL(m_pSndLocked, play);
-		return;
-	}
-	if(m_bState)
-	{
-		return;
-	}
-	m_bState = true;
-	if(m_isClosing)
-	{
-		stop();
-	}
-
-	m_pInflictor = pInputdata->pInflictor;
-	FIRE_OUTPUT(m_onOpen, pInputdata->pInflictor);
-
-	SAFE_CALL(m_pSndOpen, setWorldPos, getPos());
-	SAFE_CALL(m_pSndOpen, play);
-
-	m_isOpening = true;
-	m_idThinkInterval = SET_INTERVAL(think, 0);
-}
-void CPropDoor::inputClose(inputdata_t * pInputdata)
-{
-	if(m_isLocked)
-	{
-		FIRE_OUTPUT(m_onUseLocked, pInputdata->pInflictor);
-
-		SAFE_CALL(m_pSndLocked, setWorldPos, getPos());
-		SAFE_CALL(m_pSndLocked, play);
-		return;
-	}
-	if(!m_bState)
-	{
-		return;
-	}
-	m_bState = false;
-	if(m_isOpening)
-	{
-		stop();
-	}
-
-	if(ID_VALID(m_idAutoCloseTimeout))
-	{
-		CLEAR_TIMEOUT(m_idAutoCloseTimeout);
-		m_idAutoCloseTimeout = -1;
-	}
-
-	m_pInflictor = pInputdata->pInflictor;
-	FIRE_OUTPUT(m_onClose, pInputdata->pInflictor);
-
-	SAFE_CALL(m_pSndClose, setWorldPos, getPos());
-	SAFE_CALL(m_pSndClose, play);
-
-	m_isClosing = true;
-	m_idThinkInterval = SET_INTERVAL(think, 0);
-}
-void CPropDoor::inputToggle(inputdata_t * pInputdata)
-{
-	if(m_bState)
-	{
-		inputClose(pInputdata);
-	}
-	else
-	{
-		inputOpen(pInputdata);
-	}
-}
-void CPropDoor::inputLock(inputdata_t * pInputdata)
-{
-	m_isLocked = true;
-}
-void CPropDoor::inputUnlock(inputdata_t * pInputdata)
-{
-	m_isLocked = false;
-}
-
-void CPropDoor::onUse(CBaseEntity *pUser)
-{
-	if(getFlags() & DOOR_NO_USE)
-	{
-		return;
-	}
-
-	inputdata_t inputdata;
-	memset(&inputdata, 0, sizeof(inputdata_t));
-	inputdata.pActivator = pUser;
-	inputdata.pInflictor = pUser;
-	inputdata.type = PDF_NONE;
-
-	if(getFlags() & DOOR_AUTOCLOSE)
-	{
-		inputOpen(&inputdata);
-	}
-	else
-	{
-		inputToggle(&inputdata);
-	}
-
-	BaseClass::onUse(pUser);
-}
-
 void CPropDoor::createPhysBody()
 {
 	if(m_pCollideShape)
 	{
-		float3 vPos = getPos();
+		m_vStartPos = getPos();
 		SMQuaternion qRot = getOrient();
 
-		GetPhysics()->newGhostObject(&m_pGhostObject, true);
-		m_pGhostObject->setPosition(vPos);
-		m_pGhostObject->setRotation(qRot);
-		m_pGhostObject->setCollisionShape(m_pCollideShape);
-		m_pGhostObject->setCollisionFlags(XCF_KINEMATIC_OBJECT);
-		m_pGhostObject->setUserPointer(this);
-		m_pGhostObject->setUserTypeId(1);
-		
-		GetPhysWorld()->addCollisionObject(m_pGhostObject, CG_DOOR, CG_CHARACTER | CG_DEFAULT);
+		BaseClass::createPhysBody();
 
 		//m_pGhostObject->activate();
 		//m_pGhostObject->setActivationState(DISABLE_DEACTIVATION);
@@ -267,177 +67,23 @@ void CPropDoor::createPhysBody()
 			m_fDistance = fMax - fMin;
 		}
 
-		m_vStartPos = vPos;
 		m_vEndPos = (float3)(m_vStartPos + vDir * m_fDistance);
+		setMoveTime(m_fDistance / m_fSpeed);
 
 		if(m_bState)
 		{
-			setPos(m_vEndPos);
-			m_fPositionFrac = 1.0f;
-		}
-	}
-}
-void CPropDoor::removePhysBody()
-{
-	if(m_pGhostObject)
-	{
-		GetPhysWorld()->removeCollisionObject(m_pGhostObject);
-		mem_release(m_pGhostObject);
-	}
-
-	BaseClass::removePhysBody();
-}
-
-void CPropDoor::think(float fDT)
-{
-	testPenetration();
-
-	if(m_isOpening)
-	{
-		m_fPositionFrac += fDT * m_fSpeed / m_fDistance;
-		if(m_fPositionFrac >= 1.0f)
-		{
-			m_fPositionFrac = 1.0f;
-			FIRE_OUTPUT(m_onOpened, m_pInflictor);
-			stop();
-
-			if(getFlags() & DOOR_AUTOCLOSE)
-			{
-				m_idAutoCloseTimeout = SET_TIMEOUT(close, m_fAutoCloseTime);
-			}
-		}
-	}
-	else if(m_isClosing)
-	{
-		m_fPositionFrac -= fDT * m_fSpeed / m_fDistance;
-		if(m_fPositionFrac <= 0.0f)
-		{
-			m_fPositionFrac = 0.0f;
-			FIRE_OUTPUT(m_onClosed, m_pInflictor);
-			stop();
-		}
-	}
-
-	float2 p1(0.0f, 0.0f);
-	float2 p2(0.42f, 0.0f);
-	float2 p3(0.58f, 1.0f);
-	float2 p4(1.0f, 1.0f);
-	float t = m_fPositionFrac;
-	float2 p = powf(1.0f - t, 3.0f) * p1 + 3.0f * powf(1.0f - t, 2.0f) * t * p2 + 3.0f * (1.0f - t) * powf(t, 2.0f) * p3 + powf(t, 3.0f) * p4;
-	t = p.y;
-
-	setPos(SMVectorLerp(m_vStartPos, m_vEndPos, t));
-}
-
-void CPropDoor::stop()
-{
-	if(ID_VALID(m_idThinkInterval))
-	{
-		CLEAR_INTERVAL(m_idThinkInterval);
-		m_idThinkInterval = -1;
-	}
-	m_isOpening = false;
-	m_isClosing = false;
-
-	//@TODO: Stop sound
-}
-
-void CPropDoor::setPos(const float3 & pos)
-{
-	BaseClass::setPos(pos);
-	if(m_pGhostObject)
-	{
-		m_pGhostObject->setPosition(pos);
-		//m_pGhostObject->setInterpolationLinearVelocity(btVector3(-0.02f, 0.0f, 0.0f));
-	}
-}
-
-bool CPropDoor::testPenetration()
-{
-	bool hasContact = false;
-
-	for(UINT i = 0, l = m_pGhostObject->getOverlappingPairCount(); i < l; ++i)
-	{
-
-		IXCollisionPair *collisionPair = m_pGhostObject->getOverlappingPair(i);
-		
-		IXCollisionObject* obj0 = collisionPair->getObject0();
-		IXCollisionObject* obj1 = collisionPair->getObject1();
-
-		if((obj0 && !obj0->hasContactResponse()) || (obj1 && !obj1->hasContactResponse()))
-			continue;
-
-		if(!needsCollision(obj0, obj1))
-			continue;
-
-		for(UINT j = 0, jl = collisionPair->getContactManifoldCount(); j < jl; ++j)
-		{
-			IXContactManifold *manifold = collisionPair->getContactManifold(j);
-			//btScalar directionSign = manifold->getBody0() == m_pGhostObject ? btScalar(-1.0) : btScalar(1.0);
-			for(UINT p = 0, pl = manifold->getContactCount(); p<pl; ++p)
-			{
-				const IXContactManifoldPoint *pt = manifold->getContact(p);
-
-				float dist = pt->getDistance();
-
-				if(dist < -DOOR_MAX_PENETRATION_DEPTH)
-				{
-					hasContact = true;
-					const IXCollisionObject *pObject = (obj0 == m_pGhostObject) ? obj1 : obj0;
-
-					if(pObject->getUserPointer() && pObject->getUserTypeId() == 1)
-					{
-						CBaseEntity *pEnt = (CBaseEntity*)pObject->getUserPointer();
-						if(pEnt)
-						{
-							CTakeDamageInfo takeDamageInfo(this, m_fBlockDamage);
-							takeDamageInfo.m_pInflictor = this;
-
-							pEnt->dispatchDamage(takeDamageInfo);
-						}
-					}
-				}
-			}
-
-			//manifold->clearManifold();
-		}
-	}
-
-	if(hasContact)
-	{
-		if(!(getFlags() & DOOR_FORCE))
-		{
-			inputdata_t inputdata;
-			memset(&inputdata, 0, sizeof(inputdata_t));
-			inputdata.pActivator = this;
-			inputdata.pInflictor = m_pInflictor;
-			inputdata.type = PDF_NONE;
-			inputToggle(&inputdata);
-			return(false);
-		}
-		else
-		{
-			return(true);
+			updatePositionFrac(1.0f);
 		}
 	}
-	return(false);
 }
 
-bool CPropDoor::needsCollision(const IXCollisionObject *body0, const IXCollisionObject *body1)
+void CPropDoor::setSpeed(float fSpeed)
 {
-	bool collides = (body0->getFilterGroup() & body1->getFilterMask()) != 0;
-	collides = collides && (body1->getFilterGroup() & body0->getFilterMask());
-	return(collides);
+	m_fSpeed = fSpeed;
+	setMoveTime(m_fDistance / m_fSpeed);
 }
 
-void CPropDoor::close(float fDT)
+void CPropDoor::updatePos(float fPosNormalized) 
 {
-	m_idAutoCloseTimeout = -1;
-
-	inputdata_t inputdata;
-	memset(&inputdata, 0, sizeof(inputdata_t));
-	inputdata.pActivator = this;
-	inputdata.pInflictor = m_pInflictor;
-	inputdata.type = PDF_NONE;
-	inputClose(&inputdata);
+	setPos(SMVectorLerp(m_vStartPos, m_vEndPos, fPosNormalized));
 }
diff --git a/source/game/PropDoor.h b/source/game/PropDoor.h
index 125fe229e..c125b193e 100644
--- a/source/game/PropDoor.h
+++ b/source/game/PropDoor.h
@@ -12,95 +12,37 @@ See the license in LICENSE
 #ifndef __PROP_DOOR_H
 #define __PROP_DOOR_H
 
-#include "PropDynamic.h"
-
-#define DOOR_START_LOCKED ENT_FLAG_0
-#define DOOR_NO_USE ENT_FLAG_1
-#define DOOR_START_OPENED ENT_FLAG_2
-#define DOOR_AUTOCLOSE ENT_FLAG_3
-#define DOOR_FORCE ENT_FLAG_4
+#include "BaseDoor.h"
 
 //! базовое направление для двери
 #define DOOR_BASE_DIR float3(0, 0, -1.0f)
 
-#define DOOR_MAX_PENETRATION_DEPTH 0.16f
-
 /*! Дверь
 \ingroup cbaseanimating
 */
-class CPropDoor: public CPropDynamic
+class CPropDoor: public CBaseDoor
 {
-	DECLARE_CLASS(CPropDoor, CPropDynamic);
+	DECLARE_CLASS(CPropDoor, CBaseDoor);
 	DECLARE_PROPTABLE();
 public:
 	DECLARE_TRIVIAL_CONSTRUCTOR();
-	~CPropDoor();
-
-	void onPostLoad() override;
-
-	void onUse(CBaseEntity *pUser);
-
-	//! Устанавливает положение в мире
-	void setPos(const float3 & pos);
 
 protected:
-	void inputOpen(inputdata_t * pInputdata);
-	void inputClose(inputdata_t * pInputdata);
-	void inputLock(inputdata_t * pInputdata);
-	void inputUnlock(inputdata_t * pInputdata);
-	void inputToggle(inputdata_t * pInputdata);
-
-	void think(float fDT);
-	ID m_idThinkInterval = -1;
-	ID m_idAutoCloseTimeout = -1;
-
-	void stop();
-
-	bool testPenetration();
-	bool needsCollision(const IXCollisionObject *body0, const IXCollisionObject *body1);
-
-	output_t m_onClose;
-	output_t m_onClosed;
-	output_t m_onOpen;
-	output_t m_onOpened;
-	output_t m_onUseLocked;
+	void updatePos(float fPosNormalized) override;
 
-	virtual void createPhysBody();
-	virtual void removePhysBody();
+	void createPhysBody() override;
 
-	float m_fAutoCloseTime = 0.0f;
 	float m_fDistanceOverride = 0.0f;
 	float m_fSpeed = 0.1f;
 	SMQuaternion m_qAngle;
-	float m_fBlockDamage = 100.0f;
-
-	const char * m_szSndClose;
-	IXSoundPlayer *m_pSndClose = NULL;
-	const char * m_szSndOpen;
-	IXSoundPlayer *m_pSndOpen = NULL;
-	const char * m_szSndLocked;
-	IXSoundPlayer *m_pSndLocked = NULL;
-
-	bool m_isLocked = false;
-
-	bool m_bState = false;
-
-	IXGhostObject *m_pGhostObject = NULL;
 
 private:
+	void setSpeed(float fSpeed);
+
 	float m_fDistance = 0.0f;
 
 	float3_t m_vStartPos;
 	float3_t m_vEndPos;
-
-	float m_fPositionFrac = 0.0f;
-
-	bool m_isOpening = false;
-	bool m_isClosing = false;
-
-	CBaseEntity *m_pInflictor = NULL;
-
-	void close(float fDT);
 };
 
 #endif
diff --git a/source/game/PropDoorRotating.cpp b/source/game/PropDoorRotating.cpp
new file mode 100644
index 000000000..9720624e4
--- /dev/null
+++ b/source/game/PropDoorRotating.cpp
@@ -0,0 +1,52 @@
+#include "PropDoorRotating.h"
+
+/*! \skydocent prop_door
+Дверь
+*/
+
+BEGIN_PROPTABLE(CPropDoorRotating)
+	//! Величина смещение (0-авто)
+	DEFINE_FIELD_FLOAT(m_fDistanceOverride, PDFF_NONE, "distance_override", "Distance override", EDITOR_TEXTFIELD)
+	//! Скорость, м/с
+	DEFINE_FIELD_FLOATFN(m_fSpeed, PDFF_NONE, "speed", "Speed", setSpeed, EDITOR_TEXTFIELD)
+	//! Направление открытия
+	DEFINE_FIELD_ANGLES(m_qAngle, PDFF_NONE, "open_angle", "Open angle", EDITOR_ANGLES)
+END_PROPTABLE()
+
+// REGISTER_ENTITY(CPropDoorRotating, prop_door_rotating, REC_MODEL_FIELD("model"));
+
+CPropDoorRotating::~CPropDoorRotating()
+{
+	releasePhysics();
+
+	mem_release(m_pSndClose);
+	mem_release(m_pSndOpen);
+	mem_release(m_pSndLocked);
+}
+
+void CPropDoorRotating::createPhysBody()
+{
+	if(m_pCollideShape)
+	{
+		float3 vPos = getPos();
+		SMQuaternion qRot = getOrient();
+
+		GetPhysics()->newGhostObject(&m_pGhostObject, true);
+		m_pGhostObject->setPosition(vPos);
+		m_pGhostObject->setRotation(qRot);
+		m_pGhostObject->setCollisionShape(m_pCollideShape);
+		m_pGhostObject->setCollisionFlags(XCF_KINEMATIC_OBJECT);
+		m_pGhostObject->setUserPointer(this);
+		m_pGhostObject->setUserTypeId(1);
+		
+		GetPhysWorld()->addCollisionObject(m_pGhostObject, CG_DOOR, CG_STATIC_MASK);
+
+		//m_pGhostObject->activate();
+		//m_pGhostObject->setActivationState(DISABLE_DEACTIVATION);
+	}
+}
+
+void CPropDoorRotating::setSpeed(float fSpeed)
+{
+
+}
diff --git a/source/game/PropDoorRotating.h b/source/game/PropDoorRotating.h
new file mode 100644
index 000000000..aea441ddf
--- /dev/null
+++ b/source/game/PropDoorRotating.h
@@ -0,0 +1,40 @@
+/*!
+\file
+Дверь
+*/
+
+#ifndef __PROP_DOOR_ROTATING_H
+#define __PROP_DOOR_ROTATING_H
+
+#include "BaseDoor.h"
+
+/*! Дверь
+\ingroup cbaseanimating
+*/
+class CPropDoorRotating: public CBaseDoor
+{
+	DECLARE_CLASS(CPropDoorRotating, CBaseDoor);
+	DECLARE_PROPTABLE();
+public:
+	DECLARE_TRIVIAL_CONSTRUCTOR();
+	~CPropDoorRotating();
+
+protected:
+	virtual void updatePos(float fPosNormalized) {};
+
+	void createPhysBody() override;
+
+	float m_fDistanceOverride = 0.0f;
+	float m_fSpeed = 0.1f;
+	SMQuaternion m_qAngle;
+
+private:
+	void setSpeed(float fSpeed);
+
+	float m_fDistance = 0.0f;
+
+	float3_t m_vStartPos;
+	float3_t m_vEndPos;
+};
+
+#endif
diff --git a/source/physics/Constraint.cpp b/source/physics/Constraint.cpp
new file mode 100644
index 000000000..fc144c096
--- /dev/null
+++ b/source/physics/Constraint.cpp
@@ -0,0 +1,62 @@
+#include "Constraint.h"
+#include "CollisionObject.h"
+
+btTypedConstraint* GetConstraint(IXBaseConstraint *pConstraint)
+{
+	btTypedConstraint *pBtConstraint = NULL;
+	switch(pConstraint->getType())
+	{
+	case XCT_HINGE:
+		pBtConstraint = ((CHingeConstraint*)pConstraint->asHinge())->getBtConstraint();
+		break;
+	default:
+		assert(!"Unknown constraint type!");
+		break;
+	}
+	assert(pBtConstraint);
+
+	return(pBtConstraint);
+}
+
+//#############################################################################
+
+CHingeConstraint::CHingeConstraint(const XHingeConstraintBodyDesc *pBodyA, const XHingeConstraintBodyDesc *pBodyB, bool useReferenceFrameA):
+	BaseClass(XCT_HINGE)
+{
+	setBodyA(pBodyA->pBody);
+	btRigidBody *pRigidBodyA = ((CRigidBody*)pBodyA->pBody)->getBtRigidBody();
+	btVector3 vPivotA = F3_BTVEC(pBodyA->vPivot);
+	btVector3 vAxisA = F3_BTVEC(pBodyA->vAxis);
+	if(pBodyB)
+	{
+		setBodyB(pBodyB->pBody);
+
+		btRigidBody *pRigidBodyB = ((CRigidBody*)pBodyB->pBody)->getBtRigidBody();
+		btVector3 vPivotB = F3_BTVEC(pBodyB->vPivot);
+		btVector3 vAxisB = F3_BTVEC(pBodyB->vAxis);
+
+		m_pConstraint = new btHingeConstraint(*pRigidBodyA, *pRigidBodyB, vPivotA, vPivotB, vAxisA, vAxisB, useReferenceFrameA);
+	}
+	else
+	{
+		m_pConstraint = new btHingeConstraint(*pRigidBodyA, vPivotA, vAxisA, useReferenceFrameA);
+	}
+
+	m_pConstraint->setDbgDrawSize(0.5f);
+
+	setConstraint(m_pConstraint);
+}
+CHingeConstraint::~CHingeConstraint()
+{
+	mem_delete(m_pConstraint);
+}
+
+void XMETHODCALLTYPE CHingeConstraint::setAngularOnly(bool yesNo)
+{
+	m_pConstraint->setAngularOnly(yesNo);
+}
+
+void XMETHODCALLTYPE CHingeConstraint::setLimit(float fLow, float fHigh, float fSoftness, float fBiasFactor, float fRelaxationFactor)
+{
+	m_pConstraint->setLimit(fLow, fHigh, fSoftness, fBiasFactor, fRelaxationFactor);
+}
diff --git a/source/physics/Constraint.h b/source/physics/Constraint.h
new file mode 100644
index 000000000..49be90500
--- /dev/null
+++ b/source/physics/Constraint.h
@@ -0,0 +1,110 @@
+#ifndef __CONSTRAINT_H
+#define __CONSTRAINT_H
+
+#include <btBulletDynamicsCommon.h>
+#include <xcommon/physics/IXConstraint.h>
+#include <xcommon/physics/IXCollisionObject.h>
+
+btTypedConstraint* GetConstraint(IXBaseConstraint *pConstraint);
+
+template<class T>
+class CConstraint: public IXUnknownImplementation<T>
+{
+public:
+	typedef CConstraint<T> BaseClass;
+
+	CConstraint(XCONSTRAINT_TYPE type) :
+		m_type(type)
+	{
+	}
+
+	~CConstraint()
+	{
+		mem_release(m_pBodyA);
+		mem_release(m_pBodyB);
+	}
+	
+	void XMETHODCALLTYPE setUserPointer(void *pUser) override
+	{
+		m_pUserPointer = pUser;
+	}
+	void* XMETHODCALLTYPE getUserPointer() const override
+	{
+		return(m_pUserPointer);
+	}
+
+	void XMETHODCALLTYPE setUserTypeId(int iUser) override
+	{
+		m_iUserTypeId = iUser;
+	}
+	int XMETHODCALLTYPE getUserTypeId() const override
+	{
+		return(m_iUserTypeId);
+	}
+
+	XCONSTRAINT_TYPE XMETHODCALLTYPE getType() const override
+	{
+		return(m_type);
+	}
+
+	IXHingeConstraint* XMETHODCALLTYPE asHinge() override
+	{
+		return(NULL);
+	}
+
+	btTypedConstraint* getBtConstraint()
+	{
+		return(m_pBtConstraint);
+	}
+
+protected:
+	void setConstraint(btTypedConstraint *pBtConstraint)
+	{
+		m_pBtConstraint = pBtConstraint;
+		pBtConstraint->setUserConstraintPtr((IXBaseConstraint*)this);
+	}
+
+	void setBodyA(IXRigidBody *pBody)
+	{
+		m_pBodyA = pBody;
+		add_ref(pBody);
+	}
+
+	void setBodyB(IXRigidBody *pBody)
+	{
+		m_pBodyB = pBody;
+		add_ref(pBody);
+	}
+
+private:
+	XCONSTRAINT_TYPE m_type;
+	void *m_pUserPointer = NULL;
+	btTypedConstraint *m_pBtConstraint = NULL;
+	int m_iUserTypeId = -1;
+
+	IXRigidBody *m_pBodyA = NULL;
+	IXRigidBody *m_pBodyB = NULL;
+};
+
+//#############################################################################
+
+class CHingeConstraint: public CConstraint<IXHingeConstraint>
+{
+public:
+	CHingeConstraint(const XHingeConstraintBodyDesc *pBodyA, const XHingeConstraintBodyDesc *pBodyB = NULL, bool useReferenceFrameA = false);
+	~CHingeConstraint();
+
+	IXHingeConstraint* XMETHODCALLTYPE asHinge() override
+	{
+		return(this);
+	}
+
+	void XMETHODCALLTYPE setAngularOnly(bool yesNo) override;
+
+	void XMETHODCALLTYPE setLimit(float fLow, float fHigh, float fSoftness = 0.9f, float fBiasFactor = 0.3f, float fRelaxationFactor = 1.0f) override;
+
+private:
+	btHingeConstraint *m_pConstraint;
+};
+
+#endif
diff --git a/source/physics/PhyWorld.cpp b/source/physics/PhyWorld.cpp
index c4eb6d186..c5d5176de 100644
--- a/source/physics/PhyWorld.cpp
+++ b/source/physics/PhyWorld.cpp
@@ -11,6 +11,8 @@ See the license in LICENSE
 
 #include "CollisionObject.h"
 
+#include "Constraint.h"
+
 //#include <BulletDynamics/MLCPSolvers/btDantzigSolver.h>
 //#include <BulletDynamics/MLCPSolvers/btMLCPSolver.h>
 
@@ -393,8 +395,18 @@ void CPhyWorld::runQueue()
 				m_pDynamicsWorld->updateSingleAabb(i.pBtObj);
 			}
 			break;
+
+		case QIT_ADD_CONSTRAINT:
+			// TODO disable collision between linked bodies
+			m_pDynamicsWorld->addConstraint(GetConstraint(i.pConstraint));
+			break;
+
+		case QIT_REMOVE_CONSTRAINT:
+			m_pDynamicsWorld->removeConstraint(GetConstraint(i.pConstraint));
+			break;
 		}
 		mem_release(i.pObj);
+		mem_release(i.pConstraint);
 	}
 }
 
@@ -453,6 +465,26 @@ void CPhyWorld::updateSingleAABB(IXCollisionObject *pObj, btCollisionObject *pBt
 	enqueue({QIT_UPDATE_SINGLE_AABB, pObj, 0, 0, pBtObj});
 }
 
+void XMETHODCALLTYPE CPhyWorld::addConstraint(IXBaseConstraint *pConstraint)
+{
+	assert(pConstraint);
+
+	// One ref for world, the second for queue
+	add_ref(pConstraint);
+	add_ref(pConstraint);
+	enqueue({QIT_ADD_CONSTRAINT, NULL, 0, 0, NULL, pConstraint});
+}
+void XMETHODCALLTYPE CPhyWorld::removeConstraint(IXBaseConstraint *pConstraint)
+{
+	if(!pConstraint)
+	{
+		return;
+	}
+
+	add_ref(pConstraint);
+	enqueue({QIT_REMOVE_CONSTRAINT, NULL, 0, 0, NULL, pConstraint});
+}
+
 //##############################################################
 
 void XMETHODCALLTYPE CPhyWorld::CRenderable::renderStage(X_RENDER_STAGE stage, IXRenderableVisibility *pVisibility)
diff --git a/source/physics/PhyWorld.h b/source/physics/PhyWorld.h
index 00a0a53ba..28dedc01b 100644
--- a/source/physics/PhyWorld.h
+++ b/source/physics/PhyWorld.h
@@ -168,6 +168,9 @@ public:
 
 	void XMETHODCALLTYPE convexSweepTest(IXConvexShape *pShape, const transform_t &xfFrom, const transform_t &xfTo, IXConvexCallback *pCallback, COLLISION_GROUP collisionGroup = CG_DEFAULT, COLLISION_GROUP collisionMask = CG_ALL) override;
 
+	void XMETHODCALLTYPE addConstraint(IXBaseConstraint *pConstraint) override;
+	void XMETHODCALLTYPE removeConstraint(IXBaseConstraint *pConstraint) override;
+
 	template<class T>
 	void updateSingleAABB(T *pObj)
 	{
@@ -198,7 +201,9 @@ private:
 	{
 		QIT_ADD_COLLISION_OBJECT,
 		QIT_REMOVE_COLLISION_OBJECT,
-		QIT_UPDATE_SINGLE_AABB
+		QIT_UPDATE_SINGLE_AABB,
+		QIT_ADD_CONSTRAINT,
+		QIT_REMOVE_CONSTRAINT
 	};
 
 	struct QueueItem
@@ -208,6 +213,7 @@ private:
 		int iGroup;
 		int iMask;
 		btCollisionObject *pBtObj;
+		IXBaseConstraint *pConstraint;
 	};
 
 	Queue<QueueItem> m_queue;
diff --git a/source/physics/Physics.cpp b/source/physics/Physics.cpp
index 34a9850cb..69c6d0100 100644
--- a/source/physics/Physics.cpp
+++ b/source/physics/Physics.cpp
@@ -7,6 +7,7 @@
 #include "MutationObserver.h"
 #include <core/sxcore.h>
 #include <LinearMath/btGeometryUtil.h>
+#include "Constraint.h"
 
 CPhysics::CPhysics(CPhyWorld *pDefaultWorld):
 	m_pDefaultWorld(pDefaultWorld)
@@ -193,3 +194,8 @@ UINT XMETHODCALLTYPE CPhysics::adjustConvexHullForMargin(UINT uPoints, const flo
 
 	return(uResultVertexCount);
 }
+
+void XMETHODCALLTYPE CPhysics::newHingeConstraint(IXHingeConstraint **ppOut, const XHingeConstraintBodyDesc *pBodyA, const XHingeConstraintBodyDesc *pBodyB, bool useReferenceFrameA)
+{
+	*ppOut = new CHingeConstraint(pBodyA, pBodyB, useReferenceFrameA);
+}
diff --git a/source/physics/Physics.h b/source/physics/Physics.h
index b03133fca..8e89e7d53 100644
--- a/source/physics/Physics.h
+++ b/source/physics/Physics.h
@@ -30,6 +30,8 @@ public:
 
 	UINT XMETHODCALLTYPE adjustConvexHullForMargin(UINT uPoints, const float3_t *pInPoints, IXBuffer **ppOutBuffer, float *pfMargin = NULL, byte u8Stride = sizeof(float3_t), bool bOptimize = true) override;
 	
+	void XMETHODCALLTYPE newHingeConstraint(IXHingeConstraint **ppOut, const XHingeConstraintBodyDesc *pBodyA, const XHingeConstraintBodyDesc *pBodyB = NULL, bool useReferenceFrameA = false) override;
+
 private:
 	CPhyWorld *m_pDefaultWorld;
 };
diff --git a/source/terrax/ProxyObject.cpp b/source/terrax/ProxyObject.cpp
index 20d1e4131..f812b3d79 100644
--- a/source/terrax/ProxyObject.cpp
+++ b/source/terrax/ProxyObject.cpp
@@ -120,6 +120,7 @@ void XMETHODCALLTYPE CProxyObject::render(bool is3D, bool bRenderSelection, IXGi
 	{
 		m_aObjects[i].pObj->render(is3D, bRenderSelection, pGizmoRenderer);
 	}
+	m_pTargetObject->render(is3D, bRenderSelection, pGizmoRenderer);
 }
 
 bool XMETHODCALLTYPE CProxyObject::rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut, float3 *pvNormal, ID *pidMtrl, bool bReturnNearestPoint)
diff --git a/source/xcommon/physics/IXConstraint.h b/source/xcommon/physics/IXConstraint.h
new file mode 100644
index 000000000..a8ebf3368
--- /dev/null
+++ b/source/xcommon/physics/IXConstraint.h
@@ -0,0 +1,46 @@
+#ifndef __IXCONSTRAINT_H
+#define __IXCONSTRAINT_H
+
+#include <gdefines.h>
+#include "IXCollisionObject.h"
+
+enum XCONSTRAINT_TYPE
+{
+	XCT_HINGE
+};
+
+class IXHingeConstraint;
+
+class IXBaseConstraint: public IXUnknown
+{
+public:
+	virtual void XMETHODCALLTYPE setUserPointer(void *pUser) = 0;
+	virtual void* XMETHODCALLTYPE getUserPointer() const = 0;
+
+	virtual void XMETHODCALLTYPE setUserTypeId(int iUser) = 0;
+	virtual int XMETHODCALLTYPE getUserTypeId() const = 0;
+
+	virtual XCONSTRAINT_TYPE XMETHODCALLTYPE getType() const = 0;
+
+	virtual IXHingeConstraint* XMETHODCALLTYPE asHinge() = 0;
+};
+
+//#############################################################################
+
+struct XHingeConstraintBodyDesc
+{
+	IXRigidBody *pBody;
+	float3_t vPivot;
+	float3_t vAxis;
+};
+
+class IXHingeConstraint: public IXBaseConstraint
+{
+public:
+	virtual void XMETHODCALLTYPE setAngularOnly(bool yesNo) = 0;
+
+	virtual void XMETHODCALLTYPE setLimit(float fLow, float fHigh, float fSoftness = 0.9f, float fBiasFactor = 0.3f, float fRelaxationFactor = 1.0f) = 0;
+};
+
+
+#endif
diff --git a/source/xcommon/physics/IXPhysics.h b/source/xcommon/physics/IXPhysics.h
index 5597e5326..b79336738 100644
--- a/source/xcommon/physics/IXPhysics.h
+++ b/source/xcommon/physics/IXPhysics.h
@@ -6,6 +6,7 @@
 #include "IXCollisionObject.h"
 #include "IXCharacterController.h"
 #include "IXMutationObserver.h"
+#include "IXConstraint.h"
 
 /*
 typedef enum PHY_ScalarType
@@ -66,6 +67,8 @@ public:
 
 	virtual void XMETHODCALLTYPE convexSweepTest(IXConvexShape *pShape, const transform_t &xfFrom, const transform_t &xfTo, IXConvexCallback *pCallback, COLLISION_GROUP collisionGroup = CG_DEFAULT, COLLISION_GROUP collisionMask = CG_ALL) = 0;
 
+	virtual void XMETHODCALLTYPE addConstraint(IXBaseConstraint *pConstraint) = 0;
+	virtual void XMETHODCALLTYPE removeConstraint(IXBaseConstraint *pConstraint) = 0;
 	// add/remove action
 	// convexSweepTest
 	// add/remove constraint
@@ -104,6 +107,8 @@ public:
 	virtual IXPhysicsWorld* XMETHODCALLTYPE getWorld(void *pReserved = NULL) = 0;
 
 	virtual UINT XMETHODCALLTYPE adjustConvexHullForMargin(UINT uPoints, const float3_t *pInPoints, IXBuffer **ppOutBuffer, float *pfMargin = NULL, byte u8Stride = sizeof(float3_t), bool bOptimize = true) = 0;
+
+	virtual void XMETHODCALLTYPE newHingeConstraint(IXHingeConstraint **ppOut, const XHingeConstraintBodyDesc *pBodyA, const XHingeConstraintBodyDesc *pBodyB = NULL, bool useReferenceFrameA = false) = 0;
 };
 
 #endif
diff --git a/source/xcsg/BrushMesh.cpp b/source/xcsg/BrushMesh.cpp
index a1a1cba20..874754644 100644
--- a/source/xcsg/BrushMesh.cpp
+++ b/source/xcsg/BrushMesh.cpp
@@ -2404,6 +2404,11 @@ void CBrushMesh::setColor(const float3_t &vColor)
 	SAFE_CALL(m_pModel, setColor, (float4)vColor);
 }
 
+bool CBrushMesh::isFinalized() const
+{
+	return(m_isFinalized);
+}
+
 //##########################################################################
 
 Subset& CMeshBuilder::getSubset(UINT id)
diff --git a/source/xcsg/BrushMesh.h b/source/xcsg/BrushMesh.h
index 39f72d434..02a6ffb90 100644
--- a/source/xcsg/BrushMesh.h
+++ b/source/xcsg/BrushMesh.h
@@ -126,6 +126,8 @@ public:
 
 	void setColor(const float3_t &vColor);
 
+	bool isFinalized() const;
+
 private:
 	void buildModel(bool bBuildPhysbox = true);
 	void setupFromOutline(COutline *pOutline, UINT uContour, float fHeight);
diff --git a/source/xcsg/EditorObject.cpp b/source/xcsg/EditorObject.cpp
index 47fcd5fd4..990e9d32c 100644
--- a/source/xcsg/EditorObject.cpp
+++ b/source/xcsg/EditorObject.cpp
@@ -214,7 +214,10 @@ void XMETHODCALLTYPE CEditorObject::create()
 	m_isRemoved = false;
 	for(UINT i = 0, l = m_aBrushes.size(); i < l; ++i)
 	{
-		m_aBrushes[i]->enable(true);
+		if(!m_aBrushes[i]->isFinalized())
+		{
+			m_aBrushes[i]->enable(true);
+		}
 	}
 	m_isVisible = true;
 	//SAFE_CALL(m_pModel, onObjectAdded, this);
-- 
GitLab