Browse Source

Import new code.
Coding convention changed.
Large refactoring of the object model, scene model and script API.
Rendering improvements, including automatic instancing.
High-level network protocol and scene editor need reimplementation.

Lasse Öörni 14 years ago
parent
commit
7c45da5588
100 changed files with 5304 additions and 11 deletions
  1. 4 0
      Bin/CoreData/Materials/Default.xml
  2. 9 0
      Bin/CoreData/Techniques/Diff.xml
  3. 3 0
      Bin/CoreData/Techniques/DiffAdd.xml
  4. 6 0
      Bin/CoreData/Techniques/DiffAlpha.xml
  5. 9 0
      Bin/CoreData/Techniques/DiffAlphaMask.xml
  6. 3 0
      Bin/CoreData/Techniques/DiffMultiply.xml
  7. 9 0
      Bin/CoreData/Techniques/DiffNormal.xml
  8. 6 0
      Bin/CoreData/Techniques/DiffNormalAlpha.xml
  9. 6 0
      Bin/CoreData/Techniques/DiffNormalAlphaMask.xml
  10. 3 0
      Bin/CoreData/Techniques/DiffSkybox.xml
  11. 3 0
      Bin/CoreData/Techniques/DiffUnlitAlpha.xml
  12. 3 0
      Bin/CoreData/Techniques/DiffVColAdd.xml
  13. 3 0
      Bin/CoreData/Techniques/DiffVColMultiply.xml
  14. 3 0
      Bin/CoreData/Techniques/DiffVColUnlitAlpha.xml
  15. 9 0
      Bin/CoreData/Techniques/NoTexture.xml
  16. 3 0
      Bin/CoreData/Techniques/NoTextureAdd.xml
  17. 5 0
      Bin/CoreData/Techniques/NoTextureAlpha.xml
  18. 3 0
      Bin/CoreData/Techniques/NoTextureMultiply.xml
  19. 3 0
      Bin/CoreData/Techniques/NoTextureUnlitAlpha.xml
  20. 3 0
      Bin/CoreData/Techniques/NoTextureVColAdd.xml
  21. 3 0
      Bin/CoreData/Techniques/NoTextureVColMultiply.xml
  22. BIN
      Bin/CoreData/Textures/Ramp.png
  23. 7 0
      Bin/CoreData/Textures/Ramp.xml
  24. BIN
      Bin/CoreData/Textures/RampExtreme.png
  25. 6 0
      Bin/CoreData/Textures/RampExtreme.xml
  26. BIN
      Bin/CoreData/Textures/RampWide.png
  27. 6 0
      Bin/CoreData/Textures/RampWide.xml
  28. BIN
      Bin/CoreData/Textures/Spot.png
  29. 7 0
      Bin/CoreData/Textures/Spot.xml
  30. BIN
      Bin/CoreData/Textures/SpotWide.png
  31. 6 0
      Bin/CoreData/Textures/SpotWide.xml
  32. BIN
      Bin/CoreData/Textures/UI.png
  33. 5 0
      Bin/CoreData/Textures/UI.xml
  34. 47 0
      Bin/CoreData/UI/ComponentWindow.xml
  35. 413 0
      Bin/CoreData/UI/DefaultStyle.xml
  36. 129 0
      Bin/CoreData/UI/EditorSettingsDialog.xml
  37. 65 0
      Bin/CoreData/UI/SceneSettingsDialog.xml
  38. 77 0
      Bin/CoreData/UI/SceneWindow.xml
  39. BIN
      Bin/Data/Fonts/BlueHighway.ttf
  40. 5 0
      Bin/Data/Materials/CloudPlane.xml
  41. 4 0
      Bin/Data/Materials/Jack.xml
  42. 5 0
      Bin/Data/Materials/Mushroom.xml
  43. 5 0
      Bin/Data/Materials/Ninja.xml
  44. 4 0
      Bin/Data/Materials/Particle.xml
  45. 5 0
      Bin/Data/Materials/Potion.xml
  46. 4 0
      Bin/Data/Materials/Smoke.xml
  47. 5 0
      Bin/Data/Materials/Snow.xml
  48. 5 0
      Bin/Data/Materials/SnowCrate.xml
  49. 7 0
      Bin/Data/Materials/Test.xml
  50. BIN
      Bin/Data/Models/Box.mdl
  51. BIN
      Bin/Data/Models/CloudPlane.mdl
  52. BIN
      Bin/Data/Music/Ninja Gods.ogg
  53. 20 0
      Bin/Data/Particle/Smoke.xml
  54. 16 0
      Bin/Data/Particle/SnowExplosion.xml
  55. 18 0
      Bin/Data/Particle/SnowExplosionBig.xml
  56. 173 0
      Bin/Data/Scripts/AIController.as
  57. 188 0
      Bin/Data/Scripts/GameObject.as
  58. 36 0
      Bin/Data/Scripts/LightFlash.as
  59. 346 0
      Bin/Data/Scripts/Ninja.as
  60. 494 0
      Bin/Data/Scripts/NinjaSnowWar.as
  61. 58 0
      Bin/Data/Scripts/Potion.as
  62. 103 0
      Bin/Data/Scripts/SnowBall.as
  63. 60 0
      Bin/Data/Scripts/SnowCrate.as
  64. 390 0
      Bin/Data/Scripts/TestScene.as
  65. BIN
      Bin/Data/Sounds/BigExplosion.wav
  66. BIN
      Bin/Data/Sounds/NutThrow.wav
  67. BIN
      Bin/Data/Sounds/PlayerFist.wav
  68. BIN
      Bin/Data/Sounds/PlayerFistHit.wav
  69. BIN
      Bin/Data/Sounds/PlayerLand.wav
  70. BIN
      Bin/Data/Sounds/Powerup.wav
  71. BIN
      Bin/Data/Sounds/SmallExplosion.wav
  72. BIN
      Bin/Data/Textures/CloudPlane.dds
  73. BIN
      Bin/Data/Textures/Diffuse.dds
  74. BIN
      Bin/Data/Textures/DiffuseMask.dds
  75. BIN
      Bin/Data/Textures/Flare.dds
  76. BIN
      Bin/Data/Textures/HealthBarBorder.png
  77. BIN
      Bin/Data/Textures/HealthBarInside.png
  78. BIN
      Bin/Data/Textures/Jack_body_color.jpg
  79. BIN
      Bin/Data/Textures/Jack_face.jpg
  80. BIN
      Bin/Data/Textures/Mushroom.dds
  81. BIN
      Bin/Data/Textures/Ninja.dds
  82. BIN
      Bin/Data/Textures/Normal.dds
  83. BIN
      Bin/Data/Textures/Normal.png
  84. BIN
      Bin/Data/Textures/Sight.png
  85. 5 0
      Bin/Data/Textures/Sight.xml
  86. BIN
      Bin/Data/Textures/Smoke.dds
  87. BIN
      Bin/Data/Textures/Snow.dds
  88. BIN
      Bin/Data/Textures/SnowCrate.dds
  89. 1 0
      Bin/NinjaSnowWar.bat
  90. 1 0
      Bin/TestScene.bat
  91. 8 11
      CMakeLists.txt
  92. 451 0
      Engine/Audio/Audio.cpp
  93. 157 0
      Engine/Audio/Audio.h
  94. 34 0
      Engine/Audio/AudioDefs.h
  95. 18 0
      Engine/Audio/CMakeLists.txt
  96. 24 0
      Engine/Audio/Precompiled.cpp
  97. 29 0
      Engine/Audio/Precompiled.h
  98. 417 0
      Engine/Audio/Sound.cpp
  99. 122 0
      Engine/Audio/Sound.h
  100. 1219 0
      Engine/Audio/SoundSource.cpp

+ 4 - 0
Bin/CoreData/Materials/Default.xml

@@ -0,0 +1,4 @@
+<material>
+    <technique name="Techniques/NoTexture.xml" />
+    <parameter name="MatSpecProperties" value="0.5 16" />
+</material>

+ 9 - 0
Bin/CoreData/Techniques/Diff.xml

@@ -0,0 +1,9 @@
+<technique>
+    <pass name="deferred" vs="Deferred/GBuffer" ps="Deferred/GBuffer_Diff" />
+    <pass name="prepass" vs="Prepass/GBuffer" ps="Prepass/GBuffer" />
+    <pass name="material" vs="Prepass/Material" ps="Prepass/Material_Diff" depthwrite="false" depthtest="equal" />
+    <pass name="base" vs="Forward" ps="Forward_DiffAmbient" />
+    <pass name="litbase" vs="Forward" ps="Forward_DiffAmbient" />
+    <pass name="light" vs="Forward" ps="Forward_Diff" depthwrite="false" depthtest="equal" blend="add" />
+    <pass name="shadow" vs="Shadow" ps="Shadow" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffAdd.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_DiffUnlit" depthwrite="false" blend="add" />
+</technique>

+ 6 - 0
Bin/CoreData/Techniques/DiffAlpha.xml

@@ -0,0 +1,6 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_DiffAmbient" depthwrite="false" blend="alpha" />
+    <pass name="litbase" vs="Forward" ps="Forward_DiffAmbient" depthwrite="false" blend="alpha" />
+    <pass name="light" vs="Forward" ps="Forward_Diff" depthwrite="false" blend="addalpha" />
+    <pass name="shadow" vs="Shadow" ps="Shadow" />
+</technique>

+ 9 - 0
Bin/CoreData/Techniques/DiffAlphaMask.xml

@@ -0,0 +1,9 @@
+<technique>
+    <pass name="deferred" vs="Deferred/GBuffer" ps="Deferred/GBuffer_DiffMask" alphamask="true" />
+    <pass name="prepass" vs="Prepass/GBuffer" ps="Prepass/GBuffer_Mask" alphamask="true" />
+    <pass name="material" vs="Prepass/Material" ps="Prepass/Material_DiffMask" alphamask="true" depthwrite="false" depthtest="equal" />
+    <pass name="base" vs="Forward" ps="Forward_DiffAmbient" alphatest="true" />
+    <pass name="litbase" vs="Forward" ps="Forward_DiffAmbient" alphatest="true" />
+    <pass name="light" vs="Forward" ps="Forward_Diff" alphatest="true" depthwrite="false" depthtest="equal" blend="add" />
+    <pass name="shadow" vs="Shadow_Mask" ps="Shadow_Mask" alphamask="true" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffMultiply.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_DiffUnlit" depthwrite="false" blend="multiply" />
+</technique>

+ 9 - 0
Bin/CoreData/Techniques/DiffNormal.xml

@@ -0,0 +1,9 @@
+<technique>
+    <pass name="deferred" vs="Deferred/GBuffer_Normal" ps="Deferred/GBuffer_DiffNormal" />
+    <pass name="prepass" vs="Prepass/GBuffer_Normal" ps="Prepass/GBuffer_Normal" />
+    <pass name="material" vs="Prepass/Material" ps="Prepass/Material_Diff" depthwrite="false" depthtest="equal" />
+    <pass name="base" vs="Forward" ps="Forward_DiffAmbient" />
+    <pass name="litbase" vs="Forward_Normal" ps="Forward_DiffNormalAmbient" />
+    <pass name="light" vs="Forward_Normal" ps="Forward_DiffNormal" depthwrite="false" depthtest="equal" blend="add" />
+    <pass name="shadow" vs="Shadow" ps="Shadow" />
+</technique>

+ 6 - 0
Bin/CoreData/Techniques/DiffNormalAlpha.xml

@@ -0,0 +1,6 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_DiffAmbient" depthwrite="false" blend="alpha" />
+    <pass name="litbase" vs="Forward_Normal" ps="Forward_DiffNormalAmbient" depthwrite="false" blend="alpha" />
+    <pass name="light" vs="Forward_Normal" ps="Forward_DiffNormal" depthwrite="false" blend="addalpha" />
+    <pass name="shadow" vs="Shadow" ps="Shadow" />
+</technique>

+ 6 - 0
Bin/CoreData/Techniques/DiffNormalAlphaMask.xml

@@ -0,0 +1,6 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_DiffAmbient" alphatest="true" />
+    <pass name="litbase" vs="Forward_Normal" ps="Forward_DiffNormalAmbient" alphatest="true" />
+    <pass name="light" vs="Forward_Normal" ps="Forward_DiffNormal" alphatest="true" depthwrite="false" depthtest="equal" blend="add" />
+    <pass name="shadow" vs="Shadow_Mask" ps="Shadow_Mask" alphamask="true" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffSkybox.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="extra" vs="Forward" ps="Forward_DiffUnlit" depthwrite="false" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffUnlitAlpha.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_DiffUnlit" depthwrite="false" blend="alpha" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffVColAdd.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward_VCol" ps="Forward_DiffVColUnlit" depthwrite="false" blend="add" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffVColMultiply.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward_VCol" ps="Forward_DiffVColUnlit" depthwrite="false" blend="multiply" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/DiffVColUnlitAlpha.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward_VCol" ps="Forward_DiffVColUnlit" depthwrite="false" blend="alpha" />
+</technique>

+ 9 - 0
Bin/CoreData/Techniques/NoTexture.xml

@@ -0,0 +1,9 @@
+<technique>
+    <pass name="deferred" vs="Deferred/GBuffer" ps="Deferred/GBuffer" />
+    <pass name="prepass" vs="Prepass/GBuffer" ps="Prepass/GBuffer" />
+    <pass name="material" vs="Prepass/Material" ps="Prepass/Material" depthwrite="false" depthtest="equal" />
+    <pass name="base" vs="Forward" ps="Forward_Ambient" />
+    <pass name="litbase" vs="Forward" ps="Forward_Ambient" />
+    <pass name="light" vs="Forward" ps="Forward" depthwrite="false" depthtest="equal" blend="add" />
+    <pass name="shadow" vs="Shadow" ps="Shadow" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/NoTextureAdd.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_Unlit" depthwrite="false" blend="add" />
+</technique>

+ 5 - 0
Bin/CoreData/Techniques/NoTextureAlpha.xml

@@ -0,0 +1,5 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_Ambient" depthwrite="false" blend="alpha" />
+    <pass name="litbase" vs="Forward" ps="Forward_Ambient" depthwrite="false" blend="alpha" />
+    <pass name="light" vs="Forward" ps="Forward" depthwrite="false" blend="addalpha" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/NoTextureMultiply.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward" ps="Forward_Unlit" depthwrite="false" blend="multiply" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/NoTextureUnlitAlpha.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward_Unlit" ps="Forward_Unlit" depthwrite="false" blend="alpha" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/NoTextureVColAdd.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward_VCol" ps="Forward_VColUnlit" depthwrite="false" blend="add" />
+</technique>

+ 3 - 0
Bin/CoreData/Techniques/NoTextureVColMultiply.xml

@@ -0,0 +1,3 @@
+<technique>
+    <pass name="base" vs="Forward_VCol" ps="Forward_VColUnlit" depthwrite="false" blend="multiply" />
+</technique>

BIN
Bin/CoreData/Textures/Ramp.png


+ 7 - 0
Bin/CoreData/Textures/Ramp.xml

@@ -0,0 +1,7 @@
+<texture>
+    <address coord="u" mode="clamp" />
+    <address coord="v" mode="clamp" />
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>
+

BIN
Bin/CoreData/Textures/RampExtreme.png


+ 6 - 0
Bin/CoreData/Textures/RampExtreme.xml

@@ -0,0 +1,6 @@
+<texture>
+    <address coord="u" mode="clamp" />
+    <address coord="v" mode="clamp" />
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>

BIN
Bin/CoreData/Textures/RampWide.png


+ 6 - 0
Bin/CoreData/Textures/RampWide.xml

@@ -0,0 +1,6 @@
+<texture>
+    <address coord="u" mode="clamp" />
+    <address coord="v" mode="clamp" />
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>

BIN
Bin/CoreData/Textures/Spot.png


+ 7 - 0
Bin/CoreData/Textures/Spot.xml

@@ -0,0 +1,7 @@
+<texture>
+    <address coord="u" mode="clamp" />
+    <address coord="v" mode="clamp" />
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>
+

BIN
Bin/CoreData/Textures/SpotWide.png


+ 6 - 0
Bin/CoreData/Textures/SpotWide.xml

@@ -0,0 +1,6 @@
+<texture>
+    <address coord="u" mode="clamp" />
+    <address coord="v" mode="clamp" />
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>

BIN
Bin/CoreData/Textures/UI.png


+ 5 - 0
Bin/CoreData/Textures/UI.xml

@@ -0,0 +1,5 @@
+<texture>
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>
+

+ 47 - 0
Bin/CoreData/UI/ComponentWindow.xml

@@ -0,0 +1,47 @@
+<element type="Window" name="ComponentWindow">
+    <movable enable="true" />
+    <resizable enable="true" />
+    <resizeborder value="6 6 6 6" />
+    <layout mode="vertical" spacing="4" border="6 6 6 6" />
+    <element>
+        <fixedheight value="16" />
+        <layout mode="horizontal" />
+        <element type="Text" name="WindowTitle">
+            <text value="Entity / component edit" />
+        </element>
+        <element type="Button" style="CloseButton" name="CloseButton" />
+    </element>
+    <element type="BorderImage" style="EditorDivider">
+        <fixedheight value="4" />
+    </element>
+    <element type="Text" name="EntityTitle">
+        <fixedheight value="16" />
+    </element>
+    <element>
+        <fixedheight value="16" />
+        <layout mode="horizontal" />
+        <element type="Text">
+            <fixedwidth value="50" />
+            <text value="Name" />
+        </element>
+        <element type="LineEdit" name="EntityNameEdit" />
+    </element>
+    <element type="BorderImage" style="EditorDivider">
+        <fixedheight value="4" />
+    </element>
+    <element type="Text" name="ComponentTitle">
+        <fixedheight value="16" />
+    </element>
+    <element>
+        <fixedheight value="16" />
+        <layout mode="horizontal" />
+        <element type="Text">
+            <fixedwidth value="50" />
+            <text value="Name" />
+        </element>
+        <element type="LineEdit" name="ComponentNameEdit" />
+    </element>
+    <element type="ListView" name="AttributeList">
+        <highlight value="always" />
+    </element>
+</element>

+ 413 - 0
Bin/CoreData/UI/DefaultStyle.xml

