diff --git a/build/gamesource/textures/decal/decals.ini b/build/engine/config/decals/decals.cfg
similarity index 87%
rename from build/gamesource/textures/decal/decals.ini
rename to build/engine/config/decals/decals.cfg
index 438710c7aed537951bd0844fc34a9322d07eba3c..5054a8a4802bee12ac478bb04d96107770bbc8da 100644
--- a/build/gamesource/textures/decal/decals.ini
+++ b/build/engine/config/decals/decals.cfg
@@ -2,7 +2,7 @@
 [decal_0]
 id=0
 base_scale=2.0
-tex=decal_test.dds
+tex=decal_test
 tex0=[0,0,64,64]
 tex1=[64,0,128,64]
 tex2=[0,64,64,128]
@@ -12,7 +12,7 @@ tex3=[64,64,128,128]
 [decal_1]
 id=1
 base_scale=2.0
-tex=decal_test.dds
+tex=decal_test
 tex0=[128,0,192,64]
 tex1=[192,0,256,64]
 tex2=[128,64,192,128]
@@ -20,9 +20,9 @@ tex3=[192,64,256,128]
 
 ;wood
 [decal_2]
-id=2
+id=4
 base_scale=2.0
-tex=decal_test.dds
+tex=decal_test
 tex0=[256,0,320,64]
 tex1=[320,0,384,64]
 tex2=[256,64,320,128]
@@ -31,9 +31,9 @@ tex4=[384,0,448,64]
 
 ;glass
 [decal_3]
-id=3
+id=2
 base_scale=2.0
-tex=decal_test.dds
+tex=decal_test
 tex0=[0,128,128,256]
 tex1=[128,128,256,256]
 tex2=[256,128,384,256]
@@ -44,7 +44,7 @@ tex4=[128,256,256,384]
 [decal_4]
 id=7
 base_scale=8.0
-tex=decal_test.dds
+tex=decal_test
 tex0=[256,256,384,384]
 tex1=[384,256,512,384]
 tex2=[384,128,512,256]