@@ -0,0 +1,413 @@
+<elements>
+    <element type="Button">
+        <size value="16 16" />
+        <texture name="Textures/UI.png" />
+        <imagerect value="16 0 32 16" />
+        <border value="4 4 4 4" />
+        <pressedoffset value="16 0" />
+        <hoveroffset value="0 16" />
+        <labeloffset value="-1 1" />
+    </element>
+    <element type="CheckBox">
+        <size value="16 16" />
+        <texture name="Textures/UI.png" />
+        <imagerect value="64 0 80 16" />
+        <border value="4 4 4 4" />
+        <checkedoffset value="16 0" />
+        <hoveroffset value="0 16" />
+    </element>
+    <element type="CloseButton">
+        <fixedsize value="16 16" />
+        <texture name="Textures/UI.png" />
+        <imagerect value="128 0 144 16" />
+        <border value="4 4 4 4" />
+        <pressedoffset value="16 0" />
+        <hoveroffset value="0 16" />
+        <labeloffset value="-1 1" />
+    </element>
+    <element type="Cursor">
+        <shape name="normal" texture="Textures/UI.png" imagerect="0 0 12 24" hotspot="0 0" />
+        <shape name="resizevertical" texture="Textures/UI.png" imagerect="0 64 20 84" hotspot="9 9" />
+        <shape name="resizediagonal_topright" texture="Textures/UI.png" imagerect="20 64 40 84" hotspot="9 9" />
+        <shape name="resizehorizontal" texture="Textures/UI.png" imagerect="40 64 60 84" hotspot="9 9" />
+        <shape name="resizediagonal_topleft" texture="Textures/UI.png" imagerect="60 64 80 84" hotspot="9 9" />
+        <shape name="rejectdrop" texture="Textures/UI.png" imagerect="80 64 100 84" hotspot="9 9" />
+        <shape name="acceptdrop" texture="Textures/UI.png" imagerect="100 64 128 90" hotspot="0 0" />
+    </element>
+    <element type="DropDownList">
+        <texture name="Textures/UI.png" />
+        <imagerect value="16 0 32 16" />
+        <border value="4 4 4 4" />
+        <pressedoffset value="16 0" />
+        <hoveroffset value="0 16" />
+        <labeloffset value="-1 1" />
+        <layout mode="horizontal" border="4 2 4 2" />
+        <popup>
+            <texture name="Textures/UI.png" />
+            <imagerect value="48 0 64 16" />
+            <border value="3 3 3 3" />
+            <layout border="4 2 4 2" />
+        </popup>
+        <listview>
+            <horizontalscrollbar>
+                <height value="12" />
+                <backbutton>
+                    <texture name="Textures/UI.png" />
+                    <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+                    <border value="3 3 3 3" />
+                    <pressedoffset value="64 0" />
+                    <hoveroffset value="0 16" />
+                </backbutton>
+                <forwardbutton>
+                    <texture name="Textures/UI.png" />
+                    <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+                    <border value="3 3 3 3" />
+                    <pressedoffset value="64 0" />
+                    <hoveroffset value="0 16" />
+                </forwardbutton>
+                <slider>
+                    <texture name="Textures/UI.png" />
+                    <imagerect value="48 0 64 16" />
+                    <border value="3 3 3 3" />
+                    <knob>
+                        <texture name="Textures/UI.png" />
+                        <imagerect value="16 0 32 16" />
+                        <border value="4 4 4 4" />
+                        <hoveroffset value="0 16" />
+                    </knob>
+                </slider>
+            </horizontalscrollbar>
+            <verticalscrollbar>
+                <width value="12" />
+                <backbutton>
+                    <texture name="Textures/UI.png" />
+                    <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+                    <border value="3 3 3 3" />
+                    <pressedoffset value="64 0" />
+                    <hoveroffset value="0 16" />
+                </backbutton>
+                <forwardbutton>
+                    <texture name="Textures/UI.png" />
+                    <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+                    <border value="3 3 3 3" />
+                    <pressedoffset value="64 0" />
+                    <hoveroffset value="0 16" />
+                </forwardbutton>
+                <slider>
+                    <texture name="Textures/UI.png" />
+                    <imagerect value="48 0 64 16" />
+                    <border value="3 3 3 3" />
+                    <knob>
+                        <texture name="Textures/UI.png" />
+                        <imagerect value="16 0 32 16" />
+                        <border value="4 4 4 4" />
+                        <hoveroffset value="0 16" />
+                    </knob>
+                </slider>
+            </verticalscrollbar>
+            <scrollpanel>
+                <color value="0 0 0 0" />
+            </scrollpanel>
+        </listview>
+    </element>
+    <element type="LineEdit">
+        <texture name="Textures/UI.png" />
+        <imagerect value="112 0 128 16" />
+        <border value="2 2 2 2" />
+        <clipborder value="1 1 1 1" />
+        <hoveroffset value="0 16" />
+        <text>
+            <position value="1 1" />
+            <font name="Cour.ttf" size="10" />
+            <hovercolor value="0.45 0.70 0.45" />
+            <selectioncolor value="0.70 0.70 0.70" />
+        </text>
+        <cursor>
+            <size value="4 16" />
+            <texture name="Textures/UI.png" />
+            <imagerect value="12 0 16 16" />
+        </cursor>
+    </element>
+    <element type="ListView">
+        <horizontalscrollbar>
+            <height value="12" />
+            <backbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </backbutton>
+            <forwardbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </forwardbutton>
+            <slider>
+                <texture name="Textures/UI.png" />
+                <imagerect value="48 0 64 16" />
+                <border value="3 3 3 3" />
+                <knob>
+                    <texture name="Textures/UI.png" />
+                    <imagerect value="16 0 32 16" />
+                    <border value="4 4 4 4" />
+                    <hoveroffset value="0 16" />
+                </knob>
+            </slider>
+        </horizontalscrollbar>
+        <verticalscrollbar>
+            <width value="12" />
+            <backbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </backbutton>
+            <forwardbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </forwardbutton>
+            <slider>
+                <texture name="Textures/UI.png" />
+                <imagerect value="48 0 64 16" />
+                <border value="3 3 3 3" />
+                <knob>
+                    <texture name="Textures/UI.png" />
+                    <imagerect value="16 0 32 16" />
+                    <border value="4 4 4 4" />
+                    <hoveroffset value="0 16" />
+                </knob>
+            </slider>
+        </verticalscrollbar>
+        <scrollpanel>
+            <texture name="Textures/UI.png" />
+            <imagerect value="112 0 128 16" />
+            <border value="2 2 2 2" />
+            <clipborder value="1 1 1 1" />
+        </scrollpanel>
+    </element>
+    <element type="Menu">
+        <texture name="Textures/UI.png" />
+        <imagerect value="96 0 112 16" />
+        <border value="2 2 2 2" />
+        <pressedoffset value="16 0" />
+        <hoveroffset value="0 16" />
+    </element>
+    <element type="ScrollBar">
+        <backbutton>
+            <size value="12 12" />
+            <texture name="Textures/UI.png" />
+            <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+            <border value="3 3 3 3" />
+            <pressedoffset value="64 0" />
+            <hoveroffset value="0 16" />
+        </backbutton>
+        <forwardbutton>
+            <size value="12 12" />
+            <texture name="Textures/UI.png" />
+            <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+            <border value="3 3 3 3" />
+            <pressedoffset value="64 0" />
+            <hoveroffset value="0 16" />
+        </forwardbutton>
+        <slider>
+            <size value="12 12" />
+            <texture name="Textures/UI.png" />
+            <imagerect value="48 0 64 16" />
+            <border value="3 3 3 3" />
+            <knob>
+                <texture name="Textures/UI.png" />
+                <imagerect value="16 0 32 16" />
+                <border value="4 4 4 4" />
+                <hoveroffset value="0 16" />
+            </knob>
+        </slider>
+    </element>
+    <element type="ScrollView">
+        <horizontalscrollbar>
+            <height value="12" />
+            <backbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </backbutton>
+            <forwardbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </forwardbutton>
+            <slider>
+                <texture name="Textures/UI.png" />
+                <imagerect value="48 0 64 16" />
+                <border value="3 3 3 3" />
+                <knob>
+                    <texture name="Textures/UI.png" />
+                    <imagerect value="16 0 32 16" />
+                    <border value="4 4 4 4" />
+                    <hoveroffset value="0 16" />
+                </knob>
+            </slider>
+        </horizontalscrollbar>
+        <verticalscrollbar>
+            <width value="12" />
+            <backbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="32 32 48 48" vertical="0 32 16 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </backbutton>
+            <forwardbutton>
+                <texture name="Textures/UI.png" />
+                <imagerect horizontal="48 32 64 48" vertical="16 32 32 48" />
+                <border value="3 3 3 3" />
+                <pressedoffset value="64 0" />
+                <hoveroffset value="0 16" />
+            </forwardbutton>
+            <slider>
+                <texture name="Textures/UI.png" />
+                <imagerect value="48 0 64 16" />
+                <border value="3 3 3 3" />
+                <knob>
+                    <texture name="Textures/UI.png" />
+                    <imagerect value="16 0 32 16" />
+                    <border value="4 4 4 4" />
+                    <hoveroffset value="0 16" />
+                </knob>
+            </slider>
+        </verticalscrollbar>
+        <scrollpanel>
+            <texture name="Textures/UI.png" />
+            <imagerect value="112 0 128 16" />
+            <border value="2 2 2 2" />
+            <clipborder value="1 1 1 1" />
+        </scrollpanel>
+    </element>
+    <element type="Slider">
+        <size value="12 12" />
+        <texture name="Textures/UI.png" />
+        <imagerect value="48 0 64 16" />
+        <border value="3 3 3 3" />
+        <knob>
+            <texture name="Textures/UI.png" />
+            <imagerect value="16 0 32 16" />
+            <border value="4 4 4 4" />
+            <hoveroffset value="0 16" />
+        </knob>
+    </element>
+    <element type="Window">
+        <texture name="Textures/UI.png" />
+        <imagerect value="48 0 64 16" />
+        <border value="3 3 3 3" />
+        <resizeborder value="8 8 8 8" />
+    </element>
+    <element type="Text">
+        <font name="Cour.ttf" size="10" />
+    </element>
+    <element type="DebugHudText">
+        <font name="Cour.ttf" size="10" />
+    </element>
+    <element type="ConsoleBackground">
+        <color topleft="0 0.25 0 0.75" topright="0 0.25 0 0.75" bottomleft="0.25 0.75 0.25 0.75" bottomright="0.25 0.75 0.25 0.75" />
+        <layout spacing="0" border="4 4 4 4" />
+    </element>
+    <element type="ConsoleText">
+        <font name="Cour.ttf" size="10" />
+    </element>
+    <element type="ConsoleLineEdit">
+        <color value="0 0 0 0.5" />
+        <text>
+            <font name="Cour.ttf" size="10" />
+            <selectioncolor value="0 0.5 0 0.75" />
+        </text>
+        <cursor>
+            <size value="4 16" />
+            <texture name="Textures/UI.png" />
+            <imagerect value="12 0 16 16" />
+        </cursor>
+    </element>
+    <element type="FileSelector">
+        <size value="400 300" />
+        <movable enable="true" />
+        <resizable enable="true" />
+        <resizeborder value="6 6 6 6" />
+        <layout mode="vertical" spacing="4" border="6 6 6 6" />
+    </element>
+    <element type="FileSelectorButton">
+        <fixedsize value="80 22" />
+    </element>
+    <element type="FileSelectorButtonText">
+        <font name="Cour.ttf" size="10" />
+    </element>
+    <element type="FileSelectorFilterList">
+        <fixedwidth value="64" />
+        <resizepopup enable="true" />
+    </element>
+    <element type="FileSelectorFilterText">
+        <font name="Cour.ttf" size="10" />
+        <hovercolor value="0.45 0.70 0.45" />
+    </element>
+    <element type="FileSelectorTitleLayout">
+        <fixedheight value="16" />
+        <layout spacing="4" />
+    </element>
+    <element type="FileSelectorLayout">
+        <layout spacing="4" />
+    </element>
+    <element type="FileSelectorListText">
+        <font name="Cour.ttf" size="10" />
+        <hovercolor value="0.45 0.70 0.45" />
+        <selectioncolor value="0.70 0.70 0.70" />
+    </element>
+    <element type="FileSelectorTitleText">
+        <font name="Cour.ttf" size="10" />
+    </element>
+    <element type="EditorDivider">
+        <texture name="Textures/UI.png" />
+        <imagerect value="112 0 128 16" />
+        <border value="2 2 2 2" />
+    </element>
+    <element type="EditorMenuBar">
+        <texture name="Textures/UI.png" />
+        <imagerect value="96 0 112 16" />
+        <border value="2 2 2 2" />
+    </element>
+    <element type="EditorMenuText">
+        <font name="cour.ttf" size="10" />
+    </element>
+    <element type="EditorAttributeText">
+        <font name="cour.ttf" size="9" />
+    </element>
+    <element type="EditorEnumAttributeText">
+        <font name="Cour.ttf" size="9" />
+        <hovercolor value="0.45 0.70 0.45" />
+    </element>
+    <element type="EditorAttributeEdit">
+        <texture name="Textures/UI.png" />
+        <imagerect value="112 0 128 16" />
+        <border value="2 2 2 2" />
+        <clipborder value="1 1 1 1" />
+        <hoveroffset value="0 16" />
+        <text>
+            <position value="1 1" />
+            <font name="Cour.ttf" size="9" />
+            <hovercolor value="0.45 0.70 0.45" />
+            <selectioncolor value="0.70 0.70 0.70" />
+        </text>
+        <cursor>
+            <size value="4 16" />
+            <texture name="Textures/UI.png" />
+            <imagerect value="12 0 16 16" />
+        </cursor>
+    </element>
+</elements>

+ 129 - 0
Bin/CoreData/UI/EditorSettingsDialog.xml

@@ -0,0 +1,129 @@
+<element type="Window" name="CameraDialog">
+    <movable enable="true" />
+    <layout mode="vertical" spacing="4" border="6 6 6 6" />
+    <element>
+        <fixedheight value="16" />
+        <layout mode="horizontal" />
+        <element type="Text">
+            <text value="Editor camera settings" />
+        </element>
+        <element type="Button" style="CloseButton" name="CloseButton" />
+    </element>
+    <element type="BorderImage" style="EditorDivider">
+        <fixedheight value="4" />
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Near clip distance" />
+        </element>
+        <element type="LineEdit" name="NearClipEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Far clip distance" />
+        </element>
+        <element type="LineEdit" name="FarClipEdit">
+            <fixedwidth value="80" />
+         </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Field of view" />
+        </element>
+        <element type="LineEdit" name="FOVEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Movement speed" />
+        </element>
+        <element type="LineEdit" name="SpeedEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element type="BorderImage" style="EditorDivider">
+        <fixedheight value="4" />
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="New node distance" />
+        </element>
+        <element type="LineEdit" name="DistanceEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="8" />
+        <element type="Text">
+            <text value="Node move step" />
+        </element>
+        <element type="CheckBox" name="MoveSnapToggle">
+            <fixedsize value="16 16" />
+        </element>
+        <element type="Text">
+            <fixedwidth value="34" />
+            <text value="Snap" />
+        </element>
+        <element type="LineEdit" name="MoveStepEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="8" />
+        <element type="Text">
+            <text value="Node rotate step" />
+        </element>
+        <element type="CheckBox" name="RotateSnapToggle">
+            <fixedsize value="16 16" />
+        </element>
+        <element type="Text">
+            <fixedwidth value="34" />
+            <text value="Snap" />
+        </element>
+        <element type="LineEdit" name="RotateStepEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="8" />
+        <element type="Text">
+            <text value="Node scale step" />
+        </element>
+        <element type="CheckBox" name="ScaleSnapToggle">
+            <fixedsize value="16 16" />
+        </element>
+        <element type="Text">
+            <fixedwidth value="34" />
+            <text value="Snap" />
+        </element>
+        <element type="LineEdit" name="ScaleStepEdit">
+            <fixedwidth value="80" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="8" />
+        <element type="CheckBox" name="LocalIDToggle">
+            <fixedsize value="16 16" />
+        </element>
+        <element type="Text">
+            <text value="Use local entity IDs on import" />
+        </element>
+    </element>
+</element>

+ 65 - 0
Bin/CoreData/UI/SceneSettingsDialog.xml

@@ -0,0 +1,65 @@
+<element type="Window" name="SceneSettingsDialog">
+    <movable enable="true" />
+    <layout mode="vertical" spacing="4" border="6 6 6 6" />
+    <element>
+        <fixedheight value="16" />
+        <layout mode="horizontal" />
+        <element type="Text">
+            <text value="Global scene settings" />
+        </element>
+        <element type="Button" style="CloseButton" name="CloseButton" />
+    </element>
+    <element type="BorderImage" style="EditorDivider">
+        <fixedheight value="4" />
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Octree min bounds" />
+        </element>
+        <element type="LineEdit" name="OctreeMinEdit">
+            <fixedwidth value="160" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Octree max bounds" />
+        </element>
+       <element type="LineEdit" name="OctreeMaxEdit">
+            <fixedwidth value="160" />
+        </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Octree levels" />
+        </element>
+       <element type="LineEdit" name="OctreeLevelsEdit">
+            <fixedwidth value="160" />
+       </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Gravity" />
+        </element>
+       <element type="LineEdit" name="GravityEdit">
+            <fixedwidth value="160" />
+       </element>
+    </element>
+    <element>
+        <fixedheight value="17" />
+        <layout mode="horizontal" spacing="20" />
+        <element type="Text">
+            <text value="Physics FPS" />
+        </element>
+       <element type="LineEdit" name="PhysicsFPSEdit">
+            <fixedwidth value="160" />
+       </element>
+    </element>
+</element>

+ 77 - 0
Bin/CoreData/UI/SceneWindow.xml

@@ -0,0 +1,77 @@
+<element type="Window" name="SceneWindow">
+    <movable enable="true" />
+    <resizable enable="true" />
+    <resizeborder value="6 6 6 6" />
+    <layout mode="vertical" spacing="4" border="6 6 6 6" />
+    <element>
+        <fixedheight value="16" />
+        <layout mode="horizontal" />
+        <element type="Text">
+            <text value="Scene hierarchy" />
+        </element>
+        <element type="Button" style="CloseButton" name="CloseButton" />
+    </element>
+    <element type="BorderImage" style="EditorDivider">
+        <fixedheight value="4" />
+    </element>
+    <element>
+        <fixedheight value="18" />
+        <layout mode="horizontal" spacing="4" />
+        <element type="Button" name="ExpandAllButton">
+            <fixedwidth value="70" />
+            <element type="Text">
+                <font name="cour.ttf" size="9" />
+                <text value="Expand" />
+                <alignment horizontal="center" vertical="center" />
+            </element>
+        </element>
+        <element type="Button" name="CollapseAllButton">
+            <fixedwidth value="70" />
+            <element type="Text">
+                <font name="cour.ttf" size="9" />
+                <text value="Collapse" />
+                <alignment horizontal="center" vertical="center" />
+            </element>
+        </element>
+        <element />
+    </element>
+    <element type="ListView" name="EntityList">
+        <hierarchy enable="true" />
+        <highlight value="always" />
+    </element>
+    <element>
+        <fixedheight value="22" />
+        <layout mode="horizontal" spacing="4" />
+        <element type="Element" />
+        <element type="DropDownList" name="NewEntityList">
+            <fixedsize value="90 22" />
+            <layout border="4 4 4 4" />
+            <placeholder>
+                <visible enable="false" />
+            </placeholder>
+            <element type="Text">
+                <text value="New Entity" />
+                <textalignment value="center" />
+            </element>
+            <popup>
+                <layout border="4 4 4 4" />   
+            </popup>         
+        </element>
+        <element />
+        <element type="DropDownList" name="NewComponentList">
+            <fixedsize value="90 22" />
+            <layout border="4 4 4 4" />
+            <placeholder>
+                <visible enable="false" />
+            </placeholder>
+            <element type="Text">
+                <text value="New Comp" />
+                <textalignment value="center" />
+            </element>
+            <popup>
+                <layout border="4 4 4 4" />   
+            </popup>
+        </element>
+        <element />
+    </element>
+</element>

BIN
Bin/Data/Fonts/BlueHighway.ttf


+ 5 - 0
Bin/Data/Materials/CloudPlane.xml

@@ -0,0 +1,5 @@
+<material>
+    <technique name="Techniques/DiffSkybox.xml" />
+    <texture unit="diffuse" name="Textures/CloudPlane.dds" />
+    <cull value="none" />
+</material>

+ 4 - 0
Bin/Data/Materials/Jack.xml

@@ -0,0 +1,4 @@
+<material>
+    <technique name="Techniques/NoTexture.xml" />
+    <parameter name="MatSpecProperties" value="0.5 16" />
+</material>

+ 5 - 0
Bin/Data/Materials/Mushroom.xml

@@ -0,0 +1,5 @@
+<material>
+    <technique name="Techniques/Diff.xml" />
+    <texture unit="diffuse" name="Textures/Mushroom.dds" />
+    <parameter name="MatSpecProperties" value="0.1 16" />
+</material>

+ 5 - 0
Bin/Data/Materials/Ninja.xml

@@ -0,0 +1,5 @@
+<material>
+    <technique name="Techniques/Diff.xml" />
+    <texture unit="diffuse" name="Textures/Ninja.dds" />
+    <parameter name="MatSpecProperties" value="0 0" />
+</material>

+ 4 - 0
Bin/Data/Materials/Particle.xml

@@ -0,0 +1,4 @@
+<material>
+    <technique name="Techniques/DiffVColAdd.xml" />
+    <texture unit="diffuse" name="Textures/Flare.dds" />
+</material>

+ 5 - 0
Bin/Data/Materials/Potion.xml

@@ -0,0 +1,5 @@
+<material>
+    <technique name="Techniques/NoTexture.xml" />
+    <parameter name="MatDiffColor" value="1 0 0 1" />
+    <parameter name="MatSpecProperties" value="0.25 16" />
+</material>

+ 4 - 0
Bin/Data/Materials/Smoke.xml

@@ -0,0 +1,4 @@
+<material>
+    <technique name="Techniques/DiffVColUnlitAlpha.xml" />
+    <texture unit="diffuse" name="Textures/Smoke.dds" />
+</material>

+ 5 - 0
Bin/Data/Materials/Snow.xml

@@ -0,0 +1,5 @@
+<material>
+    <technique name="Techniques/Diff.xml" />
+    <texture unit="diffuse" name="Textures/Snow.dds" />
+    <parameter name="MatSpecProperties" value="0.25 16" />
+</material>

+ 5 - 0
Bin/Data/Materials/SnowCrate.xml

@@ -0,0 +1,5 @@
+<material>
+    <technique name="Techniques/Diff.xml" />
+    <texture unit="diffuse" name="Textures/SnowCrate.dds" />
+    <parameter name="MatSpecProperties" value="0.15 16" />
+</material>

+ 7 - 0
Bin/Data/Materials/Test.xml

@@ -0,0 +1,7 @@
+<material>
+    <technique name="Techniques/DiffNormal.xml" quality="1" />
+    <technique name="Techniques/Diff.xml" quality="0" />
+    <texture unit="diffuse" name="Textures/Diffuse.dds" />
+    <texture unit="normal" name="Textures/Normal.dds" />
+    <parameter name="MatSpecProperties" value="0.5 16" />
+</material>

BIN
Bin/Data/Models/Box.mdl


BIN
Bin/Data/Models/CloudPlane.mdl


BIN
Bin/Data/Music/Ninja Gods.ogg


+ 20 - 0
Bin/Data/Particle/Smoke.xml

@@ -0,0 +1,20 @@
+<particleemitter>
+    <material name="Materials/Smoke.xml" />
+    <updateinvisible enable="true" />
+    <relative enable="false" />
+    <numparticles value="10" />
+    <activetime value="2" />
+    <inactivetime value="0" />
+    <interval value="0.075" />
+    <sorting enable="true" />
+    <rotationspeed min="-60" max="60" />
+    <direction min="-0.15 1 -0.15" max="0.15 1 0.15" />
+    <velocity min="100" max="130" />
+    <particlesize min="20 20" max="30 30" />
+    <sizedelta add="0" mul="1.3" />
+    <timetolive value="4" />
+    <constantforce value="0.0 -20 0.0" />
+    <colorfade color="0.2 0.2 0.2 0.0" time="0.0" />
+    <colorfade color="0.2 0.2 0.2 1.0" time="0.25" />
+    <colorfade color="0.6 0.6 0.6 0.0" time="4.0" />
+</particleemitter>

+ 16 - 0
Bin/Data/Particle/SnowExplosion.xml

@@ -0,0 +1,16 @@
+<particleemitter>
+    <material name="Materials/Particle.xml" />
+    <updateinvisible enable="true" />
+    <numparticles value="10" />
+    <activetime value="0.1" />
+    <inactivetime value="0" />
+    <interval value="0.02" />
+    <sorting enable="true" />
+    <direction min="-1 0 -1" max="1 1 1" />
+    <velocity min="50" max="100" />
+    <particlesize value="25 25" />
+    <timetolive value="0.5" />
+    <constantforce value="0.0 -200 0.0" />
+    <colorfade color="0.35 0.35 0.5 1.0" time="0.0" />
+    <colorfade color="0.0 0.0 0.0 1.0" time="0.5" />
+</particleemitter>

+ 18 - 0
Bin/Data/Particle/SnowExplosionBig.xml

@@ -0,0 +1,18 @@
+<particleemitter>
+    <material name="Materials/Particle.xml" />
+    <updateinvisible enable="true" />
+    <numparticles value="10" />
+    <emittertype value="box" />
+    <emittersize value="30 30 30" />
+    <activetime value="0.2" />
+    <inactivetime value="0" />
+    <interval value="0.02" />
+    <sorting enable="true" />
+    <direction min="-1 0.5 -1" max="1 1 1" />
+    <velocity min="200" max="300" />
+    <particlesize value="60 60" />
+    <timetolive value="0.5" />
+    <constantforce value="0.0 -400 0.0" />
+    <colorfade color="0.35 0.35 0.5 1.0" time="0.0" />
+    <colorfade color="0.0 0.0 0.0 1.0" time="0.5" />
+</particleemitter>

+ 173 - 0
Bin/Data/Scripts/AIController.as

@@ -0,0 +1,173 @@
+const float initialAggression = 0.0025;
+const float initialPrediction = 3000;
+const float initialAimSpeed = 5;
+const float deltaAggression = 0.00005;
+const float deltaPrediction = -20;
+const float deltaAimSpeed = 0.15;
+const float maxAggression = 0.01;
+const float maxPrediction = 2000;
+const float maxAimSpeed = 20;
+
+float aiAggression = initialAggression;
+float aiPrediction = initialPrediction;
+float aiAimSpeed = initialAimSpeed;
+
+void ResetAI()
+{
+    aiAggression = initialAggression;
+    aiPrediction = initialPrediction;
+    aiAimSpeed = initialAimSpeed;
+}
+
+void MakeAIHarder()
+{
+    aiAggression += deltaAggression;
+    if (aiAggression > maxAggression)
+        aiAggression = maxAggression;
+
+    aiPrediction += deltaPrediction;
+    if (aiPrediction < maxPrediction)
+        aiPrediction = maxPrediction;
+
+    aiAimSpeed += deltaAimSpeed;
+    if (aiAimSpeed > maxAimSpeed)
+        aiAimSpeed = maxAimSpeed;
+}
+
+class AIController
+{
+    void Control(Ninja@ ninja, Node@ node, float timeStep)
+    {
+        Scene@ scene = node.scene;
+
+        // Get closest ninja on the player's side
+        Node@ targetNode;
+        Ninja@ targetNinja;
+        Array<Node@> nodes = scene.GetScriptedChildren("Ninja", true);
+        float closestDistance = M_INFINITY;
+        for (uint i = 0; i < nodes.length; ++i)
+        {
+            Node@ otherNode = nodes[i];
+            Ninja@ otherNinja = cast<Ninja>(otherNode.scriptObject);
+            if (otherNinja.side == SIDE_PLAYER)
+            {
+                float distance = (node.position - otherNode.position).length;
+                if (distance < closestDistance)
+                {
+                    @targetNode = otherNode;
+                    @targetNinja = otherNinja;
+                    closestDistance = distance;
+                }
+            }
+        }
+
+        if ((targetNode !is null) && (targetNinja.health > 0))
+        {
+            RigidBody@ targetBody = targetNode.GetComponent("RigidBody");
+
+            ninja.controls.Set(CTRL_FIRE, false);
+            ninja.controls.Set(CTRL_JUMP, false);
+
+            float deltaX = 0.0f;
+            float deltaY = 0.0f;
+
+            // Aim from own head to target's feet
+            Vector3 ownPos(node.position + Vector3(0, 90, 0));
+            Vector3 targetPos(targetNode.position + Vector3(0, -90, 0));
+            float distance = (targetPos - ownPos).length;
+
+            // Use prediction according to target distance & estimated snowball speed
+            Vector3 currentAim(ninja.GetAim() * Vector3(0, 0, 1));
+            float predictDistance = distance;
+            if (predictDistance > 5000) predictDistance = 5000;
+            Vector3 predictedPos = targetPos + targetBody.linearVelocity * predictDistance / aiPrediction;
+            Vector3 targetAim = (predictedPos - ownPos);
+
+            // Add distance/height compensation
+            float compensation = Max(targetAim.length - 1500, 0);
+            targetAim += Vector3(0, 0.6, 0) * compensation;
+
+            // X-aiming
+            targetAim.Normalize();
+            Vector3 currentYaw(currentAim.x, 0, currentAim.z);
+            Vector3 targetYaw(targetAim.x, 0, targetAim.z);
+            currentYaw.Normalize();
+            targetYaw.Normalize();
+            deltaX = Clamp(Quaternion(currentYaw, targetYaw).yaw, -aiAimSpeed, aiAimSpeed);
+
+            // Y-aiming
+            Vector3 currentPitch(0, currentAim.y, 1);
+            Vector3 targetPitch(0, targetAim.y, 1);
+            currentPitch.Normalize();
+            targetPitch.Normalize();
+            deltaY = Clamp(Quaternion(currentPitch, targetPitch).pitch, -aiAimSpeed, aiAimSpeed);
+
+            ninja.controls.yaw += 0.1 * deltaX;
+            ninja.controls.pitch += 0.1 * deltaY;
+
+            // Firing? if close enough and relatively correct aim
+            if ((distance < 2500) && (currentAim.DotProduct(targetAim) > 0.75))
+            {
+                if (Random(1.0) < aiAggression)
+                    ninja.controls.Set(CTRL_FIRE, true);
+            }
+
+            // Movement
+            ninja.dirChangeTime -= timeStep;
+            if (ninja.dirChangeTime <= 0)
+            {
+                ninja.dirChangeTime = 0.5 + Random(1.0);
+                ninja.controls.Set(CTRL_UP|CTRL_DOWN|CTRL_LEFT|CTRL_RIGHT, false);
+
+                // Far distance: go forward
+                if (distance > 3000)
+                    ninja.controls.Set(CTRL_UP, true);
+                else if (distance > 600)
+                {
+                    // Medium distance: random strafing, predominantly forward
+                    float v = Random(1.0);
+                    if (v < 0.8)
+                        ninja.controls.Set(CTRL_UP, true);
+                    float h = Random(1.0);
+                    if (h < 0.3)
+                        ninja.controls.Set(CTRL_LEFT, true);
+                    if (h > 0.7)
+                        ninja.controls.Set(CTRL_RIGHT, true);
+                }
+                else
+                {
+                    // Close distance: random strafing backwards
+                    float v = Random(1.0);
+                    if (v < 0.8)
+                        ninja.controls.Set(CTRL_DOWN, true);
+                    float h = Random(1.0);
+                    if (h < 0.4)
+                        ninja.controls.Set(CTRL_LEFT, true);
+                    if (h > 0.6)
+                        ninja.controls.Set(CTRL_RIGHT, true);
+                }
+            }
+
+            // Random jump, if going forward
+            if ((ninja.controls.IsDown(CTRL_UP)) && (distance < 1000))
+            {
+                if (Random(1.0) < (aiAggression / 5.0))
+                    ninja.controls.Set(CTRL_JUMP, true);
+            }
+        }
+        else
+        {
+            // If no target, walk idly
+            ninja.controls.Set(CTRL_ALL, false);
+            ninja.controls.Set(CTRL_UP, true);
+            ninja.dirChangeTime -= timeStep;
+            if (ninja.dirChangeTime <= 0)
+            {
+                ninja.dirChangeTime = 1.0 + Random(2.0);
+                ninja.controls.yaw += 0.1 * (Random(600.0) - 300.0);
+            }
+            if (ninja.isSliding)
+                ninja.controls.yaw += 0.2;
+        }
+    }
+}

+ 188 - 0
Bin/Data/Scripts/GameObject.as

@@ -0,0 +1,188 @@
+const int CTRL_UP = 1;
+const int CTRL_DOWN = 2;
+const int CTRL_LEFT = 4;
+const int CTRL_RIGHT = 8;
+const int CTRL_FIRE = 16;
+const int CTRL_JUMP = 32;
+const int CTRL_ALL = 63;
+
+const int SIDE_NEUTRAL = 0;
+const int SIDE_PLAYER = 1;
+const int SIDE_ENEMY = 2;
+
+class GameObject : ScriptObject
+{
+    bool onGround;
+    bool isSliding;
+    float duration;
+    int health;
+    int maxHealth;
+    int side;
+    int lastDamageSide;
+
+    GameObject()
+    {
+        onGround = false;
+        isSliding = false;
+        duration = -1; // Infinite
+        health = 0;
+        maxHealth = 0;
+        side = SIDE_NEUTRAL;
+        lastDamageSide = SIDE_NEUTRAL;
+    }
+
+    void Create(const Vector3&in position, const Quaternion&in rotation)
+    {
+    }
+
+    void FixedUpdate(float timeStep)
+    {
+        // Disappear when duration expired
+        if (duration >= 0)
+        {
+            duration -= timeStep;
+            if (duration <= 0)
+                node.Remove();
+        }
+    }
+
+    bool Damage(GameObject@ origin, int amount)
+    {
+        if ((origin.side == side) || (health == 0))
+            return false;
+
+        lastDamageSide = origin.side;
+        health -= amount;
+        if (health < 0)
+            health = 0;
+        return true;
+    }
+
+    bool Heal(int amount)
+    {
+        // By default do not heal
+        return false;
+    }
+
+    void PlaySound(const String&in soundName)
+    {
+        // Create the sound channel
+        SoundSource3D@ source = node.CreateComponent("SoundSource3D");
+        Sound@ sound = cache.GetResource("Sound", soundName);
+
+        source.SetDistanceAttenuation(200, 5000, 1);
+        source.Play(sound);
+        source.autoRemove = true;
+    }
+
+    Node@ SpawnObject(const Vector3&in position, const Quaternion&in rotation, const String&in className)
+    {
+        Node@ newNode = scene.CreateChild();
+
+        // Create the script object with specified class
+        GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, className));
+        if (object !is null)
+            object.Create(position, rotation);
+
+        return newNode;
+    }
+
+    Node@ SpawnParticleEffect(const Vector3&in position, const String&in effectName, float duration)
+    {
+        Node@ newNode = scene.CreateChild();
+        newNode.position = position;
+
+        // Create the particle emitter
+        ParticleEmitter@ emitter = newNode.CreateComponent("ParticleEmitter");
+        emitter.parameters = cache.GetResource("XMLFile", effectName);
+
+        // Create a GameObject for managing the effect lifetime
+        GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject"));
+        object.duration = duration;
+
+        return newNode;
+    }
+
+    Node@ SpawnSound(const Vector3&in position, const String&in soundName, float duration)
+    {
+        Node@ newNode = scene.CreateChild();
+        newNode.position = position;
+
+        // Create the sound source
+        SoundSource3D@ source = newNode.CreateComponent("SoundSource3D");
+        Sound@ sound = cache.GetResource("Sound", soundName);
+        source.SetDistanceAttenuation(200, 5000, 1);
+        source.Play(sound);
+
+        // Create a GameObject for managing the sound lifetime
+        GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject"));
+        object.duration = duration;
+
+        return newNode;
+    }
+
+    void HandleNodeCollision(StringHash eventType, VariantMap& eventData)
+    {
+        Node@ otherNode = eventData["OtherNode"].GetNode();
+        CollisionShape@ otherShape = eventData["OtherShape"].GetCollisionShape();
+
+        // If the other collision shape belongs to static geometry, perform world collision
+        if (otherShape.collisionGroup == 2)
+            WorldCollision(eventData);
+
+        // If the other node is scripted, perform object-to-object collision
+        GameObject@ otherObject = cast<GameObject>(otherNode.scriptObject);
+        if (otherObject !is null)
+            ObjectCollision(otherObject, eventData);
+    }
+
+    void WorldCollision(VariantMap& eventData)
+    {
+        VectorBuffer contacts = eventData["Contacts"].GetBuffer();
+        while (!contacts.eof)
+        {
+            Vector3 contactPosition = contacts.ReadVector3();
+            Vector3 contactNormal = contacts.ReadVector3();
+            float contactDepth = contacts.ReadFloat();
+            float contactVelocity = contacts.ReadFloat();
+
+            // If contact is below node center and mostly vertical, assume it's ground contact
+            if (contactPosition.y < node.position.y)
+            {
+                float level = Abs(contactNormal.y);
+                if (level > 0.75)
+                    onGround = true;
+                else
+                {
+                    // If contact is somewhere inbetween vertical/horizontal, is sliding a slope
+                    if (level > 0.1)
+                        isSliding = true;
+                }
+            }
+        }
+
+        // Ground contact has priority over sliding contact
+        if (onGround == true)
+            isSliding = false;
+    }
+
+    void ObjectCollision(GameObject@ otherObject, VariantMap& eventData)
+    {
+    }
+
+    void ResetWorldCollision()
+    {
+        RigidBody@ body = node.GetComponent("RigidBody");
+        if (body.active)
+        {
+            onGround = false;
+            isSliding = false;
+        }
+        else
+        {
+            // If body is not active, assume it rests on the ground
+            onGround = true;
+            isSliding = false;
+        }
+    }
+}

+ 36 - 0
Bin/Data/Scripts/LightFlash.as

@@ -0,0 +1,36 @@
+#include "Scripts/GameObject.as"
+
+class LightFlash : GameObject
+{
+    LightFlash()
+    {
+    }
+
+    void Create(const Vector3&in position, const Quaternion&in rotation)
+    {
+        node.position = position;
+        node.rotation = rotation;
+
+        Light@ light = node.CreateComponent("Light");
+        light.lightType = LIGHT_POINT;
+        light.range = 500.0;
+        light.color = Color(2.0, 2.0, 2.0);
+        light.castShadows = true;
+        light.shadowResolution = 0.25;
+        light.rampTexture = cache.GetResource("Texture2D", "Textures/RampWide.png");
+        duration = 0.1;
+    }
+
+    void FixedUpdate(float timeStep)
+    {
+        Light@ light = node.GetComponent("Light");
+        light.color = light.color * Max(1.0 - timeStep * 10.0, 0.0);
+
+        if (duration >= 0)
+        {
+            duration -= timeStep;
+            if (duration <= 0)
+                node.Remove();
+        }
+    }
+}

+ 346 - 0
Bin/Data/Scripts/Ninja.as

@@ -0,0 +1,346 @@
+#include "Scripts/GameObject.as"
+#include "Scripts/AIController.as"
+
+const int ANIM_MOVE = 1;
+const int ANIM_ATTACK = 2;
+
+const float ninjaMass = 80;
+const float ninjaFriction = 0.5;
+const float ninjaMoveForce = 500000;
+const float ninjaAirMoveForce = 25000;
+const float ninjaDampingForce = 1000;
+const float ninjaJumpForce = 9000000;
+const Vector3 ninjaThrowVelocity(0, 425, 2000);
+const Vector3 ninjaThrowPosition(0, 20, 100);
+const float ninjaThrowDelay = 0.1;
+const float ninjaDrawDistance = 15000;
+const float ninjaCorpseDuration = 3;
+const int ninjaPoints = 250;
+
+class Ninja : GameObject
+{
+    Controls controls;
+    Controls prevControls;
+    AIController@ controller;
+    bool okToJump;
+    bool smoke;
+    float inAirTime;
+    float onGroundTime;
+    float throwTime;
+    float deathTime;
+    float deathDir;
+    float dirChangeTime;
+    float aimX;
+    float aimY;
+
+    Ninja()
+    {
+        health = maxHealth = 2;
+        okToJump = false;
+        smoke = false;
+        onGround = false;
+        isSliding = false;
+        inAirTime = 1;
+        onGroundTime = 0;
+        throwTime = 0;
+        deathTime = 0;
+        deathDir = 0;
+        dirChangeTime = 0;
+        aimX = 0;
+        aimY = 0;
+    }
+
+    void Start()
+    {
+        SubscribeToEvent("NodeCollision", "HandleNodeCollision");
+    }
+
+    void Create(const Vector3&in position, const Quaternion&in rotation)
+    {
+        node.position = position;
+        node.rotation = rotation;
+
+        // Create model
+        Node@ modelNode = node.CreateChild();
+        modelNode.position = Vector3(0, -90, 0);
+
+        AnimatedModel@ model = modelNode.CreateComponent("AnimatedModel");
+        model.model = cache.GetResource("Model", "Models/Ninja.mdl");
+        model.material = cache.GetResource("Material", "Materials/Ninja.xml");
+        model.drawDistance = ninjaDrawDistance;
+        model.castShadows = true;
+        model.invisibleLodFactor = 3.0f;
+
+        // Create animation controller
+        AnimationController@ controller = modelNode.CreateComponent("AnimationController");
+
+        // Create collision shape
+        CollisionShape@ shape = node.CreateComponent("CollisionShape");
+        shape.SetCapsule(35, 110, Vector3(), Quaternion(90, 0, 0));
+        shape.collisionGroup = 1;
+        shape.collisionMask = 3;
+        shape.friction = ninjaFriction;
+
+        // Create body
+        RigidBody@ body = node.CreateComponent("RigidBody");
+        body.mass = ninjaMass;
+        body.angularMaxVelocity = 0;
+
+        aimX = rotation.yaw;
+    }
+
+    void SetControls(const Controls&in newControls)
+    {
+        controls = newControls;
+    }
+
+    Quaternion GetAim()
+    {
+        Quaternion q = Quaternion(aimX, Vector3(0, 1, 0));
+        q = q * Quaternion(aimY, Vector3(1, 0, 0));
+        return q;
+    }
+
+    void FixedUpdate(float timeStep)
+    {
+        if (health <= 0)
+        {
+            DeathUpdate(timeStep);
+            return;
+        }
+
+        // AI control if controller exists
+        if (controller !is null)
+            controller.Control(this, node, timeStep);
+
+        RigidBody@ body = node.GetComponent("RigidBody");
+        AnimationController@ controller = node.children[0].GetComponent("AnimationController");
+
+        // Turning / horizontal aiming
+        if (aimX != controls.yaw)
+        {
+            aimX = controls.yaw;
+            body.active = true;
+        }
+        // Vertical aiming
+        if (aimY != controls.pitch)
+            aimY = controls.pitch;
+
+        // Force the physics rotation
+        Quaternion q(aimX, Vector3(0, 1, 0));
+        body.rotation = q;
+
+        // Movement ground/air
+        Vector3 vel = body.linearVelocity;
+        if (onGround)
+        {
+            inAirTime = 0;
+            onGroundTime += timeStep;
+        }
+        else
+        {
+            onGroundTime = 0;
+            inAirTime += timeStep;
+        }
+
+        if ((inAirTime < 0.3f) && (!isSliding))
+        {
+            bool sidemove = false;
+
+            // Movement in four directions
+            if (controls.IsDown(CTRL_UP|CTRL_DOWN|CTRL_LEFT|CTRL_RIGHT))
+            {
+                float animDir = 1.0f;
+                Vector3 force(0, 0, 0);
+                if (controls.IsDown(CTRL_UP))
+                    force += q * Vector3(0, 0, 1);
+                if (controls.IsDown(CTRL_DOWN))
+                {
+                    animDir = -1.0f;
+                    force += q * Vector3(0, 0, -1);
+                }
+                if (controls.IsDown(CTRL_LEFT))
+                {
+                    sidemove = true;
+                    force += q * Vector3(-1, 0, 0);
+                }
+                if (controls.IsDown(CTRL_RIGHT))
+                {
+                    sidemove = true;
+                    force += q * Vector3(1, 0, 0);
+                }
+                // Normalize so that diagonal strafing isn't faster
+                force.Normalize();
+                force *= ninjaMoveForce;
+                body.ApplyForce(force);
+
+                // Walk or sidestep animation
+                if (sidemove)
+                    controller.SetAnimation("Models/Ninja_Stealth.ani", ANIM_MOVE, true, false, animDir * 2.2, 1.0, 0.2, 0.0, true);
+                else
+                    controller.SetAnimation("Models/Ninja_Walk.ani", ANIM_MOVE, true, false, animDir * 1.6, 1.0, 0.2, 0.0, true);
+            }
+            else
+            {
+                // Idle animation
+                controller.SetAnimation("Models/Ninja_Idle3.ani", ANIM_MOVE, true, false, 1.0, 1.0, 0.2, 0.0, true);
+            }
+
+            // Overall damping to cap maximum speed
+            body.ApplyForce(Vector3(-ninjaDampingForce * vel.x, 0, -ninjaDampingForce * vel.z));
+
+            // Jumping
+            if (controls.IsDown(CTRL_JUMP))
+            {
+                if ((okToJump) && (inAirTime < 0.1f))
+                {
+                    // Lift slightly off the ground for better animation
+                    node.position = node.position + Vector3(0, 3, 0);
+                    body.ApplyForce(Vector3(0, ninjaJumpForce, 0));
+                    inAirTime = 1.0f;
+                    controller.SetAnimation("Models/Ninja_JumpNoHeight.ani", ANIM_MOVE, false, true, 1.0, 1.0, 0.0, 0.0, true);
+                    okToJump = false;
+                }
+            }
+            else okToJump = true;
+        }
+        else
+        {
+            // Motion in the air
+            // Note: when sliding a steep slope, control (or damping) isn't allowed!
+            if ((inAirTime > 0.3f) && (!isSliding))
+            {
+                if (controls.IsDown(CTRL_UP|CTRL_DOWN|CTRL_LEFT|CTRL_RIGHT))
+                {
+                    Vector3 force(0, 0, 0);
+                    if (controls.IsDown(CTRL_UP))
+                        force += q * Vector3(0, 0, 1);
+                    if (controls.IsDown(CTRL_DOWN))
+                        force += q * Vector3(0, 0, -1);
+                    if (controls.IsDown(CTRL_LEFT))
+                        force += q * Vector3(-1, 0, 0);
+                    if (controls.IsDown(CTRL_RIGHT))
+                        force += q * Vector3(1, 0, 0);
+                    // Normalize so that diagonal strafing isn't faster
+                    force.Normalize();
+                    force *= ninjaAirMoveForce;
+                    body.ApplyForce(force);
+                }
+            }
+
+            // Falling/jumping/sliding animation
+            if (inAirTime > 0.01f)
+                controller.SetAnimation("Models/Ninja_JumpNoHeight.ani", ANIM_MOVE, false, false, 1.0, 1.0, 0.2, 0.0, true);
+        }
+
+        // Shooting
+        if (throwTime >= 0)
+            throwTime -= timeStep;
+
+        if ((controls.IsPressed(CTRL_FIRE, prevControls)) && (throwTime <= 0))
+        {
+            Vector3 projectileVel = GetAim() * ninjaThrowVelocity;
+
+            controller.SetAnimation("Models/Ninja_Attack1.ani", ANIM_ATTACK, false, true, 1.0, 0.75, 0.0, 0.0, false);
+            controller.SetFade("Models/Ninja_Attack1.ani", 0.0, 0.5);
+            controller.priority["Models/Ninja_Attack1.ani"] = 1;
+
+            Node@ snowball = SpawnObject(node.position + vel * timeStep + q * ninjaThrowPosition, GetAim(), "SnowBall");
+            RigidBody@ snowballBody = snowball.GetComponent("RigidBody");
+            snowballBody.linearVelocity = projectileVel;
+            GameObject@ snowballObject = cast<GameObject>(snowball.scriptObject);
+            snowballObject.side = side;
+
+            PlaySound("Sounds/NutThrow.wav");
+
+            throwTime = ninjaThrowDelay;
+        }
+
+        prevControls = controls;
+
+        ResetWorldCollision();
+    }
+
+    void DeathUpdate(float timeStep)
+    {
+        RigidBody@ body = node.GetComponent("RigidBody");
+        CollisionShape@ shape = node.GetComponent("CollisionShape");
+        Node@ modelNode = node.children[0];
+        AnimationController@ controller = modelNode.GetComponent("AnimationController");
+        AnimatedModel@ model = modelNode.GetComponent("AnimatedModel");
+
+        Vector3 vel = body.linearVelocity;
+
+        // Overall damping to cap maximum speed
+        body.ApplyForce(Vector3(-ninjaDampingForce * vel.x, 0, -ninjaDampingForce * vel.z));
+
+        // Collide only to world geometry
+        shape.collisionMask = 2;
+
+        // Pick death animation on first death update
+        if (deathDir == 0)
+        {
+            if (Random(1.0) < 0.5)
+                deathDir = -1;
+            else
+                deathDir = 1;
+
+            PlaySound("Sounds/SmallExplosion.wav");
+
+            VariantMap eventData;
+            eventData["Points"] = ninjaPoints;
+            eventData["DamageSide"] = lastDamageSide;
+            SendEvent("Points", eventData);
+            SendEvent("Kill", eventData);
+        }
+
+        deathTime += timeStep;
+
+        // Move the model node to center the corpse mostly within the physics cylinder
+        // (because of the animation)
+        if (deathDir < 0)
+        {
+            // Backward death
+            controller.RemoveAnimations(ANIM_ATTACK, 0.1);
+            controller.SetAnimation("Models/Ninja_Death1.ani", ANIM_MOVE, false, false, 0.5, 1.0, 0.2, 0.0, true);
+            if ((deathTime >= 0.3) && (deathTime < 0.8))
+                modelNode.Translate(Vector3(0, 0, 425 * timeStep));
+        }
+        else if (deathDir > 0)
+        {
+            // Forward death
+            controller.RemoveAnimations(ANIM_ATTACK, 0.1);
+            controller.SetAnimation("Models/Ninja_Death2.ani", ANIM_MOVE, false, false, 0.5, 1.0, 0.2, 0.0, true);
+            if ((deathTime >= 0.4) && (deathTime < 0.8))
+                modelNode.Translate(Vector3(0, 0, -425 * timeStep));
+        }
+
+        // Create smokecloud just before vanishing
+        if ((deathTime > (ninjaCorpseDuration - 1)) && (!smoke))
+        {
+            SpawnParticleEffect(node.position + Vector3(0, -40, 0), "Particle/Smoke.xml", 8);
+            smoke = true;
+        }
+
+        if (deathTime > ninjaCorpseDuration)
+        {
+            SpawnObject(node.position + Vector3(0, -50, 0), Quaternion(), "LightFlash");
+            SpawnSound(node.position + Vector3(0, -50, 0), "Sounds/BigExplosion.wav", 2);
+            node.Remove();
+        }
+    }
+
+    bool Heal(int amount)
+    {
+        if (health == maxHealth)
+            return false;
+
+        health += amount;
+        if (health > maxHealth)
+            health = maxHealth;
+        // If player, play the "powerup" sound
+        if (side == SIDE_PLAYER)
+            PlaySound("Sounds/Powerup.wav");
+        return true;
+    }
+}