diff --git a/build/gamesource/textures/decal/decal_test.dds b/build/engine/textures/decal/decal_test.dds
similarity index 100%
rename from build/gamesource/textures/decal/decal_test.dds
rename to build/engine/textures/decal/decal_test.dds
diff --git a/build/engine/textures/decal/decal_test2.dds b/build/engine/textures/decal/decal_test2.dds
new file mode 100644
index 0000000000000000000000000000000000000000..497ec0a645bbe369790dd9d2d78db10214bb2d5e
Binary files /dev/null and b/build/engine/textures/decal/decal_test2.dds differ
diff --git a/build/engine/textures/decal/decal_test3.dds b/build/engine/textures/decal/decal_test3.dds
new file mode 100644
index 0000000000000000000000000000000000000000..b068b6500463898c431ee6069992113c5d9bc5de
Binary files /dev/null and b/build/engine/textures/decal/decal_test3.dds differ
diff --git a/docs/gen-entity.js b/docs/gen-entity.js
index 95ba19a9209b1a6bffd67fe2eb9df10021af47ee..6641926e08179d5d94270d297b48a0a3cea78f36 100644
--- a/docs/gen-entity.js
+++ b/docs/gen-entity.js
@@ -19,6 +19,7 @@ g_mFieldTypes = {
 	"DEFINE_FIELD_STRING": "string",
 	"DEFINE_FIELD_ANGLES": "angles",
 	"DEFINE_FIELD_INT": "int",
+	"DEFINE_FIELD_UINT": "uint",
 	"DEFINE_FIELD_ENUM": null,
 	"DEFINE_FIELD_FLOAT": "float",
 	"DEFINE_FIELD_BOOL": "bool",
@@ -29,6 +30,7 @@ g_mFieldTypes = {
 g_mInputTypes = {
 	"PDF_NONE": "none",
 	"PDF_INT": "int",
+	"PDF_UINT": "uint",
 	"PDF_FLOAT": "float",
 	"PDF_VECTOR": "float3",
 	"PDF_VECTOR4": "float4",
diff --git a/proj/sxanim/vs2013/sxanim.vcxproj b/proj/sxanim/vs2013/sxanim.vcxproj
index bd4a7d117b7fa96a3a354af493a33fc234c34eac..e20becf019bca210c7f7fcd838fbbad66df5363a 100644
--- a/proj/sxanim/vs2013/sxanim.vcxproj
+++ b/proj/sxanim/vs2013/sxanim.vcxproj
@@ -181,26 +181,36 @@
     <ClCompile Include="..\..\..\source\anim\AnimatedModel.cpp" />
     <ClCompile Include="..\..\..\source\anim\AnimatedModelProvider.cpp" />
     <ClCompile Include="..\..\..\source\anim\AnimatedModelShared.cpp" />
+    <ClCompile Include="..\..\..\source\anim\Decal.cpp" />
+    <ClCompile Include="..\..\..\source\anim\DecalProvider.cpp" />
     <ClCompile Include="..\..\..\source\anim\dllmain.cpp" />
     <ClCompile Include="..\..\..\source\anim\DynamicModel.cpp" />
     <ClCompile Include="..\..\..\source\anim\DynamicModelProvider.cpp" />
     <ClCompile Include="..\..\..\source\anim\DynamicModelShared.cpp" />
+    <ClCompile Include="..\..\..\source\anim\ModelOverlay.cpp" />
     <ClCompile Include="..\..\..\source\anim\plugin_main.cpp" />
     <ClCompile Include="..\..\..\source\anim\Renderable.cpp" />
     <ClCompile Include="..\..\..\source\anim\RenderableVisibility.cpp" />
+    <ClCompile Include="..\..\..\source\anim\StaticModelProvider.cpp" />
     <ClCompile Include="..\..\..\source\anim\Updatable.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\source\anim\AnimatedModel.h" />
     <ClInclude Include="..\..\..\source\anim\AnimatedModelProvider.h" />
     <ClInclude Include="..\..\..\source\anim\AnimatedModelShared.h" />
+    <ClInclude Include="..\..\..\source\anim\Decal.h" />
+    <ClInclude Include="..\..\..\source\anim\DecalProvider.h" />
     <ClInclude Include="..\..\..\source\anim\DynamicModel.h" />
     <ClInclude Include="..\..\..\source\anim\DynamicModelProvider.h" />
     <ClInclude Include="..\..\..\source\anim\DynamicModelShared.h" />
+    <ClInclude Include="..\..\..\source\anim\ModelOverlay.h" />
     <ClInclude Include="..\..\..\source\anim\Renderable.h" />
     <ClInclude Include="..\..\..\source\anim\RenderableVisibility.h" />
+    <ClInclude Include="..\..\..\source\anim\StaticModelProvider.h" />
     <ClInclude Include="..\..\..\source\anim\Updatable.h" />
     <ClInclude Include="..\..\..\source\xcommon\IXScene.h" />
+    <ClInclude Include="..\..\..\source\xcommon\resource\IXDecal.h" />
+    <ClInclude Include="..\..\..\source\xcommon\resource\IXDecalProvider.h" />
     <ClInclude Include="..\..\..\source\xcommon\resource\IXModelProvider.h" />
   </ItemGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
diff --git a/proj/sxanim/vs2013/sxanim.vcxproj.filters b/proj/sxanim/vs2013/sxanim.vcxproj.filters
index 1db63868b08793ae349c2374e6c93b2f85f5ed5b..e907c6c64e5b8546f8410d5adccef4efb1c75ec3 100644
--- a/proj/sxanim/vs2013/sxanim.vcxproj.filters
+++ b/proj/sxanim/vs2013/sxanim.vcxproj.filters
@@ -49,6 +49,18 @@
     <ClCompile Include="..\..\..\source\anim\dllmain.cpp">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\source\anim\DecalProvider.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\anim\ModelOverlay.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\anim\Decal.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\..\source\anim\StaticModelProvider.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\source\anim\Renderable.h">
@@ -84,5 +96,23 @@
     <ClInclude Include="..\..\..\source\xcommon\IXScene.h">
       <Filter>Header Files\xcommon</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\source\xcommon\resource\IXDecalProvider.h">
+      <Filter>Header Files\xcommon</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\xcommon\resource\IXDecal.h">
+      <Filter>Header Files\xcommon</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\anim\DecalProvider.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\anim\ModelOverlay.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\anim\Decal.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\..\source\anim\StaticModelProvider.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
 </Project>
\ No newline at end of file
diff --git a/proj/sxgame/vs2013/sxgame.vcxproj b/proj/sxgame/vs2013/sxgame.vcxproj
index 6b2bc8d735699766e886b7b69dfee7e44688adcd..8ab0fd8f3d070671bc27d0323b1e6e3104e4d851 100644
--- a/proj/sxgame/vs2013/sxgame.vcxproj
+++ b/proj/sxgame/vs2013/sxgame.vcxproj
@@ -210,6 +210,7 @@
     <ClCompile Include="..\..\..\source\game\GUICraftController.cpp" />
     <ClCompile Include="..\..\..\source\game\GUIInventoryController.cpp" />
     <ClCompile Include="..\..\..\source\game\HUDcontroller.cpp" />
+    <ClCompile Include="..\..\..\source\game\InfoOverlay.cpp" />
     <ClCompile Include="..\..\..\source\game\InfoParticlePlayer.cpp" />
     <ClCompile Include="..\..\..\source\game\LadderMovementController.cpp" />
     <ClCompile Include="..\..\..\source\game\LightDirectional.cpp" />
@@ -297,6 +298,7 @@
     <ClInclude Include="..\..\..\source\game\HUDcontroller.h" />
     <ClInclude Include="..\..\..\source\game\IGameState.h" />
     <ClInclude Include="..\..\..\source\game\IMovementController.h" />
+    <ClInclude Include="..\..\..\source\game\InfoOverlay.h" />
     <ClInclude Include="..\..\..\source\game\InfoParticlePlayer.h" />
     <ClInclude Include="..\..\..\source\game\LadderMovementController.h" />
     <ClInclude Include="..\..\..\source\game\LightSun.h" />
diff --git a/proj/sxgame/vs2013/sxgame.vcxproj.filters b/proj/sxgame/vs2013/sxgame.vcxproj.filters
index ad313030c273047139c37747e35db9ac6b354975..01a241c50c71b92e5a88a35df59b9daf88dae08e 100644
--- a/proj/sxgame/vs2013/sxgame.vcxproj.filters
+++ b/proj/sxgame/vs2013/sxgame.vcxproj.filters
@@ -381,6 +381,9 @@
     <ClCompile Include="..\..\..\source\game\NarrowPassageMovementController.cpp">
       <Filter>Source Files\character_movement</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\source\game\InfoOverlay.cpp">
+      <Filter>Source Files\ents\info</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\..\source\game\sxgame.h">
@@ -665,6 +668,9 @@
     <ClInclude Include="..\..\..\source\game\NarrowPassageMovementController.h">
       <Filter>Header Files\character_movement</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\source\game\InfoOverlay.h">
+      <Filter>Header Files\ents\info</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\..\source\game\sxgame.rc">
diff --git a/proj/terrax/vs2013/terrax.vcxproj b/proj/terrax/vs2013/terrax.vcxproj
index fad89107a72f7c7eefc68fbdd5884a7f2d5b1e37..461cd97712f84454376cb5f6e7a277ec2993bd4f 100644
--- a/proj/terrax/vs2013/terrax.vcxproj
+++ b/proj/terrax/vs2013/terrax.vcxproj
@@ -244,6 +244,7 @@
     <ClCompile Include="..\..\..\source\terrax\MaterialEditor.cpp" />
     <ClCompile Include="..\..\..\source\terrax\PropertyWindow.cpp" />
     <ClCompile Include="..\..\..\source\terrax\ProxyObject.cpp" />
+    <ClCompile Include="..\..\..\source\terrax\ResourceBrowser.cpp" />
     <ClCompile Include="..\..\..\source\terrax\SceneTreeWindow.cpp" />
     <ClCompile Include="..\..\..\source\terrax\ScrollBar.cpp" />
     <ClCompile Include="..\..\..\source\terrax\terrax.cpp" />
@@ -303,6 +304,7 @@
     <ClInclude Include="..\..\..\source\terrax\MaterialEditor.h" />
     <ClInclude Include="..\..\..\source\terrax\PropertyWindow.h" />
     <ClInclude Include="..\..\..\source\terrax\ProxyObject.h" />
+    <ClInclude Include="..\..\..\source\terrax\ResourceBrowser.h" />
     <ClInclude Include="..\..\..\source\terrax\SceneTreeWindow.h" />
     <ClInclude Include="..\..\..\source\terrax\ScrollBar.h" />
     <ClInclude Include="..\..\..\source\terrax\TextureViewGraphNode.h" />
diff --git a/proj/terrax/vs2013/terrax.vcxproj.filters b/proj/terrax/vs2013/terrax.vcxproj.filters
index 919fa263ed4e46045b8d5715f2b3abe0c6500dfc..71fef577fe56b1114d70f818b5cdfa5b198c4d30 100644
--- a/proj/terrax/vs2013/terrax.vcxproj.filters
+++ b/proj/terrax/vs2013/terrax.vcxproj.filters
@@ -213,6 +213,9 @@
     <ClCompile Include="..\..\..\source\terrax\CommandUngroup.cpp">
       <Filter>Source Files\cmd</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\..\source\terrax\ResourceBrowser.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\..\source\terrax\terrax.rc">
@@ -415,6 +418,9 @@
     <ClInclude Include="..\..\..\source\terrax\CommandUngroup.h">
       <Filter>Header Files\cmd</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\..\source\terrax\ResourceBrowser.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <Image Include="..\..\..\source\terrax\resource\new.bmp">
diff --git a/source/anim/AnimatedModelProvider.cpp b/source/anim/AnimatedModelProvider.cpp
index 7265f05afe21f7cc46a3741aacdbd90febda6034..9147e9e896e73633729ddec5f844894268343fb8 100644
--- a/source/anim/AnimatedModelProvider.cpp
+++ b/source/anim/AnimatedModelProvider.cpp
@@ -202,6 +202,11 @@ void CAnimatedModelProvider::bindVertexFormat()
 	m_pMaterialSystem->bindVS(m_pVertexShaderHandler);
 }
 
+bool CAnimatedModelProvider::hasPendingOps()
+{
+	return(!m_queueGPUinitModel.empty() || !m_queueGPUinitShared.empty());
+}
+
 void CAnimatedModelProvider::render(CRenderableVisibility *pVisibility)
 {
 	XPROFILE_FUNCTION();
diff --git a/source/anim/AnimatedModelProvider.h b/source/anim/AnimatedModelProvider.h
index a9242e6352a19a3fd0edb65edaa17fee6974dfb9..4789f1b6483a20fd8d9f5ca619c67c5886ce0af3 100644
--- a/source/anim/AnimatedModelProvider.h
+++ b/source/anim/AnimatedModelProvider.h
@@ -37,6 +37,8 @@ public:
 
 	void bindVertexFormat();
 
+	bool hasPendingOps();
+
 protected:
 	AssotiativeArray<IXResourceModelAnimated*, Array<CAnimatedModelShared*>> m_mModels;
 
diff --git a/source/anim/Decal.cpp b/source/anim/Decal.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..18c808f591f1dc7419b80d62252a1c0d9c76d326
--- /dev/null
+++ b/source/anim/Decal.cpp
@@ -0,0 +1,657 @@
+#include "Decal.h"
+#include "DynamicModel.h"
+#include "DecalProvider.h"
+
+CDecal::CDecal(IXSceneQuery *pSceneQuery, IXRender *pRender, CDecalProvider *pProvider):
+	m_pSceneQuery(pSceneQuery),
+	m_pRender(pRender),
+	m_pProvider(pProvider)
+{
+}
+
+CDecal::~CDecal()
+{
+	removeOverlays();
+	mem_release(m_pMaterial);
+}
+
+bool XMETHODCALLTYPE CDecal::isEnabled() const
+{
+	return(m_isEnabled);
+}
+void XMETHODCALLTYPE CDecal::enable(bool yesNo)
+{
+	m_isEnabled = yesNo;
+	TODO("Change state");
+}
+
+float3 XMETHODCALLTYPE CDecal::getPosition() const
+{
+	return(m_vPos);
+}
+void XMETHODCALLTYPE CDecal::setPosition(const float3 &vPos)
+{
+	m_vPos = vPos;
+	setDirty();
+}
+
+SMQuaternion XMETHODCALLTYPE CDecal::getOrientation() const
+{
+	return(m_qRot);
+}
+void XMETHODCALLTYPE CDecal::setOrientation(const SMQuaternion &qRot)
+{
+	m_qRot = qRot;
+	setDirty();
+}
+
+float3 XMETHODCALLTYPE CDecal::getLocalBoundMin() const
+{
+	float3 vBound;
+	for(UINT i = 0; i < DECAL_POINTS; ++i)
+	{
+		if(i == 0)
+		{
+			vBound = m_qRot * float3(m_avCorners[0], 0.0f);
+		}
+		else
+		{
+			vBound = SMVectorMin(vBound, m_qRot * float3(m_avCorners[0], 0.0f));
+		}
+		vBound = SMVectorMin(vBound, m_qRot * float3(m_avCorners[0], m_fHeight));
+	}
+
+	return(vBound);
+}
+float3 XMETHODCALLTYPE CDecal::getLocalBoundMax() const
+{
+	float3 vBound;
+	for(UINT i = 0; i < DECAL_POINTS; ++i)
+	{
+		if(i == 0)
+		{
+			vBound = m_qRot * float3(m_avCorners[0], 0.0f);
+		}
+		else
+		{
+			vBound = SMVectorMax(vBound, m_qRot * float3(m_avCorners[0], 0.0f));
+		}
+		vBound = SMVectorMax(vBound, m_qRot * float3(m_avCorners[0], m_fHeight));
+	}
+
+	return(vBound);
+}
+SMAABB XMETHODCALLTYPE CDecal::getLocalBound() const
+{
+	return(SMAABB(getLocalBoundMin(), getLocalBoundMax()));
+}
+
+bool XMETHODCALLTYPE CDecal::rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut, float3 *pvNormal)
+{
+	fora(i, m_aOverlays)
+	{
+		const Overlay &overlay = m_aOverlays[i];
+		const Array<XResourceModelStaticVertex> &aVertices = overlay.pOverlay->getVertices();
+
+		float3 vRayStart = vStart;
+		float3 vRayEnd = vEnd;
+
+		if(!overlay.pModel->isStatic())
+		{
+			// transform ray to local space
+			float3 vPos = overlay.pModel->getPosition();
+			SMQuaternion qRot = overlay.pModel->getOrientation();
+
+			vRayStart = qRot.Conjugate() * vRayStart - vPos;
+			vRayEnd = qRot.Conjugate() * vRayEnd - vPos;
+		}
+
+		for(UINT j = 0, jl = aVertices.size(); j < jl; j += 4)
+		{
+			// 012 213
+			if(SMTriangleIntersectLine(aVertices[j].vPos, aVertices[j + 1].vPos, aVertices[j + 2].vPos, vRayStart, vRayEnd, pvOut))
+			{
+				if(pvNormal)
+				{
+					*pvNormal = SMVector3Normalize(SMVector3Cross(aVertices[j + 1].vPos - aVertices[j].vPos, aVertices[j + 2].vPos - aVertices[j].vPos));
+				}
+				return(true);
+			}
+
+			if(!SMIsZero(SMVector3Length2(aVertices[j + 3].vPos - aVertices[j + 2].vPos)) && SMTriangleIntersectLine(aVertices[j + 2].vPos, aVertices[j + 1].vPos, aVertices[j + 3].vPos, vRayStart, vRayEnd, pvOut))
+			{
+				if(pvNormal)
+				{
+					*pvNormal = SMVector3Normalize(SMVector3Cross(aVertices[j + 1].vPos - aVertices[j + 2].vPos, aVertices[j + 3].vPos - aVertices[j + 2].vPos));
+				}
+				return(true);
+			}
+		}
+	}
+	return(false);
+}
+
+void XMETHODCALLTYPE CDecal::setLayer(UINT uLayer)
+{
+	m_uLayer = uLayer;
+	setDirty();
+}
+UINT XMETHODCALLTYPE CDecal::getLayer()
+{
+	return(m_uLayer);
+}
+
+void XMETHODCALLTYPE CDecal::setHeight(float fHeight)
+{
+	m_fHeight = fHeight;
+	setDirty();
+}
+float XMETHODCALLTYPE CDecal::getHeight()
+{
+	return(m_fHeight);
+}
+
+void XMETHODCALLTYPE CDecal::setTextureRangeU(const float2_t &vRange)
+{
+	m_vTexRangeU = vRange;
+}
+float2_t XMETHODCALLTYPE CDecal::getTextureRangeU()
+{
+	return(m_vTexRangeU);
+}
+
+void XMETHODCALLTYPE CDecal::setTextureRangeV(const float2_t &vRange)
+{
+	m_vTexRangeV = vRange;
+}
+float2_t XMETHODCALLTYPE CDecal::getTextureRangeV()
+{
+	return(m_vTexRangeV);
+}
+
+void XMETHODCALLTYPE CDecal::setMaterial(const char *szMaterial)
+{
+	mem_release(m_pMaterial);
+	m_pProvider->getMaterialSystem()->loadMaterial(szMaterial, &m_pMaterial);
+
+	setDirty();
+}
+
+void CDecal::setMaterial(IXMaterial *pMaterial)
+{
+	mem_release(m_pMaterial);
+	m_pMaterial = pMaterial;
+	add_ref(m_pMaterial);
+
+	setDirty();
+}
+void XMETHODCALLTYPE CDecal::setCorner(UINT uCorner, const float2_t &vCorner)
+{
+	assert(uCorner < DECAL_POINTS);
+	if(uCorner < DECAL_POINTS)
+	{
+		m_avCorners[uCorner] = vCorner;
+		setDirty();
+	}
+}
+
+void CDecal::update(
+#ifdef DECAL_DEBUG_DRAW
+	IXGizmoRenderer *pDebugRenderer
+#endif
+)
+{
+	if(m_isDirty)
+	{
+		// cleanup old overlays
+		removeOverlays();
+
+		// query for objects
+		
+		SMPLANE aPlanes[] = {
+			SMPlaneNormalize(SMPLANE(float3(m_avCorners[0], 0.0f), float3(m_avCorners[1], 0.0f), float3(m_avCorners[1], m_fHeight))),
+			SMPlaneNormalize(SMPLANE(float3(m_avCorners[2], 0.0f), float3(m_avCorners[3], 0.0f), float3(m_avCorners[3], m_fHeight))),
+
+			SMPlaneNormalize(SMPLANE(float3(m_avCorners[1], 0.0f), float3(m_avCorners[2], 0.0f), float3(m_avCorners[2], m_fHeight))),
+			SMPlaneNormalize(SMPLANE(float3(m_avCorners[3], 0.0f), float3(m_avCorners[0], 0.0f), float3(m_avCorners[0], m_fHeight))),
+
+			SMPLANE(0.0f, 0.0f, 1.0f, m_fHeight),
+			SMPLANE(0.0f, 0.0f, -1.0f, m_fHeight),
+		};
+				
+		//SMMATRIX mWorld = SMMatrixTranspose(m_qRot.GetMatrix() * SMMatrixTranslation(m_vPos));
+		//SMMATRIX mWorld = m_qRot.GetMatrix();
+		for(UINT i = 0; i < ARRAYSIZE(aPlanes); ++i)
+		{
+			//aPlanes[i] = SMPlaneTransformTI(aPlanes[i], mWorld);
+			aPlanes[i] = SMPLANE(m_qRot * float3(aPlanes[i]), aPlanes[i].w);
+			aPlanes[i].w -= SMVector3Dot(aPlanes[i], m_vPos);
+		}
+
+		IXFrustum *pFrustum;
+		m_pRender->newFrustum(&pFrustum);
+		
+		pFrustum->update(aPlanes, false);
+
+#ifdef DECAL_DEBUG_DRAW
+		pDebugRenderer->setColor(float4(1.0f, 1.0f, 0.0f, 1.0f));
+		for(UINT i = 0; i < 8; ++i)
+		{
+			pDebugRenderer->drawPoint(pFrustum->getPoint(i));
+		}
+		pDebugRenderer->setColor(float4(1.0f, 1.0f, 0.0f, 0.2f));
+#endif
+
+		CDynamicModel **ppObjects = NULL;
+		m_pSceneQuery->setLayerMask(1 << m_uLayer);
+		UINT uCount = m_pSceneQuery->execute(pFrustum, (void***)&ppObjects);
+
+		SMPLANE aPlanesObjectSpace[ARRAYSIZE(aPlanes)];
+
+		// build overlays
+		for(UINT i = 0; i < uCount; ++i)
+		{
+			TODO("Check ppObjects[i]->receivingDecals()");
+
+			for(UINT j = 0; j < ARRAYSIZE(aPlanes); ++j)
+			{
+				aPlanesObjectSpace[j] = SMPLANE(ppObjects[i]->getOrientation().Conjugate() * float3(aPlanes[j]), aPlanes[j].w + SMVector3Dot(aPlanes[j], ppObjects[i]->getPosition()));
+			}
+			pFrustum->update(aPlanesObjectSpace, false);
+
+			spawnOverlayForModel(ppObjects[i], pFrustum
+#ifdef DECAL_DEBUG_DRAW
+				, pDebugRenderer
+#endif
+			);
+		}
+
+		mem_release(pFrustum);
+
+		m_isDirty = false;
+	}
+}
+
+void CDecal::onOverlayRemoved(CModelOverlay *pOverlay)
+{
+	int idx = m_aOverlays.indexOf(pOverlay, [](const Overlay &a, CModelOverlay *pB){
+		return(a.pOverlay == pB);
+	});
+	assert(idx >= 0);
+	if(idx >= 0)
+	{
+		m_aOverlays.erase(idx);
+		m_pProvider->freeOverlay(pOverlay);
+		if(!m_aOverlays.size())
+		{
+			m_pProvider->onDecalEmptied(this);
+		}
+	}
+}
+
+template<typename T>
+static T ArrGet(T *pArr, UINT uSize, UINT uIdx, int iStartOffset = 0)
+{
+	return(pArr[((int)uIdx + iStartOffset) % uSize]);
+}
+
+template<typename T>
+static void ArrDel(T *pArr, UINT &uSize, UINT uIdx, int &iStartOffset)
+{
+	UINT uStartIndex = ((int)uIdx + iStartOffset) % uSize;
+	--uSize;
+	if(uStartIndex != uSize)
+	{
+		for(UINT i = uStartIndex; i < uSize; ++i)
+		{
+			pArr[i] = pArr[i + 1];
+		}
+	}
+	if(uStartIndex < iStartOffset)
+	{
+		--iStartOffset;
+	}
+}
+
+template<typename T>
+static void ArrIns(const T &val, T *pArr, UINT &uSize, UINT uIdx, int &iStartOffset)
+{
+	UINT uStartIndex = ((int)uIdx + iStartOffset) % uSize;
+	if(uStartIndex == 0)
+	{
+		uStartIndex = uSize;
+	}
+	else
+	{
+		for(UINT i = uSize; i > uStartIndex; --i)
+		{
+			pArr[i] = pArr[i - 1];
+		}
+	}
+	assert(uStartIndex < 10);
+	pArr[uStartIndex] = val;
+	++uSize;
+
+	if(uStartIndex < iStartOffset)
+	{
+		++iStartOffset;
+	}
+}
+
+template<typename T>
+static void ArrSet(const T &val, T *pArr, UINT &uSize, UINT uIdx, int iStartOffset = 0)
+{
+	pArr[((int)uIdx + iStartOffset) % uSize] = val;
+}
+
+static bool IsInside(const SMPLANE &plane, const float3_t &vPos)
+{
+	return(SMVector4Dot(plane, float4(vPos, 1.0f)) > 0.0f);
+}
+
+void XMETHODCALLTYPE CDecal::FinalRelease()
+{
+	m_pProvider->onDecalReleased(this);
+}
+
+void CDecal::spawnOverlayForModel(CDynamicModel *pModel, IXFrustum *pFrustum
+#ifdef DECAL_DEBUG_DRAW
+	, IXGizmoRenderer *pDebugRenderer
+#endif
+)
+{
+	const IXResourceModelStatic *pResource = pModel->getResource()->asStatic();
+	if(!pResource)
+	{
+		return;
+	}
+
+	if(pResource->getPrimitiveTopology() != XPT_TRIANGLELIST)
+	{
+		LogError("CDecal::spawnOverlayForModel(): Unsupported primitive topology");
+	}
+
+	SMQuaternion qDecalToModel = m_qRot * pModel->getOrientation().Conjugate();
+	float3 vS = qDecalToModel * float3(1.0f, 0.0f, 0.0f);
+	float3 vT = qDecalToModel * float3(0.0f, 1.0f, 0.0f);
+	float3 vN = qDecalToModel * float3(0.0f, 0.0f, 1.0f);
+
+	float2_t sBound;
+	float2_t tBound;
+	// center point
+	float3 vModelSpaceDecalCenter = pModel->getOrientation().Conjugate() * (m_vPos - pModel->getPosition());
+	
+	for(UINT j = 0; j < DECAL_POINTS; ++j)
+	{
+		float s = m_avCorners[j].x;
+		float t = m_avCorners[j].y;
+		if(j == 0)
+		{
+			sBound.x = s;
+			sBound.y = s;
+			tBound.x = t;
+			tBound.y = t;
+		}
+		else
+		{
+			if(sBound.x > s)
+			{
+				sBound.x = s;
+			}
+			if(sBound.y < s)
+			{
+				sBound.y = s;
+			}
+			if(tBound.x > t)
+			{
+				tBound.x = t;
+			}
+			if(tBound.y < t)
+			{
+				tBound.y = t;
+			}
+		}
+	}
+	float2_t vInvBoundRange = float2(1.0f, 1.0f) / float2(sBound.y - sBound.x, tBound.y - tBound.x);
+	XResourceModelStaticVertex vtx;
+	vtx.vTangent = vS;
+	vtx.vBinorm = vT;
+	vtx.vNorm = vN;
+
+	float3_t aTempVertices[10]; // 9 - max vertex count of clipped triangle + 1 temp vertex
+	UINT uVertexCount;
+
+	Array<XResourceModelStaticVertex> aOverlayVertices;
+
+	float fCosThreshold = m_isLeakAllowed ? -0.1f : 0.2f;
+
+	UINT uSubsets = pResource->getSubsetCount(0);
+	for(UINT uSubset = 0; uSubset < uSubsets; ++uSubset)
+	{
+		TODO("Separate transparent subsets");
+		const XResourceModelStaticSubset *pSubset = pResource->getSubset(0, uSubset);
+		for(UINT i = 0; i < pSubset->iIndexCount; i += 3)
+		{
+			float3_t &a = pSubset->pVertices[pSubset->pIndices[i]].vPos;
+			float3_t &b = pSubset->pVertices[pSubset->pIndices[i + 1]].vPos;
+			float3_t &c = pSubset->pVertices[pSubset->pIndices[i + 2]].vPos;
+
+			float3 vTriN = SMVector3Normalize(SMVector3Cross(b - a, c - a));
+			float fCos = SMVector3Dot(vN, vTriN);
+
+			if(fCos > fCosThreshold && pFrustum->polyInFrustum(a, b, c))
+			{
+				aTempVertices[0] = a;
+				aTempVertices[1] = b;
+				aTempVertices[2] = c;
+				uVertexCount = 3;
+
+				//pDebugRenderer->drawPoly(a, b, c);
+
+				// clip by frustum planes
+				for(UINT j = 0, jl = pFrustum->getPlaneCount(); j < jl && uVertexCount >= 3/* && j < 4*/; ++j)
+				{
+					const SMPLANE &plane = pFrustum->getPlaneAt(j);
+					int iInsideVertex = -1;
+					// - find inside vertex
+					for(UINT k = 0; k < uVertexCount; ++k)
+					{
+						if(IsInside(plane, aTempVertices[k]))
+						{
+							iInsideVertex = (int)k;
+							break;
+						}
+					}
+
+					if(iInsideVertex < 0)
+					{
+						// all vertices outside clipping plane
+						uVertexCount = 0;
+						break;
+					}
+					// - rotate array to make inside vertex first
+					// - clip
+
+					for(UINT k = 0; k < uVertexCount; ++k)
+					{
+						float3_t vCur = ArrGet(aTempVertices, uVertexCount, k, iInsideVertex);
+						float3_t vNext = ArrGet(aTempVertices, uVertexCount, k + 1, iInsideVertex);
+						bool bCurInside = IsInside(plane, vCur);
+						bool bNextInside = IsInside(plane, vNext);
+						if(bCurInside)
+						{
+							if(bNextInside)
+							{
+								continue;
+							}
+							else
+							{
+								// clip
+								float3 vPt;
+								bool b = plane.intersectLine(&vPt, vCur, vNext);
+								assert(b);
+								// insert after cur
+								ArrIns((float3_t)vPt, aTempVertices, uVertexCount, k + 1, iInsideVertex);
+								++k;
+							}
+						}
+						else
+						{
+							if(bNextInside)
+							{
+								// clip
+								float3 vPt;
+								bool b = plane.intersectLine(&vPt, vCur, vNext);
+								assert(b);
+								// replace cur
+								ArrSet((float3_t)vPt, aTempVertices, uVertexCount, k, iInsideVertex);
+							}
+							else
+							{
+								// drop cur
+								ArrDel(aTempVertices, uVertexCount, k, iInsideVertex);
+								--k;
+							}
+						}
+					}
+				}
+
+				// put into overlay
+				if(uVertexCount >= 3)
+				{
+					if(pModel->isStatic())
+					{
+						// transform to world pos
+						float3 vPos = pModel->getPosition();
+						SMQuaternion qRot = pModel->getOrientation();
+						for(UINT j = 0; j < uVertexCount; ++j)
+						{
+							aTempVertices[j] = qRot * aTempVertices[j] + vPos;
+						}
+					}
+
+					UINT uStartVtx = aOverlayVertices.size();
+					for(UINT begin = 0, end = uVertexCount - 1; begin + 1 <= end - 1; ++begin, --end)
+					{
+						vtx.vPos = aTempVertices[end];
+						aOverlayVertices.push_back(vtx);
+
+						vtx.vPos = aTempVertices[begin];
+						aOverlayVertices.push_back(vtx);
+
+						vtx.vPos = aTempVertices[end - 1];
+						aOverlayVertices.push_back(vtx);
+
+						vtx.vPos = aTempVertices[begin + 1];
+						aOverlayVertices.push_back(vtx);
+					}
+
+					float3 vTriS = vS;
+					float3 vTriT = vT;
+
+					if(fCos < 0.2f)
+					{
+						float3 vRotAxis = SMVector3Cross(vTriN, vN);
+
+						float fTempS = SMVector3Dot(vRotAxis, vS);
+						float fTempT = SMVector3Dot(vRotAxis, vT);
+						
+						float fSign = max(fTempS, fTempT);
+						float fAngle = safe_acosf(fCos);
+						if(fSign < 0.0f)
+						{
+							fAngle = SM_2PI - fAngle;
+						}
+
+						SMQuaternion q(vRotAxis, fAngle);
+						vTriS = q * vS;
+						vTriT = q * vT;
+					}
+
+					for(UINT j = uStartVtx, jl = aOverlayVertices.size(); j < jl; ++j)
+					{
+						XResourceModelStaticVertex &vt = aOverlayVertices[j];
+
+						float3 vPos = vt.vPos - vModelSpaceDecalCenter;
+						float s = SMVector3Dot(vPos, vTriS);
+						float t = SMVector3Dot(vPos, vTriT);
+						vt.vPos = vt.vPos + vTriN * 0.0001f;
+
+						vt.vTex.x = lerpf(m_vTexRangeU.x, m_vTexRangeU.y, clampf((s - sBound.x) * vInvBoundRange.x, 0.0f, 1.0f));
+						vt.vTex.y = lerpf(m_vTexRangeV.x, m_vTexRangeV.y, clampf((t - tBound.x) * vInvBoundRange.y, 0.0f, 1.0f));
+					}
+				}
+			}
+		}
+	}
+
+	// aOverlayVertices
+	CModelOverlay *pOverlay = m_pProvider->allocOverlay(this, m_pMaterial, aOverlayVertices, vN);
+	m_aOverlays.push_back({pModel, pOverlay});
+
+	CModelOverlay *pLastOverlay = pModel->getOverlay();
+	if(!pLastOverlay)
+	{
+		pModel->setOverlay(pOverlay);
+	}
+	else
+	{
+		while(pLastOverlay->getNextOverlay())
+		{
+			pLastOverlay = pLastOverlay->getNextOverlay();
+		}
+		pLastOverlay->setNextOverlay(pOverlay);
+	}
+}
+
+void CDecal::removeOverlays()
+{
+	fora(i, m_aOverlays)
+	{
+		Overlay &ovl = m_aOverlays[i];
+
+		CModelOverlay *pPrev = NULL;
+		CModelOverlay *pCur = ovl.pModel->getOverlay();
+		while(pCur)
+		{
+			if(pCur == ovl.pOverlay)
+			{
+				if(pPrev)
+				{
+					pPrev->setNextOverlay(pCur->getNextOverlay());
+				}
+				else
+				{
+					ovl.pModel->setOverlay(pCur->getNextOverlay());
+				}
+
+				break;
+			}
+
+			pPrev = pCur;
+			pCur = pCur->getNextOverlay();
+		}
+
+		m_pProvider->freeOverlay(ovl.pOverlay);
+	}
+
+	m_aOverlays.clearFast();
+}
+
+void CDecal::setDirty()
+{
+	if(!m_isDirty)
+	{
+		m_isDirty = true;
+		m_pProvider->updateDecal(this);
+	}
+}
+
+void XMETHODCALLTYPE CDecal::setLeakAllowed(bool yesNo)
+{
+	if(!!yesNo != !!m_isLeakAllowed)
+	{
+		m_isLeakAllowed = yesNo;
+		setDirty();
+	}
+}
diff --git a/source/anim/Decal.h b/source/anim/Decal.h
new file mode 100644
index 0000000000000000000000000000000000000000..da5016ff7958b6f6d8c081341235125a3d3e55eb
--- /dev/null
+++ b/source/anim/Decal.h
@@ -0,0 +1,110 @@
+#ifndef __DECAL_H
+#define __DECAL_H
+
+#include <xcommon/resource/IXDecal.h>
+#include <xcommon/IXScene.h>
+#include <xcommon/render/IXRender.h>
+#include "ModelOverlay.h"
+
+#define DECAL_POINTS 4
+
+// #define DECAL_DEBUG_DRAW
+
+class CDynamicModel;
+class CDecalProvider;
+
+class CDecal final: public IXUnknownImplementation<IXDecal>
+{
+public:
+	CDecal(IXSceneQuery *pSceneQuery, IXRender *pRender, CDecalProvider *pProvider);
+	~CDecal();
+
+	bool XMETHODCALLTYPE isEnabled() const override;
+	void XMETHODCALLTYPE enable(bool yesNo) override;
+
+	float3 XMETHODCALLTYPE getPosition() const override;
+	void XMETHODCALLTYPE setPosition(const float3 &vPos) override;
+
+	SMQuaternion XMETHODCALLTYPE getOrientation() const override;
+	void XMETHODCALLTYPE setOrientation(const SMQuaternion &qRot) override;
+
+	float3 XMETHODCALLTYPE getLocalBoundMin() const override;
+	float3 XMETHODCALLTYPE getLocalBoundMax() const override;
+	SMAABB XMETHODCALLTYPE getLocalBound() const override;
+
+	bool XMETHODCALLTYPE rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut = NULL, float3 *pvNormal = NULL) override;
+
+	void XMETHODCALLTYPE setLayer(UINT uLayer) override;
+	UINT XMETHODCALLTYPE getLayer() override;
+
+	void XMETHODCALLTYPE setHeight(float fHeight) override;
+	float XMETHODCALLTYPE getHeight() override;
+
+	void XMETHODCALLTYPE setTextureRangeU(const float2_t &vRange) override;
+	float2_t XMETHODCALLTYPE getTextureRangeU() override;
+
+	void XMETHODCALLTYPE setTextureRangeV(const float2_t &vRange) override;
+	float2_t XMETHODCALLTYPE getTextureRangeV() override;
+
+	void XMETHODCALLTYPE setMaterial(const char *szMaterial) override;
+
+	void setMaterial(IXMaterial *pMaterial);
+	void XMETHODCALLTYPE setCorner(UINT uCorner, const float2_t &vCorner) override;
+
+	void XMETHODCALLTYPE setLeakAllowed(bool yesNo) override;
+
+	void update(
+#ifdef DECAL_DEBUG_DRAW
+		IXGizmoRenderer *pDebugRenderer
+#endif
+	);
+
+	// Must be called only for overlays removed with model
+	void onOverlayRemoved(CModelOverlay *pOverlay);
+
+private:
+	IXSceneQuery *m_pSceneQuery = NULL;
+	IXRender *m_pRender = NULL;
+	CDecalProvider *m_pProvider = NULL;
+
+	IXMaterial *m_pMaterial = NULL;
+	
+	float2_t m_avCorners[4];
+
+	SMQuaternion m_qRot;
+	float3_t m_vPos;
+	
+	float m_fHeight = 0.1f;
+
+	UINT m_uLayer = 0;
+
+	float2_t m_vTexRangeU = float2_t(0.0f, 1.0f);
+	float2_t m_vTexRangeV = float2_t(0.0f, 1.0f);
+
+	struct Overlay
+	{
+		CDynamicModel *pModel;
+		CModelOverlay *pOverlay;
+	};
+	Array<Overlay> m_aOverlays;
+
+	bool m_isEnabled = true;
+
+	bool m_isDirty = false;
+
+	bool m_isLeakAllowed = true;
+
+private:
+	void XMETHODCALLTYPE FinalRelease() override;
+	void spawnOverlayForModel(CDynamicModel *pModel, IXFrustum *pFrustum
+#ifdef DECAL_DEBUG_DRAW
+		, IXGizmoRenderer *pDebugRenderer
+#endif
+	);
+
+	void removeOverlays();
+
+	void setDirty();
+};
+
+#endif
diff --git a/source/anim/DecalProvider.cpp b/source/anim/DecalProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1aaec25f9fe51e9aaccd002b98c58724e9f0e827
--- /dev/null
+++ b/source/anim/DecalProvider.cpp
@@ -0,0 +1,619 @@
+#include "DecalProvider.h"
+//#include <xcommon/IPluginManager.h>
+//#include <core/sxcore.h>
+#include "ModelOverlay.h"
+#include "DynamicModel.h"
+#include "DynamicModelProvider.h"
+#include <xcommon/IPluginManager.h>
+#include "Decal.h"
+
+CDecalProvider::CDecalProvider(IXCore *pCore, CDynamicModelProvider *pProviderDynamic):
+	m_pCore(pCore),
+	m_pProviderDynamic(pProviderDynamic)
+{
+	m_pMaterialSystem = (IXMaterialSystem*)pCore->getPluginManager()->getInterface(IXMATERIALSYSTEM_GUID);
+	m_pRenderUtils = (IXRenderUtils*)pCore->getPluginManager()->getInterface(IXRENDERUTILS_GUID);
+	m_pRender = (IXRender*)pCore->getPluginManager()->getInterface(IXRENDER_GUID);
+
+	IXScene *pScene = (IXScene*)pCore->getPluginManager()->getInterface(IXSCENE_GUID);
+	IXSceneObjectType *pType = pScene->getObjectType("xDynamic");
+	m_pQuery = pType->newQuery();
+	mem_release(pType);
+	
+	pCore->getConsole()->registerCVar("r_maxdecals", 300, "Max temp decals");
+	//pCore->getConsole()->registerCVar("r_max_overlapped_decals", 4, "Max overlapped decals");
+	
+#ifdef DECAL_DEBUG_DRAW
+	m_pRenderUtils->newGizmoRenderer(&m_pDebugRenderer);
+#endif
+
+	m_pMaterialSystem->loadMaterial("dev_null", &m_pNullMaterial);
+
+	loadDecals();
+}
+
+CDecalProvider::~CDecalProvider()
+{
+	mem_release(m_pNullMaterial);
+	fora(i, m_aTempDecals)
+	{
+		mem_release(m_aTempDecals[i]);
+	}
+	for(UINT i = 0; i < XDT__COUNT; ++i)
+	{
+		mem_release(m_aDecalTypes[i].pMaterial);
+	}
+	mem_release(m_pQuery);
+	mem_release(m_pBlendState);
+	mem_release(m_pDSState);
+	mem_release(m_pWorldBuffer);
+#ifdef DECAL_DEBUG_DRAW
+	mem_release(m_pDebugRenderer);
+#endif
+}
+
+void CDecalProvider::render(CRenderableVisibility *pVisibility)
+{
+	XPROFILE_FUNCTION();
+
+	CRenderableVisibility::OverlayData &overlayData = pVisibility->getOverlayData();
+	
+	UINT uVertexCount = overlayData.aVertices.size();
+
+	if(uVertexCount)
+	{
+		if(overlayData.uVertexBufferAllocSize < uVertexCount)
+		{
+			overlayData.uVertexBufferAllocSize = uVertexCount;
+			mem_release(overlayData.pRB);
+			mem_release(overlayData.pVB);
+			mem_release(overlayData.pIB);
+
+			overlayData.pVB = m_pDevice->createVertexBuffer(sizeof(XResourceModelStaticVertexGPU) * uVertexCount, GXBUFFER_USAGE_STREAM);
+			overlayData.pRB = m_pDevice->createRenderBuffer(1, &overlayData.pVB, m_pProviderDynamic->getVertexDeclaration());
+			m_pRenderUtils->getQuadIndexBuffer(uVertexCount / 4, &overlayData.pIB);
+		}
+
+		XResourceModelStaticVertexGPU *pVertices;
+		if(overlayData.pVB->lock((void**)&pVertices, GXBL_WRITE))
+		{
+			memcpy(pVertices, overlayData.aVertices, sizeof(XResourceModelStaticVertexGPU) * uVertexCount);
+			overlayData.pVB->unlock();
+		}
+
+		m_pProviderDynamic->bindVertexFormat();
+
+		IGXContext *pCtx = m_pDevice->getThreadContext();
+		pCtx->setRenderBuffer(overlayData.pRB);
+		pCtx->setIndexBuffer(overlayData.pIB);
+		pCtx->setPrimitiveTopology(GXPT_TRIANGLELIST);
+		IGXBlendState *pOldBlendState = pCtx->getBlendState();
+		pCtx->setBlendState(m_pBlendState);
+		m_pDevice->getThreadContext()->setVSConstant(m_pWorldBuffer, 1 /* SCR_OBJECT */);
+		IGXDepthStencilState *pOldDSState = pCtx->getDepthStencilState();
+		pCtx->setDepthStencilState(m_pDSState);
+
+		fora(i, overlayData.aSubsets)
+		{
+			auto &ss = overlayData.aSubsets[i];
+			m_pMaterialSystem->bindMaterial(ss.pMaterial);
+			pCtx->drawIndexed(ss.uQuadCount * 4, ss.uQuadCount * 2, ss.uStartIndex);
+		}
+
+		pCtx->setBlendState(pOldBlendState);
+		mem_release(pOldBlendState);
+
+		pCtx->setDepthStencilState(pOldDSState);
+		mem_release(pOldDSState);
+	}
+
+#ifdef DECAL_DEBUG_DRAW
+	m_pDebugRenderer->render(false, false);
+#endif
+}
+
+bool XMETHODCALLTYPE CDecalProvider::newDecal(IXDecal **ppDecal)
+{
+	CDecal *pDecal = allocDecal();
+	pDecal->setMaterial(m_pNullMaterial);
+
+	*ppDecal = pDecal;
+
+	return(true);
+}
+
+void XMETHODCALLTYPE CDecalProvider::shootDecal(XDECAL_TYPE type, const float3 &vWorldPos, const float3 &vNormal, float fScale, const float3 *pvSAxis)
+{
+	//type = XDT_GLASS;
+	const DecalType *pType = getDecalType(type);
+	if(!pType || !pType->pMaterial)
+	{
+		return;
+	}
+
+	fScale *= pType->fBaseScale * 0.0008f;
+
+	IXTexture *pTex = pType->pMaterial->getTexture("txBase");
+	if(!pTex)
+	{
+		IKeyIterator *pIter = pType->pMaterial->getTexturesIterator();
+		if(pIter)
+		{
+			pTex = pType->pMaterial->getTexture(pIter->getCurrent());
+		}
+		mem_release(pIter);
+	}
+
+	UINT uTexWidth = pTex->getWidth();
+	UINT uTexHeight = pTex->getHeight();
+
+	DecalTexRange texRange = {0, 0, uTexWidth, uTexHeight};
+	if(pType->aTextures.size())
+	{
+		texRange = pType->aTextures[rand() % pType->aTextures.size()];
+	}
+	
+	float2_t vSize((float)(texRange.xmax - texRange.xmin) * fScale, (float)(texRange.ymax - texRange.ymin) * fScale);
+
+	float2_t sBound(-vSize.x * 0.5f, vSize.x * 0.5f);
+	float2_t tBound(-vSize.y * 0.5f, vSize.y * 0.5f);
+
+	//compute basis
+	SMMATRIX mBasis;
+	computeBasis(&mBasis, SMVector3Normalize(vNormal), pvSAxis);
+
+	SMQuaternion qOrient = SMQuaternion(mBasis.r[0], mBasis.r[1], mBasis.r[2]).Normalize();
+
+	//vWorldPos;
+	//qOrient;
+	//pType->pMaterial;
+	float2_t avCorners[] = {
+		float2_t(sBound.x, tBound.x),
+		float2_t(sBound.x, tBound.y),
+		float2_t(sBound.y, tBound.y),
+		float2_t(sBound.y, tBound.x),
+	};
+	float fHeight = min(vSize.x, vSize.y) * 0.5f + 0.1f;
+
+#ifdef DECAL_DEBUG_DRAW
+	m_pDebugRenderer->setPointSize(0.01f);
+	m_pDebugRenderer->setLineWidth(0.01f);
+	m_pDebugRenderer->setColor(float4(1.0f, 0.0f, 0.0f, 1.0f));
+	m_pDebugRenderer->jumpTo(vWorldPos);
+	m_pDebugRenderer->lineTo(vWorldPos + mBasis.r[0]);
+	m_pDebugRenderer->setColor(float4(0.0f, 1.0f, 0.0f, 1.0f));
+	m_pDebugRenderer->jumpTo(vWorldPos);
+	m_pDebugRenderer->lineTo(vWorldPos + mBasis.r[1]);
+	m_pDebugRenderer->setColor(float4(0.0f, 0.0f, 1.0f, 1.0f));
+	m_pDebugRenderer->jumpTo(vWorldPos);
+	m_pDebugRenderer->lineTo(vWorldPos + mBasis.r[2]);
+
+	m_pDebugRenderer->setColor(float4(1.0f, 1.0f, 1.0f, 1.0f));
+	/*for(UINT i = 0; i < ARRAYSIZE(avCorners); ++i)
+	{
+		m_pDebugRenderer->drawPoint(qOrient * float3(avCorners[i], -fHeight) + vWorldPos);
+		m_pDebugRenderer->drawPoint(qOrient * float3(avCorners[i], fHeight) + vWorldPos);
+	}*/
+#endif
+
+	// create CDecal
+	CDecal *pDecal = allocDecal();
+	pDecal->setMaterial(pType->pMaterial);
+	for(UINT i = 0; i < DECAL_POINTS; ++i)
+	{
+		pDecal->setCorner(i, avCorners[i]);
+	}
+	pDecal->setPosition(vWorldPos);
+	pDecal->setOrientation(qOrient);
+	pDecal->setHeight(fHeight);
+	pDecal->setTextureRangeU(float2((float)texRange.xmin, (float)texRange.xmax) / (float)uTexWidth);
+	pDecal->setTextureRangeV(float2((float)texRange.ymin, (float)texRange.ymax) / (float)uTexHeight);
+
+	addTempDecal(pDecal);
+}
+
+void CDecalProvider::computeVisibility(const float3 &vHintDir, CRenderableVisibility *pVisibility)
+{
+	Array<CDynamicModel*> &aRenderList = pVisibility->getRenderList();
+
+	struct TmpOverlay
+	{
+		CDynamicModel *pModel;
+		CModelOverlay *pOverlay;
+	};
+
+	Array<TmpOverlay> aOverlays;
+
+	bool useHintDir = !SMIsZero(SMVector3Length2(vHintDir));
+
+	UINT uVertices = 0;
+	fora(i, aRenderList)
+	{
+		CDynamicModel *pMdl = aRenderList[i];
+		CModelOverlay *pOverlay = pMdl->getOverlay();
+		while(pOverlay)
+		{
+			// skip backface overlays
+			bool bRender = !useHintDir || pOverlay->getMaterial()->isTwoSided();
+			if(!bRender)
+			{
+				float3 vNormal = pOverlay->getNormal();
+				if(!pMdl->isStatic())
+				{
+					vNormal = pMdl->getOrientation() * vNormal;
+				}
+				bRender = SMVector3Dot(vNormal, vHintDir) < 0.0f;
+			}
+			if(bRender)
+			{
+				aOverlays.push_back({pMdl, pOverlay});
+
+				uVertices += pOverlay->getVertices().size();
+			}
+
+			pOverlay = pOverlay->getNextOverlay();
+		}
+	}
+
+	CRenderableVisibility::OverlayData &overlayData = pVisibility->getOverlayData();
+
+	Array<XResourceModelStaticVertexGPU> &aVertices = overlayData.aVertices;
+	aVertices.clearFast();
+	aVertices.reserve(uVertices);
+	Array<CRenderableVisibility::OverlaySubset>& aSubsets = overlayData.aSubsets;
+	aSubsets.clearFast();
+	
+	aOverlays.quickSort([](const TmpOverlay &a, const TmpOverlay &b){
+		return(a.pOverlay->getMaterial() < b.pOverlay->getMaterial());
+	});
+
+	XResourceModelStaticVertex tempVertex;
+	XResourceModelStaticVertexGPU tempGpuVertex;
+	CRenderableVisibility::OverlaySubset curSubset = {};
+	fora(i, aOverlays)
+	{
+		TmpOverlay &overlay = aOverlays[i];
+
+		if(curSubset.pMaterial != overlay.pOverlay->getMaterial())
+		{
+			if(curSubset.uStartVertex != aVertices.size())
+			{
+				curSubset.uQuadCount = (aVertices.size() - curSubset.uStartVertex) / 4;
+				aSubsets.push_back(curSubset);
+			}
+			curSubset.pMaterial = overlay.pOverlay->getMaterial();
+
+			curSubset.uStartVertex = aVertices.size();
+			curSubset.uStartIndex = aVertices.size() / 4 * 6;
+		}
+
+		const Array<XResourceModelStaticVertex>& aOverlayVertices = overlay.pOverlay->getVertices();
+		bool bNeedTransform = !overlay.pModel->isStatic();
+
+		float3 vPos = overlay.pModel->getPosition();
+		SMQuaternion qRot = overlay.pModel->getOrientation();
+
+		fora(j, aOverlayVertices)
+		{
+			tempVertex = aOverlayVertices[j];
+			if(bNeedTransform)
+			{
+				// transform vertices
+				tempVertex.vPos = qRot * tempVertex.vPos + vPos;
+				tempVertex.vNorm = qRot * tempVertex.vNorm;
+				tempVertex.vTangent = qRot * tempVertex.vTangent;
+				tempVertex.vBinorm = qRot * tempVertex.vBinorm;
+			}
+			
+#define TO_SHORT(v) ((short)((v) * 32767.0f))
+			auto &dst = tempGpuVertex;
+			auto &src = tempVertex;
+			dst.vPos = src.vPos;
+			dst.vTex = src.vTex;
+			dst.vNorm[0] = TO_SHORT(src.vNorm.x);
+			dst.vNorm[1] = TO_SHORT(src.vNorm.y);
+			dst.vNorm[2] = TO_SHORT(src.vNorm.z);
+			dst.vTangent[0] = TO_SHORT(src.vTangent.x);
+			dst.vTangent[1] = TO_SHORT(src.vTangent.y);
+			dst.vTangent[2] = TO_SHORT(src.vTangent.z);
+			dst.vBinorm[0] = TO_SHORT(src.vBinorm.x);
+			dst.vBinorm[1] = TO_SHORT(src.vBinorm.y);
+			dst.vBinorm[2] = TO_SHORT(src.vBinorm.z);
+#undef TO_SHORT
+
+			aVertices.push_back(tempGpuVertex);
+		}	
+	}
+
+	if(curSubset.uStartVertex != aVertices.size())
+	{
+		curSubset.uQuadCount = (aVertices.size() - curSubset.uStartVertex) / 4;
+		aSubsets.push_back(curSubset);
+	}
+}
+
+void CDecalProvider::setDevice(IGXDevice *pDevice)
+{
+	m_pDevice = pDevice;
+
+	GXBlendDesc blendDesc;
+	memset(&blendDesc, 0, sizeof(blendDesc));
+	blendDesc.renderTarget[0].u8RenderTargetWriteMask = GXCOLOR_WRITE_ENABLE_RED | GXCOLOR_WRITE_ENABLE_GREEN | GXCOLOR_WRITE_ENABLE_BLUE;
+	blendDesc.renderTarget[0].blendSrcColor = GXBLEND_DEST_COLOR;
+	blendDesc.renderTarget[0].blendDestColor = GXBLEND_SRC_COLOR;
+	blendDesc.renderTarget[0].blendSrcAlpha = GXBLEND_SRC_ALPHA;
+	blendDesc.renderTarget[0].blendDestAlpha = GXBLEND_INV_SRC_ALPHA;
+	blendDesc.renderTarget[0].blendOpColor = GXBLEND_OP_ADD;
+	blendDesc.renderTarget[0].blendOpAlpha = GXBLEND_OP_ADD;
+
+	/*blendDesc.renderTarget[0].blendSrcColor = GXBLEND_DEST_COLOR;
+	blendDesc.renderTarget[0].blendDestColor = GXBLEND_SRC_COLOR;
+	blendDesc.renderTarget[0].blendSrcAlpha = GXBLEND_DEST_ALPHA;
+	blendDesc.renderTarget[0].blendDestAlpha = GXBLEND_SRC_ALPHA;
+	blendDesc.renderTarget[0].blendOpColor = GXBLEND_OP_ADD;
+	blendDesc.renderTarget[0].blendOpAlpha = GXBLEND_OP_ADD;*/
+
+	blendDesc.renderTarget[0].useBlend = TRUE;
+
+	blendDesc.renderTarget[1].blendSrcColor = GXBLEND_ZERO;
+	blendDesc.renderTarget[1].blendDestColor = GXBLEND_ONE;
+
+	blendDesc.renderTarget[2].blendSrcColor = GXBLEND_ONE;
+	blendDesc.renderTarget[2].blendDestColor = GXBLEND_ZERO;
+
+	blendDesc.renderTarget[3].blendSrcColor = GXBLEND_ONE;
+	blendDesc.renderTarget[3].blendDestColor = GXBLEND_ZERO;
+
+	blendDesc.renderTarget[1].useBlend = FALSE;
+	blendDesc.renderTarget[2].useBlend = FALSE;
+	blendDesc.renderTarget[3].useBlend = FALSE;
+
+	blendDesc.useIndependentBlend = TRUE;
+
+	m_pBlendState = pDevice->createBlendState(&blendDesc);
+
+
+	GXDepthStencilDesc dsDesc = {};
+	dsDesc.cmpFuncDepth = GXCMP_GREATER_EQUAL;
+	dsDesc.useDepthWrite = FALSE;
+	m_pDSState = pDevice->createDepthStencilState(&dsDesc);
+
+
+	m_pWorldBuffer = pDevice->createConstantBuffer(sizeof(SMMATRIX));
+	m_pWorldBuffer->update(&SMMatrixIdentity());
+}
+
+void CDecalProvider::onDecalReleased(CDecal *pDecal)
+{
+	ScopedSpinLock lock(m_slMemDecals);
+	m_memDecals.Delete(pDecal);
+}
+
+void CDecalProvider::onDecalEmptied(CDecal *pDecal)
+{
+	int idx = -1;
+	{
+		ScopedSpinLock lock(m_slTempDecals);
+
+		idx = m_aTempDecals.indexOf(pDecal);
+		if(idx >= 0)
+		{
+			m_aTempDecals.erase(idx);
+		}
+	}
+
+	if(idx >= 0)
+	{
+		mem_release(pDecal);
+	}
+}
+
+void CDecalProvider::updateDecal(CDecal *pDecal)
+{
+	add_ref(pDecal);
+	m_qDecalsForUpdate.push(pDecal);
+}
+
+void CDecalProvider::update()
+{
+	if(!m_isEnabled)
+	{
+		return;
+	}
+
+	CDecal *pDecal;
+	while(m_qDecalsForUpdate.pop(&pDecal))
+	{
+		pDecal->update(
+#ifdef DECAL_DEBUG_DRAW
+			m_pDebugRenderer
+#endif
+		);
+		mem_release(pDecal);
+	}
+}
+
+CDecal* CDecalProvider::allocDecal()
+{
+	ScopedSpinLock lock(m_slMemDecals);
+	return(m_memDecals.Alloc(m_pQuery, m_pRender, this));
+}
+
+CModelOverlay* CDecalProvider::allocOverlay(CDecal *pDecal, IXMaterial *pMaterial, Array<XResourceModelStaticVertex> &aVertices, const float3_t &vNormal)
+{
+	ScopedSpinLock lock(m_slMemOverlays);
+	return(m_memOverlays.Alloc(pDecal, pMaterial, aVertices, vNormal));
+}
+void CDecalProvider::freeOverlay(CModelOverlay *pOverlay)
+{
+	ScopedSpinLock lock(m_slMemOverlays);
+	m_memOverlays.Delete(pOverlay);
+}
+
+IXMaterialSystem* CDecalProvider::getMaterialSystem()
+{
+	return(m_pMaterialSystem);
+}
+
+void CDecalProvider::setEnabled(bool yesNo)
+{
+	m_isEnabled = yesNo;
+}
+
+void CDecalProvider::loadDecals()
+{
+	IXConfig *pConfig = m_pCore->newConfig();
+	if(pConfig->open("config/decals/decals.cfg"))
+	{
+		UINT uSections = pConfig->getSectionCount();
+		for(UINT i = 0; i < uSections; ++i)
+		{
+			const char *szSection = pConfig->getSectionName(i);
+
+			int id;
+			if(pConfig->keyExists(szSection, "id") && sscanf(pConfig->getKey(szSection, "id"), "%d", &id))
+			{
+				if(id < 0 || id >= XDT__COUNT)
+				{
+					LogError("Incorrect decal type id '%s'\n", szSection);
+					continue;
+				}
+			}
+			else
+			{
+				LogError("Unable to read decal id '%s'\n", szSection);
+				continue;
+			}
+
+			const char *szTex = pConfig->getKey(szSection, "tex");
+			if(!szTex)
+			{
+				LogError("Unable to read decal tex '%s'\n", szSection);
+				continue;
+			}
+
+			const char *szScale = pConfig->getKey(szSection, "base_scale");
+			if(szScale)
+			{
+				sscanf(szScale, "%f", &m_aDecalTypes[id].fBaseScale);
+			}
+
+			
+			int j = 0;
+
+			DecalTexRange rng;
+			char key[64];
+			while(sprintf(key, "tex%d", j) && pConfig->keyExists(szSection, key))
+			{
+				if(sscanf(pConfig->getKey(szSection, key), "[%d,%d,%d,%d]", &rng.xmin, &rng.ymin, &rng.xmax, &rng.ymax) != 4)
+				{
+					LogError("Unable to read decal tex coords \"%s\" \"%s\"\n", pConfig->getKey(szSection, key), szSection);
+				}
+				else
+				{
+					m_aDecalTypes[id].aTextures.push_back(rng);
+				}
+				j++;
+			}
+
+			m_pMaterialSystem->loadMaterial(szTex, &m_aDecalTypes[id].pMaterial);
+		}
+	}
+
+	mem_release(pConfig);
+}
+
+const CDecalProvider::DecalType* CDecalProvider::getDecalType(XDECAL_TYPE type)
+{
+	if(type < 0 || type >= XDT__COUNT)
+	{
+		LogError("Incorrect decal type %d\n", type);
+		return(NULL);
+	}
+	return(&m_aDecalTypes[type]);
+}
+
+void CDecalProvider::computeBasis(SMMATRIX *pmOut, const float3_t &vSurfaceNormal, const float3 *pvSAxis)
+{
+	// s, t, textureSpaceNormal (S cross T = textureSpaceNormal(N))
+	//         
+	//      +---->S
+	//     /| 
+	//	  /	|  
+	//   N  |T    
+	//
+	// S = textureSpaceBasis[0]
+	// T = textureSpaceBasis[1]
+	// N = textureSpaceBasis[2]
+
+	// Get the surface normal.
+	pmOut->r[2] = vSurfaceNormal;
+
+	if(pvSAxis)
+	{
+		// T = N cross S
+		pmOut->r[1] = SMVector3Cross(pmOut->r[2], *pvSAxis);
+
+		// Name sure they aren't parallel or antiparallel
+		// In that case, fall back to the normal algorithm.
+		if(SMVector3Dot(pmOut->r[1], pmOut->r[1]) > 1e-6)
+		{
+			// S = T cross N
+			pmOut->r[0] = SMVector3Cross(pmOut->r[1], pmOut->r[2]);
+
+			pmOut->r[0] = SMVector3Normalize(pmOut->r[0]);
+			pmOut->r[1] = SMVector3Normalize(pmOut->r[1]);
+			return;
+		}
+
+		// Fall through to the standard algorithm for parallel or antiparallel
+	}
+
+	// floor/ceiling?
+	if(fabs(vSurfaceNormal.y) > SIN_45_DEGREES)
+	{
+		pmOut->r[0] = float4(1.0f, 0.0f, 0.0f, 0.0f);
+
+		// T = N cross S
+		pmOut->r[1] = SMVector3Cross(pmOut->r[2], pmOut->r[0]);
+
+		// S = T cross N
+		pmOut->r[0] = SMVector3Cross(pmOut->r[1], pmOut->r[2]);
+	}
+	// wall
+	else
+	{
+		pmOut->r[1] = float4(0.0f, -1.0f, 0.0f, 0.0f);
+
+		// S = T cross N
+		pmOut->r[0] = SMVector3Cross(pmOut->r[1], pmOut->r[2]);
+		// T = N cross S
+		pmOut->r[1] = SMVector3Cross(pmOut->r[2], pmOut->r[0]);
+	}
+
+	pmOut->r[0] = SMVector3Normalize(pmOut->r[0]);
+	pmOut->r[1] = SMVector3Normalize(pmOut->r[1]);
+}
+
+void CDecalProvider::addTempDecal(CDecal *pDecal)
+{
+	ScopedSpinLock lock(m_slTempDecals);
+
+	static const int *r_maxdecals = m_pCore->getConsole()->getPCVarInt("r_maxdecals");
+	if(m_aTempDecals.size() > *r_maxdecals)
+	{
+		for(UINT i = (UINT)*r_maxdecals, l = m_aTempDecals.size(); i < l; ++i)
+		{
+			mem_release(m_aTempDecals[i]);
+		}
+	}
+	m_aTempDecals.reserve(*r_maxdecals);
+
+	TODO("Use ring array for that");
+	if(m_aTempDecals.size() == *r_maxdecals)
+	{
+		mem_release(m_aTempDecals[0]);
+		m_aTempDecals.erase(0);
+	}
+	m_aTempDecals.push_back(pDecal);
+}
diff --git a/source/anim/DecalProvider.h b/source/anim/DecalProvider.h
new file mode 100644
index 0000000000000000000000000000000000000000..58580748fa70f2bdb7161c697fee0b133a41d625
--- /dev/null
+++ b/source/anim/DecalProvider.h
@@ -0,0 +1,114 @@
+#ifndef __DECALPROVIDER_H
+#define __DECALPROVIDER_H
+
+#include <xcommon/resource/IXDecalProvider.h>
+#include <xcommon/IXCore.h>
+#include "RenderableVisibility.h"
+//#include <mtrl/IXMaterialSystem.h>
+//#include <common/ConcurrentQueue.h>
+//#include <xcommon/XEvents.h>
+#include <xcommon/IXScene.h>
+#include <common/queue.h>
+#include "Decal.h"
+
+#define SIN_45_DEGREES 0.70710678118654752440084436210485f
+
+class CDecalProvider: public IXUnknownImplementation<IXDecalProvider>
+{
+public:
+	CDecalProvider(IXCore *pCore, CDynamicModelProvider *pProviderDynamic);
+	~CDecalProvider();
+
+	void render(CRenderableVisibility *pVisibility);
+
+	bool XMETHODCALLTYPE newDecal(IXDecal **ppDecal) override;
+
+	void XMETHODCALLTYPE shootDecal(XDECAL_TYPE type, const float3 &vWorldPos, const float3 &vNormal, float fScale = 1.0f, const float3 *pvSAxis = NULL) override;
+
+	void computeVisibility(const float3 &vHintDir, CRenderableVisibility *pVisibility);
+
+	void setDevice(IGXDevice *pDevice);
+
+	void onDecalReleased(CDecal *pDecal);
+
+	void onDecalEmptied(CDecal *pDecal);
+
+	void updateDecal(CDecal *pDecal);
+
+	void update();
+
+	CModelOverlay* allocOverlay(CDecal *pDecal, IXMaterial *pMaterial, Array<XResourceModelStaticVertex> &aVertices, const float3_t &vNormal);
+	void freeOverlay(CModelOverlay *pOverlay);
+
+	IXMaterialSystem* getMaterialSystem();
+
+	void setEnabled(bool yesNo);
+
+private:
+	struct DecalTexRange
+	{
+		int xmin;
+		int ymin;
+		int xmax;
+		int ymax;
+	};
+
+	struct DecalType
+	{
+		Array<DecalTexRange> aTextures;
+		float fBaseScale = 1.0f;
+		IXMaterial *pMaterial = NULL;
+	};
+
+	/*struct TempDecal
+	{
+		CDecal *pDecal;
+	};*/
+
+private:
+	IXCore *m_pCore = NULL;
+	CDynamicModelProvider *m_pProviderDynamic = NULL;
+	IGXDevice *m_pDevice = NULL;
+	IXMaterialSystem *m_pMaterialSystem = NULL;
+	IXRender *m_pRender = NULL;
+	IXRenderUtils *m_pRenderUtils = NULL;
+	IXSceneQuery *m_pQuery = NULL;
+
+	IXMaterial *m_pNullMaterial = NULL;
+
+	IGXConstantBuffer *m_pWorldBuffer = NULL;
+
+	IGXBlendState *m_pBlendState = NULL;
+	IGXDepthStencilState *m_pDSState = NULL;
+
+	DecalType m_aDecalTypes[XDT__COUNT];
+
+#ifdef DECAL_DEBUG_DRAW
+	IXGizmoRenderer *m_pDebugRenderer = NULL;
+#endif
+
+	MemAlloc<CDecal> m_memDecals;
+	MemAlloc<CModelOverlay> m_memOverlays;
+	SpinLock m_slMemDecals;
+	SpinLock m_slMemOverlays;
+
+	Array<CDecal*> m_aTempDecals;
+	SpinLock m_slTempDecals;
+
+	Queue<CDecal*> m_qDecalsForUpdate;
+
+	bool m_isEnabled = true;
+
+private:
+	void loadDecals();
+
+	const DecalType* getDecalType(XDECAL_TYPE type);
+
+	void computeBasis(SMMATRIX *pmOut, const float3_t &vSurfaceNormal, const float3 *pvSAxis = NULL);
+
+	CDecal* allocDecal();
+
+	void addTempDecal(CDecal *pDecal);
+};
+
+#endif
diff --git a/source/anim/DynamicModel.cpp b/source/anim/DynamicModel.cpp
index 0a79dbe82ff68b47fe0a024e1a8761ada11a8a6d..85ad2e562d43aec535b0cc217b183e07db86198f 100644
--- a/source/anim/DynamicModel.cpp
+++ b/source/anim/DynamicModel.cpp
@@ -31,6 +31,8 @@ CDynamicModel::~CDynamicModel()
 	mem_release(m_pWorldBuffer);
 	mem_release(m_pColorBuffer);
 	mem_release(m_pSceneObject);
+
+	SAFE_CALL(m_pOverlay, onModelRemoved);
 }
 
 void CDynamicModel::initGPUresources()
@@ -80,7 +82,7 @@ IXDynamicModel * XMETHODCALLTYPE CDynamicModel::asDynamicModel()
 }
 IXStaticModel * XMETHODCALLTYPE CDynamicModel::asStaticModel()
 {
-	return(NULL);
+	return(isStatic() ? this : NULL);
 }
 
 float3 XMETHODCALLTYPE CDynamicModel::getPosition() const
@@ -209,11 +211,11 @@ void CDynamicModel::_updateAABB() const
 	m_vLocalMin = (float3)SMVectorMin(
 		SMVectorMin(SMVectorMin(vCurrent[0], vCurrent[1]), SMVectorMin(vCurrent[2], vCurrent[3])),
 		SMVectorMin(SMVectorMin(vCurrent[4], vCurrent[5]), SMVectorMin(vCurrent[6], vCurrent[7]))
-		);
+	);
 	m_vLocalMax = (float3)SMVectorMax(
 		SMVectorMax(SMVectorMax(vCurrent[0], vCurrent[1]), SMVectorMax(vCurrent[2], vCurrent[3])),
 		SMVectorMax(SMVectorMax(vCurrent[4], vCurrent[5]), SMVectorMax(vCurrent[6], vCurrent[7]))
-		);
+	);
 
 	m_isLocalAABBvalid = true;
 }
@@ -466,3 +468,23 @@ UINT XMETHODCALLTYPE CDynamicModel::getLayer()
 {
 	return(m_uLayer);
 }
+
+CModelOverlay* CDynamicModel::getOverlay()
+{
+	return(m_pOverlay);
+}
+
+void CDynamicModel::setOverlay(CModelOverlay *pOverlay)
+{
+	m_pOverlay = pOverlay;
+}
+
+void CDynamicModel::setStatic(bool yesNo)
+{
+	m_isStatic = yesNo;
+}
+
+bool CDynamicModel::isStatic()
+{
+	return(m_isStatic);
+}
diff --git a/source/anim/DynamicModel.h b/source/anim/DynamicModel.h
index 836ebdda06dc00c7aafa6efa8f93dd7fe363cd42..97612f2543b3fec1422acfc786f337b60bd9aeea 100644
--- a/source/anim/DynamicModel.h
+++ b/source/anim/DynamicModel.h
@@ -4,6 +4,7 @@
 #include <xcommon/resource/IXModel.h>
 #include <xcommon/IXScene.h>
 #include "DynamicModelShared.h"
+#include "ModelOverlay.h"
 
 class CDynamicModel final: public IXUnknownImplementation<IXDynamicModel>
 {
@@ -58,7 +59,14 @@ public:
 
 	void XMETHODCALLTYPE setLayer(UINT uLayer) override;
 	UINT XMETHODCALLTYPE getLayer() override;
-protected:
+
+	CModelOverlay* getOverlay();
+	void setOverlay(CModelOverlay *pOverlay);
+
+	void setStatic(bool yesNo);
+	bool isStatic();
+
+private:
 	CDynamicModelProvider *m_pProvider;
 	CDynamicModelShared *m_pShared;
 	IGXDevice *m_pDevice;
@@ -75,17 +83,21 @@ protected:
 	UINT m_uSkin = 0;
 	float4_t m_vColor{1.0f, 1.0f, 1.0f, 1.0f};
 	bool m_isEnabled = true;
+	bool m_isStatic = false;
 	float m_fScale = 1.0f;
 
 	mutable bool m_isLocalAABBvalid = false;
 	mutable float3_t m_vLocalMin;
 	mutable float3_t m_vLocalMax;
 
+	UINT m_uLayer = 0;
+
+	CModelOverlay *m_pOverlay = NULL;
+
+private:
 	void _updateAABB() const;
 
 	void XMETHODCALLTYPE FinalRelease() override;
-
-	UINT m_uLayer = 0;
 };
 
 #endif