+ 494 - 0
Bin/Data/Scripts/NinjaSnowWar.as

@@ -0,0 +1,494 @@
+// Remake of NinjaSnowWar in script
+// Does not support load/save, or multiplayer yet.
+
+#include "Scripts/Ninja.as"
+#include "Scripts/LightFlash.as"
+#include "Scripts/Potion.as"
+#include "Scripts/SnowBall.as"
+#include "Scripts/SnowCrate.as"
+
+const float mouseSensitivity = 0.125;
+const float cameraMinDist = 25;
+const float cameraMaxDist = 500;
+const float cameraSafetyDist = 30;
+const int initialMaxEnemies = 5;
+const int finalMaxEnemies = 25;
+const int maxPowerups = 5;
+const int incrementEach = 10;
+const int playerHealth = 20;
+const float enemySpawnRate = 1;
+const float powerupSpawnRate = 15;
+
+Scene@ gameScene;
+Node@ gameCameraNode;
+Camera@ gameCamera;
+Text@ scoreText;
+Text@ hiscoreText;
+Text@ messageText;
+BorderImage@ healthBar;
+BorderImage@ sight;
+SoundSource@ musicSource;
+
+Controls playerControls;
+Controls prevPlayerControls;
+bool gameOn = false;
+bool drawDebug = false;
+bool drawOctreeDebug = false;
+int score = 0;
+int hiscore = 0;
+int maxEnemies = 0;
+int incrementCounter = 0;
+float enemySpawnTimer = 0;
+float powerupSpawnTimer = 0;
+
+void Start()
+{
+    InitAudio();
+    InitConsole();
+    InitScene();
+    CreateCamera();
+    CreateOverlays();
+    StartGame();
+
+    SubscribeToEvent("Update", "HandleUpdate");
+    SubscribeToEvent(gameScene.physicsWorld, "PhysicsPreStep", "HandleFixedUpdate");
+    SubscribeToEvent(gameScene, "ScenePostUpdate", "HandlePostUpdate");
+    SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
+    SubscribeToEvent("Points", "HandlePoints");
+    SubscribeToEvent("Kill", "HandleKill");
+    SubscribeToEvent("KeyDown", "HandleKeyDown");
+    SubscribeToEvent("ScreenMode", "HandleScreenMode");
+}
+
+void InitAudio()
+{
+    // Lower mastervolumes slightly
+    audio.masterGain[SOUND_MASTER] =  0.75f;
+    audio.masterGain[SOUND_MUSIC] = 0.75;
+
+    // Start music playback
+    Sound@ musicFile = cache.GetResource("Sound", "Music/Ninja Gods.ogg");
+    musicFile.looped = true;
+    musicSource = SoundSource();
+    musicSource.Play(musicFile);
+}
+
+void InitConsole()
+{
+    if (engine.headless)
+        return;
+
+    XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+
+    Console@ console = engine.CreateConsole();
+    console.style = uiStyle;
+    console.numRows = 16;
+
+    engine.CreateDebugHud();
+    debugHud.style = uiStyle;
+}
+
+void InitScene()
+{
+    gameScene = Scene("NinjaSnowWar");
+
+    // Create the static level programmatically
+    Octree@ octree = gameScene.CreateComponent("Octree");
+    octree.Resize(BoundingBox(-20000, 20000), 7);
+
+    PhysicsWorld@ world = gameScene.CreateComponent("PhysicsWorld");
+    world.fps = 200;
+    world.gravity = Vector3(0, -981, 0);
+    world.linearRestThreshold = 0.1;
+    world.linearDampingThreshold = 0;
+    world.linearDampingScale = 0.001;
+
+    gameScene.CreateComponent("DebugRenderer");
+
+    Node@ zoneNode = gameScene.CreateChild("Zone");
+    Zone@ zone = zoneNode.CreateComponent("Zone");
+    zone.boundingBox = BoundingBox(-100000, 100000);
+    zone.ambientColor = Color(0.2, 0.2, 0.7);
+    zone.fogColor = Color(0.2, 0.2, 0.7);
+    zone.fogStart = 5000;
+    zone.fogEnd = 15000;
+    
+    Node@ lightNode = gameScene.CreateChild("Sunlight");
+    lightNode.rotation = Quaternion(0.888074, 0.325058, -0.325058, 0);
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.castShadows = true;
+    light.shadowNearFarRatio = 0.002;
+    light.shadowBias = BiasParameters(0.00025, 0.001);
+    light.shadowCascade = CascadeParameters(2, 0.5, 0.2, 5000);
+    light.shadowFocus = FocusParameters(true, true, true, 50, 900);
+
+    Node@ staticNode = gameScene.CreateChild("Static");
+    StaticModel@ staticModel = staticNode.CreateComponent("StaticModel");
+    staticModel.model = cache.GetResource("Model", "Models/Level.mdl");
+    staticModel.material = cache.GetResource("Material", "Materials/Snow.xml");
+    CollisionShape@ shape = staticNode.CreateComponent("CollisionShape");
+    shape.SetTriangleMesh(cache.GetResource("Model", "Models/Level.mdl"), 0, Vector3(), Quaternion());
+    shape.collisionGroup = 2;
+    shape.collisionMask = 3;
+
+    Node@ skyNode = gameScene.CreateChild("Sky");
+    skyNode.position = Vector3(0, 3000, 0);
+    skyNode.scale = Vector3(30000, 1, 30000);
+    Skybox@ skybox = skyNode.CreateComponent("Skybox");
+    skybox.model = cache.GetResource("Model", "Models/CloudPlane.mdl");
+    skybox.material = cache.GetResource("Material", "Materials/CloudPlane.xml");
+
+    // Enable access to this script file & scene from the console
+    script.defaultScene = gameScene;
+    script.defaultScriptFile = scriptFile;
+}
+
+void CreateCamera()
+{
+    gameCameraNode = Node();
+    gameCameraNode.position = Vector3(0, 200, -1000);
+
+    gameCamera = gameCameraNode.CreateComponent("Camera");
+    gameCamera.nearClip = 10.0;
+    gameCamera.farClip = 16000.0;
+
+    if (!engine.headless)
+        renderer.viewports[0] = Viewport(gameScene, gameCamera);
+}
+
+void CreateOverlays()
+{
+    if (engine.headless)
+        return;
+
+    int height = graphics.height / 22;
+    if (height > 64)
+        height = 64;
+
+    sight = BorderImage();
+    sight.texture = cache.GetResource("Texture2D", "Textures/Sight.png");
+    sight.SetAlignment(HA_CENTER, VA_CENTER);
+    sight.SetSize(height, height);
+    ui.rootElement.AddChild(sight);
+
+    Font@ font = cache.GetResource("Font", "Fonts/BlueHighway.ttf");
+
+    scoreText = Text();
+    scoreText.SetFont(font, 17);
+    scoreText.SetAlignment(HA_LEFT, VA_TOP);
+    scoreText.SetPosition(5, 5);
+    scoreText.colors[C_BOTTOMLEFT] = Color(1, 1, 0.25);
+    scoreText.colors[C_BOTTOMRIGHT] = Color(1, 1, 0.25);
+    ui.rootElement.AddChild(scoreText);
+
+    @hiscoreText = Text();
+    hiscoreText.SetFont(font, 17);
+    hiscoreText.SetAlignment(HA_RIGHT, VA_TOP);
+    hiscoreText.SetPosition(-5, 5);
+    hiscoreText.colors[C_BOTTOMLEFT] = Color(1, 1, 0.25);
+    hiscoreText.colors[C_BOTTOMRIGHT] = Color(1, 1, 0.25);
+    ui.rootElement.AddChild(hiscoreText);
+
+    @messageText = Text();
+    messageText.SetFont(font, 17);
+    messageText.SetAlignment(HA_CENTER, VA_CENTER);
+    messageText.SetPosition(0, -height * 2);
+    messageText.color = Color(1, 0, 0);
+    ui.rootElement.AddChild(messageText);
+
+    BorderImage@ healthBorder = BorderImage();
+    healthBorder.texture = cache.GetResource("Texture2D", "Textures/HealthBarBorder.png");
+    healthBorder.SetAlignment(HA_CENTER, VA_TOP);
+    healthBorder.SetPosition(0, 8);
+    healthBorder.SetSize(120, 20);
+    ui.rootElement.AddChild(healthBorder);
+
+    healthBar = BorderImage();
+    healthBar.texture = cache.GetResource("Texture2D", "Textures/HealthBarInside.png");
+    healthBar.SetPosition(2, 2);
+    healthBar.SetSize(116, 16);
+    healthBorder.AddChild(healthBar);
+}
+
+void StartGame()
+{
+    // Clear the scene of all existing scripted objects
+    {
+        Array<Node@> scriptedNodes = gameScene.GetScriptedChildren(true);
+        for (uint i = 0; i < scriptedNodes.length; ++i)
+            scriptedNodes[i].Remove();
+    }
+
+    Node@ playerNode = gameScene.CreateChild("Player");
+    Ninja@ playerNinja = cast<Ninja>(playerNode.CreateScriptObject(scriptFile, "Ninja"));
+    playerNinja.Create(Vector3(0, 90, 0), Quaternion());
+    playerNinja.health = playerNinja.maxHealth = playerHealth;
+    playerNinja.side = SIDE_PLAYER;
+    // Make sure the player can not shoot on first frame by holding the button down
+    playerNinja.controls = playerNinja.prevControls = playerControls;
+
+    ResetAI();
+
+    gameOn = true;
+    score = 0;
+    maxEnemies = initialMaxEnemies;
+    incrementCounter = 0;
+    enemySpawnTimer = 0;
+    powerupSpawnTimer = 0;
+    playerControls.yaw = 0;
+    playerControls.pitch = 0;
+
+    messageText.text = "";
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    float timeStep = eventData["TimeStep"].GetFloat();
+
+    if (input.keyPress[KEY_F1])
+        debugHud.ToggleAll();
+    if (input.keyPress[KEY_F2])
+        drawDebug = !drawDebug;
+    if (input.keyPress[KEY_F3])
+        drawOctreeDebug = !drawOctreeDebug;
+
+    if (input.keyPress[KEY_F5])
+        gameScene.Save(File(fileSystem.programDir + "Data/Save.dat", FILE_WRITE));
+
+    if ((!console.visible) && (input.keyPress['P']) && (gameOn))
+    {
+        gameScene.active = !gameScene.active;
+        if (!gameScene.active)
+            messageText.text = "PAUSED";
+        else
+            messageText.text = "";
+    }
+
+    if (input.keyPress[KEY_ESC])
+    {
+        if (!console.visible)
+            engine.Exit();
+        else
+            console.visible = false;
+    }
+
+    if (gameScene.active)
+        UpdateControls();
+}
+
+void HandleFixedUpdate(StringHash eventType, VariantMap& eventData)
+{
+    float timeStep = eventData["TimeStep"].GetFloat();
+
+    // Spawn new objects and check for end/restart of game
+    SpawnObjects(timeStep);
+    CheckEndAndRestart();
+}
+
+void HandlePostUpdate()
+{
+    UpdateCamera();
+    UpdateStatus();
+}
+
+void HandlePostRenderUpdate()
+{
+    if (drawDebug)
+        gameScene.physicsWorld.DrawDebugGeometry(true);
+    if (drawOctreeDebug)
+        gameScene.octree.DrawDebugGeometry(true);
+}
+
+void HandlePoints(StringHash eventType, VariantMap& eventData)
+{
+    if (eventData["DamageSide"].GetInt() == SIDE_PLAYER)
+    {
+        score += eventData["Points"].GetInt();
+        if (score > hiscore)
+            hiscore = score;
+    }
+}
+
+void HandleKill(StringHash eventType, VariantMap& eventData)
+{
+    if (eventData["DamageSide"].GetInt() == SIDE_PLAYER)
+    {
+        MakeAIHarder();
+
+        // Increment amount of simultaneous enemies after enough kills
+        incrementCounter++;
+        if (incrementCounter >= incrementEach)
+        {
+            incrementCounter = 0;
+            if (maxEnemies < finalMaxEnemies)
+                maxEnemies++;
+        }
+    }
+}
+
+void SpawnObjects(float timeStep)
+{
+    // Spawn powerups
+    powerupSpawnTimer += timeStep;
+    if (powerupSpawnTimer >= powerupSpawnRate)
+    {
+        powerupSpawnTimer = 0;
+        int numPowerups = gameScene.GetScriptedChildren("SnowCrate", true).length + gameScene.GetScriptedChildren("Potion", true).length;
+
+        if (numPowerups < maxPowerups)
+        {
+            const float maxOffset = 4000;
+            float xOffset = Random(maxOffset * 2.0f) - maxOffset;
+            float zOffset = Random(maxOffset * 2.0f) - maxOffset;
+
+            Vector3 position(xOffset, 5000, zOffset);
+            Node@ crateNode = gameScene.CreateChild();
+            GameObject@ crateObject = cast<GameObject>(crateNode.CreateScriptObject(scriptFile, "SnowCrate"));
+            crateObject.Create(position, Quaternion());
+        }
+    }
+
+    // Spawn enemies
+    enemySpawnTimer += timeStep;
+    if (enemySpawnTimer > enemySpawnRate)
+    {
+        enemySpawnTimer = 0;
+        // Take the player ninja into account
+        int numEnemies = gameScene.GetScriptedChildren("Ninja", true).length - 1;
+
+        if (numEnemies < maxEnemies)
+        {
+            const float maxOffset = 4000;
+            float offset = Random(maxOffset * 2.0) - maxOffset;
+            // Random north/east/south/west direction
+            int dir = RandomInt() & 3;
+            dir *= 90;
+            Quaternion q(dir, Vector3(0, 1, 0));
+            Vector3 position(q * Vector3(offset, 1000, -12000));
+
+            Node@ enemyNode = gameScene.CreateChild();
+            Ninja@ enemyNinja = cast<Ninja>(enemyNode.CreateScriptObject(scriptFile, "Ninja"));
+            enemyNinja.Create(position, q);
+            enemyNinja.side = SIDE_ENEMY;
+            @enemyNinja.controller = AIController();
+            RigidBody@ enemyBody = enemyNode.GetComponent("RigidBody");
+            enemyBody.linearVelocity = (q * Vector3(0, 1000, 3000));
+        }
+    }
+}
+
+void CheckEndAndRestart()
+{
+    if ((gameOn) && (gameScene.GetChild("Player", true) is null))
+    {
+        gameOn = false;
+        messageText.text = "Press Fire or Jump to restart!";
+        return;
+    }
+
+    if ((!gameOn) && (playerControls.IsPressed(CTRL_FIRE | CTRL_JUMP, prevPlayerControls)))
+        StartGame();
+}
+
+void UpdateControls()
+{
+    prevPlayerControls = playerControls;
+    playerControls.Set(CTRL_ALL, false);
+
+    if (!console.visible)
+    {
+        if (input.keyDown['W'])
+            playerControls.Set(CTRL_UP, true);
+        if (input.keyDown['S'])
+            playerControls.Set(CTRL_DOWN, true);
+        if (input.keyDown['A'])
+            playerControls.Set(CTRL_LEFT, true);
+        if (input.keyDown['D'])
+            playerControls.Set(CTRL_RIGHT, true);
+        if (input.keyDown[KEY_CTRL])
+            playerControls.Set(CTRL_FIRE, true);
+        if (input.keyDown[' '])
+            playerControls.Set(CTRL_JUMP, true);
+    }
+
+    if (input.mouseButtonDown[MOUSEB_LEFT])
+        playerControls.Set(CTRL_FIRE, true);
+    if (input.mouseButtonDown[MOUSEB_RIGHT])
+        playerControls.Set(CTRL_JUMP, true);
+
+    playerControls.yaw += mouseSensitivity * input.mouseMoveX;
+    playerControls.pitch += mouseSensitivity * input.mouseMoveY;
+    playerControls.pitch = Clamp(playerControls.pitch, -60, 60);
+
+    Node@ playerNode = gameScene.GetChild("Player", true);
+    if (playerNode !is null)
+    {
+        Ninja@ playerNinja = cast<Ninja>(playerNode.scriptObject);
+        playerNinja.controls = playerControls;
+    }
+}
+
+void UpdateCamera()
+{
+    Node@ playerNode = gameScene.GetChild("Player", true);
+    if (playerNode is null)
+        return;
+
+    Vector3 pos = playerNode.position;
+    Quaternion dir;
+    dir = dir * Quaternion(playerControls.yaw, Vector3(0, 1, 0));
+    dir = dir * Quaternion(playerControls.pitch, Vector3(1, 0, 0));
+
+    Vector3 aimPoint = pos + Vector3(0, 100, 0);
+    Vector3 minDist = aimPoint + dir * Vector3(0, 0, -cameraMinDist);
+    Vector3 maxDist = aimPoint + dir * Vector3(0, 0, -cameraMaxDist);
+
+    // Collide camera ray with static objects (collision mask 2)
+    Vector3 rayDir = (maxDist - minDist).GetNormalized();
+    float rayDistance = cameraMaxDist - cameraMinDist + cameraSafetyDist;
+    Array<PhysicsRaycastResult>@ result = gameScene.physicsWorld.Raycast(Ray(minDist, rayDir), rayDistance, 2);
+    if (result.length > 0)
+        rayDistance = Min(rayDistance, result[0].distance - cameraSafetyDist);
+
+    gameCameraNode.position = minDist + rayDir * rayDistance;
+    gameCameraNode.rotation = dir;
+
+    audio.listenerPosition = pos;
+    audio.listenerRotation = dir;
+}
+
+void UpdateStatus()
+{
+    if (engine.headless)
+        return;
+
+    scoreText.text = "Score " + score;
+    hiscoreText.text = "Hiscore " + hiscore;
+
+    Node@ playerNode = gameScene.GetChild("Player", true);
+    if (playerNode is null)
+        return;
+
+    GameObject@ object = cast<GameObject>(playerNode.scriptObject);
+    healthBar.width = 116 * object.health / playerHealth;
+}
+
+void HandleKeyDown(StringHash eventType, VariantMap& eventData)
+{
+    // Check for toggling the console
+    if (eventData["Key"].GetInt() == 220)
+    {
+        console.Toggle();
+        input.SuppressNextChar();
+    }
+}
+
+void HandleScreenMode()
+{
+    int height = graphics.height / 22;
+    if (height > 64)
+        height = 64;
+    sight.SetSize(height, height);
+    messageText.SetPosition(0, -height * 2);
+}

+ 58 - 0
Bin/Data/Scripts/Potion.as

@@ -0,0 +1,58 @@
+#include "Scripts/GameObject.as"
+
+const int potionHealAmount = 5;
+const float potionMass = 10;
+const float potionFriction = 0.5;
+const float potionDrawDistance = 15000;
+
+class Potion : GameObject
+{
+    int healAmount;
+
+    Potion()
+    {
+        healAmount = potionHealAmount;
+    }
+
+    void Start()
+    {
+        SubscribeToEvent("NodeCollision", "HandleNodeCollision");
+    }
+    
+    void Create(const Vector3&in position, const Quaternion&in rotation)
+    {
+        node.position = position;
+        node.rotation = rotation;
+
+        // Create model
+        StaticModel@ model = node.CreateComponent("StaticModel");
+        model.model = cache.GetResource("Model", "Models/Potion.mdl");
+        model.material = cache.GetResource("Material", "Materials/Potion.xml");
+        model.drawDistance = potionDrawDistance;
+        model.castShadows = true;
+
+        // Create collision shape
+        CollisionShape@ shape = node.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(20, 40, 20), Vector3(), Quaternion());
+        shape.collisionGroup = 1;
+        shape.collisionMask = 3;
+        shape.friction = potionFriction;
+
+        // Create body
+        RigidBody@ body = node.CreateComponent("RigidBody");
+        body.mass = potionMass;
+    }
+    
+    void ObjectCollision(GameObject@ otherObject, VariantMap& eventData)
+    {
+        if (healAmount > 0)
+        {
+            if (otherObject.Heal(healAmount))
+            {
+                // Could also remove the potion directly, but this way it gets removed on next update
+                healAmount = 0;
+                duration = 0;
+            }
+        }
+    }
+}

+ 103 - 0
Bin/Data/Scripts/SnowBall.as

@@ -0,0 +1,103 @@
+#include "Scripts/GameObject.as"
+
+const float snowballMass = 10;
+const float snowballFriction = 0.5;
+const float snowballDampingForce = 20;
+const float snowballMinHitSpeed = 100;
+const float snowballDuration = 5;
+const float snowballGroundHitDuration = 1;
+const float snowballObjectHitDuration = 0;
+const float snowballDrawDistance = 7500;
+const int snowballDamage = 1;
+
+class SnowBall : GameObject
+{
+    int hitDamage;
+
+    SnowBall()
+    {
+        duration = snowballDuration;
+        hitDamage = snowballDamage;
+    }
+
+    void Start()
+    {
+        SubscribeToEvent("NodeCollision", "HandleNodeCollision");
+    }
+
+    void Create(const Vector3&in position, const Quaternion&in rotation)
+    {
+        node.position = position;
+        node.rotation = rotation;
+
+        // Create model
+        StaticModel@ model = node.CreateComponent("StaticModel");
+        model.model = cache.GetResource("Model", "Models/SnowBall.mdl");
+        model.material = cache.GetResource("Material", "Materials/Snow.xml");
+        model.drawDistance = snowballDrawDistance;
+        model.castShadows = true;
+    
+        // Create collision shape
+        CollisionShape@ shape = node.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(15, 15, 15), Vector3(), Quaternion());
+        shape.collisionGroup = 1;
+        shape.collisionMask = 3;
+        shape.friction = snowballFriction;
+
+        // Create body
+        RigidBody@ body = node.CreateComponent("RigidBody");
+        body.mass = snowballMass;
+    }
+    
+    void FixedUpdate(float timeStep)
+    {
+        // Apply damping when rolling on the ground, or near disappearing
+        RigidBody@ body = node.GetComponent("RigidBody");
+        if ((onGround) || (duration < snowballGroundHitDuration))
+        {
+            Vector3 vel = body.linearVelocity;
+            body.ApplyForce(Vector3(-snowballDampingForce * vel.x, 0, -snowballDampingForce * vel.z));
+        }
+
+        // Disappear when duration expired
+        if (duration >= 0)
+        {
+            duration -= timeStep;
+            if (duration <= 0)
+            {
+                SpawnParticleEffect(node.position, "Particle/SnowExplosion.xml", 1);
+                node.Remove();
+            }
+        }
+    }
+    
+    void WorldCollision(VariantMap& eventData)
+    {
+        GameObject::WorldCollision(eventData);
+        
+        // If hit the ground, disappear after a short while
+        if (duration > snowballGroundHitDuration)
+            duration = snowballGroundHitDuration;
+    }
+
+    void ObjectCollision(GameObject@ otherObject, VariantMap& eventData)
+    {
+        if (hitDamage > 0)
+        {
+            RigidBody@ body = node.GetComponent("RigidBody");
+            if ((body.linearVelocity.length >= snowballMinHitSpeed))
+            {
+                if (side != otherObject.side)
+                {
+                    otherObject.Damage(this, hitDamage);
+                    // Create a temp entity for the hit sound
+                    SpawnSound(node.position, "Sounds/PlayerFistHit.wav", 0.2);
+                }
+
+                hitDamage = 0;
+            }
+        }
+        if (duration > snowballObjectHitDuration)
+            duration = snowballObjectHitDuration;
+    }
+}

+ 60 - 0
Bin/Data/Scripts/SnowCrate.as

@@ -0,0 +1,60 @@
+#include "Scripts/GameObject.as"
+
+const int snowcrateHealth = 5;
+const float snowcrateMass = 200;
+const float snowcrateFriction = 0.35;
+const float snowcrateDrawDistance = 15000;
+const int snowcratePoints = 250;
+
+class SnowCrate : GameObject
+{
+    SnowCrate()
+    {
+        health = maxHealth = snowcrateHealth;
+    }
+    
+    void Start()
+    {
+        SubscribeToEvent("NodeCollision", "HandleNodeCollision");
+    }
+
+    void Create(const Vector3&in position, const Quaternion&in rotation)
+    {
+        node.position = position;
+        node.rotation = rotation;
+
+        // Create model
+        StaticModel@ model = node.CreateComponent("StaticModel");
+        model.model = cache.GetResource("Model", "Models/SnowCrate.mdl");
+        model.material = cache.GetResource("Material", "Materials/SnowCrate.xml");
+        model.drawDistance = snowcrateDrawDistance;
+        model.castShadows = true;
+
+        // Create collision shape
+        CollisionShape@ shape = node.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(80, 80, 80), Vector3(), Quaternion());
+        shape.collisionGroup = 2;
+        shape.collisionMask = 3;
+        shape.friction = snowcrateFriction;
+
+        // Create body
+        RigidBody@ body = node.CreateComponent("RigidBody");
+        body.mass = snowcrateMass;
+    }
+    
+    void FixedUpdate(float timeStep)
+    {
+        if (health <= 0)
+        {
+            SpawnParticleEffect(node.position, "Particle/SnowExplosionBig.xml", 2);
+            SpawnObject(node.position, Quaternion(), "Potion");
+
+            VariantMap eventData;
+            eventData["Points"] = snowcratePoints;
+            eventData["DamageSide"] = lastDamageSide;
+            SendEvent("Points", eventData);
+        
+            node.Remove();
+        }
+    }
+}

+ 390 - 0
Bin/Data/Scripts/TestScene.as

@@ -0,0 +1,390 @@
+Scene@ testScene;
+Camera@ camera;
+Node@ cameraNode;
+
+float yaw = 0.0;
+float pitch = 0.0;
+int drawDebug = 0;
+
+void Start()
+{
+    if (engine.headless)
+    {
+        ErrorDialog("TestScene", "Headless mode is not supported. The program will now exit.");
+        engine.Exit();
+        return;
+    }
+
+    InitConsole();
+    InitUI();
+    InitScene();
+
+    SubscribeToEvent("Update", "HandleUpdate");
+    SubscribeToEvent("KeyDown", "HandleKeyDown");
+    SubscribeToEvent("MouseMove", "HandleMouseMove");
+    SubscribeToEvent("MouseButtonDown", "HandleMouseButtonDown");
+    SubscribeToEvent("MouseButtonUp", "HandleMouseButtonUp");
+    SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
+}
+
+void InitConsole()
+{
+    XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+
+    engine.CreateDebugHud();
+    debugHud.style = uiStyle;
+    debugHud.mode = DEBUGHUD_SHOW_ALL;
+
+    engine.CreateConsole();
+    console.style = uiStyle;
+}
+
+void InitUI()
+{
+    XMLFile@ uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+
+    Cursor@ newCursor = Cursor("Cursor");
+    newCursor.style = uiStyle;
+    newCursor.position = IntVector2(graphics.width / 2, graphics.height / 2);
+    ui.cursor = newCursor;
+}
+
+void InitScene()
+{
+    testScene = Scene("TestScene");
+
+    PhysicsWorld@ world = testScene.CreateComponent("PhysicsWorld");
+    testScene.CreateComponent("Octree");
+    testScene.CreateComponent("DebugRenderer");
+
+    world.gravity = Vector3(0.0, -9.81, 0.0);
+    world.fps = 100;
+    world.linearRestThreshold = 0.1;
+    world.angularRestThreshold = 0.1;
+    world.contactSurfaceLayer = 0.001;
+
+    Zone@ zone = testScene.CreateComponent("Zone");
+    zone.ambientColor = Color(0.1, 0.1, 0.1);
+    zone.fogColor = Color(0.5, 0.5, 0.7);
+    zone.fogStart = 100.0;
+    zone.fogEnd = 300.0;
+    zone.boundingBox = BoundingBox(-1000.0, 1000.0);
+
+    {
+        Node@ objectNode = testScene.CreateChild("Floor");
+        objectNode.position = Vector3(0.0, -0.5, 0.0);
+        objectNode.scale = Vector3(100.0, 0.5, 100.0);
+
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Box.mdl");
+        object.material = cache.GetResource("Material", "Materials/Test.xml");
+        object.occluder = true;
+
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(2.0, 2.0, 2.0), Vector3(), Quaternion());
+        shape.collisionGroup = 2;
+        shape.collisionMask = 1;
+    }
+
+    for (uint i = 0; i < 50; ++i)
+    {
+        Node@ objectNode = testScene.CreateChild("Box");
+        objectNode.position = Vector3(Random(180.0) - 90.0, 1.0, Random(180.0) - 90.0);
+
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Box.mdl");
+        object.material = cache.GetResource("Material", "Materials/Test.xml");
+        object.castShadows = true;
+
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(2.0, 2.0, 2.0), Vector3(), Quaternion());
+        shape.collisionGroup = 2;
+        shape.collisionMask = 1;
+    }
+
+    for (uint i = 0; i < 10; ++i)
+    {
+        Node@ objectNode = testScene.CreateChild("Box");
+        objectNode.position = Vector3(Random(180.0) - 90.0, 10.0, Random(180.0) - 90.0);
+        objectNode.SetScale(10.0);
+
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Box.mdl");
+        object.material = cache.GetResource("Material", "Materials/Test.xml");
+        object.castShadows = true;
+        object.occluder = true;
+
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(2.0, 2.0, 2.0), Vector3(), Quaternion());
+        shape.collisionGroup = 2;
+        shape.collisionMask = 1;
+    }
+
+    for (uint i = 0; i < 50; ++i)
+    {
+        Node@ objectNode = testScene.CreateChild("Mushroom");
+        objectNode.position = Vector3(Random(180.0) - 90.0, 0.0, Random(180.0) - 90.0);
+        objectNode.rotation = Quaternion(0.0, Random(360.0), 0.0);
+        objectNode.SetScale(5.0);
+
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Mushroom.mdl");
+        object.material = cache.GetResource("Material", "Materials/Mushroom.xml");
+        object.castShadows = true;
+
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetTriangleMesh(cache.GetResource("Model", "Models/Mushroom.mdl"), 0, Vector3(), Quaternion());
+        shape.collisionGroup = 2;
+        shape.collisionMask = 1;
+    }
+
+    for (uint i = 0; i < 50; ++i)
+    {
+        Node@ objectNode = testScene.CreateChild("Jack");
+        objectNode.position = Vector3(Random(180.0) - 90.0, 0.0, Random(180.0) - 90.0);
+        objectNode.rotation = Quaternion(0.0, Random(360.0), 0.0);
+
+        AnimatedModel@ object = objectNode.CreateComponent("AnimatedModel");
+        object.model = cache.GetResource("Model", "Models/Jack.mdl");
+        object.material = cache.GetResource("Material", "Materials/Jack.xml");
+        object.castShadows = true;
+
+        AnimationController@ ctrl = objectNode.CreateComponent("AnimationController");
+        ctrl.SetAnimation("Models/Jack_Walk.ani", 1, true, true, 1.0, 1.0, 0.0, 0.0, false);
+    }
+
+    {
+        Node@ lightNode = testScene.CreateChild("Light");
+        lightNode.direction = Vector3(0.5, -0.5, 0.5);
+
+        Light@ light = lightNode.CreateComponent("Light");
+        light.lightType = LIGHT_DIRECTIONAL;
+        light.castShadows = true;
+        light.shadowBias = BiasParameters(0.0001, 0.5);
+        light.shadowCascade = CascadeParameters(3, 0.90, 0.2, 200.0);
+        light.specularIntensity = 0.5f;
+    }
+
+    // Enable access to this script file & scene from the console
+    script.defaultScene = testScene;
+    script.defaultScriptFile = scriptFile;
+
+    // Create the camera outside the scene so it is unaffected by scene load/save
+    cameraNode = Node();
+    camera = cameraNode.CreateComponent("Camera");
+    cameraNode.position = Vector3(0, 2, 0);
+
+    renderer.viewports[0] = Viewport(testScene, camera);
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    float timeStep = eventData["TimeStep"].GetFloat();
+
+    if (ui.focusElement is null)
+    {
+        float speedMultiplier = 1.0;
+        if (input.keyDown[KEY_SHIFT])
+            speedMultiplier = 5.0;
+        if (input.keyDown[KEY_CTRL])
+            speedMultiplier = 0.1;
+
+        if (input.keyDown['W'])
+            cameraNode.TranslateRelative(Vector3(0, 0, 10) * timeStep * speedMultiplier);
+        if (input.keyDown['S'])
+            cameraNode.TranslateRelative(Vector3(0, 0, -10) * timeStep * speedMultiplier);
+        if (input.keyDown['A'])
+            cameraNode.TranslateRelative(Vector3(-10, 0, 0) * timeStep * speedMultiplier);
+        if (input.keyDown['D'])
+            cameraNode.TranslateRelative(Vector3(10, 0, 0) * timeStep * speedMultiplier);
+
+        if (input.keyPress['1'])
+        {
+            int nextRenderMode = graphics.renderMode;
+            if (input.keyDown[KEY_SHIFT])
+            {
+                --nextRenderMode;
+                if (nextRenderMode < 0)
+                    nextRenderMode = 2;
+            }
+            else
+            {
+                ++nextRenderMode;
+                if (nextRenderMode > 2)
+                    nextRenderMode = 0;
+            }
+
+            graphics.SetMode(RenderMode(nextRenderMode));
+        }
+
+        if (input.keyPress['2'])
+        {
+            int quality = renderer.textureQuality;
+            ++quality;
+            if (quality > 2)
+                quality = 0;
+            renderer.textureQuality = quality;
+        }
+
+        if (input.keyPress['3'])
+        {
+            int quality = renderer.materialQuality;
+            ++quality;
+            if (quality > 2)
+                quality = 0;
+            renderer.materialQuality = quality;
+        }
+
+        if (input.keyPress['4'])
+            renderer.specularLighting = !renderer.specularLighting;
+
+        if (input.keyPress['5'])
+            renderer.drawShadows = !renderer.drawShadows;
+
+        if (input.keyPress['6'])
+        {
+            int size = renderer.shadowMapSize;
+            size *= 2;
+            if (size > 2048)
+                size = 512;
+            renderer.shadowMapSize = size;
+        }
+
+        if (input.keyPress['7'])
+            renderer.shadowMapHiresDepth = !renderer.shadowMapHiresDepth;
+
+        if (input.keyPress['8'])
+        {
+            bool occlusion = renderer.maxOccluderTriangles > 0;
+            occlusion = !occlusion;
+            renderer.maxOccluderTriangles = occlusion ? 5000 : 0;
+        }
+        
+        if (input.keyPress['9'])
+            renderer.dynamicInstancing = !renderer.dynamicInstancing;
+
+        if (input.keyPress[' '])
+        {
+            drawDebug++;
+            if (drawDebug > 2)
+                drawDebug = 0;
+        }
+
+        if (input.keyPress['C'])
+            camera.orthographic = !camera.orthographic;
+
+        if (input.keyPress['T'])
+            debugHud.Toggle(DEBUGHUD_SHOW_PROFILER);
+
+        if (input.keyPress[KEY_F5])
+        {
+            File@ xmlFile = File("scene.xml", FILE_WRITE);
+            testScene.SaveXML(xmlFile);
+        }
+
+        if (input.keyPress[KEY_F7])
+        {
+            File@ xmlFile = File("scene.xml", FILE_READ);
+            if (xmlFile.open)
+                testScene.LoadXML(xmlFile);
+        }
+    }
+
+    if (input.keyPress[KEY_ESC])
+    {
+        if (ui.focusElement is null)
+            engine.Exit();
+        else
+            console.visible = false;
+    }
+}
+
+void HandleKeyDown(StringHash eventType, VariantMap& eventData)
+{
+    // Check for toggling the console
+    if (eventData["Key"].GetInt() == 220)
+    {
+        console.Toggle();
+        input.SuppressNextChar();
+    }
+}
+
+void HandleMouseMove(StringHash eventType, VariantMap& eventData)
+{
+    if (eventData["Buttons"].GetInt() & MOUSEB_RIGHT != 0)
+    {
+        int mousedx = eventData["DX"].GetInt();
+        int mousedy = eventData["DY"].GetInt();
+        yaw += mousedx / 10.0f;
+        pitch += mousedy / 10.0f;
+        if (pitch < -90.0f)
+            pitch = -90.0f;
+        if (pitch > 90.0f)
+            pitch = 90.0f;
+
+        cameraNode.rotation = Quaternion(pitch, yaw, 0);
+    }
+}
+
+void HandleMouseButtonDown(StringHash eventType, VariantMap& eventData)
+{
+    int button = eventData["Button"].GetInt();
+    if (button == MOUSEB_RIGHT)
+        ui.cursor.visible = false;
+
+    // Test creating a new physics object
+    if ((button == MOUSEB_LEFT) && (ui.GetElementAt(ui.cursorPosition, true) is null) && (ui.focusElement is null))
+    {
+        Node@ newNode = testScene.CreateChild();
+        newNode.position = cameraNode.position;
+        newNode.rotation = cameraNode.rotation;
+        newNode.SetScale(0.1);
+
+        CollisionShape@ shape = newNode.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(2, 2, 2), Vector3(), Quaternion());
+        shape.friction = 1.0;
+        shape.collisionGroup = 1;
+        shape.collisionMask = 3;
+
+        RigidBody@ body = newNode.CreateComponent("RigidBody");
+        body.angularMaxVelocity = 500.0;
+        body.linearVelocity = camera.upVector + camera.forwardVector * 10.0;
+        body.mass = 1;
+        
+        StaticModel@ object = newNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Box.mdl");
+        object.material = cache.GetResource("Material", "Materials/Test.xml");
+        object.castShadows = true;
+        object.shadowDistance = 150.0;
+        object.drawDistance = 200.0;
+    }
+}
+
+void HandleMouseButtonUp(StringHash eventType, VariantMap& eventData)
+{
+    if (eventData["Button"].GetInt() == MOUSEB_RIGHT)
+        ui.cursor.visible = true;
+}
+
+void HandlePostRenderUpdate()
+{
+    // Draw rendering debug geometry without depth test to see the effect of occlusion
+    if (drawDebug == 1)
+        renderer.DrawDebugGeometry(false);
+    if (drawDebug == 2)
+        testScene.physicsWorld.DrawDebugGeometry(true);
+
+    IntVector2 pos = ui.cursorPosition;
+    if (ui.GetElementAt(pos, true) is null)
+    {
+        Ray cameraRay = camera.GetScreenRay(float(pos.x) / graphics.width, float(pos.y) / graphics.height);
+        Array<RayQueryResult> result = testScene.octree.Raycast(cameraRay, DRAWABLE_GEOMETRY, 250.0, RAY_TRIANGLE);
+        if (result.length > 0)
+        {
+            Drawable@ object = result[0].drawable;
+            Vector3 rayHitPos = cameraRay.origin + cameraRay.direction * result[0].distance;
+            testScene.debugRenderer.AddBoundingBox(BoundingBox(rayHitPos + Vector3(-0.01, -0.01, -0.01), rayHitPos +
+                Vector3(0.01, 0.01, 0.01)), Color(1.0, 1.0, 1.0), true);
+        }
+    }
+}