diff --git a/source/anim/DynamicModelProvider.cpp b/source/anim/DynamicModelProvider.cpp
index 4ce0af11d6c8855f0963e177aa8165be14fbcbe7..36af3b00e4a7f2d4c05454888f9b305f7f49e4cf 100644
--- a/source/anim/DynamicModelProvider.cpp
+++ b/source/anim/DynamicModelProvider.cpp
@@ -541,6 +541,8 @@ void CDynamicModelProvider::computeVisibility(const IXFrustum *pFrustum, const f
 {
 	XPROFILE_FUNCTION();
 
+	TODO("Make some hints to not to compute stages is not required for render. eg selfillum stage for shadows");
+
 	if(pCamera && pCamera->getProjectionMode() == XCPM_PERSPECTIVE)
 	{
 		FIXME("Use actual target width!");
@@ -806,3 +808,8 @@ void CDynamicModelProvider::enqueueModelDelete(CDynamicModel* pModel)
 {
 	m_qModelDelete.push(pModel);
 }
+
+bool CDynamicModelProvider::hasPendingOps()
+{
+	return(!m_queueGPUinitModel.empty() || !m_queueGPUinitShared.empty());
+}
diff --git a/source/anim/DynamicModelProvider.h b/source/anim/DynamicModelProvider.h
index 3b126cae9b99e10c9e4de2f924fea8fa1b8af757..ce66cf3ea02d6d24e9fea5c88a0ffa2df4b4b933 100644
--- a/source/anim/DynamicModelProvider.h
+++ b/source/anim/DynamicModelProvider.h
@@ -70,6 +70,9 @@ public:
 	IXSceneFeature* getFeature(XMODEL_FEATURE bmFeature);
 
 	void enqueueModelDelete(CDynamicModel* pModel);
+
+	bool hasPendingOps();
+
 protected:
 	void onMaterialEmissivityChanged(const IXMaterial *pMaterial);
 	void onMaterialTransparencyChanged(const IXMaterial *pMaterial);
diff --git a/source/anim/ModelOverlay.cpp b/source/anim/ModelOverlay.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ead2048997a2aea32fff6996aa5b413880429bf5
--- /dev/null
+++ b/source/anim/ModelOverlay.cpp
@@ -0,0 +1,49 @@
+#include "ModelOverlay.h"
+#include "Decal.h"
+
+CModelOverlay::CModelOverlay(CDecal *pDecal, IXMaterial *pMaterial, Array<XResourceModelStaticVertex> &aVertices, const float3_t &vNormal):
+	m_pDecal(pDecal),
+	m_pMaterial(pMaterial),
+	m_vNormal(vNormal)
+{
+	m_aVertices.swap(aVertices);
+	add_ref(m_pMaterial);
+}
+
+CModelOverlay::~CModelOverlay()
+{
+	mem_release(m_pMaterial);
+}
+
+
+const Array<XResourceModelStaticVertex>& CModelOverlay::getVertices()
+{
+	return(m_aVertices);
+}
+
+CModelOverlay* CModelOverlay::getNextOverlay()
+{
+	return(m_pNextOverlay);
+}
+void CModelOverlay::setNextOverlay(CModelOverlay *pOverlay)
+{
+	m_pNextOverlay = pOverlay;
+}
+
+IXMaterial* CModelOverlay::getMaterial()
+{
+	return(m_pMaterial);
+}
+
+const float3_t& CModelOverlay::getNormal()
+{
+	return(m_vNormal);
+}
+
+void CModelOverlay::onModelRemoved()
+{
+	SAFE_CALL(m_pNextOverlay, onModelRemoved);
+
+	//notify decal
+	m_pDecal->onOverlayRemoved(this);
+}
diff --git a/source/anim/ModelOverlay.h b/source/anim/ModelOverlay.h
new file mode 100644
index 0000000000000000000000000000000000000000..4404a20c1a65f57d4472ef95f4bb4bf5efea15ef
--- /dev/null
+++ b/source/anim/ModelOverlay.h
@@ -0,0 +1,43 @@
+#ifndef __MODELOVERLAY_H
+#define __MODELOVERLAY_H
+
+#include <xcommon/resource/IXModel.h>
+#include <graphix/graphix.h>
+#include <mtrl/IXMaterial.h>
+
+class IXMaterialSystem;
+class CDecal;
+
+class CModelOverlay final
+{
+public:
+	CModelOverlay(CDecal *pDecal, IXMaterial *pMaterial, Array<XResourceModelStaticVertex> &aVertices, const float3_t &vNormal);
+	~CModelOverlay();
+
+	const Array<XResourceModelStaticVertex>& getVertices();
+
+	CModelOverlay* getNextOverlay();
+	void setNextOverlay(CModelOverlay *pOverlay);
+
+	IXMaterial* getMaterial();
+
+	const float3_t& getNormal();
+
+	void onModelRemoved();
+
+private:
+	// Model's overlays stored as linked list
+	CModelOverlay *m_pNextOverlay = NULL;
+	CDecal *m_pDecal;
+
+	TODO("Use memory pool");
+	Array<XResourceModelStaticVertex> m_aVertices;
+
+	IXMaterial *m_pMaterial = NULL;
+
+	float3_t m_vNormal;
+
+	bool m_isTransparent = false;
+};
+
+#endif
diff --git a/source/anim/Renderable.cpp b/source/anim/Renderable.cpp
index 81ac9ab42dbb1d20f0597036e2df28da16bcfc76..52ad958bccb76178cbb80d6718cf70d4f22c2fed 100644
--- a/source/anim/Renderable.cpp
+++ b/source/anim/Renderable.cpp
@@ -1,10 +1,12 @@
 #include "Renderable.h"
 #include "RenderableVisibility.h"
+#include "DecalProvider.h"
 
-CRenderable::CRenderable(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic):
+CRenderable::CRenderable(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic, CDecalProvider *pProviderDecal):
 	m_idPlugin(idPlugin),
 	m_pAnimatedModelProvider(pProviderAnimated),
-	m_pDynamicModelProvider(pProviderDynamic)
+	m_pDynamicModelProvider(pProviderDynamic),
+	m_pDecalProvider(pProviderDecal)
 {
 }
 
@@ -40,6 +42,7 @@ void XMETHODCALLTYPE CRenderable::renderStage(X_RENDER_STAGE stage, IXRenderable
 	case XRS_GBUFFER:
 		m_pAnimatedModelProvider->render(pVis);
 		m_pDynamicModelProvider->render(false, pVis);
+		m_pDecalProvider->render(pVis);
 		break;
 	case XRS_SHADOWS:
 		m_pAnimatedModelProvider->render(pVis);
@@ -89,6 +92,7 @@ void XMETHODCALLTYPE CRenderable::startup(IXRender *pRender, IXMaterialSystem *p
 
 	m_pAnimatedModelProvider->setDevice(m_pDevice);
 	m_pDynamicModelProvider->setDevice(m_pDevice);
+	m_pDecalProvider->setDevice(m_pDevice);
 }
 void XMETHODCALLTYPE CRenderable::shutdown()
 {
@@ -96,7 +100,7 @@ void XMETHODCALLTYPE CRenderable::shutdown()
 
 void XMETHODCALLTYPE CRenderable::newVisData(IXRenderableVisibility **ppVisibility)
 {
-	*ppVisibility = new CRenderableVisibility(m_idPlugin, m_pAnimatedModelProvider, m_pDynamicModelProvider);
+	*ppVisibility = new CRenderableVisibility(m_idPlugin, m_pAnimatedModelProvider, m_pDynamicModelProvider, m_pDecalProvider);
 }
 
 IXMaterialSystem* CRenderable::getMaterialSystem()
diff --git a/source/anim/Renderable.h b/source/anim/Renderable.h
index cafcc2fbfc260553f1c8f3cf5f857b0b7919a2e8..85f03cc5139e0e8d2d3a4b34f849466849376c73 100644
--- a/source/anim/Renderable.h
+++ b/source/anim/Renderable.h
@@ -9,7 +9,7 @@
 class CRenderable: public IXUnknownImplementation<IXRenderable>
 {
 public:
-	CRenderable(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic);
+	CRenderable(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic, CDecalProvider *pProviderDecal);
 
 	XIMPLEMENT_VERSION(IXRENDERABLE_VERSION);
 
@@ -56,6 +56,7 @@ protected:
 
 	CAnimatedModelProvider *m_pAnimatedModelProvider = NULL;
 	CDynamicModelProvider *m_pDynamicModelProvider = NULL;
+	CDecalProvider *m_pDecalProvider = NULL;
 };
 
 #endif
diff --git a/source/anim/RenderableVisibility.cpp b/source/anim/RenderableVisibility.cpp
index d37373b47f114ba2fe3b7f3101020a0965ec53a3..f012c9831787a31e0f85b2e09e058b7df230044b 100644
--- a/source/anim/RenderableVisibility.cpp
+++ b/source/anim/RenderableVisibility.cpp
@@ -1,11 +1,13 @@
 #include "RenderableVisibility.h"
 #include "AnimatedModelProvider.h"
 #include "DynamicModelProvider.h"
+#include "DecalProvider.h"
 
-CRenderableVisibility::CRenderableVisibility(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic):
+CRenderableVisibility::CRenderableVisibility(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic, CDecalProvider *pProviderDecal):
 	m_idPlugin(idPlugin),
 	m_pProviderAnimated(pProviderAnimated),
-	m_pProviderDynamic(pProviderDynamic)
+	m_pProviderDynamic(pProviderDynamic),
+	m_pProviderDecal(pProviderDecal)
 {
 }
 
@@ -38,6 +40,7 @@ void CRenderableVisibility::updateForCamera(IXCamera *pCamera, const IXRenderabl
 	IXFrustum *pFrustum = pCamera->getFrustum();
 	m_pProviderAnimated->computeVisibility(pFrustum, this, pCamera->getLayerMask(), pRef);
 	m_pProviderDynamic->computeVisibility(pFrustum, pCamera->getLook(), this, pCamera->getLayerMask(), pRef, pCamera);
+	m_pProviderDecal->computeVisibility(pCamera->getLook(), this);
 	mem_release(pFrustum);
 }
 
@@ -52,6 +55,7 @@ void CRenderableVisibility::updateForFrustum(const IXFrustum *pFrustum, UINT bmL
 
 	m_pProviderAnimated->computeVisibility(pFrustum, this, bmLayers, pRef);
 	m_pProviderDynamic->computeVisibility(pFrustum, float3(), this, bmLayers, pRef);
+	m_pProviderDecal->computeVisibility(float3(), this);
 }
 
 static void SortRenderList(Array<CDynamicModel*> &aList)
@@ -139,7 +143,8 @@ void CRenderableVisibility::append(const IXRenderableVisibility *pOther_)
 	MergeArrays(m_aSelfillumList, pOther->m_aSelfillumList);
 	SortRenderList(m_aSelfillumList);
 
-	//! @todo implement for transparency too!
+	TODO("Implement for transparency too!");
+	TODO("Implement for decals too!");
 }
 
 void CRenderableVisibility::substract(const IXRenderableVisibility *pOther_)
@@ -159,7 +164,8 @@ void CRenderableVisibility::substract(const IXRenderableVisibility *pOther_)
 		}
 	}
 
-	//! @todo implement for transparency too!
+	TODO("Implement for transparency too!");
+	TODO("Implement for decals too!");
 }
 
 void CRenderableVisibility::setItemCount(UINT uCount)
@@ -257,3 +263,8 @@ Array<CDynamicModel*>& CRenderableVisibility::getTransparentList()
 {
 	return(m_aTransparentList);
 }
+
+CRenderableVisibility::OverlayData& CRenderableVisibility::getOverlayData()
+{
+	return(m_overlayData);
+}
diff --git a/source/anim/RenderableVisibility.h b/source/anim/RenderableVisibility.h
index 6d96b4998ce3e48771a923a55da214e68c183b2e..8032e469b6fd1bdb59f61f1b95513719956feac8 100644
--- a/source/anim/RenderableVisibility.h
+++ b/source/anim/RenderableVisibility.h
@@ -2,14 +2,16 @@
 #define __RENDERABLE_VISIBILITY_H
 
 #include <xcommon/IXRenderable.h>
+#include <xcommon/resource/IXResourceModel.h>
 
 class CAnimatedModelProvider;
 class CDynamicModelProvider;
+class CDecalProvider;
 class CDynamicModel;
 class CRenderableVisibility final: public IXUnknownImplementation<IXRenderableVisibility>
 {
 public:
-	CRenderableVisibility(ID idPlugin, CAnimatedModelProvider *m_pProviderAnimated, CDynamicModelProvider *m_pProviderDynamic);
+	CRenderableVisibility(ID idPlugin, CAnimatedModelProvider *pProviderAnimated, CDynamicModelProvider *pProviderDynamic, CDecalProvider *pProviderDecal);
 	~CRenderableVisibility();
 
 	ID getPluginId() const override;
@@ -40,6 +42,24 @@ public:
 		UINT uLod;
 		IXMaterial *pMaterial;
 	};
+	
+	struct OverlaySubset
+	{
+		IXMaterial *pMaterial;
+		UINT uStartVertex;
+		UINT uStartIndex;
+		UINT uQuadCount;
+	};
+
+	struct OverlayData
+	{
+		Array<XResourceModelStaticVertexGPU> aVertices;
+		Array<OverlaySubset> aSubsets;
+		IGXVertexBuffer *pVB = NULL;
+		IGXRenderBuffer *pRB = NULL;
+		IGXIndexBuffer *pIB = NULL;
+		UINT uVertexBufferAllocSize = 0;
+	};
 
 	void setItemCount(UINT uCount);
 	item_s* getItem(UINT uIndex);
@@ -57,16 +77,18 @@ public:
 	Array<CDynamicModel*>& getRenderList();
 	Array<CDynamicModel*>& getTransparentList();
 	Array<CDynamicModel*>& getSelfillumList();
+	OverlayData& getOverlayData();
 
 	IXOcclusionCuller* getCuller()
 	{
 		return(m_pOcclusionCuller);
 	}
 
-protected:
+private:
 	ID m_idPlugin;
 	CAnimatedModelProvider *m_pProviderAnimated;
 	CDynamicModelProvider *m_pProviderDynamic;
+	CDecalProvider *m_pProviderDecal;
 	IXOcclusionCuller *m_pOcclusionCuller = NULL;
 
 	Array<item_s> m_aItems;
@@ -75,6 +97,8 @@ protected:
 	Array<CDynamicModel*> m_aRenderList;
 	Array<CDynamicModel*> m_aTransparentList;
 	Array<CDynamicModel*> m_aSelfillumList;
+	
+	OverlayData m_overlayData;
 };
 
 #endif
diff --git a/source/anim/StaticModelProvider.cpp b/source/anim/StaticModelProvider.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..99cbb517abaa174b2d4e455bbe092bf52944648f
--- /dev/null
+++ b/source/anim/StaticModelProvider.cpp
@@ -0,0 +1,20 @@
+#include "StaticModelProvider.h"
+
+CStaticModelProvider::CStaticModelProvider(CDynamicModelProvider *pDynamicModelProvider):
+	m_pDynamicModelProvider(pDynamicModelProvider)
+{
+}
+
+bool XMETHODCALLTYPE CStaticModelProvider::createModel(IXResourceModelStatic *pResource, IXStaticModel **ppModel)
+{
+	IXDynamicModel *pModel;
+	if(m_pDynamicModelProvider->createModel(pResource, &pModel))
+	{
+		((CDynamicModel*)pModel)->setStatic(true);
+
+		*ppModel = pModel;
+		return(true);
+	}
+
+	return(false);
+}
diff --git a/source/anim/StaticModelProvider.h b/source/anim/StaticModelProvider.h
new file mode 100644
index 0000000000000000000000000000000000000000..37fd0b31938fa825400c5cf9bb52e85f312e9d8d
--- /dev/null
+++ b/source/anim/StaticModelProvider.h
@@ -0,0 +1,18 @@
+#ifndef __STATICMODELPROVIDER_H
+#define __STATICMODELPROVIDER_H
+
+#include <xcommon/resource/IXModelProvider.h>
+#include "DynamicModelProvider.h"
+
+class CStaticModelProvider final: public IXUnknownImplementation<IXStaticModelProvider>
+{
+public:
+	CStaticModelProvider(CDynamicModelProvider *pDynamicModelProvider);
+
+	bool XMETHODCALLTYPE createModel(IXResourceModelStatic *pResource, IXStaticModel **ppModel) override;
+
+private:
+	CDynamicModelProvider *m_pDynamicModelProvider;
+};
+
+#endif
diff --git a/source/anim/Updatable.cpp b/source/anim/Updatable.cpp
index d04c08cd13b194c8bac23e7493ff8084328d2471..2a48fd286fed63e6c019b14c508c3ed413305dd1 100644
--- a/source/anim/Updatable.cpp
+++ b/source/anim/Updatable.cpp
@@ -1,8 +1,9 @@
 #include "Updatable.h"
 
-CUpdatable::CUpdatable(CAnimatedModelProvider *pAnimatedModelProvider, CDynamicModelProvider *pDynamicModelProvider):
+CUpdatable::CUpdatable(CAnimatedModelProvider *pAnimatedModelProvider, CDynamicModelProvider *pDynamicModelProvider, CDecalProvider *pDecalProvider):
 	m_pAnimatedModelProvider(pAnimatedModelProvider),
-	m_pDynamicModelProvider(pDynamicModelProvider)
+	m_pDynamicModelProvider(pDynamicModelProvider),
+	m_pDecalProvider(pDecalProvider)
 {
 
 }
@@ -23,6 +24,8 @@ ID CUpdatable::run(float fDelta)
 
 	m_pDynamicModelProvider->update();
 
+	m_pDecalProvider->update();
+
 	return(-1);
 }
 
@@ -30,4 +33,5 @@ void CUpdatable::sync()
 {
 	m_pAnimatedModelProvider->sync();
 	m_pDynamicModelProvider->sync();
+	//m_pDecalProvider->update();
 }
diff --git a/source/anim/Updatable.h b/source/anim/Updatable.h
index 7123026acff06ca4a1edf9f3efeabeeec7ecc1e1..54089837a4775ebad5c966dc60b1d1c316b9545d 100644
--- a/source/anim/Updatable.h
+++ b/source/anim/Updatable.h
@@ -4,11 +4,12 @@
 #include <xcommon/IXUpdatable.h>
 #include "AnimatedModelProvider.h"
 #include "DynamicModelProvider.h"
+#include "DecalProvider.h"
 
 class CUpdatable: public IXUnknownImplementation<IXUpdatable>
 {
 public:
-	CUpdatable(CAnimatedModelProvider *pAnimatedModelProvider, CDynamicModelProvider *pDynamicModelProvider);
+	CUpdatable(CAnimatedModelProvider *pAnimatedModelProvider, CDynamicModelProvider *pDynamicModelProvider, CDecalProvider *pDecalProvider);
 
 	UINT startup() override;
 	void shutdown() override;
@@ -19,6 +20,7 @@ public:
 protected:
 	CAnimatedModelProvider *m_pAnimatedModelProvider;
 	CDynamicModelProvider *m_pDynamicModelProvider;
+	CDecalProvider *m_pDecalProvider;
 };
 
 #endif
diff --git a/source/anim/plugin_main.cpp b/source/anim/plugin_main.cpp
index 307928b5c6c0bcc1ad61feda2a988a7dda710a94..c0c068c21cb63c94a496f3b9c67af7987c57fcca 100644
--- a/source/anim/plugin_main.cpp
+++ b/source/anim/plugin_main.cpp
@@ -4,6 +4,8 @@
 #include "Updatable.h"
 #include "AnimatedModelProvider.h"
 #include "DynamicModelProvider.h"