BIN
Bin/Data/Sounds/BigExplosion.wav


BIN
Bin/Data/Sounds/NutThrow.wav


BIN
Bin/Data/Sounds/PlayerFist.wav


BIN
Bin/Data/Sounds/PlayerFistHit.wav


BIN
Bin/Data/Sounds/PlayerLand.wav


BIN
Bin/Data/Sounds/Powerup.wav


BIN
Bin/Data/Sounds/SmallExplosion.wav


BIN
Bin/Data/Textures/CloudPlane.dds


BIN
Bin/Data/Textures/Diffuse.dds


BIN
Bin/Data/Textures/DiffuseMask.dds


BIN
Bin/Data/Textures/Flare.dds


BIN
Bin/Data/Textures/HealthBarBorder.png


BIN
Bin/Data/Textures/HealthBarInside.png


BIN
Bin/Data/Textures/Jack_body_color.jpg


BIN
Bin/Data/Textures/Jack_face.jpg


BIN
Bin/Data/Textures/Mushroom.dds


BIN
Bin/Data/Textures/Ninja.dds


BIN
Bin/Data/Textures/Normal.dds


BIN
Bin/Data/Textures/Normal.png


BIN
Bin/Data/Textures/Sight.png


+ 5 - 0
Bin/Data/Textures/Sight.xml

@@ -0,0 +1,5 @@
+<texture>
+    <mipmap enable="false" />
+    <quality low="0" />
+</texture>
+

BIN
Bin/Data/Textures/Smoke.dds


BIN
Bin/Data/Textures/Snow.dds


BIN
Bin/Data/Textures/SnowCrate.dds


+ 1 - 0
Bin/NinjaSnowWar.bat

@@ -0,0 +1 @@
+Urho3D.exe Scripts/NinjaSnowWar.as %1 %2 %3 %4 %5 %6 %7 %8

+ 1 - 0
Bin/TestScene.bat

@@ -0,0 +1 @@
+Urho3D.exe Scripts/TestScene.as %1 %2 %3 %4 %5 %6 %7 %8

+ 8 - 11
CMakeLists.txt

@@ -20,13 +20,10 @@ else ()
     set (DBGHELP_LIB "")
 endif ()
   
-# Enable profiling. This only affects whether autoprofile-blocks (PROFILE macro) will be generated;
-# the profiler is compiled and instantiated nevertheless.
+# Enable profiling. If disabled, autoprofileblocks become no-ops and the Profiler subsystem is not
+# instantiated.
 add_definitions (-DENABLE_PROFILING)
 
-# Enable runtime integrity checks for RefCounted objects.
-add_definitions (-DENABLE_REFCOUNTED_CHECKS)
-
 # Compiler-specific options
 if (MSVC)
     add_definitions (-D_CRT_SECURE_NO_WARNINGS)
@@ -76,6 +73,7 @@ macro (finalize_lib)
     endif ()
 endmacro ()
 
+
 # Macro for model asset compilation
 macro (add_model NAME PARAMETERS)
     add_custom_command (
@@ -104,22 +102,20 @@ macro (add_shader NAME)
     set (ALL_SHADERS ${ALL_SHADERS} ../../Bin/CoreData/Shaders/SM2/${NAME}.xml ../../Bin/CoreData/Shaders/SM3/${NAME}.xml)
 endmacro ()
 
-# Recurse subdirectories
+# Add projects
 add_subdirectory (Engine/Audio)
-add_subdirectory (Engine/Common)
+add_subdirectory (Engine/Core)
 add_subdirectory (Engine/Engine)
+add_subdirectory (Engine/Graphics)
 add_subdirectory (Engine/Input)
+add_subdirectory (Engine/IO)
 add_subdirectory (Engine/Math)
 add_subdirectory (Engine/Network)
 add_subdirectory (Engine/Physics)
-add_subdirectory (Engine/Renderer)
 add_subdirectory (Engine/Resource)
 add_subdirectory (Engine/Scene)
 add_subdirectory (Engine/Script)
 add_subdirectory (Engine/UI)
-add_subdirectory (Examples/NetworkTest)
-add_subdirectory (Examples/NinjaSnowWar)
-add_subdirectory (Examples/Urho3D)
 add_subdirectory (SourceAssets/Models)
 add_subdirectory (SourceAssets/Shaders)
 add_subdirectory (ThirdParty/AngelScript)
@@ -136,3 +132,4 @@ add_subdirectory (Tools/OgreImporter)
 add_subdirectory (Tools/PackageTool)
 add_subdirectory (Tools/RampGenerator)
 add_subdirectory (Tools/ShaderCompiler)
+add_subdirectory (Urho3D)

+ 451 - 0
Engine/Audio/Audio.cpp

@@ -0,0 +1,451 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "Precompiled.h"
+#include "Audio.h"
+#include "Context.h"
+#include "CoreEvents.h"
+#include "Graphics.h"
+#include "GraphicsEvents.h"
+#include "Log.h"
+#include "Profiler.h"
+#include "Sound.h"
+#include "SoundSource3D.h"
+#include "StringUtils.h"
+
+#define DIRECTSOUND_VERSION 0x0800
+
+#include <windows.h>
+#include <dsound.h>
+#include <mmsystem.h>
+#include <process.h>
+
+#include "DebugNew.h"
+
+static const int AUDIO_FPS = 100;
+
+/// Audio implementation. Contains the DirectSound buffer
+class AudioImpl
+{
+    friend class Audio;
+    
+public:
+    /// Construct
+    AudioImpl() :
+        dsObject_(0),
+        dsBuffer_(0)
+    {
+    }
+    
+private:
+    /// DirectSound interface
+    IDirectSound* dsObject_;
+    /// DirectSound buffer
+    IDirectSoundBuffer* dsBuffer_;
+};
+
+OBJECTTYPESTATIC(Audio);
+
+Audio::Audio(Context* context) :
+    Object(context),
+    impl_(new AudioImpl()),
+    playing_(false),
+    windowHandle_(0),
+    bufferSamples_(0),
+    bufferSize_(0),
+    sampleSize_(0),
+    listenerPosition_(Vector3::ZERO),
+    listenerRotation_(Quaternion::IDENTITY)
+{
+    SubscribeToEvent(E_SCREENMODE, HANDLER(Audio, HandleScreenMode));
+    SubscribeToEvent(E_RENDERUPDATE, HANDLER(Audio, HandleRenderUpdate));
+    
+    for (unsigned i = 0; i < MAX_SOUND_TYPES; ++i)
+        masterGain_[i] = 1.0f;
+    
+    // Try to initialize right now, but skip if screen mode is not yet set
+    Initialize();
+}
+
+Audio::~Audio()
+{
+    ReleaseBuffer();
+    
+    if (impl_->dsObject_)
+    {
+        impl_->dsObject_->Release();
+        impl_->dsObject_ = 0;
+    }
+    
+    delete impl_;
+    impl_ = 0;
+}
+
+bool Audio::SetMode(int bufferLengthMSec, int mixRate, bool sixteenBit, bool stereo, bool interpolate)
+{
+    ReleaseBuffer();
+    
+    if (!impl_->dsObject_)
+    {
+        if (DirectSoundCreate(0, &impl_->dsObject_, 0) != DS_OK)
+        {
+            LOGERROR("Could not create DirectSound object");
+            return false;
+        }
+    }
+    
+    if (impl_->dsObject_->SetCooperativeLevel((HWND)windowHandle_, DSSCL_PRIORITY) != DS_OK)
+    {
+        LOGERROR("Could not set DirectSound cooperative level");
+        return false;
+    }
+    DSCAPS dsCaps;
+    dsCaps.dwSize = sizeof(dsCaps);
+    if (impl_->dsObject_->GetCaps(&dsCaps) != DS_OK)
+    {
+        LOGERROR("Could not get DirectSound capabilities");
+        return false;
+    }
+    
+    if (!(dsCaps.dwFlags & (DSCAPS_SECONDARY16BIT|DSCAPS_PRIMARY16BIT)))
+        sixteenBit = false;
+    if (!(dsCaps.dwFlags & (DSCAPS_SECONDARYSTEREO|DSCAPS_PRIMARYSTEREO)))
+        stereo = false;
+    
+    bufferLengthMSec = Max(bufferLengthMSec, 50);
+    mixRate = Clamp(mixRate, 11025, 48000);
+    
+    WAVEFORMATEX waveFormat;
+    waveFormat.wFormatTag = WAVE_FORMAT_PCM;
+    waveFormat.nSamplesPerSec = mixRate;
+    
+    if (sixteenBit)
+        waveFormat.wBitsPerSample = 16;
+    else
+        waveFormat.wBitsPerSample = 8;
+    
+    if (stereo)
+        waveFormat.nChannels = 2;
+    else
+        waveFormat.nChannels = 1;
+    
+    unsigned sampleSize = waveFormat.nChannels * waveFormat.wBitsPerSample / 8;
+    unsigned numSamples = (bufferLengthMSec * mixRate) / 1000;
+    
+    waveFormat.nAvgBytesPerSec = mixRate * sampleSize;
+    waveFormat.nBlockAlign = sampleSize;
+    waveFormat.cbSize = 0;
+    
+    DSBUFFERDESC bufferDesc;
+    memset(&bufferDesc, 0, sizeof(bufferDesc));
+    bufferDesc.dwSize = sizeof(bufferDesc);
+    bufferDesc.dwFlags = DSBCAPS_STICKYFOCUS;
+    bufferDesc.dwBufferBytes = numSamples * sampleSize;
+    bufferDesc.lpwfxFormat = &waveFormat;
+    
+    if (impl_->dsObject_->CreateSoundBuffer(&bufferDesc, &impl_->dsBuffer_, 0) != DS_OK)
+    {
+        LOGERROR("Could not create DirectSound buffer");
+        return false;
+    }
+    
+    clipBuffer_ = new int[numSamples * waveFormat.nChannels];
+    bufferSamples_ = numSamples;
+    bufferSize_ = numSamples * sampleSize;
+    sampleSize_ = sampleSize;
+    mixRate_ = mixRate;
+    sixteenBit_ = sixteenBit;
+    stereo_ = stereo;
+    interpolate_ = interpolate;
+    
+    LOGINFO("Set audio mode " + ToString(mixRate_) + " Hz " + (stereo_ ? "stereo" : "mono") + " " + (sixteenBit_ ? "16-bit" : "8-bit") + " " +
+        (interpolate_ ? "interpolated" : ""));
+    
+    return Play();
+}
+
+void Audio::Update(float timeStep)
+{
+    PROFILE(UpdateAudio);
+    
+    MutexLock Lock(audioMutex_);
+    
+    // Update in reverse order, because sound sources might remove themselves
+    for (unsigned i = soundSources_.size() - 1; i < soundSources_.size(); --i)
+        soundSources_[i]->Update(timeStep);
+}
+
+bool Audio::Play()
+{
+    if (playing_)
+        return true;
+    
+    if (!impl_->dsBuffer_)
+    {
+        LOGERROR("No audio buffer, can not start playback");
+        return false;
+    }
+    
+    // Clear buffer before starting playback
+    DWORD bytes1, bytes2;
+    void *ptr1, *ptr2;
+    unsigned char value = sixteenBit_ ? 0 : 128;
+    if (impl_->dsBuffer_->Lock(0, bufferSize_, &ptr1, &bytes1, &ptr2, &bytes2, 0) == DS_OK)
+    {
+        if (bytes1)
+            memset(ptr1, value, bytes1);
+        if (bytes2)
+            memset(ptr2, value, bytes2);
+        impl_->dsBuffer_->Unlock(ptr1, bytes1, ptr2, bytes2);
+    }
+    
+    // Create playback thread
+    if (!Start())
+    {
+        LOGERROR("Could not create audio thread");
+        return false;
+    }
+    
+    // Adjust playback thread priority
+    SetPriority(THREAD_PRIORITY_ABOVE_NORMAL);
+    playing_ = true;
+    return true;
+}
+
+void Audio::Stop()
+{
+    Thread::Stop();
+    playing_ = false;
+}
+
+void Audio::SetMasterGain(SoundType type, float gain)
+{
+    if (type >= MAX_SOUND_TYPES)
+        return;
+    
+    masterGain_[type] = Clamp(gain, 0.0f, 1.0f);
+}
+
+void Audio::SetListenerPosition(const Vector3& position)
+{
+    listenerPosition_ = position;
+}
+
+void Audio::SetListenerRotation(const Quaternion& rotation)
+{
+    listenerRotation_ = rotation;
+}
+
+void Audio::SetListenerTransform(const Vector3& position, const Quaternion& rotation)
+{
+    listenerPosition_ = position;
+    listenerRotation_ = rotation;
+}
+
+void Audio::StopSound(Sound* soundClip)
+{
+    for (std::vector<SoundSource*>::iterator i = soundSources_.begin(); i != soundSources_.end(); ++i)
+    {
+        if ((*i)->GetSound() == soundClip)
+            (*i)->Stop();
+    }
+}
+
+bool Audio::IsInitialized() const
+{
+    return impl_->dsBuffer_ != 0;
+}
+
+float Audio::GetMasterGain(SoundType type) const
+{
+    if (type >= MAX_SOUND_TYPES)
+        return 0.0f;
+    
+    return masterGain_[type];
+}
+
+void Audio::AddSoundSource(SoundSource* channel)
+{
+    MutexLock Lock(audioMutex_);
+    
+    soundSources_.push_back(channel);
+}
+
+void Audio::RemoveSoundSource(SoundSource* channel)
+{
+    MutexLock Lock(audioMutex_);
+    
+    for (std::vector<SoundSource*>::iterator i = soundSources_.begin(); i != soundSources_.end(); ++i)
+    {
+        if (*i == channel)
+        {
+            soundSources_.erase(i);
+            return;
+        }
+    }
+}
+
+void Audio::ThreadFunction()
+{
+    AudioImpl* impl = impl_;
+    
+    DWORD playCursor = 0;
+    DWORD writeCursor = 0;
+    
+    while (shouldRun_)
+    {
+        Timer audioUpdateTimer;
+        
+        // Restore buffer / restart playback if necessary
+        DWORD status;
+        impl->dsBuffer_->GetStatus(&status);
+        if (status == DSBSTATUS_BUFFERLOST)
+        {
+            impl->dsBuffer_->Restore();
+            impl->dsBuffer_->GetStatus(&status);
+        }
+        if (!(status & DSBSTATUS_PLAYING))
+        {
+            impl->dsBuffer_->Play(0, 0, DSBPLAY_LOOPING);
+            writeCursor = 0;
+        }
+        
+        // Get current buffer position
+        impl->dsBuffer_->GetCurrentPosition(&playCursor, 0);
+        playCursor %= bufferSize_;
+        playCursor &= -((int)sampleSize_);
+        
+        if (playCursor != writeCursor)
+        {
+            int writeBytes = playCursor - writeCursor;
+            if (writeBytes < 0)
+                writeBytes += bufferSize_;
+            
+            // Try to lock buffer
+            DWORD bytes1, bytes2;
+            void *ptr1, *ptr2;
+            if (impl->dsBuffer_->Lock(writeCursor, writeBytes, &ptr1, &bytes1, &ptr2, &bytes2, 0) == DS_OK)
+            {
+                // Mix sound to locked positions
+                {
+                    MutexLock Lock(audioMutex_);
+                    
+                    if (bytes1)
+                        MixOutput(ptr1, bytes1);
+                    if (bytes2)
+                        MixOutput(ptr2, bytes2);
+                }
+                
+                // Unlock buffer and update write cursor
+                impl->dsBuffer_->Unlock(ptr1, bytes1, ptr2, bytes2);
+                writeCursor += writeBytes;
+                if (writeCursor >= bufferSize_)
+                    writeCursor -= bufferSize_;
+            }
+        }
+        
+        // Sleep the remaining time of the audio update period
+        int audioSleepTime = Max(1000 / AUDIO_FPS - (int)audioUpdateTimer.GetMSec(false), 0);
+        Sleep(audioSleepTime);
+    }
+    
+    impl->dsBuffer_->Stop();
+}
+
+void Audio::Initialize()
+{
+    Graphics* graphics = GetSubsystem<Graphics>();
+    if ((!graphics) || (!graphics->IsInitialized()))
+        return;
+    
+    windowHandle_ = graphics->GetWindowHandle();
+}
+
+void Audio::MixOutput(void *dest, unsigned bytes)
+{
+    unsigned mixSamples = bytes;
+    unsigned clipSamples = bytes;
+    
+    if (stereo_)
+        mixSamples >>= 1;
+    
+    if (sixteenBit_)
+    {
+        clipSamples >>= 1;
+        mixSamples >>= 1;
+    }
+    
+    // Clear clip buffer
+    memset(clipBuffer_.GetPtr(), 0, clipSamples * sizeof(int));
+    int* clipPtr = clipBuffer_.GetPtr();
+    
+    // Mix samples to clip buffer
+    for (std::vector<SoundSource*>::iterator i = soundSources_.begin(); i != soundSources_.end(); ++i)
+        (*i)->Mix(clipPtr, mixSamples, mixRate_, stereo_, interpolate_);
+    
+    // Copy output from clip buffer to destination
+    clipPtr = clipBuffer_.GetPtr();
+    if (sixteenBit_)
+    {
+        short* destPtr = (short*)dest;
+        while (clipSamples--)
+            *destPtr++ = Clamp(*clipPtr++, -32768, 32767);
+    }
+    else
+    {
+        unsigned char* destPtr = (unsigned char*)dest;
+        while (clipSamples--)
+            *destPtr++ = Clamp(((*clipPtr++) >> 8) + 128, 0, 255);
+    }
+}
+
+void Audio::ReleaseBuffer()
+{
+    Stop();
+    
+    if (impl_->dsBuffer_)
+    {
+        impl_->dsBuffer_->Release();
+        impl_->dsBuffer_ = 0;
+    }
+}
+
+void Audio::HandleScreenMode(StringHash eventType, VariantMap& eventData)
+{
+    if (!windowHandle_)
+        Initialize();
+}
+
+void Audio::HandleRenderUpdate(StringHash eventType, VariantMap& eventData)
+{
+    using namespace RenderUpdate;
+    
+    Update(eventData[P_TIMESTEP].GetFloat());
+}
+
+void RegisterAudioLibrary(Context* context)
+{
+    Sound::RegisterObject(context);
+    SoundSource::RegisterObject(context);
+    SoundSource3D::RegisterObject(context);
+}

+ 157 - 0
Engine/Audio/Audio.h

@@ -0,0 +1,157 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "AudioDefs.h"
+#include "Mutex.h"
+#include "Object.h"
+#include "Quaternion.h"
+#include "Thread.h"
+#include "SharedArrayPtr.h"
+
+#include <vector>
+
+class AudioImpl;
+class Sound;
+class SoundSource;
+
+/// Audio subsystem. Produces sound output using DirectSound
+class Audio : public Object, public Thread
+{
+    OBJECT(Audio);
+    
+public:
+    /// Construct
+    Audio(Context* context);
+    /// Destruct. Terminate the audio thread and free the audio buffer
+    virtual ~Audio();
+    
+    /// Initialize sound output with specified buffer length and output mode
+    bool SetMode(int bufferLengthMSec, int mixRate, bool sixteenBit, bool stereo, bool interpolate = true);
+    /// Run update on sound sources. Not required for continued playback, but frees unused sound sources & sounds and updates 3D positions.
+    void Update(float timeStep);
+    /// Restart sound output
+    bool Play();
+    /// Suspend sound output
+    void Stop();
+    /// Set master gain on a specific sound type such as sound effects, music or voice.
+    void SetMasterGain(SoundType type, float gain);
+    /// Set listener position
+    void SetListenerPosition(const Vector3& position);
+    /// Set listener rotation
+    void SetListenerRotation(const Quaternion& rotation);
+    /// Set listener position and rotation
+    void SetListenerTransform(const Vector3& position, const Quaternion& rotation);
+    /// Stop any sound source playing a certain sound clip
+    void StopSound(Sound* sound);
+    
+    /// Return audio implementation, which contains the actual DirectSound objects
+    AudioImpl* GetImpl() { return impl_; }
+    /// Return sound buffer size in samples
+    unsigned GetBufferSamples() const { return bufferSamples_; }
+    /// Return sound buffer size in bytes
+    unsigned GetBufferSize() const { return bufferSize_; }
+    /// Return byte size of one sample
+    unsigned GetSampleSize() const { return sampleSize_; }
+    /// Return mixing rate
+    int GetMixRate() const { return mixRate_; }
+    /// Return whether output is sixteen bit
+    bool IsSixteenBit() const { return sixteenBit_; }
+    /// Return whether output is stereo
+    bool IsStereo() const { return stereo_; }
+    /// Return whether output is interpolated
+    bool IsInterpolated() const { return interpolate_; }
+    /// Return whether audio is being output
+    bool IsPlaying() const { return playing_; }
+    /// Return whether an audio buffer has been reserved
+    bool IsInitialized() const;
+    /// Return master gain for a specific sound source type
+    float GetMasterGain(SoundType type) const;
+    /// Return listener position
+    const Vector3& GetListenerPosition() const { return listenerPosition_; }
+    /// Return listener rotation
+    const Quaternion& GetListenerRotation() const { return listenerRotation_; }
+    /// Return all sound sources
+    const std::vector<SoundSource*>& GetSoundSources() const { return soundSources_; }
+    
+    /// Add a sound source to keep track of. Called by SoundSource
+    void AddSoundSource(SoundSource* soundSource);
+    /// Remove a sound source. Called by SoundSource
+    void RemoveSoundSource(SoundSource* soundSource);
+    /// Return audio thread mutex
+    Mutex& GetMutex() { return audioMutex_; }
+    /// Return sound type specific gain multiplied by master gain
+    float GetSoundSourceMasterGain(SoundType type) const { return masterGain_[SOUND_MASTER] * masterGain_[type]; }
+    
+    /// Mixing thread function
+    virtual void ThreadFunction();
+    
+private:
+    /// Initialize when screen mode initially set
+    void Initialize();
+    /// Mix sound sources into the buffer
+    void MixOutput(void* dest, unsigned bytes);
+    /// Release the sound buffer
+    void ReleaseBuffer();
+    /// Handle screen mode event
+    void HandleScreenMode(StringHash eventType, VariantMap& eventData);
+    /// Handle render update event
+    void HandleRenderUpdate(StringHash eventType, VariantMap& eventData);
+    
+    /// Implementation
+    AudioImpl* impl_;
+    /// Clipping buffer for mixing
+    SharedArrayPtr<int> clipBuffer_;
+    /// Audio thread mutex
+    Mutex audioMutex_;
+    /// Window handle
+    unsigned windowHandle_;
+    /// Sound buffer size in samples
+    unsigned bufferSamples_;
+    /// Sound buffer size in bytes
+    unsigned bufferSize_;
+    /// Sample size
+    unsigned sampleSize_;
+    /// Mixing rate
+    int mixRate_;
+    /// Sixteen bit flag
+    bool sixteenBit_;
+    /// Stereo flag
+    bool stereo_;
+    /// Interpolation flag
+    bool interpolate_;
+    /// Playing flag
+    bool playing_;
+    /// Master gain by sound source type
+    float masterGain_[MAX_SOUND_TYPES];
+    /// Sound sources
+    std::vector<SoundSource*> soundSources_;
+    /// Listener position
+    Vector3 listenerPosition_;
+    /// Listener rotation
+    Quaternion listenerRotation_;
+};
+
+/// Register Sound library objects
+void RegisterAudioLibrary(Context* context);

+ 34 - 0
Engine/Audio/AudioDefs.h

@@ -0,0 +1,34 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+/// SoundSource type enumeration for master gain control
+enum SoundType
+{
+    SOUND_MASTER = 0,
+    SOUND_EFFECT,
+    SOUND_MUSIC,
+    SOUND_VOICE,
+    MAX_SOUND_TYPES
+};

+ 18 - 0
Engine/Audio/CMakeLists.txt

@@ -0,0 +1,18 @@
+# Define target name
+set (TARGET_NAME Audio)
+
+# Define source files
+file (GLOB CPP_FILES *.cpp)
+file (GLOB H_FILES *.h)
+set (SOURCE_FILES ${CPP_FILES} ${H_FILES})
+
+# Include directories
+include_directories (
+    ../Core ../Graphics ../IO ../Math ../Resource ../Scene ../../ThirdParty/STB
+)
+
+# Define target & libraries to link
+add_library (${TARGET_NAME} STATIC ${SOURCE_FILES})
+target_link_libraries (${TARGET_NAME} Core Graphics IO Math Resource Scene STB dsound.lib)
+enable_pch ()
+finalize_lib ()

+ 24 - 0
Engine/Audio/Precompiled.cpp

@@ -0,0 +1,24 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "Precompiled.h"

+ 29 - 0
Engine/Audio/Precompiled.h

@@ -0,0 +1,29 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>

+ 417 - 0
Engine/Audio/Sound.cpp

@@ -0,0 +1,417 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "Precompiled.h"
+#include "Context.h"
+#include "FileSystem.h"
+#include "Log.h"
+#include "Profiler.h"
+#include "ResourceCache.h"
+#include "Sound.h"
+#include "XMLFile.h"
+
+#include <cstring>
+#include <stb_vorbis.h>
+
+#include "DebugNew.h"
+
+/// WAV format header
+struct WavHeader
+{
+    unsigned char riffText_[4];
+    unsigned totalLength_;
+    unsigned char waveText_[4];
+    unsigned char formatText_[4];
+    unsigned formatLength_;
+    unsigned short format_;
+    unsigned short soundSources_;
+    unsigned frequency_;
+    unsigned avgBytes_;
+    unsigned short blockAlign_;
+    unsigned short bits_;
+    unsigned char dataText_[4];
+    unsigned dataLength_;
+};
+
+static const unsigned IP_SAFETY = 4;
+
+OBJECTTYPESTATIC(Sound);
+
+Sound::Sound(Context* context) :
+    Resource(context),
+    repeat_(0),
+    end_(0),
+    frequency_(44100),
+    looped_(false),
+    sixteenBit_(false),
+    stereo_(false),
+    compressed_(false),
+    compressedLength_(0.0f)
+{
+}
+
+Sound::~Sound()
+{
+}
+
+void Sound::RegisterObject(Context* context)
+{
+    context->RegisterFactory<Sound>();
+}
+
+bool Sound::Load(Deserializer& source)
+{
+    PROFILE(LoadSound);
+    
+    bool success = false;
+    if (GetExtension(source.GetName()) == ".ogg")
+        success = LoadOggVorbis(source);
+    else if (GetExtension(source.GetName()) == ".wav")
+        success = LoadWav(source);
+    else
+        success = LoadRaw(source);
+    
+    // Load optional parameters
+    if (success)
+        LoadParameters();
+    
+    return success;
+}
+
+bool Sound::LoadOggVorbis(Deserializer& source)
+{
+    unsigned dataSize = source.GetSize();
+    SharedArrayPtr<signed char> data(new signed char[dataSize]);
+    source.Read(data.GetPtr(), dataSize);
+    
+    // Check for validity of data
+    int error;
+    stb_vorbis* vorbis = stb_vorbis_open_memory((unsigned char*)data.GetPtr(), dataSize, &error, 0);
+    if (!vorbis)
+    {
+        LOGERROR("Could not read Ogg Vorbis data from " + source.GetName());
+        return false;
+    }
+    
+    // Store length, frequency and stereo flag
+    stb_vorbis_info info = stb_vorbis_get_info(vorbis);
+    compressedLength_ = stb_vorbis_stream_length_in_seconds(vorbis);
+    frequency_ = info.sample_rate;
+    stereo_ = info.channels > 1;
+    stb_vorbis_close(vorbis);
+    
+    data_ = data;
+    dataSize_ = dataSize;
+    sixteenBit_ = true;
+    compressed_ = true;
+    
+    SetMemoryUse(dataSize);
+    return true;
+}
+
+bool Sound::LoadWav(Deserializer& source)
+{
+    WavHeader header;
+    
+    // Try to open
+    memset(&header, 0, sizeof header);
+    source.Read(&header.riffText_, 4);
+    header.totalLength_ = source.ReadUInt();
+    source.Read(&header.waveText_, 4);
+    
+    if ((memcmp("RIFF", header.riffText_, 4)) || (memcmp("WAVE", header.waveText_, 4)))
+    {
+        LOGERROR("Could not read WAV data from " + source.GetName());
+        return false;
+    }
+    
+    // Search for the FORMAT chunk
+    for (;;)
+    {
+        source.Read(&header.formatText_, 4);
+        header.formatLength_ = source.ReadUInt();
+        if (!memcmp("fmt ", &header.formatText_, 4))
+            break;
+        
+        source.Seek(source.GetPosition() + header.formatLength_);
+        if ((!header.formatLength_) || (source.GetPosition() >= source.GetSize()))
+        {
+            LOGERROR("Could not read WAV data from " + source.GetName());
+            return false;
+        }
+    }
+    
+    // Read the FORMAT chunk
+    header.format_ = source.ReadUShort();
+    header.soundSources_ = source.ReadUShort();
+    header.frequency_ = source.ReadUInt();
+    header.avgBytes_ = source.ReadUInt();
+    header.blockAlign_ = source.ReadUShort();
+    header.bits_ = source.ReadUShort();
+    
+    // Skip data if the format chunk was bigger than what we use
+    source.Seek(source.GetPosition() + header.formatLength_ - 16);
+    
+    // Check for correct format
+    if (header.format_ != 1)
+    {
+        LOGERROR("Could not read WAV data from " + source.GetName());
+        return false;
+    }
+    
+    // Search for the DATA chunk
+    for (;;)
+    {
+        source.Read(&header.dataText_, 4);
+        header.dataLength_ = source.ReadUInt();
+        if (!memcmp("data", &header.dataText_, 4))
+            break;
+        
+        source.Seek(source.GetPosition() + header.dataLength_);
+        if ((!header.dataLength_) || (source.GetPosition() >= source.GetSize()))
+        {
+            LOGERROR("Could not read WAV data from " + source.GetName());
+            return false;
+        }
+    }
+    
+    // Allocate sound and load audio data
+    unsigned length = header.dataLength_;
+    SetSize(length);
+    SetFormat(header.frequency_, header.bits_ == 16, header.soundSources_ == 2);
+    source.Read(data_.GetPtr(), length);
+    
+    // Convert 8-bit audio to signed
+    if (!sixteenBit_)
+    {
+        for (unsigned i = 0; i < length; ++i)
+            data_[i] -= 128;
+    }
+    
+    return true;
+}
+
+bool Sound::LoadRaw(Deserializer& source)
+{
+    unsigned dataSize = source.GetSize();
+    SetSize(dataSize);
+    return source.Read(data_.GetPtr(), dataSize) == dataSize;
+}
+
+void Sound::SetSize(unsigned dataSize)
+{
+    if (!dataSize)
+        return;
+    
+    data_ = new signed char[dataSize + IP_SAFETY];
+    dataSize_ = dataSize;
+    compressed_ = false;
+    SetLooped(false);
+    
+    SetMemoryUse(dataSize + IP_SAFETY);
+}
+
+void Sound::SetData(const void* data, unsigned dataSize)
+{
+    if (!dataSize)
+        return;
+    
+    SetSize(dataSize);
+    memcpy(data_.GetPtr(), data, dataSize);
+}
+
+void Sound::SetFormat(unsigned frequency, bool sixteenBit, bool stereo)
+{
+    frequency_ = frequency;
+    sixteenBit_ = sixteenBit;
+    stereo_ = stereo;
+    compressed_ = false;
+}
+
+void Sound::SetLooped(bool enable)
+{
+    if (enable)
+        SetLoop(0, dataSize_);
+    else
+    {
+        if (!compressed_)
+        {
+            end_ = data_.GetPtr() + dataSize_;
+            looped_ = false;
+            
+            FixInterpolation();
+        }
+        else
+            looped_ = false;
+    }
+}
+
+void Sound::SetLoop(unsigned repeatOffset, unsigned endOffset)
+{
+    if (!compressed_)
+    {
+        if (repeatOffset > dataSize_)
+            repeatOffset = dataSize_;
+        if (endOffset > dataSize_)
+            endOffset = dataSize_;
+        
+        // Align repeat and end on sample boundaries
+        int sampleSize = GetSampleSize();
+        repeatOffset &= -sampleSize;
+        endOffset &= -sampleSize;
+        
+        repeat_ = data_.GetPtr() + repeatOffset;
+        end_ = data_.GetPtr() + endOffset;
+        looped_ = true;
+        
+        FixInterpolation();
+    }
+    else
+        looped_ = true;
+}
+
+void Sound::FixInterpolation()
+{
+    if (!data_)
+        return;
+    
+    // If looped, copy loop start to loop end. If oneshot, insert silence to end
+    if (looped_)
+    {
+        for (unsigned i = 0; i < IP_SAFETY; ++i)
+            end_[i] = repeat_[i];
+    }
+    else
+    {
+        for (unsigned i = 0; i < IP_SAFETY; ++i)
+            end_[i] = 0;
+    }
+}
+
+void* Sound::AllocateDecoder()
+{
+    if (!compressed_)
+        return 0;
+    
+    int error;
+    stb_vorbis* vorbis = stb_vorbis_open_memory((unsigned char*)data_.GetPtr(), dataSize_, &error, 0);
+    return vorbis;
+}
+
+unsigned Sound::Decode(void* Decoder, signed char* dest, unsigned bytes)
+{
+    if (!Decoder)
+        return 0;
+    
+    unsigned soundSources = stereo_ ? 2 : 1;
+    stb_vorbis* vorbis = static_cast<stb_vorbis*>(Decoder);
+    unsigned outSamples = stb_vorbis_get_samples_short_interleaved(vorbis, soundSources, (short*)dest, bytes >> 1);
+    return (outSamples * soundSources) << 1;
+}
+
+void Sound::RewindDecoder(void* Decoder)
+{
+    if (!Decoder)
+        return;
+    
+    stb_vorbis* vorbis = static_cast<stb_vorbis*>(Decoder);
+    stb_vorbis_seek_start(vorbis);
+}
+
+void Sound::FreeDecoder(void* Decoder)
+{
+    if (!Decoder)
+        return;
+    
+    stb_vorbis* vorbis = static_cast<stb_vorbis*>(Decoder);
+    stb_vorbis_close(vorbis);
+}
+
+float Sound::GetLength() const
+{
+    if (!compressed_)
+    {
+        if (!frequency_)
+            return 0.0f;
+        else
+            return ((float)dataSize_) / GetSampleSize() / frequency_;
+    }
+    else
+        return compressedLength_;
+}
+
+unsigned Sound::GetSampleSize() const
+{
+    unsigned size = 1;
+    if (sixteenBit_)
+        size <<= 1;
+    if (stereo_)
+        size <<= 1;
+    return size;
+}
+
+void Sound::LoadParameters()
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    
+    std::string soundPath, soundName, soundExt;
+    SplitPath(GetName(), soundPath, soundName, soundExt);
+    std::string xmlName = soundPath + soundName + ".xml";
+    
+    if (!cache->Exists(xmlName))
+        return;
+    
+    XMLFile* file = cache->GetResource<XMLFile>(xmlName);
+    if (!file)
+        return;
+    
+    XMLElement rootElem = file->GetRootElement();
+    XMLElement paramElem = rootElem.GetChildElement("");
+    
+    while (paramElem)
+    {
+        std::string name = paramElem.GetName();
+        
+        if ((name == "format") && (!compressed_))
+        {
+            if (paramElem.HasAttribute("frequency"))
+                frequency_ = paramElem.GetInt("frequency");
+            if (paramElem.HasAttribute("sixteenbit"))
+                sixteenBit_ = paramElem.GetBool("sixteenbit");
+            if (paramElem.HasAttribute("16bit"))
+                sixteenBit_ = paramElem.GetBool("16bit");
+            if (paramElem.HasAttribute("stereo"))
+                stereo_ = paramElem.GetBool("stereo");
+        }
+        
+        if (name == "loop")
+        {
+            if (paramElem.HasAttribute("enable"))
+                SetLooped(paramElem.GetBool("enable"));
+            if ((paramElem.HasAttribute("start")) && (paramElem.HasAttribute("end")))
+                SetLoop(paramElem.GetInt("start"), paramElem.GetInt("end"));
+        }
+        
+        paramElem = paramElem.GetNextElement();
+    }
+}

+ 122 - 0
Engine/Audio/Sound.h

@@ -0,0 +1,122 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "Resource.h"
+#include "SharedArrayPtr.h"
+
+/// Sound resource
+class Sound : public Resource
+{
+    OBJECT(Sound);
+    
+public:
+    /// Construct
+    Sound(Context* context);
+    /// Destruct and free sound data
+    virtual ~Sound();
+    /// Register object factory
+    static void RegisterObject(Context* context);
+    
+    /// Load resource. Return true if successful
+    virtual bool Load(Deserializer& source);
+    
+    /// Load raw sound data
+    bool LoadRaw(Deserializer& source);
+    /// Load WAV format sound data
+    bool LoadWav(Deserializer& source);
+    /// Load Ogg Vorbis format sound data. Does not Decode at load, will be Decoded while playing
+    bool LoadOggVorbis(Deserializer& source);
+    /// Set sound size in bytes. Also resets the sound to be uncompressed and one-shot
+    void SetSize(unsigned dataSize);
+    /// Set uncompressed sound data
+    void SetData(const void* data, unsigned dataSize);
+    /// Set uncompressed sound data format
+    void SetFormat(unsigned frequency, bool sixteenBit, bool stereo);
+    /// Set loop on/off. If loop is enabled, sets the full sound as loop range
+    void SetLooped(bool enable);
+    /// Define loop
+    void SetLoop(unsigned repeatOffset, unsigned endOffset);
+    /// Fix interpolation by copying data from loop start to loop end (looped), or adding silence (oneshot)
+    void FixInterpolation();
+    
+    /// Create and return a Decoder. Return null if fails
+    void* AllocateDecoder();
+    /// Decode compressed audio data. Return number of actually Decoded bytes
+    unsigned Decode(void* Decoder, signed char* dest, unsigned bytes);
+    /// Rewind Decoder to beginning of audio data
+    void RewindDecoder(void* Decoder);
+    /// Free Decoder
+    void FreeDecoder(void* Decoder);
+    
+    /// Return sound data start
+    signed char* GetStart() const { return data_.GetPtr(); }
+    /// Return loop start
+    signed char* GetRepeat() const { return repeat_; }
+    /// Return sound data end
+    signed char* Getend() const { return end_; }
+    /// Return length in seconds
+    float GetLength() const;
+    /// Return total sound data size
+    unsigned GetDataSize() const { return dataSize_; }
+    /// Return sample size
+    unsigned GetSampleSize() const;
+    /// Return default frequency
+    float GetFrequency() { return (float)frequency_; }
+    /// Return default frequency
+    unsigned GetIntFrequency() { return frequency_; }
+    /// Return whether is looped
+    bool IsLooped() const { return looped_; }
+    /// Return whether data is sixteen bit
+    bool IsSixteenBit() const { return sixteenBit_; }
+    /// Return whether data is stereo
+    bool IsStereo() const { return stereo_; }
+    /// Return whether is compressed in Ogg Vorbis format
+    bool IsCompressed() const { return compressed_; }
+    
+private:
+    /// Load optional parameters from an XML file
+    void LoadParameters();
+    
+    /// Sound data
+    SharedArrayPtr<signed char> data_;
+    /// Loop start
+    signed char* repeat_;
+    /// Sound data end
+    signed char* end_;
+    /// Sound data size in bytes
+    unsigned dataSize_;
+    /// Default frequency
+    unsigned frequency_;
+    /// Looped flag
+    bool looped_;
+    /// Sixteen bit flag
+    bool sixteenBit_;
+    /// Stereo flag
+    bool stereo_;
+    /// Compressed flag
+    bool compressed_;
+    /// Compressed sound length
+    float compressedLength_;
+};

+ 1219 - 0
Engine/Audio/SoundSource.cpp