+#include "StaticModelProvider.h"
+#include "DecalProvider.h"
 
 class CLevelSizeEventListener final: public IEventListener<XEventLevelSize>
 {
@@ -27,8 +29,11 @@ protected:
 class CLoadLevelEventListener final: public IEventListener<XEventLevel>
 {
 public:
-	CLoadLevelEventListener(CRenderable *pRenderable):
-		m_pRenderable(pRenderable)
+	CLoadLevelEventListener(CRenderable *pRenderable, CAnimatedModelProvider *pAnimatedModelProvider, CDynamicModelProvider *pDynamicModelProvider, CDecalProvider *pDecalProvider):
+		m_pRenderable(pRenderable),
+		m_pAnimatedModelProvider(pAnimatedModelProvider),
+		m_pDynamicModelProvider(pDynamicModelProvider),
+		m_pDecalProvider(pDecalProvider)
 	{}
 	void onEvent(const XEventLevel *pData)
 	{
@@ -36,9 +41,18 @@ public:
 		{
 		case XEventLevel::TYPE_LOAD_BEGIN:
 			m_pRenderable->setEnabled(false);
+			m_pDecalProvider->setEnabled(false);
 			break;
 		case XEventLevel::TYPE_LOAD_END:
 			m_pRenderable->setEnabled(true);
+			m_pDecalProvider->setEnabled(true);
+			break;
+
+		case XEventLevel::TYPE_LOAD_WAIT_ASYNC_TASKS:
+			while(m_pAnimatedModelProvider->hasPendingOps() && m_pDynamicModelProvider->hasPendingOps())
+			{
+				Sleep(100);
+			}
 			break;
 
 		default:
@@ -48,6 +62,9 @@ public:
 
 protected:
 	CRenderable *m_pRenderable;
+	CAnimatedModelProvider *m_pAnimatedModelProvider;
+	CDynamicModelProvider *m_pDynamicModelProvider;
+	CDecalProvider *m_pDecalProvider;
 };
 
 class CDSEPlugin: public IXUnknownImplementation<IXPlugin>
@@ -57,10 +74,12 @@ public:
 	{
 		m_pAnimatedModelProvider = new CAnimatedModelProvider(m_pCore);
 		m_pDynamicModelProvider = new CDynamicModelProvider(m_pCore);
-		m_pRenderable = new CRenderable(getID(), m_pAnimatedModelProvider, m_pDynamicModelProvider);
-		m_pUpdatable = new CUpdatable(m_pAnimatedModelProvider, m_pDynamicModelProvider);
+		m_pStaticModelProvider = new CStaticModelProvider(m_pDynamicModelProvider);
+		m_pDecalProvider = new CDecalProvider(m_pCore, m_pDynamicModelProvider);
+		m_pRenderable = new CRenderable(getID(), m_pAnimatedModelProvider, m_pDynamicModelProvider, m_pDecalProvider);
+		m_pUpdatable = new CUpdatable(m_pAnimatedModelProvider, m_pDynamicModelProvider, m_pDecalProvider);
 		m_pLevelSizeEventListener = new CLevelSizeEventListener(m_pAnimatedModelProvider, m_pDynamicModelProvider);
-		m_pLevelLoadEventListener = new CLoadLevelEventListener(m_pRenderable);
+		m_pLevelLoadEventListener = new CLoadLevelEventListener(m_pRenderable, m_pAnimatedModelProvider, m_pDynamicModelProvider, m_pDecalProvider);
 
 		m_pCore->getEventChannel<XEventLevelSize>(EVENT_LEVEL_GET_SIZE_GUID)->addListener(m_pLevelSizeEventListener);
 		m_pCore->getEventChannel<XEventLevel>(EVENT_LEVEL_GUID)->addListener(m_pLevelLoadEventListener);
@@ -77,16 +96,22 @@ public:
 		{
 			m_pCore->getEventChannel<XEventLevelSize>(EVENT_LEVEL_GET_SIZE_GUID)->removeListener(m_pLevelSizeEventListener);
 		}
+		if(m_pLevelLoadEventListener)
+		{
+			m_pCore->getEventChannel<XEventLevel>(EVENT_LEVEL_GUID)->removeListener(m_pLevelLoadEventListener);
+		}
 		mem_delete(m_pLevelSizeEventListener);
 		mem_delete(m_pRenderable);
 		mem_delete(m_pUpdatable);
 		mem_delete(m_pAnimatedModelProvider);
+		mem_delete(m_pStaticModelProvider);
 		mem_delete(m_pDynamicModelProvider);
+//		mem_delete(m_pDecalProvider);
 	}
 
 	UINT XMETHODCALLTYPE getInterfaceCount() override
 	{
-		return(4);
+		return(6);
 	}
 	const XGUID* XMETHODCALLTYPE getInterfaceGUID(UINT id) override
 	{
@@ -105,6 +130,12 @@ public:
 		case 3:
 			s_guid = IXDYNAMICMODELPROVIDER_GUID;
 			break;
+		case 4:
+			s_guid = IXDECALPROVIDER_GUID;
+			break;
+		case 5:
+			s_guid = IXSTATICMODELPROVIDER_GUID;
+			break;
 		default:
 			return(NULL);
 		}
@@ -149,6 +180,24 @@ public:
 			add_ref(m_pDynamicModelProvider);
 			*ppOut = m_pDynamicModelProvider;
 			break;
+
+		case 4:
+			if(!m_pDecalProvider)
+			{
+				init();
+			}
+			add_ref(m_pDecalProvider);
+			*ppOut = m_pDecalProvider;
+			break;
+
+		case 5:
+			if(!m_pStaticModelProvider)
+			{
+				init();
+			}
+			add_ref(m_pStaticModelProvider);
+			*ppOut = m_pStaticModelProvider;
+			break;
 		
 		default:
 			*ppOut = NULL;
@@ -161,6 +210,8 @@ protected:
 	IXCore *m_pCore = NULL;
 	CAnimatedModelProvider *m_pAnimatedModelProvider = NULL;
 	CDynamicModelProvider *m_pDynamicModelProvider = NULL;
+	CStaticModelProvider *m_pStaticModelProvider = NULL;
+	CDecalProvider *m_pDecalProvider = NULL;
 	CLevelSizeEventListener *m_pLevelSizeEventListener = NULL;
 	CLoadLevelEventListener *m_pLevelLoadEventListener = NULL;
 };
diff --git a/source/common b/source/common
index 8103fff484d6dd1f7006973af8bf581334d8ea1e..bcaae290724afea7b115514dd8e7b9e75bb67726 160000
--- a/source/common
+++ b/source/common
@@ -1 +1 @@
-Subproject commit 8103fff484d6dd1f7006973af8bf581334d8ea1e
+Subproject commit bcaae290724afea7b115514dd8e7b9e75bb67726
diff --git a/source/core/Config.cpp b/source/core/Config.cpp
index 2f336a6083ffe5365da016044463d7cbdb3594d6..d8a5b37a5d0e1ca78e52a7693f75eeacb31574e2 100644
--- a/source/core/Config.cpp
+++ b/source/core/Config.cpp
@@ -479,275 +479,243 @@ void CConfig::set(const char * sectionp, const char * key, const char * val)
 
 int CConfig::save()
 {
-	static const bool *s_pbDebug = GET_PCVAR_BOOL("dbg_config_save");
-	if(*s_pbDebug)
-	{
-		printf(COLOR_GRAY "====== " COLOR_CYAN "CConfig::save() " COLOR_GRAY "======" COLOR_RESET "\n");
-	}
 	int terror = 0;
 	for(AssotiativeArray<CConfigString, CSection>::Iterator i = m_mSections.begin(); i; ++i)
 	{
-		if(*s_pbDebug)
-		{
-			printf("Testing section: " COLOR_LGREEN "%s" COLOR_RESET "...", i.first->c_str());
-		}
 		if(i.second->isModified)
 		{
-			if(*s_pbDebug)
-			{
-				printf(COLOR_YELLOW " modified" COLOR_RESET "\n");
-			}
 			for(AssotiativeArray<CConfigString, CValue>::Iterator j = i.second->mValues.begin(); j; ++j)
 			{
-				if(*s_pbDebug)
-				{
-					printf("  testing key: " COLOR_LGREEN "%s" COLOR_RESET "...", j.first->c_str());
-				}
 				if(j.second->isModified)
 				{
-					if(*s_pbDebug)
-					{
-						printf(COLOR_YELLOW " modified" COLOR_RESET "\n");
-					}
 					if(i.second->native) // Write to BaseFile
 					{
-						if(*s_pbDebug)
-						{
-							printf("    writing to base file " COLOR_CYAN "%s" COLOR_RESET "...\n", BaseFile.c_str());
-						}
 						terror = writeFile(BaseFile, *i.first, *j.first, j.second->val);
 						if(terror != 0)
 							goto end;
 					}
 					else // Write to i.second->Include
 					{
-						if(*s_pbDebug)
-						{
-							printf("    writing to include file " COLOR_CYAN "%s" COLOR_RESET "...\n", i.second->Include.c_str());
-						}
 						terror = writeFile(i.second->Include, *i.first, *j.first, j.second->val);
 						if(terror != 0)
 							goto end;
 					}
 				}
-				else
-				{
-					if(*s_pbDebug)
-					{
-						printf(COLOR_GRAY " not modified" COLOR_RESET "\n");
-					}
-				}
-			}
-		}
-		else
-		{
-			if(*s_pbDebug)
-			{
-				printf(COLOR_GRAY " not modified" COLOR_RESET "\n");
 			}
 		}
 	}
+
+	terror = commitFiles();
 end:
-	if(*s_pbDebug)
-	{
-		printf(COLOR_GRAY "=============================" COLOR_RESET "\n");
-	}
 	return(terror);
 }
 
 int CConfig::writeFile(const CConfigString & name, CConfigString section, CConfigString key, const CConfigString & val)
 {
-	static const bool *s_pbDebug = GET_PCVAR_BOOL("dbg_config_save");
-	//printf("W: %s\t[%s]: %s = %s\n", name.c_str(), section.c_str(), key.c_str(), val.c_str());
-	FILE * pF = fopen(name.c_str(), "rb");
-	if(pF)
+	const AssotiativeArray<String, String>::Node *pUncommittedNode;
+	
+	if(!m_mapUncommitted.KeyExists(name, &pUncommittedNode))
 	{
-		if(*s_pbDebug)
-		{
-			printf("    file opened\n");
-		}
+		pUncommittedNode = m_mapUncommitted.insert(name, "");
 
-		fseek(pF, 0, SEEK_END);
-		UINT fl = ftell(pF);
-		fseek(pF, 0, SEEK_SET);
-		char * szData = new char[fl + 1];
-		if(!szData)
+		// read file
+		IFile *pFile = m_pFS->openFile(name.c_str());
+		if(pFile)
 		{
-			printf(COLOR_LRED "Unable to allocate memory (%d) in CConfig::writeFile()" COLOR_RESET "\n", fl + 1);
-			fclose(pF);
-			return(-1);
+			size_t sizeFile = pFile->getSize();
+
+			pUncommittedNode->Val->resize(sizeFile);
+			char *szPtr = &((*(pUncommittedNode->Val))[0]);
+			pFile->readBin(szPtr, sizeFile);
+			szPtr[sizeFile] = 0;
+
+			mem_release(pFile);
 		}
-		fread(szData, 1, fl, pF);
-		szData[fl] = 0;
-		fclose(pF);
-		UINT sl = section.length();
-		UINT kl = key.length();
-		bool sf = false;
-		bool kf = false;
-		bool se = false;
-		UINT sp = 0;
-		for(UINT i = 0; i < fl; ++i)
+	}
+
+	String &sFileData = *(pUncommittedNode->Val);
+	
+
+	UINT fl = sFileData.length();
+	UINT sl = section.length();
+	UINT kl = key.length();
+	bool sf = false;
+	bool kf = false;
+	bool se = false;
+	UINT sp = 0;
+	const char *szData = sFileData.c_str();
+	for(UINT i = 0; i < fl; ++i)
+	{
+		if(szData[i] == '[' && ((i > 0 && (szData[i - 1] == '\r' || szData[i - 1] == '\n')) || i == 0))
 		{
-			if(szData[i] == '[' && ((i > 0 && (szData[i - 1] == '\r' || szData[i - 1] == '\n')) || i == 0))
+			bool cmp = true;
+			UINT j;
+			for(j = i + 1; j < fl - 1 && j - i - 1 < sl; ++j)
+			{
+				if(szData[j] != section[j - i - 1])
+				{
+					cmp = false;
+					break;
+				}
+			}
+			if(cmp && szData[j] == ']')//Section Found!
 			{
-				bool cmp = true;
-				UINT j;
-				for(j = i + 1; j < fl - 1 && j - i - 1 < sl; ++j)
+				sf = true;
+				i = j;
+				for(; i < fl; ++i)
 				{
-					if(szData[j] != section[j - i - 1])
+					if(szData[i] == '\r' || szData[i] == '\n')
 					{
-						cmp = false;
 						break;
 					}
 				}
-				if(cmp && szData[j] == ']')//Section Found!
+				while(i < fl && (szData[i] == '\r' || szData[i] == '\n'))
 				{
-					sf = true;
-					i = j;
-					for(; i < fl; ++i)
+					++i;
+				}
+				sp = i;
+				//We are inside the section
+				//So, let's find the key
+				while(i < fl)
+				{
+					if(szData[i] == '[') // New section begin. There is no our key, so, add it!
 					{
-						if(szData[i] == '\r' || szData[i] == '\n')
-						{
-							break;
-						}
+						kf = false;
+						se = true;
+						break;
 					}
-					while(i < fl && (szData[i] == '\r' || szData[i] == '\n'))
+					while(i < fl && (szData[i] == ' ' || szData[i] == '\t'))
 					{
 						++i;
 					}
-					sp = i;
-					//We are inside the section
-					//So, let's find the key
-					while(i < fl)
+					bool f = true;
+					for(j = i; j < fl && j - i < kl; ++j)
 					{
-						if(szData[i] == '[') // New section begin. There is no our key, so, add it!
+						if(szData[j] != key[j - i])
 						{
-							kf = false;
-							se = true;
+							f = false;
 							break;
 						}
-						while(i < fl && (szData[i] == ' ' || szData[i] == '\t'))
-						{
-							++i;
-						}
-						bool f = true;
-						for(j = i; j < fl && j - i < kl; ++j)
+					}
+					if(f && (isspace((unsigned char)szData[j]) || szData[j] == '='))//KeyFound!
+					{
+						i = j;
+						kf = true;
+						for(j = i; j < fl; ++j)
 						{
-							if(szData[j] != key[j - i])
+							if(szData[j] == '\n' || szData[j] == '\r' || szData[j] == ';')
 							{
-								f = false;
+								//f = false;
 								break;
 							}
 						}
-						if(f && (isspace((unsigned char)szData[j]) || szData[j] == '='))//KeyFound!
+						//while(szData[j] == '\r' || szData[j] == '\n')
+						//{
+						//	++j;
+						//}
+						//Let's write the file
+
+						if(i != j)
 						{
-							i = j;
-							kf = true;
-							for(j = i; j < fl; ++j)
-							{
-								if(szData[j] == '\n' || szData[j] == '\r' || szData[j] == ';')
-								{
-									//f = false;
-									break;
-								}
-							}
-							//while(szData[j] == '\r' || szData[j] == '\n')
-							//{
-							//	++j;
-							//}
-							//Let's write the file
-							FILE * pF = fopen(name.c_str(), "wb");
-							if(!pF)
-							{
-								ErrorFile = name;
-								mem_delete_a(szData);
-								return -1;
-							}
-							fwrite(szData, 1, i, pF); // First file part, including key
-							fwrite(" = ", 1, 3, pF);
-							fwrite(val.c_str(), 1, val.length(), pF);
-							//fwrite("\n", 1, 1, pF);
-							fwrite(&szData[j], 1, fl - j, pF);
-							fclose(pF);
+							sFileData.remove(i, j - i);
+						}
+
+						sFileData.insert(i, String(" = ") + val);
+
+						/*
+						FILE * pF = fopen(name.c_str(), "wb");
+						if(!pF)
+						{
+							ErrorFile = name;
 							mem_delete_a(szData);
-							return 0;
+							return -1;
 						}
-						else // Skip current row
+						fwrite(szData, 1, i, pF); // First file part, including key
+						fwrite(" = ", 1, 3, pF);
+						fwrite(val.c_str(), 1, val.length(), pF);
+						//fwrite("\n", 1, 1, pF);
+						fwrite(&szData[j], 1, fl - j, pF);
+						fclose(pF);
+						mem_delete_a(szData);*/
+						return 0;
+					}
+					else // Skip current row
+					{
+						for(; i < fl; ++i)
 						{
-							for(; i < fl; ++i)
-							{
-								if(szData[i] == '\n' || szData[i] == '\r')
-								{
-									//f = false;
-									break;
-								}
-							}
-							while(i < fl && (szData[i] == '\r' || szData[i] == '\n'))
+							if(szData[i] == '\n' || szData[i] == '\r')
 							{
-								++i;
+								//f = false;
+								break;
 							}
 						}
-					}
-					//
-					if(se)
-					{
-						break;
+						while(i < fl && (szData[i] == '\r' || szData[i] == '\n'))
+						{
+							++i;
+						}
 					}
 				}
+				//
+				if(se)
+				{
+					break;
+				}
 			}
 		}
-		if(sf && !kf)
-		{
-			if(*s_pbDebug)
-			{
-				printf("    adding key to section(sp=" COLOR_LCYAN "%d" COLOR_RESET ")\n", sp);
-			}
-			FILE * pF = fopen(name.c_str(), "wb");
-			if(!pF)
-			{
-				ErrorFile = name;
-				mem_delete_a(szData);
-				return -1;
-			}
-			fwrite(szData, 1, sp, pF); // First file part
-			fwrite(key.c_str(), 1, key.length(), pF);
-			fwrite(" = ", 1, 3, pF);
-			fwrite(val.c_str(), 1, val.length(), pF);
-			fwrite("\n", 1, 1, pF);
-			fwrite(&szData[sp], 1, fl - sp, pF);
-			fclose(pF);
-			mem_delete_a(szData);
-			return 0;
-		}
-		if(!sf)//!(Section found) == Add new
-		{
-			FILE * pF = fopen(name.c_str(), "ab");
-			if(!pF)
-			{
-				ErrorFile = name;
-				return -1;
-			}
-			fwrite((CConfigString("\n[") + section + "]\n" + key + " = " + val + "\n").c_str(), sizeof(char), section.length() + key.length() + val.length() + 8, pF);
-			fclose(pF);
-			mem_delete_a(szData);
-			return 0;
-		}
 	}
-	else
+	if(sf && !kf)
 	{
-		FILE * pF = fopen(name.c_str(), "wb");
-		if(!pF)
-		{
-			ErrorFile = name;
-			return -1;
-		}
-		fwrite((CConfigString("[") + section + "]\n" + key + " = " + val + "\n").c_str(), sizeof(char), section.length() + key.length() + val.length() + 7, pF);
-		fclose(pF);
+		sFileData.insert(sp, key + " = " + val + '\n');
+
+		return 0;
+	}
+	if(!sf)//!(Section found) == Add new
+	{
+		sFileData += '\n';
+		sFileData += '[';
+		sFileData += section;
+		sFileData += ']';
+		sFileData += '\n';
+		sFileData += key;
+		sFileData += " = ";
+		sFileData += val;
+		sFileData += '\n';
+
 		return 0;
 	}
+	
 	return 0;
 }
 
+int CConfig::commitFiles()
+{
+	for(AssotiativeArray<String, String>::Iterator i = m_mapUncommitted.begin(); i; ++i)
+	{
+		//String sTempName = (*i.first) + ".new";
+		const String &sName = (*i.first);
+		IFile *pFile = m_pFS->openFile(sName.c_str(), FILE_MODE_WRITE);
+		if(!pFile)
+		{
+			ErrorFile = sName;
+			return(-1);
+		}
+
+		if(i.second->length() != pFile->writeBin(i.second->c_str(), i.second->length()))
+		{
+			ErrorFile = sName;
+			pFile->close();
+			return(-2);
+		}
+		pFile->close();
+
+		//m_pFS->rename
+	}
+
+	m_mapUncommitted.clear();
+	
+	return(0);
+}
+
 int CConfig::getSectionCount()
 {
 	return(m_mSections.Size());
@@ -804,7 +772,6 @@ void CConfig::clear()
 	for(int i = 0; i < size; ++i)
 	{
 		mem_release(m_vIncludes[i].pParser);
-		mem_delete(m_vIncludes[i].pParser);
 	}
 	m_vIncludes.clear();
 	m_mFinalValues.clear();
diff --git a/source/core/Config.h b/source/core/Config.h
index a921ce2cac390b981e2ef30e75072363085586cc..a2455e8582c2d464e8a3d23a6672e757511c07be 100644
--- a/source/core/Config.h
+++ b/source/core/Config.h
@@ -37,6 +37,8 @@ public:
 	}
 };
 
+//##########################################################################
+
 class CConfig: public ISXConfig
 {
 public:
@@ -117,8 +119,16 @@ protected:
 	CConfigString baseName(CConfigString dir);
 
 	void modify(AssotiativeArray<CConfigString, CSection> & sections, AssotiativeArray<CConfigString, CValue> & values, CConfigString IncName);
+
+private:
+	int commitFiles();
+
+private:
+	AssotiativeArray<String, String> m_mapUncommitted;
 };
 
+//##########################################################################
+
 class CXConfig: public IXUnknownImplementation<IXConfig>
 {
 public:
diff --git a/source/core/Core.cpp b/source/core/Core.cpp
index 701dbefd9fe74d6521b702d63a072f39c1fa7952..567a43d4b214155d1aee509d45d86f0e63e5694e 100644
--- a/source/core/Core.cpp
+++ b/source/core/Core.cpp
@@ -35,7 +35,6 @@ CCore::CCore(const char *szName)
 
 	Core_0RegisterCVarBool("g_time_run", true, "Запущено ли игровое время?", FCVAR_NOTIFY_OLD);
 	Core_0RegisterCVarFloat("g_time_speed", 1.f, "Скорость/соотношение течения игрового времени", FCVAR_NOTIFY_OLD);
-	Core_0RegisterCVarBool("dbg_config_save", false, "Отладочный вывод процесса сохранения конфига");
 	Core_0RegisterCVarInt("r_stats", 0, "Показывать ли статистику? 0 - нет, 1 - fps и игровое время, 2 - показать полностью");
 
 	Core_0RegisterConcmd("on_g_time_run_change", []()
diff --git a/source/core/JSON.cpp b/source/core/JSON.cpp
index 01cf57087512654df973e57ae71807a483decf4f..c25acd44e60fe9529a69ea96a0fa640453d535e7 100644
--- a/source/core/JSON.cpp
+++ b/source/core/JSON.cpp
@@ -1,5 +1,9 @@
 #include "JSON.h"
 
+MemAlloc<CJSONValue> CJSON::ms_memValues;
+MemAlloc<CJSONArray> CJSON::ms_memArrays;
+MemAlloc<CJSONObject> CJSON::ms_memObjects;
+SpinLock CJSON::ms_slMem;
 
 bool XMETHODCALLTYPE CJSON::parse(const char *szString, IXJSONItem **ppOut, void *pReserved) const
 {
@@ -25,7 +29,7 @@ bool CJSON::Parse(const char **str, IXJSONItem **ppOut)
 
 	if(**str == '{')
 	{
-		CJSONObject *o = new CJSONObject();
+		CJSONObject *o = AllocObject();
 		if(!o->load(str))
 		{
 			mem_release(o);
@@ -35,7 +39,7 @@ bool CJSON::Parse(const char **str, IXJSONItem **ppOut)
 	}
 	else if(**str == '[')
 	{
-		CJSONArray *o = new CJSONArray();
+		CJSONArray *o = AllocArray();
 		if(!o->load(str))
 		{
 			mem_release(o);
@@ -45,7 +49,7 @@ bool CJSON::Parse(const char **str, IXJSONItem **ppOut)
 	}
 	else
 	{
-		CJSONValue *o = new CJSONValue();
+		CJSONValue *o = AllocValue();
 		if(!o->load(str))
 		{
 			mem_release(o);
@@ -56,16 +60,40 @@ bool CJSON::Parse(const char **str, IXJSONItem **ppOut)
 	return(true);
 }
 
-//##########################################################################
+CJSONValue* CJSON::AllocValue()
+{
+	ScopedSpinLock lock(ms_slMem);
+	return(ms_memValues.Alloc());
+}
+CJSONArray* CJSON::AllocArray()
+{
+	ScopedSpinLock lock(ms_slMem);
+	return(ms_memArrays.Alloc());
+}
+CJSONObject* CJSON::AllocObject()
+{
+	ScopedSpinLock lock(ms_slMem);
+	return(ms_memObjects.Alloc());
+}
 
-CJSONArray::~CJSONArray()
+void CJSON::ReleaseItem(CJSONValue *pItem)
 {
-	for(UINT i = 0, l = m_aItems.size(); i < l; ++i)
-	{
-		mem_release(m_aItems[i]);
-	}
+	ScopedSpinLock lock(ms_slMem);
+	ms_memValues.Delete(pItem);
+}
+void CJSON::ReleaseItem(CJSONArray *pItem)
+{
+	ScopedSpinLock lock(ms_slMem);
+	ms_memArrays.Delete(pItem);
+}
+void CJSON::ReleaseItem(CJSONObject *pItem)
+{
+	ScopedSpinLock lock(ms_slMem);
+	ms_memObjects.Delete(pItem);
 }
 
+//##########################################################################
+
 UINT XMETHODCALLTYPE CJSONArray::size() const
 {
 	return(m_aItems.size());
@@ -122,16 +150,18 @@ bool CJSONArray::loadArr(const char **str)
 	return(true);
 }
 
-//##########################################################################
-
-CJSONObject::~CJSONObject()
+void XMETHODCALLTYPE CJSONArray::FinalRelease()
 {
-	for(UINT i = 0, l = m_aPairs.size(); i < l; ++i)
+	for(UINT i = 0, l = m_aItems.size(); i < l; ++i)
 	{
-		mem_release(m_aPairs[i].pVal);
+		mem_release(m_aItems[i]);
 	}
+
+	CJSON::ReleaseItem(this);
 }
 
+//##########################################################################
+
 UINT XMETHODCALLTYPE CJSONObject::size() const
 {
 	return(m_aPairs.size());
@@ -216,3 +246,21 @@ bool CJSONObject::loadObj(const char **str)
 	++*str;
 	return(true);
 }
+
+void XMETHODCALLTYPE CJSONObject::FinalRelease()
+{
+	for(UINT i = 0, l = m_aPairs.size(); i < l; ++i)
+	{
+		mem_release(m_aPairs[i].pVal);
+	}
+
+	CJSON::ReleaseItem(this);
+}
+
+//##########################################################################
+
+void XMETHODCALLTYPE CJSONValue::FinalRelease()
+{
+	CJSON::ReleaseItem(this);
+}
+
diff --git a/source/core/JSON.h b/source/core/JSON.h
index 0e04ec2ec0c70dae94c7e44f710533160c6883a4..92b7d5f534b5ba6f9bf0e38091b4740cff9b80f0 100644
--- a/source/core/JSON.h
+++ b/source/core/JSON.h
@@ -195,8 +195,10 @@ public:
 private:
 	bool loadString(const char **prm)
 	{
+		m_sVal = "";
+
 		m_type = XJI_STRING;
-		Array<char> tmp;
+		//Array<char> tmp;
 		++*prm;
 		while(**prm && **prm != '"')
 		{
@@ -208,22 +210,22 @@ private:
 				case L'"':
 				case L'\\':
 				case L'/':
-					tmp.push_back(**prm);
+					m_sVal += **prm;
 					break;
 				case L'b':
-					tmp.push_back('\b');
+					m_sVal += '\b';
 					break;
 				case L'f':
-					tmp.push_back('\f');
+					m_sVal += '\f';
 					break;
 				case L'n':
-					tmp.push_back('\n');
+					m_sVal += '\n';
 					break;
 				case L'r':
-					tmp.push_back('\r');
+					m_sVal += '\r';
 					break;
 				case L't':
-					tmp.push_back('\t');
+					m_sVal += '\t';
 					break;
 				case L'u':
 				{
@@ -249,7 +251,7 @@ private:
 							return(false);
 						}
 					}
-					writeChar(code, &tmp);
+					writeChar(code);
 				}
 				break;
 				default:
@@ -258,7 +260,7 @@ private:
 			}
 			else
 			{
-				tmp.push_back(**prm);
+				m_sVal += **prm;
 			}
 			++*prm;
 		}
@@ -267,8 +269,6 @@ private:
 			return(false);
 		}
 		++*prm;
-		tmp.push_back(0);
-		m_sVal = tmp;
 		return(true);
 	}
 	bool loadNum(const char **prm)
@@ -399,7 +399,7 @@ private:
 		return(false);
 	}
 
-	void writeChar(UINT c, Array<char> *pOut)
+	void writeChar(UINT c)
 	{
 		UINT codepoint = 0;
 		short *in = (short*)&c;
@@ -422,28 +422,28 @@ private:
 
 				if(codepoint <= 0x7f)
 				{
-					pOut->push_back(codepoint);
+					m_sVal += (char)codepoint;
 					break;
 				}
 				else if(codepoint <= 0x7ff)
 				{
-					pOut->push_back(0xc0 | ((codepoint >> 6) & 0x1f));
-					pOut->push_back(0x80 | (codepoint & 0x3f));
+					m_sVal += (char)(0xc0 | ((codepoint >> 6) & 0x1f));
+					m_sVal += (char)(0x80 | (codepoint & 0x3f));
 					break;
 				}
 				else if(codepoint <= 0xffff)
 				{
-					pOut->push_back(0xe0 | ((codepoint >> 12) & 0x0f));
-					pOut->push_back(0x80 | ((codepoint >> 6) & 0x3f));
-					pOut->push_back(0x80 | (codepoint & 0x3f));
+					m_sVal += (char)(0xe0 | ((codepoint >> 12) & 0x0f));
+					m_sVal += (char)(0x80 | ((codepoint >> 6) & 0x3f));
+					m_sVal += (char)(0x80 | (codepoint & 0x3f));
 					break;
 				}
 				else
 				{
-					pOut->push_back(0xf0 | ((codepoint >> 18) & 0x07));
-					pOut->push_back(0x80 | ((codepoint >> 12) & 0x3f));
-					pOut->push_back(0x80 | ((codepoint >> 6) & 0x3f));
-					pOut->push_back(0x80 | (codepoint & 0x3f));
+					m_sVal += (char)(0xf0 | ((codepoint >> 18) & 0x07));
+					m_sVal += (char)(0x80 | ((codepoint >> 12) & 0x3f));
+					m_sVal += (char)(0x80 | ((codepoint >> 6) & 0x3f));
+					m_sVal += (char)(0x80 | (codepoint & 0x3f));
 					break;
 				}
 			}
@@ -458,15 +458,19 @@ protected:
 	mutable String m_sVal;
 };
 
+//##########################################################################
+
 class CJSONValue: public CJSONItem<IXJSONItem>
 {
+private:
+	void XMETHODCALLTYPE FinalRelease() override;
 };
 
+//##########################################################################
+
 class CJSONArray: public CJSONItem<IXJSONArray>
 {
 public:
-	~CJSONArray();
-
 	UINT XMETHODCALLTYPE size() const override;
 	IXJSONItem* XMETHODCALLTYPE at(UINT idx) const override;
 
@@ -481,16 +485,18 @@ public:
 
 private:
 	bool loadArr(const char **str) override;
+	
+	void XMETHODCALLTYPE FinalRelease() override;
 
 private:
 	Array<IXJSONItem*> m_aItems;
 };
 
+//##########################################################################
+
 class CJSONObject: public CJSONItem<IXJSONObject>
 {
 public:
-	~CJSONObject();
-
 	UINT XMETHODCALLTYPE size() const override;
 	IXJSONItem* XMETHODCALLTYPE at(UINT idx) const override;
 	
@@ -509,7 +515,10 @@ public:
 
 private:
 	bool loadObj(const char **str) override;
-	
+
+	void XMETHODCALLTYPE FinalRelease() override;
+
+private:
 	struct KeyValue
 	{
 		String sKey;
@@ -519,12 +528,30 @@ private:
 	Array<KeyValue> m_aPairs;
 };
 
+//##########################################################################
+
 class CJSON: public IXUnknownImplementation<IXJSON>
 {
 public:
 	bool XMETHODCALLTYPE parse(const char *szString, IXJSONItem **ppOut, void *pReserved = NULL) const override;
 
 	static bool Parse(const char **str, IXJSONItem **ppOut);
+
+	static void ReleaseItem(CJSONValue *pItem);
+	static void ReleaseItem(CJSONArray *pItem);
+	static void ReleaseItem(CJSONObject *pItem);
+
+private:
+	static MemAlloc<CJSONValue> ms_memValues;
+	static MemAlloc<CJSONArray> ms_memArrays;
+	static MemAlloc<CJSONObject> ms_memObjects;
+	
+	static SpinLock ms_slMem;
+
+private:
+	static CJSONValue* AllocValue();
+	static CJSONArray* AllocArray();
+	static CJSONObject* AllocObject();
 };
 
 #endif
diff --git a/source/game/BaseAmmo.cpp b/source/game/BaseAmmo.cpp
index 8714102d3348320990b7f1ca63891f49ba742bac..35abac1eea8a590650ec7e3abe5d2d9420f35cb7 100644
--- a/source/game/BaseAmmo.cpp
+++ b/source/game/BaseAmmo.cpp
@@ -9,11 +9,10 @@ See the license in LICENSE
 #include "BaseTool.h"
 #include <particles/sxparticles.h>
 #include <mtrl/IXMaterial.h>
-#include <decals/sxdecals.h>
 //#include <BulletCollision/NarrowPhaseCollision/btRaycastCallback.h>
 
 //! TODO Reimplement me!
-#define SMtrl_MtlGetPhysicMaterial(id) MTLTYPE_PHYSIC_METAL
+#define SMtrl_MtlGetPhysicMaterial(id) MTLTYPE_PHYSIC_CONCRETE
 #define SMtrl_MtlGetDurability(id) 1.0f
 #define SMtrl_MtlGetDensity(id) 1.0f
 #define SMtrl_MtlGetHitChance(id) 1.0f
@@ -107,8 +106,7 @@ void CBaseAmmo::fire(const float3 &_vStart, const float3 &_vDir, CBaseCharacter
 			});
 			for(int i = 0, l = aHitPoints.size(); i < l; ++i)
 			{
-
-				ID idMtl = -1; // SPhysics_GetMtlID(aHitPoints[i].pCollisionObject, &aHitPoints[i].shapeInfo);
+				ID idMtl = 0; // SPhysics_GetMtlID(aHitPoints[i].pCollisionObject, &aHitPoints[i].shapeInfo);
 				if(ID_VALID(idMtl) && !aHitPoints[i].isExit)
 				{
 					float fHitChance = SMtrl_MtlGetHitChance(idMtl);
@@ -184,6 +182,15 @@ void CBaseAmmo::fire(const float3 &_vStart, const float3 &_vDir, CBaseCharacter
 					// restart fire with new dir and speed
 
 					float3 vStart2 = aHitPoints[i].vPosition + SMVector3Normalize(vDir) * 0.001f;
+					for(int j = i + 1; j < l; ++j)
+					{
+						if(aHitPoints[j].isExit)
+						{
+							// restart at exit point
+							vStart2 = aHitPoints[i].vPosition + SMVector3Normalize(vDir) * 0.001f;
+						}
+					}
+
 					vDir = vNewDir;
 					fSpeed = fNewSpeed;
 
@@ -240,26 +247,26 @@ void CBaseAmmo::shootDecal(const float3 &vPos, const float3 &vNormal, ID idMtl)
 	if(ID_VALID(idMtl))
 	{
 		MTLTYPE_PHYSIC type = SMtrl_MtlGetPhysicMaterial(idMtl);
-		DECAL_TYPE decalType = DECAL_TYPE_CONCRETE;
+		XDECAL_TYPE decalType = XDT_CONCRETE;
 		switch(type)
 		{
 		case MTLTYPE_PHYSIC_METAL:
-			decalType = DECAL_TYPE_METAL;
+			decalType = XDT_METAL;
 			break;
 		case MTLTYPE_PHYSIC_FLESH:
-			decalType = DECAL_TYPE_FLESH;
+			decalType = XDT_FLESH;
 			break;
 		case MTLTYPE_PHYSIC_GROUD_SAND:
-			decalType = DECAL_TYPE_EARTH;
+			decalType = XDT_EARTH;
 			break;
 		case MTLTYPE_PHYSIC_PLASTIC:
-			decalType = DECAL_TYPE_PLASTIC;
+			decalType = XDT_PLASTIC;
 			break;
 		case MTLTYPE_PHYSIC_TREE:
-			decalType = DECAL_TYPE_WOOD;
+			decalType = XDT_WOOD;
 			break;
 		}
-		SXDecals_ShootDecal(decalType, vPos, vNormal);
+		SAFE_CALL(GetDecalProvider(), shootDecal, decalType, vPos, vNormal);
 
 		//SPE_EffectPlayByName("create_decal_test", &aHitPoints[i].vPosition, &aHitPoints[i].vNormal);
 	}
@@ -267,7 +274,7 @@ void CBaseAmmo::shootDecal(const float3 &vPos, const float3 &vNormal, ID idMtl)
 
 void CBaseAmmo::shootBlood(const float3 &vPos, const float3 &vNormal)
 {
-	SXDecals_ShootDecal(DECAL_TYPE_BLOOD_BIG, vPos, vNormal);
+	SAFE_CALL(GetDecalProvider(), shootDecal, XDT_BLOOD_BIG, vPos, vNormal);
 }
 
 bool CBaseAmmo::shouldRecochet(const float3 &vPos, const float3 &vNormal, const float3 &vDir, ID idMtl, float fSpeed, float3 *pvNewDir, float *pfNewSpeed)
diff --git a/source/game/BaseEntity.h b/source/game/BaseEntity.h
index 53ecd6de3f3eb3fbfd47e08a0069ab9b61f2c086..a6bee19631543f2b0abd55b67c9582dcbba2c7cb 100644
--- a/source/game/BaseEntity.h
+++ b/source/game/BaseEntity.h
@@ -36,11 +36,13 @@ See the license in LICENSE
 
 #include <light/IXLightSystem.h>
 
+#include <xcommon/resource/IXDecalProvider.h>
+
 #pragma pointers_to_members(full_generality, virtual_inheritance)
 
 IXRender* GetRender();
 IXParticleSystem* GetParticleSystem();
-
+IXDecalProvider* GetDecalProvider();
 
 
 
@@ -170,6 +172,10 @@ public:
 		return(false);
 	}
 
+	virtual void setSize(const float3_t &vSize)
+	{
+	}
+
 private:
 	void setClassName(const char *name);
 	void setDefaults();
diff --git a/source/game/BaseMover.cpp b/source/game/BaseMover.cpp
index d1eed1100fb19396ccf75ec46eaaea9469b83150..0008c78b676ec18bab8b1c443c1cfb37d3aaec24 100644
--- a/source/game/BaseMover.cpp
+++ b/source/game/BaseMover.cpp
@@ -31,7 +31,7 @@ BEGIN_PROPTABLE(CBaseMover)
 	DEFINE_FLAG(MOVER_NO_AUTOMOUNT, "Disable automount")
 END_PROPTABLE()
 
-REGISTER_ENTITY(CBaseMover, base_mover);
+REGISTER_ENTITY_NOLISTING(CBaseMover, base_mover);
 
 IEventChannel<XEventPhysicsStep> *CBaseMover::m_pTickEventChannel = NULL;
 
diff --git a/source/game/BaseTool.cpp b/source/game/BaseTool.cpp
index 5d84a3a0fa5b3c5dfe5026fd5c4fe95d425a8bfb..ccc15248bd43e6211a5ace7862af189df0c4ef6a 100644
--- a/source/game/BaseTool.cpp
+++ b/source/game/BaseTool.cpp
@@ -7,7 +7,6 @@ See the license in LICENSE
 #include "BaseTool.h"
 
 #include <particles/sxparticles.h>
-#include <decals/sxdecals.h>
 #include "Player.h"
 
 /*! \skydocent base_tool
@@ -138,7 +137,8 @@ void CBaseTool::primaryAction(BOOL st)
 		if(cb.hasHit())
 		{
 			//shoot decal
-			SXDecals_ShootDecal(DECAL_TYPE_CONCRETE, BTVEC_F3(cb.m_hitPointWorld), BTVEC_F3(cb.m_hitNormalWorld));
+			SAFE_CALL(GetDecalProvider(), shootDecal, XDT_CONCRETE, cb.m_result.vHitPoint, cb.m_result.vHitNormal);
+			//SXDecals_ShootDecal(DECAL_TYPE_CONCRETE, BTVEC_F3(cb.m_hitPointWorld), BTVEC_F3(cb.m_hitNormalWorld));
 			SPE_EffectPlayByName("fire", &BTVEC_F3(cb.m_hitPointWorld), &BTVEC_F3(cb.m_hitNormalWorld));
 
 			IXRigidBody *pRB = cb.m_result.pCollisionObject->asRigidBody();
diff --git a/source/game/Editable.cpp b/source/game/Editable.cpp
index d9484c6714bbd3ddf3e66f29daa70a44428a01ba..dc3239c24b6876ba31a7ec1cd237fc0ba6c81dd1 100644
--- a/source/game/Editable.cpp
+++ b/source/game/Editable.cpp
@@ -110,14 +110,15 @@ void CEditable::onSelectionChanged(CEditorObject *pObject)
 
 bool XMETHODCALLTYPE CEditable::canUseModel(const char *szClass)
 {
-	IEntityFactory *pFactory = CEntityFactoryMap::GetInstance()->getFactory(szClass);
+	return(getClassKV(szClass, "model_field") != NULL);
+}
+
+const char* XMETHODCALLTYPE CEditable::getClassKV(const char *szClassName, const char *szKey)
+{
+	IEntityFactory *pFactory = CEntityFactoryMap::GetInstance()->getFactory(szClassName);
 	if(pFactory)
 	{
-		const char *szModelField = pFactory->getKV("model_field");
-		if(szModelField)
-		{
-			return(true);
-		}
+		return(pFactory->getKV(szKey));
 	}
-	return(false);
+	return(NULL);
 }
diff --git a/source/game/Editable.h b/source/game/Editable.h
index 3db86f3e571426fb60a88cd3b09ed1836d3342f3..b2f66e35549c4af59d1f7c199a496c3d6519261d 100644
--- a/source/game/Editable.h
+++ b/source/game/Editable.h
@@ -80,6 +80,8 @@ public:
 		return(m_pDevice);
 	}
 
+	const char* XMETHODCALLTYPE getClassKV(const char *szClassName, const char *szKey) override;
+
 protected:
 	IGXDevice *m_pDevice = NULL;
 	IXCore *m_pCore = NULL;
diff --git a/source/game/EditorObject.cpp b/source/game/EditorObject.cpp
index 944656b71a186737cba9e37b9ab212f9c68ef0af..83da6cb47cfffe04f57985332e1ca7ddc2dd1e01 100644
--- a/source/game/EditorObject.cpp
+++ b/source/game/EditorObject.cpp
@@ -71,6 +71,8 @@ void CEditorObject::_iniFieldList()
 				&& pField->editor.type != PDE_NONE
 				)
 			{
+				xField.pEditorData = NULL;
+
 				switch(pField->editor.type)
 				{
 				case PDE_COMBOBOX:
@@ -79,27 +81,30 @@ void CEditorObject::_iniFieldList()
 					break;
 				case PDE_FILEFIELD:
 					xField.editorType = XPET_FILE;
-					xField.pEditorData = NULL;
 					{
-						editor_kv *pKV = (editor_kv*)pField->editor.pData;
-						if(pKV && pKV[0].value)
+						kv_t *pKV = (kv_t*)pField->editor.pData;
+						if(pKV && pKV[0].szValue)
 						{
-							if(!fstrcmp(pKV[0].value, "dse"))
+							if(!fstrcmp(pKV[0].szValue, "dse"))
 							{
 								xField.pEditorData = "model";
 							}
-							else if(!fstrcmp(pKV[0].value, "ogg"))
+							else if(!fstrcmp(pKV[0].szValue, "ogg"))
 							{
 								xField.pEditorData = "sound";
 							}
-							else if(!fstrcmp(pKV[0].value, "dds"))
+							else if(!fstrcmp(pKV[0].szValue, "dds"))
 							{
 								xField.pEditorData = "texture";
 							}
-							else if(!fstrcmp(pKV[0].value, "eff"))
+							else if(!fstrcmp(pKV[0].szValue, "eff"))
 							{
 								xField.pEditorData = "effect";
 							}
+							else if(!fstrcmp(pKV[0].szValue, "mtl"))
+							{
+								xField.pEditorData = "material";
+							}
 						}
 						//xField.pEditorData
 					}
@@ -119,6 +124,7 @@ void CEditorObject::_iniFieldList()
 					break;
 				case PDE_POINTCOORD:
 					xField.editorType = XPET_POINTCOORD;
+					xField.pEditorData = pField->editor.pData;
 					break;
 				case PDE_RADIUS:
 					xField.editorType = XPET_RADIUS;
@@ -129,6 +135,9 @@ void CEditorObject::_iniFieldList()
 				case PDE_HDRCOLOR:
 					xField.editorType = XPET_HDRCOLOR;
 					break;
+				case PDE_SIZE:
+					xField.editorType = XPET_SIZE;
+					break;
 				}
 				xField.szHelp = "";
 				xField.szKey = pField->szKey;
@@ -218,8 +227,8 @@ void CEditorObject::setPos(const float3_t &pos, bool isSeparate)
 
 void XMETHODCALLTYPE CEditorObject::setSize(const float3_t &vSize)
 {
-	// TODO Implement me
 	//m_vScale = vScale;
+	m_pEntity->setSize(vSize);
 }
 
 /*void CEditorObject::setScale(const float3_t &vScale, bool isSeparate)
diff --git a/source/game/EntityFactory.h b/source/game/EntityFactory.h
index 46ff5c91c3305bdbaa86b8b6eeeedd4507ae70c3..1ec3331aa9c0e3a792d98f708298c4b0b350f746 100644
--- a/source/game/EntityFactory.h
+++ b/source/game/EntityFactory.h
@@ -203,6 +203,9 @@ private:
 #define REC_ICON(s) REC_KV("icon", s)
 #define REC_MODEL(s) REC_KV("model", s)
 #define REC_MODEL_FIELD(s) REC_KV("model_field", s)
+#define REC_NO_HEIGHT_ADJUST() REC_KV("no_height_adj", "1")
+#define REC_ALIGN_WITH_NORMAL(axis) REC_KV("align_with_norm", axis)
+#define REC_MATERIAL_FIELD(s) REC_KV("material_field", s)
 
 #define REGISTER_ENTITY(cls, name, ...) \
 	CEntityFactory<cls> ent_ ## name ## _factory(#name, {__VA_ARGS__})
diff --git a/source/game/EntityManager.cpp b/source/game/EntityManager.cpp
index f7bdfed35d1ef3e3676bd0e6d9c6ab1ce8179b1f..f7a14bf636cc0f33f3d042888fdb676cea6dac76 100644
--- a/source/game/EntityManager.cpp
+++ b/source/game/EntityManager.cpp
@@ -400,14 +400,14 @@ bool CEntityManager::exportList(const char * file)
 	int ic = 0;
 
 	//if(m_isOldImported)
-	{
+	/*{
 		FILE *fp = fopen(file, "w");
 		if(fp)
 		{
 			fclose(fp);
 		}
 		m_isOldImported = false;
-	}
+	}*/
 
 	// conf->set("meta", "count", "0");
 
@@ -463,13 +463,7 @@ bool CEntityManager::import(const char * file, bool shouldSendProgress)
 	CBaseEntity *pEnt = NULL;
 	Array<CBaseEntity*> tmpList;
 
-	char szFullPath[1024];
-	if(!Core_GetIXCore()->getFileSystem()->resolvePath(file, szFullPath, sizeof(szFullPath)))
-	{
-		goto err;
-	}
-
-	if(conf->open(szFullPath))
+	if(conf->open(file))
 	{
 		goto err;
 	}
diff --git a/source/game/GameData.cpp b/source/game/GameData.cpp
index a759bab3943e33f7e5b6181e9951d5975667c0d7..1f6d4ed04c74d4e943320d065d60fc55a9e820e5 100644
--- a/source/game/GameData.cpp
+++ b/source/game/GameData.cpp
@@ -81,6 +81,7 @@ static IXPhysics *g_pPhysics = NULL;
 static IXPhysicsWorld *g_pPhysWorld = NULL;
 static IXRender *g_pRender = NULL;
 static IXParticleSystem *g_pParticleSystem = NULL;
+static IXDecalProvider *g_pDecalProvider = NULL;
 
 //##########################################################################
 
@@ -114,6 +115,10 @@ IXParticleSystem* GetParticleSystem()
 {
 	return(g_pParticleSystem);
 }
+IXDecalProvider* GetDecalProvider()
+{
+	return(g_pDecalProvider);
+}
 
 //##########################################################################
 
@@ -342,9 +347,12 @@ void XMETHODCALLTYPE CLevelLoadTask::exec()
 	evLevel.szLevelName = m_szLevelName;
 
 	g_levelProgressListener.onLoadBegin();
-	
+
 	evLevel.type = XEventLevel::TYPE_LOAD;
 	g_pLevelChannel->broadcastEvent(&evLevel);
+
+	evLevel.type = XEventLevel::TYPE_LOAD_WAIT_ASYNC_TASKS;
+	g_pLevelChannel->broadcastEvent(&evLevel);
 }
 
 void XMETHODCALLTYPE CLevelLoadTask::onFinished()
@@ -489,6 +497,7 @@ GameData::GameData(HWND hWnd, bool isGame):
 	g_pPhysics = (IXPhysics*)Core_GetIXCore()->getPluginManager()->getInterface(IXPHYSICS_GUID);
 	g_pPhysWorld = g_pPhysics->getWorld();
 	g_pParticleSystem = (IXParticleSystem*)Core_GetIXCore()->getPluginManager()->getInterface(IXPARTICLESYSTEM_GUID);
+	g_pDecalProvider = (IXDecalProvider*)Core_GetIXCore()->getPluginManager()->getInterface(IXDECALPROVIDER_GUID);
 
 	if(m_pLightSystem && false)
 	{
diff --git a/source/game/InfoOverlay.cpp b/source/game/InfoOverlay.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d534185547e5ba48ac82dacbb5350993b783db45
--- /dev/null
+++ b/source/game/InfoOverlay.cpp
@@ -0,0 +1,372 @@
+#include "InfoOverlay.h"
+
+BEGIN_PROPTABLE(CInfoOverlay)
+	//! Значение по умолчанию
+	DEFINE_FIELD_STRINGFN(m_szMaterial, PDFF_NONE, "material", "Overlay material", setMaterial, EDITOR_MATERIAL)
+
+	//! Высота объема декали
+	DEFINE_FIELD_FLOATFN(m_fHeight, PDFF_NONE, "height", "Height", setHeight, EDITOR_TEXTFIELD)
+
+	//! Угол 1
+	DEFINE_FIELD_VECTORFN(m_vCorner0, PDFF_USE_GIZMO, "point0", "Point 1", setCorner0, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "z")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+	//! Угол 2
+	DEFINE_FIELD_VECTORFN(m_vCorner1, PDFF_USE_GIZMO, "point1", "Point 2", setCorner1, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "z")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+	//! Угол 3
+	DEFINE_FIELD_VECTORFN(m_vCorner2, PDFF_USE_GIZMO, "point2", "Point 3", setCorner2, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "z")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+	//! Угол 4
+	DEFINE_FIELD_VECTORFN(m_vCorner3, PDFF_USE_GIZMO, "point3", "Point 4", setCorner3, EDITOR_POINTCOORDEX)
+		EDITOR_KV("lock", "plane")
+		EDITOR_KV("axis", "z")
+		EDITOR_KV("local", "1")
+	EDITOR_END()
+
+	//! Разрешить перетекание на перпендикулярные поверхности
+	DEFINE_FIELD_BOOLFN(m_isLeakAllowed, PDFF_NONE, "allow_leak", "Allow leak to perpendicular surface", setLeakAllowed, EDITOR_YESNO)
+
+	//! Угол 4
+	//DEFINE_FIELD_VECTORFN(m_vSize, PDFF_USE_GIZMO, "size", "Size", setSize, EDITOR_SIZE)
+
+	//! Включает
+	//DEFINE_INPUT(turnOn, "turnOn", "Turn on", PDF_NONE)
+	//! Выключает
+	//DEFINE_INPUT(turnOff, "turnOff", "Turn off", PDF_NONE)
+	//! Переключает состояние
+	//DEFINE_INPUT(toggle, "toggle", "Toggle", PDF_NONE)
+
+	
+
+	//! Изначально выключена
+	//DEFINE_FLAG(MOVER_INITIALLY_DISABLED, "Start disabled")
+	//! Отключить автомонтирование (требуется нажать кнопку взаимодействия)
+	//DEFINE_FLAG(MOVER_NO_AUTOMOUNT, "Disable automount")
+END_PROPTABLE()
+
+REGISTER_ENTITY(CInfoOverlay, info_overlay, REC_NO_HEIGHT_ADJUST(), REC_ALIGN_WITH_NORMAL("z"), REC_MATERIAL_FIELD("material"));
+
+CInfoOverlay::CInfoOverlay()
+{
+	float2_t vSize(1.0f, 1.0f);
+
+	float2_t sBound(-vSize.x * 0.5f, vSize.x * 0.5f);
+	float2_t tBound(-vSize.y * 0.5f, vSize.y * 0.5f);
+
+	m_vCorner0 = float3_t(sBound.x, tBound.x, 0.0f);
+	m_vCorner1 = float3_t(sBound.x, tBound.y, 0.0f);
+	m_vCorner2 = float3_t(sBound.y, tBound.y, 0.0f);
+	m_vCorner3 = float3_t(sBound.y, tBound.x, 0.0f);
+
+	m_fHeight = min(vSize.x, vSize.y) * 0.25f;
+
+	GetDecalProvider()->newDecal(&m_pDecal);
+
+	m_pDecal->setHeight(m_fHeight);
+	
+	m_pDecal->setCorner(0, float2(m_vCorner0));
+	m_pDecal->setCorner(1, float2(m_vCorner1));
+	m_pDecal->setCorner(2, float2(m_vCorner2));
+	m_pDecal->setCorner(3, float2(m_vCorner3));
+
+	updateSize();
+}
+
+CInfoOverlay::~CInfoOverlay()
+{
+	mem_release(m_pDecal);
+}
+
+void CInfoOverlay::setCorner0(const float3 &v)
+{
+	m_vCorner0 = v;
+	m_pDecal->setCorner(0, float2(v));
+	updateSize();
+}
+void CInfoOverlay::setCorner1(const float3 &v)
+{
+	m_vCorner1 = v;
+	m_pDecal->setCorner(1, float2(v));
+	updateSize();
+}
+void CInfoOverlay::setCorner2(const float3 &v)
+{
+	m_vCorner2 = v;
+	m_pDecal->setCorner(2, float2(v));
+	updateSize();
+}
+void CInfoOverlay::setCorner3(const float3 &v)
+{
+	m_vCorner3 = v;
+	m_pDecal->setCorner(3, float2(v));
+	updateSize();
+}
+
+void CInfoOverlay::setPos(const float3 &vPos)
+{
+	BaseClass::setPos(vPos);
+
+	m_pDecal->setPosition(vPos);
+}
+
+void CInfoOverlay::setOrient(const SMQuaternion &qRot)
+{
+	BaseClass::setOrient(qRot);
+
+	m_pDecal->setOrientation(qRot);
+}
+
+void CInfoOverlay::setMaterial(const char *szValue)
+{
+	_setStrVal(&m_szMaterial, szValue);
+
+	m_pDecal->setMaterial(szValue);
+}
+
+void CInfoOverlay::setHeight(float fHeight)
+{
+	m_fHeight = fHeight;
+	m_pDecal->setHeight(fHeight);
+}
+
+void CInfoOverlay::renderEditor(bool is3D, bool bRenderSelection, IXGizmoRenderer *pRenderer)
+{
+	if(pRenderer)
+	{
+		pRenderer->setLineWidth(is3D ? 0.01f : 1.0f);
+
+		pRenderer->setColor(float4(1.0f, 0.0f, 0.0f, 1.0f));
+		pRenderer->jumpTo(getPos());
+		pRenderer->lineTo(getPos() + getOrient() * float3(0.1f, 0.0f, 0.0f));
+
+		pRenderer->setColor(float4(0.0f, 1.0f, 0.0f, 1.0f));
+		pRenderer->jumpTo(getPos());
+		pRenderer->lineTo(getPos() + getOrient() * float3(0.0f, 0.1f, 0.0f));
+
+		pRenderer->setColor(float4(0.0f, 0.0f, 1.0f, 1.0f));
+		pRenderer->jumpTo(getPos());
+		pRenderer->lineTo(getPos() + getOrient() * float3(0.0f, 0.0f, 0.2f));
+
+		if(bRenderSelection || !is3D)
+		{
+			pRenderer->setLineWidth(is3D ? 0.005f : 1.0f);
+			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);
+
+			float3 vS = getOrient() * float3(1.0f, 0.0f, 0.0f);
+			float3 vT = getOrient() * float3(0.0f, 1.0f, 0.0f);
+			float3 vN = getOrient() * float3(0.0f, 0.0f, 1.0f);
+
+			float3_t vPt0 = getPos() + m_vCorner0.x * vS + m_vCorner0.y * vT;
+			float3_t vPt1 = getPos() + m_vCorner1.x * vS + m_vCorner1.y * vT;
+			float3_t vPt2 = getPos() + m_vCorner2.x * vS + m_vCorner2.y * vT;
+			float3_t vPt3 = getPos() + m_vCorner3.x * vS + m_vCorner3.y * vT;
+
+			float3 vTopOffset = vN * m_fHeight;
+
+			pRenderer->jumpTo(vPt0 - vTopOffset);
+			pRenderer->lineTo(vPt1 - vTopOffset);
+			pRenderer->lineTo(vPt2 - vTopOffset);
+			pRenderer->lineTo(vPt3 - vTopOffset);
+			pRenderer->lineTo(vPt0 - vTopOffset);
+			pRenderer->lineTo(vPt0 + vTopOffset);
+			pRenderer->lineTo(vPt1 + vTopOffset);
+			pRenderer->lineTo(vPt2 + vTopOffset);
+			pRenderer->lineTo(vPt3 + vTopOffset);
+			pRenderer->lineTo(vPt0 + vTopOffset);
+
+			pRenderer->jumpTo(vPt1 - vTopOffset);
+			pRenderer->lineTo(vPt1 + vTopOffset);
+
+			pRenderer->jumpTo(vPt2 - vTopOffset);
+			pRenderer->lineTo(vPt2 + vTopOffset);
+
+			pRenderer->jumpTo(vPt3 - vTopOffset);
+			pRenderer->lineTo(vPt3 + vTopOffset);
+		}
+	}
+}
+
+void CInfoOverlay::getMinMax(float3 *min, float3 *max)
+{
+	float3 vS = getOrient() * float3(1.0f, 0.0f, 0.0f);
+	float3 vT = getOrient() * float3(0.0f, 1.0f, 0.0f);
+	float3 vN = getOrient() * float3(0.0f, 0.0f, 1.0f);
+
+	float3 vTopOffset = vN * m_fHeight;
+
+	float3_t vPt0 = m_vCorner0.x * vS + m_vCorner0.y * vT;
+	float3_t vPt1 = m_vCorner1.x * vS + m_vCorner1.y * vT;
+	float3_t vPt2 = m_vCorner2.x * vS + m_vCorner2.y * vT;
+	float3_t vPt3 = m_vCorner3.x * vS + m_vCorner3.y * vT;
+
+	float3_t vPt10 = vPt0 + vTopOffset;
+	float3_t vPt11 = vPt1 + vTopOffset;
+	float3_t vPt12 = vPt2 + vTopOffset;
+	float3_t vPt13 = vPt3 + vTopOffset;
+
+	vPt0 = vPt0 - vTopOffset;
+	vPt1 = vPt1 - vTopOffset;
+	vPt2 = vPt2 - vTopOffset;
+	vPt3 = vPt3 - vTopOffset;
+
+	if(min)
+	{
+		*min = SMVectorMin(
+			SMVectorMin(SMVectorMin(vPt0, vPt1), SMVectorMin(vPt2, vPt3)),
+			SMVectorMin(SMVectorMin(vPt10, vPt11), SMVectorMin(vPt12, vPt13))
+		);
+	}
+
+	if(max)
+	{
+		*max = SMVectorMax(
+			SMVectorMax(SMVectorMax(vPt0, vPt1), SMVectorMax(vPt2, vPt3)),
+			SMVectorMax(SMVectorMax(vPt10, vPt11), SMVectorMax(vPt12, vPt13))
+		);
+	}
+}
+
+void CInfoOverlay::setSize(const float3_t &vSize)
+{
+	float3 vMin, vMax;
+	getMinMax(&vMin, &vMax);
+	BaseClass::setSize(vSize);
+
+	float3 vRelativeSize = vSize / (vMax - vMin);
+
+	float3 vS = getOrient() * float3(1.0f, 0.0f, 0.0f);
+	float3 vT = getOrient() * float3(0.0f, 1.0f, 0.0f);
+	float3 vN = getOrient() * float3(0.0f, 0.0f, 1.0f);
+
+	float3_t vPt0 = (m_vCorner0.x * vS + m_vCorner0.y * vT) * vRelativeSize;
+	float3_t vPt1 = (m_vCorner1.x * vS + m_vCorner1.y * vT) * vRelativeSize;
+	float3_t vPt2 = (m_vCorner2.x * vS + m_vCorner2.y * vT) * vRelativeSize;
+	float3_t vPt3 = (m_vCorner3.x * vS + m_vCorner3.y * vT) * vRelativeSize;
+
+	m_vCorner0 = float3_t(SMVector3Dot(vPt0, vS), SMVector3Dot(vPt0, vT), 0.0f);
+	m_vCorner1 = float3_t(SMVector3Dot(vPt1, vS), SMVector3Dot(vPt1, vT), 0.0f);
+	m_vCorner2 = float3_t(SMVector3Dot(vPt2, vS), SMVector3Dot(vPt2, vT), 0.0f);
+	m_vCorner3 = float3_t(SMVector3Dot(vPt3, vS), SMVector3Dot(vPt3, vT), 0.0f);
+
+	m_pDecal->setCorner(0, float2(m_vCorner0));
+	m_pDecal->setCorner(1, float2(m_vCorner1));
+	m_pDecal->setCorner(2, float2(m_vCorner2));
+	m_pDecal->setCorner(3, float2(m_vCorner3));
+
+	m_fHeight *= SMVector3Dot(vN, vRelativeSize);
+	m_pDecal->setHeight(m_fHeight);
+
+	updateSize();
+}
+
+bool CInfoOverlay::rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut, float3 *pvNormal, bool isRayInWorldSpace, bool bReturnNearestPoint)
+{
+	if(!isRayInWorldSpace)
+	{
+		float3 vPos = getPos();
+		SMQuaternion qRot = getOrient().Conjugate();
+
+		float3 vRayStart = qRot * vStart - vPos;
+		float3 vRayEnd = qRot * vEnd - vPos;
+		return(m_pDecal->rayTest(vRayStart, vRayEnd, pvOut, pvNormal));
+	}
+
+	return(m_pDecal->rayTest(vStart, vEnd, pvOut, pvNormal));
+}
+
+void CInfoOverlay::updateSize()
+{
+	float3 vMin = SMVectorMin(SMVectorMin(m_vCorner0, m_vCorner1), SMVectorMin(m_vCorner2, m_vCorner3));
+	float3 vMax = SMVectorMax(SMVectorMax(m_vCorner0, m_vCorner1), SMVectorMax(m_vCorner2, m_vCorner3));
+
+	m_vSize = vMax - vMin;
+	m_vSize.z = m_fHeight * 2.0f;
+}
+
+void CInfoOverlay::setSize(const float3 &v)
+{
+}
+
+void CInfoOverlay::setLeakAllowed(bool yesNo)
+{
+	m_isLeakAllowed = yesNo;
+	m_pDecal->setLeakAllowed(yesNo);
+}
+
+#if 0
+
+void CInfoOverlay::updateFlags()
+{
+	BaseClass::updateFlags();
+
+	if(getFlags() & MOVER_INITIALLY_DISABLED)
+	{
+		disable();
+	}
+	else
+	{
+		enable();
+	}
+}
+
+void CInfoOverlay::turnOn(inputdata_t *pInputdata)
+{
+	enable();
+}
+
+void CInfoOverlay::turnOff(inputdata_t *pInputdata)
+{
+	disable();
+}
+
+void CInfoOverlay::toggle(inputdata_t *pInputdata)
+{
+	if(m_isEnabled)
+	{
+		turnOff(pInputdata);
+	}
+	else
+	{
+		turnOn(pInputdata);
+	}
+}
+
+
+
+void CInfoOverlay::enable()
+{
+	if(!m_isEnabled)
+	{
+		if(m_pGhostObject)
+		{
+			GetPhysWorld()->addCollisionObject(m_pGhostObject, CG_LADDER, CG_CHARACTER);
+		}
+		m_pTickEventChannel->addListener(&m_physicsTicker);
+		m_isEnabled = true;
+	}
+}
+
+void CInfoOverlay::disable()
+{
+	if(m_isEnabled)
+	{
+		if(m_pGhostObject)
+		{
+			GetPhysWorld()->removeCollisionObject(m_pGhostObject);
+		}
+		m_pTickEventChannel->removeListener(&m_physicsTicker);
+		m_isEnabled = false;
+	}
+}
+
+#endif
diff --git a/source/game/InfoOverlay.h b/source/game/InfoOverlay.h
new file mode 100644
index 0000000000000000000000000000000000000000..ca46e39c52d3ffeb2fe6891bd5113822b7fa8764
--- /dev/null
+++ b/source/game/InfoOverlay.h
@@ -0,0 +1,71 @@
+#ifndef __INFO_OVERLAY_H
+#define __INFO_OVERLAY_H
+
+#include "PointEntity.h"
+
+//#define MOVER_INITIALLY_DISABLED ENT_FLAG_0
+//#define MOVER_NO_AUTOMOUNT ENT_FLAG_1
+
+class CInfoOverlay: public CPointEntity
+{
+	DECLARE_CLASS(CInfoOverlay, CPointEntity);
+	DECLARE_PROPTABLE();
+public:
+	DECLARE_CONSTRUCTOR();
+	~CInfoOverlay();
+
+	void setPos(const float3 &vPos) override;
+	void setOrient(const SMQuaternion &qRot) override;
+
+	void renderEditor(bool is3D, bool bRenderSelection, IXGizmoRenderer *pRenderer) override;
+
+	void getMinMax(float3 *min, float3 *max) override;
+
+	
+
+	bool rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut = NULL, float3 *pvNormal = NULL, bool isRayInWorldSpace = true, bool bReturnNearestPoint = false) override;
+
+	void setHeight(float fHeight);
+	void setMaterial(const char *szValue);
+
+	void setSize(const float3_t &vSize) override;
+
+	void setLeakAllowed(bool yesNo);
+
+private:
+	void setCorner0(const float3 &v);
+	void setCorner1(const float3 &v);
+	void setCorner2(const float3 &v);
+	void setCorner3(const float3 &v);
+
+	void setSize(const float3 &v);
+	//void updateFlags() override;
+
+	//void turnOn(inputdata_t *pInputdata);
+	//void turnOff(inputdata_t *pInputdata);
+	//void toggle(inputdata_t *pInputdata);
+	//void enable();
+	//void disable();
+
+	//SMAABB getBound();
+	void updateSize();
+
+private:
+	IXDecal *m_pDecal = NULL;
+	const char *m_szMaterial = NULL;
+
+	float3_t m_vCorner0;
+	float3_t m_vCorner1;
+	float3_t m_vCorner2;
+	float3_t m_vCorner3;
+	
+	float m_fHeight = 0.1f;
+
+	float3_t m_vSize;
+
+	bool m_isLeakAllowed = true;
+
+	//bool m_isEnabled = false;
+};
+
+#endif
diff --git a/source/game/ZombieHands.cpp b/source/game/ZombieHands.cpp
index 19b7df3658635fc40ebad62f6021db8467b8a3ec..4e8e4f1887143866e16efce5465d843585c807ef 100644
--- a/source/game/ZombieHands.cpp
+++ b/source/game/ZombieHands.cpp
@@ -1,7 +1,6 @@
 
 #include "ZombieHands.h"
 #include "BaseCharacter.h"