@@ -0,0 +1,1219 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "Precompiled.h"
+#include "Audio.h"
+#include "Context.h"
+#include "ResourceCache.h"
+#include "Sound.h"
+#include "SoundSource.h"
+
+#include <cstring>
+
+#include "DebugNew.h"
+
+#define INC_POS_LOOPED() \
+    pos += intAdd; \
+    fractPos += fractAdd; \
+    if (fractPos > 65535) \
+    { \
+        fractPos &= 65535; \
+        ++pos; \
+    } \
+    while (pos >= end) \
+        pos -= (end - repeat); \
+
+#define INC_POS_ONESHOT() \
+    pos += intAdd; \
+    fractPos += fractAdd; \
+    if (fractPos > 65535) \
+    { \
+        fractPos &= 65535; \
+        ++pos; \
+    } \
+    if (pos >= end) \
+    { \
+        pos = 0; \
+        break; \
+    } \
+
+#define INC_POS_STEREO_LOOPED() \
+    pos += (intAdd << 1); \
+    fractPos += fractAdd; \
+    if (fractPos > 65535) \
+    { \
+        fractPos &= 65535; \
+        pos += 2; \
+    } \
+    while (pos >= end) \
+        pos -= (end - repeat); \
+
+#define INC_POS_STEREO_ONESHOT() \
+    pos += (intAdd << 1); \
+    fractPos += fractAdd; \
+    if (fractPos > 65535) \
+    { \
+        fractPos &= 65535; \
+        pos += 2; \
+    } \
+    if (pos >= end) \
+    { \
+        pos = 0; \
+        break; \
+    } \
+
+#define GET_IP_SAMPLE() (((((int)pos[1] - (int)pos[0]) * fractPos) / 65536) + (int)pos[0])
+
+#define GET_IP_SAMPLE_LEFT() (((((int)pos[2] - (int)pos[0]) * fractPos) / 65536) + (int)pos[0])
+
+#define GET_IP_SAMPLE_RIGHT() (((((int)pos[3] - (int)pos[1]) * fractPos) / 65536) + (int)pos[1])
+
+static const char* typeNames[] = 
+{
+    "master",
+    "effect",
+    "music",
+    "voice",
+    0
+};
+
+// Compressed audio decode buffer length in milliseconds
+static const int DECODE_BUFFER_LENGTH = 100;
+static const float AUTOREMOVE_DELAY = 0.25f;
+
+OBJECTTYPESTATIC(SoundSource);
+
+SoundSource::SoundSource(Context* context) :
+    Component(context),
+    position_(0),
+    fractPosition_(0),
+    timePosition_(0.0f),
+    soundType_(SOUND_EFFECT),
+    frequency_(0.0f),
+    gain_(1.0f),
+    attenuation_(1.0f),
+    panning_(0.0f),
+    autoRemoveTimer_(0.0f),
+    autoRemove_(false),
+    decoder_(0),
+    decodePosition_(0)
+{
+    audio_ = GetSubsystem<Audio>();
+    
+    if (audio_)
+        audio_->AddSoundSource(this);
+}
+
+SoundSource::~SoundSource()
+{
+    if (audio_)
+        audio_->RemoveSoundSource(this);
+    
+    FreeDecoder();
+}
+
+void SoundSource::RegisterObject(Context* context)
+{
+    context->RegisterFactory<SoundSource>();
+    
+    ENUM_ATTRIBUTE(SoundSource, "Sound Type", soundType_, typeNames, SOUND_EFFECT);
+    ATTRIBUTE(SoundSource, VAR_FLOAT, "Frequency", frequency_, 0.0f);
+    ATTRIBUTE(SoundSource, VAR_FLOAT, "Gain", gain_, 1.0f);
+    ATTRIBUTE(SoundSource, VAR_FLOAT, "Attenuation", attenuation_, 1.0f);
+    ATTRIBUTE(SoundSource, VAR_FLOAT, "Panning", panning_, 0.0f);
+    ATTRIBUTE(SoundSource, VAR_BOOL, "Autoremove on Stop", autoRemove_, false);
+    ATTRIBUTE(SoundSource, VAR_RESOURCEREF, "Sound", sound_, ResourceRef(Sound::GetTypeStatic()));
+    ATTRIBUTE(SoundSource, VAR_INT, "Play Position", position_, 0);
+}
+
+void SoundSource::OnSetAttribute(const AttributeInfo& attr, const Variant& value)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    
+    switch (attr.offset_)
+    {
+    case offsetof(SoundSource, sound_):
+        Play(cache->GetResource<Sound>(value.GetResourceRef().id_));
+        break;
+        
+    case offsetof(SoundSource, position_):
+        if (sound_)
+            SetPlayPosition(sound_->GetStart() + value.GetInt());
+        break;
+        
+    default:
+        Serializable::OnSetAttribute(attr, value);
+        break;
+    }
+}
+
+Variant SoundSource::OnGetAttribute(const AttributeInfo& attr)
+{
+    switch (attr.offset_)
+    {
+    case offsetof(SoundSource, sound_):
+        return GetResourceRef(sound_, Sound::GetTypeStatic());
+        
+    case offsetof(SoundSource, position_):
+        if (sound_)
+            return (int)(GetPlayPosition() - sound_->GetStart());
+        else
+            return 0;
+        
+    default:
+        return Serializable::OnGetAttribute(attr);
+    }
+}
+
+void SoundSource::Play(Sound* sound)
+{
+    if (!audio_)
+        return;
+    
+    // If no frequency set yet, set from the sound's default
+    if ((frequency_ == 0.0f) && (sound))
+        SetFrequency(sound->GetFrequency());
+    
+    // If sound source is currently playing, have to lock the audio mutex
+    if (position_)
+    {
+        MutexLock Lock(audio_->GetMutex());
+        PlayLockless(sound);
+    }
+    else
+        PlayLockless(sound);
+}
+
+void SoundSource::Play(Sound* sound, float frequency)
+{
+    SetFrequency(frequency);
+    Play(sound);
+}
+
+void SoundSource::Play(Sound* sound, float frequency, float gain)
+{
+    SetFrequency(frequency);
+    SetGain(gain);
+    Play(sound);
+}
+
+void SoundSource::Play(Sound* sound, float frequency, float gain, float panning)
+{
+    SetFrequency(frequency);
+    SetGain(gain);
+    SetPanning(panning);
+    Play(sound);
+}
+
+void SoundSource::Stop()
+{
+    if (!audio_)
+        return;
+    
+    // If sound source is currently playing, have to lock the audio mutex
+    if (position_)
+    {
+        MutexLock Lock(audio_->GetMutex());
+        StopLockless();
+    }
+    
+    // Free the compressed sound decoder now if any
+    FreeDecoder();
+    sound_.Reset();
+}
+
+void SoundSource::SetSoundType(SoundType type)
+{
+    if ((type == SOUND_MASTER) || (type >= MAX_SOUND_TYPES))
+        return;
+    
+    soundType_ = type;
+}
+
+void SoundSource::SetFrequency(float frequency)
+{
+    frequency_ = Clamp(frequency, 0.0f, 535232.0f);
+}
+
+void SoundSource::SetGain(float gain)
+{
+    gain_ = Max(gain, 0.0f);
+}
+
+void SoundSource::SetAttenuation(float attenuation)
+{
+    attenuation_ = Clamp(attenuation, 0.0f, 1.0f);
+}
+
+void SoundSource::SetPanning(float panning)
+{
+    panning_ = Clamp(panning, -1.0f, 1.0f);
+}
+
+void SoundSource::SetAutoRemove(bool enable)
+{
+    autoRemove_ = enable;
+}
+
+bool SoundSource::IsPlaying() const
+{
+    return sound_.GetPtr() != 0;
+}
+
+void SoundSource::SetPlayPosition(signed char* pos)
+{
+    if ((!audio_) || (!sound_))
+        return;
+    
+    MutexLock Lock(audio_->GetMutex());
+    SetPlayPositionLockless(pos);
+}
+
+void SoundSource::PlayLockless(Sound* sound)
+{
+    // Reset the time position in any case
+    timePosition_ = 0.0f;
+    
+    if (sound)
+    {
+        if (!sound->IsCompressed())
+        {
+            // Uncompressed sound start
+            signed char* start = sound->GetStart();
+            if (start)
+            {
+                // Free Decoder in case previous sound was compressed
+                FreeDecoder();
+                sound_ = sound;
+                position_ = start;
+                fractPosition_ = 0;
+                return;
+            }
+        }
+        else
+        {
+            // Compressed sound start
+            if (sound == sound_)
+            {
+                // If same compressed sound is already playing, rewind the Decoder
+                sound_->RewindDecoder(decoder_);
+                return;
+            }
+            else
+            {
+                // Else just set the new sound with a dummy start position. The mixing routine will allocate the new Decoder
+                FreeDecoder();
+                sound_ = sound;
+                position_ = sound->GetStart();
+                return;
+            }
+        }
+    }
+    
+    // If sound pointer is null or if sound has no data, stop playback
+    FreeDecoder();
+    sound_.Reset();
+    position_ = 0;
+}
+
+void SoundSource::StopLockless()
+{
+    position_ = 0;
+    timePosition_ = 0.0f;
+}
+
+void SoundSource::SetPlayPositionLockless(signed char* pos)
+{
+    // Setting position on a compressed sound is not supported
+    if ((!sound_) || (sound_->IsCompressed()))
+        return;
+    
+    signed char* start = sound_->GetStart();
+    signed char* end = sound_->Getend();
+    if (pos < start)
+        pos = start;
+    if ((sound_->IsSixteenBit()) && ((pos - start) & 1))
+        ++pos;
+    if (pos > end)
+        pos = end;
+    
+    position_ = pos;
+    timePosition_ = (float)((int)pos - (int)sound_->GetStart()) / (sound_->GetSampleSize() * sound_->GetFrequency());
+}
+
+void SoundSource::Update(float timeStep)
+{
+    if (!audio_)
+        return;
+    
+    // If there is no actual audio output, perform fake mixing into a nonexistent buffer to check stopping/looping
+    if (!audio_->IsInitialized())
+        MixNull(timeStep);
+    
+    // Free the sound if playback has stopped
+    if ((sound_) && (!position_))
+    {
+        FreeDecoder();
+        sound_.Reset();
+    }
+    
+    // Check for autoremove
+    if (autoRemove_)
+    {
+        if (!IsPlaying())
+        {
+            autoRemoveTimer_ += timeStep;
+            if (autoRemoveTimer_ > AUTOREMOVE_DELAY)
+            {
+                if (node_)
+                    node_->RemoveComponent(this);
+                return;
+            }
+        }
+        else
+            autoRemoveTimer_ = 0.0f;
+    }
+}
+
+void SoundSource::Mix(int* dest, unsigned samples, int mixRate, bool stereo, bool interpolate)
+{
+    if ((!position_) || (!sound_))
+        return;
+    
+    if (sound_->IsCompressed())
+    {
+        if (decoder_)
+        {
+            // If Decoder already exists, Decode new compressed audio
+            bool eof = false;
+            unsigned currentPos = position_ - decodeBuffer_->GetStart();
+            if (currentPos != decodePosition_)
+            {
+                // If buffer has wrapped, Decode first to the end
+                if (currentPos < decodePosition_)
+                {
+                    unsigned bytes = decodeBuffer_->GetDataSize() - decodePosition_;
+                    unsigned outBytes = sound_->Decode(decoder_, decodeBuffer_->GetStart() + decodePosition_, bytes);
+                    // If produced less output, end of sound encountered. Fill rest with zero
+                    if (outBytes < bytes)
+                    {
+                        memset(decodeBuffer_->GetStart() + decodePosition_ + outBytes, 0, bytes - outBytes);
+                        eof = true;
+                    }
+                    decodePosition_ = 0;
+                }
+                if (currentPos > decodePosition_)
+                {
+                    unsigned bytes = currentPos - decodePosition_;
+                    unsigned outBytes = sound_->Decode(decoder_, decodeBuffer_->GetStart() + decodePosition_, bytes);
+                    // If produced less output, end of sound encountered. Fill rest with zero
+                    if (outBytes < bytes)
+                    {
+                        memset(decodeBuffer_->GetStart() + decodePosition_ + outBytes, 0, bytes - outBytes);
+                        if (sound_->IsLooped())
+                            eof = true;
+                    }
+                    
+                    // If wrote to buffer start, correct interpolation wraparound
+                    if (!decodePosition_)
+                        decodeBuffer_->FixInterpolation();
+                }
+            }
+            
+            // If end of stream encountered, check whether we should rewind or stop
+            if (eof)
+            {
+                if (sound_->IsLooped())
+                {
+                    sound_->RewindDecoder(decoder_);
+                    timePosition_ = 0.0f;
+                }
+                else
+                    decodeBuffer_->SetLooped(false); // Stop after the current Decode buffer has been played
+            }
+            
+            decodePosition_ = currentPos;
+        }
+        else
+        {
+            // Setup the decoder and decode buffer
+            decoder_ = sound_->AllocateDecoder();
+            unsigned sampleSize = sound_->GetSampleSize();
+            unsigned DecodeBufferSize = sampleSize * sound_->GetIntFrequency() * DECODE_BUFFER_LENGTH / 1000;
+            decodeBuffer_ = new Sound(context_);
+            decodeBuffer_->SetSize(DecodeBufferSize);
+            decodeBuffer_->SetFormat(sound_->GetIntFrequency(), true, sound_->IsStereo());
+            
+            // Clear the decode buffer, then fill with initial audio data and set it to loop
+            memset(decodeBuffer_->GetStart(), 0, DecodeBufferSize);
+            sound_->Decode(decoder_, decodeBuffer_->GetStart(), DecodeBufferSize);
+            decodeBuffer_->SetLooped(true);
+            decodePosition_ = 0;
+            
+            // Start playing the decode buffer
+            position_ = decodeBuffer_->GetStart();
+            fractPosition_ = 0;
+        }
+    }
+    
+    // If compressed, play the decode buffer. Otherwise play the original sound
+    Sound* sound = sound_->IsCompressed() ? decodeBuffer_ : sound_;
+    if (!sound)
+        return;
+    
+    // Choose the correct mixing routine
+    if (!sound->IsStereo())
+    {
+        if (interpolate)
+        {
+            if (stereo)
+                MixMonoToStereoIP(sound, dest, samples, mixRate);
+            else
+                MixMonoToMonoIP(sound, dest, samples, mixRate);
+        }
+        else
+        {
+            if (stereo)
+                MixMonoToStereo(sound, dest, samples, mixRate);
+            else
+                MixMonoToMono(sound, dest, samples, mixRate);
+        }
+    }
+    else
+    {
+        if (interpolate)
+        {
+            if (stereo)
+                MixStereoToStereoIP(sound, dest, samples, mixRate);
+            else
+                MixStereoToMonoIP(sound, dest, samples, mixRate);
+        }
+        else
+        {
+            if (stereo)
+                MixStereoToStereo(sound, dest, samples, mixRate);
+            else
+                MixStereoToMono(sound, dest, samples, mixRate);
+        }
+    }
+    
+    // Update the time position
+    if (!sound_->IsCompressed())
+        timePosition_ = (float)((int)position_ - (int)sound_->GetStart()) / (sound_->GetSampleSize() * sound_->GetFrequency());
+    else
+        timePosition_ += ((float)samples / (float)mixRate) * frequency_ / sound_->GetFrequency();
+}
+
+void SoundSource::MixMonoToMono(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int vol = (int)(256.0f * totalGain + 0.5f);
+    if (!vol)
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + (*pos * vol) / 256;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + (*pos * vol) / 256;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + *pos * vol;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + *pos * vol;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixMonoToStereo(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int leftVol = (int)((-panning_ + 1.0f) * (256.0f * totalGain + 0.5f));
+    int rightVol = (int)((panning_ + 1.0f) * (256.0f * totalGain + 0.5f));
+    if ((!leftVol) && (!rightVol))
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + (*pos * leftVol) / 256;
+                ++dest;
+                *dest = *dest + (*pos * rightVol) / 256;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + (*pos * leftVol) / 256;
+                ++dest;
+                *dest = *dest + (*pos * rightVol) / 256;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + *pos * leftVol;
+                ++dest;
+                *dest = *dest + *pos * rightVol;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + *pos * leftVol;
+                ++dest;
+                *dest = *dest + *pos * rightVol;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixMonoToMonoIP(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int vol = (int)(256.0f * totalGain + 0.5f);
+    if (!vol)
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + (GET_IP_SAMPLE() * vol) / 256;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + (GET_IP_SAMPLE() * vol) / 256;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + GET_IP_SAMPLE() * vol;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + GET_IP_SAMPLE() * vol;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixMonoToStereoIP(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int leftVol = (int)((-panning_ + 1.0f) * (256.0f * totalGain + 0.5f));
+    int rightVol = (int)((panning_ + 1.0f) * (256.0f * totalGain + 0.5f));
+    if ((!leftVol) && (!rightVol))
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                int s = GET_IP_SAMPLE();
+                *dest = *dest + (s * leftVol) / 256;
+                ++dest;
+                *dest = *dest + (s * rightVol) / 256;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                int s = GET_IP_SAMPLE();
+                *dest = *dest + (s * leftVol) / 256;
+                ++dest;
+                *dest = *dest + (s * rightVol) / 256;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                int s = GET_IP_SAMPLE();
+                *dest = *dest + s * leftVol;
+                ++dest;
+                *dest = *dest + s * rightVol;
+                ++dest;
+                INC_POS_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                int s = GET_IP_SAMPLE();
+                *dest = *dest + s * leftVol;
+                ++dest;
+                *dest = *dest + s * rightVol;
+                ++dest;
+                INC_POS_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixStereoToMono(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int vol = (int)(256.0f * totalGain + 0.5f);
+    if (!vol)
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                int s = ((int)pos[0] + (int)pos[1]) / 2;
+                *dest = *dest + (s * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                int s = ((int)pos[0] + (int)pos[1]) / 2;
+                *dest = *dest + (s * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                int s = ((int)pos[0] + (int)pos[1]) / 2;
+                *dest = *dest + s * vol;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                int s = ((int)pos[0] + (int)pos[1]) / 2;
+                *dest = *dest + s * vol;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixStereoToStereo(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int vol = (int)(256.0f * totalGain + 0.5f);
+    if (!vol)
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + (pos[0] * vol) / 256;
+                ++dest;
+                *dest = *dest + (pos[1] * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + (pos[0] * vol) / 256;
+                ++dest;
+                *dest = *dest + (pos[1] * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + pos[0] * vol;
+                ++dest;
+                *dest = *dest + pos[1] * vol;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + pos[0] * vol;
+                ++dest;
+                *dest = *dest + pos[1] * vol;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixStereoToMonoIP(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int vol = (int)(256.0f * totalGain + 0.5f);
+    if (!vol)
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                int s = (GET_IP_SAMPLE_LEFT() + GET_IP_SAMPLE_RIGHT()) / 2;
+                *dest = *dest + (s * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                int s = (GET_IP_SAMPLE_LEFT() + GET_IP_SAMPLE_RIGHT()) / 2;
+                *dest = *dest + (s * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                int s = (GET_IP_SAMPLE_LEFT() + GET_IP_SAMPLE_RIGHT()) / 2;
+                *dest = *dest + s * vol;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                int s = (GET_IP_SAMPLE_LEFT() + GET_IP_SAMPLE_RIGHT()) / 2;
+                *dest = *dest + s * vol;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixStereoToStereoIP(Sound* sound, int* dest, unsigned samples, int mixRate)
+{
+    float totalGain = audio_->GetSoundSourceMasterGain(soundType_) * attenuation_ * gain_;
+    int vol = (int)(256.0f * totalGain + 0.5f);
+    if (!vol)
+    {
+        MixZeroVolume(sound, samples, mixRate);
+        return;
+    }
+    
+    float add = frequency_ / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    int fractPos = fractPosition_;
+    
+    if (sound->IsSixteenBit())
+    {
+        short* pos = (short*)position_;
+        short* end = (short*)sound->Getend();
+        short* repeat = (short*)sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + (GET_IP_SAMPLE_LEFT() * vol) / 256;
+                ++dest;
+                *dest = *dest + (GET_IP_SAMPLE_RIGHT() * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = (signed char*)pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + (GET_IP_SAMPLE_LEFT() * vol) / 256;
+                ++dest;
+                *dest = *dest + (GET_IP_SAMPLE_RIGHT() * vol) / 256;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = (signed char*)pos;
+        }
+    }
+    else
+    {
+        signed char* pos = (signed char*)position_;
+        signed char* end = sound->Getend();
+        signed char* repeat = sound->GetRepeat();
+        
+        if (sound->IsLooped())
+        {
+            while (samples--)
+            {
+                *dest = *dest + GET_IP_SAMPLE_LEFT() * vol;
+                ++dest;
+                *dest = *dest + GET_IP_SAMPLE_RIGHT() * vol;
+                ++dest;
+                INC_POS_STEREO_LOOPED();
+            }
+            position_ = pos;
+        }
+        else
+        {
+            while (samples--)
+            {
+                *dest = *dest + GET_IP_SAMPLE_LEFT() * vol;
+                ++dest;
+                *dest = *dest + GET_IP_SAMPLE_RIGHT() * vol;
+                ++dest;
+                INC_POS_STEREO_ONESHOT();
+            }
+            position_ = pos;
+        }
+    }
+    
+    fractPosition_ = fractPos;
+}
+
+void SoundSource::MixZeroVolume(Sound* sound, unsigned samples, int mixRate)
+{
+    float add = frequency_ * (float)samples / (float)mixRate;
+    int intAdd = (int)add;
+    int fractAdd = (int)((add - floorf(add)) * 65536.0f);
+    unsigned sampleSize = sound->GetSampleSize();
+    
+    fractPosition_ += fractAdd;
+    if (fractPosition_ > 65535)
+    {
+        fractPosition_ &= 65535;
+        position_ += sampleSize;
+    }
+    position_ += intAdd * sampleSize;
+    
+    if (position_ > sound->Getend())
+    {
+        if (sound->IsLooped())
+        {
+            while (position_ >= sound->Getend())
+            {
+                position_ -= (sound->Getend() - sound->GetRepeat());
+            }
+        }
+        else
+            position_ = 0;
+    }
+}
+
+void SoundSource::MixNull(float timeStep)
+{
+    if ((!position_) || (!sound_))
+        return;
+    
+    // Advance only the time position
+    timePosition_ += timeStep * frequency_ / sound_->GetFrequency();
+    
+    if (sound_->IsLooped())
+    {
+        // For simulated playback, simply reset the time position to zero when the sound loops
+        if (timePosition_ >= sound_->GetLength())
+            timePosition_ -= sound_->GetLength();
+    }
+    else
+    {
+        if (timePosition_ >= sound_->GetLength())
+        {
+            position_ = 0;
+            timePosition_ = 0.0f;
+        }
+    }
+}
+
+void SoundSource::FreeDecoder()
+{
+    if ((sound_) && (decoder_))
+    {
+        sound_->FreeDecoder(decoder_);
+        decoder_ = 0;
+    }
+    
+    decodeBuffer_.Reset();
+}

Some files were not shown because too many files changed in this diff