-#include <decals/sxdecals.h>
 #include "Tracer.h"
 
 /*! \skydocent wpn_zombie_hands
@@ -52,7 +51,7 @@ void CZombieHands::actualShoot(float dt)
 
 	if(cb.hasHit())
 	{
-		SXDecals_ShootDecal(DECAL_TYPE_BLOOD_BIG, BTVEC_F3(cb.m_hitPointWorld), BTVEC_F3(cb.m_hitNormalWorld));
+		SAFE_CALL(GetDecalProvider(), shootDecal, XDT_BLOOD_BIG, cb.m_result.vHitPoint, cb.m_result.vHitNormal);
 
 		if(cb.m_result.pCollisionObject->getUserPointer() && cb.m_result.pCollisionObject->getUserTypeId() == 1)
 		{
diff --git a/source/game/proptable.cpp b/source/game/proptable.cpp
index 619cba20ad883d8889af3b7296df202deaa73c98..57723b117e69977e564bfb0a58cf06182a0c2dec 100644
--- a/source/game/proptable.cpp
+++ b/source/game/proptable.cpp
@@ -11,14 +11,14 @@ See the license in LICENSE
 
 #include "BaseEntity.h"
 
-prop_editor_t _GetEditorCombobox(int start, ...)
+prop_editor_t _GetEditor(PDE_TYPE type, ...)
 {
 	prop_editor_t out;
-	out.type = PDE_COMBOBOX;
-	editor_kv kvs[ED_COMBO_MAXELS];
+	out.type = type;
+	kv_t kvs[ED_COMBO_MAXELS];
 
 	va_list va;
-	va_start(va, start);
+	va_start(va, type);
 
 	const char * el;
 	int i = 0;
@@ -28,56 +28,21 @@ prop_editor_t _GetEditorCombobox(int start, ...)
 		{
 			break;
 		}
-		kvs[i].key = el;
+		kvs[i].szKey = el;
 		if(!(el = va_arg(va, const char *)))
 		{
 			break;
 		}
-		kvs[i].value = el;
+		kvs[i].szValue = el;
 		++i;
 	}
-	kvs[i].value = kvs[i].key = 0;
+	kvs[i].szValue = kvs[i].szKey = 0;
 	++i;
 
 	va_end(va);
 
-	out.pData = new editor_kv[i];
-	memcpy(out.pData, kvs, sizeof(editor_kv)* i);
-	return(out);
-}
-
-prop_editor_t _GetEditorFilefield(int start, ...)
-{
-	prop_editor_t out;
-	out.type = PDE_FILEFIELD;
-	editor_kv kvs[ED_COMBO_MAXELS];
-
-	va_list va;
-	va_start(va, start);
-
-	const char * el;
-	int i = 0;
-	while(i < ED_COMBO_MAXELS)
-	{
-		if(!(el = va_arg(va, const char *)))
-		{
-			break;
-		}
-		kvs[i].key = el;
-		if(!(el = va_arg(va, const char *)))
-		{
-			break;
-		}
-		kvs[i].value = el;
-		++i;
-	}
-	kvs[i].value = kvs[i].key = 0;
-	++i;
-
-	va_end(va);
-
-	out.pData = new editor_kv[i];
-	memcpy(out.pData, kvs, sizeof(editor_kv)* i);
+	out.pData = new kv_t[i];
+	memcpy(out.pData, kvs, sizeof(kv_t)* i);
 	return(out);
 }
 
diff --git a/source/game/proptable.h b/source/game/proptable.h
index b73c430f918522042ca208b713ced369059cd7f1..3f97ecba76de87cf3078cd626635b3999668dc39 100644
--- a/source/game/proptable.h
+++ b/source/game/proptable.h
@@ -74,7 +74,8 @@ enum PDE_TYPE
 	PDE_POINTCOORD,
 	PDE_RADIUS,
 	PDE_COLOR,
-	PDE_HDRCOLOR
+	PDE_HDRCOLOR,
+	PDE_SIZE
 };
 
 enum PDF_FLAG
@@ -170,12 +171,6 @@ union PFNFIELDSET
 	int __;
 };
 
-struct editor_kv
-{
-	const char * key;
-	const char * value;
-};
-
 struct prop_editor_t
 {
 	PDE_TYPE type;
@@ -367,8 +362,7 @@ struct proptable_t
 
 #define ED_COMBO_MAXELS 256
 
-prop_editor_t _GetEditorCombobox(int start, ...);
-prop_editor_t _GetEditorFilefield(int start, ...);
+prop_editor_t _GetEditor(PDE_TYPE type, ...);
 
 #define DECLARE_PROPTABLE() \
 	\
@@ -454,6 +448,10 @@ void cls::InitPropData() \
 
 const char * GetEmptyString();
 
+#define EDITOR_BEGIN(type) _GetEditor(type
+#define EDITOR_KV(name, value) , name, value
+#define EDITOR_END() , NULL)}
+
 #define EDITOR_NONE {PDE_NONE, NULL}}
 #define EDITOR_TEXTFIELD {PDE_TEXTFIELD, NULL}}
 #define EDITOR_TIMEFIELD {PDE_TIME, NULL}}
@@ -463,19 +461,24 @@ const char * GetEmptyString();
 #define EDITOR_FLAGS {PDE_FLAGS, NULL}}
 #define EDITOR_COLOR {PDE_COLOR, NULL}}
 #define EDITOR_HDRCOLOR {PDE_HDRCOLOR, NULL}}
+#define EDITOR_SIZE {PDE_SIZE, NULL}}
+
+#define EDITOR_POINTCOORDEX EDITOR_BEGIN(PDE_POINTCOORD)
+
 
-#define EDITOR_COMBOBOX _GetEditorCombobox(0
-#define COMBO_OPTION(name, value) , name, value
-#define EDITOR_COMBO_END() , NULL)}
+#define EDITOR_COMBOBOX EDITOR_BEGIN(PDE_COMBOBOX)
+#define COMBO_OPTION(name, value) EDITOR_KV(name, value)
+#define EDITOR_COMBO_END() EDITOR_END()
 
-#define EDITOR_FILEFIELD _GetEditorFilefield(0
-#define FILE_OPTION(name, value) , name, value
-#define EDITOR_FILE_END() , NULL)}
+#define EDITOR_FILEFIELD EDITOR_BEGIN(PDE_FILEFIELD)
+#define FILE_OPTION(name, value) EDITOR_KV(name, value)
+#define EDITOR_FILE_END() EDITOR_END()
 
 #define EDITOR_YESNO EDITOR_COMBOBOX COMBO_OPTION("Yes", "1") COMBO_OPTION("No", "0") EDITOR_COMBO_END()
 #define EDITOR_MODEL EDITOR_FILEFIELD FILE_OPTION("Select model", "dse") EDITOR_FILE_END()
 #define EDITOR_SOUND EDITOR_FILEFIELD FILE_OPTION("Select sound", "ogg") EDITOR_FILE_END()
 #define EDITOR_TEXTURE EDITOR_FILEFIELD FILE_OPTION("Select texture", "dds") EDITOR_FILE_END()
+#define EDITOR_MATERIAL EDITOR_FILEFIELD FILE_OPTION("Select material", "mtl") EDITOR_FILE_END()
 #define EDITOR_EFFECT EDITOR_FILEFIELD FILE_OPTION("Select effect", "eff") EDITOR_FILE_END()
 
 #define DEFINE_FIELD_STRING(field, flags, keyname, edname, editor)              , {propdata_t::ToFieldType<const char* DataClass::*>(&DataClass::field),          PDF_STRING,  flags, keyname, edname, editor
diff --git a/source/gdefines.h b/source/gdefines.h
index 30f4bac0d425986a774eff01ce5cf0072086fefc..bc8b37e1f5df43181052d80037318ab599bb27cd 100644
--- a/source/gdefines.h
+++ b/source/gdefines.h
@@ -221,6 +221,12 @@ typedef void(*report_func) (int iLevel, const char *szLibName, const char *szMes
 #define GET_Y_LPARAM(lp)                        ((int)(short)HIWORD(lp))
 #endif
 
+struct kv_t
+{
+	const char *szKey;
+	const char *szValue;
+};
+
 /** \name Уровни критичности сообщений для функции репортов */
 //! @{
 #define REPORT_MSG_LEVEL_NOTICE		0	/*!< заметка */
diff --git a/source/physics/PhyWorld.cpp b/source/physics/PhyWorld.cpp
index 0dd249c57282fc0b1fd30fa45d528f3a1bc7590a..c4eb6d186be5e804983271a66b263e81adc9e567 100644
--- a/source/physics/PhyWorld.cpp
+++ b/source/physics/PhyWorld.cpp
@@ -56,7 +56,7 @@ struct XRayResultCallback: public btCollisionWorld::RayResultCallback
 		m_hitPointWorld.setInterpolate3(m_rayFromWorld, m_rayToWorld, rayResult.m_hitFraction);
 
 		m_result.vHitPoint = BTVEC_F3(m_hitPointWorld);
-		m_result.vHitNormal = BTVEC_F3(m_hitPointWorld);
+		m_result.vHitNormal = BTVEC_F3(m_hitNormalWorld);
 		m_result.pCollisionObject = m_collisionObject->getUserIndex() == 2 ? (IXCollisionObject*)m_collisionObject->getUserPointer() : NULL;
 		m_result.fHitFraction = rayResult.m_hitFraction;
 
diff --git a/source/render/Scene.cpp b/source/render/Scene.cpp
index 42e5ad8a8b557b67fe6cb19cc90f7b3a5803fc4e..46d10cb9cc88b4586996f9a5299367e1b7c2cb09 100644
--- a/source/render/Scene.cpp
+++ b/source/render/Scene.cpp
@@ -1287,6 +1287,11 @@ public:
 		return(m_pEditorExt->getRenderer());
 	}
 
+	const char* XMETHODCALLTYPE getClassKV(const char *szClassName, const char *szKey) override
+	{
+		return(NULL);
+	}
+
 private:
 	CEditorExt *m_pEditorExt = NULL;
 };
@@ -1696,3 +1701,8 @@ void CScene::print()
 	m_pRootNode->print(0, &uNodesCount, &uObjectsCount, &uMaxDepth);
 	printf("Total objects: %u, Total nodes: %u, Max depth: %u, avg objects per node: %f\n", uObjectsCount, uNodesCount, uMaxDepth, (float)uObjectsCount / (float)uNodesCount);
 }
+
+bool CScene::hasPendingOps()
+{
+	return(!m_qUpdate.empty());
+}
diff --git a/source/render/Scene.h b/source/render/Scene.h
index 4d810b09aaf95bef82711d954347bdc953ae975b..7b42aeff747a16a0e22954db3f3ce7b83bcc62d2 100644
--- a/source/render/Scene.h
+++ b/source/render/Scene.h
@@ -275,6 +275,8 @@ public:
 	bool validate();
 	void print();
 
+	bool hasPendingOps();
+
 protected:
 	void addObject(CSceneObject *pObject);
 	void removeObject(CSceneObject *pObject);
diff --git a/source/render/plugin_main.cpp b/source/render/plugin_main.cpp
index 0e510908d5fba6132a52ba7642596c3f96e1a97a..01d6d674333e70fb40f671902f51f54515d336df 100644
--- a/source/render/plugin_main.cpp
+++ b/source/render/plugin_main.cpp
@@ -15,6 +15,33 @@
 #pragma comment(lib, "sxgame.lib")
 #endif
 
+class CLoadLevelEventListener final: public IEventListener<XEventLevel>
+{
+public:
+	CLoadLevelEventListener(CScene *pScene):
+		m_pScene(pScene)
+	{
+	}
+	void onEvent(const XEventLevel *pData)
+	{
+		switch(pData->type)
+		{
+		case XEventLevel::TYPE_LOAD_WAIT_ASYNC_TASKS:
+			while(m_pScene->hasPendingOps())
+			{
+				Sleep(100);
+			}
+			break;
+
+		default:
+			break;
+		}
+	}
+
+protected:
+	CScene *m_pScene;
+};
+
 class CRenderPlugin: public IXUnknownImplementation<IXPlugin>
 {
 public:
@@ -25,6 +52,10 @@ public:
 
 	void XMETHODCALLTYPE shutdown() override
 	{
+		if(m_pLevelLoadEventListener)
+		{
+			m_pCore->getEventChannel<XEventLevel>(EVENT_LEVEL_GUID)->removeListener(m_pLevelLoadEventListener);
+		}
 	}
 
 	UINT XMETHODCALLTYPE getInterfaceCount() override
@@ -68,6 +99,9 @@ public:
 			if(!m_pScene)
 			{
 				m_pScene = new CScene(m_pCore);
+				m_pLevelLoadEventListener = new CLoadLevelEventListener(m_pScene);
+
+				m_pCore->getEventChannel<XEventLevel>(EVENT_LEVEL_GUID)->addListener(m_pLevelLoadEventListener);
 			}
 			*ppOut = m_pScene;
 			break;
@@ -144,6 +178,7 @@ protected:
 	CScene *m_pScene = NULL;
 	CUpdatable *m_pUpdatable = NULL;
 	CRender *m_pRender = NULL;
+	CLoadLevelEventListener *m_pLevelLoadEventListener = NULL;
 };
 
 DECLARE_XPLUGIN(CRenderPlugin);
diff --git a/source/terrax/CommandBuildModel.cpp b/source/terrax/CommandBuildModel.cpp
index f3ff59d42f8b94b22cca96ca04f36814cf8e700e..30484344f531eb76436cf3aa900bb4d877df50d1 100644
--- a/source/terrax/CommandBuildModel.cpp
+++ b/source/terrax/CommandBuildModel.cpp
@@ -5,7 +5,7 @@ extern AssotiativeArray<AAString, IXEditable*> g_mEditableSystems;
 
 CCommandBuildModel::CCommandBuildModel(const char *szTypeName, const char *szClassName)
 {
-	m_pCommandCreate = new CCommandCreate(float3_t(), szTypeName, szClassName);
+	m_pCommandCreate = new CCommandCreate(float3_t(), float3_t(), szTypeName, szClassName);
 }
 
 CCommandBuildModel::~CCommandBuildModel()
diff --git a/source/terrax/CommandCreate.cpp b/source/terrax/CommandCreate.cpp
index 7dac8882c0c4ba1f15c956157d58bd30376f6040..df0279c186a20a1bd50bb06be8390cf9023337ec 100644
--- a/source/terrax/CommandCreate.cpp
+++ b/source/terrax/CommandCreate.cpp
@@ -3,15 +3,15 @@
 
 extern AssotiativeArray<AAString, IXEditable*> g_mEditableSystems;
 
-CCommandCreate::CCommandCreate(const float3_t &vPos, const char *szTypeName, const char *szClassName, bool useRandomScaleYaw)
+CCommandCreate::CCommandCreate(const float3_t &vPos, const float3_t &vNorm, const char *szTypeName, const char *szClassName, bool useRandomScaleYaw):
+	m_vPos(vPos),
+	m_vNorm(vNorm),
+	m_sClassName(szClassName)
 {
-	m_vPos = vPos;
-	m_sClassName = szClassName;
-
 	if(useRandomScaleYaw)
 	{
 		m_fScale = randf(0.7, 1.3);
-		m_qOrient = SMQuaternion(randf(0, SM_2PI), 'y');
+		m_fRotAngle = randf(0, SM_2PI);
 	}
 
 	const AssotiativeArray<AAString, IXEditable*>::Node *pNode;
@@ -42,10 +42,82 @@ bool XMETHODCALLTYPE CCommandCreate::exec()
 	m_pObject->setPos(m_vPos);
 	//! @TODO Implement random scale?
 	//m_pObject->setScale(float3(m_fScale));
-	m_pObject->setOrient(m_qOrient);
+	const char *szNormalAlign = m_pEditable->getClassKV(m_sClassName.c_str(), "align_with_norm");
+	if(!SMIsZero(SMVector3Length2(m_vNorm)) && szNormalAlign)
+	{
+		char cAxis = szNormalAlign[0];
+		float fVal = 1.0f;
+		if(cAxis == '-')
+		{
+			cAxis = szNormalAlign[1];
+			float fVal = -1.0f;
+		}
+		float3_t vFrom;
+		switch(tolower(cAxis))
+		{
+		case 'x':
+			vFrom.x = fVal;
+			break;
+		case 'y':
+			vFrom.y = fVal;
+			break;
+		case 'z':
+			vFrom.z = fVal;
+			break;
+		}
+		SMQuaternion qRot(vFrom, m_vNorm);
+		m_pObject->setOrient(qRot * SMQuaternion(m_vNorm, m_fRotAngle));
+	}
+	else
+	{
+		m_pObject->setOrient(SMQuaternion(m_fRotAngle, 'y'));
+	}
 	m_pObject->setSelected(true);
 	m_pObject->create();
 
+	if(!SMIsZero(SMVector3Length2(m_vNorm)) && !m_pEditable->getClassKV(m_sClassName.c_str(), "no_height_adj"))
+	{
+		float3 vMin, vMax;
+		m_pObject->getBound(&vMin, &vMax);
+
+		float3 avPoints[] = {
+			float3(vMin),
+			float3(vMin.x, vMin.y, vMax.z),
+			float3(vMin.x, vMax.y, vMin.z),
+			float3(vMin.x, vMax.y, vMax.z),
+			float3(vMax.x, vMin.y, vMin.z),
+			float3(vMax.x, vMin.y, vMax.z),
+			float3(vMax.x, vMax.y, vMin.z),
+			float3(vMax),
+		};
+		//printf("%.2f, %.2f, %.2f\n", vNorm.x, vNorm.y, vNorm.z);
+
+		float fProj = FLT_MAX;
+		for(UINT i = 0, l = ARRAYSIZE(avPoints); i < l; ++i)
+		{
+			float fTmp = SMVector3Dot(avPoints[i], m_vNorm);
+			//printf("%.2f\n", fTmp);
+			if(fTmp < fProj)
+			{
+				fProj = fTmp;
+			}
+		}
+
+		fProj = SMVector3Dot(m_vPos, m_vNorm) - fProj;
+		//printf("%.2f, %.2f, %.2f\n%.2f, %.2f, %.2f\n%.2f, %.2f, %.2f\n%f\n\n", vPos.x, vPos.y, vPos.z, vMin.x, vMin.y, vMin.z, vMax.x, vMax.y, vMax.z, fProj);
+		if(fProj > 0)
+		{
+			m_pObject->setPos((float3)(m_vPos + m_vNorm * fProj));
+		}
+	}
+
+	const char *szMaterialField = m_pEditable->getClassKV(m_sClassName.c_str(), "material_field");
+	if(szMaterialField)
+	{
+		m_sMaterial = g_pEditor->getMaterialBrowser()->getCurrentMaterial();
+		m_pObject->setKV(szMaterialField, m_sMaterial.c_str());
+	}
+
 	g_pEditor->addObject(m_pObject);
 
 	XUpdatePropWindow();
diff --git a/source/terrax/CommandCreate.h b/source/terrax/CommandCreate.h
index 7b72e90d6951e71d3c9341e4bf8d7b3caa57e828..ba0b86dee61cb72cf1a9bb2e7985b64f7ac614f0 100644
--- a/source/terrax/CommandCreate.h
+++ b/source/terrax/CommandCreate.h
@@ -11,7 +11,7 @@
 class CCommandCreate final: public IXUnknownImplementation<IXEditorCommand>
 {
 public:
-	CCommandCreate(const float3_t &vPos, const char *szTypeName, const char *szClassName, bool useRandomScaleYaw=false);
+	CCommandCreate(const float3_t &vPos, const float3_t &vNorm, const char *szTypeName, const char *szClassName, bool useRandomScaleYaw = false);
 	~CCommandCreate();
 
 	bool XMETHODCALLTYPE exec() override;
@@ -34,11 +34,13 @@ public:
 
 protected:
 	float3_t m_vPos;
-	SMQuaternion m_qOrient;
+	float3_t m_vNorm;
+	float m_fRotAngle = 0.0f;
 	float m_fScale = 1.0f;
 	String m_sClassName;
 	IXEditable *m_pEditable = NULL;
 	IXEditorObject *m_pObject = NULL;
+	String m_sMaterial;
 };
 
 #endif
diff --git a/source/terrax/Editor.cpp b/source/terrax/Editor.cpp
index e17b7ce2adc3b572aa9e263cca13c1cfda303392..51185b8270e370ecc459640a978b514bc7400f05 100644
--- a/source/terrax/Editor.cpp
+++ b/source/terrax/Editor.cpp
@@ -38,6 +38,8 @@ CEditor::CEditor(IXCore *pCore):
 	pCore->getPluginManager()->registerInterface(IXCOLORPICKER_GUID, &m_colorPicker);
 
 	m_pSceneTreeWindow = new CSceneTreeWindow(this, pCore);
+
+	registerResourceBrowser(new CResourceBrowser());
 }
 
 CEditor::~CEditor()
diff --git a/source/terrax/Editor.h b/source/terrax/Editor.h
index fb18b38d0b571fa69c8fce49d0453603c580b7ce..3f15126b872c664856d8b2da1629f4a8945e3cf8 100644
--- a/source/terrax/Editor.h
+++ b/source/terrax/Editor.h
@@ -15,6 +15,7 @@
 #include "ColorGradientEditorDialog.h"
 #include "ColorPicker.h"
 #include "SceneTreeWindow.h"
+#include "ResourceBrowser.h"
 
 #define GIZMO_TYPES() \
 	GTO(Handle)\
diff --git a/source/terrax/GroupObject.cpp b/source/terrax/GroupObject.cpp
index 5beb78e13a37a1a825bd2c00fd5406de316446e1..5fa1a1aa1a60ef5e00e2cd63c3326ec7ac12e930 100644
--- a/source/terrax/GroupObject.cpp
+++ b/source/terrax/GroupObject.cpp
@@ -303,7 +303,7 @@ void CGroupObject::addChildObject(IXEditorObject *pObject)
 
 	ICompoundObject *pOldContainer = XTakeObject(pObject, this);
 	assert(pOldContainer == NULL);
-	//add_ref(pObject);
+	add_ref(pObject);
 	m_aObjects.push_back({pObject, m_qOrient.Conjugate() * (pObject->getPos() - m_vPos), pObject->getOrient() * m_qOrient.Conjugate()});
 	
 	g_pEditor->onObjectAdded(pObject);
@@ -323,7 +323,7 @@ void CGroupObject::removeChildObject(IXEditorObject *pObject)
 
 		g_pEditor->onObjectRemoved(pObject);
 
-		//mem_release(pObject);
+		mem_release(pObject);
 
 		if(!m_aObjects.size())
 		{
diff --git a/source/terrax/PropertyWindow.cpp b/source/terrax/PropertyWindow.cpp
index 75896547e181e0092e1bd2b93c0af00c23960e1b..475f2471c523e876fa3eba1e55e7639d3c323cc2 100644
--- a/source/terrax/PropertyWindow.cpp
+++ b/source/terrax/PropertyWindow.cpp
@@ -63,6 +63,7 @@ static UINT g_uEditorDlgIds[] = {
 	IDD_PROPEDIT_TEXT,
 	IDD_PROPEDIT_TEXT,
 	IDD_PROPEDIT_TEXT,
+	IDD_PROPEDIT_TEXT,
 };
 
 static_assert(ARRAYSIZE(g_uEditorDlgIds) == XPET__LAST, "g_uEditorDlgIds must match X_PROP_EDITOR_TYPE");
@@ -819,7 +820,7 @@ void CPropertyWindow::initEditor(X_PROP_EDITOR_TYPE type, const void *pData, con
 		{
 			HWND hCombo = GetDlgItem(hEditorDlg, IDC_OPE_COMBO);
 			ComboBox_ResetContent(hCombo);
-			edt_kv *pKV = (edt_kv*)pData;
+			kv_t *pKV = (kv_t*)pData;
 			while(pKV->szKey)
 			{
 				ComboBox_AddString(hCombo, pKV->szKey);
diff --git a/source/terrax/PropertyWindow.h b/source/terrax/PropertyWindow.h
index 9047f29ce296a1cbb3b513e0ef1095741b25f932..2f8db9f8154cb4f20569f33891098f0abba36205 100644
--- a/source/terrax/PropertyWindow.h
+++ b/source/terrax/PropertyWindow.h
@@ -105,13 +105,7 @@ protected:
 		X_PROP_FIELD field;
 		String sValue;
 	};
-
-	struct edt_kv
-	{
-		const char *szKey;
-		const char *szValue;
-	};
-
+	
 	AssotiativeArray<AAString, prop_s> m_aPropFields;
 	X_PROP_EDITOR_TYPE m_editorActive = XPET__LAST;
 
diff --git a/source/terrax/ResourceBrowser.cpp b/source/terrax/ResourceBrowser.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5f0be58de59bbfd1f595898efd1bd4e7537348d6
--- /dev/null
+++ b/source/terrax/ResourceBrowser.cpp
@@ -0,0 +1,26 @@
+#include "ResourceBrowser.h"
+#include "terrax.h"
+
+UINT XMETHODCALLTYPE CResourceBrowser::getResourceTypeCount()
+{
+	return(1);
+}
+const char* XMETHODCALLTYPE CResourceBrowser::getResourceType(UINT uId)
+{
+	if(uId == 0)
+	{
+		return("material");
+	}
+
+	return(NULL);
+}
+
+void XMETHODCALLTYPE CResourceBrowser::browse(const char *szType, const char *szOldValue, IXEditorResourceBrowserCallback *pCallback)
+{
+	m_callback.init(pCallback);
+	g_pMaterialBrowser->browse(&m_callback);
+}
+void XMETHODCALLTYPE CResourceBrowser::cancel()
+{
+	g_pMaterialBrowser->abort();
+}
diff --git a/source/terrax/ResourceBrowser.h b/source/terrax/ResourceBrowser.h
new file mode 100644
index 0000000000000000000000000000000000000000..04b530571e612b34d2ab3904d5d6de3979039b22
--- /dev/null
+++ b/source/terrax/ResourceBrowser.h
@@ -0,0 +1,46 @@
+#ifndef __RESOURCEBROWSER_H
+#define __RESOURCEBROWSER_H
+
+#include <xcommon/editor/IXEditorExtension.h>
+#include "MaterialBrowser.h"
+
+class CResourceMaterialBrowserCallback: public IMaterialBrowserCallback
+{
+public:
+	void init(IXEditorResourceBrowserCallback *pCallback)
+	{
+		SAFE_CALL(m_pCallback, onCancelled);
+		m_pCallback = pCallback;
+	}
+	void onSelected(const char *szName) override
+	{
+		SAFE_CALL(m_pCallback, onSelected, szName);
+		m_pCallback = NULL;
+	}
+	void onCancel() override
+	{
+		SAFE_CALL(m_pCallback, onCancelled);
+		m_pCallback = NULL;
+	}
+
+
+private:
+	IXEditorResourceBrowserCallback *m_pCallback = NULL;
+};
+
+//#############################################################################
+
+class CResourceBrowser: public IXUnknownImplementation<IXEditorResourceBrowser>
+{
+public:
+	UINT XMETHODCALLTYPE getResourceTypeCount() override;
+	const char* XMETHODCALLTYPE getResourceType(UINT uId) override;
+
+	void XMETHODCALLTYPE browse(const char *szType, const char *szOldValue, IXEditorResourceBrowserCallback *pCallback) override;
+	void XMETHODCALLTYPE cancel() override;
+
+private:
+	CResourceMaterialBrowserCallback m_callback;
+};
+
+#endif
diff --git a/source/terrax/mainWindow.cpp b/source/terrax/mainWindow.cpp
index ab6b568737a1f8b14722bf9d522b926ad95de81d..763ed59dfcc6e89cb97f7dec07fc4299f6543749 100644
--- a/source/terrax/mainWindow.cpp
+++ b/source/terrax/mainWindow.cpp
@@ -972,7 +972,7 @@ guid = {9D7D2E62-24C7-42B7-8D83-8448FC4604F0}
 	mem_release(pConfig);
 }
 
-static CCommandCreate* CreateObjectAtPosition(const float3 &vPos, bool bDeselectAll)
+static CCommandCreate* CreateObjectAtPosition(const float3 &vPos, const float3 &vNorm, bool bDeselectAll)
 {
 	int iSel1 = ComboBox_GetCurSel(g_hComboTypesWnd);
 	int iLen1 = ComboBox_GetLBTextLen(g_hComboTypesWnd, iSel1);
@@ -989,7 +989,7 @@ static CCommandCreate* CreateObjectAtPosition(const float3 &vPos, bool bDeselect
 		SendMessage(g_hWndMain, WM_COMMAND, MAKEWPARAM(ID_EDIT_CLEARSELECTION, 0), (LPARAM)0);
 	}
 
-	CCommandCreate *pCmd = new CCommandCreate(vPos, szTypeName, szClassName, Button_GetCheck(g_hCheckboxRandomScaleYawWnd));
+	CCommandCreate *pCmd = new CCommandCreate(vPos, vNorm, szTypeName, szClassName, Button_GetCheck(g_hCheckboxRandomScaleYawWnd));
 	g_pUndoManager->execCommand(pCmd);
 
 	return(pCmd);
@@ -1947,7 +1947,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 		case IDC_CTRL_RETURN:
 			if(g_xState.bCreateMode)
 			{
-				CreateObjectAtPosition(g_xState.vCreateOrigin, GetKeyState(VK_CONTROL) >= 0);
+				CreateObjectAtPosition(g_xState.vCreateOrigin, float3(0.0f), GetKeyState(VK_CONTROL) >= 0);
 
 				g_xState.bCreateMode = false;
 			}
@@ -3285,40 +3285,8 @@ LRESULT CALLBACK RenderWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lP
 					vPos = s_aRaytracedItems[0].vPos;
 					vNorm = SMVector3Normalize(s_aRaytracedItems[0].vNorm);
 
-					IXEditorObject *pObj = CreateObjectAtPosition(vPos, GetKeyState(VK_CONTROL) >= 0)->getObject();
-					float3 vMin, vMax;
-					pObj->getBound(&vMin, &vMax);
-
-					float3 avPoints[] = {
-						float3(vMin),
-						float3(vMin.x, vMin.y, vMax.z),
-						float3(vMin.x, vMax.y, vMin.z),
-						float3(vMin.x, vMax.y, vMax.z),
-						float3(vMax.x, vMin.y, vMin.z),
-						float3(vMax.x, vMin.y, vMax.z),
-						float3(vMax.x, vMax.y, vMin.z),
-						float3(vMax),
-					};
-					//printf("%.2f, %.2f, %.2f\n", vNorm.x, vNorm.y, vNorm.z);
-
-					float fProj = FLT_MAX;
-					for(UINT i = 0, l = ARRAYSIZE(avPoints); i < l; ++i)
-					{
-						float fTmp = SMVector3Dot(avPoints[i], vNorm);
-						//printf("%.2f\n", fTmp);
-						if(fTmp < fProj)
-						{
-							fProj = fTmp;
-						}
-					}
-
-					fProj = SMVector3Dot(vPos, vNorm) - fProj;
-					//printf("%.2f, %.2f, %.2f\n%.2f, %.2f, %.2f\n%.2f, %.2f, %.2f\n%f\n\n", vPos.x, vPos.y, vPos.z, vMin.x, vMin.y, vMin.z, vMax.x, vMax.y, vMax.z, fProj);
-					if(fProj > 0)
-					{
-						pObj->setPos((float3)(vPos + vNorm * fProj));
-					}
-
+					IXEditorObject *pObj = CreateObjectAtPosition(vPos, vNorm, GetKeyState(VK_CONTROL) >= 0)->getObject();
+					
 					s_aRaytracedItems.clearFast();
 				}
 			}
@@ -4499,7 +4467,7 @@ case editor_type:
 		if(pItem->m_pObj == pObj && !fstrcmp(pItem->m_field.szKey, pField->szKey))       \
 		{                                                                                \
 			pItem->m_uUpdateRevision = g_uPropGizmoUpdateRevision;                       \
-			pItem->init();                                                               \
+			pItem->init(pField->pEditorData);                                            \
 			return;                                                                      \
 		}                                                                                \
 	}                                                                                    \
@@ -4508,7 +4476,7 @@ case editor_type:
 		pCb->m_pObj = pObj;                                                              \
 		pCb->m_field = *pField;                                                          \
 		pCb->m_uUpdateRevision = g_uPropGizmoUpdateRevision;                             \
-		pCb->init();                                                                     \
+		pCb->init(pField->pEditorData);                                                  \
 		g_aPropGizmo##gizmo##Items.push_back(pCb);                                       \
 	}                                                                                    \
 	break
@@ -4531,7 +4499,7 @@ XDECLARE_PROP_GIZMO(Radius, void XMETHODCALLTYPE onChange(float fNewRadius, IXEd
 	char tmp[16];
 	sprintf(tmp, "%f", fNewRadius);
 	m_pCommand->setKV(m_field.szKey, tmp);
-}, void init()
+}, void init(const void *pEditorData)
 {
 	m_pGizmo->setPos(m_pObj->getPos());
 	float fRadius;
@@ -4548,13 +4516,69 @@ XDECLARE_PROP_GIZMO(Handle, void XMETHODCALLTYPE moveTo(const float3 &vNewPos, I
 	char tmp[64];
 	sprintf(tmp, "%f %f %f", vTmp.x, vTmp.y, vTmp.z);
 	m_pCommand->setKV(m_field.szKey, tmp);
-}, void init()
+}, void init(const void *pEditorData)
 {
 	float3_t vec;
 	if(sscanf(m_pObj->getKV(m_field.szKey), "%f %f %f", &vec.x, &vec.y, &vec.z))
 	{
 		m_pGizmo->setPos(m_pObj->getOrient() * vec + m_pObj->getPos());
 	}
+
+	if(pEditorData)
+	{
+		kv_t *pKV = (kv_t*)pEditorData;
+		bool bLockPlane = false;
+		bool bLockAxis = false;
+		bool bLockLocal = true;
+		char axis = 0;
+		while(pKV->szKey)
+		{
+			if(!strcmp(pKV->szKey, "lock"))
+			{
+				if(!strcmp(pKV->szValue, "plane"))
+				{
+					bLockPlane = true;
+				}
+				else if(!strcmp(pKV->szValue, "axis"))
+				{
+					bLockAxis = true;
+				}
+			}
+			else if(!strcmp(pKV->szKey, "axis"))
+			{
+				axis = pKV->szValue[0];
+			}
+			else if(!strcmp(pKV->szKey, "local"))
+			{
+				if(pKV->szValue[0] != '0')
+				{
+					bLockLocal = true;
+				}
+			}
+			++pKV;
+		}
+
+		if((bLockPlane || bLockAxis) && axis)
+		{
+			float3_t vAxis(axis == 'x' ? 1.0f : 0.0, axis == 'y' ? 1.0f : 0.0, axis == 'z' ? 1.0f : 0.0);
+			if(bLockLocal)
+			{
+				vAxis = m_pObj->getOrient() * vAxis;
+			}
+			if(bLockPlane)
+			{
+				m_pGizmo->lockInPlane(vAxis);
+			}
+			else
+			{
+				m_pGizmo->lockInDir(vAxis);
+			}
+		}
+		else
+		{
+			m_pGizmo->unLock();
+		}
+	}
 });
 
 UINT g_uPropGizmoUpdateRevision = 0;
diff --git a/source/terrax/terrax.cpp b/source/terrax/terrax.cpp
index fe5e84feea376064c905a7c4a976e976873275ab..3a73567ac904ab6f2d5fc1e6b3a97cc438d8095a 100644
--- a/source/terrax/terrax.cpp
+++ b/source/terrax/terrax.cpp
@@ -827,7 +827,7 @@ int main(int argc, char **argv)
 						g_pEditor->registerResourceBrowser(pResourceBrowser);
 						mem_release(pResourceBrowser);
 						// XInitTool(pResourceBrowser, pEditable);
-			}
+					}
 				}
 			}
 
diff --git a/source/xParticles/Editable.h b/source/xParticles/Editable.h
index af162845fe61a18fbfdc016fd118a546abdc6f4c..bbfe486f44d41ad889888c5f3b0df3289089b280 100644
--- a/source/xParticles/Editable.h
+++ b/source/xParticles/Editable.h
@@ -79,6 +79,11 @@ public:
 		return(false);
 	}
 
+	const char* XMETHODCALLTYPE getClassKV(const char *szClassName, const char *szKey) override
+	{
+		return(NULL);
+	}
+
 private:
 	CEditorObject* getObjectByGUID(const XGUID &guid);
 
diff --git a/source/xUI/UIViewport.cpp b/source/xUI/UIViewport.cpp
index 989921e9b303b116db11a8f351c3a6adaac8ec87..495b9560f381daa598a714ad30deb99776ff3455 100644
--- a/source/xUI/UIViewport.cpp
+++ b/source/xUI/UIViewport.cpp
@@ -46,5 +46,5 @@ void XMETHODCALLTYPE CUIViewport::setSize(float fSizeX, float fSizeY)
 {
 	BaseClass::setSize(fSizeX, fSizeY);
 
-	m_pRenderTarget->resize((UINT)fSizeX, (UINT)fSizeY);
+	m_pRenderTarget->resize(fSizeX > 1.0f ? (UINT)fSizeX : 1, fSizeY > 1.0f ? (UINT)fSizeY : 1);
 }
diff --git a/source/xUI/UIWindow.cpp b/source/xUI/UIWindow.cpp
index 4fa6863ed70af72eb8a495facfa4295db91fd1cb..a6ae35a5e2741262a33708894ba7c31cc7700207 100644
--- a/source/xUI/UIWindow.cpp
+++ b/source/xUI/UIWindow.cpp
@@ -108,9 +108,22 @@ CUIWindow::CUIWindow(CXUI *pXUI, const XWINDOW_DESC *pWindowDesc, IUIWindow *pPa
 	m_pXWindowCallback = new CWindowCallback(this, m_pDesktopStack);
 	m_pXWindow = pXUI->getWindowSystem()->createWindow(pWindowDesc, m_pXWindowCallback, pXParent);
 
-	createSwapChain((UINT)pWindowDesc->iSizeX, (UINT)pWindowDesc->iSizeY);
+	float fScale = m_pXWindow->getScale();
+
+	if(fScale != 1.0f)
+	{
+		XWINDOW_DESC newDesc = *pWindowDesc;
+		newDesc.iSizeX *= fScale;
+		newDesc.iSizeY *= fScale;
+		m_pXWindow->update(&newDesc);
+		createSwapChain((UINT)newDesc.iSizeX, (UINT)newDesc.iSizeY);
+	}
+	else
+	{
+		createSwapChain((UINT)pWindowDesc->iSizeX, (UINT)pWindowDesc->iSizeY);
+	}
 
-	m_pDesktopStack->setScale(m_pXWindow->getScale());
+	m_pDesktopStack->setScale(fScale);
 
 	m_pControl = new CUIWindowControl(this, 0);
 }
diff --git a/source/xcommon/XEvents.h b/source/xcommon/XEvents.h
index fc73870971bcc377ed16e646b9d90a5a76425af9..09b2ae9f8172df10d57b6dccef02e38c1f14e9b7 100644
--- a/source/xcommon/XEvents.h
+++ b/source/xcommon/XEvents.h
@@ -95,6 +95,8 @@ struct XEventLevel
 		TYPE_LOAD_END, //!< Уровень полностью загружен
 		TYPE_UNLOAD_BEGIN, //!< До начала выгрузки уровня
 		TYPE_UNLOAD_END, //!< Уровень полностью выгружен
+
+		TYPE_LOAD_WAIT_ASYNC_TASKS, //!< Ожидание завершения асинхронных задач
 	} type;
 	const char *szLevelName;
 };
diff --git a/source/xcommon/editor/IXEditable.h b/source/xcommon/editor/IXEditable.h
index 17cd062da54204939f4feff6707b9018f3f4d3db..abcd208faa589896dfe7271dd0a4bff7d7c60398 100644
--- a/source/xcommon/editor/IXEditable.h
+++ b/source/xcommon/editor/IXEditable.h
@@ -58,7 +58,7 @@ public:
 	virtual const char* XMETHODCALLTYPE getName() = 0;
 	virtual UINT XMETHODCALLTYPE getClassCount() = 0;
 	virtual const char* XMETHODCALLTYPE getClass(UINT id) = 0;
-
+	
 	virtual void XMETHODCALLTYPE startup(IGXDevice *pDevice) = 0;
 	virtual void XMETHODCALLTYPE shutdown() = 0;
 
@@ -72,6 +72,7 @@ public:
 
 	virtual bool XMETHODCALLTYPE canUseModel(const char *szClass) = 0;
 
+	virtual const char* XMETHODCALLTYPE getClassKV(const char *szClassName, const char *szKey) = 0;
 };
 
 
diff --git a/source/xcommon/editor/IXEditorObject.h b/source/xcommon/editor/IXEditorObject.h
index b1643d970bec41590c397ed6122c0340bdf92191..156c7e2dd35d6a65ba9c4f4ed9564369ea539b15 100644
--- a/source/xcommon/editor/IXEditorObject.h
+++ b/source/xcommon/editor/IXEditorObject.h
@@ -16,6 +16,7 @@ enum X_PROP_EDITOR_TYPE
 	XPET_RADIUS,
 	XPET_COLOR,
 	XPET_HDRCOLOR,
+	XPET_SIZE,
 	//XPET_YESNO,
 
 	XPET__LAST
diff --git a/source/xcommon/resource/IXDecal.h b/source/xcommon/resource/IXDecal.h
new file mode 100644
index 0000000000000000000000000000000000000000..67038ed8f4d4febb2d62e7248b1459d922034050
--- /dev/null
+++ b/source/xcommon/resource/IXDecal.h
@@ -0,0 +1,44 @@
+#ifndef __IXDECAL_H
+#define __IXDECAL_H
+
+#include <gdefines.h>
+#include "IXResourceModel.h"
+
+class IXDecal: public IXUnknown
+{
+public:
+	virtual float3 XMETHODCALLTYPE getPosition() const = 0;
+	virtual void XMETHODCALLTYPE setPosition(const float3 &vPos) = 0;
+
+	virtual SMQuaternion XMETHODCALLTYPE getOrientation() const = 0;
+	virtual void XMETHODCALLTYPE setOrientation(const SMQuaternion &qRot) = 0;
+
+	virtual float3 XMETHODCALLTYPE getLocalBoundMin() const = 0;
+	virtual float3 XMETHODCALLTYPE getLocalBoundMax() const = 0;
+	virtual SMAABB XMETHODCALLTYPE getLocalBound() const = 0;
+
+	virtual bool XMETHODCALLTYPE isEnabled() const = 0;
+	virtual void XMETHODCALLTYPE enable(bool yesNo) = 0;
+
+	virtual bool XMETHODCALLTYPE rayTest(const float3 &vStart, const float3 &vEnd, float3 *pvOut = NULL, float3 *pvNormal = NULL) = 0;
+
+	virtual void XMETHODCALLTYPE setLayer(UINT uLayer) = 0;
+	virtual UINT XMETHODCALLTYPE getLayer() = 0;
+
+	virtual void XMETHODCALLTYPE setHeight(float fHeight) = 0;
+	virtual float XMETHODCALLTYPE getHeight() = 0;
+
+	virtual void XMETHODCALLTYPE setTextureRangeU(const float2_t &vRange) = 0;
+	virtual float2_t XMETHODCALLTYPE getTextureRangeU() = 0;
+
+	virtual void XMETHODCALLTYPE setTextureRangeV(const float2_t &vRange) = 0;
+	virtual float2_t XMETHODCALLTYPE getTextureRangeV() = 0;
+
+	virtual void XMETHODCALLTYPE setMaterial(const char *szMaterial) = 0;
+
+	virtual void XMETHODCALLTYPE setCorner(UINT uCorner, const float2_t &vCorner) = 0;
+
+	virtual void XMETHODCALLTYPE setLeakAllowed(bool yesNo) = 0;
+};
+
+#endif
diff --git a/source/xcommon/resource/IXDecalProvider.h b/source/xcommon/resource/IXDecalProvider.h
new file mode 100644
index 0000000000000000000000000000000000000000..38420258815f8383c1d60823f6254c6a031263da
--- /dev/null
+++ b/source/xcommon/resource/IXDecalProvider.h
@@ -0,0 +1,34 @@
+#ifndef __IXDECALPROVIDER_H
+#define __IXDECALPROVIDER_H
+
+#include "IXDecal.h"
+
+// {878E7A1E-86D4-4967-9A6D-6054B4363119}
+#define IXDECALPROVIDER_GUID DEFINE_XGUID(0x878e7a1e, 0x86d4, 0x4967, 0x9a, 0x6d, 0x60, 0x54, 0xb4, 0x36, 0x31, 0x19)
+
+enum XDECAL_TYPE
+{
+	XDT_CONCRETE = 0,
+	XDT_METAL,
+	XDT_GLASS,
+	XDT_PLASTIC,
+	XDT_WOOD,
+	XDT_FLESH,
+	XDT_EARTH,
+	XDT_BLOOD_BIG,
+
+	XDT__COUNT
+};
+
+// Implemented in anim plugin
+class IXDecalProvider: public IXUnknown
+{
+public:
+	// create custom decal
+	virtual bool XMETHODCALLTYPE newDecal(IXDecal **ppDecal) = 0;
+	
+	// create temporal standard decal at point/normal
+	virtual void XMETHODCALLTYPE shootDecal(XDECAL_TYPE type, const float3 &vWorldPos, const float3 &vNormal, float fScale = 1.0f, const float3 *pvSAxis = NULL) = 0;
+};
+
+#endif
diff --git a/source/xcsg/Editable.h b/source/xcsg/Editable.h
index 8d88c2d9b081b928410d7b1bd7868b17cde75ba1..6df39e2befc5f1b11dd02eb6d368ad5be5c9cddf 100644
--- a/source/xcsg/Editable.h
+++ b/source/xcsg/Editable.h
@@ -103,6 +103,11 @@ public:
 	void onModelDestroy(CEditorModel *pModel);
 	void onModelRestored(CEditorModel *pModel);
 
+	const char* XMETHODCALLTYPE getClassKV(const char *szClassName, const char *szKey) override
+	{
+		return(NULL);
+	}
+
 	SX_ALIGNED_OP_MEM();
 
 	CVertexTool* getVertexTool();
diff --git a/source/xcsg/EditorObject.cpp b/source/xcsg/EditorObject.cpp
index e168c533cfc99678ed213f1ca686c5f2cfd59edc..1ff89234361bfa37c2bc648d36d39f0b4de0bf33 100644
--- a/source/xcsg/EditorObject.cpp
+++ b/source/xcsg/EditorObject.cpp
@@ -144,7 +144,7 @@ bool XMETHODCALLTYPE CEditorObject::rayTest(const float3 &vStart, const float3 &
 		if(bReturnNearestPoint)
 		{
 			float3 vOut, vMinOut, vNormal, *pNormal = NULL;
-			if(pNormal)
+			if(pvNormal)
 			{
 				pNormal = &vNormal;
 			}