diff --git a/.gitignore b/.gitignore
index 594c411c2..51d98a916 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,6 @@ project.xcworkspace/
 *.dSYM/
 
 .DS_Store
+Platform/Windows/Build
+Platform/Windows/Win32
+Platform/Windows/x64
diff --git a/platform/Windows/eduke32.sln b/platform/Windows/eduke32.sln
index 8d9faaf28..25a48ad95 100644
--- a/platform/Windows/eduke32.sln
+++ b/platform/Windows/eduke32.sln
@@ -29,6 +29,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "glad", "glad.vcxproj", "{6A
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rednukem", "rednukem.vcxproj", "{44C4B4F0-B489-4612-B9C7-A38503B7FB67}"
 EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libsmackerdec", "libsmackerdec.vcxproj", "{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "nblood", "nblood.vcxproj", "{5407CB5A-4B15-41FA-B79B-8B386A14D275}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Win32 = Debug|Win32
@@ -101,6 +105,22 @@ Global
 		{44C4B4F0-B489-4612-B9C7-A38503B7FB67}.Release|Win32.Build.0 = Release|Win32
 		{44C4B4F0-B489-4612-B9C7-A38503B7FB67}.Release|x64.ActiveCfg = Release|x64
 		{44C4B4F0-B489-4612-B9C7-A38503B7FB67}.Release|x64.Build.0 = Release|x64
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Debug|Win32.ActiveCfg = Debug|Win32
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Debug|Win32.Build.0 = Debug|Win32
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Debug|x64.ActiveCfg = Debug|x64
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Debug|x64.Build.0 = Debug|x64
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Release|Win32.ActiveCfg = Release|Win32
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Release|Win32.Build.0 = Release|Win32
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Release|x64.ActiveCfg = Release|x64
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}.Release|x64.Build.0 = Release|x64
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Debug|Win32.ActiveCfg = Debug|Win32
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Debug|Win32.Build.0 = Debug|Win32
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Debug|x64.ActiveCfg = Debug|x64
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Debug|x64.Build.0 = Debug|x64
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Release|Win32.ActiveCfg = Release|Win32
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Release|Win32.Build.0 = Release|Win32
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Release|x64.ActiveCfg = Release|x64
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275}.Release|x64.Build.0 = Release|x64
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -114,6 +134,8 @@ Global
 		{32D4CF70-A3D6-4CEA-81D7-64C743980276} = {8BD117A0-7FA2-44B0-88A5-29D6C220601E}
 		{6AC1D997-8DAE-4343-8DD8-DA2A1CA63212} = {8BD117A0-7FA2-44B0-88A5-29D6C220601E}
 		{44C4B4F0-B489-4612-B9C7-A38503B7FB67} = {18C3CB5F-1DE6-4CFE-B7F7-ABE824222E1C}
+		{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45} = {8BD117A0-7FA2-44B0-88A5-29D6C220601E}
+		{5407CB5A-4B15-41FA-B79B-8B386A14D275} = {18C3CB5F-1DE6-4CFE-B7F7-ABE824222E1C}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {0346D2C0-3EB9-4AFB-973F-2904758D6C02}
diff --git a/platform/Windows/libsmackerdec.vcxproj b/platform/Windows/libsmackerdec.vcxproj
new file mode 100644
index 000000000..ea5001327
--- /dev/null
+++ b/platform/Windows/libsmackerdec.vcxproj
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\source\libsmackerdec\include\BitReader.h" />
+    <ClInclude Include="..\..\source\libsmackerdec\include\FileStream.h" />
+    <ClInclude Include="..\..\source\libsmackerdec\include\HuffmanVLC.h" />
+    <ClInclude Include="..\..\source\libsmackerdec\include\LogError.h" />
+    <ClInclude Include="..\..\source\libsmackerdec\include\SmackerDecoder.h" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="..\..\source\libsmackerdec\src\BitReader.cpp" />
+    <ClCompile Include="..\..\source\libsmackerdec\src\FileStream.cpp" />
+    <ClCompile Include="..\..\source\libsmackerdec\src\HuffmanVLC.cpp" />
+    <ClCompile Include="..\..\source\libsmackerdec\src\LogError.cpp" />
+    <ClCompile Include="..\..\source\libsmackerdec\src\SmackerDecoder.cpp" />
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>15.0</VCProjectVersion>
+    <ProjectGuid>{598F0D83-2C1B-4F7C-B04D-7FDD471C8C45}</ProjectGuid>
+    <RootNamespace>libsmackerdec</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0.17134.0</WindowsTargetPlatformVersion>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>StaticLibrary</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>StaticLibrary</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <ConfigurationType>StaticLibrary</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <ConfigurationType>StaticLibrary</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="props\build_x64.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="props\build_x64.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="props\build_x86.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="props\build_x86.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <LinkIncremental>false</LinkIncremental>
+    <OutDir>$(SolutionDir)Build\$(ProjectName)\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>$(Platform)\Build\$(ProjectName)\$(Configuration)\</IntDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <LinkIncremental>true</LinkIncremental>
+    <OutDir>$(SolutionDir)Build\$(ProjectName)\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>$(Platform)\Build\$(ProjectName)\$(Configuration)\</IntDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <LinkIncremental>true</LinkIncremental>
+    <OutDir>$(SolutionDir)Build\$(ProjectName)\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>$(Platform)\Build\$(ProjectName)\$(Configuration)\</IntDir>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <LinkIncremental>false</LinkIncremental>
+    <OutDir>$(SolutionDir)Build\$(ProjectName)\$(Platform)\$(Configuration)\</OutDir>
+    <IntDir>$(Platform)\Build\$(ProjectName)\$(Configuration)\</IntDir>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <ClCompile>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <PreprocessorDefinitions>NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
+      <OmitFramePointers>true</OmitFramePointers>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <DebugInformationFormat>None</DebugInformationFormat>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <ClCompile>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>Disabled</Optimization>
+      <PreprocessorDefinitions>_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+      <BufferSecurityCheck>false</BufferSecurityCheck>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <ClCompile>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>Disabled</Optimization>
+      <PreprocessorDefinitions>_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
+      <OmitFramePointers>false</OmitFramePointers>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+      <BufferSecurityCheck>false</BufferSecurityCheck>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <PrecompiledHeader>
+      </PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <PreprocessorDefinitions>NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
+      <OmitFramePointers>true</OmitFramePointers>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <DebugInformationFormat>None</DebugInformationFormat>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+    </Link>
+  </ItemDefinitionGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>
\ No newline at end of file
diff --git a/platform/Windows/libsmackerdec.vcxproj.filters b/platform/Windows/libsmackerdec.vcxproj.filters
new file mode 100644
index 000000000..13241b553
--- /dev/null
+++ b/platform/Windows/libsmackerdec.vcxproj.filters
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Header Files" />
+    <Filter Include="Source Files" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\source\libsmackerdec\include\BitReader.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\libsmackerdec\include\FileStream.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\libsmackerdec\include\HuffmanVLC.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\libsmackerdec\include\LogError.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\libsmackerdec\include\SmackerDecoder.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="..\..\source\libsmackerdec\src\BitReader.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\libsmackerdec\src\FileStream.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\libsmackerdec\src\HuffmanVLC.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\libsmackerdec\src\LogError.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\libsmackerdec\src\SmackerDecoder.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/platform/Windows/nblood.vcxproj b/platform/Windows/nblood.vcxproj
new file mode 100644
index 000000000..45823caa8
--- /dev/null
+++ b/platform/Windows/nblood.vcxproj
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>15.0</VCProjectVersion>
+    <ProjectGuid>{5407CB5A-4B15-41FA-B79B-8B386A14D275}</ProjectGuid>
+    <RootNamespace>nblood</RootNamespace>
+    <Keyword>MakeFileProj</Keyword>
+    <PlatformToolset>v142</PlatformToolset>
+    <WindowsTargetPlatformVersion>10.0.17134.0</WindowsTargetPlatformVersion>
+    <ProjectName>nblood</ProjectName>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+  </PropertyGroup>
+  <PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="props\build_x64.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="props\build_x64.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="props\build_x86.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="props\build_x86.props" />
+    <Import Project="props\build_common.props" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup>
+    <ConfigurationType>Application</ConfigurationType>
+    <OutDir>$(SolutionDir)..\..\</OutDir>
+    <IntDir>$(Platform)\Build\$(ProjectName)\$(Configuration)\</IntDir>
+    <NMakeIncludeSearchPath>$(NMakeIncludeSearchPath);..\..\source\build\include;..\..\source\mact\include;..\..\source\audiolib\include;..\..\source\enet\include;..\..\platform\windows\include</NMakeIncludeSearchPath>
+    <NMakeBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">nmake /f msvc.mak DEBUG=1 WINBITS=32 RENDERTYPE=SDL</NMakeBuildCommandLine>
+    <NMakeReBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">nmake /f msvc.mak veryclean all DEBUG=1 WINBITS=32 RENDERTYPE=SDL</NMakeReBuildCommandLine>
+    <NMakeCleanCommandLine Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">nmake /f msvc.mak veryclean DEBUG=1 WINBITS=32 RENDERTYPE=SDL</NMakeCleanCommandLine>
+    <NMakePreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">USE_OPENGL;POLYMER;SDL_USEFOLDER;SDL_TARGET=2</NMakePreprocessorDefinitions>
+    <NMakeBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">nmake /f msvc.mak WINBITS=32 RENDERTYPE=SDL</NMakeBuildCommandLine>
+    <NMakeReBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">nmake /f msvc.mak veryclean all WINBITS=32 RENDERTYPE=SDL</NMakeReBuildCommandLine>
+    <NMakeCleanCommandLine Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">nmake /f msvc.mak veryclean WINBITS=32 RENDERTYPE=SDL</NMakeCleanCommandLine>
+    <NMakePreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">USE_OPENGL;POLYMER;SDL_USEFOLDER;SDL_TARGET=2</NMakePreprocessorDefinitions>
+    <NMakeBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">nmake /f msvc.mak DEBUG=1 WINBITS=64 RENDERTYPE=SDL</NMakeBuildCommandLine>
+    <NMakeReBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">nmake /f msvc.mak veryclean all DEBUG=1 WINBITS=64 RENDERTYPE=SDL</NMakeReBuildCommandLine>
+    <NMakeCleanCommandLine Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">nmake /f msvc.mak veryclean DEBUG=1 WINBITS=64 RENDERTYPE=SDL</NMakeCleanCommandLine>
+    <NMakePreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">USE_OPENGL;POLYMER;NOASM;SDL_USEFOLDER;SDL_TARGET=2</NMakePreprocessorDefinitions>
+    <NMakeBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Release|x64'">nmake /f msvc.mak WINBITS=64 RENDERTYPE=SDL</NMakeBuildCommandLine>
+    <NMakeReBuildCommandLine Condition="'$(Configuration)|$(Platform)'=='Release|x64'">nmake /f msvc.mak veryclean all WINBITS=64 RENDERTYPE=SDL</NMakeReBuildCommandLine>
+    <NMakeCleanCommandLine Condition="'$(Configuration)|$(Platform)'=='Release|x64'">nmake /f msvc.mak veryclean WINBITS=64 RENDERTYPE=SDL</NMakeCleanCommandLine>
+    <NMakePreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">USE_OPENGL;POLYMER;NOASM;SDL_USEFOLDER;SDL_TARGET=2</NMakePreprocessorDefinitions>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <GenerateManifest>false</GenerateManifest>
+    <EmbedManifest>false</EmbedManifest>
+    <LinkIncremental>false</LinkIncremental>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <GenerateManifest>false</GenerateManifest>
+    <EmbedManifest>false</EmbedManifest>
+    <LinkIncremental>false</LinkIncremental>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <EmbedManifest>false</EmbedManifest>
+    <LinkIncremental>false</LinkIncremental>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <EmbedManifest>false</EmbedManifest>
+    <LinkIncremental>false</LinkIncremental>
+    <GenerateManifest>false</GenerateManifest>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Link>
+      <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+    </Link>
+    <ClCompile>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
+      <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
+      <WarningLevel>Level3</WarningLevel>
+      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+      <SupportJustMyCode>true</SupportJustMyCode>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <ConformanceMode>true</ConformanceMode>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <Optimization>Disabled</Optimization>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <BufferSecurityCheck>false</BufferSecurityCheck>
+      <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;WIN32;RENDERTYPESDL=1;MIXERTYPEWIN=1;SDL_USEFOLDER;SDL_TARGET=2;USE_OPENGL=1;POLYMER=1;STARTUP_WINDOW;USE_LIBVPX;HAVE_VORBIS;HAVE_XMP;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ClCompile>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Link>
+      <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <LinkTimeCodeGeneration>UseLinkTimeCodeGeneration</LinkTimeCodeGeneration>
+    </Link>
+    <ClCompile>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
+      <OmitFramePointers>true</OmitFramePointers>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <ConformanceMode>true</ConformanceMode>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <DebugInformationFormat>None</DebugInformationFormat>
+    </ClCompile>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <ClCompile>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
+      <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
+      <OmitFramePointers>false</OmitFramePointers>
+      <WarningLevel>Level3</WarningLevel>
+      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+      <SupportJustMyCode>true</SupportJustMyCode>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <ConformanceMode>true</ConformanceMode>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <Optimization>Disabled</Optimization>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <BufferSecurityCheck>false</BufferSecurityCheck>
+    </ClCompile>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Link>
+      <LinkTimeCodeGeneration>UseLinkTimeCodeGeneration</LinkTimeCodeGeneration>
+    </Link>
+    <ClCompile>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
+      <OmitFramePointers>true</OmitFramePointers>
+      <WarningLevel>Level3</WarningLevel>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <ConformanceMode>true</ConformanceMode>
+      <LanguageStandard>stdcpp14</LanguageStandard>
+      <ExceptionHandling>SyncCThrow</ExceptionHandling>
+      <DebugInformationFormat>None</DebugInformationFormat>
+    </ClCompile>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClCompile Include="..\..\source\blood\rsrc\eduke32_icon.c" />
+    <ClCompile Include="..\..\source\blood\src\actor.cpp" />
+    <ClCompile Include="..\..\source\blood\src\ai.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aibat.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aibeast.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aiboneel.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aiburn.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aicaleb.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aicerber.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aicult.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aigarg.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aighost.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aigilbst.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aihand.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aihound.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aiinnoc.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aipod.cpp" />
+    <ClCompile Include="..\..\source\blood\src\airat.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aispid.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aitchern.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aiunicult.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aizomba.cpp" />
+    <ClCompile Include="..\..\source\blood\src\aizombf.cpp" />
+    <ClCompile Include="..\..\source\blood\src\asound.cpp" />
+    <ClCompile Include="..\..\source\blood\src\blood.cpp" />
+    <ClCompile Include="..\..\source\blood\src\callback.cpp" />
+    <ClCompile Include="..\..\source\blood\src\choke.cpp" />
+    <ClCompile Include="..\..\source\blood\src\common.cpp" />
+    <ClCompile Include="..\..\source\blood\src\config.cpp" />
+    <ClCompile Include="..\..\source\blood\src\controls.cpp" />
+    <ClCompile Include="..\..\source\blood\src\credits.cpp" />
+    <ClCompile Include="..\..\source\blood\src\db.cpp" />
+    <ClCompile Include="..\..\source\blood\src\demo.cpp" />
+    <ClCompile Include="..\..\source\blood\src\dude.cpp" />
+    <ClCompile Include="..\..\source\blood\src\endgame.cpp" />
+    <ClCompile Include="..\..\source\blood\src\eventq.cpp" />
+    <ClCompile Include="..\..\source\blood\src\fire.cpp" />
+    <ClCompile Include="..\..\source\blood\src\fx.cpp" />
+    <ClCompile Include="..\..\source\blood\src\gamemenu.cpp" />
+    <ClCompile Include="..\..\source\blood\src\gameutil.cpp" />
+    <ClCompile Include="..\..\source\blood\src\getopt.cpp" />
+    <ClCompile Include="..\..\source\blood\src\gib.cpp" />
+    <ClCompile Include="..\..\source\blood\src\globals.cpp" />
+    <ClCompile Include="..\..\source\blood\src\gmtimbre.cpp" />
+    <ClCompile Include="..\..\source\blood\src\inifile.cpp" />
+    <ClCompile Include="..\..\source\blood\src\iob.cpp" />
+    <ClCompile Include="..\..\source\blood\src\levels.cpp" />
+    <ClCompile Include="..\..\source\blood\src\loadsave.cpp" />
+    <ClCompile Include="..\..\source\blood\src\map2d.cpp" />
+    <ClCompile Include="..\..\source\blood\src\menu.cpp" />
+    <ClCompile Include="..\..\source\blood\src\messages.cpp" />
+    <ClCompile Include="..\..\source\blood\src\midi.cpp" />
+    <ClCompile Include="..\..\source\blood\src\mirrors.cpp" />
+    <ClCompile Include="..\..\source\blood\src\misc.cpp" />
+    <ClCompile Include="..\..\source\blood\src\mpu401.cpp" />
+    <ClCompile Include="..\..\source\blood\src\music.cpp" />
+    <ClCompile Include="..\..\source\blood\src\network.cpp" />
+    <ClCompile Include="..\..\source\blood\src\osdcmd.cpp" />
+    <ClCompile Include="..\..\source\blood\src\player.cpp" />
+    <ClCompile Include="..\..\source\blood\src\pqueue.cpp" />
+    <ClCompile Include="..\..\source\blood\src\qav.cpp" />
+    <ClCompile Include="..\..\source\blood\src\qheap.cpp" />
+    <ClCompile Include="..\..\source\blood\src\replace.cpp" />
+    <ClCompile Include="..\..\source\blood\src\resource.cpp" />
+    <ClCompile Include="..\..\source\blood\src\screen.cpp" />
+    <ClCompile Include="..\..\source\blood\src\sectorfx.cpp" />
+    <ClCompile Include="..\..\source\blood\src\seq.cpp" />
+    <ClCompile Include="..\..\source\blood\src\sfx.cpp" />
+    <ClCompile Include="..\..\source\blood\src\sound.cpp" />
+    <ClCompile Include="..\..\source\blood\src\startwin.game.cpp" />
+    <ClCompile Include="..\..\source\blood\src\tile.cpp" />
+    <ClCompile Include="..\..\source\blood\src\trig.cpp" />
+    <ClCompile Include="..\..\source\blood\src\triggers.cpp" />
+    <ClCompile Include="..\..\source\blood\src\view.cpp" />
+    <ClCompile Include="..\..\source\blood\src\warp.cpp" />
+    <ClCompile Include="..\..\source\blood\src\weapon.cpp" />
+    <ClCompile Include="..\..\source\blood\src\winbits.cpp" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="audiolib.vcxproj">
+      <Project>{0029c61b-b63d-4e61-99f2-f4e49aabfc47}</Project>
+    </ProjectReference>
+    <ProjectReference Include="build.vcxproj">
+      <Project>{dbecb851-5624-4fa8-9a9d-7169d0f12ff1}</Project>
+    </ProjectReference>
+    <ProjectReference Include="enet.vcxproj">
+      <Project>{a68cc5e4-567a-44c8-94fe-c1de09aeeb40}</Project>
+    </ProjectReference>
+    <ProjectReference Include="glad.vcxproj">
+      <Project>{6ac1d997-8dae-4343-8dd8-da2a1ca63212}</Project>
+    </ProjectReference>
+    <ProjectReference Include="libsmackerdec.vcxproj">
+      <Project>{598f0d83-2c1b-4f7c-b04d-7fdd471c8c45}</Project>
+    </ProjectReference>
+    <ProjectReference Include="libxmp-lite.vcxproj">
+      <Project>{32d4cf70-a3d6-4cea-81d7-64c743980276}</Project>
+    </ProjectReference>
+    <ProjectReference Include="mact.vcxproj">
+      <Project>{bcde1852-e2c6-4abb-84fb-5cd431764a9a}</Project>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <ResourceCompile Include="..\..\source\blood\rsrc\gameres.rc">
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
+    </ResourceCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\source\blood\src\actor.h" />
+    <ClInclude Include="..\..\source\blood\src\ai.h" />
+    <ClInclude Include="..\..\source\blood\src\aibat.h" />
+    <ClInclude Include="..\..\source\blood\src\aibeast.h" />
+    <ClInclude Include="..\..\source\blood\src\aiboneel.h" />
+    <ClInclude Include="..\..\source\blood\src\aiburn.h" />
+    <ClInclude Include="..\..\source\blood\src\aicaleb.h" />
+    <ClInclude Include="..\..\source\blood\src\aicerber.h" />
+    <ClInclude Include="..\..\source\blood\src\aicult.h" />
+    <ClInclude Include="..\..\source\blood\src\aigarg.h" />
+    <ClInclude Include="..\..\source\blood\src\aighost.h" />
+    <ClInclude Include="..\..\source\blood\src\aigilbst.h" />
+    <ClInclude Include="..\..\source\blood\src\aihand.h" />
+    <ClInclude Include="..\..\source\blood\src\aihound.h" />
+    <ClInclude Include="..\..\source\blood\src\aiinnoc.h" />
+    <ClInclude Include="..\..\source\blood\src\aipod.h" />
+    <ClInclude Include="..\..\source\blood\src\airat.h" />
+    <ClInclude Include="..\..\source\blood\src\aispid.h" />
+    <ClInclude Include="..\..\source\blood\src\aitchern.h" />
+    <ClInclude Include="..\..\source\blood\src\aiunicult.h" />
+    <ClInclude Include="..\..\source\blood\src\aizomba.h" />
+    <ClInclude Include="..\..\source\blood\src\aizombf.h" />
+    <ClInclude Include="..\..\source\blood\src\asound.h" />
+    <ClInclude Include="..\..\source\blood\src\blood.h" />
+    <ClInclude Include="..\..\source\blood\src\callback.h" />
+    <ClInclude Include="..\..\source\blood\src\choke.h" />
+    <ClInclude Include="..\..\source\blood\src\common_game.h" />
+    <ClInclude Include="..\..\source\blood\src\config.h" />
+    <ClInclude Include="..\..\source\blood\src\controls.h" />
+    <ClInclude Include="..\..\source\blood\src\credits.h" />
+    <ClInclude Include="..\..\source\blood\src\db.h" />
+    <ClInclude Include="..\..\source\blood\src\demo.h" />
+    <ClInclude Include="..\..\source\blood\src\dude.h" />
+    <ClInclude Include="..\..\source\blood\src\endgame.h" />
+    <ClInclude Include="..\..\source\blood\src\eventq.h" />
+    <ClInclude Include="..\..\source\blood\src\fire.h" />
+    <ClInclude Include="..\..\source\blood\src\function.h" />
+    <ClInclude Include="..\..\source\blood\src\fx.h" />
+    <ClInclude Include="..\..\source\blood\src\gamedefs.h" />
+    <ClInclude Include="..\..\source\blood\src\gamemenu.h" />
+    <ClInclude Include="..\..\source\blood\src\gameutil.h" />
+    <ClInclude Include="..\..\source\blood\src\getopt.h" />
+    <ClInclude Include="..\..\source\blood\src\gib.h" />
+    <ClInclude Include="..\..\source\blood\src\globals.h" />
+    <ClInclude Include="..\..\source\blood\src\map2d.h" />
+    <ClInclude Include="..\..\source\blood\src\menu.h" />
+    <ClInclude Include="..\..\source\blood\src\messages.h" />
+    <ClInclude Include="..\..\source\blood\src\mirrors.h" />
+    <ClInclude Include="..\..\source\blood\src\warp.h" />
+    <ClInclude Include="..\..\source\blood\src\inifile.h" />
+    <ClInclude Include="..\..\source\blood\src\iob.h" />
+    <ClInclude Include="..\..\source\blood\src\levels.h" />
+    <ClInclude Include="..\..\source\blood\src\loadsave.h" />
+    <ClInclude Include="..\..\source\blood\src\midi.h" />
+    <ClInclude Include="..\..\source\blood\src\misc.h" />
+    <ClInclude Include="..\..\source\blood\src\mpu401.h" />
+    <ClInclude Include="..\..\source\blood\src\network.h" />
+    <ClInclude Include="..\..\source\blood\src\osdcmds.h" />
+    <ClInclude Include="..\..\source\blood\src\player.h" />
+    <ClInclude Include="..\..\source\blood\src\pqueue.h" />
+    <ClInclude Include="..\..\source\blood\src\qav.h" />
+    <ClInclude Include="..\..\source\blood\src\qheap.h" />
+    <ClInclude Include="..\..\source\blood\src\replace.h" />
+    <ClInclude Include="..\..\source\blood\src\resource.h" />
+    <ClInclude Include="..\..\source\blood\src\screen.h" />
+    <ClInclude Include="..\..\source\blood\src\sectorfx.h" />
+    <ClInclude Include="..\..\source\blood\src\seq.h" />
+    <ClInclude Include="..\..\source\blood\src\sfx.h" />
+    <ClInclude Include="..\..\source\blood\src\sound.h" />
+    <ClInclude Include="..\..\source\blood\src\startwin.game.h" />
+    <ClInclude Include="..\..\source\blood\src\tile.h" />
+    <ClInclude Include="..\..\source\blood\src\trig.h" />
+    <ClInclude Include="..\..\source\blood\src\triggers.h" />
+    <ClInclude Include="..\..\source\blood\src\view.h" />
+    <ClInclude Include="..\..\source\blood\src\weapon.h" />
+    <ClInclude Include="..\..\source\blood\src\_functio.h" />
+    <ClInclude Include="..\..\source\blood\src\_midi.h" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+  </ImportGroup>
+</Project>
\ No newline at end of file
diff --git a/platform/Windows/nblood.vcxproj.filters b/platform/Windows/nblood.vcxproj.filters
new file mode 100644
index 000000000..f9a20c903
--- /dev/null
+++ b/platform/Windows/nblood.vcxproj.filters
@@ -0,0 +1,485 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Source Files">
+      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Header Files">
+      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="Resource Files">
+      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="..\..\source\blood\rsrc\eduke32_icon.c">
+      <Filter>Resource Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\blood.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\levels.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\winbits.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\startwin.game.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\network.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\osdcmd.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\config.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\getopt.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\controls.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\demo.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\screen.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\qheap.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\resource.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\misc.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\inifile.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\tile.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\trig.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\replace.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\globals.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\db.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\iob.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\seq.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\sfx.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\dude.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\player.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\sound.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\mpu401.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\music.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\midi.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\gameutil.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\pqueue.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\eventq.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\callback.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\fx.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\actor.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\endgame.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\qav.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\triggers.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\view.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\sectorfx.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\gib.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\weapon.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\warp.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\mirrors.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\ai.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\gamemenu.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\menu.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\messages.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\map2d.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\asound.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\credits.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\choke.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\fire.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aibat.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aibeast.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aiboneel.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aiburn.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aicaleb.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aicerber.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aicult.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aigarg.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aighost.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aigilbst.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aihand.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aihound.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aiinnoc.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aipod.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\airat.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aispid.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aitchern.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aizomba.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aizombf.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\loadsave.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\gmtimbre.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\common.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\source\blood\src\aiunicult.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <ResourceCompile Include="..\..\source\blood\rsrc\gameres.rc">
+      <Filter>Resource Files</Filter>
+    </ResourceCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\source\blood\src\levels.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\common_game.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\startwin.game.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\network.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\blood.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\_functio.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\function.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\config.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\gamedefs.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\getopt.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\controls.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\demo.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\db.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\actor.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\trig.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\screen.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\qheap.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\resource.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\misc.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\inifile.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\tile.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\globals.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\replace.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\iob.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\seq.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\sfx.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\player.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\dude.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\sound.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\midi.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\mpu401.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\_midi.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\loadsave.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\gameutil.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\pqueue.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\eventq.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\callback.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\fx.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\endgame.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\ai.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\qav.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\triggers.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\view.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\sectorfx.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\gib.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\weapon.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\warp.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\mirrors.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\gamemenu.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\menu.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\messages.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\map2d.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\asound.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\credits.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\choke.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\fire.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aibat.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aibeast.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aiboneel.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aiburn.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aicaleb.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aicerber.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aicult.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aigarg.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aighost.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aigilbst.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aihand.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aihound.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aiinnoc.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aipod.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\airat.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aispid.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aitchern.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aizomba.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aizombf.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\osdcmds.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\source\blood\src\aiunicult.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/platform/Windows/props/build_common.props b/platform/Windows/props/build_common.props
index 649af037e..3eb400a68 100644
--- a/platform/Windows/props/build_common.props
+++ b/platform/Windows/props/build_common.props
@@ -6,7 +6,7 @@
   <ItemDefinitionGroup>
     <ClCompile>
       <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;WIN32;RENDERTYPESDL=1;MIXERTYPEWIN=1;SDL_USEFOLDER;SDL_TARGET=2;USE_OPENGL=1;STARTUP_WINDOW;USE_LIBVPX;HAVE_VORBIS;HAVE_XMP;%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories>./include;./include/vpx/;./include/sdl2/;../../source/build/include;../../source/mact/include;../../source/audiolib/include;../../source/enet/include;../../source/glad/include;../../source/libxmp-lite/include;../../source/libxmp-lite/include/libxmp-lite</AdditionalIncludeDirectories>
+      <AdditionalIncludeDirectories>./include;./include/vpx/;./include/sdl2/;../../source/build/include;../../source/mact/include;../../source/audiolib/include;../../source/enet/include;../../source/glad/include;../../source/libxmp-lite/include;../../source/libxmp-lite/include/libxmp-lite;../../source/libsmackerdec/include</AdditionalIncludeDirectories>
       <DisableSpecificWarnings>4996;4244;4018;4267</DisableSpecificWarnings>
       <AdditionalOptions>/J %(AdditionalOptions)</AdditionalOptions>
     </ClCompile>
diff --git a/platform/Windows/rednukem.vcxproj b/platform/Windows/rednukem.vcxproj
index 03f24f6ff..73e8c1908 100644
--- a/platform/Windows/rednukem.vcxproj
+++ b/platform/Windows/rednukem.vcxproj
@@ -225,7 +225,6 @@
     <ClCompile Include="..\..\source\rr\src\config.cpp" />
     <ClCompile Include="..\..\source\rr\src\common.cpp" />
     <ClCompile Include="..\..\source\rr\src\demo.cpp" />
-    <ClCompile Include="..\..\source\rr\src\file_lib.cpp" />
     <ClCompile Include="..\..\source\rr\src\game.cpp" />
     <ClCompile Include="..\..\source\rr\src\gamedef.cpp" />
     <ClCompile Include="..\..\source\rr\src\gameexec.cpp" />
diff --git a/platform/Windows/rednukem.vcxproj.filters b/platform/Windows/rednukem.vcxproj.filters
index 646966325..add4d899c 100644
--- a/platform/Windows/rednukem.vcxproj.filters
+++ b/platform/Windows/rednukem.vcxproj.filters
@@ -272,9 +272,6 @@
     <ClCompile Include="..\..\source\rr\rsrc\eduke32_icon.c">
       <Filter>Resource Files</Filter>
     </ClCompile>
-    <ClCompile Include="..\..\source\rr\src\file_lib.cpp">
-      <Filter>Source Files</Filter>
-    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\source\rr\rsrc\gameres.rc">
diff --git a/source/audiolib/include/multivoc.h b/source/audiolib/include/multivoc.h
index 2dd5c9987..c07ec70a2 100644
--- a/source/audiolib/include/multivoc.h
+++ b/source/audiolib/include/multivoc.h
@@ -67,6 +67,9 @@ enum MV_Errors
     MV_InvalidFile,
 };
 
+void DisableInterrupts(void);
+void RestoreInterrupts(void);
+
 extern void (*MV_Printf)(const char *fmt, ...);
 const char *MV_ErrorString(int32_t ErrorNumber);
 int32_t MV_VoicePlaying(int32_t handle);
diff --git a/source/audiolib/include/music.h b/source/audiolib/include/music.h
index 18a84ead6..678b94a60 100644
--- a/source/audiolib/include/music.h
+++ b/source/audiolib/include/music.h
@@ -53,12 +53,6 @@ typedef struct
    uint32_t  tick;
    } songposition;
 
-enum MIDI_Device
-{
-    MIDIDEVICE_NONE = -1,
-    MIDIDEVICE_MPU = 0,
-    MIDIDEVICE_OPL
-};
 
 #define MUSIC_LoopSong ( 1 == 1 )
 #define MUSIC_PlayOnce ( !MUSIC_LoopSong )
diff --git a/source/audiolib/src/multivoc.cpp b/source/audiolib/src/multivoc.cpp
index 0c03f66b2..76f723087 100644
--- a/source/audiolib/src/multivoc.cpp
+++ b/source/audiolib/src/multivoc.cpp
@@ -96,13 +96,13 @@ float MV_GlobalVolume = 1.f;
 
 static int32_t lockdepth = 0;
 
-static FORCE_INLINE void DisableInterrupts(void)
+void DisableInterrupts(void)
 {
     if (!lockdepth++)
         SoundDriver_Lock();
 }
 
-static FORCE_INLINE void RestoreInterrupts(void)
+void RestoreInterrupts(void)
 {
     if (!--lockdepth)
         SoundDriver_Unlock();
diff --git a/source/blood/Dependencies.mak b/source/blood/Dependencies.mak
new file mode 100644
index 000000000..0794ac543
--- /dev/null
+++ b/source/blood/Dependencies.mak
@@ -0,0 +1,191 @@
+ai_h=\
+	$(blood_src)/aibat.h \
+	$(blood_src)/aibeast.h \
+	$(blood_src)/aiboneel.h \
+	$(blood_src)/aiburn.h \
+	$(blood_src)/aicaleb.h \
+	$(blood_src)/aicerber.h \
+	$(blood_src)/aicult.h \
+	$(blood_src)/aigarg.h \
+	$(blood_src)/aighost.h \
+	$(blood_src)/aigilbst.h \
+	$(blood_src)/aihand.h \
+	$(blood_src)/aihound.h \
+	$(blood_src)/aiinnoc.h \
+	$(blood_src)/aipod.h \
+	$(blood_src)/airat.h \
+	$(blood_src)/aispid.h \
+	$(blood_src)/aitchern.h \
+	$(blood_src)/aiunicult.h \
+	$(blood_src)/aizomba.h \
+	$(blood_src)/aizombf.h
+	
+common_h=\
+    $(engine_inc)/compat.h \
+    $(engine_inc)/common.h \
+    $(engine_inc)/pragmas.h \
+    $(engine_inc)/build.h \
+    $(engine_inc)/baselayer.h \
+    $(engine_inc)/palette.h \
+    $(engine_inc)/polymer.h \
+    $(engine_inc)/polymost.h \
+    $(engine_inc)/texcache.h \
+    $(engine_inc)/cache1d.h \
+    $(mact_inc)/file_lib.h \
+    $(mact_inc)/keyboard.h \
+    $(mact_inc)/mouse.h \
+    $(mact_inc)/joystick.h \
+    $(mact_inc)/control.h \
+    $(audiolib_inc)/fx_man.h \
+    $(audiolib_inc)/music.h \
+    $(blood_src)/common_game.h \
+    $(blood_src)/blood.h \
+    $(blood_src)/actor.h \
+	$(blood_src)/ai.h \
+    $(blood_src)/al_midi.h \
+    $(blood_src)/asound.h \
+    $(blood_src)/callback.h \
+    $(blood_src)/choke.h \
+    $(blood_src)/config.h \
+    $(blood_src)/controls.h \
+    $(blood_src)/credits.h \
+    $(blood_src)/db.h \
+    $(blood_src)/demo.h \
+    $(blood_src)/dude.h \
+    $(blood_src)/endgame.h \
+    $(blood_src)/eventq.h \
+    $(blood_src)/fire.h \
+    $(blood_src)/function.h \
+    $(blood_src)/fx.h \
+    $(blood_src)/gamedefs.h \
+    $(blood_src)/gamemenu.h \
+    $(blood_src)/getopt.h \
+    $(blood_src)/gib.h \
+    $(blood_src)/globals.h \
+    $(blood_src)/inifile.h \
+    $(blood_src)/iob.h \
+    $(blood_src)/levels.h \
+    $(blood_src)/loadsave.h \
+    $(blood_src)/map2d.h \
+    $(blood_src)/menu.h \
+    $(blood_src)/messages.h \
+    $(blood_src)/midi.h \
+    $(blood_src)/mirrors.h \
+    $(blood_src)/misc.h \
+    $(blood_src)/mpu401.h \
+    $(blood_src)/network.h \
+    $(blood_src)/opl3.h \
+    $(blood_src)/osdcmds.h \
+    $(blood_src)/player.h \
+    $(blood_src)/pqueue.h \
+    $(blood_src)/qav.h \
+    $(blood_src)/qheap.h \
+    $(blood_src)/replace.h \
+    $(blood_src)/resource.h \
+    $(blood_src)/screen.h \
+    $(blood_src)/sectorfx.h \
+    $(blood_src)/seq.h \
+    $(blood_src)/sfx.h \
+    $(blood_src)/sound.h \
+    $(blood_src)/tile.h \
+    $(blood_src)/trig.h \
+    $(blood_src)/triggers.h \
+    $(blood_src)/view.h \
+    $(blood_src)/warp.h \
+    $(blood_src)/tile.h
+
+$(blood_obj)/blood.$o: $(blood_src)/blood.cpp $(common_h)
+$(blood_obj)/actor.$o: $(blood_src)/actor.cpp $(common_h) $(ai_h)
+$(blood_obj)/ai.$o: $(blood_src)/ai.cpp $(common_h) $(ai_h)
+$(blood_obj)/aibat.$o: $(blood_src)/aibat.cpp $(common_h) $(ai_h)
+$(blood_obj)/aibeast.$o: $(blood_src)/aibeast.cpp $(common_h) $(ai_h)
+$(blood_obj)/aiboneel.$o: $(blood_src)/aiboneel.cpp $(common_h) $(ai_h)
+$(blood_obj)/aiburn.$o: $(blood_src)/aiburn.cpp $(common_h) $(ai_h)
+$(blood_obj)/aicaleb.$o: $(blood_src)/aicaleb.cpp $(common_h) $(ai_h)
+$(blood_obj)/aicerber.$o: $(blood_src)/aicerber.cpp $(common_h) $(ai_h)
+$(blood_obj)/aicult.$o: $(blood_src)/aicult.cpp $(common_h) $(ai_h)
+$(blood_obj)/aigarg.$o: $(blood_src)/aigarg.cpp $(common_h) $(ai_h)
+$(blood_obj)/aighost.$o: $(blood_src)/aighost.cpp $(common_h) $(ai_h)
+$(blood_obj)/aigilbst.$o: $(blood_src)/aigilbst.cpp $(common_h) $(ai_h)
+$(blood_obj)/aihand.$o: $(blood_src)/aihand.cpp $(common_h) $(ai_h)
+$(blood_obj)/aihound.$o: $(blood_src)/aihound.cpp $(common_h) $(ai_h)
+$(blood_obj)/aiinnoc.$o: $(blood_src)/aiinnoc.cpp $(common_h) $(ai_h)
+$(blood_obj)/aipod.$o: $(blood_src)/aipod.cpp $(common_h) $(ai_h)
+$(blood_obj)/airat.$o: $(blood_src)/airat.cpp $(common_h) $(ai_h)
+$(blood_obj)/aispid.$o: $(blood_src)/aispid.cpp $(common_h) $(ai_h)
+$(blood_obj)/aitchern.$o: $(blood_src)/aitchern.cpp $(common_h) $(ai_h)
+$(blood_obj)/aiunicult.$o: $(blood_src)/aiunicult.cpp $(common_h) $(ai_h)
+$(blood_obj)/aizomba.$o: $(blood_src)/aizomba.cpp $(common_h) $(ai_h)
+$(blood_obj)/aizombf.$o: $(blood_src)/aizombf.cpp $(common_h) $(ai_h)
+$(blood_obj)/asound.$o: $(blood_src)/asound.cpp $(common_h)
+$(blood_obj)/callback.$o: $(blood_src)/callback.cpp $(common_h)
+$(blood_obj)/choke.$o: $(blood_src)/choke.cpp $(common_h)
+$(blood_obj)/common.$o: $(blood_src)/common.cpp $(common_h)
+$(blood_obj)/config.$o: $(blood_src)/config.cpp $(common_h)
+$(blood_obj)/controls.$o: $(blood_src)/controls.cpp $(common_h)
+$(blood_obj)/credits.$o: $(blood_src)/credits.cpp $(common_h)
+$(blood_obj)/db.$o: $(blood_src)/db.cpp $(common_h)
+$(blood_obj)/demo.$o: $(blood_src)/demo.cpp $(common_h)
+$(blood_obj)/dude.$o: $(blood_src)/dude.cpp $(common_h)
+$(blood_obj)/endgame.$o: $(blood_src)/endgame.cpp $(common_h)
+$(blood_obj)/eventq.$o: $(blood_src)/eventq.cpp $(common_h)
+$(blood_obj)/fire.$o: $(blood_src)/fire.cpp $(common_h)
+$(blood_obj)/fx.$o: $(blood_src)/fx.cpp $(common_h)
+$(blood_obj)/gamemenu.$o: $(blood_src)/gamemenu.cpp $(common_h)
+$(blood_obj)/gameutil.$o: $(blood_src)/gameutil.cpp $(common_h)
+$(blood_obj)/getopt.$o: $(blood_src)/getopt.cpp $(common_h)
+$(blood_obj)/gib.$o: $(blood_src)/gib.cpp $(common_h)
+$(blood_obj)/globals.$o: $(blood_src)/globals.cpp $(common_h)
+$(blood_obj)/inifile.$o: $(blood_src)/inifile.cpp $(common_h)
+$(blood_obj)/iob.$o: $(blood_src)/iob.cpp $(common_h)
+$(blood_obj)/levels.$o: $(blood_src)/levels.cpp $(common_h)
+$(blood_obj)/loadsave.$o: $(blood_src)/loadsave.cpp $(common_h)
+$(blood_obj)/map2d.$o: $(blood_src)/map2d.cpp $(common_h)
+$(blood_obj)/menu.$o: $(blood_src)/menu.cpp $(common_h)
+$(blood_obj)/messages.$o: $(blood_src)/messages.cpp $(common_h)
+$(blood_obj)/mirrors.$o: $(blood_src)/mirrors.cpp $(common_h)
+$(blood_obj)/misc.$o: $(blood_src)/misc.cpp $(common_h)
+$(blood_obj)/network.$o: $(blood_src)/network.cpp $(common_h)
+$(blood_obj)/osdcmd.$o: $(blood_src)/osdcmd.cpp $(common_h)
+$(blood_obj)/player.$o: $(blood_src)/player.cpp $(common_h)
+$(blood_obj)/pqueue.$o: $(blood_src)/pqueue.cpp $(common_h)
+$(blood_obj)/qav.$o: $(blood_src)/qav.cpp $(common_h)
+$(blood_obj)/qheap.$o: $(blood_src)/qheap.cpp $(common_h)
+$(blood_obj)/replace.$o: $(blood_src)/replace.cpp $(common_h)
+$(blood_obj)/resource.$o: $(blood_src)/resource.cpp $(common_h)
+$(blood_obj)/screen.$o: $(blood_src)/screen.cpp $(common_h)
+$(blood_obj)/sectorfx.$o: $(blood_src)/sectorfx.cpp $(common_h)
+$(blood_obj)/seq.$o: $(blood_src)/seq.cpp $(common_h)
+$(blood_obj)/sfx.$o: $(blood_src)/sfx.cpp $(common_h)
+$(blood_obj)/sound.$o: $(blood_src)/sound.cpp $(common_h)
+$(blood_obj)/tile.$o: $(blood_src)/tile.cpp $(common_h)
+$(blood_obj)/trig.$o: $(blood_src)/trig.cpp $(common_h)
+$(blood_obj)/triggers.$o: $(blood_src)/triggers.cpp $(common_h)
+$(blood_obj)/view.$o: $(blood_src)/view.cpp $(common_h) $(ai_h)
+$(blood_obj)/warp.$o: $(blood_src)/warp.cpp $(common_h)
+$(blood_obj)/weapon.$o: $(blood_src)/weapon.cpp $(common_h)
+$(blood_obj)/winbits.$o: $(blood_src)/winbits.cpp
+
+# misc objects
+$(blood_obj)/game_icon.$o: $(blood_rsrc)/game_icon.c $(blood_rsrc)/game_icon.ico
+
+$(blood_obj)/gameres.$o: $(blood_rsrc)/gameres.rc $(blood_src)/startwin.game.h $(blood_rsrc)/game.bmp
+$(blood_obj)/buildres.$o: $(blood_rsrc)/buildres.rc $(engine_inc)/startwin.editor.h $(blood_rsrc)/build.bmp
+$(blood_obj)/startwin.game.$o: $(blood_src)/startwin.game.cpp $(blood_h) $(engine_inc)/build.h $(engine_inc)/winlayer.h $(engine_inc)/compat.h
+$(blood_obj)/startgtk.game.$o: $(blood_src)/startgtk.game.cpp $(blood_h) $(engine_inc)/dynamicgtk.h $(engine_inc)/build.h $(engine_inc)/baselayer.h $(engine_inc)/compat.h
+
+# mact objects
+$(mact_obj)/animlib.$o: $(mact_src)/animlib.cpp $(mact_inc)/animlib.h $(engine_inc)/compat.h
+$(mact_obj)/file_lib.$o: $(mact_src)/file_lib.cpp $(mact_inc)/file_lib.h
+$(mact_obj)/control.$o: $(mact_src)/control.cpp $(mact_inc)/control.h $(mact_inc)/keyboard.h $(mact_inc)/mouse.h $(mact_inc)/joystick.h $(engine_inc)/baselayer.h
+$(mact_obj)/keyboard.$o: $(mact_src)/keyboard.cpp $(mact_inc)/keyboard.h $(engine_inc)/compat.h $(engine_inc)/baselayer.h
+$(mact_obj)/joystick.$o: $(mact_src)/joystick.cpp $(mact_inc)/joystick.h $(engine_inc)/baselayer.h
+$(mact_obj)/scriplib.$o: $(mact_src)/scriplib.cpp $(mact_inc)/scriplib.h $(mact_src)/_scrplib.h $(engine_inc)/compat.h
+
+$(blood_obj)/al_midi.$o: $(blood_src)/al_midi.cpp $(engine_inc)/compat.h $(blood_src)/al_midi.h $(blood_src)/_al_midi.h $(blood_src)/opl3.h
+$(blood_obj)/gmtimbre.$o: $(blood_src)/gmtimbre.cpp
+$(blood_obj)/opl3.$o: $(blood_src)/opl3.cpp
+$(blood_obj)/midi.$o: $(blood_src)/midi.cpp $(blood_src)/_midi.h $(blood_src)/midi.h $(blood_src)/al_midi.h $(audiolib_inc)/music.h
+$(blood_obj)/oplmidi.$o: $(blood_src)/oplmidi.cpp $(blood_src)/_oplmidi.h $(blood_src)/oplmidi.h $(blood_src)/al_midi.h $(audiolib_inc)/music.h
+$(blood_obj)/mpu401.$o: $(blood_src)/mpu401.cpp $(blood_src)/mpu401.h $(audiolib_inc)/music.h
+$(blood_obj)/music.$o: $(blood_src)/music.cpp $(blood_src)/midi.h $(blood_src)/mpu401.h $(blood_src)/al_midi.h $(audiolib_inc)/music.h
diff --git a/source/blood/gpl-2.0.txt b/source/blood/gpl-2.0.txt
new file mode 100644
index 000000000..d159169d1
--- /dev/null
+++ b/source/blood/gpl-2.0.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/source/blood/rsrc/build.bmp b/source/blood/rsrc/build.bmp
new file mode 100644
index 000000000..e101f838b
Binary files /dev/null and b/source/blood/rsrc/build.bmp differ
diff --git a/source/blood/rsrc/build_icon.c b/source/blood/rsrc/build_icon.c
new file mode 100644
index 000000000..a9b6fe43b
--- /dev/null
+++ b/source/blood/rsrc/build_icon.c
@@ -0,0 +1 @@
+#include "eduke32_icon.c"
diff --git a/source/blood/rsrc/build_icon.ico b/source/blood/rsrc/build_icon.ico
new file mode 100644
index 000000000..0bfa7fb2c
Binary files /dev/null and b/source/blood/rsrc/build_icon.ico differ
diff --git a/source/blood/rsrc/buildres.rc b/source/blood/rsrc/buildres.rc
new file mode 100644
index 000000000..18557f388
--- /dev/null
+++ b/source/blood/rsrc/buildres.rc
@@ -0,0 +1,72 @@
+#define NEED_COMMCTRL_H
+#include "../../build/include/windows_inc.h"
+#include "../../build/include/startwin.editor.h"
+
+RSRC_ICON ICON "build_icon.ico"
+RSRC_BMP BITMAP "build.bmp"
+
+WIN_STARTWIN DIALOGEX DISCARDABLE  20, 40, 260, 200
+STYLE DS_MODALFRAME | DS_CENTER | DS_SETFONT | DS_FIXEDSYS | WS_OVERLAPPED | WS_CAPTION | WS_VISIBLE | WS_SYSMENU
+CAPTION "Startup"
+FONT 8, "MS Shell Dlg"
+BEGIN
+	CONTROL "", WIN_STARTWIN_BITMAP, "STATIC", SS_BITMAP | SS_CENTERIMAGE | WS_CHILD | WS_VISIBLE, 0, 0, 66, 172
+	CONTROL "", WIN_STARTWIN_TABCTL, WC_TABCONTROL, WS_CLIPSIBLINGS | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 5, 5, 250, 170
+	CONTROL "&Start", WIN_STARTWIN_START, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 180, 48, 14
+	CONTROL "&Cancel", WIN_STARTWIN_CANCEL, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 207, 180, 48, 14
+
+	CONTROL "", WIN_STARTWIN_MESSAGES, "EDIT", ES_MULTILINE | ES_READONLY | WS_CHILD | WS_VSCROLL, 0, 0, 32, 32
+END
+
+WIN_STARTWINPAGE_CONFIG DIALOGEX DISCARDABLE  20, 40, 279, 168
+STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD
+CAPTION "Dialog"
+FONT 8, "MS Shell Dlg"
+BEGIN
+	CONTROL "&2D Video mode:",   -1, "STATIC", SS_LEFT | WS_CHILD | WS_VISIBLE, 5, 8, 50, 8
+	CONTROL "",            IDC2DVMODE, "COMBOBOX", CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 60, 6, 80, 56
+	CONTROL "&Fullscreen", IDCFULLSCREEN, "BUTTON", BS_CHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 148, 8, 49, 10
+	CONTROL "&3D Video mode:",   -1, "STATIC", SS_LEFT | WS_CHILD | WS_VISIBLE, 5, 24, 50, 8
+	CONTROL "",            IDC3DVMODE, "COMBOBOX", CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 60, 22, 80, 56
+	CONTROL "&Always show this window at startup", IDCALWAYSSHOW, "BUTTON", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 118, 116, 140, 8
+END
+
+#define FILEVER        1,9,9,9
+#define PRODUCTVER     1,9,9,9
+#define STRFILEVER     "2.0.0devel\0"
+#define STRPRODUCTVER  "2.0.0devel\0"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION FILEVER
+ PRODUCTVERSION PRODUCTVER
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x3L
+#else
+ FILEFLAGS 0x2L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+            VALUE "FileDescription", "Mapster32 for EDuke32"
+            VALUE "FileVersion", STRFILEVER
+            VALUE "InternalName", "Mapster32"
+            VALUE "LegalCopyright", "Copyright � 2018 EDuke32 Developers, 1996, 2003 3D Realms Entertainment"
+            VALUE "LegalTrademarks", "Duke Nukem� is a Registered Trademark of Gearbox Software, LLC."
+            VALUE "OriginalFilename", "mapster32.exe"
+            VALUE "ProductName", "Mapster32"
+            VALUE "ProductVersion", STRPRODUCTVER
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+
+1 24 "manifest.build.xml"
diff --git a/source/blood/rsrc/eduke32_icon.c b/source/blood/rsrc/eduke32_icon.c
new file mode 100644
index 000000000..4a4da8e81
--- /dev/null
+++ b/source/blood/rsrc/eduke32_icon.c
@@ -0,0 +1,20 @@
+
+#include "sdl_inc.h"
+#include "sdlappicon.h"
+
+static Uint8 sdlappicon_pixels[] = {
+#if defined _WIN32 && SDL_MAJOR_VERSION==1
+# include "eduke32_icon_32px.c"
+#else
+# include "eduke32_icon_48px.c"
+#endif
+};
+
+struct sdlappicon sdlappicon = {
+#if defined _WIN32 && SDL_MAJOR_VERSION==1
+	32,32,
+#else
+	48,48,
+#endif
+	sdlappicon_pixels
+};
diff --git a/source/blood/rsrc/eduke32_icon_32px.c b/source/blood/rsrc/eduke32_icon_32px.c
new file mode 100644
index 000000000..f1dcedf2d
--- /dev/null
+++ b/source/blood/rsrc/eduke32_icon_32px.c
@@ -0,0 +1,196 @@
+/* GIMP RGBA C-Source image dump (eduke32_icon_32px.c) */
+#if 0
+static const struct {
+  unsigned int 	 width;
+  unsigned int 	 height;
+  unsigned int 	 bytes_per_pixel; /* 2:RGB16, 3:RGB, 4:RGBA */ 
+  unsigned char	 pixel_data[32 * 32 * 4 + 1];
+} sdlappicon = {
+  32, 32, 4,
+#endif
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\313\035\035\033\303\034\034]\311\034\034\225\340''\301\343<<\330\345JJ"
+  "\346\345JJ\346\342\065\065\330\337\040\040\301\314\035\035\225\303\034\034]\275\032"
+  "\032\033\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\302\033\033\062\320\035\035\240\345JJ\351\360\231\231\377\366\305\305\377"
+  "\372\341\341\377\375\364\364\377\375\360\360\377\373\343\343\377\365\272"
+  "\272\377\361\236\236\377\356\213\213\377\352mm\377\342\071\071\351\320\035\035"
+  "\240\302\033\033\062\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\257\030\030\007\311"
+  "\034\034\214\343AA\371\363\253\253\377\374\357\357\377\376\375\375\377\376"
+  "\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\371\371"
+  "\377\373\346\346\377\370\323\323\377\365\270\270\377\363\254\254\377\363"
+  "\262\262\377\360\231\231\377\344EE\371\302\033\033\214\257\030\030\007\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\313\035\035\062\322\036\036\320\353rr\377\366\301\301\377\371\330\330"
+  "\377\376\371\371\377\376\375\375\377\374\355\355\377\366\305\305\377\361"
+  "\236\236\377\356\207\207\377\355\202\202\377\356\215\215\377\363\256\256"
+  "\377\370\315\315\377\370\315\315\377\370\323\323\377\370\325\325\377\372"
+  "\341\341\377\361\236\236\377\325\036\036\320\302\033\033\062\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\307\034\034/\331\037"
+  "\037\350\353rr\377\360\235\235\377\363\262\262\377\374\353\353\377\367\307"
+  "\307\377\354{{\377\345JJ\377\341\064\064\377\340++\377\332\037\037\377\314\035"
+  "\035\377\303\034\034\377\313\035\035\377\340%%\377\351ff\377\366\277\277\377\374"
+  "\357\357\377\374\351\351\377\376\375\375\377\366\277\277\377\340''\350\302"
+  "\033\033/\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\314\035\035"
+  ",\327\036\036\360\351ll\377\354{{\377\365\272\272\377\370\317\317\377\347\\"
+  "\\\377\314\035\035\377\322\036\036\377\340++\377\341\064\064\377\342\065\065\377"
+  "\341\062\062\377\336\037\037\377\313\035\035\377\302\033\033\377\275\032\032\377\302"
+  "\033\033\377\314\035\035\377\351jj\377\374\353\353\377\376\375\375\377\376\375"
+  "\375\377\370\321\321\377\340%%\360\302\033\033,\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\237\026\026\005\320\035\035\322\351ff\377\353tt\377\363\253\253\377"
+  "\366\277\277\377\342\065\065\377\322\036\036\377\303\034\034\377\332\037\037\377"
+  "\342\065\065\377\343<<\377\343<<\377\342\065\065\377\337\040\040\377\320\035\035"
+  "\377\305\034\034\377\302\033\033\377\313\035\035\377\302\033\033\377\311\034\034\377"
+  "\346OO\377\371\334\334\377\375\366\366\377\371\330\330\377\361\236\236\377"
+  "\325\036\036\322\237\026\026\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\303\034\034\213"
+  "\350ee\377\360\225\225\377\365\270\270\377\365\272\272\377\307\034\034\377"
+  "\303\034\034\377\307\034\034\377\347WW\377\347WW\377\337\040\040\377\342\065\065"
+  "\377\342\067\067\377\340--\377\336\037\037\377\313\035\035\377\305\034\034\377\302"
+  "\033\033\377\340--\377\347XX\377\337\040\040\377\322\036\036\377\340''\377\367"
+  "\312\312\377\372\341\341\377\367\310\310\377\354yy\377\302\033\033\213\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\302\033\033+\342\065\065\372\363\256\256\377\363\253\253"
+  "\377\370\321\321\377\331\037\037\377\276\033\033\377\271\032\032\377\350ee\377"
+  "\374\357\357\377\367\307\307\377\320\035\035\377\262\031\031\377\242\027\027\377"
+  "\223\025\025\377\217\024\024\377\235\026\026\377\251\030\030\377\302\033\033\377\360"
+  "\225\225\377\376\373\373\377\355\204\204\377\320\035\035\377\275\032\032\377"
+  "\341..\377\371\330\330\377\365\270\270\377\363\253\253\377\342\067\067\372"
+  "\307\034\034+\000\000\000\000\000\000\000\000\314\035\035\242\357\224\224\377\367\307\307\377"
+  "\372\341\341\377\347WW\377\322\036\036\377\303\034\034\377\350__\377\373\346"
+  "\346\377\355\204\204\377\361\244\244\377\342\071\071\377z\021\021\377w\020\020"
+  "\377w\020\020\377w\020\020\377z\021\021\377w\020\020\377\244\027\027\377\364\263\263"
+  "\377\374\351\351\377\376\373\373\377\353tt\377\275\032\032\377\313\035\035\377"
+  "\346SS\377\370\323\323\377\360\231\231\377\353xx\377\320\035\035\242\000\000\000"
+  "\000\307\034\034\026\343<<\361\367\310\310\377\372\337\337\377\366\277\277\377"
+  "\302\033\033\377\311\034\034\377\343@@\377\373\346\346\377\356\211\211\377\340"
+  "--\377\366\277\277\377\370\323\323\377\223\025\025\377|\021\021\377|\021\021\377"
+  "|\021\021\377w\020\020\377z\021\021\377\354}}\377\367\310\310\377\347WW\377\370"
+  "\323\323\377\374\357\357\377\343@@\377\322\036\036\377\302\033\033\377\364\263"
+  "\263\377\364\263\263\377\360\227\227\377\341\062\062\361\307\034\034\026\302\033"
+  "\033\\\353xx\377\367\312\312\377\373\344\344\377\351ll\377\275\032\032\377\313"
+  "\035\035\377\365\272\272\377\371\334\334\377\345HH\377\363\262\262\377\376"
+  "\373\373\377\376\373\373\377\353rr\377z\021\021\377|\021\021\377|\021\021\377z"
+  "\021\021\377\244\027\027\377\375\366\366\377\376\373\373\377\364\267\267\377"
+  "\345JJ\377\370\325\325\377\363\260\260\377\302\033\033\377\322\036\036\377\350"
+  "]]\377\370\315\315\377\361\236\236\377\351ff\377\303\034\034\\\313\035\035\227"
+  "\357\220\220\377\367\310\310\377\370\323\323\377\340))\377\302\033\033\377"
+  "\347WW\377\376\373\373\377\360\233\233\377\353vv\377\376\373\373\377\376"
+  "\373\373\377\376\373\373\377\375\362\362\377\244\027\027\377\214\023\023\377"
+  "\235\026\026\377\217\024\024\377\353rr\377\376\373\373\377\376\373\373\377\376"
+  "\373\373\377\353tt\377\353vv\377\373\346\346\377\343AA\377\311\034\034\377"
+  "\331\037\037\377\367\312\312\377\363\256\256\377\355\202\202\377\313\035\035"
+  "\227\337\040\040\303\361\236\236\377\370\323\323\377\362\245\245\377\311\034"
+  "\034\377\275\032\032\377\357\220\220\377\374\353\353\377\347ZZ\377\371\330\330"
+  "\377\376\373\373\377\376\373\373\377\376\373\373\377\376\373\373\377\372"
+  "\337\337\377\373\346\346\377\376\373\373\377\367\307\307\377\376\373\373"
+  "\377\376\373\373\377\376\373\373\377\376\373\373\377\371\330\330\377\345"
+  "HH\377\370\325\325\377\356\213\213\377\311\034\034\377\276\033\033\377\362\247"
+  "\247\377\367\307\307\377\360\225\225\377\340%%\303\342\067\067\330\363\262"
+  "\262\377\372\335\335\377\356\215\215\377\275\032\032\377\313\035\035\377\364"
+  "\265\265\377\366\277\277\377\343AA\377\376\373\373\377\376\373\373\377\376"
+  "\373\373\377\376\373\373\377\376\373\373\377\376\373\373\377\372\341\341"
+  "\377\366\305\305\377\376\373\373\377\376\373\373\377\376\373\373\377\376"
+  "\373\373\377\376\373\373\377\376\373\373\377\343AA\377\364\265\265\377\363"
+  "\262\262\377\305\034\034\377\303\034\034\377\356\213\213\377\370\323\323\377"
+  "\363\253\253\377\342\071\071\330\344GG\346\367\314\314\377\373\346\346\377"
+  "\355\202\202\377\275\032\032\377\336\037\037\377\371\330\330\377\370\321\321"
+  "\377\357\224\224\377\376\373\373\377\376\373\373\377\376\373\373\377\376"
+  "\373\373\377\376\373\373\377\366\277\277\377\217\024\024\377z\021\021\377\337"
+  "\"\"\377\375\366\366\377\376\373\373\377\376\373\373\377\376\373\373\377"
+  "\376\373\373\377\357\220\220\377\367\310\310\377\370\325\325\377\325\036\036"
+  "\377\303\034\034\377\354}}\377\372\335\335\377\364\267\267\377\343AA\346\345"
+  "JJ\346\371\332\332\377\375\360\360\377\355\206\206\377\307\034\034\377\337"
+  "\040\040\377\355\206\206\377\360\235\235\377\353vv\377\352qq\377\353rr\377"
+  "\353rr\377\354}}\377\376\373\373\377\353vv\377w\020\020\377w\020\020\377\214"
+  "\023\023\377\376\367\367\377\371\330\330\377\353rr\377\353rr\377\352qq\377"
+  "\353vv\377\357\220\220\377\353vv\377\311\034\034\377\303\034\034\377\354}}\377"
+  "\371\332\332\377\364\267\267\377\344EE\346\343AA\330\371\330\330\377\376"
+  "\367\367\377\360\225\225\377\331\037\037\377\337\040\040\377\325\036\036\377\313"
+  "\035\035\377\230\025\025\377w\020\020\377z\021\021\377p\020\020\377\217\024\024\377\373"
+  "\350\350\377\375\362\362\377\235\026\026\377|\021\021\377\346UU\377\376\373\373"
+  "\377\342\071\071\377z\021\021\377z\021\021\377w\020\020\377\217\024\024\377\302\033"
+  "\033\377\275\032\032\377\302\033\033\377\275\032\032\377\356\213\213\377\370\325"
+  "\325\377\363\254\254\377\342\071\071\330\340))\303\367\312\312\377\376\367"
+  "\367\377\364\263\263\377\337$$\377\337\040\040\377\337\"\"\377\336\037\037\377"
+  "\260\031\031\377w\020\020\377|\021\021\377z\021\021\377z\021\021\377\345JJ\377\376"
+  "\373\373\377\375\366\366\377\374\351\351\377\376\373\373\377\371\332\332"
+  "\377\235\026\026\377|\021\021\377|\021\021\377w\020\020\377\251\030\030\377\307\034"
+  "\034\377\311\034\034\377\302\033\033\377\311\034\034\377\362\247\247\377\370\323"
+  "\323\377\361\236\236\377\340%%\303\313\035\035\227\363\262\262\377\375\362"
+  "\362\377\372\335\335\377\341\064\064\377\320\035\035\377\331\037\037\377\325\036"
+  "\036\377\302\033\033\377~\021\021\377w\020\020\377|\021\021\377w\020\020\377\212\023"
+  "\023\377\362\245\245\377\376\373\373\377\376\373\373\377\376\373\373\377\337"
+  "\"\"\377z\021\021\377|\021\021\377w\020\020\377~\021\021\377\275\032\032\377\305\034"
+  "\034\377\311\034\034\377\276\033\033\377\337$$\377\370\321\321\377\366\305\305"
+  "\377\357\220\220\377\313\035\035\227\275\032\032\\\355\202\202\377\372\341\341"
+  "\377\374\351\351\377\351jj\377\275\032\032\377\314\035\035\377\302\033\033\377"
+  "\276\033\033\377\250\030\030\377w\020\020\377z\021\021\377z\021\021\377\244\027\027\377"
+  "\375\362\362\377\376\373\373\377\376\373\373\377\376\373\373\377\362\245"
+  "\245\377z\021\021\377w\020\020\377w\020\020\377\244\027\027\377\276\033\033\377\276"
+  "\033\033\377\302\033\033\377\303\034\034\377\350__\377\371\330\330\377\365\270"
+  "\270\377\353tt\377\302\033\033\\\307\034\034\026\343<<\361\366\305\305\377\371"
+  "\332\332\377\365\272\272\377\275\032\032\377\302\033\033\377\275\032\032\377\276"
+  "\033\033\377\276\033\033\377\237\026\026\377z\021\021\377\217\024\024\377\375\362\362"
+  "\377\376\373\373\377\376\373\373\377\376\373\373\377\376\373\373\377\376"
+  "\373\373\377\346OO\377z\021\021\377\237\026\026\377\276\033\033\377\275\032\032\377"
+  "\275\032\032\377\302\033\033\377\303\034\034\377\365\270\270\377\366\305\305\377"
+  "\363\253\253\377\342\067\067\361\307\034\034\026\000\000\000\000\313\035\035\242\355\206"
+  "\206\377\363\262\262\377\371\326\326\377\346QQ\377\311\034\034\377\275\032\032"
+  "\377\275\032\032\377\276\033\033\377\276\033\033\377\232\026\026\377\346OO\377\372"
+  "\341\341\377\376\373\373\377\376\373\373\377\376\373\373\377\376\373\373"
+  "\377\372\335\335\377\366\277\277\377\260\031\031\377\302\033\033\377\276\033\033"
+  "\377\276\033\033\377\275\032\032\377\320\035\035\377\345NN\377\370\325\325\377"
+  "\361\244\244\377\355\202\202\377\320\035\035\242\000\000\000\000\000\000\000\000\302\033\033"
+  "+\340--\372\361\236\236\377\362\251\251\377\371\330\330\377\337\040\040\377"
+  "\311\034\034\377\275\032\032\377\276\033\033\377\275\032\032\377\347ZZ\377\371\334"
+  "\334\377\347\\\\\377\347WW\377\351ff\377\351ff\377\350__\377\343AA\377\365"
+  "\272\272\377\370\315\315\377\307\034\034\377\275\032\032\377\275\032\032\377\313"
+  "\035\035\377\337\040\040\377\370\323\323\377\360\235\235\377\360\235\235\377"
+  "\342\067\067\372\307\034\034+\000\000\000\000\000\000\000\000\000\000\000\000\302\033\033\213\350aa\377"
+  "\362\247\247\377\367\310\310\377\366\305\305\377\322\036\036\377\311\034\034"
+  "\377\276\033\033\377\275\032\032\377\357\217\217\377\376\373\373\377\373\350"
+  "\350\377\370\317\317\377\370\315\315\377\370\315\315\377\370\315\315\377"
+  "\374\351\351\377\376\367\367\377\363\262\262\377\325\036\036\377\275\032\032"
+  "\377\313\035\035\377\322\036\036\377\367\307\307\377\367\307\307\377\360\231"
+  "\231\377\351jj\377\311\034\034\213\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\237\026\026"
+  "\005\320\035\035\322\353xx\377\360\227\227\377\366\277\277\377\367\310\310\377"
+  "\341\062\062\377\311\034\034\377\276\033\033\377\325\036\036\377\345JJ\377\357\217"
+  "\217\377\364\265\265\377\370\315\315\377\370\315\315\377\364\265\265\377"
+  "\357\217\217\377\345JJ\377\302\033\033\377\275\032\032\377\313\035\035\377\343"
+  "<<\377\367\310\310\377\366\305\305\377\360\235\235\377\355\202\202\377\327"
+  "\036\036\322\237\026\026\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\307\034\034"
+  ",\332\037\037\360\356\207\207\377\360\235\235\377\370\315\315\377\371\326\326"
+  "\377\347\\\\\377\302\033\033\377\275\032\032\377\302\033\033\377\275\032\032\377"
+  "\320\035\035\377\337\040\040\377\337\"\"\377\320\035\035\377\275\032\032\377\311"
+  "\034\034\377\303\034\034\377\275\032\032\377\347\\\\\377\371\334\334\377\370\315"
+  "\315\377\361\236\236\377\360\225\225\377\340%%\360\307\034\034,\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\307\034\034/\336\037\037\350\355"
+  "\202\202\377\363\262\262\377\365\274\274\377\373\344\344\377\366\277\277"
+  "\377\352oo\377\341\062\062\377\320\035\035\377\307\034\034\377\302\033\033\377\302"
+  "\033\033\377\307\034\034\377\314\035\035\377\340--\377\351ll\377\364\267\267\377"
+  "\371\334\334\377\365\270\270\377\364\267\267\377\356\215\215\377\337$$\350"
+  "\307\034\034/\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\307\034\034\062\322\036\036\320\353tt\377\366\301\301\377\367\312\312"
+  "\377\372\341\341\377\373\346\346\377\370\325\325\377\363\256\256\377\360"
+  "\231\231\377\356\207\207\377\356\207\207\377\360\235\235\377\363\262\262"
+  "\377\370\325\325\377\372\341\341\377\370\323\323\377\364\263\263\377\365"
+  "\270\270\377\354}}\377\327\036\036\320\307\034\034\062\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\257\030\030"
+  "\007\303\034\034\214\343<<\371\360\225\225\377\366\305\305\377\370\315\315\377"
+  "\370\315\315\377\371\332\332\377\373\344\344\377\375\360\360\377\375\364"
+  "\364\377\373\346\346\377\371\330\330\377\367\312\312\377\367\310\310\377"
+  "\365\272\272\377\357\220\220\377\343<<\371\307\034\034\214\257\030\030\007\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\302\033\033\062\314\035\035\240\342\071\071"
+  "\351\354yy\377\360\225\225\377\363\254\254\377\366\305\305\377\371\330\330"
+  "\377\371\330\330\377\367\307\307\377\363\254\254\377\357\224\224\377\353"
+  "xx\377\343<<\351\320\035\035\240\302\033\033\062\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\275\032\032\033\276\033\033]\311\034"
+  "\034\225\337$$\301\343<<\330\344GG\346\344GG\346\343<<\330\340''\301\313\035"
+  "\035\225\276\033\033]\275\032\032\033\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
+#if 0
+};
+#endif
diff --git a/source/blood/rsrc/eduke32_icon_48px.c b/source/blood/rsrc/eduke32_icon_48px.c
new file mode 100644
index 000000000..918b9ca48
--- /dev/null
+++ b/source/blood/rsrc/eduke32_icon_48px.c
@@ -0,0 +1,423 @@
+/* GIMP RGBA C-Source image dump (test.c) */
+#if 0
+static const struct {
+  unsigned int 	 width;
+  unsigned int 	 height;
+  unsigned int 	 bytes_per_pixel; /* 2:RGB16, 3:RGB, 4:RGBA */ 
+  unsigned char	 pixel_data[48 * 48 * 4 + 1];
+} sdlappicon = {
+  48, 48, 4,
+#endif
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\037\004\004\001\325\036"
+  "\036(\325\036\036_\325\036\036\222\325\036\036\270\325\036\036\321\325\036\036\343\325"
+  "\036\036\355\325\036\036\355\325\036\036\343\325\036\036\321\325\036\036\270\325\036"
+  "\036\222\325\036\036_\325\036\036(\037\004\004\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036"
+  "\036\325\036\036s\325\036\036\304\325\036\036\365\343<<\377\350aa\377\355\202\202"
+  "\377\361\242\242\377\366\277\277\377\366\301\301\377\366\277\277\377\362"
+  "\251\251\377\355\202\202\377\350ee\377\345JJ\377\340--\377\325\036\036\365"
+  "\325\036\036\304\325\036\036s\325\036\036\036\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\334\037\037&\325\036\036\222\325\036\036\354\343"
+  "AA\377\360\227\227\377\371\334\334\377\375\364\364\377\376\371\371\377\376"
+  "\371\371\377\375\366\366\377\374\353\353\377\372\341\341\377\371\330\330"
+  "\377\370\315\315\377\365\274\274\377\364\267\267\377\364\267\267\377\363"
+  "\253\253\377\360\235\235\377\352oo\377\341\062\062\377\325\036\036\354\325\036"
+  "\036\222\334\037\037&\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\311\034\034\014\325\036\036"
+  "|\325\036\036\354\345NN\377\365\270\270\377\373\350\350\377\375\362\362\377"
+  "\375\362\362\377\375\366\366\377\375\360\360\377\375\362\362\377\375\362"
+  "\362\377\373\350\350\377\371\334\334\377\371\326\326\377\367\312\312\377"
+  "\365\270\270\377\363\262\262\377\363\254\254\377\361\244\244\377\361\242"
+  "\242\377\362\247\247\377\363\262\262\377\361\236\236\377\344GG\377\325\036"
+  "\036\354\325\036\036|\311\034\034\014\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036\064\325\036\036\305\341\062"
+  "\062\377\360\231\231\377\367\310\310\377\370\325\325\377\371\334\334\377\373"
+  "\350\350\377\374\357\357\377\375\364\364\377\375\366\366\377\375\366\366"
+  "\377\376\367\367\377\375\366\366\377\375\366\366\377\375\364\364\377\374"
+  "\353\353\377\372\341\341\377\370\323\323\377\365\270\270\377\361\236\236"
+  "\377\361\236\236\377\361\244\244\377\363\262\262\377\366\301\301\377\367"
+  "\310\310\377\363\254\254\377\342\067\067\377\325\036\036\305\325\036\036\064\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036Z\325"
+  "\036\036\356\344GG\377\361\236\236\377\363\262\262\377\365\272\272\377\367"
+  "\310\310\377\371\326\326\377\373\350\350\377\375\364\364\377\376\371\371"
+  "\377\371\330\330\377\362\251\251\377\355\206\206\377\351ll\377\347WW\377"
+  "\346OO\377\347XX\377\352oo\377\360\227\227\377\370\325\325\377\375\362\362"
+  "\377\370\325\325\377\365\270\270\377\365\274\274\377\366\301\301\377\367"
+  "\307\307\377\370\323\323\377\372\341\341\377\351ff\377\325\036\036\356\325"
+  "\036\036Z\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036q\325\036"
+  "\036\374\347XX\377\357\220\220\377\360\225\225\377\361\242\242\377\364\263"
+  "\263\377\370\325\325\377\375\360\360\377\370\323\323\377\356\213\213\377"
+  "\345NN\377\342\067\067\377\342\067\067\377\342\067\067\377\341\062\062\377\340++"
+  "\377\337$$\377\331\037\037\377\320\035\035\377\314\035\035\377\311\034\034\377\337"
+  "\040\040\377\353tt\377\370\315\315\377\374\353\353\377\371\326\326\377\370"
+  "\315\315\377\371\330\330\377\373\346\346\377\374\351\351\377\357\224\224"
+  "\377\325\036\036\374\325\036\036q\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036q\320"
+  "\035\035\377\347XX\377\355\206\206\377\355\206\206\377\356\215\215\377\361"
+  "\244\244\377\371\330\330\377\370\321\321\377\347\\\\\377\340''\377\340--"
+  "\377\342\065\065\377\342\067\067\377\342\067\067\377\342\067\067\377\342\065\065\377"
+  "\340++\377\340%%\377\327\036\036\377\320\035\035\377\314\035\035\377\303\034\034"
+  "\377\302\033\033\377\307\034\034\377\322\036\036\377\347XX\377\370\323\323\377"
+  "\374\353\353\377\373\343\343\377\373\350\350\377\373\346\346\377\374\351"
+  "\351\377\363\254\254\377\332\037\037\377\325\036\036q\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036"
+  "Z\325\036\036\374\347WW\377\354yy\377\353xx\377\354\177\177\377\363\254\254"
+  "\377\376\367\367\377\353tt\377\313\035\035\377\327\036\036\377\337\040\040\377"
+  "\340++\377\341\064\064\377\342\067\067\377\342\067\067\377\342\067\067\377\342\065"
+  "\065\377\340--\377\340%%\377\327\036\036\377\320\035\035\377\313\035\035\377\303"
+  "\034\034\377\302\033\033\377\313\035\035\377\325\036\036\377\331\037\037\377\336\037"
+  "\037\377\356\207\207\377\376\371\371\377\374\353\353\377\373\346\346\377\372"
+  "\341\341\377\371\334\334\377\361\236\236\377\325\036\036\374\325\036\036Z\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\327"
+  "\036\036\061\325\036\036\355\345NN\377\354{{\377\352oo\377\353tt\377\364\263\263"
+  "\377\373\350\350\377\343<<\377\275\032\032\377\303\034\034\377\320\035\035\377"
+  "\336\037\037\377\340''\377\341..\377\342\065\065\377\342\067\067\377\342\067\067"
+  "\377\342\065\065\377\340--\377\337$$\377\327\036\036\377\320\035\035\377\311\034"
+  "\034\377\302\033\033\377\303\034\034\377\320\035\035\377\331\037\037\377\332\037\037"
+  "\377\340''\377\340--\377\351ff\377\374\357\357\377\374\353\353\377\371\330"
+  "\330\377\370\315\315\377\367\310\310\377\354}}\377\325\036\036\355\327\036\036"
+  "\061\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\331\037\037\013"
+  "\325\036\036\310\342\071\071\377\356\213\213\377\354\177\177\377\354yy\377\364"
+  "\263\263\377\371\326\326\377\320\035\035\377\262\031\031\377\266\031\031\377\275"
+  "\032\032\377\307\034\034\377\327\036\036\377\337$$\377\340--\377\342\065\065\377"
+  "\342\067\067\377\342\067\067\377\342\065\065\377\340--\377\337$$\377\327\036\036"
+  "\377\314\035\035\377\311\034\034\377\302\033\033\377\311\034\034\377\325\036\036\377"
+  "\331\037\037\377\337\040\040\377\340--\377\341..\377\341..\377\345JJ\377\371"
+  "\334\334\377\373\343\343\377\366\277\277\377\365\270\270\377\366\277\277"
+  "\377\346QQ\377\325\036\036\310\331\037\037\013\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\325\036\036|\336\037\037\377\360\225\225\377\357\220\220\377"
+  "\356\207\207\377\363\262\262\377\372\341\341\377\303\034\034\377\257\030\030"
+  "\377\260\031\031\377\262\031\031\377\271\032\032\377\340''\377\363\254\254\377"
+  "\340--\377\340))\377\341\062\062\377\342\067\067\377\342\067\067\377\342\067\067"
+  "\377\340--\377\337$$\377\322\036\036\377\314\035\035\377\307\034\034\377\303\034"
+  "\034\377\320\035\035\377\331\037\037\377\353tt\377\345JJ\377\341..\377\341..\377"
+  "\340++\377\340''\377\342\065\065\377\373\344\344\377\370\323\323\377\364\263"
+  "\263\377\364\263\263\377\363\254\254\377\337$$\377\325\036\036|\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\322\036\036\"\325\036\036\356\354\177\177\377\361"
+  "\244\244\377\360\227\227\377\361\242\242\377\375\366\366\377\340''\377\257"
+  "\030\030\377\253\030\030\377\253\030\030\377\260\031\031\377\343<<\377\375\360\360"
+  "\377\370\323\323\377\361\236\236\377\337$$\377\331\037\037\377\275\032\032\377"
+  "\246\027\027\377\223\025\025\377\203\022\022\377\203\022\022\377\214\023\023\377\232"
+  "\026\026\377\250\030\030\377\273\032\032\377\322\036\036\377\342\071\071\377\376\367"
+  "\367\377\375\362\362\377\351jj\377\340--\377\340''\377\336\037\037\377\325"
+  "\036\036\377\343AA\377\375\366\366\377\365\272\272\377\362\251\251\377\361"
+  "\236\236\377\355\202\202\377\325\036\036\356\322\036\036\"\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\325\036\036\222\342\067\067\377\364\267\267\377\362\251\251\377"
+  "\361\244\244\377\370\325\325\377\351ll\377\271\032\032\377\264\031\031\377\262"
+  "\031\031\377\253\030\030\377\346SS\377\376\367\367\377\362\251\251\377\360\225"
+  "\225\377\375\366\366\377\313\035\035\377\177\022\022\377~\021\021\377x\021\021\377"
+  "w\020\020\377u\020\020\377u\020\020\377w\020\020\377x\021\021\377~\021\021\377\177\022"
+  "\022\377\250\030\030\377\363\253\253\377\374\357\357\377\374\357\357\377\376"
+  "\371\371\377\354yy\377\336\037\037\377\327\036\036\377\322\036\036\377\314\035\035"
+  "\377\353tt\377\371\326\326\377\360\231\231\377\360\227\227\377\360\235\235"
+  "\377\342\065\065\377\325\036\036\222\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036\036\325"
+  "\036\036\355\360\227\227\377\366\277\277\377\363\262\262\377\367\310\310\377"
+  "\370\315\315\377\303\034\034\377\276\033\033\377\275\032\032\377\266\031\031\377"
+  "\343<<\377\376\367\367\377\357\220\220\377\354{{\377\351ff\377\356\213\213"
+  "\377\360\227\227\377w\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377"
+  "u\020\020\377u\020\020\377u\020\020\377u\020\020\377w\020\020\377\253\030\030\377\371"
+  "\330\330\377\363\253\253\377\373\344\344\377\371\334\334\377\376\371\371"
+  "\377\347XX\377\322\036\036\377\314\035\035\377\303\034\034\377\302\033\033\377\370"
+  "\315\315\377\364\267\267\377\357\220\220\377\357\220\220\377\354\177\177"
+  "\377\325\036\036\355\325\036\036\036\000\000\000\000\000\000\000\000\325\036\036q\341..\377\367"
+  "\312\312\377\366\301\301\377\365\274\274\377\374\351\351\377\346OO\377\313"
+  "\035\035\377\307\034\034\377\302\033\033\377\340''\377\374\353\353\377\361\244"
+  "\244\377\353tt\377\346QQ\377\346OO\377\365\270\270\377\376\375\375\377\302"
+  "\033\033\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020"
+  "\377u\020\020\377u\020\020\377u\020\020\377\360\231\231\377\370\323\323\377\346"
+  "OO\377\357\220\220\377\366\301\301\377\370\315\315\377\374\357\357\377\341"
+  "..\377\302\033\033\377\276\033\033\377\275\032\032\377\343AA\377\372\341\341\377"
+  "\357\220\220\377\357\224\224\377\360\235\235\377\340))\377\325\036\036q\000\000"
+  "\000\000\221\024\024\003\325\036\036\305\353tt\377\366\301\301\377\365\272\272\377"
+  "\367\312\312\377\370\315\315\377\325\036\036\377\322\036\036\377\320\035\035\377"
+  "\314\035\035\377\364\263\263\377\367\307\307\377\357\220\220\377\350aa\377"
+  "\346OO\377\367\310\310\377\376\375\375\377\376\375\375\377\367\312\312\377"
+  "|\021\021\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020"
+  "\377u\020\020\377\271\032\032\377\376\375\375\377\376\375\375\377\367\310\310"
+  "\377\346OO\377\356\207\207\377\363\262\262\377\370\315\315\377\364\263\263"
+  "\377\276\033\033\377\275\032\032\377\273\032\032\377\273\032\032\377\367\310\310"
+  "\377\363\253\253\377\360\225\225\377\360\231\231\377\350ee\377\325\036\036"
+  "\305\221\024\024\003\327\036\036'\325\036\036\366\363\253\253\377\364\267\267\377"
+  "\365\270\270\377\373\343\343\377\353rr\377\327\036\036\377\327\036\036\377\327"
+  "\036\036\377\345NN\377\374\351\351\377\363\256\256\377\357\220\220\377\341"
+  "\064\064\377\365\270\270\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\343AA\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020"
+  "\020\377u\020\020\377u\020\020\377\364\267\267\377\376\375\375\377\376\375\375"
+  "\377\376\375\375\377\365\270\270\377\341\064\064\377\356\215\215\377\360\227"
+  "\227\377\373\343\343\377\343<<\377\275\032\032\377\275\032\032\377\276\033\033"
+  "\377\350ee\377\370\323\323\377\357\224\224\377\357\224\224\377\357\220\220"
+  "\377\325\036\036\366\327\036\036'\325\036\036_\341..\377\364\267\267\377\364\263"
+  "\263\377\364\263\263\377\376\371\371\377\337\040\040\377\322\036\036\377\322"
+  "\036\036\377\327\036\036\377\366\277\277\377\370\323\323\377\365\272\272\377"
+  "\351ff\377\356\207\207\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\374\353\353\377\223\025\025\377u\020\020\377"
+  "u\020\020\377u\020\020\377u\020\020\377u\020\020\377\320\035\035\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\356\207"
+  "\207\377\346SS\377\357\224\224\377\365\270\270\377\365\272\272\377\275\032"
+  "\032\377\275\032\032\377\275\032\032\377\314\035\035\377\376\367\367\377\360\231"
+  "\231\377\360\231\231\377\361\242\242\377\340++\377\325\036\036_\325\036\036\222"
+  "\344GG\377\364\267\267\377\363\262\262\377\366\277\277\377\367\307\307\377"
+  "\320\035\035\377\320\035\035\377\320\035\035\377\340--\377\376\371\371\377\365"
+  "\274\274\377\361\242\242\377\343<<\377\372\335\335\377\376\375\375\377\376"
+  "\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\354\177\177\377\241\027\027\377\346SS\377\352oo\377\340%%\377\212\023\023"
+  "\377\367\312\312\377\376\375\375\377\376\375\375\377\376\375\375\377\376"
+  "\375\375\377\376\375\375\377\372\335\335\377\343<<\377\355\202\202\377\360"
+  "\225\225\377\376\367\367\377\331\037\037\377\275\032\032\377\276\033\033\377\302"
+  "\033\033\377\366\305\305\377\364\263\263\377\361\236\236\377\363\253\253\377"
+  "\344CC\377\325\036\036\222\325\036\036\272\351ff\377\364\263\263\377\363\256"
+  "\256\377\370\323\323\377\357\220\220\377\320\035\035\377\320\035\035\377\320"
+  "\035\035\377\351ff\377\372\341\341\377\364\263\263\377\352oo\377\354yy\377"
+  "\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375"
+  "\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375"
+  "\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\354yy\377\350]]\377\360\231\231\377\371"
+  "\330\330\377\350aa\377\302\033\033\377\302\033\033\377\303\034\034\377\357\220"
+  "\220\377\367\312\312\377\361\242\242\377\362\247\247\377\351jj\377\325\036"
+  "\036\272\325\036\036\321\355\202\202\377\365\270\270\377\363\262\262\377\372"
+  "\341\341\377\352mm\377\320\035\035\377\314\035\035\377\314\035\035\377\360\227"
+  "\227\377\370\321\321\377\363\256\256\377\345JJ\377\363\256\256\377\376\375"
+  "\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375"
+  "\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375"
+  "\375\377\376\375\375\377\363\256\256\377\344EE\377\361\236\236\377\367\310"
+  "\310\377\357\224\224\377\303\034\034\377\303\034\034\377\303\034\034\377\351jj"
+  "\377\371\330\330\377\362\247\247\377\363\253\253\377\356\207\207\377\325"
+  "\036\036\321\325\036\036\343\361\242\242\377\365\270\270\377\364\263\263\377"
+  "\373\350\350\377\347WW\377\320\035\035\377\320\035\035\377\320\035\035\377\365"
+  "\272\272\377\367\307\307\377\363\262\262\377\341..\377\371\326\326\377\376"
+  "\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\376\375\375\377\376\375\375\377\376\375\375\377\363\256\256\377\244"
+  "\027\027\377u\020\020\377\276\033\033\377\370\323\323\377\376\375\375\377\376\375"
+  "\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377"
+  "\376\375\375\377\371\326\326\377\340--\377\362\247\247\377\365\274\274\377"
+  "\366\277\277\377\311\034\034\377\311\034\034\377\311\034\034\377\346QQ\377\373"
+  "\344\344\377\363\253\253\377\363\254\254\377\357\224\224\377\325\036\036\343"
+  "\325\036\036\355\364\263\263\377\366\305\305\377\366\277\277\377\374\357\357"
+  "\377\345NN\377\327\036\036\377\327\036\036\377\327\036\036\377\370\315\315\377"
+  "\367\312\312\377\367\310\310\377\345JJ\377\375\364\364\377\376\375\375\377"
+  "\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375"
+  "\375\377\376\375\375\377\376\367\367\377\223\025\025\377u\020\020\377u\020\020"
+  "\377u\020\020\377\275\032\032\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\375"
+  "\364\364\377\344GG\377\365\274\274\377\365\274\274\377\367\312\312\377\313"
+  "\035\035\377\313\035\035\377\313\035\035\377\344CC\377\374\351\351\377\363\254"
+  "\254\377\363\256\256\377\361\244\244\377\325\036\036\355\325\036\036\355\365"
+  "\270\270\377\367\312\312\377\367\310\310\377\375\360\360\377\346QQ\377\336"
+  "\037\037\377\336\037\037\377\336\037\037\377\363\254\254\377\370\321\321\377\370"
+  "\321\321\377\365\272\272\377\364\267\267\377\364\267\267\377\364\267\267"
+  "\377\364\267\267\377\364\267\267\377\364\267\267\377\372\341\341\377\376"
+  "\375\375\377\367\307\307\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377"
+  "\201\022\022\377\376\375\375\377\376\375\375\377\370\323\323\377\364\267\267"
+  "\377\364\267\267\377\364\267\267\377\364\267\267\377\364\267\267\377\365"
+  "\272\272\377\370\315\315\377\370\315\315\377\361\244\244\377\311\034\034\377"
+  "\311\034\034\377\311\034\034\377\343AA\377\373\350\350\377\363\253\253\377\363"
+  "\254\254\377\361\244\244\377\325\036\036\355\325\036\036\343\365\270\270\377"
+  "\370\323\323\377\370\315\315\377\374\357\357\377\350aa\377\337\040\040\377"
+  "\337$$\377\337$$\377\337$$\377\337$$\377\337$$\377\221\024\024\377w\020\020\377"
+  "u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377\346OO\377\376\375"
+  "\375\377\375\360\360\377\210\023\023\377u\020\020\377u\020\020\377u\020\020\377\253"
+  "\030\030\377\376\375\375\377\376\375\375\377\320\035\035\377u\020\020\377u\020\020"
+  "\377u\020\020\377u\020\020\377w\020\020\377\214\023\023\377\311\034\034\377\311\034"
+  "\034\377\311\034\034\377\311\034\034\377\311\034\034\377\311\034\034\377\346QQ\377"
+  "\373\344\344\377\363\253\253\377\363\253\253\377\360\235\235\377\325\036\036"
+  "\343\325\036\036\321\361\242\242\377\371\330\330\377\370\325\325\377\374\353"
+  "\353\377\354yy\377\337$$\377\340''\377\340''\377\340''\377\340''\377\340"
+  "%%\377\241\027\027\377x\021\021\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377"
+  "u\020\020\377\331\037\037\377\376\375\375\377\376\375\375\377\360\225\225\377"
+  "\210\023\023\377u\020\020\377\233\026\026\377\366\301\301\377\376\375\375\377\376"
+  "\375\375\377\233\026\026\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377x\021"
+  "\021\377\232\026\026\377\313\035\035\377\313\035\035\377\311\034\034\377\311\034\034"
+  "\377\311\034\034\377\311\034\034\377\351jj\377\371\334\334\377\363\253\253\377"
+  "\363\256\256\377\356\207\207\377\325\036\036\321\325\036\036\272\354\177\177"
+  "\377\371\334\334\377\371\330\330\377\373\346\346\377\360\235\235\377\340"
+  "%%\377\340''\377\340%%\377\340%%\377\337\040\040\377\337\040\040\377\262\031\031"
+  "\377~\021\021\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377~\021"
+  "\021\377\367\312\312\377\376\375\375\377\376\375\375\377\376\375\375\377\373"
+  "\344\344\377\376\375\375\377\376\375\375\377\376\375\375\377\360\231\231"
+  "\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377~\021\021\377\251"
+  "\030\030\377\311\034\034\377\311\034\034\377\311\034\034\377\311\034\034\377\311\034"
+  "\034\377\313\035\035\377\357\220\220\377\370\321\321\377\363\254\254\377\363"
+  "\262\262\377\352mm\377\325\036\036\272\325\036\036\222\347XX\377\371\334\334"
+  "\377\371\330\330\377\371\334\334\377\367\312\312\377\337$$\377\337\040\040"
+  "\377\337\040\040\377\336\037\037\377\332\037\037\377\331\037\037\377\303\034\034\377"
+  "\177\022\022\377w\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020"
+  "\020\377\235\026\026\377\367\307\307\377\376\375\375\377\376\375\375\377\376"
+  "\375\375\377\376\375\375\377\376\375\375\377\361\236\236\377\212\023\023\377"
+  "u\020\020\377u\020\020\377u\020\020\377u\020\020\377w\020\020\377\177\022\022\377\271"
+  "\032\032\377\307\034\034\377\307\034\034\377\311\034\034\377\311\034\034\377\311\034"
+  "\034\377\311\034\034\377\366\305\305\377\366\277\277\377\363\256\256\377\363"
+  "\262\262\377\345JJ\377\325\036\036\222\325\036\036_\342\065\065\377\371\330\330"
+  "\377\370\325\325\377\370\321\321\377\376\371\371\377\341..\377\336\037\037"
+  "\377\331\037\037\377\327\036\036\377\327\036\036\377\320\035\035\377\313\035\035\377"
+  "\241\027\027\377|\021\021\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020"
+  "\020\377u\020\020\377\351ll\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\376\375\375\377\376\375\375\377\340%%\377u\020\020\377u\020\020\377u\020"
+  "\020\377u\020\020\377u\020\020\377|\021\021\377\241\027\027\377\302\033\033\377\302"
+  "\033\033\377\303\034\034\377\311\034\034\377\311\034\034\377\311\034\034\377\332\037"
+  "\037\377\376\371\371\377\363\254\254\377\363\254\254\377\363\262\262\377\340"
+  "--\377\325\036\036_\327\036\036'\325\036\036\366\366\277\277\377\367\312\312\377"
+  "\367\307\307\377\373\346\346\377\353rr\377\327\036\036\377\322\036\036\377\320"
+  "\035\035\377\313\035\035\377\302\033\033\377\276\033\033\377\266\031\031\377\177\022"
+  "\022\377w\020\020\377u\020\020\377u\020\020\377u\020\020\377u\020\020\377\253\030\030"
+  "\377\376\367\367\377\376\375\375\377\376\375\375\377\376\375\375\377\376"
+  "\375\375\377\376\375\375\377\373\344\344\377\210\023\023\377u\020\020\377u\020"
+  "\020\377u\020\020\377w\020\020\377\177\022\022\377\262\031\031\377\275\032\032\377\276"
+  "\033\033\377\302\033\033\377\302\033\033\377\303\034\034\377\303\034\034\377\351ff"
+  "\377\371\334\334\377\362\251\251\377\363\253\253\377\362\247\247\377\325"
+  "\036\036\366\327\036\036'\221\024\024\003\325\036\036\305\354}}\377\367\310\310\377"
+  "\366\301\301\377\370\315\315\377\367\312\312\377\320\035\035\377\311\034\034"
+  "\377\303\034\034\377\302\033\033\377\275\032\032\377\275\032\032\377\276\033\033\377"
+  "\253\030\030\377~\021\021\377w\020\020\377u\020\020\377u\020\020\377w\020\020\377\365"
+  "\272\272\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\376\375\375\377\376\375\375\377\376\375\375\377\354\177\177\377u\020"
+  "\020\377u\020\020\377w\020\020\377~\021\021\377\253\030\030\377\271\032\032\377\271"
+  "\032\032\377\273\032\032\377\275\032\032\377\276\033\033\377\302\033\033\377\303\034"
+  "\034\377\367\310\310\377\364\267\267\377\361\244\244\377\363\253\253\377\352"
+  "oo\377\325\036\036\305\221\024\024\003\000\000\000\000\325\036\036q\340--\377\367\307\307"
+  "\377\365\270\270\377\363\262\262\377\373\346\346\377\344GG\377\302\033\033"
+  "\377\275\032\032\377\275\032\032\377\275\032\032\377\275\032\032\377\275\032\032\377"
+  "\275\032\032\377\250\030\030\377~\021\021\377w\020\020\377u\020\020\377\343AA\377\376"
+  "\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\271"
+  "\032\032\377w\020\020\377~\021\021\377\250\030\030\377\275\032\032\377\275\032\032\377"
+  "\273\032\032\377\273\032\032\377\271\032\032\377\275\032\032\377\275\032\032\377\343"
+  "AA\377\373\344\344\377\361\236\236\377\361\244\244\377\363\254\254\377\340"
+  "++\377\325\036\036q\000\000\000\000\000\000\000\000\325\036\036\036\325\036\036\355\356\215\215"
+  "\377\363\262\262\377\362\247\247\377\365\272\272\377\370\315\315\377\276"
+  "\033\033\377\275\032\032\377\275\032\032\377\275\032\032\377\275\032\032\377\275\032"
+  "\032\377\273\032\032\377\275\032\032\377\257\030\030\377\177\022\022\377\237\026\026"
+  "\377\374\353\353\377\376\375\375\377\376\375\375\377\376\375\375\377\376"
+  "\375\375\377\376\375\375\377\376\375\375\377\376\375\375\377\376\375\375"
+  "\377\373\346\346\377\370\315\315\377\206\023\023\377\260\031\031\377\276\033\033"
+  "\377\275\032\032\377\276\033\033\377\275\032\032\377\273\032\032\377\271\032\032\377"
+  "\271\032\032\377\275\032\032\377\370\315\315\377\365\270\270\377\360\227\227"
+  "\377\361\242\242\377\356\213\213\377\325\036\036\355\325\036\036\036\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\325\036\036\222\340--\377\362\247\247\377\360\227\227\377\360"
+  "\225\225\377\370\323\323\377\352mm\377\275\032\032\377\275\032\032\377\275\032"
+  "\032\377\273\032\032\377\273\032\032\377\275\032\032\377\302\033\033\377\303\034\034"
+  "\377\276\033\033\377\363\254\254\377\357\224\224\377\354yy\377\363\256\256"
+  "\377\371\326\326\377\375\362\362\377\375\362\362\377\371\326\326\377\363"
+  "\256\256\377\354yy\377\343<<\377\365\270\270\377\355\202\202\377\311\034\034"
+  "\377\303\034\034\377\276\033\033\377\275\032\032\377\275\032\032\377\275\032\032\377"
+  "\275\032\032\377\273\032\032\377\351jj\377\370\315\315\377\356\215\215\377\357"
+  "\224\224\377\361\236\236\377\343<<\377\325\036\036\222\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\322\036\036\"\325\036\036\356\353tt\377\360\227\227\377\360\225"
+  "\225\377\362\251\251\377\375\366\366\377\340--\377\275\032\032\377\273\032\032"
+  "\377\273\032\032\377\275\032\032\377\302\033\033\377\303\034\034\377\311\034\034\377"
+  "\350aa\377\373\346\346\377\361\242\242\377\353tt\377\345NN\377\341\062\062"
+  "\377\336\037\037\377\336\037\037\377\341\062\062\377\345JJ\377\352oo\377\360\231"
+  "\231\377\363\262\262\377\375\366\366\377\341\062\062\377\311\034\034\377\302"
+  "\033\033\377\275\032\032\377\275\032\032\377\275\032\032\377\275\032\032\377\340--"
+  "\377\375\366\366\377\361\236\236\377\356\213\213\377\357\220\220\377\354"
+  "}}\377\325\036\036\356\322\036\036\"\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\325\036\036|\331\037\037\377\360\227\227\377\360\231\231\377\360\227\227\377"
+  "\366\277\277\377\372\341\341\377\327\036\036\377\273\032\032\377\275\032\032\377"
+  "\276\033\033\377\302\033\033\377\307\034\034\377\332\037\037\377\373\350\350\377"
+  "\370\323\323\377\366\277\277\377\365\270\270\377\365\270\270\377\366\277"
+  "\277\377\367\310\310\377\367\312\312\377\366\277\277\377\364\267\267\377"
+  "\364\263\263\377\364\263\263\377\367\312\312\377\373\346\346\377\356\207"
+  "\207\377\311\034\034\377\303\034\034\377\302\033\033\377\275\032\032\377\275\032\032"
+  "\377\327\036\036\377\373\343\343\377\366\277\277\377\357\220\220\377\357\220"
+  "\220\377\357\224\224\377\340''\377\325\036\036|\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\331\037\037\013\325\036\036\310\342\067\067\377\361\236\236"
+  "\377\357\224\224\377\357\220\220\377\366\305\305\377\371\330\330\377\336"
+  "\037\037\377\276\033\033\377\302\033\033\377\303\034\034\377\311\034\034\377\320\035"
+  "\035\377\346QQ\377\366\277\277\377\376\367\367\377\373\344\344\377\370\325"
+  "\325\377\371\330\330\377\370\325\325\377\371\326\326\377\371\330\330\377"
+  "\370\325\325\377\373\343\343\377\375\366\366\377\365\274\274\377\345JJ\377"
+  "\303\034\034\377\307\034\034\377\311\034\034\377\307\034\034\377\302\033\033\377\336"
+  "\037\037\377\371\330\330\377\367\310\310\377\360\225\225\377\357\224\224\377"
+  "\360\225\225\377\345JJ\377\325\036\036\310\331\037\037\013\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\327\036\036\061\325\036\036\355\347XX\377"
+  "\360\225\225\377\357\220\220\377\357\220\220\377\366\301\301\377\374\351"
+  "\351\377\343AA\377\302\033\033\377\311\034\034\377\314\035\035\377\322\036\036\377"
+  "\327\036\036\377\327\036\036\377\337$$\377\350ee\377\361\236\236\377\363\253"
+  "\253\377\370\321\321\377\370\321\321\377\363\253\253\377\361\236\236\377"
+  "\350ee\377\336\037\037\377\320\035\035\377\314\035\035\377\311\034\034\377\307\034"
+  "\034\377\311\034\034\377\311\034\034\377\343AA\377\374\351\351\377\366\301\301"
+  "\377\360\225\225\377\360\225\225\377\360\231\231\377\352mm\377\325\036\036"
+  "\355\327\036\036\061\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\325\036\036Z\325\036\036\374\352mm\377\360\227\227\377\357\224"
+  "\224\377\360\227\227\377\366\277\277\377\376\371\371\377\354yy\377\313\035"
+  "\035\377\320\035\035\377\322\036\036\377\325\036\036\377\325\036\036\377\322\036\036"
+  "\377\320\035\035\377\325\036\036\377\331\037\037\377\336\037\037\377\336\037\037\377"
+  "\331\037\037\377\322\036\036\377\320\035\035\377\320\035\035\377\320\035\035\377\320"
+  "\035\035\377\313\035\035\377\307\034\034\377\307\034\034\377\354yy\377\376\371\371"
+  "\377\366\277\277\377\360\231\231\377\360\225\225\377\360\231\231\377\354"
+  "\177\177\377\325\036\036\374\325\036\036Z\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036q\322\036\036"
+  "\377\353rr\377\360\235\235\377\361\236\236\377\361\236\236\377\363\262\262"
+  "\377\371\334\334\377\370\321\321\377\345JJ\377\327\036\036\377\327\036\036\377"
+  "\322\036\036\377\322\036\036\377\320\035\035\377\327\036\036\377\331\037\037\377\336"
+  "\037\037\377\336\037\037\377\332\037\037\377\325\036\036\377\320\035\035\377\320\035"
+  "\035\377\320\035\035\377\320\035\035\377\313\035\035\377\344CC\377\370\315\315\377"
+  "\371\330\330\377\363\262\262\377\361\244\244\377\362\247\247\377\361\236"
+  "\236\377\355\202\202\377\336\037\037\377\325\036\036q\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\325\036\036q\325\036\036\374\351ff\377\361\236\236\377\362\251\251\377"
+  "\362\251\251\377\363\256\256\377\370\315\315\377\374\353\353\377\367\312"
+  "\312\377\354}}\377\341..\377\320\035\035\377\322\036\036\377\327\036\036\377\332"
+  "\037\037\377\336\037\037\377\336\037\037\377\332\037\037\377\327\036\036\377\322\036"
+  "\036\377\320\035\035\377\340++\377\354yy\377\367\312\312\377\373\350\350\377"
+  "\366\305\305\377\362\247\247\377\363\253\253\377\363\253\253\377\362\251"
+  "\251\377\353tt\377\325\036\036\374\325\036\036q\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\325\036\036Z\325\036\036\356\345JJ\377\363\254\254\377\364"
+  "\263\263\377\364\263\263\377\365\270\270\377\365\274\274\377\370\315\315"
+  "\377\372\341\341\377\375\364\364\377\370\323\323\377\361\236\236\377\354"
+  "{{\377\353tt\377\346QQ\377\346QQ\377\353tt\377\354{{\377\361\236\236\377"
+  "\370\323\323\377\375\364\364\377\371\334\334\377\366\305\305\377\363\254"
+  "\254\377\362\251\251\377\362\247\247\377\363\262\262\377\363\256\256\377"
+  "\346SS\377\325\036\036\356\325\036\036Z\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036\064\325\036\036\305\340))\377\357\220\220"
+  "\377\365\274\274\377\366\277\277\377\366\277\277\377\365\274\274\377\365"
+  "\272\272\377\364\263\263\377\366\301\301\377\370\325\325\377\373\343\343"
+  "\377\373\344\344\377\375\360\360\377\375\360\360\377\373\346\346\377\373"
+  "\343\343\377\370\325\325\377\366\301\301\377\364\267\267\377\364\267\267"
+  "\377\363\262\262\377\364\263\263\377\363\262\262\377\363\254\254\377\357"
+  "\220\220\377\341..\377\325\036\036\305\325\036\036\064\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\311\034\034\014\325"
+  "\036\036|\325\036\036\354\343<<\377\361\236\236\377\367\307\307\377\366\277\277"
+  "\377\365\272\272\377\364\267\267\377\364\267\267\377\365\272\272\377\366"
+  "\277\277\377\366\301\301\377\367\312\312\377\370\315\315\377\367\310\310"
+  "\377\366\277\277\377\365\270\270\377\364\267\267\377\364\263\263\377\364"
+  "\263\263\377\365\272\272\377\364\267\267\377\361\236\236\377\344EE\377\325"
+  "\036\036\354\325\036\036|\311\034\034\014\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\334\037"
+  "\037&\325\036\036\222\325\036\036\354\340))\377\353tt\377\363\256\256\377\366"
+  "\277\277\377\366\301\301\377\366\277\277\377\366\305\305\377\367\307\307"
+  "\377\370\315\315\377\370\321\321\377\370\315\315\377\367\307\307\377\366"
+  "\277\277\377\366\277\277\377\365\270\270\377\363\254\254\377\354{{\377\341"
+  "\064\064\377\325\036\036\354\325\036\036\222\334\037\037&\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\325\036\036\036\325\036\036s\325\036\036"
+  "\304\325\036\036\365\340++\377\345JJ\377\352mm\377\357\220\220\377\363\254"
+  "\254\377\364\267\267\377\364\267\267\377\363\256\256\377\357\220\220\377"
+  "\353rr\377\346OO\377\340--\377\325\036\036\365\325\036\036\304\325\036\036s\325"
+  "\036\036\036\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\037\004\004\001\325\036\036(\325\036\036_\325\036\036"
+  "\222\325\036\036\270\325\036\036\321\325\036\036\343\325\036\036\355\325\036\036\355"
+  "\325\036\036\343\325\036\036\321\325\036\036\270\325\036\036\222\325\036\036_\325\036"
+  "\036(\037\004\004\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+  "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
+#if 0
+};
+#endif
diff --git a/source/blood/rsrc/game.bmp b/source/blood/rsrc/game.bmp
new file mode 100644
index 000000000..fe974c0c8
Binary files /dev/null and b/source/blood/rsrc/game.bmp differ
diff --git a/source/blood/rsrc/game_icon.c b/source/blood/rsrc/game_icon.c
new file mode 100644
index 000000000..a9b6fe43b
--- /dev/null
+++ b/source/blood/rsrc/game_icon.c
@@ -0,0 +1 @@
+#include "eduke32_icon.c"
diff --git a/source/blood/rsrc/game_icon.ico b/source/blood/rsrc/game_icon.ico
new file mode 100644
index 000000000..e38c0303f
Binary files /dev/null and b/source/blood/rsrc/game_icon.ico differ
diff --git a/source/blood/rsrc/gameres.rc b/source/blood/rsrc/gameres.rc
new file mode 100644
index 000000000..abe958b9f
--- /dev/null
+++ b/source/blood/rsrc/gameres.rc
@@ -0,0 +1,80 @@
+#define NEED_COMMCTRL_H
+#include "../../build/include/windows_inc.h"
+#include "../src/startwin.game.h"
+
+RSRC_ICON ICON "game_icon.ico"
+RSRC_BMP BITMAP "game.bmp"
+
+WIN_STARTWIN DIALOGEX DISCARDABLE  20, 40, 260, 200
+STYLE DS_MODALFRAME | DS_CENTER | DS_SETFONT | DS_FIXEDSYS | WS_OVERLAPPED | WS_CAPTION | WS_VISIBLE | WS_SYSMENU
+CAPTION "Startup"
+FONT 8, "MS Shell Dlg"
+BEGIN
+    CONTROL "", WIN_STARTWIN_BITMAP, "STATIC", SS_BITMAP | SS_CENTERIMAGE | WS_CHILD | WS_VISIBLE, 0, 0, 66, 172
+    CONTROL "", WIN_STARTWIN_TABCTL, WC_TABCONTROL, WS_CLIPSIBLINGS | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 5, 5, 250, 170
+    CONTROL "&Start", WIN_STARTWIN_START, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 180, 48, 14
+    CONTROL "&Cancel", WIN_STARTWIN_CANCEL, "BUTTON", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 207, 180, 48, 14
+
+    CONTROL "", WIN_STARTWIN_MESSAGES, "EDIT", ES_MULTILINE | ES_READONLY | WS_CHILD | WS_VSCROLL, 0, 0, 32, 32
+END
+WIN_STARTWINPAGE_CONFIG DIALOGEX DISCARDABLE  20, 40, 279, 168
+STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD
+CAPTION "Dialog"
+FONT 8, "MS Shell Dlg"
+BEGIN
+    CONTROL "&Video mode:",   -1, "STATIC", SS_LEFT | WS_CHILD | WS_VISIBLE, 5, 6, 50, 8
+    CONTROL "",               IDCVMODE, "COMBOBOX", CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 60, 4, 86, 56
+    CONTROL "&Fullscreen",    IDCFULLSCREEN, "BUTTON", BS_CHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 154, 6, 46, 10
+//#if defined POLYMER && POLYMER != 0
+//    CONTROL "&Polymer",       IDCPOLYMER, "BUTTON", BS_CHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 203, 6, 40, 10
+//#endif
+    CONTROL "Input devices:", -1, "STATIC", SS_LEFT | WS_CHILD | WS_VISIBLE, 5, 20, 50, 8
+    CONTROL "",               IDCINPUT, "COMBOBOX", CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 60, 19, 86, 56
+    CONTROL "&Game:", 	  -1, "STATIC", SS_LEFT | WS_CHILD | WS_VISIBLE, 5, 35, 100, 8
+    CONTROL "",               IDCDATA, "LISTBOX", LBS_NOINTEGRALHEIGHT | LBS_USETABSTOPS | LBS_STANDARD | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 10, 45, 226, 43
+
+    CONTROL "Custom game content &directory:", -1, "STATIC", SS_LEFT | WS_CHILD | WS_VISIBLE, 5, 90, 160, 8
+    CONTROL "",               IDCGAMEDIR, "COMBOBOX", CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP, 10, 99, 226, 156
+    CONTROL "&Enable ""autoload"" folder", IDCAUTOLOAD, "BUTTON", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 5, 116, 100, 8
+    CONTROL "&Always show this window at startup", IDCALWAYSSHOW, "BUTTON", BS_AUTOCHECKBOX | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 118, 116, 140, 8
+END
+
+#define FILEVER        1,9,9,9
+#define PRODUCTVER     1,9,9,9
+#define STRFILEVER     "2.0.0devel\0"
+#define STRPRODUCTVER  "2.0.0devel\0"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION FILEVER
+ PRODUCTVERSION PRODUCTVER
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x3L
+#else
+ FILEFLAGS 0x2L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+            VALUE "FileDescription", "NBlood"
+            VALUE "FileVersion", STRFILEVER
+            VALUE "InternalName", "NBlood"
+            VALUE "LegalCopyright", "Copyright � 2018 EDuke32 Developers, 1996, 2003 3D Realms Entertainment"
+            VALUE "LegalTrademarks", "Duke Nukem� is a Registered Trademark of Gearbox Software, LLC."
+            VALUE "OriginalFilename", "nblood.exe"
+            VALUE "ProductName", "NBlood"
+            VALUE "ProductVersion", STRPRODUCTVER
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END
+
+1 24 "manifest.game.xml"
diff --git a/source/blood/rsrc/manifest.build.xml b/source/blood/rsrc/manifest.build.xml
new file mode 100644
index 000000000..9abe41969
--- /dev/null
+++ b/source/blood/rsrc/manifest.build.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+  <asmv3:application>
+    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+      <dpiAware>true</dpiAware>
+      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+    </asmv3:windowsSettings>
+  </asmv3:application>
+  <assemblyIdentity
+      version="1.0.0.0"
+      processorArchitecture="*"
+      name="EDuke32.Mapster32"
+      type="win32"
+/>
+  <description>Mapster32 for EDuke32</description>
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+          type="win32"
+          name="Microsoft.Windows.Common-Controls"
+          version="6.0.0.0"
+          processorArchitecture="*"
+          publicKeyToken="6595b64144ccf1df"
+          language="*"
+        />
+    </dependentAssembly>
+  </dependency>
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- Windows Vista -->
+      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+    </application>
+  </compatibility>
+</assembly>
diff --git a/source/blood/rsrc/manifest.game.xml b/source/blood/rsrc/manifest.game.xml
new file mode 100644
index 000000000..d447255e2
--- /dev/null
+++ b/source/blood/rsrc/manifest.game.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+  <asmv3:application>
+    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+      <dpiAware>true</dpiAware>
+      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
+    </asmv3:windowsSettings>
+  </asmv3:application>
+  <assemblyIdentity
+      version="1.0.0.0"
+      processorArchitecture="*"
+      name="NBlood"
+      type="win32"
+/>
+  <description>NBlood</description>
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+          type="win32"
+          name="Microsoft.Windows.Common-Controls"
+          version="6.0.0.0"
+          processorArchitecture="*"
+          publicKeyToken="6595b64144ccf1df"
+          language="*"
+        />
+    </dependentAssembly>
+  </dependency>
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- Windows Vista -->
+      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+    </application>
+  </compatibility>
+</assembly>
diff --git a/source/blood/rsrc/source/EDuke32_logo_21_large.psd b/source/blood/rsrc/source/EDuke32_logo_21_large.psd
new file mode 100644
index 000000000..5aeecef60
Binary files /dev/null and b/source/blood/rsrc/source/EDuke32_logo_21_large.psd differ
diff --git a/source/blood/rsrc/source/EDuke32_logo_21_large_blue.psd b/source/blood/rsrc/source/EDuke32_logo_21_large_blue.psd
new file mode 100644
index 000000000..ff32e9335
Binary files /dev/null and b/source/blood/rsrc/source/EDuke32_logo_21_large_blue.psd differ
diff --git a/source/blood/rsrc/source/EDuke32_logo_21_large_opaque.psd b/source/blood/rsrc/source/EDuke32_logo_21_large_opaque.psd
new file mode 100644
index 000000000..be7b6cbc1
Binary files /dev/null and b/source/blood/rsrc/source/EDuke32_logo_21_large_opaque.psd differ
diff --git a/source/blood/rsrc/source/game2.psd b/source/blood/rsrc/source/game2.psd
new file mode 100644
index 000000000..3b7c1fbf2
Binary files /dev/null and b/source/blood/rsrc/source/game2.psd differ
diff --git a/source/blood/rsrc/source/game3.psd b/source/blood/rsrc/source/game3.psd
new file mode 100644
index 000000000..dc6eaefe9
Binary files /dev/null and b/source/blood/rsrc/source/game3.psd differ
diff --git a/source/blood/rsrc/source/wii-hbc-icon.xcf b/source/blood/rsrc/source/wii-hbc-icon.xcf
new file mode 100644
index 000000000..7dc3df63b
Binary files /dev/null and b/source/blood/rsrc/source/wii-hbc-icon.xcf differ
diff --git a/source/blood/src/_functio.h b/source/blood/src/_functio.h
new file mode 100644
index 000000000..1bc7d564f
--- /dev/null
+++ b/source/blood/src/_functio.h
@@ -0,0 +1,376 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+// _functio.h
+
+// file created by makehead.exe
+// these headers contain default key assignments, as well as
+// default button assignments and game function names
+// axis defaults are also included
+
+#include "_control.h"
+#include "control.h"
+
+#ifndef function_private_h_
+#define function_private_h_
+#ifdef __cplusplus
+extern "C" {
+#endif
+// KEEPINSYNC lunatic/con_lang.lua
+char gamefunctions[NUMGAMEFUNCTIONS][MAXGAMEFUNCLEN] =
+   {
+   "Move_Forward",
+   "Move_Backward",
+   "Turn_Left",
+   "Turn_Right",
+   "Turn_Around",
+   "Strafe",
+   "Strafe_Left",
+   "Strafe_Right",
+   "Jump",
+   "Crouch",
+   "Run",
+   "AutoRun",
+   "Open",
+   "Weapon_Fire",
+   "Weapon_Special_Fire",
+   "Aim_Up",
+   "Aim_Down",
+   "Aim_Center",
+   "Look_Up",
+   "Look_Down",
+   "Tilt_Left",
+   "Tilt_Right",
+   "Weapon_1",
+   "Weapon_2",
+   "Weapon_3",
+   "Weapon_4",
+   "Weapon_5",
+   "Weapon_6",
+   "Weapon_7",
+   "Weapon_8",
+   "Weapon_9",
+   "Weapon_10",
+   "Inventory_Use",
+   "Inventory_Left",
+   "Inventory_Right",
+   "Map_Toggle",
+   "Map_Follow_Mode",
+   "Shrink_Screen",
+   "Enlarge_Screen",
+   "Send_Message",
+   "See_Coop_View",
+   "See_Chase_View",
+   "Mouse_Aiming",
+   "Toggle_Crosshair",
+   "Next_Weapon",
+   "Previous_Weapon",
+   "Holster_Weapon",
+   "Show_Opponents_Weapon",
+   "BeastVision",
+   "CrystalBall",
+   "JumpBoots",
+   "MedKit",
+   "ProximityBombs",
+   "RemoteBombs",
+   "Show_Console",
+   };
+
+#ifdef __SETUP__
+
+const char keydefaults[NUMGAMEFUNCTIONS*2][MAXGAMEFUNCLEN] =
+   {
+   "W", "Kpad8",
+   "S", "Kpad2",
+   "Left", "Kpad4",
+   "Right", "KPad6",
+   "BakSpc", "",
+   "LAlt", "RAlt",
+   "A", "",
+   "D", "",
+   "Space", "/",
+   "LCtrl", "",
+   "LShift", "RShift",
+   "CapLck", "",
+   "E", "",
+   "RCtrl", "",
+   "X", "",
+   "Home", "KPad7",
+   "End", "Kpad1",
+   "KPad5", "",
+   "PgUp", "Kpad9",
+   "PgDn", "Kpad3",
+   "Insert", "Kpad0",
+   "Delete", "Kpad.",
+   "1", "",
+   "2", "",
+   "3", "",
+   "4", "",
+   "5", "",
+   "6", "",
+   "7", "",
+   "8", "",
+   "9", "",
+   "0", "",
+   "Enter", "KpdEnt",
+   "[", "",
+   "]", "",
+   "Tab", "",
+   "F", "",
+   "-", "Kpad-",
+   "=", "Kpad+",
+   "T", "",
+   "K", "",
+   "F7", "",
+   "U", "",
+   "I", "",
+   "'", "",
+   ";", "",
+   "ScrLck", "",
+   "Y", "",
+   "B", "",
+   "C", "",
+   "J", "",
+   "M", "",
+   "P", "",
+   "R", "",
+   "`", "",
+   };
+
+const char oldkeydefaults[NUMGAMEFUNCTIONS*2][MAXGAMEFUNCLEN] =
+   {
+   "Up", "Kpad8",
+   "Down", "Kpad2",
+   "Left", "Kpad4",
+   "Right", "KPad6",
+   "BakSpc", "",
+   "LAlt", "RAlt",
+   ",", "",
+   ".", "",
+   "A", "/",
+   "Z", "",
+   "LShift", "RShift",
+   "CapLck", "",
+   "Space", "",
+   "LCtrl", "RCtrl",
+   "X", "",
+   "Home", "KPad7",
+   "End", "Kpad1",
+   "KPad5", "",
+   "PgUp", "Kpad9",
+   "PgDn", "Kpad3",
+   "Insert", "Kpad0",
+   "Delete", "Kpad.",
+   "1", "",
+   "2", "",
+   "3", "",
+   "4", "",
+   "5", "",
+   "6", "",
+   "7", "",
+   "8", "",
+   "9", "",
+   "0", "",
+   "Enter", "KpdEnt",
+   "[", "",
+   "]", "",
+   "Tab", "",
+   "F", "",
+   "-", "Kpad-",
+   "=", "Kpad+",
+   "T", "",
+   "K", "",
+   "F7", "",
+   "U", "",
+   "I", "",
+   "'", "",
+   ";", "",
+   "ScrLck", "",
+   "W", "",
+   "B", "",
+   "C", "",
+   "J", "",
+   "M", "",
+   "P", "",
+   "R", "",
+   "`", "",
+   };
+
+static const char * mousedefaults[MAXMOUSEBUTTONS] =
+   {
+   "Weapon_Fire",
+   "Weapon_Special_Fire",
+   "",
+   "",
+   "Previous_Weapon",
+   "Next_Weapon",
+   };
+
+
+static const char * mouseclickeddefaults[MAXMOUSEBUTTONS] =
+   {
+   };
+
+
+static const char * mouseanalogdefaults[MAXMOUSEAXES] =
+   {
+   "analog_turning",
+   "analog_moving",
+   };
+
+
+static const char * mousedigitaldefaults[MAXMOUSEDIGITAL] =
+   {
+   };
+
+#if defined(GEKKO)
+static const char * joystickdefaults[MAXJOYBUTTONSANDHATS] =
+   {
+   "Open", // A
+   "Fire", // B
+   "Run", // 1
+   "Map", // 2
+   "Previous_Weapon", // -
+   "Next_Weapon", // +
+   "", // Home
+   "Jump", // Z
+   "Crouch", // C
+   "Map", // X
+   "Run", // Y
+   "Jump", // L
+   "Quick_Kick", // R
+   "Crouch", // ZL
+   "Fire", // ZR
+   "Quick_Kick", // D-Pad Up
+   "Inventory_Right", // D-Pad Right
+   "Inventory", // D-Pad Down
+   "Inventory_Left", // D-Pad Left
+   };
+
+
+static const char * joystickclickeddefaults[MAXJOYBUTTONSANDHATS] =
+   {
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "Inventory",
+   };
+
+
+static const char * joystickanalogdefaults[MAXJOYAXES] =
+   {
+   "analog_strafing",
+   "analog_moving",
+   "analog_turning",
+   "analog_lookingupanddown",
+   };
+
+
+static const char * joystickdigitaldefaults[MAXJOYDIGITAL] =
+   {
+   };
+#else
+static const char * joystickdefaults[MAXJOYBUTTONSANDHATS] =
+   {
+   "Fire",
+   "Strafe",
+   "Run",
+   "Open",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "Aim_Down",
+   "Look_Right",
+   "Aim_Up",
+   "Look_Left",
+   };
+
+
+static const char * joystickclickeddefaults[MAXJOYBUTTONSANDHATS] =
+   {
+   "",
+   "Inventory",
+   "Jump",
+   "Crouch",
+   };
+
+
+static const char * joystickanalogdefaults[MAXJOYAXES] =
+   {
+   "analog_turning",
+   "analog_moving",
+   "analog_strafing",
+   };
+
+
+static const char * joystickdigitaldefaults[MAXJOYDIGITAL] =
+   {
+   "",
+   "",
+   "",
+   "",
+   "",
+   "",
+   "Run",
+   };
+#endif
+
+#endif
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/source/blood/src/_midi.h b/source/blood/src/_midi.h
new file mode 100644
index 000000000..f522fafca
--- /dev/null
+++ b/source/blood/src/_midi.h
@@ -0,0 +1,149 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#ifndef ___MIDI_H
+#define ___MIDI_H
+#include "compat.h"
+
+#define RELATIVE_BEAT( measure, beat, tick ) \
+   ( ( tick ) + ( ( beat ) << 9 ) + ( ( measure ) << 16 ) )
+
+//Bobby Prince thinks this may be 100
+//#define GENMIDI_DefaultVolume 100
+#define GENMIDI_DefaultVolume 90
+
+#define MAX_FORMAT            1
+
+#define NUM_MIDI_CHANNELS     16
+
+#define TIME_PRECISION        16
+
+#define MIDI_HEADER_SIGNATURE 0x6468544d    // "MThd"
+#define MIDI_TRACK_SIGNATURE  0x6b72544d    // "MTrk"
+
+#define MIDI_VOLUME                7
+#define MIDI_PAN                   10
+#define MIDI_DETUNE                94
+#define MIDI_RHYTHM_CHANNEL        9
+#define MIDI_RPN_MSB               100
+#define MIDI_RPN_LSB               101
+#define MIDI_DATAENTRY_MSB         6
+#define MIDI_DATAENTRY_LSB         38
+#define MIDI_PITCHBEND_MSB         0
+#define MIDI_PITCHBEND_LSB         0
+#define MIDI_RUNNING_STATUS        0x80
+#define MIDI_NOTE_OFF              0x8
+#define MIDI_NOTE_ON               0x9
+#define MIDI_POLY_AFTER_TCH        0xA
+#define MIDI_CONTROL_CHANGE        0xB
+#define MIDI_PROGRAM_CHANGE        0xC
+#define MIDI_AFTER_TOUCH           0xD
+#define MIDI_PITCH_BEND            0xE
+#define MIDI_SPECIAL               0xF
+#define MIDI_SYSEX                 0xF0
+#define MIDI_SYSEX_CONTINUE        0xF7
+#define MIDI_META_EVENT            0xFF
+#define MIDI_END_OF_TRACK          0x2F
+#define MIDI_TEMPO_CHANGE          0x51
+#define MIDI_TIME_SIGNATURE        0x58
+#define MIDI_RESET_ALL_CONTROLLERS 0x79
+#define MIDI_ALL_NOTES_OFF         0x7b
+#define MIDI_MONO_MODE_ON          0x7E
+#define MIDI_SYSTEM_RESET          0xFF
+
+#define GET_NEXT_EVENT( track, data ) do { \
+    ( data ) = *( track )->pos; \
+    ( track )->pos += 1; \
+} while (0)
+
+#define GET_MIDI_CHANNEL( event )       ( ( event ) & 0xf )
+#define GET_MIDI_COMMAND( event )       ( ( event ) >> 4 )
+
+#define EMIDI_INFINITE          -1
+#define EMIDI_END_LOOP_VALUE    127
+#define EMIDI_ALL_CARDS         127
+#define EMIDI_INCLUDE_TRACK     110
+#define EMIDI_EXCLUDE_TRACK     111
+#define EMIDI_PROGRAM_CHANGE    112
+#define EMIDI_VOLUME_CHANGE     113
+#define EMIDI_CONTEXT_START     114
+#define EMIDI_CONTEXT_END       115
+#define EMIDI_LOOP_START        116
+#define EMIDI_LOOP_END          117
+#define EMIDI_SONG_LOOP_START   118
+#define EMIDI_SONG_LOOP_END     119
+
+#define EMIDI_GeneralMIDI       0
+#define EMIDI_SoundBlaster      4
+
+#define EMIDI_AffectsCurrentCard(c, type) (((c) == EMIDI_ALL_CARDS) || ((c) == (type)))
+#define EMIDI_NUM_CONTEXTS      7
+
+typedef struct
+{
+    char *pos;
+    char *loopstart;
+    int16_t loopcount;
+    int16_t RunningStatus;
+    unsigned time;
+    int32_t FPSecondsPerTick;
+    int16_t tick;
+    int16_t beat;
+    int16_t measure;
+    int16_t BeatsPerMeasure;
+    int16_t TicksPerBeat;
+    int16_t TimeBase;
+    int32_t delay;
+    int16_t active;
+} songcontext;
+
+typedef struct
+{
+    char *start;
+    char *pos;
+
+    int32_t delay;
+    int16_t active;
+    int16_t RunningStatus;
+
+    int16_t currentcontext;
+    songcontext context[EMIDI_NUM_CONTEXTS];
+
+    char EMIDI_IncludeTrack;
+    char EMIDI_ProgramChange;
+    char EMIDI_VolumeChange;
+} track;
+
+static int32_t _MIDI_ReadNumber(void *from, size_t size);
+static int32_t _MIDI_ReadDelta(track *ptr);
+static void _MIDI_ResetTracks(void);
+static void _MIDI_AdvanceTick(void);
+static void _MIDI_MetaEvent(track *Track);
+static void _MIDI_SysEx(track *Track);
+static int32_t _MIDI_InterpretControllerInfo(track *Track, int32_t TimeSet, int32_t channel, int32_t c1, int32_t c2);
+static int32_t _MIDI_SendControlChange(int32_t channel, int32_t c1, int32_t c2);
+static void _MIDI_SetChannelVolume(int32_t channel, int32_t volume);
+static void _MIDI_SendChannelVolumes(void);
+static void _MIDI_InitEMIDI(void);
+
+#endif
diff --git a/source/blood/src/actor.cpp b/source/blood/src/actor.cpp
new file mode 100644
index 000000000..888d08a39
--- /dev/null
+++ b/source/blood/src/actor.cpp
@@ -0,0 +1,7247 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#include <random>
+#include <iostream>
+
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aibat.h"
+#include "aibeast.h"
+#include "aiboneel.h"
+#include "aiburn.h"
+#include "aicaleb.h"
+#include "aicerber.h"
+#include "aicult.h"
+#include "aigarg.h"
+#include "aighost.h"
+#include "aigilbst.h"
+#include "aihand.h"
+#include "aihound.h"
+#include "aiinnoc.h"
+#include "aipod.h"
+#include "airat.h"
+#include "aispid.h"
+#include "aitchern.h"
+#include "aizomba.h"
+#include "aizombf.h"
+#include "aiunicult.h"
+#include "blood.h"
+#include "callback.h"
+#include "config.h"
+#include "db.h"
+#include "endgame.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gameutil.h"
+#include "gib.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "tile.h"
+#include "trig.h"
+#include "triggers.h"
+#include "view.h"
+#include "warp.h"
+#include "weapon.h"
+
+VECTORDATA gVectorData[] = {
+    // Tine
+    {
+        DAMAGE_TYPE_2,
+        17,
+        174762,
+        1152,
+        10240,
+        0,
+        1,
+        20480,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_43, FX_6, FX_NONE, 502,
+        FX_43, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_7, 502,
+        FX_43, FX_6, FX_7, 502,
+        FX_NONE, FX_NONE, FX_NONE, 503,
+        FX_43, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 503,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        1207, 1207,
+    },
+    // Shell
+    {
+        DAMAGE_TYPE_2,
+        4,
+        65536,
+        0,
+        8192,
+        0,
+        1,
+        12288,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_5, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_43, FX_6, FX_NONE, -1,
+        FX_43, FX_0, FX_NONE, -1,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_43, FX_6, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        1001, 1001,
+    },
+    // Bullet
+    {
+        DAMAGE_TYPE_2,
+        7,
+        21845,
+        0,
+        32768,
+        0,
+        1,
+        12288,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_5, FX_7, 510,
+        FX_NONE, FX_5, FX_7, 511,
+        FX_43, FX_6, FX_NONE, 512,
+        FX_43, FX_0, FX_NONE, 513,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_7, 512,
+        FX_43, FX_6, FX_7, 512,
+        FX_NONE, FX_NONE, FX_NONE, 513,
+        FX_43, FX_NONE, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        4001, 4002,
+    },
+    // Tommy AP
+    {
+        DAMAGE_TYPE_2,
+        20,
+        65536,
+        0,
+        16384,
+        0,
+        1,
+        20480,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_5, FX_7, 510,
+        FX_NONE, FX_5, FX_7, 511,
+        FX_43, FX_6, FX_NONE, 512,
+        FX_43, FX_0, FX_NONE, 513,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_7, 512,
+        FX_43, FX_6, FX_7, 512,
+        FX_NONE, FX_NONE, FX_NONE, 513,
+        FX_43, FX_NONE, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        431, 431
+    },
+    // Shell AP
+    {
+        DAMAGE_TYPE_2,
+        6,
+        87381,
+        0,
+        12288,
+        0,
+        1,
+        6144,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_5, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_43, FX_6, FX_NONE, -1,
+        FX_43, FX_0, FX_NONE, -1,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_43, FX_6, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        1002, 1002,
+    },
+    // Tommy regular
+    {
+        DAMAGE_TYPE_2,
+        12,
+        65536,
+        0,
+        16384,
+        0,
+        1,
+        12288,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_43, FX_5, FX_7, 510,
+        FX_NONE, FX_5, FX_7, 511,
+        FX_43, FX_6, FX_NONE, 512,
+        FX_43, FX_0, FX_NONE, 513,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_7, 512,
+        FX_43, FX_6, FX_7, 512,
+        FX_NONE, FX_NONE, FX_NONE, 513,
+        FX_43, FX_NONE, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_6, FX_NONE, 513,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        359, 359,
+    },
+    // Bat bite
+    {
+        DAMAGE_TYPE_2,
+        4,
+        0,
+        921,
+        0,
+        0,
+        1,
+        4096,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        521, 521,
+    },
+    // Eel bite
+    {
+        DAMAGE_TYPE_2,
+        12,
+        0,
+        1177,
+        0,
+        0,
+        0,
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        513, 513
+    },
+    // Gill bite
+    {
+        DAMAGE_TYPE_2,
+        9,
+        0,
+        1177,
+        0,
+        0,
+        0,
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        499, 499
+    },
+            // Beast slash
+    {
+        DAMAGE_TYPE_3,
+        50,
+        43690,
+        1024,
+        8192,
+        0,
+        4,
+        32768,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        9012, 9014
+    },
+            // Axe
+    {
+        DAMAGE_TYPE_2,
+        18,
+        436906,
+        1024,
+        16384,
+        0,
+        2,
+        20480,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        1101, 1101
+    },
+            // Cleaver
+    {
+        DAMAGE_TYPE_2,
+        9,
+        218453,
+        1024,
+        0,
+        0,
+        1,
+        24576,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        1207, 1207
+    },
+            // Phantasm slash
+    {
+        DAMAGE_TYPE_2,
+        20,
+        436906,
+        1024,
+        16384,
+        0,
+        3,
+        24576,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        499, 495
+    },
+            // Gargoyle Slash
+    {
+        DAMAGE_TYPE_2,
+        16,
+        218453,
+        1024,
+        8192,
+        0,
+        4,
+        20480,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        495, 496
+    },
+            // Cerberus bite
+    {
+        DAMAGE_TYPE_2,
+        19,
+        218453,
+        614,
+        8192,
+        0,
+        2,
+        24576,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        9013, 499
+    },
+            // Hound bite
+    {
+        DAMAGE_TYPE_2,
+        10,
+        218453,
+        614,
+        8192,
+        0,
+        2,
+        24576,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        1307, 1308
+    },
+            // Rat bite
+    {
+        DAMAGE_TYPE_2,
+        4,
+        0,
+        921,
+        0,
+        0,
+        1,
+        24576,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        499, 499
+    },
+            // Spider bite
+    {
+        DAMAGE_TYPE_2,
+        8,
+        0,
+        614,
+        0,
+        0,
+        1,
+        24576,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        499, 499
+    },
+            // Unk
+    {
+        DAMAGE_TYPE_2,
+        9,
+        0,
+        512,
+        0,
+        0,
+        0,
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_5, FX_NONE, 500,
+        FX_NONE, FX_5, FX_NONE, 501,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_0, FX_NONE, 503,
+        FX_NONE, FX_4, FX_NONE, -1,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_6, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 502,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        499, 499
+    },
+    
+    {
+        (DAMAGE_TYPE)-1,
+        0,
+        0,
+        2560,
+        0,
+        0,
+        0,
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_34, FX_35, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        499, 499
+    },
+    // Tchernobog burn vector
+    {
+        DAMAGE_TYPE_1,
+        2,
+        0,
+        0,
+        0,
+        15,
+        0,
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        351, 351
+    },
+    // Vodoo 1.0 vector
+    {
+        DAMAGE_TYPE_5,
+        25,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        0,0
+    },
+    // 22 kVectorGenDudePunch
+    {
+    DAMAGE_TYPE_0, 
+        37, 
+        874762, 
+        620, 
+        0, 
+        0, 
+        0, 
+        0,
+        FX_NONE, FX_NONE, FX_NONE, -1,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        FX_NONE, FX_NONE, FX_NONE, 357,
+        357, 499
+    },
+};
+
+ITEMDATA gItemData[] = {
+    {
+        0,
+        2552,
+        (char)-8,
+        0,
+        32,
+        32,
+        -1,
+    },
+    {
+        0,
+        2553,
+        (char)-8,
+        0,
+        32,
+        32,
+        -1,
+    },
+    {
+        0,
+        2554,
+        (char)-8,
+        0,
+        32,
+        32,
+        -1,
+    },
+    {
+        0,
+        2555,
+        (char)-8,
+        0,
+        32,
+        32,
+        -1,
+    },
+    {
+        0,
+        2556,
+        (char)-8,
+        0,
+        32,
+        32,
+        -1,
+    },
+    {
+        0,
+        2557,
+        (char)-8,
+        0,
+        32,
+        32,
+        -1,
+    },
+    {
+        0,
+        -1,
+        (char)-8,
+        0,
+        255,
+        255,
+        -1,
+    },
+    {
+        0,
+        519,
+        (char)-8,
+        0,
+        48,
+        48,
+        0,
+    },
+    {
+        0,
+        822,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        2169,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        2433,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        517,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        783,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        896,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        825,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        827,
+        (char)-8,
+        0,
+        40,
+        40,
+        4,
+    },
+    {
+        0,
+        828,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        829,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        830,
+        (char)-8,
+        0,
+        80,
+        64,
+        1,
+    },
+    {
+        0,
+        831,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        863,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        760,
+        (char)-8,
+        0,
+        40,
+        40,
+        2,
+    },
+    {
+        0,
+        836,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        851,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        2428,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        839,
+        (char)-8,
+        0,
+        40,
+        40,
+        3,
+    },
+    {
+        0,
+        768,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        840,
+        (char)-8,
+        0,
+        48,
+        48,
+        -1,
+    },
+    {
+        0,
+        841,
+        (char)-8,
+        0,
+        48,
+        48,
+        -1,
+    },
+    {
+        0,
+        842,
+        (char)-8,
+        0,
+        48,
+        48,
+        -1,
+    },
+    {
+        0,
+        843,
+        (char)-8,
+        0,
+        48,
+        48,
+        -1,
+    },
+    {
+        0,
+        683,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        521,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        604,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        520,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        803,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        518,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        522,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        523,
+        (char)-8,
+        0,
+        40,
+        40,
+        -1,
+    },
+    {
+        0,
+        837,
+        (char)-8,
+        0,
+        80,
+        64,
+        -1,
+    },
+    {
+        0,
+        2628,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        2586,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        2578,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        2602,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        2594,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        753,
+        (char)-8,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        753,
+        (char)-8,
+        7,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        3558,
+        (char)-128,
+        0,
+        64,
+        64,
+        -1,
+    },
+    {
+        0,
+        3558,
+        (char)-128,
+        7,
+        64,
+        64,
+        -1,
+    }
+};
+
+AMMOITEMDATA gAmmoItemData[] = {
+    {
+        0,
+        618,
+        (char)-8,
+        0,
+        40,
+        40,
+        480,
+        6,
+        7
+    },
+    {
+        0,
+        589,
+        (char)-8,
+        0,
+        48,
+        48,
+        1,
+        5,
+        6
+    },
+    {
+        0,
+        589,
+        (char)-8,
+        0,
+        48,
+        48,
+        1,
+        5,
+        6
+    },
+    {
+        0,
+        809,
+        (char)-8,
+        0,
+        48,
+        48,
+        5,
+        5,
+        6
+    },
+    {
+        0,
+        811,
+        (char)-8,
+        0,
+        48,
+        48,
+        1,
+        10,
+        11
+    },
+    {
+        0,
+        810,
+        (char)-8,
+        0,
+        48,
+        48,
+        1,
+        11,
+        12
+    },
+    {
+        0,
+        820,
+        (char)-8,
+        0,
+        24,
+        24,
+        10,
+        8,
+        0
+    },
+    {
+        0,
+        619,
+        (char)-8,
+        0,
+        48,
+        48,
+        4,
+        2,
+        0
+    },
+    {
+        0,
+        812,
+        (char)-8,
+        0,
+        48,
+        48,
+        15,
+        2,
+        0
+    },
+    {
+        0,
+        813,
+        (char)-8,
+        0,
+        48,
+        48,
+        15,
+        3,
+        0
+    },
+    {
+        0,
+        525,
+        (char)-8,
+        0,
+        48,
+        48,
+        100,
+        9,
+        10
+    },
+    {
+        0,
+        814,
+        (char)-8,
+        0,
+        48,
+        48,
+        15,
+        255,
+        0
+    },
+    {
+        0,
+        817,
+        (char)-8,
+        0,
+        48,
+        48,
+        100,
+        3,
+        0
+    },
+    {
+        0,
+        548,
+        (char)-8,
+        0,
+        24,
+        24,
+        32,
+        7,
+        0
+    },
+    {
+        0,
+        0,
+        (char)-8,
+        0,
+        48,
+        48,
+        6,
+        255,
+        0
+    },
+    {
+        0,
+        0,
+        (char)-8,
+        0,
+        48,
+        48,
+        6,
+        255,
+        0
+    },
+    {
+        0,
+        816,
+        (char)-8,
+        0,
+        48,
+        48,
+        8,
+        1,
+        0
+    },
+    {
+        0,
+        818,
+        (char)-8,
+        0,
+        48,
+        48,
+        8,
+        255,
+        0
+    },
+    {
+        0,
+        819,
+        (char)-8,
+        0,
+        48,
+        48,
+        8,
+        255,
+        0
+    },
+    {
+        0,
+        801,
+        (char)-8,
+        0,
+        48,
+        48,
+        6,
+        4,
+        0
+    },
+    {
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0
+    },
+};
+
+WEAPONITEMDATA gWeaponItemData[] = {
+    {
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        0,
+        -1,
+        0
+    },
+    {
+        0,
+        559,
+        (char)-8,
+        0,
+        48,
+        48,
+        3,
+        2,
+        8
+    },
+    {
+        0,
+        558,
+        (char)-8,
+        0,
+        48,
+        48,
+        4,
+        3,
+        50
+    },
+    {
+        0,
+        524,
+        (char)-8,
+        0,
+        48,
+        48,
+        2,
+        1,
+        9
+    },
+    {
+        0,
+        525,
+        (char)-8,
+        0,
+        48,
+        48,
+        10,
+        9,
+        100
+    },
+    {
+        0,
+        539,
+        (char)-8,
+        0,
+        48,
+        48,
+        8,
+        7,
+        64
+    },
+    {
+        0,
+        526,
+        (char)-8,
+        0,
+        48,
+        48,
+        5,
+        4,
+        6
+    },
+    {
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        1,
+        -1,
+        0
+    },
+    {
+        0,
+        618,
+        (char)-8,
+        0,
+        48,
+        48,
+        7,
+        6,
+        480
+    },
+    {
+        0,
+        589,
+        (char)-8,
+        0,
+        48,
+        48,
+        6,
+        5,
+        1
+    },
+    {
+        0,
+        800,
+        (char)-8,
+        0,
+        48,
+        48,
+        9,
+        8,
+        35
+    }
+};
+
+MissileType missileInfo[] = {
+    // Cleaver
+    {
+        2138,
+        978670,
+        512,
+        40,
+        40,
+        (char)-16,
+        16,
+        1207, 1207
+    },
+    // Regular flare
+    {
+        2424,
+        3145728,
+        0,
+        32,
+        32,
+        (char)-128,
+        32,
+        420, 420
+    },
+    // Tesla alt
+    {
+        3056,
+        2796202,
+        0,
+        32,
+        32,
+        (char)-128,
+        32,
+        471, 471
+    },
+    // Flare alt
+    {
+        2424,
+        2446677,
+        0,
+        32,
+        32,
+        (char)-128,
+        4,
+        421, 421
+    },
+    // Spray flame
+    {
+        0,
+        1118481,
+        0,
+        24,
+        24,
+        (char)-128,
+        16,
+        1309, 351
+    },
+    // Fireball
+    {
+        0,
+        1118481,
+        0,
+        32,
+        32,
+        (char)-128,
+        32,
+        480, 480
+    },
+    // Tesla regular
+    {
+        2130,
+        2796202,
+        0,
+        32,
+        32,
+        (char)-128,
+        16,
+        470, 470
+    },
+    // EctoSkull
+    {
+        870,
+        699050,
+        0,
+        32,
+        32,
+        (char)-24,
+        32,
+        489, 490
+    },
+    // Hellhound flame
+    {
+        0,
+        1118481,
+        0,
+        24,
+        24,
+        (char)-128,
+        16,
+        462, 351
+    },
+    // Puke
+    {
+        0,
+        838860,
+        0,
+        16,
+        16,
+        (char)-16,
+        16,
+        1203, 172
+    },
+    // Reserved
+    {
+        0,
+        838860,
+        0,
+        8,
+        8,
+        (char)0,
+        16,
+        0,0
+    },
+    // Stone gargoyle projectile
+    {
+        3056,
+        2097152,
+        0,
+        32,
+        32,
+        (char)-128,
+        16,
+        1457, 249
+    },
+    // Napalm launcher
+    {
+        0,
+        2446677,
+        0,
+        30,
+        30,
+        (char)-128,
+        24,
+        480, 489
+    },
+    // Cerberus fireball
+    {
+        0,
+        2446677,
+        0,
+        30,
+        30,
+        (char)-128,
+        24,
+        480, 489
+    },
+    // Tchernobog fireball
+    {
+        0,
+        1398101,
+        0,
+        24,
+        24,
+        (char)-128,
+        16,
+        480, 489
+    },
+    // Regular life leech
+    {
+        2446,
+        2796202,
+        0,
+        32,
+        32,
+        (char)-128,
+        16,
+        491, 491
+    },
+    // Dropped life leech (enough ammo)
+    {
+        3056,
+        2446677,
+        0,
+        16,
+        16,
+        (char)-128,
+        16,
+        520, 520
+    },
+    // Dropped life leech (no ammo)
+    {
+        3056,
+        1747626,
+        0,
+        32,
+        32,
+        (char)-128,
+        16,
+        520, 520
+    }
+};
+
+THINGINFO thingInfo[] = {
+    //TNT Barrel
+    {
+        25,
+        250,
+        32,
+        11,
+        4096,
+        80,
+        384,
+        907,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 256, 128, 64, 0, 0, 128,
+        1
+    },
+    // Armed Proxy Dynamite
+    {
+        5,
+        5,
+        16,
+        3,
+        24576,
+        1600,
+        256,
+        3444,
+        (char)-16,
+        0,
+        32,
+        32,
+        256, 256, 256, 64, 0, 0, 512,
+        1
+    },
+    // Armed Remote Dynamite
+    {
+        5,
+        5,
+        16,
+        3,
+        24576,
+        1600,
+        256,
+        3457,
+        (char)-16,
+        0,
+        32,
+        32,
+        256, 256, 256, 64, 0, 0, 512,
+        1
+    },
+    // Vase1
+    {
+        1,
+        20,
+        32,
+        3,
+        32768,
+        80,
+        0,
+        739,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 0, 256, 128, 0, 0, 0,
+        0
+    },
+    // Vase2
+    {
+        1,
+        150,
+        32,
+        3,
+        32768,
+        80,
+        0,
+        642,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 256, 256, 128, 0, 0, 0,
+        0
+    },
+    // Crate face
+    {
+        10,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        462,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 0, 0, 256, 0, 0, 0,
+        0
+    },
+    // Glass window
+    {
+        1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        266,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 0, 256, 256, 0, 0, 0,
+        0,
+    },
+    // Flourescent Light
+    {
+        1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        796,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 0, 256, 256, 0, 0, 512,
+        0,
+    },
+    // Wall Crack
+    {
+        50,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1127,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 0, 0, 256, 0, 0, 0,
+        0,
+    },
+    // Wood Beam
+    {
+        8,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1142,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 0, 256, 128, 0, 0, 0,
+        0,
+    },
+    // Spider's Web
+    {
+        4,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        1069,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 256, 64, 256, 0, 0, 128,
+        0,
+    },
+    // Metal Grate
+    {
+        40,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        483,
+        (char)0,
+        0,
+        0,
+        0,
+        64, 0, 128, 256, 0, 0, 0,
+        0,
+    },
+    // Flammable Tree
+    {
+        1,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 256, 0, 256, 0, 0, 128,
+        0,
+    },
+    // MachineGun Trap
+    {
+        1000,
+        0,
+        0,
+        8,
+        0,
+        0,
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 0, 128, 256, 0, 0, 512,
+        0,
+    },
+    // Falling Rock
+    {
+        0,
+        15,
+        8,
+        3,
+        32768,
+        0,
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+    },
+    // Kickable Pail
+    {
+        0,
+        8,
+        48,
+        3,
+        49152,
+        0,
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 0, 0, 0, 0, 0, 0,
+        1,
+    },
+    // Gib Object
+    {
+        10,
+        2,
+        0,
+        0,
+        32768,
+        0,
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 0, 256, 256, 0, 0, 128,
+        0,
+    },
+    // Explode Object
+    {
+        20,
+        2,
+        0,
+        0,
+        32768,
+        0,
+        0,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        0, 0, 0, 256, 0, 0, 128,
+        0,
+    },
+    // Armed stick Of TNT
+    {
+        5,
+        14,
+        16,
+        3,
+        24576,
+        1600,
+        256,
+        3422,
+        (char)-32,
+        0,
+        32,
+        32,
+        64, 256, 128, 64, 0, 0, 256,
+        1
+    },
+    // Armed bundle Of TNT
+    {
+        5,
+        14,
+        16,
+        3,
+        24576,
+        1600,
+        256,
+        3433,
+        (char)-32,
+        0,
+        32,
+        32,
+        64, 256, 128, 64, 0, 0, 256,
+        1
+    },
+    // Armed aerosol
+    {
+        5,
+        14,
+        16,
+        3,
+        32768,
+        1600,
+        256,
+        3467,
+        (char)-128,
+        0,
+        32,
+        32,
+        64, 256, 128, 64, 0, 0, 256,
+        1
+    },
+    // Bone (Flesh Garg.)
+    {
+        5,
+        6,
+        16,
+        3,
+        32768,
+        1600,
+        256,
+        1462,
+        (char)0,
+        0,
+        32,
+        32,
+        0, 0, 0, 0, 0, 0, 0,
+        1
+    },
+    // Some alpha stuff
+    {
+        8,
+        3,
+        16,
+        11,
+        32768,
+        1600,
+        256,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        256, 0, 256, 256, 0, 0, 0,
+        0,
+    },
+    // WaterDrip 
+    {
+        0,
+        1,
+        1,
+        2,
+        0,
+        0,
+        0,
+        1147,
+        (char)0,
+        10,
+        0,
+        0,
+        0, 0, 0, 0, 0, 0, 0,
+        0
+    },
+    // BloodDrip 
+    {
+        0,
+        1,
+        1,
+        2,
+        0,
+        0,
+        0,
+        1160,
+        (char)0,
+        2,
+        0,
+        0,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+    },
+    // Blood chucks1 
+    {
+        15,
+        4,
+        4,
+        3,
+        24576,
+        0,
+        257,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        128, 64, 256, 256, 0, 0, 256,
+        0,
+    },
+    // Blood chucks2
+    {
+        30,
+        30,
+        8,
+        3,
+        8192,
+        0,
+        257,
+        -1,
+        (char)0,
+        0,
+        0,
+        0,
+        128, 64, 256, 256, 0, 0, 64,
+        0,
+    },
+    // Axe Zombie Head 
+    {
+        60,
+        5,
+        32,
+        3,
+        40960,
+        1280,
+        257,
+        3405,
+        (char)0,
+        0,
+        40,
+        40,
+        128, 64, 256, 256, 0, 0, 64,
+        1,
+    },
+    // Napalm's Alt Fire explosion
+    {
+        80,
+        30,
+        32,
+        3,
+        57344,
+        1600,
+        256,
+        3281,
+        (char)-128,
+        0,
+        32,
+        32,
+        0, 0, 0, 0, 0, 0, 0,
+        1,
+    },
+    // Fire Pod Explosion
+    {
+        80,
+        30,
+        32,
+        3,
+        57344,
+        1600,
+        256,
+        2020,
+        (char)-128,
+        0,
+        32,
+        32,
+        256, 0, 256, 256, 0, 0, 0,
+        1,
+    },
+    // Green Pod Explosion
+    {
+        80,
+        30,
+        32,
+        3,
+        57344,
+        1600,
+        256,
+        1860,
+        (char)-128,
+        0,
+        32,
+        32,
+        256, 0, 256, 256, 0, 0, 0,
+        1,
+    },
+    // Life Leech
+    {
+        150,
+        30,
+        48,
+        3,
+        32768,
+        1600,
+        257,
+        800,
+        (char)-128,
+        0,
+        48,
+        48,
+        64, 64, 112, 64, 0, 96, 96,
+        1,
+    },
+    // Voodoo Head
+    {
+        1,
+        30,
+        48,
+        3,
+        32768,
+        1600,
+        0,
+        2443,
+        (char)-128,
+        0,
+        16,
+        16,
+        0, 0, 0, 0, 0, 0, 0,
+        1,
+    },
+    // 433 - kGDXThingTNTProx
+    {
+        5,
+        5,
+        16,
+        3,
+        24576,
+        1600,
+        256,
+        3444,
+        (char)-16,
+        7,
+        32,
+        32,
+        256, 256, 256, 64, 0, 0, 512,
+        1
+    },
+    // 434 - kGDXThingThrowableRock
+    {
+        5,
+        6,
+        16,
+        3,
+        32768,
+        1600,
+        256,
+        1462,
+        (char)0,
+        0,
+        32,
+        32,
+        0, 0, 0, 0, 0, 0, 0,
+        1
+    },
+    // 435 - kGDXThingCustomDudeLifeLeech
+    {
+        150,
+        30,
+        48,
+        3,
+        32768,
+        1600,
+        257,
+        800,
+        (char)-128,
+        0,
+        48,
+        48,
+        64, 64, 112, 64, 0, 96, 96,
+        1,
+    },
+};
+
+EXPLOSION explodeInfo[] = {
+    {
+        40,
+        10,
+        10,
+        75,
+        450,
+        0,
+        60,
+        80,
+        40
+    },
+    {
+        80,
+        20,
+        10,
+        150,
+        900,
+        0,
+        60,
+        160,
+        60
+    },
+    {
+        120,
+        40,
+        15,
+        225,
+        1350,
+        0,
+        60,
+        240,
+        80
+    },
+    {
+        80,
+        5,
+        10,
+        120,
+        20,
+        10,
+        60,
+        0,
+        40
+    },
+    {
+        120,
+        10,
+        10,
+        180,
+        40,
+        10,
+        60,
+        0,
+        80
+    },
+    {
+        160,
+        15,
+        10,
+        240,
+        60,
+        10,
+        60,
+        0,
+        120
+    },
+    {
+        40,
+        20,
+        10,
+        120,
+        0,
+        10,
+        30,
+        60,
+        40
+    },
+    {
+        80,
+        20,
+        10,
+        150,
+        800,
+        5,
+        60,
+        160,
+        60
+    },
+};
+
+int gDudeDrag = 0x2a00;
+
+short gAffectedSectors[kMaxSectors];
+short gAffectedXWalls[kMaxXWalls];
+short gPlayerGibThingComments[] = {
+    734, 735, 736, 737, 738, 739, 740, 741, 3038, 3049
+};
+
+void FireballSeqCallback(int, int);
+void sub_38938(int, int);
+void NapalmSeqCallback(int, int);
+void sub_3888C(int, int);
+void TreeToGibCallback(int, int);
+void DudeToGibCallback1(int, int);
+void DudeToGibCallback2(int, int);
+
+SPRITEHIT gSpriteHit[kMaxXSprites];
+
+int nFireballClient = seqRegisterClient(FireballSeqCallback);
+int dword_2192D8 = seqRegisterClient(sub_38938);
+int nNapalmClient = seqRegisterClient(NapalmSeqCallback);
+int dword_2192E0 = seqRegisterClient(sub_3888C);
+int nTreeToGibClient = seqRegisterClient(TreeToGibCallback);
+int nDudeToGibClient1 = seqRegisterClient(DudeToGibCallback1);
+int nDudeToGibClient2 = seqRegisterClient(DudeToGibCallback2);
+
+int gPostCount = 0;
+
+struct POSTPONE {
+    short at0;
+    short at2;
+};
+
+POSTPONE gPost[kMaxSprites];
+
+static char buffer[120];
+
+bool IsItemSprite(spritetype *pSprite)
+{
+    return pSprite->type >= 100 && pSprite->type < 149;
+}
+
+bool IsWeaponSprite(spritetype *pSprite)
+{
+    return pSprite->type >= 40 && pSprite->type < 51;
+}
+
+bool IsAmmoSprite(spritetype *pSprite)
+{
+    return pSprite->type >= 60 && pSprite->type < 81;
+}
+
+bool IsUnderwaterSector(int nSector)
+{
+    int nXSector = sector[nSector].extra;
+    if (nXSector > 0 && xsector[nXSector].Underwater)
+        return 1;
+    return 0;
+}
+
+int actSpriteOwnerToSpriteId(spritetype *pSprite)
+{
+    dassert(pSprite != NULL);
+    if (pSprite->owner == -1)
+        return -1;
+    int nSprite = pSprite->owner & 0xfff;
+    if (pSprite->owner & 0x1000)
+        nSprite = gPlayer[nSprite].pSprite->index;
+    return nSprite;
+}
+
+void actPropagateSpriteOwner(spritetype *pTarget, spritetype *pSource)
+{
+    dassert(pTarget != NULL && pSource != NULL);
+    if (IsPlayerSprite(pSource))
+        pTarget->owner = (pSource->type-kDudePlayer1) | 0x1000;
+    else
+        pTarget->owner = pSource->index;
+}
+
+int actSpriteIdToOwnerId(int nSprite)
+{
+    if (nSprite == -1)
+        return -1;
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    spritetype *pSprite = &sprite[nSprite];
+    if (IsPlayerSprite(pSprite))
+        nSprite = (pSprite->type-kDudePlayer1) | 0x1000;
+    return nSprite;
+}
+
+int actOwnerIdToSpriteId(int nSprite)
+{
+    if (nSprite == -1)
+        return -1;
+    if (nSprite & 0x1000)
+        nSprite = gPlayer[nSprite&0xfff].pSprite->index;
+    return nSprite;
+}
+
+bool actTypeInSector(int nSector, int nType)
+{
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        if (sprite[nSprite].index == nType)
+            return 1;
+    }
+    return 0;
+}
+
+void actAllocateSpares(void)
+{
+}
+
+int DudeDifficulty[5] = {
+    512, 384, 256, 208, 160
+};
+
+void actInit(void)
+{
+    for (int nSprite = headspritestat[3]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->type == 44) // Voodoo doll (ammo)
+            pSprite->type = 70; // Voodoo doll (weapon)
+
+        switch (pSprite->type) {
+        case 44:
+            pSprite->type = 70;
+            break;
+        
+        // By NoOne: add Random pickup feature
+        case 40: // Random weapon
+        case 80: // Random ammo
+
+            // Make sprites invisible and non-blocking
+            pSprite->cstat &= ~kSprBlock;
+            pSprite->cstat |= kSprInvisible;
+
+            if (pSprite->extra > 0 && xsprite[pSprite->extra].state == 1)
+                trTriggerSprite(nSprite, &xsprite[pSprite->extra], COMMAND_ID_0);
+            break;
+        }
+    }
+    for (int nSprite = headspritestat[11]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        int nXSprite = pSprite->extra;
+        XSPRITE *pXSprite = NULL;
+        if (nXSprite > 0 && nXSprite < kMaxXSprites)
+            pXSprite = &xsprite[nXSprite];
+        if (pSprite->type == 459)
+        {
+            pXSprite->state = 0;
+            pXSprite->waitTime = ClipLow(pXSprite->waitTime, 1);
+            pSprite->cstat &= ~1;
+            pSprite->cstat |= 32768;
+        }
+    }
+    for (int nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        int nXSprite = pSprite->extra;
+        if (nXSprite <= 0 || nXSprite >= kMaxXSprites)
+            ThrowError("WARNING: Sprite %d is on the wrong status list!\n", nSprite);
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        int nType = pSprite->type - 400;
+        pSprite->clipdist = thingInfo[nType].at4;
+        pSprite->hitag = thingInfo[nType].at5;
+        if (pSprite->hitag&2)
+            pSprite->hitag |= 4;
+        xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+        pXSprite->health = thingInfo[nType].at0<<4;
+        switch (pSprite->type)
+        {
+        case 401:
+        case 413:
+        case kGDXThingTNTProx:
+            pXSprite->state = 0;
+            break;
+        case 426:
+        {
+            SEQINST *pInst = GetInstance(3, nXSprite);
+            if (pInst && pInst->at13)
+            {
+                DICTNODE *hSeq = gSysRes.Lookup(pInst->at8, "SEQ");
+                if (!hSeq)
+                    break;
+                seqSpawn(pInst->at8, 3, nXSprite);
+            }
+            break;
+        }
+        default:
+            pXSprite->state = 1;
+            break;
+        }
+    }
+    if (gGameOptions.nMonsterSettings == 0)
+    {
+        gKillMgr.SetCount(0);
+        while (headspritestat[6] >= 0)
+        {
+            spritetype *pSprite = &sprite[headspritestat[6]];
+            int nXSprite = pSprite->extra;
+            dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            // Drop Key
+            if (pXSprite->key > 0)
+                actDropObject(pSprite, 99 + pXSprite->key);
+            DeleteSprite(headspritestat[6]);
+        }
+    }
+    else
+    {
+        char unk[kDudeMax-kDudeBase];
+        memset(unk, 0, sizeof(unk));
+        for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            if (pSprite->type < kDudeBase || pSprite->type >= kDudeMax)
+                ThrowError("Non-enemy sprite (%d) in the enemy sprite list.\n", nSprite);
+            unk[pSprite->type-kDudeBase] = 1;
+        }
+        gKillMgr.sub_2641C();
+        for (int i = 0; i < kDudeMax-kDudeBase; i++)
+            for (int j = 0; j < 7; j++)
+                dudeInfo[i].at70[j] = mulscale8(DudeDifficulty[gGameOptions.nDifficulty], dudeInfo[i].startDamage[j]);
+        for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            int nXSprite = pSprite->extra;
+            dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            int nType = pSprite->type-kDudeBase;
+            if (!IsPlayerSprite(pSprite))
+            {
+                pSprite->cstat |= 4096+256+1;
+                
+                // By NoOne: allow user clipdist for custom dude.
+                switch (pSprite->type) {
+                    case kGDXDudeUniversalCultist:
+                    case kGDXGenDudeBurning:
+                        break;
+                    default:
+                        pSprite->clipdist = dudeInfo[nType].clipdist;
+                        break;
+                }
+
+                xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+                
+                // By NoOne: add a way to set custom hp for every enemy - should work only if map just started and not loaded.
+                if (pXSprite->data4 <= 0) pXSprite->health = dudeInfo[nType].startHealth << 4;
+                else {
+                    long hp = pXSprite->data4 << 4;
+                    pXSprite->health = (hp > 0) ? ((hp <= 65535) ? hp : 65535) : 1;
+                }
+            }
+
+            int seqStartId = dudeInfo[nType].seqStartID;
+            // By NoOne: store seqStartId in data2 field for custom dude
+            if (pSprite->type == kGDXDudeUniversalCultist) {
+                
+                if (pXSprite->data2 > 0) {
+                    seqStartId = pXSprite->data2;
+                    int seqEndId = pXSprite->data2 + 19;
+
+                    // check for full set of animations
+                    for (int i = seqStartId; i <= seqEndId; i++) {
+
+                        // exceptions
+                        switch (i - seqStartId) {
+                            case 3:
+                            case 4:
+                            case 11:
+                            case 12:
+                            case 18:
+                            case 19:
+                                continue;
+                        }
+
+                        if (!gSysRes.Lookup(i, "SEQ")) {
+                            //ThrowError("No SEQ file  found for custom dude!");
+                            pXSprite->data2 = dudeInfo[nType].seqStartID;
+                            seqStartId = pXSprite->data2;
+                            break;
+                        }
+                    }
+
+                } else {
+                    pXSprite->data2 = seqStartId;
+                }
+            }
+            
+            if (gSysRes.Lookup(seqStartId, "SEQ")) seqSpawn(seqStartId, 3, nXSprite);
+        }
+        aiInit();
+    }
+}
+
+void ConcussSprite(int a1, spritetype *pSprite, int x, int y, int z, int a6)
+{
+    dassert(pSprite != NULL);
+    int dx = pSprite->x-x;
+    int dy = pSprite->y-y;
+    int dz = (pSprite->z-z)>>4;
+    int dist2 = 0x40000+dx*dx+dy*dy+dz*dz;
+    dassert(dist2 > 0);
+    a6 = scale(0x40000, a6, dist2);
+    if (pSprite->hitag & 1)
+    {
+        int mass = 0;
+        if (IsDudeSprite(pSprite)) {
+            mass = dudeInfo[pSprite->lotag - kDudeBase].mass;
+            switch (pSprite->lotag) {
+            case kGDXDudeUniversalCultist:
+            case kGDXGenDudeBurning:
+                mass = getDudeMassBySpriteSize(pSprite);
+                break;
+            }
+        }
+        else if (pSprite->type >= kThingBase && pSprite->type < kThingMax)
+            mass = thingInfo[pSprite->type-400].at2;
+        else
+            ThrowError("Unexpected type in ConcussSprite(): Sprite: %d  Type: %d  Stat: %d", (int)pSprite->index, (int)pSprite->type, (int)pSprite->statnum);
+        int size = (tilesiz[pSprite->picnum].x*pSprite->xrepeat*tilesiz[pSprite->picnum].y*pSprite->yrepeat)>>1;
+        dassert(mass > 0);
+
+        int t = scale(a6, size, mass);
+        dx = mulscale16(t, dx);
+        dy = mulscale16(t, dy);
+        dz = mulscale16(t, dz);
+        int nSprite = pSprite->index;
+        dassert(nSprite >= 0 && nSprite < kMaxSprites);
+        xvel[nSprite] += dx;
+        yvel[nSprite] += dy;
+        zvel[nSprite] += dz;
+    }
+    actDamageSprite(a1, pSprite, DAMAGE_TYPE_3, a6);
+}
+
+int actWallBounceVector(int *x, int *y, int nWall, int a4)
+{
+    int wx, wy;
+    GetWallNormal(nWall, &wx, &wy);
+    int t = dmulscale16(*x, wx, *y, wy);
+    int t2 = mulscale16r(t, a4+0x10000);
+    *x -= mulscale16(wx, t2);
+    *y -= mulscale16(wy, t2);
+    return mulscale16r(t, 0x10000-a4);
+}
+
+int actFloorBounceVector(int *x, int *y, int *z, int nSector, int a5)
+{
+    int t = 0x10000-a5;
+    if (sector[nSector].floorheinum == 0)
+    {
+        int t2 = mulscale16(*z, t);
+        *z = -(*z-t2);
+        return t2;
+    }
+    walltype *pWall = &wall[sector[nSector].wallptr];
+    walltype *pWall2 = &wall[pWall->point2];
+    int angle = getangle(pWall2->x-pWall->x, pWall2->y-pWall->y)+512;
+    int t2 = sector[nSector].floorheinum<<4;
+    int t3 = approxDist(-0x10000, t2);
+    int t4 = divscale16(-0x10000, t3);
+    int t5 = divscale16(t2, t3);
+    int t6 = mulscale30(t5, Cos(angle));
+    int t7 = mulscale30(t5, Sin(angle));
+    int t8 = tmulscale16(*x, t6, *y, t7, *z, t4);
+    int t9 = mulscale16(t8, 0x10000+a5);
+    *x -= mulscale16(t6, t9);
+    *y -= mulscale16(t7, t9);
+    *z -= mulscale16(t4, t9);
+    return mulscale16r(t8, t);
+}
+
+void sub_2A620(int nSprite, int x, int y, int z, int nSector, int nDist, int a7, int a8, DAMAGE_TYPE a9, int a10, int a11, int a12, int a13)
+{
+    UNREFERENCED_PARAMETER(a12);
+    UNREFERENCED_PARAMETER(a13);
+    char va0[(kMaxSectors+7)>>3];
+    int nOwner = actSpriteIdToOwnerId(nSprite);
+    gAffectedSectors[0] = 0;
+    gAffectedXWalls[0] = 0;
+    GetClosestSpriteSectors(nSector, x, y, nDist, gAffectedSectors, va0, gAffectedXWalls);
+    nDist <<= 4;
+    if (a10 & 2)
+    {
+        for (int i = headspritestat[6]; i >= 0; i = nextspritestat[i])
+        {
+            if (i != nSprite || (a10 & 1))
+            {
+                spritetype *pSprite2 = &sprite[i];
+                if (pSprite2->extra > 0 && pSprite2->extra < kMaxXSprites)
+                {
+                    if (pSprite2->hitag & 0x20)
+                        continue;
+                    if (!TestBitString(va0, pSprite2->sectnum))
+                        continue;
+                    if (!CheckProximity(pSprite2, x, y, z, nSector, nDist))
+                        continue;
+                    int dx = klabs(x-pSprite2->x);
+                    int dy = klabs(y-pSprite2->y);
+                    int dz = klabs(z-pSprite2->z)>>4;
+                    int dist = ksqrt(dx*dx+dy*dy+dz*dz);
+                    if (dist > nDist)
+                        continue;
+                    int vcx;
+                    if (dist != 0)
+                        vcx = a7+((nDist-dist)*a8)/nDist;
+                    else
+                        vcx = a7+a8;
+                    actDamageSprite(nSprite, pSprite2, a9, vcx<<4);
+                    if (a11)
+                        actBurnSprite(nOwner, &xsprite[pSprite2->extra], a11);
+                }
+            }
+        }
+    }
+    if (a10 & 4)
+    {
+        for (int i = headspritestat[4]; i >= 0; i = nextspritestat[i])
+        {
+            spritetype *pSprite2 = &sprite[i];
+            if (pSprite2->hitag&0x20)
+                continue;
+            if (!TestBitString(va0, pSprite2->sectnum))
+                continue;
+            if (!CheckProximity(pSprite2, x, y, z, nSector, nDist))
+                continue;
+            XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+            if (pXSprite2->locked)
+                continue;
+            int dx = klabs(x-pSprite2->x);
+            int dy = klabs(y-pSprite2->y);
+            int dist = ksqrt(dx*dx+dy*dy);
+            if (dist > nDist)
+                continue;
+            int vcx;
+            if (dist != 0)
+                vcx = a7+((nDist-dist)*a8)/nDist;
+            else
+                vcx = a7+a8;
+            actDamageSprite(nSprite, pSprite2, a9, vcx<<4);
+            if (a11)
+                actBurnSprite(nOwner, pXSprite2, a11);
+        }
+    }
+}
+
+void sub_2AA94(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = actOwnerIdToSpriteId(pSprite->owner);
+    actPostSprite(pSprite->index, 0);
+    seqSpawn(9, 3, pSprite->extra);
+    if (Chance(0x8000))
+        pSprite->cstat |= 4;
+    sfxPlay3DSound(pSprite, 303, 24+(pSprite->hitag&3), 1);
+    sub_2A620(nSprite, pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, 128, 0, 60, DAMAGE_TYPE_3, 15, 120, 0, 0);
+    if (pXSprite->data4 > 1)
+    {
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        int v14[2];
+        v14[0] = pXSprite->data4>>1;
+        v14[1] = pXSprite->data4-v14[0];
+        int v4 = pSprite->ang;
+        xvel[pSprite->index] = 0;
+        yvel[pSprite->index] = 0;
+        zvel[pSprite->index] = 0;
+        for (int i = 0; i < 2; i++)
+        {
+            int t1 = Random(0x33333)+0x33333;
+            int t2 = Random2(0x71);
+            pSprite->ang = (t2+v4+2048)&2047;
+            spritetype *pSprite2 = actFireThing(pSprite, 0, 0, -0x93d0, 428, t1);
+            XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+            pSprite2->owner = pSprite->owner;
+            seqSpawn(61, 3, pSprite2->extra, nNapalmClient);
+            pXSprite2->data4 = v14[i];
+        }
+    }
+}
+
+spritetype *actSpawnFloor(spritetype *pSprite)
+{
+    short nSector = pSprite->sectnum;
+    int x = pSprite->x;
+    int y = pSprite->y;
+    updatesector(x, y, &nSector);
+    int zFloor = getflorzofslope(nSector, x, y);
+    spritetype *pSprite2 = actSpawnSprite(nSector, x, y, zFloor, 3, 0);
+    if (pSprite2)
+        pSprite2->cstat &= ~257;
+    return pSprite2;
+}
+
+spritetype *actDropAmmo(spritetype *pSprite, int nType)
+{
+    spritetype *pSprite2 = NULL;
+    if (pSprite && pSprite->statnum < kMaxStatus && nType >= 60 && nType < 81)
+    {
+        pSprite2 = actSpawnFloor(pSprite);
+        AMMOITEMDATA *pAmmo = &gAmmoItemData[nType-60];
+        pSprite2->type = nType;
+        pSprite2->picnum = pAmmo->picnum;
+        pSprite2->shade = pAmmo->shade;
+        pSprite2->xrepeat = pAmmo->xrepeat;
+        pSprite2->yrepeat = pAmmo->yrepeat;
+    }
+    return pSprite2;
+}
+
+spritetype *actDropWeapon(spritetype *pSprite, int nType)
+{
+    spritetype *pSprite2 = NULL;
+    if (pSprite && pSprite->statnum < kMaxStatus && nType >= 40 && nType < 51)
+    {
+        pSprite2 = actSpawnFloor(pSprite);
+        WEAPONITEMDATA *pWeapon = &gWeaponItemData[nType-40];
+        pSprite2->type = nType;
+        pSprite2->picnum = pWeapon->picnum;
+        pSprite2->shade = pWeapon->shade;
+        pSprite2->xrepeat = pWeapon->xrepeat;
+        pSprite2->yrepeat = pWeapon->yrepeat;
+    }
+    return pSprite2;
+}
+
+spritetype *actDropItem(spritetype *pSprite, int nType)
+{
+    spritetype *pSprite2 = NULL;
+    if (pSprite && pSprite->statnum < kMaxStatus && nType >= 100 && nType < 149)
+    {
+        pSprite2 = actSpawnFloor(pSprite);
+        ITEMDATA *pItem = &gItemData[nType-100];
+        pSprite2->type = nType;
+        pSprite2->picnum = pItem->picnum;
+        pSprite2->shade = pItem->shade;
+        pSprite2->xrepeat = pItem->xrepeat;
+        pSprite2->yrepeat = pItem->yrepeat;
+    }
+    return pSprite2;
+}
+
+spritetype *actDropKey(spritetype *pSprite, int nType)
+{
+    spritetype *pSprite2 = NULL;
+    if (pSprite && pSprite->statnum < kMaxStatus && nType >= 100 && nType <= 106)
+    {
+        pSprite2 = actDropItem(pSprite, nType);
+        if (pSprite2 && gGameOptions.nGameType == 1)
+        {
+            if (pSprite2->extra == -1)
+                dbInsertXSprite(pSprite2->index);
+            xsprite[pSprite2->extra].respawn = 3;
+            gSpriteHit[pSprite2->extra].florhit = 0;
+            gSpriteHit[pSprite2->extra].ceilhit = 0;
+        }
+    }
+    return pSprite2;
+}
+
+spritetype *actDropFlag(spritetype *pSprite, int nType)
+{
+    spritetype *pSprite2 = NULL;
+    if (pSprite && pSprite->statnum < kMaxStatus && (nType == 147 || nType == 148))
+    {
+        pSprite2 = actDropItem(pSprite, nType);
+        if (pSprite2 && gGameOptions.nGameType == 3)
+        {
+            evPost(pSprite2->index, 3, 1800, CALLBACK_ID_17);
+        }
+    }
+    return pSprite2;
+}
+
+spritetype *actDropObject(spritetype *pSprite, int nType)
+{
+    spritetype *pSprite2 = NULL;
+    if (nType >= 100 && nType <= 106)
+        pSprite2 = actDropKey(pSprite, nType);
+    else if (nType == 147 || nType == 148)
+        pSprite2 = actDropFlag(pSprite, nType);
+    else if (nType >= 100 && nType < 149)
+        pSprite2 = actDropItem(pSprite, nType);
+    else if (nType >= 60 && nType < 81)
+        pSprite2 = actDropAmmo(pSprite, nType);
+    else if (nType >= 40 && nType < 51)
+        pSprite2 = actDropWeapon(pSprite, nType);
+    if (pSprite2)
+    {
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (bottom >= pSprite2->z)
+            pSprite2->z -= bottom - pSprite2->z;
+    }
+    return pSprite2;
+}
+
+bool actHealDude(XSPRITE *pXDude, int a2, int a3)
+{
+    dassert(pXDude != NULL);
+    a2 <<= 4;
+    a3 <<= 4;
+    if (pXDude->health < a3)
+    {
+        spritetype *pSprite = &sprite[pXDude->reference];
+        if (IsPlayerSprite(pSprite))
+            sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, 780, pSprite->sectnum);
+        pXDude->health = ClipHigh(pXDude->health+a2, a3);
+        return 1;
+    }
+    return 0;
+}
+
+void actKillDude(int a1, spritetype *pSprite, DAMAGE_TYPE a3, int a4)
+{
+    spritetype *pSprite2 = &sprite[a1];
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    int nType = pSprite->type-kDudeBase;
+    int nXSprite = pSprite->extra;
+    dassert(nXSprite > 0);
+    XSPRITE *pXSprite = &xsprite[pSprite->extra];
+    switch (pSprite->type)
+    {
+    case kGDXDudeUniversalCultist:
+    {
+        removeDudeStuff(pSprite);
+        XSPRITE* pXIncarnation = getNextIncarnation(pXSprite);
+        if (pXIncarnation == NULL) {
+            
+            if (pXSprite->data1 >= 459 && pXSprite->data1 < (459 + kExplodeMax) - 1 &&
+                Chance(0x4000) && a3 != 5 && a3 != 4) {
+
+                doExplosion(pSprite, pXSprite->data1 - 459);
+                if (Chance(0x9000)) a3 = (DAMAGE_TYPE) 3;
+            }
+
+            if (a3 == DAMAGE_TYPE_1) {
+                if ((gSysRes.Lookup(pXSprite->data2 + 15, "SEQ") || gSysRes.Lookup(pXSprite->data2 + 16, "SEQ")) && pXSprite->medium == 0) {
+                    if (gSysRes.Lookup(pXSprite->data2 + 3, "SEQ")) {
+                        pSprite->type = kGDXGenDudeBurning;
+                        if (pXSprite->data2 == 11520) // don't inherit palette for burning if using default animation
+                            pSprite->pal = 0;
+
+                        aiNewState(pSprite, pXSprite, &GDXGenDudeBurnGoto);
+                        actHealDude(pXSprite, dudeInfo[55].startHealth, dudeInfo[55].startHealth);
+                        if (pXSprite->burnTime <= 0) pXSprite->burnTime = 1200;
+                        gDudeExtra[pSprite->extra].at0 = gFrameClock + 360;
+                        return;
+                    }
+
+                } else {
+                    pXSprite->burnTime = 0;
+                    pXSprite->burnSource = -1;
+                    a3 = DAMAGE_TYPE_0;
+                }
+            }
+            
+        } else {
+            int seqId = pXSprite->data2 + 18;
+            if (!gSysRes.Lookup(seqId, "SEQ")) {
+                seqKill(3, nXSprite);
+                sfxPlayGDXGenDudeSound(pSprite, 10, pXSprite->data3);
+                spritetype* pEffect = gFX.fxSpawn((FX_ID)52, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, pSprite->ang);
+                if (pEffect != NULL) {
+                    pEffect->cstat = kSprFace;
+                    pEffect->pal = 6;
+                    pEffect->xrepeat = pSprite->xrepeat;
+                    pEffect->yrepeat = pSprite->yrepeat;
+                }
+                
+                GIBTYPE nGibType;
+                for (int i = 0; i < 3; i++) {
+                    if (Chance(0x3000)) nGibType = GIBTYPE_6;
+                    else if (Chance(0x2000)) nGibType = GIBTYPE_5;
+                    else nGibType = GIBTYPE_17;
+
+                    int top, bottom;
+                    GetSpriteExtents(pSprite, &top, &bottom);
+                    CGibPosition gibPos(pSprite->x, pSprite->y, top);
+                    CGibVelocity gibVel(xvel[pSprite->index] >> 1, yvel[pSprite->index] >> 1, -0xccccc);
+                    GibSprite(pSprite, nGibType, &gibPos, &gibVel);
+                }
+
+                return;
+            }
+            seqSpawn(seqId, 3, nXSprite, -1);
+            sfxPlayGDXGenDudeSound(pSprite, 10, pXSprite->data3);
+            return;
+        }
+        break;
+    }
+    case 227: // Cerberus
+        seqSpawn(dudeInfo[nType].seqStartID+1, 3, nXSprite, -1);
+        return;
+    case 201:
+    case 202:
+    case 247:
+    case 248:
+        if (a3 == DAMAGE_TYPE_1 && pXSprite->medium == 0)
+        {
+            pSprite->type = 240;
+            aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+            actHealDude(pXSprite, dudeInfo[40].startHealth, dudeInfo[40].startHealth);
+            return;
+        }
+        // no break
+        fallthrough__;
+    case 251:
+        if (a3 == DAMAGE_TYPE_1 && pXSprite->medium == 0)
+        {
+            pSprite->type = 253;
+            aiNewState(pSprite, pXSprite, &beastBurnGoto);
+            actHealDude(pXSprite, dudeInfo[53].startHealth, dudeInfo[53].startHealth);
+            return;
+        }
+        // no break
+        fallthrough__;
+    case 245:
+        if (a3 == DAMAGE_TYPE_1 && pXSprite->medium == 0)
+        {
+            pSprite->type = 239;
+            aiNewState(pSprite, pXSprite, &innocentBurnGoto);
+            actHealDude(pXSprite, dudeInfo[39].startHealth, dudeInfo[39].startHealth);
+            return;
+        }
+        break;
+    }
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+    {
+        if (gPlayer[p].at2ee == pSprite->index && gPlayer[p].at1fe > 0)
+            gPlayer[p].at2ee = -1;
+    }
+    if (pSprite->type != 249)
+        trTriggerSprite(pSprite->index, pXSprite, 0);
+    pSprite->hitag |= 7;
+    if (IsPlayerSprite(pSprite2))
+    {
+        PLAYER *pPlayer = &gPlayer[pSprite2->index-kDudePlayer1];
+        if (gGameOptions.nGameType == 1)
+            pPlayer->at2c6++;
+    }
+
+    if (pXSprite->key > 0)
+        actDropObject(pSprite, 100+pXSprite->key-1);
+    if (pXSprite->dropMsg > 0)
+        actDropObject(pSprite, pXSprite->dropMsg);
+    if (pSprite->type == 201)
+    {
+        int nRand = Random(100);
+        if (nRand < 10)
+            actDropObject(pSprite, 42);
+        else if (nRand < 50)
+            actDropObject(pSprite, 69);
+    }
+    else if (pSprite->type == 202)
+    {
+        int nRand = Random(100);
+        if (nRand <= 10)
+            actDropObject(pSprite, 41);
+        else if (nRand <= 50)
+            actDropObject(pSprite, 67);
+    }
+    int nSeq;
+    switch (a3)
+    {
+    case DAMAGE_TYPE_3:
+        nSeq = 2;
+        switch (pSprite->type)
+        {
+        case kGDXDudeUniversalCultist:
+        case kGDXGenDudeBurning:
+            sfxPlayGDXGenDudeSound(pSprite, 4, pXSprite->data3);
+            break;
+        case 201:
+        case 202:
+        case 230:
+        case 239:
+        case 240:
+        case 245:
+        case 246:
+        case 247:
+        case 248:
+        case 249:
+        case 250:
+        case 252:
+            sfxPlay3DSound(pSprite, 717,-1,0);
+            break;
+        }
+        break;
+    case DAMAGE_TYPE_1:
+        nSeq = 3;
+        sfxPlay3DSound(pSprite, 351, -1, 0);
+        break;
+    case DAMAGE_TYPE_5:
+        switch (pSprite->type)
+        {
+        case 203:
+        case 205:
+            nSeq = 14;
+            break;
+        case 204:
+            nSeq = 11;
+            break;
+        default:
+            nSeq = 1;
+            break;
+        }
+        break;
+    case DAMAGE_TYPE_0:
+        switch (pSprite->type)
+        {
+        case 201:
+        case 202:
+            nSeq = 1;
+            break;
+        default:
+            nSeq = 1;
+            break;
+        }
+        break;
+    default:
+        nSeq = 1;
+        break;
+    }
+
+    if (!gSysRes.Lookup(dudeInfo[nType].seqStartID + nSeq, "SEQ"))
+    {
+        seqKill(3, nXSprite);
+        gKillMgr.AddKill(pSprite);
+        actPostSprite(pSprite->index, 1024);
+        return;
+    }
+
+    switch (pSprite->type)
+    {
+    case 203:
+        sfxPlay3DSound(pSprite, 1107+Random(2), -1, 0);
+        if (nSeq == 2)
+        {
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, nDudeToGibClient1);
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            CGibPosition gibPos(pSprite->x, pSprite->y, top);
+            CGibVelocity gibVel(xvel[pSprite->index]>>1, yvel[pSprite->index]>>1, -0xccccc);
+            GibSprite(pSprite, GIBTYPE_27, &gibPos, &gibVel);
+        }
+        else if (nSeq == 1 && Chance(0x4000))
+        {
+            seqSpawn(dudeInfo[nType].seqStartID+7, 3, nXSprite, nDudeToGibClient1);
+            evPost(pSprite->index, 3, 0, CALLBACK_ID_5);
+            sfxPlay3DSound(pSprite, 362, -1, 0);
+            pXSprite->data1 = 35;
+            pXSprite->data2 = 5;
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            CGibPosition gibPos(pSprite->x, pSprite->y, top);
+            CGibVelocity gibVel(xvel[pSprite->index] >> 1, yvel[pSprite->index] >> 1, -0x111111);
+            GibSprite(pSprite, GIBTYPE_27, &gibPos, &gibVel);
+        }
+        else if (nSeq == 14)
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        else if (nSeq == 3)
+            seqSpawn(dudeInfo[nType].seqStartID+13, 3, nXSprite, nDudeToGibClient2);
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, nDudeToGibClient1);
+        break;
+    case 201:
+    case 202:
+    case 247:
+    case 248:
+        sfxPlay3DSound(pSprite, 1018+Random(2), -1, 0);
+        if (nSeq == 3)
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, nDudeToGibClient2);
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, nDudeToGibClient1);
+        break;
+    case 240:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 718, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1018+Random(2), -1, 0);
+        a3 = DAMAGE_TYPE_3;
+        if (Chance(0x8000))
+        {
+            for (int i = 0; i < 3; i++)
+                GibSprite(pSprite, GIBTYPE_7, NULL, NULL);
+            seqSpawn(dudeInfo[nType].seqStartID+16-Random(1), 3, nXSprite, nDudeToGibClient1);
+        }
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+15, 3, nXSprite, nDudeToGibClient2);
+        break;
+    case kGDXDudeUniversalCultist:
+        sfxPlayGDXGenDudeSound(pSprite, 2, pXSprite->data3);
+        if (nSeq == 3) {
+            if (gSysRes.Lookup(pXSprite->data2 + 3, "SEQ")) seqSpawn(3 + pXSprite->data2, 3, nXSprite, nDudeToGibClient2);
+            else if (gSysRes.Lookup(pXSprite->data2 + 16, "SEQ")) seqSpawn((15 + Random(2)) + pXSprite->data2, 3, nXSprite, nDudeToGibClient2);
+            else seqSpawn(15 + pXSprite->data2, 3, nXSprite, nDudeToGibClient2);
+
+        } else {
+            seqSpawn(nSeq + pXSprite->data2, 3, nXSprite, nDudeToGibClient1);
+        }
+
+        pXSprite->txID = 0; // to avoid second trigger.
+        break;
+
+    case kGDXGenDudeBurning:
+    {
+        sfxPlayGDXGenDudeSound(pSprite, 4, pXSprite->data3);
+        a3 = DAMAGE_TYPE_3;
+
+        if (Chance(0x4000)) {
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            CGibPosition gibPos(pSprite->x, pSprite->y, top);
+            CGibVelocity gibVel(xvel[pSprite->index] >> 1, yvel[pSprite->index] >> 1, -0xccccc);
+            GibSprite(pSprite, GIBTYPE_7, &gibPos, &gibVel);
+        }
+
+        int seqId = pXSprite->data2;
+        if (gSysRes.Lookup(pXSprite->data2 + 16, "SEQ")) seqSpawn(seqId += 15 + Random(2), 3, nXSprite, nDudeToGibClient1);
+        else seqSpawn(seqId += 15, 3, nXSprite, nDudeToGibClient1);
+        break;
+    }
+    case 241:
+        if (Chance(0x8000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1109, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1107+Random(2), -1, 0);
+        a3 = DAMAGE_TYPE_3;
+        if (Chance(0x8000))
+        {
+            seqSpawn(dudeInfo[nType].seqStartID+13, 3, nXSprite, nDudeToGibClient1);
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            CGibPosition gibPos(pSprite->x, pSprite->y, top);
+            CGibVelocity gibVel(xvel[pSprite->index]>>1, yvel[pSprite->index]>>1, -0xccccc);
+            GibSprite(pSprite, GIBTYPE_27, &gibPos, &gibVel);
+        }
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+13, 3, nXSprite, nDudeToGibClient2);
+        break;
+    case 242:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1206, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1204+Random(2), -1, 0);
+        seqSpawn(dudeInfo[4].seqStartID+10, 3, nXSprite, -1);
+        break;
+    case 239:
+        a3 = DAMAGE_TYPE_3;
+        seqSpawn(dudeInfo[nType].seqStartID+7, 3, nXSprite, nDudeToGibClient1);
+        break;
+    case 204:
+        if (nSeq == 14)
+        {
+            sfxPlay3DSound(pSprite, 1206, -1, 0);
+            seqSpawn(dudeInfo[nType].seqStartID+11, 3, nXSprite, -1);
+            break;
+        }
+        sfxPlay3DSound(pSprite, 1204+Random(2), -1, 0);
+        if (nSeq == 3)
+            seqSpawn(dudeInfo[nType].seqStartID+10, 3, nXSprite, -1);
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 206:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1405, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1403+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 207:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1455, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1453+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 210:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1605, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1603+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 211:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1305, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1303+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 212:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1905, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1903+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 213:
+        if (pSprite->owner != -1)
+        {
+            spritetype *pOwner = &sprite[actSpriteOwnerToSpriteId(pSprite)];
+            gDudeExtra[pOwner->extra].at6.u1.at4--;
+        }
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1805, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1803+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 214:
+        if (pSprite->owner != -1)
+        {
+            spritetype *pOwner = &sprite[actSpriteOwnerToSpriteId(pSprite)];
+            gDudeExtra[pOwner->extra].at6.u1.at4--;
+        }
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1805, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1803+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 215:
+        if (pSprite->owner != -1)
+        {
+            spritetype *pOwner = &sprite[actSpriteOwnerToSpriteId(pSprite)];
+            gDudeExtra[pOwner->extra].at6.u1.at4--;
+        }
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1805, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1803+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 216:
+        sfxPlay3DSound(pSprite, 1850, -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 217:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1705, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1703+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 218:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 1505, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 1503+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 219:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2005, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2003+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 220:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2105, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2103+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 221:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2205, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2203+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 222:
+        if (a4 == 5)
+            sfxPlay3DSound(pSprite, 2471, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2472, -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 223:
+        if (a4 == 5)
+            sfxPlay3DSound(pSprite, 2451, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2452, -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 224:
+        sfxPlay3DSound(pSprite, 2501, -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 225:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2205, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2203+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 226:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2205, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2203+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 227:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2305, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2305+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 228:
+        if (Chance(0x4000) && nSeq == 3)
+            sfxPlay3DSound(pSprite, 2305, -1, 0);
+        else
+            sfxPlay3DSound(pSprite, 2305+Random(2), -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 229:
+        sfxPlay3DSound(pSprite, 2380, -1, 0);
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    case 252:
+        a3 = DAMAGE_TYPE_3;
+        seqSpawn(dudeInfo[nType].seqStartID+11, 3, nXSprite, nDudeToGibClient1);
+        break;
+    case 251:
+        sfxPlay3DSound(pSprite, 9000+Random(2), -1, 0);
+        if (nSeq == 3)
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, nDudeToGibClient2);
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, nDudeToGibClient1);
+        break;
+    case 253:
+        a3 = DAMAGE_TYPE_3;
+        seqSpawn(dudeInfo[nType].seqStartID+12, 3, nXSprite, nDudeToGibClient1);
+        break;
+    default:
+        seqSpawn(dudeInfo[nType].seqStartID+nSeq, 3, nXSprite, -1);
+        break;
+    }
+    
+                                    // kMaxSprites = custom dude had once life leech
+    if (pSprite->owner != -1 && pSprite->owner != kMaxSprites) {
+        //int owner = actSpriteIdToOwnerId(pSprite->xvel);
+        int owner = pSprite->owner;
+        switch (sprite[owner].lotag) {
+        case kGDXDudeUniversalCultist:
+        case kGDXGenDudeBurning:
+            if (owner != -1) gDudeExtra[sprite[owner].extra].at6.u1.at4--;
+            break;
+        default:
+            break;
+        }
+    }
+    
+    if (a3 == DAMAGE_TYPE_3)
+    {
+        DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+        for (int i = 0; i < 3; i++)
+            if (pDudeInfo->nGibType[i] > -1)
+                GibSprite(pSprite, (GIBTYPE)pDudeInfo->nGibType[i], NULL, NULL);
+        for (int i = 0; i < 4; i++)
+            fxSpawnBlood(pSprite, a4);
+    }
+    gKillMgr.AddKill(pSprite);
+    actCheckRespawn(pSprite);
+    pSprite->type = 426;
+    actPostSprite(pSprite->index, 4);
+}
+
+int actDamageSprite(int nSource, spritetype *pSprite, DAMAGE_TYPE a3, int a4)
+{
+    dassert(nSource < kMaxSprites);
+    if (pSprite->hitag&32)
+        return 0;
+    int nXSprite = pSprite->extra;
+    if (nXSprite <= 0)
+        return 0;
+    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    dassert(pXSprite->reference == pSprite->index);
+    if ((pXSprite->health == 0 && pSprite->statnum != 6) || pXSprite->locked)
+        return 0;
+    if (nSource == -1)
+        nSource = pSprite->index;
+    PLAYER *pSourcePlayer = NULL;
+    if (IsPlayerSprite(&sprite[nSource]))
+        pSourcePlayer = &gPlayer[sprite[nSource].type-kDudePlayer1];
+    switch (pSprite->statnum)
+    {
+    case 6:
+    {
+        if (pSprite->type < kDudeBase || pSprite->type >= kDudeMax)
+        {
+            sprintf(buffer, "Bad Dude Failed: initial=%d type=%d %s\n", (int)pSprite->zvel, (int)pSprite->type, (int)(pSprite->hitag&16) ? "RESPAWN" : "NORMAL");
+            ThrowError(buffer);
+        }
+        dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+        int nType = pSprite->type-kDudeBase;
+        int nDamageFactor = dudeInfo[nType].at70[a3];
+        if (!nDamageFactor)
+            return 0;
+        if (nDamageFactor != 256)
+            a4 = mulscale8(a4, nDamageFactor);
+        if (!IsPlayerSprite(pSprite))
+        {
+            if (!pXSprite->health)
+                return 0;
+            a4 = aiDamageSprite(pSprite, pXSprite, nSource, a3, a4);
+            if (!pXSprite->health)
+            {
+                if (a3 == DAMAGE_TYPE_3 && a4 < 160)
+                    a3 = DAMAGE_TYPE_0;
+                actKillDude(nSource, pSprite, a3, a4);
+            }
+        }
+        else
+        {
+            PLAYER *pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+            if (pXSprite->health > 0 || playerSeqPlaying(pPlayer, 16))
+                a4 = playerDamageSprite(nSource, pPlayer, a3, a4);
+        }
+        break;
+    }
+    case 4:
+        dassert(pSprite->type >= kThingBase && pSprite->type < kThingMax);
+        int nType = pSprite->type-kThingBase;
+        int nDamageFactor = thingInfo[nType].at17[a3];
+        if (!nDamageFactor)
+            return 0;
+        if (nDamageFactor != 256)
+            a4 = mulscale8(a4, nDamageFactor);
+        pXSprite->health = ClipLow(pXSprite->health-a4, 0);
+        if (!pXSprite->health)
+        {
+            if (pSprite->type == 431 || pSprite->type == kGDXThingCustomDudeLifeLeech)
+            {
+                GibSprite(pSprite, GIBTYPE_14, NULL, NULL);
+                pXSprite->data1 = 0;
+                pXSprite->data2 = 0;
+                pXSprite->data3 = 0;
+                pXSprite->stateTimer = 0;
+                pXSprite->data4 = 0;
+                pXSprite->isTriggered = 0;
+                pXSprite->DudeLockout = 0;
+
+                if (pSprite->owner >= 0 && sprite[pSprite->owner].type == kGDXDudeUniversalCultist)
+                    sprite[pSprite->owner].owner = kMaxSprites; // By NoOne: indicates if custom dude had life leech.
+            }
+            else if (!(pSprite->hitag&16))
+                actPropagateSpriteOwner(pSprite, &sprite[nSource]);
+            trTriggerSprite(pSprite->index, pXSprite, 0);
+            switch (pSprite->type)
+            {
+            case 416:
+            case 417:
+            case 425:
+            case 426:
+            case 427:
+                if (a3 == 3 && pSourcePlayer && gFrameClock > pSourcePlayer->at312 && Chance(0x4000))
+                {
+                    sfxPlay3DSound(pSourcePlayer->pSprite, gPlayerGibThingComments[Random(10)], 0, 2);
+                    pSourcePlayer->at312 = gFrameClock+3600;
+                }
+                break;
+            case 413:
+                seqSpawn(28, 3, pSprite->extra, -1);
+                break;
+            case 407:
+                seqSpawn(12, 3, pSprite->extra, -1);
+                GibSprite(pSprite, GIBTYPE_6, NULL, NULL);
+                break;
+            case 410:
+                seqSpawn(15, 3, pSprite->extra, -1);
+                break;
+            case 411:
+                seqSpawn(21, 3, pSprite->extra, -1);
+                GibSprite(pSprite, GIBTYPE_4, NULL, NULL);
+                break;
+            case 412:
+                switch (pXSprite->data1)
+                {
+                case -1:
+                    GibSprite(pSprite, GIBTYPE_14, NULL, NULL);
+                    sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, 312, pSprite->sectnum);
+                    actPostSprite(pSprite->index, 1024);
+                    break;
+                case 0:
+                    seqSpawn(25, 3, pSprite->extra, nTreeToGibClient);
+                    sfxPlay3DSound(pSprite, 351, -1, 0);
+                    break;
+                case 1:
+                    seqSpawn(26, 3, pSprite->extra, nTreeToGibClient);
+                    sfxPlay3DSound(pSprite, 351, -1, 0);
+                    break;
+                }
+                break;
+            case 422:
+                if (seqGetStatus(3, nXSprite) < 0)
+                    seqSpawn(19, 3, pSprite->extra, -1);
+                break;
+            }
+        }
+        break;
+    }
+    return a4>>4;
+}
+
+void actHitcodeToData(int a1, HITINFO *pHitInfo, int *a3, spritetype **a4, XSPRITE **a5, int *a6, walltype **a7, XWALL **a8, int *a9, sectortype **a10, XSECTOR **a11)
+{
+    dassert(pHitInfo != NULL);
+    int nSprite = -1;
+    spritetype *pSprite = NULL;
+    XSPRITE *pXSprite = NULL;
+    int nWall = -1;
+    walltype *pWall = NULL;
+    XWALL *pXWall = NULL;
+    int nSector = -1;
+    sectortype *pSector = NULL;
+    XSECTOR *pXSector = NULL;
+    switch (a1)
+    {
+    case 3:
+    case 5:
+        nSprite = pHitInfo->hitsprite;
+        dassert(nSprite >= 0 && nSprite < kMaxSprites);
+        pSprite = &sprite[nSprite];
+        if (pSprite->extra > 0)
+            pXSprite = &xsprite[pSprite->extra];
+        break;
+    case 0:
+    case 4:
+        nWall = pHitInfo->hitwall;
+        dassert(nWall >= 0 && nWall < kMaxWalls);
+        pWall = &wall[nWall];
+        if (pWall->extra > 0)
+            pXWall = &xwall[pWall->extra];
+        break;
+    case 1:
+    case 2:
+    case 6:
+        nSector = pHitInfo->hitsect;
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        pSector = &sector[nSector];
+        if (pSector->extra > 0)
+            pXSector = &xsector[pSector->extra];
+        break;
+    }
+    if (a3)
+        *a3 = nSprite;
+    if (a4)
+        *a4 = pSprite;
+    if (a5)
+        *a5 = pXSprite;
+    if (a6)
+        *a6 = nWall;
+    if (a7)
+        *a7 = pWall;
+    if (a8)
+        *a8 = pXWall;
+    if (a9)
+        *a9 = nSector;
+    if (a10)
+        *a10 = pSector;
+    if (a11)
+        *a11 = pXSector;
+}
+
+void actImpactMissile(spritetype *pMissile, int a2)
+{
+    int nXMissile = pMissile->extra;
+    dassert(nXMissile > 0 && nXMissile < kMaxXSprites);
+    XSPRITE *pXMissile = &xsprite[pMissile->extra];
+    int nSpriteHit = -1;
+    spritetype *pSpriteHit = NULL;
+    XSPRITE *pXSpriteHit = NULL;
+    int nWallHit = -1;
+    walltype *pWallHit = NULL;
+    XWALL *pXWallHit = NULL;
+    int nSectorHit = -1;
+    sectortype *pSectorHit = NULL;
+    XSECTOR *pXSectorHit = NULL;
+    actHitcodeToData(a2, &gHitInfo, &nSpriteHit, &pSpriteHit, &pXSpriteHit, &nWallHit, &pWallHit, &pXWallHit, &nSectorHit, &pSectorHit, &pXSectorHit);
+    THINGINFO *pThingInfo = NULL;
+    DUDEINFO *pDudeInfo = NULL;
+    if (a2 == 3 && pSpriteHit)
+    {
+        if (pSpriteHit->statnum == 4)
+        {
+            dassert(pXSpriteHit != NULL);
+            pThingInfo = &thingInfo[pSpriteHit->type-kThingBase];
+        }
+        else if (pSpriteHit->statnum == 6)
+        {
+            dassert(pXSpriteHit != NULL);
+            pDudeInfo = &dudeInfo[pSpriteHit->type-kDudeBase];
+        }
+    }
+    switch (pMissile->type)
+    {
+    case 315:
+        if (a2 == 3 && pSpriteHit && (pThingInfo || pDudeInfo))
+        {
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            DAMAGE_TYPE rand1 = (DAMAGE_TYPE)Random(7);
+            int rand2 = (7+Random(7))<<4;
+            actDamageSprite(nOwner, pSpriteHit, rand1, rand2);
+            if ((pThingInfo && pThingInfo->at17[DAMAGE_TYPE_1] != 0) || (pDudeInfo && pDudeInfo->at70[DAMAGE_TYPE_1] != 0))
+                actBurnSprite(pMissile->owner, pXSpriteHit, 360);
+        }
+        if (pMissile->extra > 0)
+        {
+            actPostSprite(pMissile->index, 0);
+            if (pMissile->ang == 1024)
+                sfxPlay3DSound(pMissile, 307, -1, 0);
+            pMissile->type = 0;
+            seqSpawn(9, 3, pMissile->extra, -1);
+        }
+        else
+        {
+            actPostSprite(pMissile->index, 1024);
+        }
+        break;
+    case 302:
+        sub_51340(pMissile, a2);
+        if ((a2 == 0 || a2 == 4) && pWallHit)
+        {
+            spritetype *pFX = gFX.fxSpawn(FX_52, pMissile->sectnum, pMissile->x, pMissile->y, pMissile->z, 0);
+            if (pFX)
+                pFX->ang = (GetWallAngle(nWallHit)+512)&2047;
+        }
+        GibSprite(pMissile, GIBTYPE_24, NULL, NULL);
+        actPostSprite(pMissile->index, 1024);
+        break;
+    case 309:
+        seqKill(3, nXMissile);
+        if (a2 == 3 && pSpriteHit && (pThingInfo || pDudeInfo))
+        {
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            int nDamage = (15+Random(7))<<4;
+            actDamageSprite(nOwner, pSpriteHit, DAMAGE_TYPE_2, nDamage);
+        }
+        actPostSprite(pMissile->index, 1024);
+        break;
+    case 311:
+        sfxKill3DSound(pMissile, -1, -1);
+        sfxPlay3DSound(pMissile->x, pMissile->y, pMissile->z, 306, pMissile->sectnum);
+        GibSprite(pMissile, GIBTYPE_6, NULL, NULL);
+        if (a2 == 3 && pSpriteHit && (pThingInfo || pDudeInfo))
+        {
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            int nDamage = (25+Random(20))<<4;
+            actDamageSprite(nOwner, pSpriteHit, DAMAGE_TYPE_5, nDamage);
+        }
+        actPostSprite(pMissile->index, 1024);
+        break;
+    case 316:
+    case 317:
+        sfxKill3DSound(pMissile, -1, -1);
+        sfxPlay3DSound(pMissile->x, pMissile->y, pMissile->z, 306, pMissile->sectnum);
+        if (a2 == 3 && pSpriteHit && (pThingInfo || pDudeInfo))
+        {
+            if (pDudeInfo)
+            {
+            }
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            int nDmgMul = 3;
+            if (pMissile->type == 317)
+                nDmgMul = 6;
+            int nDamage = (nDmgMul+Random(nDmgMul))<<4;
+            actDamageSprite(nOwner, pSpriteHit, DAMAGE_TYPE_5, nDamage);
+        }
+        actPostSprite(pMissile->index, 1024);
+        break;
+    case 305:
+    case 312:
+        if (a2 == 3 && pSpriteHit && (pThingInfo || pDudeInfo))
+        {
+            if (pThingInfo && pSpriteHit->type == 400 && pXSpriteHit->burnTime == 0)
+                evPost(nSpriteHit, 3, 0, CALLBACK_ID_0);
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            int nDamage = (50+Random(50))<<4;
+            actDamageSprite(nOwner, pSpriteHit, DAMAGE_TYPE_2, nDamage);
+        }
+        actExplodeSprite(pMissile);
+        break;
+    case 303:
+        sfxKill3DSound(pMissile, -1, -1);
+        actExplodeSprite(pMissile);
+        break;
+    case 301:
+        sfxKill3DSound(pMissile, -1, -1);
+        if (a2 == 3 && pSpriteHit)
+        {
+            if (pThingInfo || pDudeInfo)
+            {
+                int nOwner = actSpriteOwnerToSpriteId(pMissile);
+                if ((pThingInfo && pThingInfo->at17[DAMAGE_TYPE_1] != 0) || (pDudeInfo && pDudeInfo->at70[DAMAGE_TYPE_1] != 0))
+                {
+                    if (pThingInfo && pSpriteHit->type == 400 && pXSpriteHit->burnTime == 0)
+                        evPost(nSpriteHit, 3, 0, CALLBACK_ID_0);
+                    actBurnSprite(pMissile->owner, pXSpriteHit, 480);
+                    sub_2A620(nOwner, pMissile->x, pMissile->y, pMissile->z, pMissile->sectnum, 16, 20, 10, DAMAGE_TYPE_2, 6, 480, 0, 0);
+                }
+                else
+                {
+                    int nDamage = (20+Random(10))<<4;
+                    actDamageSprite(nOwner, pSpriteHit, DAMAGE_TYPE_2, nDamage);
+                }
+                if (surfType[pSpriteHit->picnum] == 4)
+                {
+                    pMissile->picnum = 2123;
+                    pXMissile->target = nSpriteHit;
+                    pXMissile->targetZ = pMissile->z-pSpriteHit->z;
+                    pXMissile->goalAng = getangle(pMissile->x-pSpriteHit->x, pMissile->y-pSpriteHit->y)-pSpriteHit->ang;
+                    pXMissile->state = 1;
+                    actPostSprite(pMissile->index, 14);
+                    pMissile->cstat &= ~257;
+                    break;
+                }
+            }
+        }
+        GibSprite(pMissile, GIBTYPE_17, NULL, NULL);
+        actPostSprite(pMissile->index, 1024);
+        break;
+    case 304:
+    case 308:
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            if (pObject->extra > 0)
+            {
+                XSPRITE *pXObject = &xsprite[pObject->extra];
+                if ((pObject->statnum == 4 || pObject->statnum == 6) && pXObject->burnTime == 0)
+                    evPost(nObject, 3, 0, CALLBACK_ID_0);
+                int nOwner = actSpriteOwnerToSpriteId(pMissile);
+                actBurnSprite(pMissile->owner, pXObject, (4+gGameOptions.nDifficulty)<<2);
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_1, 8);
+            }
+        }
+        break;
+    case 313:
+        actExplodeSprite(pMissile);
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            if (pObject->extra > 0)
+            {
+                XSPRITE *pXObject = &xsprite[pObject->extra];
+                if ((pObject->statnum == 4 || pObject->statnum == 6) && pXObject->burnTime == 0)
+                    evPost(nObject, 3, 0, CALLBACK_ID_0);
+                int nOwner = actSpriteOwnerToSpriteId(pMissile);
+                actBurnSprite(pMissile->owner, pXObject, (4+gGameOptions.nDifficulty)<<2);
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_1, 8);
+                int nDamage = (25+Random(10))<<4;
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_2, nDamage);
+            }
+        }
+        actExplodeSprite(pMissile);
+        break;
+    case 314:
+        actExplodeSprite(pMissile);
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            if (pObject->extra > 0)
+            {
+                XSPRITE *pXObject = &xsprite[pObject->extra];
+                if ((pObject->statnum == 4 || pObject->statnum == 6) && pXObject->burnTime == 0)
+                    evPost(nObject, 3, 0, CALLBACK_ID_0);
+                int nOwner = actSpriteOwnerToSpriteId(pMissile);
+                actBurnSprite(pMissile->owner, pXObject, 32);
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_5, 12);
+                int nDamage = (25+Random(10))<<4;
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_2, nDamage);
+            }
+        }
+        actExplodeSprite(pMissile);
+        break;
+    case 307:
+        sfxKill3DSound(pMissile, -1, -1);
+        sfxPlay3DSound(pMissile->x, pMissile->y, pMissile->z, 522, pMissile->sectnum);
+        actPostSprite(pMissile->index, 15);
+        seqSpawn(20, 3, pMissile->extra, -1);
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            if (pObject->statnum == 6)
+            {
+                int nOwner = actSpriteOwnerToSpriteId(pMissile);
+                int nDamage = (25+Random(10))<<4;
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_5, nDamage);
+            }
+        }
+        break;
+    case 300:
+        actPostSprite(pMissile->index, 15);
+        pMissile->cstat &= ~16;
+        pMissile->type = 0;
+        seqSpawn(20, 3, pMissile->extra, -1);
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            if (pObject->statnum == 6)
+            {
+                int nOwner = actSpriteOwnerToSpriteId(pMissile);
+                int nDamage = (10+Random(10))<<4;
+                actDamageSprite(nOwner, pObject, DAMAGE_TYPE_5, nDamage);
+                spritetype *pOwner = &sprite[nOwner];
+                XSPRITE *pXOwner = &xsprite[pOwner->extra];
+                int nType = pOwner->type-kDudeBase;
+                if (pXOwner->health > 0)
+                    actHealDude(pXOwner, 10, dudeInfo[nType].startHealth);
+            }
+        }
+        break;
+    case 306:
+        sfxKill3DSound(pMissile, -1, -1);
+        sfxPlay3DSound(pMissile->x, pMissile->y, pMissile->z, 518, pMissile->sectnum);
+        GibSprite(pMissile, (a2 == 2) ? GIBTYPE_23 : GIBTYPE_22, NULL, NULL);
+        evKill(pMissile->index, 3);
+        seqKill(3, nXMissile);
+        actPostSprite(pMissile->index, 1024);
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            int nDamage = (15+Random(10))<<4;
+            actDamageSprite(nOwner, pObject, DAMAGE_TYPE_6, nDamage);
+        }
+        break;
+    case 310:
+    default:
+        seqKill(3, nXMissile);
+        actPostSprite(pMissile->index, 1024);
+        if (a2 == 3)
+        {
+            int nObject = gHitInfo.hitsprite;
+            dassert(nObject >= 0 && nObject < kMaxSprites);
+            spritetype *pObject = &sprite[nObject];
+            int nOwner = actSpriteOwnerToSpriteId(pMissile);
+            int nDamage = (10+Random(10))<<4;
+            actDamageSprite(nOwner, pObject, DAMAGE_TYPE_0, nDamage);
+        }
+        break;
+    }
+    pMissile->cstat &= ~257;
+}
+
+void actKickObject(spritetype *pSprite1, spritetype *pSprite2)
+{
+    int nSprite1 = pSprite1->index;
+    int nSprite2 = pSprite2->index;
+    int nSpeed = ClipLow(approxDist(xvel[nSprite1], yvel[nSprite1])*2, 0xaaaaa);
+    xvel[nSprite2] = mulscale30(nSpeed, Cos(pSprite1->ang+Random2(85)));
+    yvel[nSprite2] = mulscale30(nSpeed, Sin(pSprite1->ang+Random2(85)));
+    zvel[nSprite2] = mulscale(nSpeed, -0x2000, 14);
+    pSprite2->hitag = 7;
+}
+
+void actTouchFloor(spritetype *pSprite, int nSector)
+{
+    dassert(pSprite != NULL);
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    sectortype * pSector = &sector[nSector];
+    XSECTOR * pXSector = NULL;
+    if (pSector->extra > 0)
+        pXSector = &xsector[pSector->extra];
+
+    if (pXSector && (pSector->lotag == 618 || pXSector->damageType > 0))
+    {
+        DAMAGE_TYPE nDamageType;
+        if (pSector->lotag == 618)
+            nDamageType = (DAMAGE_TYPE)ClipRange(pXSector->damageType, DAMAGE_TYPE_0, DAMAGE_TYPE_6);
+        else
+            nDamageType = (DAMAGE_TYPE)ClipRange(pXSector->damageType - 1, DAMAGE_TYPE_0, DAMAGE_TYPE_6);
+        int nDamage;
+        if (pXSector->data)
+            nDamage = ClipRange(pXSector->data, 0, 1000);
+        else
+            nDamage = 1000;
+        actDamageSprite(pSprite->index, pSprite, nDamageType, scale(4, nDamage, 120) << 4);
+    }
+    if (tileGetSurfType(nSector + 0x4000) == 14)
+    {
+        actDamageSprite(pSprite->index, pSprite, DAMAGE_TYPE_1, 16);
+        sfxPlay3DSound(pSprite, 352, 5, 2);
+    }
+}
+
+void ProcessTouchObjects(spritetype *pSprite, int nXSprite)
+{
+    int nSprite = pSprite->index;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    SPRITEHIT *pSpriteHit = &gSpriteHit[nXSprite];
+    PLAYER *pPlayer = NULL;
+    if (IsPlayerSprite(pSprite))
+        pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+    int nHitSprite = pSpriteHit->ceilhit & 0x1fff;
+    switch (pSpriteHit->ceilhit&0xe000)
+    {
+    case 0x8000:
+        break;
+    case 0xc000:
+        if (sprite[nHitSprite].extra > 0)
+        {
+            spritetype *pSprite2 = &sprite[nHitSprite];
+            XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+            if ((pSprite2->statnum == 4 || pSprite2->statnum == 6) && (xvel[nSprite] != 0 || yvel[nSprite] != 0 || zvel[nSprite] != 0))
+            {
+                if (pSprite2->statnum == 4)
+                {
+                    int nType = pSprite2->type-kThingBase;
+                    THINGINFO *pThingInfo = &thingInfo[nType];
+                    if (pThingInfo->at5&1)
+                        pSprite2->hitag |= 1;
+                    if (pThingInfo->at5&2)
+                        pSprite2->hitag |= 4;
+                    // Inlined ?
+                    xvel[pSprite2->index] += mulscale(4, pSprite2->x-sprite[nSprite].x, 2);
+                    yvel[pSprite2->index] += mulscale(4, pSprite2->y-sprite[nSprite].y, 2);
+                }
+                else
+                {
+                    pSprite2->hitag |= 5;
+                    xvel[pSprite2->index] += mulscale(4, pSprite2->x-sprite[nSprite].x, 2);
+                    yvel[pSprite2->index] += mulscale(4, pSprite2->y-sprite[nSprite].y, 2);
+                    
+                    // by NoOne: add size shroom abilities
+                    if ((IsPlayerSprite(pSprite) && isShrinked(pSprite)) || (IsPlayerSprite(pSprite2) && isGrown(pSprite2))) {
+
+                        int mass1 = dudeInfo[pSprite2->type - kDudeBase].mass;
+                        int mass2 = dudeInfo[pSprite->type - kDudeBase].mass;
+                        switch (pSprite->type) {
+                            case kGDXDudeUniversalCultist:
+                            case kGDXGenDudeBurning:
+                                mass2 = getDudeMassBySpriteSize(pSprite);
+                                break;
+                        }
+                        if (mass1 > mass2) {
+                            int dmg = abs((mass1 - mass2) * (pSprite2->clipdist - pSprite->clipdist));
+                            if (IsDudeSprite(pSprite2)) {
+                                if (dmg > 0)
+                                    actDamageSprite(pSprite2->xvel, pSprite, (Chance(0x2000)) ? DAMAGE_TYPE_0 : (Chance(0x4000)) ? DAMAGE_TYPE_3 : DAMAGE_TYPE_2, dmg);
+
+                                if (Chance(0x0200))
+                                    actKickObject(pSprite2, pSprite);
+                            }
+                        }
+                    }
+                    
+                    if (!IsPlayerSprite(pSprite) || gPlayer[pSprite->type - kDudePlayer1].at31a == 0) {
+                        switch (pSprite2->type) {
+                            case 229:
+                                actDamageSprite(pSprite2->index, pSprite, DAMAGE_TYPE_3, pXSprite->health << 2);
+                                break;
+                            case kGDXDudeUniversalCultist:
+                            case kGDXGenDudeBurning:
+                                int dmg = (getDudeMassBySpriteSize(pSprite2) - getDudeMassBySpriteSize(pSprite)) + pSprite2->clipdist;
+                                if (dmg > 0) {
+                                    if (IsPlayerSprite(pSprite) && powerupCheck(&gPlayer[pSprite->type - kDudePlayer1],15) > 0)
+                                        actDamageSprite(pSprite2->xvel, pSprite, DAMAGE_TYPE_3, dmg);
+                                    else
+                                        actDamageSprite(pSprite2->xvel, pSprite, DAMAGE_TYPE_0, dmg);
+                                }
+
+                                if (!IsPlayerSprite(pSprite) && pSprite2->extra >= 0 && !isActive(pSprite2->xvel))
+                                    aiActivateDude(pSprite2, &xsprite[pSprite2->extra]);
+                                break;
+
+                        }
+                            
+                    }
+                }
+            }
+            if (pSprite2->type == 454)
+            {
+                if (pXSprite2->state)
+                {
+                    pXSprite2->data1 = 1;
+                    pXSprite2->data2 = ClipHigh(pXSprite2->data2+8, 600);
+                    actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_2, 16);
+                }
+                else
+                    actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_2, 1);
+            }
+        }
+        break;
+    }
+    nHitSprite = pSpriteHit->hit & 0x1fff;
+    switch (pSpriteHit->hit&0xe000)
+    {
+    case 0x8000:
+        break;
+    case 0xc000:
+        if (sprite[nHitSprite].extra > 0)
+        {
+            spritetype *pSprite2 = &sprite[nHitSprite];
+            //XSPRITE *pXSprite2 = &Xsprite[pSprite2->extra];
+            
+            // by NoOne: add size shroom abilities
+            if ((IsPlayerSprite(pSprite2) && isShrinked(pSprite2)) || (IsPlayerSprite(pSprite) && isGrown(pSprite))) {
+                if (xvel[pSprite->xvel] != 0 && IsDudeSprite(pSprite2)) {
+                    int mass1 = dudeInfo[pSprite->type - kDudeBase].mass;
+                    int mass2 = dudeInfo[pSprite2->type - kDudeBase].mass;
+                    switch (pSprite2->type) {
+                        case kGDXDudeUniversalCultist:
+                        case kGDXGenDudeBurning:
+                            mass2 = getDudeMassBySpriteSize(pSprite2);
+                            break;
+                    }
+                    if (mass1 > mass2) {
+                        actKickObject(pSprite, pSprite2);
+                        sfxPlay3DSound(pSprite, 357, -1, 1);
+                        int dmg = (mass1 - mass2) + abs(xvel[pSprite->xvel] >> 16);
+                        if (dmg > 0)
+                            actDamageSprite(nSprite, pSprite2, (Chance(0x2000)) ? DAMAGE_TYPE_0 : DAMAGE_TYPE_2, dmg);
+                    }
+                }
+            }
+
+            switch (pSprite->type) {
+                case kGDXDudeUniversalCultist:
+                case kGDXGenDudeBurning:
+                {
+                    if (IsDudeSprite(pSprite2) && !IsPlayerSprite(pSprite2)) {
+                        int mass1 = getDudeMassBySpriteSize(pSprite);
+                        int mass2 = getDudeMassBySpriteSize(pSprite2);
+
+                        if (mass1 > mass2) {
+                            if ((pXSprite->target == pSprite2->xvel && !dudeIsMelee(pXSprite) && Chance(0x0500)) || pXSprite->target != pSprite2->xvel) 
+                                actKickObject(pSprite, pSprite2);
+                            if (pSprite2->extra >= 0 && !isActive(pSprite2->xvel))
+                                aiActivateDude(pSprite2, &xsprite[pSprite2->extra]);
+                        }
+                    }
+                    break;
+                }
+            }
+            
+            switch (pSprite2->type)
+            {
+            case 415:
+                actKickObject(pSprite, pSprite2);
+                break;
+            case 427:
+                sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, 357, pSprite->sectnum);
+                actKickObject(pSprite, pSprite2);
+                actDamageSprite(-1, pSprite2, DAMAGE_TYPE_0, 80);
+                break;
+            case 239:
+            case 240:
+            case 241:
+            case 242:
+                // This does not make sense
+                pXSprite->burnTime = ClipLow(pXSprite->burnTime-4, 0);
+                actDamageSprite(actOwnerIdToSpriteId(pXSprite->burnSource), pSprite, DAMAGE_TYPE_1, 8);
+                break;
+            }
+        }
+        break;
+    }
+    nHitSprite = pSpriteHit->florhit & 0x1fff;
+    switch (pSpriteHit->florhit&0xe000)
+    {
+    case 0x8000:
+        break;
+    case 0x4000:
+        actTouchFloor(pSprite, nHitSprite);
+        break;
+    case 0xc000:
+        if (sprite[nHitSprite].extra > 0)
+        {
+            spritetype *pSprite2 = &sprite[nHitSprite];
+            XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+            
+            // by NoOne: add size shroom abilities
+            if ((IsPlayerSprite(pSprite2) && isShrinked(pSprite2)) || (IsPlayerSprite(pSprite) && isGrown(pSprite))) {
+                
+                int mass1 = dudeInfo[pSprite->type - kDudeBase].mass;
+                int mass2 = dudeInfo[pSprite2->type - kDudeBase].mass;
+                switch (pSprite2->type) {
+                    case kGDXDudeUniversalCultist:
+                    case kGDXGenDudeBurning:
+                        mass2 = getDudeMassBySpriteSize(pSprite2);
+                        break;
+                }
+                if (mass1 > mass2 && IsDudeSprite(pSprite2)) {
+                    if ((IsPlayerSprite(pSprite2) && Chance(0x500)) || !IsPlayerSprite(pSprite2))
+                        actKickObject(pSprite, pSprite2);
+
+                    int dmg = (mass1 - mass2) + pSprite->clipdist;
+                    if (dmg > 0)
+                        actDamageSprite(nSprite, pSprite2, (Chance(0x2000)) ? DAMAGE_TYPE_0 : DAMAGE_TYPE_2, dmg);
+                }
+            }
+
+            switch (pSprite->type) {
+                case kGDXDudeUniversalCultist:
+                case kGDXGenDudeBurning:
+                {
+                    if (IsDudeSprite(pSprite2) && !IsPlayerSprite(pSprite2)) {
+                        int mass1 = getDudeMassBySpriteSize(pSprite);
+                        int mass2 = getDudeMassBySpriteSize(pSprite2);
+
+                        if (mass1 > mass2) {
+                            if (Chance((pXSprite->target == pSprite2->xvel) ? 0x1000 : 0x2000)) actKickObject(pSprite, pSprite2);
+                            if (pSprite2->extra >= 0 && !isActive(pSprite2->xvel))
+                                aiActivateDude(pSprite2, &xsprite[pSprite2->extra]);
+                        }
+                    }
+                    break;
+                }
+            }
+
+            
+            switch (pSprite2->type)
+            {
+            case 415:
+                if (pPlayer)
+                {
+                    if (pPlayer->at30e > gFrameClock)
+                        return;
+                    pPlayer->at30e = gFrameClock+60;
+                }
+                actKickObject(pSprite, pSprite2);
+                sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, 357, pSprite->sectnum);
+                sfxPlay3DSound(pSprite, 374, 0, 0);
+                break;
+            case 427:
+                if (pPlayer)
+                {
+                    if (pPlayer->at30e > gFrameClock)
+                        return;
+                    pPlayer->at30e = gFrameClock+60;
+                }
+                actKickObject(pSprite, pSprite2);
+                sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, 357, pSprite->sectnum);
+                actDamageSprite(-1, pSprite2, DAMAGE_TYPE_0, 80);
+                break;
+            case 454:
+                if (pXSprite2->state)
+                {
+                    pXSprite2->data1 = 1;
+                    pXSprite2->data2 = ClipHigh(pXSprite2->data2+8, 600);
+                    actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_2, 16);
+                }
+                else
+                    actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_2, 1);
+                break;
+            case 201:
+            case 202:
+            case 203:
+            case 204:
+            case 205:
+            case 206:
+            case 207:
+            case 210:
+            case 211:
+            case 212:
+            case 213:
+            case 214:
+            case 215:
+            case 217:
+            case 219:
+            case 220:
+            case 221:
+            case 222:
+            case 223:
+            case 224:
+            case 225:
+            case 226:
+            case 227:
+            case 228:
+            case 229:
+            case 231:
+            case 232:
+            case 233:
+            case 234:
+            case 235:
+            case 236:
+            case 237:
+            case 238:
+                if (pPlayer && !isShrinked(pSprite))
+                    actDamageSprite(nSprite, pSprite2,DAMAGE_TYPE_2, 8);
+                break;
+            }
+        }
+        break;
+    }
+}
+
+void actAirDrag(spritetype *pSprite, int a2)
+{
+    int vbp = 0;
+    int v4 = 0;
+    int nSector = pSprite->sectnum;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    if (nXSector > 0)
+    {
+        dassert(nXSector < kMaxXSectors);
+        XSECTOR *pXSector = &xsector[nXSector];
+        if (pXSector->windVel && (pXSector->windAlways || pXSector->busy))
+        {
+            int vcx = pXSector->windVel<<12;
+            if (!pXSector->windAlways && pXSector->busy)
+                vcx = mulscale16(vcx, pXSector->busy);
+            vbp = mulscale30(vcx, Cos(pXSector->windAng));
+            v4 = mulscale30(vcx, Sin(pXSector->windAng));
+        }
+    }
+    xvel[pSprite->index] += mulscale16(vbp-xvel[pSprite->index], a2);
+    yvel[pSprite->index] += mulscale16(v4-yvel[pSprite->index], a2);
+    zvel[pSprite->index] -= mulscale16(zvel[pSprite->index], a2);
+}
+
+int MoveThing(spritetype *pSprite)
+{
+    int nXSprite = pSprite->extra;
+    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pSprite->index;
+    int v8 = 0;
+    dassert(pSprite->type >= kThingBase && pSprite->type < kThingMax);
+    THINGINFO *pThingInfo = &thingInfo[pSprite->type-kThingBase];
+    int nSector = pSprite->sectnum;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    if (xvel[nSprite] || yvel[nSprite])
+    {
+        short bakCstat = pSprite->cstat;
+        pSprite->cstat &= ~257;
+        v8 = gSpriteHit[nXSprite].hit = ClipMove((int*)&pSprite->x, (int*)&pSprite->y, (int*)&pSprite->z, &nSector, xvel[nSprite]>>12, yvel[nSprite]>>12, pSprite->clipdist<<2, (pSprite->z-top)/4, (bottom-pSprite->z)/4, CLIPMASK0);
+        pSprite->cstat = bakCstat;
+        dassert(nSector >= 0);
+        if (pSprite->sectnum != nSector)
+        {
+            dassert(nSector >= 0 && nSector < kMaxSectors);
+            ChangeSpriteSect(nSprite, nSector);
+        }
+        if ((gSpriteHit[nXSprite].hit&0xe000) == 0x8000)
+        {
+            int nHitWall = gSpriteHit[nXSprite].hit&0x1fff;
+            actWallBounceVector((int*)&xvel[nSprite], (int*)&yvel[nSprite], nHitWall, pThingInfo->at7);
+            switch (pSprite->type)
+            {
+            case 427:
+                sfxPlay3DSound(pSprite, 607, 0, 0);
+                actDamageSprite(-1, pSprite, DAMAGE_TYPE_0, 80);
+                break;
+            case 415:
+                sfxPlay3DSound(pSprite, 374, 0, 0);
+                break;
+            }
+        }
+    }
+    else
+    {
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        FindSector(pSprite->x, pSprite->y, pSprite->z, &nSector);
+    }
+    if (zvel[nSprite])
+        pSprite->z += zvel[nSprite]>>8;
+    int ceilZ, ceilHit, floorZ, floorHit;
+    GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist<<2, CLIPMASK0);
+    GetSpriteExtents(pSprite, &top, &bottom);
+    if ((pSprite->hitag & 2) && bottom < floorZ)
+    {
+        pSprite->z += 455;
+        zvel[nSprite] += 58254;
+        if (pSprite->type == 427)
+        {
+            spritetype *pFX = gFX.fxSpawn(FX_27, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+            if (pFX)
+            {
+                int v34 = (gFrameClock*3)&2047;
+                int v30 = (gFrameClock*5)&2047;
+                int vbx = (gFrameClock*11)&2047;
+                int v2c = 0x44444;
+                int v28 = 0;
+                int v24 = 0;
+                RotateVector(&v2c,&v28,vbx);
+                RotateVector(&v2c,&v24,v30);
+                RotateVector(&v28,&v24,v34);
+                xvel[pFX->index] = xvel[pSprite->index]+v2c;
+                yvel[pFX->index] = yvel[pSprite->index]+v28;
+                zvel[pFX->index] = zvel[pSprite->index]+v24;
+            }
+        }
+    }
+    if (CheckLink(pSprite))
+        GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist<<2, CLIPMASK0);
+    GetSpriteExtents(pSprite, &top, &bottom);
+    if (bottom >= floorZ)
+    {
+        actTouchFloor(pSprite, pSprite->sectnum);
+        gSpriteHit[nXSprite].florhit = floorHit;
+        pSprite->z += floorZ-bottom;
+        int v20 = zvel[nSprite]-velFloor[pSprite->sectnum];
+        if (v20 > 0)
+        {
+            pSprite->hitag |= 4;
+            int vax = actFloorBounceVector((int*)&xvel[nSprite], (int*)&yvel[nSprite], (int*)&v20, pSprite->sectnum, pThingInfo->at7);
+            int nDamage = mulscale(vax, vax, 30)-pThingInfo->atb;
+            if (nDamage > 0)
+                actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_0, nDamage);
+            zvel[nSprite] = v20;
+            if (velFloor[pSprite->sectnum] == 0 && klabs(zvel[nSprite]) < 0x10000)
+            {
+                zvel[nSprite] = 0;
+                pSprite->hitag &= ~4;
+            }
+            switch (pSprite->type)
+            {
+            case 428:
+                if (zvel[nSprite] == 0 || Chance(0xA000))
+                    sub_2AA94(pSprite, pXSprite);
+                break;
+            case 427:
+                if (klabs(zvel[nSprite]) > 0x80000)
+                {
+                    sfxPlay3DSound(pSprite, 607, 0, 0);
+                    actDamageSprite(-1, pSprite, DAMAGE_TYPE_0, 80);
+                }
+                break;
+            case 415:
+                if (klabs(zvel[nSprite]) > 0x80000)
+                    sfxPlay3DSound(pSprite, 374, 0, 0);
+                break;
+            }
+            v8 = 0x4000|nSector;
+        }
+        else if (zvel[nSprite] == 0)
+            pSprite->hitag &= ~4;
+    }
+    else
+    {
+        gSpriteHit[nXSprite].florhit = 0;
+        if (pSprite->hitag&2)
+            pSprite->hitag |= 4;
+    }
+    if (top <= ceilZ)
+    {
+        gSpriteHit[nXSprite].ceilhit = ceilHit;
+        pSprite->z += ClipLow(ceilZ-top, 0);
+        if (zvel[nSprite] < 0)
+        {
+            xvel[nSprite] = mulscale16(xvel[nSprite], 0xc000);
+            yvel[nSprite] = mulscale16(yvel[nSprite], 0xc000);
+            zvel[nSprite] = mulscale16(-zvel[nSprite], 0x4000);
+            switch (pSprite->type)
+            {
+            case 427:
+                if (klabs(zvel[nSprite]) > 0x80000)
+                {
+                    sfxPlay3DSound(pSprite, 607, 0, 0);
+                    actDamageSprite(-1, pSprite, DAMAGE_TYPE_0, 80);
+                }
+                break;
+            case 415:
+                if (klabs(zvel[nSprite]) > 0x80000)
+                    sfxPlay3DSound(pSprite, 374, 0, 0);
+                break;
+            }
+        }
+    }
+    else
+        gSpriteHit[nXSprite].ceilhit = 0;
+    if (bottom >= floorZ)
+    {
+        int nVel = approxDist(xvel[nSprite], yvel[nSprite]);
+        int nVelClipped = ClipHigh(nVel, 0x11111);
+        if ((floorHit & 0xe000) == 0xc000)
+        {
+            int nHitSprite = floorHit & 0x1fff;
+            if ((sprite[nHitSprite].cstat & 0x30) == 0)
+            {
+                xvel[nSprite] += mulscale(4, pSprite->x - sprite[nHitSprite].x, 2);
+                yvel[nSprite] += mulscale(4, pSprite->y - sprite[nHitSprite].y, 2);
+                v8 = gSpriteHit[nXSprite].hit;
+            }
+        }
+        if (nVel > 0)
+        {
+            int t = divscale16(nVelClipped, nVel);
+            xvel[nSprite] -= mulscale16(t, xvel[nSprite]);
+            yvel[nSprite] -= mulscale16(t, yvel[nSprite]);
+        }
+    }
+    if (xvel[nSprite] || yvel[nSprite])
+        pSprite->ang = getangle(xvel[nSprite], yvel[nSprite]);
+    return v8;
+}
+
+void MoveDude(spritetype *pSprite)
+{
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pSprite->index;
+    PLAYER *pPlayer = NULL;
+    if (IsPlayerSprite(pSprite))
+        pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    int bz = (bottom-pSprite->z)/4;
+    int tz = (pSprite->z-top)/4;
+    int wd = pSprite->clipdist<<2;
+    int nSector = pSprite->sectnum;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    if (xvel[nSprite] || yvel[nSprite])
+    {
+        if (pPlayer && gNoClip)
+        {
+            pSprite->x += xvel[nSprite]>>12;
+            pSprite->y += yvel[nSprite]>>12;
+            if (!FindSector(pSprite->x, pSprite->y, &nSector))
+                nSector = pSprite->sectnum;
+        }
+        else
+        {
+            short bakCstat = pSprite->cstat;
+            pSprite->cstat &= ~257;
+            gSpriteHit[nXSprite].hit = ClipMove((int*)&pSprite->x, (int*)&pSprite->y, (int*)&pSprite->z, &nSector, xvel[nSprite]>>12, yvel[nSprite]>>12, wd, tz, bz, 0x13001);
+            if (nSector == -1)
+            {
+                nSector = pSprite->sectnum;
+                if (pSprite->statnum == 6 || pSprite->statnum == 4)
+                    actDamageSprite(pSprite->index, pSprite, DAMAGE_TYPE_0, 1000<<4);
+            }
+            if (sector[nSector].lotag >= 612 && sector[nSector].lotag <= 617)
+            {
+                short nSector2 = nSector;
+                if (pushmove_old(&pSprite->x, &pSprite->y, &pSprite->z, &nSector2, wd, tz, bz, CLIPMASK0) == -1)
+                    actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_0, 1000 << 4);
+                if (nSector2 != -1)
+                    nSector = nSector2;
+            }
+            dassert(nSector >= 0);
+            pSprite->cstat = bakCstat;
+        }
+        switch (gSpriteHit[nXSprite].hit&0xe000)
+        {
+        case 0xc000:
+        {
+            int nHitSprite = gSpriteHit[nXSprite].hit&0x1fff;
+            spritetype *pHitSprite = &sprite[nHitSprite];
+            XSPRITE *pHitXSprite = NULL;
+            // Should be pHitSprite here
+            if (pSprite->extra > 0)
+                pHitXSprite = &xsprite[pHitSprite->extra];
+            int nOwner = actSpriteOwnerToSpriteId(pHitSprite);
+            if (pHitSprite->statnum == 5 && !(pHitSprite->hitag&32) && pSprite->index != nOwner)
+            {
+                HITINFO hitInfo = gHitInfo;
+                gHitInfo.hitsprite = nSprite;
+                actImpactMissile(pHitSprite, 3);
+                gHitInfo = hitInfo;
+            }
+                                                  // by NoOne: this is why touch for things never worked; they always ON
+            if (pHitXSprite && pHitXSprite->Touch /*&& !pHitXSprite->state*/ && !pHitXSprite->isTriggered) {
+                if (!pHitXSprite->DudeLockout || IsPlayerSprite(pSprite)) // allow dudeLockout for Touch flag
+                    trTriggerSprite(nHitSprite, pHitXSprite, 33);
+            } if (pDudeInfo->lockOut && pHitXSprite && pHitXSprite->Push && !pHitXSprite->key && !pHitXSprite->DudeLockout && !pHitXSprite->state && !pHitXSprite->busy && !pPlayer)
+                trTriggerSprite(nHitSprite, pHitXSprite, 30);
+            break;
+        }
+        case 0x8000:
+        {
+            int nHitWall = gSpriteHit[nXSprite].hit&0x1fff;
+            walltype *pHitWall = &wall[nHitWall];
+            XWALL *pHitXWall = NULL;
+            if (pHitWall->extra > 0)
+                pHitXWall = &xwall[pHitWall->extra];
+            if (pDudeInfo->lockOut && pHitXWall && pHitXWall->triggerPush && !pHitXWall->key && !pHitXWall->dudeLockout && !pHitXWall->state && !pHitXWall->busy && !pPlayer)
+                trTriggerWall(nHitWall, pHitXWall, 50);
+            if (pHitWall->nextsector != -1)
+            {
+                sectortype *pHitSector = &sector[pHitWall->nextsector];
+                XSECTOR *pHitXSector = NULL;
+                if (pHitSector->extra > 0)
+                    pHitXSector = &xsector[pHitSector->extra];
+                if (pDudeInfo->lockOut && pHitXSector && pHitXSector->Wallpush && !pHitXSector->Key && !pHitXSector->at37_7 && !pHitXSector->state && !pHitXSector->busy && !pPlayer)
+                    trTriggerSector(pHitWall->nextsector, pHitXSector, 40);
+                if (top < pHitSector->ceilingz || bottom > pHitSector->floorz)
+                {
+                    // ???
+                }
+            }
+            actWallBounceVector((int*)&xvel[nSprite], (int*)&yvel[nSprite], nHitWall, 0);
+            break;
+        }
+        }
+    }
+    else
+    {
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        FindSector(pSprite->x, pSprite->y, pSprite->z, &nSector);
+    }
+    if (pSprite->sectnum != nSector)
+    {
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Exit && (pPlayer || !pXSector->at37_7))
+            trTriggerSector(pSprite->sectnum, pXSector, 43);
+        ChangeSpriteSect(nSprite, nSector);
+        nXSector = sector[nSector].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Enter && (pPlayer || !pXSector->at37_7))
+        {
+            if (sector[nSector].lotag == 604)
+                pXSector->data = pPlayer ? nSprite : -1;
+            trTriggerSector(nSector, pXSector, 42);
+        }
+        nSector = pSprite->sectnum;
+    }
+    char bUnderwater = 0;
+    char bDepth = 0;
+    if (sector[nSector].extra > 0)
+    {
+        XSECTOR *pXSector = &xsector[sector[nSector].extra];
+        if (pXSector->Underwater)
+            bUnderwater = 1;
+        if (pXSector->Depth)
+            bDepth = 1;
+    }
+    int nUpperLink = gUpperLink[nSector];
+    int nLowerLink = gLowerLink[nSector];
+    if (nUpperLink >= 0 && (sprite[nUpperLink].type == 9 || sprite[nUpperLink].type == 13))
+        bDepth = 1;
+    if (nLowerLink >= 0 && (sprite[nLowerLink].type == 10 || sprite[nLowerLink].type == 14))
+        bDepth = 1;
+    if (pPlayer)
+        wd += 16;
+    if (zvel[nSprite])
+        pSprite->z += zvel[nSprite]>>8;
+    int ceilZ, ceilHit, floorZ, floorHit;
+    GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, wd, 0x13001);
+    GetSpriteExtents(pSprite, &top, &bottom);
+    if (pSprite->hitag & 2)
+    {
+        int vc = 58254;
+        if (bDepth)
+        {
+            if (bUnderwater)
+            {
+                int cz = getceilzofslope(nSector, pSprite->x, pSprite->y);
+                if (cz > top)
+                    vc += ((bottom-cz)*-80099) / (bottom-top);
+                else
+                    vc = 0;
+            }
+            else
+            {
+                int fz = getflorzofslope(nSector, pSprite->x, pSprite->y);
+                if (fz < bottom)
+                    vc += ((bottom-fz)*-80099) / (bottom-top);
+            }
+        }
+        else
+        {
+            if (bUnderwater)
+                vc = 0;
+            else if (bottom >= floorZ)
+                vc =  0;
+        }
+        if (vc)
+        {
+            pSprite->z += ((vc*4)/2)>>8;
+            zvel[nSprite] += vc;
+        }
+    }
+    if (pPlayer && zvel[nSprite] > 0x155555 && !pPlayer->at31b && pXSprite->height > 0)
+    {
+        pPlayer->at31b = 1;
+        sfxPlay3DSound(pSprite, 719, 0, 0);
+    }
+    int nLink = CheckLink(pSprite);
+    if (nLink)
+    {
+        GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, wd, 0x13001);
+        if (pPlayer)
+            playerResetInertia(pPlayer);
+        switch (nLink)
+        {
+        case 12:
+            if (pPlayer == gView)
+                SetBitString(gotpic, sector[pSprite->sectnum].floorpicnum);
+            break;
+        case 11:
+            if (pPlayer == gView)
+                SetBitString(gotpic, sector[pSprite->sectnum].ceilingpicnum);
+            break;
+        case 10:
+        case 14:
+            pXSprite->medium = 0;
+            if (pPlayer)
+            {
+                pPlayer->at2f = 0;
+                pPlayer->at302 = 0;
+                if (!pPlayer->at31c && pPlayer->atc.buttonFlags.jump)
+                {
+                    zvel[nSprite] = -0x6aaaa;
+                    pPlayer->at31c = 1;
+                }
+                sfxPlay3DSound(pSprite, 721, -1, 0);
+            }
+            else
+            {
+                switch (pSprite->type)
+                {
+                case 201:
+                case 202:
+                    aiNewState(pSprite, pXSprite, &cultistGoto);
+                    break;
+                case 217:
+                    aiNewState(pSprite, pXSprite, &gillBeastGoto);
+                    pSprite->hitag |= 6;
+                    break;
+                case 218:
+                    actKillDude(pSprite->index, pSprite, DAMAGE_TYPE_0, 1000<<4);
+                    break;
+                }
+            }
+            break;
+        // By NoOne: part of "change of global view palette for stacks" feature
+        case kMarkerUpWater:
+        case kMarkerUpGoo:
+        {
+            pXSprite->medium = nLink == kMarkerUpGoo ? 2 : 1;
+
+            if (pPlayer)
+            {
+                // look for palette in data2 of marker. If value <= 0, use default ones.
+                pPlayer->nWaterPal = 0;
+                int nXUpper = sprite[gUpperLink[nSector]].extra;
+                if (nXUpper >= 0)
+                    pPlayer->nWaterPal = xsprite[nXUpper].data2;
+
+                pPlayer->at2f = 1;
+                pXSprite->burnTime = 0;
+                pPlayer->at302 = klabs(zvel[nSprite]) >> 12;
+                evPost(nSprite, 3, 0, CALLBACK_ID_10);
+                sfxPlay3DSound(pSprite, 720, -1, 0);
+            }
+            else
+            {
+                switch (pSprite->type)
+                {
+                case 201:
+                case 202:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+                    break;
+                case 240:
+                {
+                    // There is no difference between water and goo except following chance:
+                    if (Chance(nLink == kMarkerUpGoo ? 0x400 : 0xa00))
+                    {
+                        pSprite->type = 201;
+                        pXSprite->burnTime = 0;
+                        evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                        sfxPlay3DSound(pSprite, 720, -1, 0);
+                        aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+                    }
+                    else
+                    {
+                        pSprite->type = 202;
+                        pXSprite->burnTime = 0;
+                        evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                        sfxPlay3DSound(pSprite, 720, -1, 0);
+                        aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+                    }
+                    break;
+                }
+                case 203:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &zombieAGoto);
+                    break;
+                case 204:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &zombieFGoto);
+                    break;
+                case 217:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &gillBeastSwimGoto);
+                    pSprite->hitag &= ~6;
+                    break;
+                case 206:
+                case 211:
+                case 213:
+                case 214:
+                case 215:
+                case 219:
+                case 220:
+                case 239:
+                    actKillDude(pSprite->index, pSprite, DAMAGE_TYPE_0, 1000 << 4);
+                    break;
+                }
+            }
+            break;
+        }
+        /*case 13:
+            pXSprite->medium = 2;
+            if (pPlayer)
+            {
+                pPlayer->changeTargetKin = 1;
+                pXSprite->burnTime = 0;
+                pPlayer->at302 = klabs(zvel[nSprite])>>12;
+                evPost(nSprite, 3, 0, CALLBACK_ID_10);
+                sfxPlay3DSound(pSprite, 720, -1, 0);
+            }
+            else
+            {
+                switch (pSprite->type)
+                {
+                case 201:
+                case 202:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+                    break;
+                case 240:
+                    if (Chance(0x400))
+                    {
+                        pSprite->type = 201;
+                        pXSprite->burnTime = 0;
+                        evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                        sfxPlay3DSound(pSprite, 720, -1, 0);
+                        aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+                    }
+                    else
+                    {
+                        pSprite->type = 202;
+                        pXSprite->burnTime = 0;
+                        evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                        sfxPlay3DSound(pSprite, 720, -1, 0);
+                        aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+                    }
+                    break;
+                case 203:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &zombieAGoto);
+                    break;
+                case 204:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &zombieFGoto);
+                    break;
+                case 217:
+                    pXSprite->burnTime = 0;
+                    evPost(nSprite, 3, 0, CALLBACK_ID_11);
+                    sfxPlay3DSound(pSprite, 720, -1, 0);
+                    aiNewState(pSprite, pXSprite, &gillBeastSwimGoto);
+                    pSprite->hitag &= ~6;
+                    break;
+                case 206:
+                case 211:
+                case 213:
+                case 214:
+                case 215:
+                case 219:
+                case 220:
+                case 239:
+                    actKillDude(pSprite->index, pSprite, DAMAGE_TYPE_0, 1000<<4);
+                    break;
+                }
+            }
+            break;*/
+        }
+    }
+    GetSpriteExtents(pSprite, &top, &bottom);
+    if (pPlayer && bottom >= floorZ)
+    {
+        int floorZ2 = floorZ;
+        int floorHit2 = floorHit;
+        GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist<<2, 0x13001);
+        if (bottom <= floorZ && pSprite->z - floorZ2 < bz)
+        {
+            floorZ = floorZ2;
+            floorHit = floorHit2;
+        }
+    }
+    if (floorZ <= bottom)
+    {
+        gSpriteHit[nXSprite].florhit = floorHit;
+        pSprite->z += floorZ-bottom;
+        int v30 = zvel[nSprite]-velFloor[pSprite->sectnum];
+        if (v30 > 0)
+        {
+            int vax = actFloorBounceVector((int*)&xvel[nSprite], (int*)&yvel[nSprite], (int*)&v30, pSprite->sectnum, 0);
+            int nDamage = mulscale(vax, vax, 30);
+            if (pPlayer)
+            {
+                pPlayer->at31b = 0;
+                if (nDamage > (15<<4) && (pSprite->hitag&4))
+                    playerLandingSound(pPlayer);
+                if (nDamage > (30<<4))
+                    sfxPlay3DSound(pSprite, 701, 0, 0);
+            }
+            nDamage -= 100<<4;
+            if (nDamage > 0)
+                actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_0, nDamage);
+            zvel[nSprite] = v30;
+            if (klabs(zvel[nSprite]) < 0x10000)
+            {
+                zvel[nSprite] = velFloor[pSprite->sectnum];
+                pSprite->hitag &= ~4;
+            }
+            else
+                pSprite->hitag |= 4;
+            switch (tileGetSurfType(floorHit))
+            {
+            case 5:
+                gFX.fxSpawn(FX_9, pSprite->sectnum, pSprite->x, pSprite->y, floorZ, 0);
+                break;
+            case 14:
+            {
+                spritetype *pFX = gFX.fxSpawn(FX_10, pSprite->sectnum, pSprite->x, pSprite->y, floorZ, 0);
+                if (pFX)
+                {
+                    for (int i = 0; i < 7; i++)
+                    {
+                        spritetype *pFX2 = gFX.fxSpawn(FX_14, pFX->sectnum, pFX->x, pFX->y, pFX->z, 0);
+                        if (pFX2)
+                        {
+                            xvel[pFX2->index] = Random2(0x6aaaa);
+                            yvel[pFX2->index] = Random2(0x6aaaa);
+                            zvel[pFX2->index] = -Random(0xd5555);
+                        }
+                    }
+                }
+                break;
+            }
+            }
+        }
+        else if (zvel[nSprite] == 0)
+            pSprite->hitag &= ~4;
+    }
+    else
+    {
+        gSpriteHit[nXSprite].florhit = 0;
+        if (pSprite->hitag&2)
+            pSprite->hitag |= 4;
+    }
+    if (top <= ceilZ)
+    {
+        gSpriteHit[nXSprite].ceilhit = ceilHit;
+        pSprite->z += ClipLow(ceilZ-top, 0);
+        if (zvel[nSprite] <= 0 && (pSprite->hitag&4))
+            zvel[nSprite] = mulscale16(-zvel[nSprite], 0x2000);
+    }
+    else
+        gSpriteHit[nXSprite].ceilhit = 0;
+    GetSpriteExtents(pSprite,&top,&bottom);
+
+    pXSprite->height = ClipLow(floorZ-bottom, 0)>>8;
+    if (xvel[nSprite] || yvel[nSprite])
+    {
+        if ((floorHit & 0xe000) == 0xc000)
+        {
+            int nHitSprite = floorHit & 0x1fff;
+            if ((sprite[nHitSprite].cstat & 0x30) == 0)
+            {
+                xvel[nSprite] += mulscale(4, pSprite->x - sprite[nHitSprite].x, 2);
+                yvel[nSprite] += mulscale(4, pSprite->y - sprite[nHitSprite].y, 2);
+                return;
+            }
+        }
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0 && xsector[nXSector].Underwater)
+            return;
+        if (pXSprite->height >= 0x100)
+            return;
+        int nDrag = gDudeDrag;
+        if (pXSprite->height > 0)
+            nDrag -= scale(gDudeDrag, pXSprite->height, 0x100);
+        xvel[nSprite] -= mulscale16r(xvel[nSprite], nDrag);
+        yvel[nSprite] -= mulscale16r(yvel[nSprite], nDrag);
+
+        if (approxDist(xvel[nSprite], yvel[nSprite]) < 0x1000)
+            xvel[nSprite] = yvel[nSprite] = 0;
+    }
+}
+
+int MoveMissile(spritetype *pSprite)
+{
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int vdi = -1;
+    spritetype *pOwner = NULL;
+    int bakCstat = 0;
+    if (pSprite->owner >= 0)
+    {
+        int nOwner = actSpriteOwnerToSpriteId(pSprite);
+        pOwner = &sprite[nOwner];
+        if (IsDudeSprite(pOwner))
+        {
+            bakCstat = pOwner->cstat;
+            pOwner->cstat &= ~257;
+        }
+        else
+            pOwner = NULL;
+    }
+    gHitInfo.hitsect = -1;
+    gHitInfo.hitwall = -1;
+    gHitInfo.hitsprite = -1;
+    if (pSprite->type == 304)
+        actAirDrag(pSprite, 0x1000);
+    int nSprite = pSprite->index;
+    if (pXSprite->target != -1 && (xvel[nSprite] || yvel[nSprite] || zvel[nSprite]))
+    {
+        spritetype *pTarget = &sprite[pXSprite->target];
+        XSPRITE *pXTarget;
+        if (pTarget->extra > 0)
+            pXTarget = &xsprite[pTarget->extra];
+        else
+            pXTarget = NULL;
+        if (pTarget->statnum == 6 && pXTarget && pXTarget->health > 0)
+        {
+            int nTargetAngle = getangle(-(pTarget->y-pSprite->y), pTarget->x-pSprite->x);
+            int UNUSED(nAngle) = getangle(xvel[nSprite]>>12,yvel[nSprite]>>12);
+            int vx = missileInfo[pSprite->type-300].at2;
+            int vy = 0;
+            RotatePoint(&vx, &vy, (nTargetAngle+1536)&2047, 0, 0);
+            xvel[nSprite] = vx;
+            yvel[nSprite] = vy;
+            int dx = pTarget->x-pSprite->x;
+            int dy = pTarget->y-pSprite->y;
+            int dz = pTarget->z-pSprite->z;
+            // Inlined
+            int vax = dz/10;
+            if (pTarget->z < pSprite->z)
+                vax = -vax;
+            zvel[nSprite] += vax;
+            ksqrt(dx*dx+dy*dy+dz*dz);
+        }
+    }
+    int vx = xvel[nSprite]>>12;
+    int vy = yvel[nSprite]>>12;
+    int vz = zvel[nSprite]>>8;
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    int i = 1;
+    while (1)
+    {
+        int x = pSprite->x;
+        int y = pSprite->y;
+        int z = pSprite->z;
+        int nSector2 = pSprite->sectnum;
+        clipmoveboxtracenum = 1;
+        int vdx = ClipMove(&x, &y, &z, &nSector2, vx, vy, pSprite->clipdist<<2, (z-top)/4, (bottom-z)/4, CLIPMASK0);
+        clipmoveboxtracenum = 3;
+        short nSector = nSector2;
+        if (nSector2 < 0)
+        {
+            vdi = -1;
+            break;
+        }
+        if (vdx)
+        {
+            int nHitSprite = vdx & 0x1fff;
+            if ((vdx&0xe000) == 0xc000)
+            {
+                gHitInfo.hitsprite = nHitSprite;
+                vdi = 3;
+            }
+            else if ((vdx & 0xe000) == 0x8000)
+            {
+                gHitInfo.hitwall = nHitSprite;
+                if (wall[nHitSprite].nextsector == -1)
+                    vdi = 0;
+                else
+                {
+                    int32_t fz, cz;
+                    getzsofslope(wall[nHitSprite].nextsector, x, y, &cz, &fz);
+                    if (z <= cz || z >= fz)
+                        vdi = 0;
+                    else
+                        vdi = 4;
+                }
+            }
+        }
+        if (vdi == 4)
+        {
+            walltype *pWall = &wall[gHitInfo.hitwall];
+            if (pWall->extra > 0)
+            {
+                XWALL *pXWall = &xwall[pWall->extra];
+                if (pXWall->triggerVector)
+                {
+                    trTriggerWall(gHitInfo.hitwall, pXWall, 51);
+                    if (!(pWall->cstat&64))
+                    {
+                        vdi = -1;
+                        if (i-- > 0)
+                            continue;
+                        vdi = 0;
+                        break;
+                    }
+                }
+            }
+        }
+        if (vdi >= 0 && vdi != 3)
+        {
+            int nAngle = getangle(xvel[nSprite], yvel[nSprite]);
+            x -= mulscale30(Cos(nAngle), 16);
+            y -= mulscale30(Sin(nAngle), 16);
+            int nVel = approxDist(xvel[nSprite], yvel[nSprite]);
+            vz -= scale(0x100, zvel[nSprite], nVel);
+            updatesector(x, y, &nSector);
+            nSector2 = nSector;
+        }
+        int ceilZ, ceilHit, floorZ, floorHit;
+        GetZRangeAtXYZ(x, y, z, nSector2, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist<<2, CLIPMASK0);
+        GetSpriteExtents(pSprite, &top, &bottom);
+        top += vz;
+        bottom += vz;
+        if (bottom >= floorZ)
+        {
+            gSpriteHit[nXSprite].florhit = floorHit;
+            vz += floorZ-bottom;
+            vdi = 2;
+        }
+        if (top <= ceilZ)
+        {
+            gSpriteHit[nXSprite].ceilhit = ceilHit;
+            vz += ClipLow(ceilZ-top, 0);
+            vdi = 1;
+        }
+        pSprite->x = x;
+        pSprite->y = y;
+        pSprite->z = z+vz;
+        updatesector(x, y, &nSector);
+        if (nSector >= 0 && nSector != pSprite->sectnum)
+        {
+            dassert(nSector >= 0 && nSector < kMaxSectors);
+            ChangeSpriteSect(nSprite, nSector);
+        }
+        CheckLink(pSprite);
+        gHitInfo.hitsect = pSprite->sectnum;
+        gHitInfo.hitx = pSprite->x;
+        gHitInfo.hity = pSprite->y;
+        gHitInfo.hitz = pSprite->z;
+        break;
+    }
+    if (pOwner)
+        pOwner->cstat = bakCstat;
+    return vdi;
+}
+
+void actExplodeSprite(spritetype *pSprite)
+{
+    int nXSprite = pSprite->extra;
+    if (nXSprite <= 0 || nXSprite >= kMaxXSprites)
+        return;
+    if (pSprite->statnum == 2)
+        return;
+    sfxKill3DSound(pSprite, -1, -1);
+    evKill(pSprite->index, 3);
+    int nType;
+    switch (pSprite->type)
+    {
+    case 312:
+        nType = 7;
+        seqSpawn(4, 3, nXSprite, -1);
+        if (Chance(0x8000))
+            pSprite->cstat |= 4;
+        sfxPlay3DSound(pSprite, 303, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    case 303:
+        nType = 3;
+        seqSpawn(9, 3, nXSprite, -1);
+        if (Chance(0x8000))
+            pSprite->cstat |= 4;
+        sfxPlay3DSound(pSprite, 306, 24+(pSprite->index&3), 1);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    case 313:
+    case 314:
+        nType = 3;
+        seqSpawn(5, 3, nXSprite, -1);
+        sfxPlay3DSound(pSprite, 304, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    case 418:
+        nType = 0;
+        if (gSpriteHit[nXSprite].florhit == 0)
+            seqSpawn(4,3,nXSprite,-1);
+        else
+            seqSpawn(3,3,nXSprite,-1);
+        sfxPlay3DSound(pSprite, 303, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    case 401:
+    case 402:
+    case 419:
+    case kGDXThingTNTProx:
+        nType = 1;
+        if (gSpriteHit[nXSprite].florhit == 0)
+            seqSpawn(4,3,nXSprite,-1);
+        else
+            seqSpawn(3,3,nXSprite,-1);
+        sfxPlay3DSound(pSprite, 304, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    case 420:
+        nType = 4;
+        seqSpawn(5, 3, nXSprite, -1);
+        sfxPlay3DSound(pSprite, 307, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    case 400:
+    {
+        spritetype *pSprite2 = actSpawnSprite(pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0, 1);
+        pSprite2->owner = pSprite->owner;
+        if (actCheckRespawn(pSprite))
+        {
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            pXSprite->state = 1;
+            pXSprite->health = thingInfo[0].at0<<4;
+        }
+        else
+            actPostSprite(pSprite->index, 1024);
+        nType = 2;
+        nXSprite = pSprite2->extra;
+        seqSpawn(4, 3, nXSprite, -1);
+        sfxPlay3DSound(pSprite2, 305, -1, 0);
+        GibSprite(pSprite2, GIBTYPE_14, NULL, NULL);
+        pSprite = pSprite2;
+        break;
+    }
+    case 459:	// By NoOne: allow to customize hidden exploder thing
+	{
+		// Defaults for exploder
+		nType = 1; int nSnd = 304; int nSeq = 4;
+
+		// Temp variables for override via data fields
+		int tSnd = 0; int tSeq = 0;
+
+
+		XSPRITE *pXSPrite = &xsprite[nXSprite];
+		nType = pXSPrite->data1;  // Explosion type
+		tSeq = pXSPrite->data2; // SEQ id
+		tSnd = pXSPrite->data3; // Sound Id
+
+		if (nType <= 1 || nType > kExplodeMax) { nType = 1; nSeq = 4; nSnd = 304; }
+		else if (nType == 2) { nSeq = 4; nSnd = 305; }
+		else if (nType == 3) { nSeq = 9; nSnd = 307; }
+		else if (nType == 4) { nSeq = 5; nSnd = 307; }
+		else if (nType <= 6) { nSeq = 4; nSnd = 303; }
+		else if (nType == 7) { nSeq = 4; nSnd = 303; }
+		else if (nType == 8) { nType = 0; nSeq = 3; nSnd = 303; }
+
+		// Override previous sound and seq assigns
+		if (tSeq > 0) nSeq = tSeq;
+		if (tSnd > 0) nSnd = tSnd;
+
+		//if (kExist(pXSPrite->data2, seq)) // GDX method to check if file exist in RFF
+		seqSpawn(nSeq, 3, nXSprite, -1);
+
+		sfxPlay3DSound(pSprite, nSnd, -1, 0);
+	}
+        break;
+    case 429:
+        nType = 3;
+        seqSpawn(9, 3, nXSprite, -1);
+        sfxPlay3DSound(pSprite, 307, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        sub_746D4(pSprite, 240);
+        break;
+    default:
+        nType = 1;
+        seqSpawn(4, 3, nXSprite, -1);
+        if (Chance(0x8000))
+            pSprite->cstat |= 4;
+        sfxPlay3DSound(pSprite, 303, -1, 0);
+        GibSprite(pSprite, GIBTYPE_5, NULL, NULL);
+        break;
+    }
+    int nSprite = pSprite->index;
+    xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+    actPostSprite(nSprite, 2);
+    pSprite->xrepeat = pSprite->yrepeat = explodeInfo[nType].at0;
+    pSprite->hitag &= ~3;
+    pSprite->type = nType;
+    EXPLOSION *pExplodeInfo = &explodeInfo[nType];
+    xsprite[nXSprite].target = 0;
+    xsprite[nXSprite].data1 = pExplodeInfo->atf;
+    xsprite[nXSprite].data2 = pExplodeInfo->at13;
+    xsprite[nXSprite].data3 = pExplodeInfo->at17;
+}
+
+void actActivateGibObject(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int vdx = ClipRange(pXSprite->data1, 0, 31);
+    int vc = ClipRange(pXSprite->data2, 0, 31);
+    int v4 = ClipRange(pXSprite->data3, 0, 31);
+    int vbp = pXSprite->data4;
+    int v8 = pXSprite->dropMsg;
+    if (vdx > 0)
+        GibSprite(pSprite, (GIBTYPE)(vdx-1), NULL, NULL);
+    if (vc > 0)
+        GibSprite(pSprite, (GIBTYPE)(vc-1), NULL, NULL);
+    if (v4 > 0 && pXSprite->burnTime > 0)
+        GibSprite(pSprite, (GIBTYPE)(v4-1), NULL, NULL);
+    if (vbp > 0)
+        sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, vbp, pSprite->sectnum);
+    if (v8 > 0)
+        actDropObject(pSprite, v8);
+    if (!(pSprite->cstat&32768) && !(pSprite->hitag&16))
+        actPostSprite(pSprite->index, 1024);
+}
+
+bool IsUnderWater(spritetype *pSprite)
+{
+    int nSector = pSprite->sectnum;
+    int nXSector = sector[nSector].extra;
+    if (nXSector > 0 && nXSector < kMaxXSectors)
+        if (xsector[nXSector].Underwater)
+            return 1;
+    return 0;
+}
+
+void MakeSplash(spritetype *pSprite, XSPRITE *pXSprite);
+
+void actProcessSprites(void)
+{
+    int nSprite;
+    int nNextSprite;
+    for (nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag&32)
+            continue;
+        int nXSprite = pSprite->extra;
+        if (nXSprite > 0)
+        {
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (pSprite->type == 425 || pSprite->type == 426 || pSprite->type == 427)
+                if (pXSprite->locked && gFrameClock >= pXSprite->targetX)
+                    pXSprite->locked = 0;
+            if (pXSprite->burnTime > 0)
+            {
+                pXSprite->burnTime = ClipLow(pXSprite->burnTime-4,0);
+                actDamageSprite(actOwnerIdToSpriteId(pXSprite->burnSource), pSprite, DAMAGE_TYPE_1, 8);
+            }
+
+                                   // by NoOne: make Sight flag work and don't process sight flag for things which is locked or triggered
+            if (pXSprite->Sight && pXSprite->locked != 1 && pXSprite->isTriggered != true) {
+                for (int i = connecthead; i >= 0; i = connectpoint2[i]) {
+                    PLAYER* pPlayer = &gPlayer[i]; int z = pPlayer->at6f - pPlayer->pSprite->z;
+                    int hitCode = VectorScan(pPlayer->pSprite, 0, z, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 512000, 1);
+                    if (hitCode != 3 || gHitInfo.hitsprite != pSprite->xvel) continue;
+                    trTriggerSprite(nSprite, pXSprite, 34);
+                    pXSprite->locked = 1; // lock it once triggered, so it can be unlocked again
+              
+                    break;
+                }
+            }
+                                       // by NoOne: don't process locked or 1-shot things for proximity
+            if (pXSprite->Proximity && pXSprite->locked != 1 && pXSprite->isTriggered != true) {
+                if (pSprite->type == 431) pXSprite->target = -1;
+                for (int nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nNextSprite)
+                {
+                    
+                    nNextSprite = nextspritestat[nSprite2];
+                    spritetype *pSprite2 = &sprite[nSprite2];
+                    if (pSprite2->hitag&32) continue;
+                    XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+                    if ((unsigned int)pXSprite2->health > 0)
+                    {
+                        int proxyDist = 96;
+                        if (pSprite->type == kGDXThingCustomDudeLifeLeech) proxyDist = 512;
+                        else if (pSprite->type == 431 && pXSprite->target == -1)
+                        {
+                            int nOwner = actOwnerIdToSpriteId(pSprite->owner);
+                            spritetype *pOwner = &sprite[nOwner];
+                            PLAYER *pPlayer = &gPlayer[pOwner->type-kDudePlayer1];
+                            PLAYER *pPlayer2 = NULL;
+                            if (IsPlayerSprite(pSprite2))
+                                pPlayer2 = &gPlayer[pSprite2->type-kDudePlayer1];
+                            if (nSprite2 == nOwner || pSprite2->type == 205 || pSprite2->type == 220 || pSprite2->type == 219)
+                                continue;
+                            if (gGameOptions.nGameType == 3 && pPlayer2 && pPlayer->at2ea == pPlayer2->at2ea)
+                                continue;
+                            if (gGameOptions.nGameType == 1 && pPlayer2)
+                                continue;
+                            proxyDist = 512;
+                        }
+                        if (CheckProximity(pSprite2, pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, proxyDist)) {
+                            
+                            switch (pSprite->type) {
+                                case kGDXThingTNTProx:
+                                    if (!IsPlayerSprite(pSprite2)) continue;
+                                    pSprite->pal = 0;
+                                    break;
+                                case 431:
+                                    if (!Chance(0x4000) && nNextSprite >= 0) continue;
+                                    if (pSprite2->cstat & 0x10001) pXSprite->target = pSprite2->index;
+                                    else continue;
+                                    break;
+                                case kGDXThingCustomDudeLifeLeech:
+                                    if (pXSprite->target != pSprite2->xvel) continue;
+                                    break;
+                            }
+                            if (pSprite->owner == -1) actPropagateSpriteOwner(pSprite, pSprite2);
+                            trTriggerSprite(nSprite, pXSprite, 35);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    for (nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        int nSector = pSprite->sectnum;
+        int nXSprite = pSprite->extra;
+        dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+        int nXSector = sector[nSector].extra;
+        XSECTOR *pXSector = NULL;
+        if (nXSector > 0)
+        {
+            dassert(nXSector > 0 && nXSector < kMaxXSectors);
+            dassert(xsector[nXSector].reference == nSector);
+            pXSector = &xsector[nXSector];
+        }
+        if (pXSector && pXSector->panVel && (pXSector->panAlways || pXSector->state || pXSector->busy))
+        {
+            int nType = pSprite->type - kThingBase;
+            THINGINFO *pThingInfo = &thingInfo[nType];
+            if (pThingInfo->at5 & 1)
+                pSprite->hitag |= 1;
+            if (pThingInfo->at5 & 2)
+                pSprite->hitag |= 4;
+        }
+        if (pSprite->hitag&3)
+        {
+            viewBackupSpriteLoc(nSprite, pSprite);
+            if (pXSector && pXSector->panVel)
+            {
+                int top, bottom;
+                GetSpriteExtents(pSprite, &top, &bottom);
+                if (getflorzofslope(nSector, pSprite->x, pSprite->y) <= bottom)
+                {
+                    int angle = pXSector->panAngle;
+                    int speed = 0;
+                    if (pXSector->panAlways || pXSector->state || pXSector->busy)
+                    {
+                        speed = pXSector->panVel << 9;
+                        if (!pXSector->panAlways && pXSector->busy)
+                            speed = mulscale16(speed, pXSector->busy);
+                    }
+                    if (sector[nSector].floorstat&64)
+                        angle = (angle+GetWallAngle(sector[nSector].wallptr)+512)&2047;
+                    int dx = mulscale30(speed, Cos(angle));
+                    int dy = mulscale30(speed, Sin(angle));
+                    xvel[nSprite] += dx;
+                    yvel[nSprite] += dy;
+                }
+            }
+            actAirDrag(pSprite, 128);
+            if ((pSprite->index>>8) == (gFrame&15) && (pSprite->hitag&2))
+                pSprite->hitag |= 4;
+            if ((pSprite->hitag&4) || xvel[nSprite] || yvel[nSprite] || zvel[nSprite] ||
+                velFloor[pSprite->sectnum] || velCeil[pSprite->sectnum])
+            {
+                int hit = MoveThing(pSprite);
+                if (hit)
+                {
+                    int nXSprite = pSprite->extra;
+                    if (nXSprite)
+                    {
+                        XSPRITE *pXSprite = &xsprite[nXSprite];
+                        if (pXSprite->Impact)
+                            trTriggerSprite(nSprite, pXSprite, 0);
+                        switch (pSprite->type)
+                        {
+                        case 423:
+                        case 424:
+                            MakeSplash(pSprite, pXSprite);
+                            break;
+                        case kGDXThingThrowableRock:
+                            seqSpawn(24, 3, nXSprite, -1);
+                            if ((hit & 0xe000) == 0xc000)
+                            {
+                                pSprite->xrepeat = 32;
+                                pSprite->yrepeat = 32;
+                                int nObject = hit & 0x1fff;
+                                dassert(nObject >= 0 && nObject < kMaxSprites);
+                                spritetype * pObject = &sprite[nObject];
+                                actDamageSprite(actSpriteOwnerToSpriteId(pSprite), pObject, DAMAGE_TYPE_0, pXSprite->data1);
+                            }
+                            break;
+                        case 421:
+                            seqSpawn(24, 3, nXSprite, -1);
+                            if ((hit&0xe000) == 0xc000)
+                            {
+                                int nObject = hit & 0x1fff;
+                                dassert(nObject >= 0 && nObject < kMaxSprites);
+                                spritetype *pObject = &sprite[nObject];
+                                actDamageSprite(actSpriteOwnerToSpriteId(pSprite), pObject, DAMAGE_TYPE_0, 12);
+                            }
+                            break;
+                        case 430:
+                            if ((hit&0xe000) == 0x4000)
+                            {
+                                sub_2A620(actSpriteOwnerToSpriteId(pSprite), pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, 200, 1, 20, DAMAGE_TYPE_3, 6, 0, 0, 0);
+                                evPost(pSprite->index, 3, 0, CALLBACK_ID_19);
+                            }
+                            else
+                            {
+                                int nObject = hit & 0x1fff;
+                                if ((hit&0xe000) != 0xc000 && (nObject < 0 || nObject >= 4096))
+                                    break;
+                                dassert(nObject >= 0 && nObject < kMaxSprites);
+                                spritetype *pObject = &sprite[nObject];
+                                actDamageSprite(actSpriteOwnerToSpriteId(pSprite), pObject, DAMAGE_TYPE_0, 12);
+                                evPost(pSprite->index, 3, 0, CALLBACK_ID_19);
+                            }
+                            break;
+                        case 429:
+                        {
+                            int nObject = hit & 0x1fff;
+                            if ((hit&0xe000) != 0xc000 && (nObject < 0 || nObject >= 4096))
+                                break;
+                            dassert(nObject >= 0 && nObject < kMaxSprites);
+                            int UNUSED(nOwner) = actSpriteOwnerToSpriteId(pSprite);
+                            actExplodeSprite(pSprite);
+                            break;
+                        }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    for (nSprite = headspritestat[5]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        viewBackupSpriteLoc(nSprite, pSprite);
+        int hit = MoveMissile(pSprite);
+        if (hit >= 0)
+            actImpactMissile(pSprite, hit);
+    }
+    for (nSprite = headspritestat[2]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        char v24c[(kMaxSectors+7)>>3];
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        int nOwner = actSpriteOwnerToSpriteId(pSprite);
+        int nType = pSprite->type;
+        dassert(nType >= 0 && nType < kExplodeMax);
+        EXPLOSION *pExplodeInfo = &explodeInfo[nType];
+        int nXSprite = pSprite->extra;
+        dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        int x = pSprite->x;
+        int y = pSprite->y;
+        int z = pSprite->z;
+        int nSector = pSprite->sectnum;
+        gAffectedSectors[0] = -1;
+        gAffectedXWalls[0] = -1;
+        
+        // By NoOne: Allow to override explosion radius by data4 field of any sprite which have statnum 2 set in editor
+        // or of Hidden Exploder.
+        int radius = pXSprite->data4;
+        if (pXSprite->data4 <= 0)
+            radius = pExplodeInfo->at3;
+        
+        GetClosestSpriteSectors(nSector, x, y, radius, gAffectedSectors, v24c, gAffectedXWalls);
+        for (int i = 0; i < kMaxXWalls; i++)
+        {
+            int nWall = gAffectedXWalls[i];
+            if (nWall == -1)
+                break;
+            XWALL *pXWall = &xwall[wall[nWall].extra];
+            trTriggerWall(nWall, pXWall, 51);
+        }
+        for (int nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+        {
+            spritetype *pDude = &sprite[nSprite2];
+            if (pDude->hitag & 32)
+                continue;
+            if (TestBitString(v24c, pDude->sectnum))
+            {
+                if (pXSprite->data1 && CheckProximity(pDude, x, y, z, nSector, radius))
+                {
+                    if (pExplodeInfo->at1 && pXSprite->target == 0)
+                    {
+                        pXSprite->target = 1;
+                        actDamageSprite(nOwner, pDude, DAMAGE_TYPE_0, (pExplodeInfo->at1+Random(pExplodeInfo->at2))<<4);
+                    }
+                    if (pExplodeInfo->at7)
+                        ConcussSprite(nOwner, pDude, x, y, z, pExplodeInfo->at7);
+                    if (pExplodeInfo->atb)
+                    {
+                        dassert(pDude->extra > 0 && pDude->extra < kMaxXSprites);
+                        XSPRITE *pXDude = &xsprite[pDude->extra];
+                        if (!pXDude->burnTime)
+                            evPost(nSprite2, 3, 0, CALLBACK_ID_0);
+                        actBurnSprite(pSprite->owner, pXDude, pExplodeInfo->atb<<2);
+                    }
+                }
+            }
+        }
+        for (int nSprite2 = headspritestat[4]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+        {
+            spritetype *pThing = &sprite[nSprite2];
+            if (pThing->hitag & 32)
+                continue;
+            if (TestBitString(v24c, pThing->sectnum))
+            {
+                if (pXSprite->data1 && CheckProximity(pThing, x, y, z, nSector, radius))
+                {
+                    XSPRITE *pXSprite2 = &xsprite[pThing->extra];
+                    if (!pXSprite2->locked)
+                    {
+                        if (pExplodeInfo->at7)
+                            ConcussSprite(nOwner, pThing, x, y, z, pExplodeInfo->at7);
+                        if (pExplodeInfo->atb)
+                        {
+                            dassert(pThing->extra > 0 && pThing->extra < kMaxXSprites);
+                            XSPRITE *pXThing = &xsprite[pThing->extra];
+                            if (pThing->type == 400 && !pXThing->burnTime)
+                                evPost(nSprite2, 3, 0, CALLBACK_ID_0);
+                            actBurnSprite(pSprite->owner, pXThing, pExplodeInfo->atb<<2);
+                        }
+                    }
+                }
+            }
+        }
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            spritetype *pSprite2 = gPlayer[p].pSprite;
+            int dx = (x - pSprite2->x)>>4;
+            int dy = (y - pSprite2->y)>>4;
+            int dz = (z - pSprite2->z)>>8;
+            int nDist = dx*dx+dy*dy+dz*dz+0x40000;
+            int t = divscale16(pXSprite->data2, nDist);
+            gPlayer[p].at35a += t;
+        }
+        
+        // By NoOne: if data4 > 0, do not remove explosion. This can be useful when designer wants put explosion generator in map manually
+	    // via sprite statnum 2.
+        if (!(pSprite->hitag & kHitagExtBit)) {
+            pXSprite->data1 = ClipLow(pXSprite->data1 - 4, 0);
+            pXSprite->data2 = ClipLow(pXSprite->data2 - 4, 0);
+            pXSprite->data3 = ClipLow(pXSprite->data3 - 4, 0);
+        }
+
+        if (pXSprite->data1 == 0 && pXSprite->data2 == 0 && pXSprite->data3 == 0 && seqGetStatus(3, nXSprite) < 0)
+            actPostSprite(nSprite, 1024);
+    }
+    for (nSprite = headspritestat[11]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        int nXSprite = pSprite->extra;
+        dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        switch (pSprite->type)
+        {
+        case 454:
+            pXSprite->data2 = ClipLow(pXSprite->data2-4, 0);
+            break;
+        case 452:
+            if (pXSprite->state && seqGetStatus(3, nXSprite) < 0)
+            {
+                int x = pSprite->x;
+                int y = pSprite->y;
+                int z = pSprite->z;
+                int t = (pXSprite->data1<<23)/120;
+                int dx = mulscale30(t, Cos(pSprite->ang));
+                int dy = mulscale30(t, Sin(pSprite->ang));
+                for (int i = 0; i < 2; i++)
+                {
+                    spritetype *pFX = gFX.fxSpawn(FX_32, pSprite->sectnum, x, y, z, 0);
+                    if (pFX)
+                    {
+                        xvel[pFX->index] = dx + Random2(0x8888);
+                        yvel[pFX->index] = dy + Random2(0x8888);
+                        zvel[pFX->index] = Random2(0x8888);
+                    }
+                    x += (dx/2)>>12;
+                    y += (dy/2)>>12;
+                }
+                dy = Sin(pSprite->ang)>>16;
+                dx = Cos(pSprite->ang)>>16;
+                gVectorData[VECTOR_TYPE_20].maxDist = pXSprite->data1<<9;
+                actFireVector(pSprite, 0, 0, dx, dy, Random2(0x8888), VECTOR_TYPE_20);
+            }
+            break;
+        }
+    }
+    for (nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        int nXSprite = pSprite->extra;
+        if (nXSprite > 0)
+        {
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (pXSprite->burnTime > 0)
+            {
+                switch (pSprite->type)
+                {
+                case 239:
+                case 240:
+                case 241:
+                case 242:
+                    actDamageSprite(actOwnerIdToSpriteId(pXSprite->burnSource), pSprite, DAMAGE_TYPE_1, 8);
+                    break;
+                default:
+                    pXSprite->burnTime = ClipLow(pXSprite->burnTime-4, 0);
+                    actDamageSprite(actOwnerIdToSpriteId(pXSprite->burnSource), pSprite, DAMAGE_TYPE_1, 8);
+                    break;
+                }
+            }
+
+            // By NoOne: handle incarnations of custom dude
+            if (pSprite->type == kGDXDudeUniversalCultist && pXSprite->health <= 0 && seqGetStatus(3, nXSprite) < 0) {
+                XSPRITE* pXIncarnation = getNextIncarnation(pXSprite);
+                if (pXIncarnation != NULL) {
+                    spritetype* pIncarnation = &sprite[pXIncarnation->reference];
+
+                    pSprite->type = pIncarnation->type;
+                    pSprite->pal = pIncarnation->pal;
+                    pSprite->shade = pIncarnation->shade;
+
+                    pXSprite->txID = pXIncarnation->txID;
+                    pXSprite->command = pXIncarnation->command;
+                    pXSprite->triggerOn = pXIncarnation->triggerOn;
+                    pXSprite->triggerOff = pXIncarnation->triggerOff;
+
+                    pXSprite->burnTime = 0;
+                    pXSprite->burnSource = -1;
+
+                    pXSprite->data1 = pXIncarnation->data1;
+                    pXSprite->data2 = pXIncarnation->data2;
+                    pXSprite->data3 = pXIncarnation->data3;
+                    pXSprite->data4 = pXIncarnation->data4;
+
+                    pXSprite->dudeGuard = pXIncarnation->dudeGuard;
+                    pXSprite->dudeDeaf = pXIncarnation->dudeDeaf;
+                    pXSprite->dudeAmbush = pXIncarnation->dudeAmbush;
+                    pXSprite->dudeFlag4 = pXIncarnation->dudeFlag4;
+
+                    pXSprite->busyTime = pXIncarnation->busyTime;
+                    aiInitSprite(pSprite);
+                    switch (pSprite->type) {
+                        case kGDXDudeUniversalCultist:
+                        case kGDXGenDudeBurning:
+                            if (pXSprite->data2 > 0) seqSpawn(pXSprite->data2, 3, nXSprite, -1);
+                            else seqSpawn(dudeInfo[pSprite->type - kDudeBase].seqStartID, 3, nXSprite, -1);
+                            break;
+                        default:
+                            seqSpawn(dudeInfo[pSprite->type - kDudeBase].seqStartID, 3, nXSprite, -1);
+                            break;
+                    }
+
+                    if (pXSprite->data4 > 0) pXSprite->health = pXSprite->data4;
+                    else pXSprite->health = dudeInfo[pSprite->type - kDudeBase].startHealth << 4;
+                    aiActivateDude(pSprite, pXSprite);
+                }
+            }
+
+            if (pSprite->type == 227)
+            {
+                if (pXSprite->health <= 0 && seqGetStatus(3, nXSprite) < 0)
+                {
+                    pXSprite->health = dudeInfo[28].startHealth<<4;
+                    pSprite->type = 228;
+                    if (pXSprite->target != -1)
+                        aiSetTarget(pXSprite, pXSprite->target);
+                    aiActivateDude(pSprite, pXSprite);
+                }
+            }
+            if (pXSprite->Proximity && !pXSprite->isTriggered)
+            {
+                for (int nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nNextSprite)
+                {
+                    nNextSprite = nextspritestat[nSprite2];
+                    spritetype *pSprite2 = &sprite[nSprite2];
+                    if (pSprite2->hitag&32)
+                        continue;
+                    XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+                    if ((unsigned int)pXSprite2->health > 0 && pSprite2->type >= kDudePlayer1 && pSprite2->type <= kDudePlayer8)
+                    {
+                        if (CheckProximity(pSprite2, pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, 128))
+                        {
+                            trTriggerSprite(nSprite, pXSprite, 35);
+                        }
+                    }
+                }
+            }
+            if (IsPlayerSprite(pSprite))
+            {
+                PLAYER *pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+                if (pPlayer->at34e)
+                    sub_41250(pPlayer);
+                if (pPlayer->at376 && Chance(0x8000))
+                    actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_4, 12);
+                if (pPlayer->at87)
+                {
+                    char bActive = packItemActive(pPlayer, 1);
+                    if (bActive || pPlayer->at31a)
+                        pPlayer->at2f2 = 1200;
+                    else
+                        pPlayer->at2f2 = ClipLow(pPlayer->at2f2-4, 0);
+                    if (pPlayer->at2f2 < 1080 && packCheckItem(pPlayer, 1) && !bActive)
+                        packUseItem(pPlayer, 1);
+                    if (!pPlayer->at2f2)
+                    {
+                        pPlayer->at36e += 4;
+                        if (Chance(pPlayer->at36e))
+                            actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_4, 3<<4);
+                    }
+                    else
+                        pPlayer->at36e = 0;
+                    if (xvel[nSprite] || yvel[nSprite])
+                        sfxPlay3DSound(pSprite, 709, 100, 2);
+                    pPlayer->at302 = ClipLow(pPlayer->at302-4, 0);
+                }
+                else if (gGameOptions.nGameType == 0)
+                {
+                    if (pPlayer->pXSprite->health > 0 && pPlayer->at30a >= 1200 && Chance(0x200))
+                    {
+                        pPlayer->at30a = -1;
+                        sfxPlay3DSound(pSprite, 3100+Random(11), 0, 2);
+                    }
+                }
+            }
+            ProcessTouchObjects(pSprite, nXSprite);
+        }
+    }
+    for (nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        int nXSprite = pSprite->extra;
+        dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+        int nSector = pSprite->sectnum;
+        viewBackupSpriteLoc(nSprite, pSprite);
+        int nXSector = sector[nSector].extra;
+        XSECTOR *pXSector = NULL;
+        if (nXSector > 0)
+        {
+            dassert(nXSector > 0 && nXSector < kMaxXSectors);
+            dassert(xsector[nXSector].reference == nSector);
+            pXSector = &xsector[nXSector];
+        }
+        if (pXSector)
+        {
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            if (getflorzofslope(nSector, pSprite->x, pSprite->y) <= bottom)
+            {
+                int angle = pXSector->panAngle;
+                int speed = 0;
+                if (pXSector->panAlways || pXSector->state || pXSector->busy)
+                {
+                    speed = pXSector->panVel << 9;
+                    if (!pXSector->panAlways && pXSector->busy)
+                        speed = mulscale16(speed, pXSector->busy);
+                }
+                if (sector[nSector].floorstat&64)
+                    angle = (angle+GetWallAngle(sector[nSector].wallptr)+512)&2047;
+                int dx = mulscale30(speed, Cos(angle));
+                int dy = mulscale30(speed, Sin(angle));
+                xvel[nSprite] += dx;
+                yvel[nSprite] += dy;
+            }
+        }
+        if (pXSector && pXSector->Underwater)
+            actAirDrag(pSprite, 5376);
+        else
+            actAirDrag(pSprite, 128);
+        if ((pSprite->hitag&4) || xvel[nSprite] || yvel[nSprite] || zvel[nSprite] ||
+            velFloor[pSprite->sectnum] || velCeil[pSprite->sectnum])
+            MoveDude(pSprite);
+    }
+    for (nSprite = headspritestat[14]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag & 32)
+            continue;
+        int nXSprite = pSprite->extra;
+        dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        int nTarget = pXSprite->target;
+        dassert(nTarget >= 0);
+        viewBackupSpriteLoc(nSprite, pSprite);
+        dassert(nTarget < kMaxSprites);
+        spritetype *pTarget = &sprite[nTarget];
+        if (pTarget->statnum == kMaxStatus)
+        {
+            GibSprite(pSprite, GIBTYPE_17, NULL, NULL);
+            actPostSprite(pSprite->index, 1024);
+        }
+        if (pTarget->extra && xsprite[pTarget->extra].health > 0)
+        {
+            int x = pTarget->x+mulscale30r(Cos(pXSprite->goalAng+pTarget->ang), pTarget->clipdist*2);
+            int y = pTarget->y+mulscale30r(Sin(pXSprite->goalAng+pTarget->ang), pTarget->clipdist*2);
+            int z = pTarget->z+pXSprite->targetZ;
+            vec3_t pos = { x, y, z };
+            setsprite(nSprite,&pos);
+            xvel[nSprite] = xvel[nTarget];
+            yvel[nSprite] = yvel[nTarget];
+            zvel[nSprite] = zvel[nTarget];
+        }
+        else
+        {
+            GibSprite(pSprite, GIBTYPE_17, NULL, NULL);
+            actPostSprite(pSprite->index, 1024);
+        }
+    }
+    aiProcessDudes();
+    gFX.fxProcess();
+}
+
+spritetype * actSpawnSprite(int nSector, int x, int y, int z, int nStat, char a6)
+{
+    int nSprite = InsertSprite(nSector, nStat);
+    if (nSprite >= 0)
+        sprite[nSprite].extra = -1;
+    else
+    {
+        nSprite = headspritestat[9];
+        dassert(nSprite >= 0);
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        ChangeSpriteSect(nSprite, nSector);
+        actPostSprite(nSprite, nStat);
+    }
+    vec3_t pos = { x, y, z };
+    setsprite(nSprite, &pos);
+    spritetype *pSprite = &sprite[nSprite];
+    pSprite->type = 0;
+    if (a6 && pSprite->extra == -1)
+    {
+        int nXSprite = dbInsertXSprite(nSprite);
+        gSpriteHit[nXSprite].florhit = 0;
+        gSpriteHit[nXSprite].ceilhit = 0;
+    }
+    return pSprite;
+}
+
+spritetype * actSpawnSprite(spritetype *pSource, int nStat);
+
+spritetype *actSpawnDude(spritetype *pSource, short nType, int a3, int a4)
+{
+    XSPRITE* pXSource = &xsprite[pSource->extra];
+    spritetype *pSprite2 = actSpawnSprite(pSource, 6);
+    if (!pSprite2) return NULL;
+    XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+    int angle = pSource->ang;
+    int nDude = nType-kDudeBase;
+    int x, y, z;
+    z = a4 + pSource->z;
+    if (a3 < 0)
+    {
+        x = pSource->x;
+        y = pSource->y;
+    }
+    else
+    {
+        x = pSource->x+mulscale30r(Cos(angle), a3);
+        y = pSource->y+mulscale30r(Sin(angle), a3);
+    }
+    pSprite2->type = nType;
+    pSprite2->ang = angle;
+    vec3_t pos = { x, y, z };
+    setsprite(pSprite2->index, &pos);
+    pSprite2->cstat |= 0x1101;
+    pSprite2->clipdist = dudeInfo[nDude].clipdist;
+    pXSprite2->health = dudeInfo[nDude].startHealth<<4;
+    if (gSysRes.Lookup(dudeInfo[nDude].seqStartID, "SEQ"))
+        seqSpawn(dudeInfo[nDude].seqStartID, 3, pSprite2->extra, -1);
+    
+    // By NoOne: add a way to inherit some values of spawner type 18 by dude.
+    // This way designer can count enemies via switches and do many other interesting things.
+
+                                                // oops, forget to check for source type previously
+    if ((pSource->hitag & kHitagExtBit) != 0 && pSource->type == 18) {
+        
+        //inherit pal?
+        if (pSprite2->pal <= 0) pSprite2->pal = pSource->pal;
+
+        // inherit spawn sprite trigger settings, so designer can count monsters.
+        pXSprite2->txID = pXSource->txID;
+        pXSprite2->command = pXSource->command;
+        pXSprite2->triggerOn = pXSource->triggerOn;
+        pXSprite2->triggerOff = pXSource->triggerOff;
+
+        // inherit drop items
+        pXSprite2->dropMsg = pXSource->dropMsg;
+
+        // inherit dude flags
+        pXSprite2->dudeDeaf = pXSource->dudeDeaf;
+        pXSprite2->dudeGuard = pXSource->dudeGuard;
+        pXSprite2->dudeAmbush = pXSource->dudeAmbush;
+        pXSprite2->dudeFlag4 = pXSource->dudeFlag4;
+    }
+
+    aiInitSprite(pSprite2);
+    return pSprite2;
+}
+
+spritetype * actSpawnSprite(spritetype *pSource, int nStat)
+{
+    int nSprite = InsertSprite(pSource->sectnum, nStat);
+    if (nSprite < 0)
+    {
+        nSprite = headspritestat[9];
+        dassert(nSprite >= 0);
+        dassert(pSource->sectnum >= 0 && pSource->sectnum < kMaxSectors);
+        ChangeSpriteSect(nSprite, pSource->sectnum);
+        actPostSprite(nSprite, nStat);
+    }
+    spritetype *pSprite = &sprite[nSprite];
+    pSprite->x = pSource->x;
+    pSprite->y = pSource->y;
+    pSprite->z = pSource->z;
+    xvel[nSprite] = xvel[pSource->index];
+    yvel[nSprite] = yvel[pSource->index];
+    zvel[nSprite] = zvel[pSource->index];
+    pSprite->hitag = 0;
+    int nXSprite = dbInsertXSprite(nSprite);
+    gSpriteHit[nXSprite].florhit = 0;
+    gSpriteHit[nXSprite].ceilhit = 0;
+    return pSprite;
+}
+
+spritetype * actSpawnThing(int nSector, int x, int y, int z, int nThingType)
+{
+    dassert(nThingType >= kThingBase && nThingType < kThingMax);
+    spritetype *pSprite = actSpawnSprite(nSector, x, y, z, 4, 1);
+    int nType = nThingType-kThingBase;
+    int nThing = pSprite->index;
+    int nXThing = pSprite->extra;
+    pSprite->type = nThingType;
+    dassert(nXThing > 0 && nXThing < kMaxXSprites);
+    XSPRITE *pXThing = &xsprite[nXThing];
+    THINGINFO *pThingInfo = &thingInfo[nType];
+    pXThing->health = pThingInfo->at0<<4;
+    pSprite->clipdist = pThingInfo->at4;
+    pSprite->hitag = pThingInfo->at5;
+    if (pSprite->hitag & 2)
+        pSprite->hitag |= 4;
+    pSprite->cstat |= pThingInfo->atf;
+    pSprite->picnum = pThingInfo->at11;
+    pSprite->shade = pThingInfo->at13;
+    pSprite->pal = pThingInfo->at14;
+    if (pThingInfo->at15)
+        pSprite->xrepeat = pThingInfo->at15;
+    if (pThingInfo->at16)
+        pSprite->yrepeat = pThingInfo->at16;
+    SetBitString(show2dsprite, pSprite->index);
+    switch (nThingType)
+    {
+    case 432:
+        pXThing->data1 = 0;
+        pXThing->data2 = 0;
+        pXThing->data3 = 0;
+        pXThing->data4 = 0;
+        pXThing->state = 1;
+        pXThing->triggerOnce = 1;
+        pXThing->isTriggered = 0;
+        break;
+    case 431:
+    case kGDXThingCustomDudeLifeLeech:
+        pXThing->data1 = 0;
+        pXThing->data2 = 0;
+        pXThing->data3 = 0;
+        pXThing->data4 = 0;
+        pXThing->state = 1;
+        pXThing->triggerOnce = 0;
+        pXThing->isTriggered = 0;
+        break;
+    case 427:
+        pXThing->data1 = 8;
+        pXThing->data2 = 0;
+        pXThing->data3 = 0;
+        pXThing->data4 = 318;
+        pXThing->targetX = gFrameClock+180.0;
+        pXThing->locked = 1;
+        pXThing->state = 1;
+        pXThing->triggerOnce = 0;
+        pXThing->isTriggered = 0;
+        break;
+    case 425:
+    case 426:
+        if (nThingType == 425)
+            pXThing->data1 = 19;
+        else if (nThingType == 426)
+            pXThing->data1 = 8;
+        pXThing->data2 = 0;
+        pXThing->data3 = 0;
+        pXThing->data4 = 318;
+        pXThing->targetX = gFrameClock+180.0;
+        pXThing->locked = 1;
+        pXThing->state = 1;
+        pXThing->triggerOnce = 0;
+        pXThing->isTriggered = 0;
+        break;
+    case 418:
+        evPost(nThing, 3, 0, CALLBACK_ID_8);
+        sfxPlay3DSound(pSprite, 450, 0, 0);
+        break;
+    case 419:
+        sfxPlay3DSound(pSprite, 450, 0, 0);
+        evPost(nThing, 3, 0, CALLBACK_ID_8);
+        break;
+    case 420:
+        evPost(nThing, 3, 0, CALLBACK_ID_8);
+        break;
+    }
+    return pSprite;
+}
+
+spritetype * actFireThing(spritetype *pSprite, int a2, int a3, int a4, int thingType, int a6)
+{
+    dassert(thingType >= kThingBase && thingType < kThingMax);
+    int x = pSprite->x+mulscale30(a2, Cos(pSprite->ang+512));
+    int y = pSprite->y+mulscale30(a2, Sin(pSprite->ang+512));
+    int z = pSprite->z+a3;
+    x += mulscale28(pSprite->clipdist, Cos(pSprite->ang));
+    y += mulscale28(pSprite->clipdist, Sin(pSprite->ang));
+    if (HitScan(pSprite, z, x-pSprite->x, y-pSprite->y, 0, CLIPMASK0, pSprite->clipdist) != -1)
+    {
+        x = gHitInfo.hitx-mulscale28(pSprite->clipdist<<1, Cos(pSprite->ang));
+        y = gHitInfo.hity-mulscale28(pSprite->clipdist<<1, Sin(pSprite->ang));
+    }
+    spritetype *pThing = actSpawnThing(pSprite->sectnum, x, y, z, thingType);
+    actPropagateSpriteOwner(pThing, pSprite);
+    pThing->ang = pSprite->ang;
+    xvel[pThing->index] = mulscale30(a6, Cos(pThing->ang));
+    yvel[pThing->index] = mulscale30(a6, Sin(pThing->ang));
+    zvel[pThing->index] = mulscale(a6, a4, 14);
+    xvel[pThing->index] += xvel[pSprite->index]/2;
+    yvel[pThing->index] += yvel[pSprite->index]/2;
+    zvel[pThing->index] += zvel[pSprite->index]/2;
+    return pThing;
+}
+
+spritetype* actFireMissile(spritetype *pSprite, int a2, int a3, int a4, int a5, int a6, int nType)
+{
+    dassert(nType >= kMissileBase && nType < kMissileMax);
+    char v4 = 0;
+    int nSprite = pSprite->index;
+    MissileType *pMissileInfo = &missileInfo[nType-kMissileBase];
+    int x = pSprite->x+mulscale30(a2, Cos(pSprite->ang+512));
+    int y = pSprite->y+mulscale30(a2, Sin(pSprite->ang+512));
+    int z = pSprite->z+a3;
+    int clipdist = pMissileInfo->atd+pSprite->clipdist;
+    x += mulscale28(clipdist, Cos(pSprite->ang));
+    y += mulscale28(clipdist, Sin(pSprite->ang));
+    int hit = HitScan(pSprite, z, x-pSprite->x, y-pSprite->y, 0, CLIPMASK0, clipdist);
+    if (hit != -1)
+    {
+        if (hit == 3 || hit == 0)
+        {
+            v4 = 1;
+            x = gHitInfo.hitx-mulscale30(Cos(pSprite->ang), 16);
+            y = gHitInfo.hity-mulscale30(Sin(pSprite->ang), 16);
+        }
+        else
+        {
+            x = gHitInfo.hitx-mulscale28(pMissileInfo->atd<<1, Cos(pSprite->ang));
+            y = gHitInfo.hity-mulscale28(pMissileInfo->atd<<1, Sin(pSprite->ang));
+        }
+    }
+    spritetype *pMissile = actSpawnSprite(pSprite->sectnum, x, y, z, 5, 1);
+    int nMissile = pMissile->index;
+    SetBitString(show2dsprite, nMissile);
+    pMissile->type = nType;
+    pMissile->shade = pMissileInfo->atc;
+    pMissile->pal = 0;
+    pMissile->clipdist = pMissileInfo->atd;
+    pMissile->hitag = 1;
+    pMissile->xrepeat = pMissileInfo->ata;
+    pMissile->yrepeat = pMissileInfo->atb;
+    pMissile->picnum = pMissileInfo->picnum;
+    pMissile->ang = (pSprite->ang+pMissileInfo->at6)&2047;
+    xvel[nMissile] = mulscale(pMissileInfo->at2, a4, 14);
+    yvel[nMissile] = mulscale(pMissileInfo->at2, a5, 14);
+    zvel[nMissile] = mulscale(pMissileInfo->at2, a6, 14);
+    actPropagateSpriteOwner(pMissile, pSprite);
+    pMissile->cstat |= 1;
+    int nXSprite = pMissile->extra;
+    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+    xsprite[nXSprite].target = -1;
+    evPost(nMissile, 3, 600, CALLBACK_ID_1);
+    switch (nType)
+    {
+    case 315:
+        evPost(nMissile, 3, 0, CALLBACK_ID_0);
+        break;
+    case 302:
+        evPost(nMissile, 3, 0, CALLBACK_ID_15);
+        break;
+    case 309:
+        seqSpawn(29, 3, nXSprite, -1);
+        break;
+    case 300:
+        pMissile->cstat |= 16;
+        break;
+    case 306:
+        sfxPlay3DSound(pMissile, 251, 0, 0);
+        break;
+    case 307:
+        seqSpawn(2, 3, nXSprite, -1);
+        sfxPlay3DSound(pMissile, 493, 0, 0);
+        break;
+    case 312:
+        seqSpawn(61, 3, nXSprite, nNapalmClient);
+        sfxPlay3DSound(pMissile, 441, 0, 0);
+        break;
+    case 305:
+        seqSpawn(22, 3, nXSprite, nFireballClient);
+        sfxPlay3DSound(pMissile, 441, 0, 0);
+        break;
+    case 308:
+        seqSpawn(27, 3, nXSprite, -1);
+        xvel[nMissile] += xvel[nSprite]/2+Random2(0x11111);
+        yvel[nMissile] += yvel[nSprite]/2+Random2(0x11111);
+        zvel[nMissile] += zvel[nSprite]/2+Random2(0x11111);
+        break;
+    case 313:
+        seqSpawn(61, 3, nXSprite, dword_2192E0);
+        sfxPlay3DSound(pMissile, 441, 0, 0);
+        break;
+    case 314:
+        seqSpawn(23, 3, nXSprite, dword_2192D8);
+        xvel[nMissile] += xvel[nSprite]/2+Random2(0x11111);
+        yvel[nMissile] += yvel[nSprite]/2+Random2(0x11111);
+        zvel[nMissile] += zvel[nSprite]/2+Random2(0x11111);
+        break;
+    case 304:
+        if (Chance(0x8000))
+            seqSpawn(0, 3, nXSprite, -1);
+        else
+            seqSpawn(1, 3, nXSprite, -1);
+        xvel[nMissile] += xvel[nSprite]+Random2(0x11111);
+        yvel[nMissile] += yvel[nSprite]+Random2(0x11111);
+        zvel[nMissile] += zvel[nSprite]+Random2(0x11111);
+        break;
+    case 303:
+        evPost(nMissile, 3, 30, CALLBACK_ID_2);
+        evPost(nMissile, 3, 0, CALLBACK_ID_3);
+        sfxPlay3DSound(pMissile, 422, 0, 0);
+        break;
+    case 301:
+        evPost(nMissile, 3, 0, CALLBACK_ID_3);
+        sfxPlay3DSound(pMissile, 422, 0, 0);
+        break;
+    case 317:
+        evPost(nMissile, 3, 0, CALLBACK_ID_7);
+        break;
+    case 311:
+        sfxPlay3DSound(pMissile, 252, 0, 0);
+        break;
+    }
+    if (v4)
+    {
+        actImpactMissile(pMissile, hit);
+        pMissile = NULL;
+    }
+    return pMissile;
+}
+
+int actGetRespawnTime(spritetype *pSprite)
+{
+    int nXSprite = pSprite->extra;
+    if (nXSprite <= 0)
+        return -1;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    if (IsDudeSprite(pSprite) && !IsPlayerSprite(pSprite))
+    {
+        if (pXSprite->respawn == 2 || (pXSprite->respawn != 1 && gGameOptions.nMonsterSettings == 2))
+            return gGameOptions.nMonsterRespawnTime;
+        return -1;
+    }
+    if (IsWeaponSprite(pSprite))
+    {
+        if (pXSprite->respawn == 3 || gGameOptions.nWeaponSettings == 1)
+            return 0;
+        if (pXSprite->respawn != 1 && gGameOptions.nWeaponSettings != 0)
+            return gGameOptions.nWeaponRespawnTime;
+        return -1;
+    }
+    if (IsAmmoSprite(pSprite))
+    {
+        if (pXSprite->respawn == 2 || (pXSprite->respawn != 1 && gGameOptions.nWeaponSettings != 0))
+            return gGameOptions.nWeaponRespawnTime;
+        return -1;
+    }
+    if (IsItemSprite(pSprite))
+    {
+        if (pXSprite->respawn == 3 && gGameOptions.nGameType == 1)
+            return 0;
+        if (pXSprite->respawn == 2 || (pXSprite->respawn != 1 && gGameOptions.nItemSettings != 0))
+        {
+            switch (pSprite->type)
+            {
+            case 113:
+            case 117:
+            case 124:
+                return gGameOptions.nSpecialRespawnTime;
+            case 114:
+                return gGameOptions.nSpecialRespawnTime<<1;
+            default:
+                return gGameOptions.nItemRespawnTime;
+            }
+        }
+        return -1;
+    }
+    return -1;
+}
+
+bool actCheckRespawn(spritetype *pSprite)
+{
+    int nSprite = pSprite->index;
+    int nXSprite = pSprite->extra;
+    if (nXSprite > 0)
+    {
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        int nRespawnTime = actGetRespawnTime(pSprite);
+        if (nRespawnTime < 0)
+            return 0;
+        pXSprite->respawnPending = 1;
+        if (pSprite->type >= kThingBase && pSprite->type < kThingMax)
+        {
+            pXSprite->respawnPending = 3;
+            if (pSprite->type == 400)
+                pSprite->cstat |= 32768;
+        }
+        if (nRespawnTime > 0)
+        {
+            if (pXSprite->respawnPending == 1)
+                nRespawnTime = mulscale16(nRespawnTime, 0xa000);
+            pSprite->owner = pSprite->statnum;
+            actPostSprite(pSprite->index, 8);
+            pSprite->hitag |= 16;
+            if (pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
+            {
+                pSprite->cstat &= ~257;
+                pSprite->x = baseSprite[nSprite].x;
+                pSprite->y = baseSprite[nSprite].y;
+                pSprite->z = baseSprite[nSprite].z;
+            }
+            evPost(nSprite, 3, nRespawnTime, CALLBACK_ID_9);
+        }
+        return 1;
+    }
+    return  0;
+}
+
+bool actCanSplatWall(int nWall)
+{
+    dassert(nWall >= 0 && nWall < kMaxWalls);
+    walltype *pWall = &wall[nWall];
+    if (pWall->cstat & 16384)
+        return 0;
+    if (pWall->cstat & 32768)
+        return 0;
+    if (pWall->lotag >= 500 && pWall->lotag < 512)
+        return 0;
+    if (pWall->nextsector != -1)
+    {
+        sectortype *pSector = &sector[pWall->nextsector];
+        if (pSector->lotag >= 600 && pSector->lotag < 620)
+            return 0;
+    }
+    return 1;
+}
+
+void actFireVector(spritetype *pShooter, int a2, int a3, int a4, int a5, int a6, VECTOR_TYPE vectorType)
+{
+    int nShooter = pShooter->index;
+    dassert(vectorType >= 0 && vectorType < kVectorMax);
+    VECTORDATA *pVectorData = &gVectorData[vectorType];
+    int nRange = pVectorData->maxDist;
+    int hit = VectorScan(pShooter, a2, a3, a4, a5, a6, nRange, 1);
+    if (hit == 3)
+    {
+        int nSprite = gHitInfo.hitsprite;
+        dassert(nSprite >= 0 && nSprite < kMaxSprites);
+        spritetype *pSprite = &sprite[nSprite];
+        if (IsPlayerSprite(pSprite))
+        {
+            PLAYER *pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+            if (powerupCheck(pPlayer, 24))
+            {
+                gHitInfo.hitsprite = nShooter;
+                gHitInfo.hitx = pShooter->x;
+                gHitInfo.hity = pShooter->y;
+                gHitInfo.hitz = pShooter->z;
+            }
+        }
+    }
+    int x = gHitInfo.hitx-mulscale(a4, 16, 14);
+    int y = gHitInfo.hity-mulscale(a5, 16, 14);
+    int z = gHitInfo.hitz-mulscale(a6, 256, 14);
+    short nSector = gHitInfo.hitsect;
+    char nSurf = 0;
+    if (nRange == 0 || approxDist(gHitInfo.hitx-pShooter->x, gHitInfo.hity-pShooter->y) < nRange)
+    {
+        switch (hit)
+        {
+        case 1:
+        {
+            int nSector = gHitInfo.hitsect;
+            if (sector[nSector].ceilingstat&1)
+                nSurf = 0;
+            else
+                nSurf = surfType[sector[nSector].ceilingpicnum];
+            break;
+        }
+        case 2:
+        {
+            int nSector = gHitInfo.hitsect;
+            if (sector[nSector].floorstat&1)
+                nSurf = 0;
+            else
+                nSurf = surfType[sector[nSector].floorpicnum];
+            break;
+        }
+        case 0:
+        {
+            int nWall = gHitInfo.hitwall;
+            dassert(nWall >= 0 && nWall < kMaxWalls);
+            nSurf = surfType[wall[nWall].picnum];
+            if (actCanSplatWall(nWall))
+            {
+                int x = gHitInfo.hitx-mulscale(a4, 16, 14);
+                int y = gHitInfo.hity-mulscale(a5, 16, 14);
+                int z = gHitInfo.hitz-mulscale(a6, 256, 14);
+                int nSurf = surfType[wall[nWall].picnum];
+                dassert(nSurf < kSurfMax);
+                if (pVectorData->at1d[nSurf].at0 >= 0)
+                {
+                    spritetype *pFX = gFX.fxSpawn(pVectorData->at1d[nSurf].at0, nSector, x, y, z, 0);
+                    if (pFX)
+                    {
+                        pFX->ang = (GetWallAngle(nWall)+512)&2047;
+                        pFX->cstat |= 16;
+                    }
+                }
+            }
+            break;
+        }
+        case 4:
+        {
+            int nWall = gHitInfo.hitwall;
+            dassert(nWall >= 0 && nWall < kMaxWalls);
+            nSurf = surfType[wall[nWall].overpicnum];
+            int nXWall = wall[nWall].extra;
+            if (nXWall > 0)
+            {
+                XWALL *pXWall = &xwall[nXWall];
+                if (pXWall->triggerVector)
+                    trTriggerWall(nWall, pXWall, 51);
+            }
+            break;
+        }
+        case 3:
+        {
+            int nSprite = gHitInfo.hitsprite;
+            nSurf = surfType[sprite[nSprite].picnum];
+            dassert(nSprite >= 0 && nSprite < kMaxSprites);
+            spritetype *pSprite = &sprite[nSprite];
+            x -= mulscale(a4, 112, 14);
+            y -= mulscale(a5, 112, 14);
+            z -= mulscale(a6, 112<<4, 14);
+            int shift = 4;
+            if (vectorType == VECTOR_TYPE_0 && !IsPlayerSprite(pSprite))
+                shift = 3;
+            actDamageSprite(nShooter, pSprite, pVectorData->at0, pVectorData->at1<<shift);
+            int nXSprite = pSprite->extra;
+            if (nXSprite > 0)
+            {
+                XSPRITE *pXSprite = &xsprite[nXSprite];
+                if (pXSprite->Vector)
+                    trTriggerSprite(nSprite, pXSprite, 31);
+            }
+            if (pSprite->statnum == 4)
+            {
+                int t = thingInfo[pSprite->type-kThingBase].at2;
+                if (t > 0 && pVectorData->at5)
+                {
+                    int t2 = divscale(pVectorData->at5, t, 8);
+                    xvel[nSprite] += mulscale16(a4, t2);
+                    yvel[nSprite] += mulscale16(a5, t2);
+                    zvel[nSprite] += mulscale16(a6, t2);
+                }
+                if (pVectorData->at11)
+                {
+                    XSPRITE *pXSprite = &xsprite[nXSprite];
+                    if (!pXSprite->burnTime)
+                        evPost(nSprite, 3, 0, CALLBACK_ID_0);
+                    actBurnSprite(actSpriteIdToOwnerId(nShooter), pXSprite, pVectorData->at11);
+                }
+            }
+            if (pSprite->statnum == 6)
+            {
+                int t = pSprite->type == 426 ? 0 : dudeInfo[pSprite->type-kDudeBase].mass;
+                
+                if (IsDudeSprite(pSprite)) {
+                    switch (pSprite->lotag) {
+                    case kGDXDudeUniversalCultist:
+                    case kGDXGenDudeBurning:
+                        t = getDudeMassBySpriteSize(pSprite);
+                        break;
+                    }
+                }
+
+                if (t > 0 && pVectorData->at5)
+                {
+                    int t2 = divscale(pVectorData->at5, t, 8);
+                    xvel[nSprite] += mulscale16(a4, t2);
+                    yvel[nSprite] += mulscale16(a5, t2);
+                    zvel[nSprite] += mulscale16(a6, t2);
+                }
+                if (pVectorData->at11)
+                {
+                    XSPRITE *pXSprite = &xsprite[nXSprite];
+                    if (!pXSprite->burnTime)
+                        evPost(nSprite, 3, 0, CALLBACK_ID_0);
+                    actBurnSprite(actSpriteIdToOwnerId(nShooter), pXSprite, pVectorData->at11);
+                }
+                if (Chance(pVectorData->atd))
+                {
+                    int t = gVectorData[19].maxDist;
+                    a4 += Random3(4000);
+                    a5 += Random3(4000);
+                    a6 += Random3(4000);
+                    if (HitScan(pSprite, gHitInfo.hitz, a4, a5, a6, CLIPMASK1, t) == 0)
+                    {
+                        if (approxDist(gHitInfo.hitx-pSprite->x, gHitInfo.hity-pSprite->y) <= t)
+                        {
+                            int nWall = gHitInfo.hitwall;
+                            int nSector = gHitInfo.hitsect;
+                            if (actCanSplatWall(nWall))
+                            {
+                                int x = gHitInfo.hitx - mulscale(a4, 16, 14);
+                                int y = gHitInfo.hity - mulscale(a5, 16, 14);
+                                int z = gHitInfo.hitz - mulscale(a6, 16<<4, 14);
+                                int nSurf = surfType[wall[nWall].picnum];
+                                VECTORDATA *pVectorData = &gVectorData[19];
+                                FX_ID t2 = pVectorData->at1d[nSurf].at1;
+                                FX_ID t3 = pVectorData->at1d[nSurf].at2;
+                                spritetype *pFX = NULL;
+                                if (t2 > FX_NONE && (t3 == FX_NONE || Chance(0x4000)))
+                                    pFX = gFX.fxSpawn(t2, nSector, x, y, z, 0);
+                                else if(t3 > FX_NONE)
+                                    pFX = gFX.fxSpawn(t3, nSector, x, y, z, 0);
+                                if (pFX)
+                                {
+                                    zvel[pFX->index] = 0x2222;
+                                    pFX->ang = (GetWallAngle(nWall)+512)&2047;
+                                    pFX->cstat |= 16;
+                                }
+                            }
+                        }
+                    }
+                }
+                for (int i = 0; i < pVectorData->at15; i++)
+                    if (Chance(pVectorData->at19))
+                        fxSpawnBlood(pSprite, pVectorData->at1<<4);
+            }
+            break;
+        }
+        }
+    }
+    dassert(nSurf < kSurfMax);
+    if (pVectorData->at1d[nSurf].at1 >= 0)
+        gFX.fxSpawn(pVectorData->at1d[nSurf].at1, nSector, x, y, z, 0);
+    if (pVectorData->at1d[nSurf].at2 >= 0)
+        gFX.fxSpawn(pVectorData->at1d[nSurf].at2, nSector, x, y, z, 0);
+    if (pVectorData->at1d[nSurf].at3 >= 0)
+        sfxPlay3DSound(x, y, z, pVectorData->at1d[nSurf].at3, nSector);
+}
+
+void FireballSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_11, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        int nFX = pFX->index;
+        xvel[nFX] = xvel[nSprite];
+        yvel[nFX] = yvel[nSprite];
+        zvel[nFX] = zvel[nSprite];
+    }
+}
+
+void NapalmSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_12, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        int nFX = pFX->index;
+        xvel[nFX] = xvel[nSprite];
+        yvel[nFX] = yvel[nSprite];
+        zvel[nFX] = zvel[nSprite];
+    }
+}
+
+void sub_3888C(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_32, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        int nFX = pFX->index;
+        xvel[nFX] = xvel[nSprite];
+        yvel[nFX] = yvel[nSprite];
+        zvel[nFX] = zvel[nSprite];
+    }
+}
+
+void sub_38938(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_33, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        int nFX = pFX->index;
+        xvel[nFX] = xvel[nSprite];
+        yvel[nFX] = yvel[nSprite];
+        zvel[nFX] = zvel[nSprite];
+    }
+}
+
+void TreeToGibCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    pSprite->type = 417;
+    pXSprite->state = 1;
+    pXSprite->data1 = 15;
+    pXSprite->data2 = 0;
+    pXSprite->data3 = 0;
+    pXSprite->health = thingInfo[17].at0;
+    pXSprite->data4 = 312;
+    pSprite->cstat |= 257;
+}
+
+void DudeToGibCallback1(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    pSprite->type = 426;
+    pXSprite->data1 = 8;
+    pXSprite->data2 = 0;
+    pXSprite->data3 = 0;
+    pXSprite->health = thingInfo[26].at0;
+    pXSprite->data4 = 319;
+    pXSprite->triggerOnce = 0;
+    pXSprite->isTriggered = 0;
+    pXSprite->locked = 0;
+    pXSprite->targetX = gFrameClock;
+    pXSprite->state = 1;
+}
+
+void DudeToGibCallback2(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    pSprite->type = 426;
+    pXSprite->data1 = 3;
+    pXSprite->data2 = 0;
+    pXSprite->data3 = 0;
+    pXSprite->health = thingInfo[26].at0;
+    pXSprite->data4 = 319;
+    pXSprite->triggerOnce = 0;
+    pXSprite->isTriggered = 0;
+    pXSprite->locked = 0;
+    pXSprite->targetX = gFrameClock;
+    pXSprite->state = 1;
+}
+
+void actPostSprite(int nSprite, int nStatus)
+{
+    int n;
+    dassert(gPostCount < kMaxSprites);
+    dassert(nSprite < kMaxSprites && sprite[nSprite].statnum < kMaxStatus);
+    dassert(nStatus >= 0 && nStatus <= kStatFree);
+    if (sprite[nSprite].hitag&32)
+    {
+        for (n = 0; n < gPostCount; n++)
+            if (gPost[n].at0 == nSprite)
+                break;
+        dassert(n < gPostCount);
+    }
+    else
+    {
+        n = gPostCount;
+        sprite[nSprite].hitag |= 32;
+        gPostCount++;
+    }
+    gPost[n].at0 = nSprite;
+    gPost[n].at2 = nStatus;
+}
+
+void actPostProcess(void)
+{
+    for (int i = 0; i < gPostCount; i++)
+    {
+        POSTPONE *pPost = &gPost[i];
+        int nSprite = pPost->at0;
+        spritetype *pSprite = &sprite[nSprite];
+        pSprite->hitag &= ~32;
+        int nStatus = pPost->at2;
+        if (nStatus == kStatFree)
+        {
+            evKill(nSprite, 3);
+            if (sprite[nSprite].extra > 0)
+                seqKill(3, sprite[nSprite].extra);
+            DeleteSprite(nSprite);
+        }
+        else
+            ChangeSpriteStat(nSprite, nStatus);
+    }
+    gPostCount = 0;
+}
+
+void MakeSplash(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    UNREFERENCED_PARAMETER(pXSprite);
+    pSprite->hitag &= ~2;
+    int nXSprite = pSprite->extra;
+    pSprite->z -= 4<<8;
+    int nSurface = tileGetSurfType(gSpriteHit[nXSprite].florhit);
+    switch (pSprite->type)
+    {
+    case 423:
+        if (nSurface == 5)
+        {
+            seqSpawn(6, 3, nXSprite, -1);
+            sfxPlay3DSound(pSprite, 356, -1, 0);
+        }
+        else
+        {
+            seqSpawn(7, 3, nXSprite, -1);
+            sfxPlay3DSound(pSprite, 354, -1, 0);
+        }
+        break;
+    case 424:
+        seqSpawn(8, 3, nXSprite, -1);
+        sfxPlay3DSound(pSprite, 354, -1, 0);
+        break;
+    }
+}
+
+class ActorLoadSave : public LoadSave
+{
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+void ActorLoadSave::Load(void)
+{
+    Read(gSpriteHit, sizeof(gSpriteHit));
+    Read(gAffectedSectors, sizeof(gAffectedSectors));
+    Read(gAffectedXWalls, sizeof(gAffectedXWalls));
+    Read(&gPostCount, sizeof(gPostCount));
+    Read(gPost, sizeof(gPost));
+    actInit();
+}
+
+void ActorLoadSave::Save(void)
+{
+    Write(gSpriteHit, sizeof(gSpriteHit));
+    Write(gAffectedSectors, sizeof(gAffectedSectors));
+    Write(gAffectedXWalls, sizeof(gAffectedXWalls));
+    Write(&gPostCount, sizeof(gPostCount));
+    Write(gPost, sizeof(gPost));
+}
+
+static ActorLoadSave *myLoadSave;
+
+void ActorLoadSaveConstruct(void)
+{
+    myLoadSave = new ActorLoadSave();
+}
+
+
+// By NoOne: The following functions required for random event features
+//-------------------------
+
+int GetDataVal(spritetype* pSprite, int data) {
+    XSPRITE* pXSprite = &xsprite[pSprite->extra];
+    int rData[4];
+
+    rData[0] = pXSprite->data1; rData[2] = pXSprite->data3;
+    rData[1] = pXSprite->data2; rData[3] = pXSprite->data4;
+
+    return rData[data];
+}
+
+
+std::default_random_engine rng;
+int my_random(int a, int b)
+{
+
+    std::uniform_int_distribution<int> dist_a_b(a, b);
+    return dist_a_b(rng);
+}
+
+// tries to get random data field of sprite
+int GetRandDataVal(int *rData, spritetype* pSprite) {
+    int temp[4];
+    if (rData != NULL && pSprite != NULL) return -1;
+    else if (pSprite != NULL) {
+
+        if (pSprite->extra < 0)
+            return -1;
+
+        if (rData == NULL)
+            rData = temp;
+
+        XSPRITE* pXSprite = &xsprite[pSprite->extra];
+        rData[0] = pXSprite->data1; rData[2] = pXSprite->data3;
+        rData[1] = pXSprite->data2; rData[3] = pXSprite->data4;
+
+    }
+    else if (rData == NULL) {
+        return -1;
+    }
+
+    int random = 0;
+    // randomize only in case if at least 2 data fields are not empty
+    int a = 1; int b = -1;
+    for (int i = 0; i <= 3; i++) {
+        if (rData[i] == 0) {
+            if (a++ > 2)
+                return -1;
+        }
+        else if (b == -1) {
+            b++;
+        }
+    }
+
+    // try randomize few times
+    int maxRetries = 10;
+    while (maxRetries > 0) {
+             
+        // use true random only for single player mode
+        if (gGameOptions.nGameType == 0 && !VanillaMode() && !DemoRecordStatus()) {
+            rng.seed(std::random_device()());
+            random = my_random(0, 4);
+        // otherwise use Blood's default one. In the future it maybe possible to make
+        // host send info to clients about what was generated.
+        } else {
+            random = Random(3);
+        }
+
+       if (rData[random] > 0) return rData[random];
+       maxRetries--;
+    }
+
+    // if nothing, get first found data value from top
+     return rData[b];
+}
+
+// this function drops random item using random pickup generator(s)
+spritetype* DropRandomPickupObject(spritetype* pSprite, short prevItem) {
+    spritetype* pSprite2 = NULL;
+
+    int rData[4]; int selected = -1;
+    rData[0] = xsprite[pSprite->extra].data1; rData[2] = xsprite[pSprite->extra].data3;
+    rData[1] = xsprite[pSprite->extra].data2; rData[3] = xsprite[pSprite->extra].data4;
+
+    // randomize only in case if at least 2 data fields fits.
+    for (int i = 0; i <= 3; i++)
+        if (rData[i] < kWeaponItemBase || rData[i] >= kItemMax)
+            rData[i] = 0;
+
+    int maxRetries = 9;
+    while ((selected = GetRandDataVal(rData, NULL)) == prevItem) if (maxRetries <= 0) break;
+    if (selected > 0) {
+        spritetype* pSource = pSprite; XSPRITE* pXSource = &xsprite[pSource->extra];
+        pSprite2 = actDropObject(pSprite, selected);
+        pXSource->dropMsg = pSprite2->lotag; // store dropped item lotag in dropMsg
+        
+        if ((pSource->hitag & kHitagExtBit) != 0)
+        {
+            int nXSprite2 = pSprite2->extra;
+            if (nXSprite2 <= 0)
+                nXSprite2 = dbInsertXSprite(pSprite2->index);
+            XSPRITE *pXSprite2 = &xsprite[nXSprite2];
+
+            // inherit spawn sprite trigger settings, so designer can send command when item picked up.
+            pXSprite2->txID = pXSource->txID;
+            pXSprite2->command = pXSource->command;
+            pXSprite2->triggerOn = pXSource->triggerOn;
+            pXSprite2->triggerOff = pXSource->triggerOff;
+
+            pXSprite2->Pickup = true;
+        }
+    }
+
+    return pSprite2;
+}
+
+// this function spawns random dude using dudeSpawn
+spritetype* spawnRandomDude(spritetype* pSprite) {
+    spritetype* pSprite2 = NULL;
+    
+    if (pSprite->extra >= 0) {
+        int rData[4]; int selected = -1;
+        rData[0] = xsprite[pSprite->extra].data1; rData[2] = xsprite[pSprite->extra].data3;
+        rData[1] = xsprite[pSprite->extra].data2; rData[3] = xsprite[pSprite->extra].data4;
+
+        // randomize only in case if at least 2 data fields fits.
+        for (int i = 0; i <= 3; i++)
+            if (rData[i] < kDudeBase || rData[i] >= kDudeMax)
+                rData[i] = 0;
+       
+        if ((selected = GetRandDataVal(rData,NULL)) > 0)
+           pSprite2 = actSpawnDude(pSprite, selected, -1, 0);
+    }
+
+    return pSprite2;
+}
+//-------------------------
+
+// By NoOne: this function plays sound predefined in missile info
+bool sfxPlayMissileSound(spritetype* pSprite, int missileId) {
+    MissileType* pMissType = &missileInfo[missileId - kMissileBase];
+    if (Chance(0x4000))
+        sfxPlay3DSound(pSprite, pMissType->fireSound[0], -1, 0);
+    else
+        sfxPlay3DSound(pSprite, pMissType->fireSound[1], -1, 0);
+
+    return true;
+}
+
+// By NoOne: this function plays sound predefined in vector info
+bool sfxPlayVectorSound(spritetype* pSprite, int vectorId) {
+    VECTORDATA* pVectorData = &gVectorData[vectorId];
+    if (Chance(0x4000))
+        sfxPlay3DSound(pSprite, pVectorData->fireSound[0], -1, 0);
+    else
+        sfxPlay3DSound(pSprite, pVectorData->fireSound[1], -1, 0);
+
+    return true;
+}
+
+// By NoOne: this function allows to spawn new custom dude and inherit spawner settings,
+// so custom dude can have different weapons, hp and so on...
+spritetype* actSpawnCustomDude(spritetype* pSprite, int nDist) {
+
+    spritetype* pSource = pSprite; XSPRITE* pXSource = &xsprite[pSource->extra];
+    spritetype* pDude = actSpawnSprite(pSprite,6); XSPRITE* pXDude = &xsprite[pDude->extra];
+
+    int x, y, z = pSprite->z, nAngle = pSprite->ang, nType = kGDXDudeUniversalCultist;
+
+    if (nDist > 0) {
+        x = pSprite->x + mulscale30r(Cos(nAngle), nDist);
+        y = pSprite->y + mulscale30r(Sin(nAngle), nDist);
+    }
+    else {
+        x = pSprite->x;
+        y = pSprite->y;
+    }
+
+    pDude->lotag = nType; pDude->ang = nAngle;
+    vec3_t pos = { x, y, z }; setsprite(pDude->index, &pos); 
+    pDude->cstat |= 0x1101; pDude->clipdist = dudeInfo[nType - kDudeBase].clipdist;
+
+    // inherit weapon and sound settings.
+    pXDude->data1 = pXSource->data1;
+    pXDude->data3 = pXSource->data3;
+
+    // inherit movement speed.
+    pXDude->busyTime = pXSource->busyTime;
+
+    // inherit custom hp settings
+    if (pXSource->data4 > 0) pXDude->health = pXSource->data4;
+    else pXDude->health = dudeInfo[nType - kDudeBase].startHealth << 4;
+
+    // inherit seq settings
+    int seqId = dudeInfo[nType - kDudeBase].seqStartID;
+    if (pXSource->data2 > 0) seqId = pXSource->data2;
+    pXDude->data2 = seqId;
+
+    if (gSysRes.Lookup(seqId,"SEQ"))
+        seqSpawn(seqId, 3, pDude->extra, -1);
+
+    if ((pSource->hitag & kHitagExtBit) != 0) {
+        //inherit pal?
+        if (pDude->pal <= 0) pDude->pal = pSource->pal;
+
+        // inherit spawn sprite trigger settings, so designer can count monsters.
+        pXDude->txID = pXSource->txID;
+        pXDude->command = pXSource->command;
+        pXDude->triggerOn = pXSource->triggerOn;
+        pXDude->triggerOff = pXSource->triggerOff;
+
+        // inherit drop items
+        pXDude->dropMsg = pXSource->dropMsg;
+
+        // inherit dude flags
+        pXDude->dudeDeaf = pXSource->dudeDeaf;
+        pXDude->dudeGuard = pXSource->dudeGuard;
+        pXDude->dudeAmbush = pXSource->dudeAmbush;
+        pXDude->dudeFlag4 = pXSource->dudeFlag4;
+    }
+
+    aiInitSprite(pDude);
+    return pDude;
+}
+
+int getDudeMassBySpriteSize(spritetype* pSprite) {
+    int mass = 0; int minMass = 5;
+    if (IsDudeSprite(pSprite)) {
+        int picnum = pSprite->picnum; Seq* pSeq = NULL;
+        int seqStartId = dudeInfo[pSprite->lotag - kDudeBase].seqStartID;
+        switch (pSprite->lotag) {
+        case kGDXDudeUniversalCultist:
+        case kGDXGenDudeBurning:
+            seqStartId = xsprite[pSprite->extra].data2;
+            break;
+        }
+
+        
+        DICTNODE* hSeq = gSysRes.Lookup(seqStartId, "SEQ");
+        pSeq = (Seq*)gSysRes.Load(hSeq);
+        if (pSeq != NULL)
+            picnum = seqGetTile(&pSeq->frames[0]);
+
+        int clipDist = pSprite->clipdist;
+        if (clipDist <= 0)
+            clipDist = dudeInfo[pSprite->lotag - kDudeBase].clipdist;
+
+        int xrepeat = pSprite->xrepeat;
+        int x = tilesiz[picnum].x;
+        if (xrepeat > 64) x += ((xrepeat - 64) * 2);
+        else if (xrepeat < 64) x -= ((64 - xrepeat) * 2);
+
+        int yrepeat = pSprite->yrepeat;
+        int y = tilesiz[picnum].y;
+        if (yrepeat > 64) y += ((yrepeat - 64) * 2);
+        else if (yrepeat < 64) y -= ((64 - yrepeat) * 2);
+
+        mass = ((x + y) * clipDist) / 25;
+        //if ((mass+=(x+y)) > 200) mass+=((mass - 200)*16);
+    }
+
+    if (mass < minMass) return minMass;
+    else if (mass > 65000) return 65000;
+    return mass;
+}
+
+
+bool ceilIsTooLow(spritetype* pSprite) {
+    if (pSprite != NULL) {
+
+        sectortype* pSector = &sector[pSprite->sectnum];
+        int a = pSector->ceilingz - pSector->floorz;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        int b = top - bottom;
+        if (a > b) return true;
+    }
+
+    return false;
+}
+
diff --git a/source/blood/src/actor.h b/source/blood/src/actor.h
new file mode 100644
index 000000000..70aefa05d
--- /dev/null
+++ b/source/blood/src/actor.h
@@ -0,0 +1,280 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common_game.h"
+#include "blood.h"
+#include "db.h"
+#include "fx.h"
+#include "gameutil.h"
+
+enum DAMAGE_TYPE {
+    DAMAGE_TYPE_0 = 0,
+    DAMAGE_TYPE_1, // Flame
+    DAMAGE_TYPE_2,
+    DAMAGE_TYPE_3,
+    DAMAGE_TYPE_4,
+    DAMAGE_TYPE_5,
+    DAMAGE_TYPE_6, // Tesla
+};
+
+enum VECTOR_TYPE {
+    VECTOR_TYPE_0 = 0,
+    VECTOR_TYPE_1,
+    VECTOR_TYPE_2,
+    VECTOR_TYPE_3,
+    VECTOR_TYPE_4,
+    VECTOR_TYPE_5,
+    VECTOR_TYPE_6,
+    VECTOR_TYPE_7,
+    VECTOR_TYPE_8,
+    VECTOR_TYPE_9,
+    VECTOR_TYPE_10,
+    VECTOR_TYPE_11,
+    VECTOR_TYPE_12,
+    VECTOR_TYPE_13,
+    VECTOR_TYPE_14,
+    VECTOR_TYPE_15,
+    VECTOR_TYPE_16,
+    VECTOR_TYPE_17,
+    VECTOR_TYPE_18,
+    VECTOR_TYPE_19,
+    VECTOR_TYPE_20,
+    VECTOR_TYPE_21,
+    VECTOR_TYPE_22,
+    kVectorMax,
+};
+
+struct THINGINFO
+{
+    short at0; // health
+    short at2; // mass
+    unsigned char at4; // clipdist
+    short at5; // flags
+    int at7; // elasticity
+    int atb; // damage resistance
+    short atf; // cstat
+    short at11; // picnum
+    char at13; // shade
+    unsigned char at14; // pal
+    unsigned char at15; // xrepeat
+    unsigned char at16; // yrepeat
+    int at17[7]; // damage
+    int allowThrow; // By NoOne: indicates if kGDXCustomDude can throw it
+};
+
+struct AMMOITEMDATA
+{
+    short at0;
+    short picnum; // startHealth
+    char shade; // mass
+    char at5;
+    unsigned char xrepeat; // at6
+    unsigned char yrepeat; // at7
+    short at8;
+    unsigned char ata;
+    unsigned char atb;
+};
+
+struct WEAPONITEMDATA
+{
+    short at0;
+    short picnum; // startHealth
+    char shade; // mass
+    char at5;
+    unsigned char xrepeat; // at6
+    unsigned char yrepeat; // at7
+    short at8;
+    short ata;
+    short atc;
+};
+
+struct ITEMDATA
+{
+    short at0; // unused?
+    short picnum; // startHealth
+    char shade; // mass
+    char at5; // unused?
+    unsigned char xrepeat; // at6
+    unsigned char yrepeat; // at7
+    short at8;
+};
+
+struct MissileType
+{
+    short picnum;
+    int at2; // speed
+    int at6; // angle
+    unsigned char ata; // xrepeat
+    unsigned char atb; // yrepeat
+    char atc; // shade
+    unsigned char atd; // clipdist
+    int fireSound[2]; // By NoOne: predefined fire sounds. used by kGDXCustomDude, but can be used for something else.
+};
+
+struct EXPLOSION
+{
+    unsigned char at0;
+    char at1; // dmg
+    char at2; // dmg rnd
+    int at3; // radius
+    int at7;
+    int atb;
+    int atf;
+    int at13;
+    int at17;
+};
+
+struct VECTORDATA_at1d {
+    FX_ID at0;
+    FX_ID at1;
+    FX_ID at2;
+    int at3;
+};
+
+struct VECTORDATA {
+    DAMAGE_TYPE at0;
+    int at1; // damage
+    int at5;
+    int maxDist; // range
+    int atd;
+    int at11; // burn
+    int at15; // blood splats
+    int at19; // blood splat chance
+    VECTORDATA_at1d at1d[15];
+    int fireSound[2]; // By NoOne: predefined fire sounds. used by kGDXCustomDude, but can be used for something else.
+};
+
+struct SPRITEHIT {
+    int hit, ceilhit, florhit;
+};
+
+extern AMMOITEMDATA gAmmoItemData[];
+extern WEAPONITEMDATA gWeaponItemData[];
+extern ITEMDATA gItemData[];
+extern MissileType missileInfo[];
+extern EXPLOSION explodeInfo[];
+extern THINGINFO thingInfo[];
+extern VECTORDATA gVectorData[];
+
+extern SPRITEHIT gSpriteHit[];
+
+extern int gDudeDrag;
+extern short gAffectedSectors[kMaxSectors];
+extern short gAffectedXWalls[kMaxXWalls];
+
+inline void GetSpriteExtents(spritetype *pSprite, int *top, int *bottom)
+{
+    *top = *bottom = pSprite->z;
+    if ((pSprite->cstat & 0x30) != 0x20)
+    {
+        int height = tilesiz[pSprite->picnum].y;
+        int center = height / 2 + picanm[pSprite->picnum].yofs;
+        *top -= (pSprite->yrepeat << 2)*center;
+        *bottom += (pSprite->yrepeat << 2)*(height - center);
+    }
+}
+
+
+inline bool IsPlayerSprite(spritetype *pSprite)
+{
+    if (pSprite->type >= kDudePlayer1 && pSprite->type <= kDudePlayer8)
+        return 1;
+    return 0;
+}
+
+inline bool IsDudeSprite(spritetype *pSprite)
+{
+    if (pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
+        return 1;
+    return 0;
+}
+
+inline void actBurnSprite(int nSource, XSPRITE *pXSprite, int nTime)
+{
+    pXSprite->burnTime = ClipHigh(pXSprite->burnTime + nTime, sprite[pXSprite->reference].statnum == 6 ? 2400 : 1200);
+    pXSprite->burnSource = nSource;
+}
+
+bool IsItemSprite(spritetype *pSprite);
+bool IsWeaponSprite(spritetype *pSprite);
+bool IsAmmoSprite(spritetype *pSprite);
+bool IsUnderwaterSector(int nSector);
+int actSpriteOwnerToSpriteId(spritetype *pSprite);
+void actPropagateSpriteOwner(spritetype *pTarget, spritetype *pSource);
+int actSpriteIdToOwnerId(int nSprite);
+int actOwnerIdToSpriteId(int nSprite);
+bool actTypeInSector(int nSector, int nType);
+void actAllocateSpares(void);
+void actInit(void);
+void ConcussSprite(int a1, spritetype *pSprite, int x, int y, int z, int a6);
+int actWallBounceVector(int *x, int *y, int nWall, int a4);
+int actFloorBounceVector(int *x, int *y, int *z, int nSector, int a5);
+void sub_2A620(int nSprite, int x, int y, int z, int nSector, int nDist, int a7, int a8, DAMAGE_TYPE a9, int a10, int a11, int a12, int a13);
+void sub_2AA94(spritetype *pSprite, XSPRITE *pXSprite);
+spritetype *actSpawnFloor(spritetype *pSprite);
+spritetype *actDropAmmo(spritetype *pSprite, int nType);
+spritetype *actDropWeapon(spritetype *pSprite, int nType);
+spritetype *actDropItem(spritetype *pSprite, int nType);
+spritetype *actDropKey(spritetype *pSprite, int nType);
+spritetype *actDropFlag(spritetype *pSprite, int nType);
+spritetype *actDropObject(spritetype *pSprite, int nType);
+bool actHealDude(XSPRITE *pXDude, int a2, int a3);
+void actKillDude(int a1, spritetype *pSprite, DAMAGE_TYPE a3, int a4);
+int actDamageSprite(int nSource, spritetype *pSprite, DAMAGE_TYPE a3, int a4);
+void actHitcodeToData(int a1, HITINFO *pHitInfo, int *a3, spritetype **a4, XSPRITE **a5, int *a6, walltype **a7, XWALL **a8, int *a9, sectortype **a10, XSECTOR **a11);
+void actImpactMissile(spritetype *pMissile, int a2);
+void actKickObject(spritetype *pSprite1, spritetype *pSprite2);
+void actTouchFloor(spritetype *pSprite, int nSector);
+void ProcessTouchObjects(spritetype *pSprite, int nXSprite);
+void actAirDrag(spritetype *pSprite, int a2);
+int MoveThing(spritetype *pSprite);
+void MoveDude(spritetype *pSprite);
+int MoveMissile(spritetype *pSprite);
+void actExplodeSprite(spritetype *pSprite);
+void actActivateGibObject(spritetype *pSprite, XSPRITE *pXSprite);
+bool IsUnderWater(spritetype *pSprite);
+void actProcessSprites(void);
+spritetype * actSpawnSprite(int nSector, int x, int y, int z, int nStat, char a6);
+spritetype *actSpawnDude(spritetype *pSource, short nType, int a3, int a4);
+spritetype * actSpawnSprite(spritetype *pSource, int nStat);
+spritetype * actSpawnThing(int nSector, int x, int y, int z, int nThingType);
+spritetype * actFireThing(spritetype *pSprite, int a2, int a3, int a4, int thingType, int a6);
+spritetype* actFireMissile(spritetype *pSprite, int a2, int a3, int a4, int a5, int a6, int nType);
+int actGetRespawnTime(spritetype *pSprite);
+bool actCheckRespawn(spritetype *pSprite);
+bool actCanSplatWall(int nWall);
+void actFireVector(spritetype *pShooter, int a2, int a3, int a4, int a5, int a6, VECTOR_TYPE vectorType);
+void actPostSprite(int nSprite, int nStatus);
+void actPostProcess(void);
+void MakeSplash(spritetype *pSprite, XSPRITE *pXSprite);
+spritetype* DropRandomPickupObject(spritetype* pSprite, short prevItem);
+spritetype* spawnRandomDude(spritetype* pSprite);
+int GetDataVal(spritetype* pSprite, int data);
+int my_random(int a, int b);
+int GetRandDataVal(int *rData, spritetype* pSprite);
+bool sfxPlayMissileSound(spritetype* pSprite, int missileId);
+bool sfxPlayVectorSound(spritetype* pSprite, int vectorId);
+spritetype* actSpawnCustomDude(spritetype* pSprite, int nDist);
+int getDudeMassBySpriteSize(spritetype* pSprite);
+bool ceilIsTooLow(spritetype* pSprite);
\ No newline at end of file
diff --git a/source/blood/src/ai.cpp b/source/blood/src/ai.cpp
new file mode 100644
index 000000000..974c25008
--- /dev/null
+++ b/source/blood/src/ai.cpp
@@ -0,0 +1,1802 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+#include "actor.h"
+#include "ai.h"
+#include "aibat.h"
+#include "aibeast.h"
+#include "aiboneel.h"
+#include "aiburn.h"
+#include "aicaleb.h"
+#include "aicerber.h"
+#include "aicult.h"
+#include "aigarg.h"
+#include "aighost.h"
+#include "aigilbst.h"
+#include "aihand.h"
+#include "aihound.h"
+#include "aiinnoc.h"
+#include "aipod.h"
+#include "airat.h"
+#include "aispid.h"
+#include "aitchern.h"
+#include "aizomba.h"
+#include "aizombf.h"
+#include "aiunicult.h" // By NoOne: add custom dude
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gameutil.h"
+#include "gib.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "player.h"
+#include "seq.h"
+#include "sound.h"
+#include "sfx.h"
+#include "trig.h"
+#include "triggers.h"
+
+int cumulDamage[kMaxXSprites];
+int gDudeSlope[kMaxXSprites];
+DUDEEXTRA gDudeExtra[kMaxXSprites];
+
+AISTATE genIdle = {kAiStateGenIdle, 0, -1, 0, NULL, NULL, NULL, NULL };
+AISTATE genRecoil = {kAiStateRecoil, 5, -1, 20, NULL, NULL, NULL, &genIdle };
+
+int dword_138BB0[5] = {0x2000, 0x4000, 0x8000, 0xa000, 0xe000};
+
+void aiSetGenIdleState(spritetype* pSprite, XSPRITE* pXSprite) {
+    aiNewState(pSprite, pXSprite, &genIdle);
+}
+
+bool sub_5BDA8(spritetype *pSprite, int nSeq)
+{
+    if (pSprite->statnum == 6 && pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
+    {
+        DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+        if (seqGetID(3, pSprite->extra) == pDudeInfo->seqStartID + nSeq && seqGetStatus(3, pSprite->extra) >= 0)
+            return true;
+    }
+    return false;
+}
+
+void aiPlay3DSound(spritetype *pSprite, int a2, AI_SFX_PRIORITY a3, int a4)
+{
+    DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
+    if (a3 == AI_SFX_PRIORITY_0)
+        sfxPlay3DSound(pSprite, a2, a4, 2);
+    else if (a3 > pDudeExtra->at5 || pDudeExtra->at0 <= gFrameClock)
+    {
+        sfxKill3DSound(pSprite, -1, -1);
+        sfxPlay3DSound(pSprite, a2, a4, 0);
+        pDudeExtra->at5 = a3;
+        pDudeExtra->at0 = gFrameClock+120;
+    }
+}
+
+void aiNewState(spritetype *pSprite, XSPRITE *pXSprite, AISTATE *pAIState)
+{
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    pXSprite->stateTimer = pAIState->at8;
+    pXSprite->aiState = pAIState;
+    int seqStartId = pDudeInfo->seqStartID;
+
+    if (pAIState->at0 >= 0) {
+        // By NoOne: Custom dude uses data2 to keep it's seqStartId
+        switch (pSprite->type) {
+            case kGDXDudeUniversalCultist:
+            case kGDXGenDudeBurning:
+                seqStartId = pXSprite->data2;
+                break;
+        }
+        seqStartId += pAIState->at0;
+        if (gSysRes.Lookup(seqStartId, "SEQ"))
+            seqSpawn(seqStartId, 3, pSprite->extra, pAIState->at4);
+    }
+    
+    if (pAIState->atc) pAIState->atc(pSprite, pXSprite); // entry function
+}
+bool dudeIsImmune(spritetype* pSprite, int dmgType) {
+    if (dmgType < 0 || dmgType > 6) return true;
+    else if (dudeInfo[pSprite->type - kDudeBase].startDamage[dmgType] == 0) return true;
+    else if (pSprite->extra >= 0 && xsprite[pSprite->extra].locked == 1) return true;  // if dude is locked, it immune to any dmg.
+    return false;
+}
+bool CanMove(spritetype *pSprite, int a2, int nAngle, int nRange)
+{
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = pSprite->z;
+    HitScan(pSprite, z, Cos(nAngle)>>16, Sin(nAngle)>>16, 0, CLIPMASK0, nRange);
+    int nDist = approxDist(x-gHitInfo.hitx, y-gHitInfo.hity);
+    if (nDist - (pSprite->clipdist << 2) < nRange)
+    {
+        if (gHitInfo.hitsprite < 0 || a2 != gHitInfo.hitsprite)
+            return false;
+        return true;
+    }
+    x += mulscale30(nRange, Cos(nAngle));
+    y += mulscale30(nRange, Sin(nAngle));
+    int nSector = pSprite->sectnum;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    if (!FindSector(x, y, z, &nSector))
+        return false;
+    int floorZ = getflorzofslope(nSector, x, y);
+    int UNUSED(ceilZ) = getceilzofslope(nSector, x, y);
+    int nXSector = sector[nSector].extra;
+    char Underwater = 0; char Water = 0; char Depth = 0; char Crusher = 0;
+    XSECTOR* pXSector = NULL;
+    if (nXSector > 0)
+    {
+        pXSector = &xsector[nXSector];
+        if (pXSector->Underwater)
+            Underwater = 1;
+        if (pXSector->Depth)
+            Depth = 1;
+        if (sector[nSector].lotag == kSecDamage || pXSector->damageType > 0) {
+            // By NoOne: a quick fix for Cerberus spinning in E3M7-like maps, where damage sectors is used.
+            // It makes ignore danger if enemy immune to N damageType. As result Cerberus start acting like
+            // in Blood 1.0 so it can move normally to player. It's up to you for adding rest of enemies here as
+            // i don't think it will broke something in game.
+            switch (pSprite->type) {
+                case 227: // Cerberus
+                case 228: // 1 Head Cerberus
+                    if (VanillaMode() || !dudeIsImmune(pSprite, pXSector->damageType))
+                        Crusher = 1;
+                    break;
+                default:
+                    Crusher = 1;
+                    break;
+            }
+        }
+    }
+    int nUpper = gUpperLink[nSector];
+    int nLower = gLowerLink[nSector];
+    if (nUpper >= 0)
+    {
+        if (sprite[nUpper].type == kMarkerUpWater || sprite[nUpper].type == kMarkerUpGoo)
+            Water = Depth = 1;
+    }
+    if (nLower >= 0)
+    {
+        if (sprite[nLower].type == kMarkerLowWater || sprite[nLower].type == kMarkerLowGoo)
+            Depth = 1;
+    }
+    switch (pSprite->type)
+    {
+    case 206:
+    case 207:
+    case 219:
+        if (pSprite->clipdist > nDist)
+            return 0;
+        if (Depth)
+        {
+            // Ouch...
+            if (Depth)
+                return false;
+            if (Crusher)
+                return false;
+        }
+        break;
+    case 218:
+        if (Water)
+            return false;
+        if (!Underwater)
+            return false;
+        if (Underwater)
+            return true;
+        break;
+    case 204:
+    case 213:
+    case 214:
+    case 215:
+    case 216:
+    case 211:
+    case 220:
+    case 227:
+    case 245:
+        if (Crusher)
+            return false;
+        if (Depth || Underwater)
+            return false;
+        if (floorZ - bottom > 0x2000)
+            return false;
+        break;
+    case 203:
+    case 210:
+    case 217:
+    case kGDXDudeUniversalCultist:
+    case kGDXGenDudeBurning:
+        if ((Crusher && !dudeIsImmune(pSprite, pXSector->damageType)) || xsprite[pSprite->extra].dudeGuard) return false;
+        return true;
+    default:
+        if (Crusher)
+            return false;
+        if ((nXSector < 0 || (!xsector[nXSector].Underwater && !xsector[nXSector].Depth)) && floorZ - bottom > 0x2000)
+            return false;
+        break;
+    }
+    return 1;
+}
+
+void aiChooseDirection(spritetype *pSprite, XSPRITE *pXSprite, int a3)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    int vc = ((a3+1024-pSprite->ang)&2047)-1024;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int UNUSED(t2) = dmulscale30(dx, nSin, -dy, nCos);
+    int vsi = ((t1*15)>>12) / 2;
+    int v8 = 341;
+    if (vc < 0)
+        v8 = -341;
+    if (CanMove(pSprite, pXSprite->target, pSprite->ang+vc, vsi))
+        pXSprite->goalAng = pSprite->ang+vc;
+    else if (CanMove(pSprite, pXSprite->target, pSprite->ang+vc/2, vsi))
+        pXSprite->goalAng = pSprite->ang+vc/2;
+    else if (CanMove(pSprite, pXSprite->target, pSprite->ang-vc/2, vsi))
+        pXSprite->goalAng = pSprite->ang-vc/2;
+    else if (CanMove(pSprite, pXSprite->target, pSprite->ang+v8, vsi))
+        pXSprite->goalAng = pSprite->ang+v8;
+    else if (CanMove(pSprite, pXSprite->target, pSprite->ang, vsi))
+        pXSprite->goalAng = pSprite->ang;
+    else if (CanMove(pSprite, pXSprite->target, pSprite->ang-v8, vsi))
+        pXSprite->goalAng = pSprite->ang-v8;
+    else if (pSprite->hitag&2)
+        pXSprite->goalAng = pSprite->ang+341;
+    else // Weird..
+        pXSprite->goalAng = pSprite->ang+341;
+    if (Chance(0x8000))
+        pXSprite->dodgeDir = 1;
+    else
+        pXSprite->dodgeDir = -1;
+    if (!CanMove(pSprite, pXSprite->target, pSprite->ang+pXSprite->dodgeDir*512, 512))
+    {
+        pXSprite->dodgeDir = -pXSprite->dodgeDir;
+        if (!CanMove(pSprite, pXSprite->target, pSprite->ang+pXSprite->dodgeDir*512, 512))
+            pXSprite->dodgeDir = 0;
+    }
+}
+
+void aiMoveForward(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (klabs(nAng) > 341)
+        return;
+    xvel[nSprite] += mulscale30(pDudeInfo->frontSpeed, Cos(pSprite->ang));
+    yvel[nSprite] += mulscale30(pDudeInfo->frontSpeed, Sin(pSprite->ang));
+}
+
+void aiMoveTurn(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+}
+
+void aiMoveDodge(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (pXSprite->dodgeDir)
+    {
+        int nCos = Cos(pSprite->ang);
+        int nSin = Sin(pSprite->ang);
+        int dx = xvel[nSprite];
+        int dy = yvel[nSprite];
+        int t1 = dmulscale30(dx, nCos, dy, nSin);
+        int t2 = dmulscale30(dx, nSin, -dy, nCos);
+        if (pXSprite->dodgeDir > 0)
+            t2 += pDudeInfo->sideSpeed;
+        else
+            t2 -= pDudeInfo->sideSpeed;
+
+        xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+        yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    }
+}
+
+void aiActivateDude(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    if (!pXSprite->state)
+    {
+        aiChooseDirection(pSprite, pXSprite, getangle(pXSprite->targetX-pSprite->x, pXSprite->targetY-pSprite->y));
+        pXSprite->state = 1;
+    }
+    switch (pSprite->type)
+    {
+    case 210:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &ghostSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 1600, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &ghostChase);
+        }
+        break;
+    }
+    case 201:
+    case 202:
+    case 247:
+    case 248:
+    case 249:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+        {
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &cultistSearch);
+                if (Chance(0x8000))
+                {
+                    if (pSprite->type == 201)
+                        aiPlay3DSound(pSprite, 4008+Random(5), AI_SFX_PRIORITY_1, -1);
+                    else
+                        aiPlay3DSound(pSprite, 1008+Random(5), AI_SFX_PRIORITY_1, -1);
+                }
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &cultistSwimSearch);
+                break;
+            }
+        }
+        else
+        {
+            if (Chance(0x8000))
+            {
+                if (pSprite->type == 201)
+                    aiPlay3DSound(pSprite, 4003+Random(4), AI_SFX_PRIORITY_1, -1);
+                else
+                    aiPlay3DSound(pSprite, 1003+Random(4), AI_SFX_PRIORITY_1, -1);
+            }
+            switch (pXSprite->medium)
+            {
+            case 0:
+                if (pSprite->type == 201)
+                    aiNewState(pSprite, pXSprite, &fanaticChase);
+                else
+                    aiNewState(pSprite, pXSprite, &cultistChase);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &cultistSwimChase);
+                break;
+            }
+        }
+        break;
+    }
+
+    case kGDXDudeUniversalCultist:
+    {
+        DUDEEXTRA_at6_u1* pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1) {
+            if (spriteIsUnderwater(pSprite, false))
+                aiNewState(pSprite, pXSprite, &GDXGenDudeSearchW);
+            else {
+                aiNewState(pSprite, pXSprite, &GDXGenDudeSearchL);
+                if (Chance(0x4000))
+                    sfxPlayGDXGenDudeSound(pSprite, 0, pXSprite->data3);
+            }
+        }
+        else {
+            if (Chance(0x4000))
+                sfxPlayGDXGenDudeSound(pSprite, 0, pXSprite->data3);
+
+            if (spriteIsUnderwater(pSprite, false))
+                aiNewState(pSprite, pXSprite, &GDXGenDudeChaseW);
+            else
+                aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+
+        }
+        break;
+    }
+    case kGDXGenDudeBurning:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnSearch);
+        else
+            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnChase);
+    break;
+    case 230:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        pSprite->type = 201;
+        if (pXSprite->target == -1)
+        {
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &cultistSearch);
+                if (Chance(0x8000))
+                    aiPlay3DSound(pSprite, 4008+Random(5), AI_SFX_PRIORITY_1, -1);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &cultistSwimSearch);
+                break;
+            }
+        }
+        else
+        {
+            if (Chance(0x8000))
+                aiPlay3DSound(pSprite, 4008+Random(5), AI_SFX_PRIORITY_1, -1);
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &cultistProneChase);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &cultistSwimChase);
+                break;
+            }
+        }
+        break;
+    }
+    case 246:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        pSprite->type = 202;
+        if (pXSprite->target == -1)
+        {
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &cultistSearch);
+                if (Chance(0x8000))
+                    aiPlay3DSound(pSprite, 1008+Random(5), AI_SFX_PRIORITY_1, -1);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &cultistSwimSearch);
+                break;
+            }
+        }
+        else
+        {
+            if (Chance(0x8000))
+                aiPlay3DSound(pSprite, 1003+Random(4), AI_SFX_PRIORITY_1, -1);
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &cultistProneChase);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &cultistSwimChase);
+                break;
+            }
+        }
+        break;
+    }
+    case 240:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &cultistBurnSearch);
+        else
+            aiNewState(pSprite, pXSprite, &cultistBurnChase);
+        break;
+    case 219:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (!pSprite->hitag)
+            pSprite->hitag = 9;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &batSearch);
+        else
+        {
+            if (Chance(0xa000))
+                aiPlay3DSound(pSprite, 2000, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &batChase);
+        }
+        break;
+    }
+    case 218:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &eelSearch);
+        else
+        {
+            if (Chance(0x8000))
+                aiPlay3DSound(pSprite, 1501, AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 1500, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &eelChase);
+        }
+        break;
+    }
+    case 217:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        XSECTOR *pXSector = NULL;
+        if (sector[pSprite->sectnum].extra > 0)
+            pXSector = &xsector[sector[pSprite->sectnum].extra];
+        pDudeExtraE->at0 = 0;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 1;
+        if (pXSprite->target == -1)
+        {
+            if (pXSector && pXSector->Underwater)
+                aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+            else
+                aiNewState(pSprite, pXSprite, &gillBeastSearch);
+        }
+        else
+        {
+            if (Chance(0x4000))
+                aiPlay3DSound(pSprite, 1701, AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 1700, AI_SFX_PRIORITY_1, -1);
+            if (pXSector && pXSector->Underwater)
+                aiNewState(pSprite, pXSprite, &gillBeastSwimChase);
+            else
+                aiNewState(pSprite, pXSprite, &gillBeastChase);
+        }
+        break;
+    }
+    case 203:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+        pDudeExtraE->at4 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &zombieASearch);
+        else
+        {
+            if (Chance(0xa000))
+            {
+                switch (Random(3))
+                {
+                default:
+                case 0:
+                case 3:
+                    aiPlay3DSound(pSprite, 1103, AI_SFX_PRIORITY_1, -1);
+                    break;
+                case 1:
+                    aiPlay3DSound(pSprite, 1104, AI_SFX_PRIORITY_1, -1);
+                    break;
+                case 2:
+                    aiPlay3DSound(pSprite, 1105, AI_SFX_PRIORITY_1, -1);
+                    break;
+                }
+            }
+            aiNewState(pSprite, pXSprite, &zombieAChase);
+        }
+        break;
+    }
+    case 205:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+        pDudeExtraE->at4 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->aiState == &zombieEIdle)
+            aiNewState(pSprite, pXSprite, &zombieEUp);
+        break;
+    }
+    case 244:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+        pDudeExtraE->at4 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->aiState == &zombieSIdle)
+            aiNewState(pSprite, pXSprite, &zombie13AC2C);
+        break;
+    }
+    case 204:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+        pDudeExtraE->at4 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &zombieFSearch);
+        else
+        {
+            if (Chance(0x4000))
+                aiPlay3DSound(pSprite, 1201, AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 1200, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &zombieFChase);
+        }
+        break;
+    }
+    case 241:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &zombieABurnSearch);
+        else
+            aiNewState(pSprite, pXSprite, &zombieABurnChase);
+        break;
+    case 242:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &zombieFBurnSearch);
+        else
+            aiNewState(pSprite, pXSprite, &zombieFBurnChase);
+        break;
+    case 206:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &gargoyleFSearch);
+        else
+        {
+            if (Chance(0x4000))
+                aiPlay3DSound(pSprite, 1401, AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &gargoyleFChase);
+        }
+        break;
+    }
+    case 207:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &gargoyleFSearch);
+        else
+        {
+            if (Chance(0x4000))
+                aiPlay3DSound(pSprite, 1451, AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 1450, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &gargoyleFChase);
+        }
+        break;
+    }
+    case 208:
+    case 209:
+        // By NoOne: play gargoyle statue breaking animation if data1 = 1.
+        if (pXSprite->data1 == 1) {
+            if (pSprite->type == 208)
+                aiNewState(pSprite, pXSprite, &statueFBreakSEQ);
+            else
+                aiNewState(pSprite, pXSprite, &statueSBreakSEQ);
+        } else {
+            if (Chance(0x4000)) aiPlay3DSound(pSprite, 1401, AI_SFX_PRIORITY_1, -1);
+            else aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);
+            
+            if (pSprite->type == 208) aiNewState(pSprite, pXSprite, &gargoyleFMorph);
+            else aiNewState(pSprite, pXSprite, &gargoyleSMorph);
+        }
+        break;
+    case 227:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &cerberusSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 2300, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &cerberusChase);
+        }
+        break;
+    case 228:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &cerberus2Search);
+        else
+        {
+            aiPlay3DSound(pSprite, 2300, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &cerberus2Chase);
+        }
+        break;
+    case 211:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &houndSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 1300, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &houndChase);
+        }
+        break;
+    case 212:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &handSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 1900, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &handChase);
+        }
+        break;
+    case 220:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &ratSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 2100, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &ratChase);
+        }
+        break;
+    case 245:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &innocentSearch);
+        else
+        {
+            if (pXSprite->health > 0)
+                aiPlay3DSound(pSprite, 7000+Random(6), AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &innocentChase);
+        }
+        break;
+    case 229:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &tchernobogSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 2350+Random(7), AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &tchernobogChase);
+        }
+        break;
+    case 213:
+    case 214:
+    case 215:
+        pSprite->hitag |= 2;
+        pSprite->cstat &= ~8;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &spidSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 1800, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &spidChase);
+        }
+        break;
+    case 216:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 1;
+        pDudeExtraE->at0 = 0;
+        pSprite->hitag |= 2;
+        pSprite->cstat &= ~8;
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &spidSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 1853+Random(1), AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &spidChase);
+        }
+        break;
+    }
+    case 250:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+        pDudeExtraE->at4 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+        {
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &tinycalebSearch);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+                break;
+            }
+        }
+        else
+        {
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &tinycalebChase);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &tinycalebSwimChase);
+                break;
+            }
+        }
+        break;
+    }
+    case 251:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+        pDudeExtraE->at4 = 1;
+        pDudeExtraE->at0 = 0;
+        if (pXSprite->target == -1)
+        {
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &beastSearch);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &beastSwimSearch);
+                break;
+            }
+        }
+        else
+        {
+            aiPlay3DSound(pSprite, 9009+Random(2), AI_SFX_PRIORITY_1, -1);
+            switch (pXSprite->medium)
+            {
+            case 0:
+                aiNewState(pSprite, pXSprite, &beastChase);
+                break;
+            case 1:
+            case 2:
+                aiNewState(pSprite, pXSprite, &beastSwimChase);
+                break;
+            }
+        }
+        break;
+    }
+    case 221:
+    case 223:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &podSearch);
+        else
+        {
+            if (pSprite->type == 223)
+                aiPlay3DSound(pSprite, 2453, AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 2473, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &podChase);
+        }
+        break;
+    case 222:
+    case 224:
+        if (pXSprite->target == -1)
+            aiNewState(pSprite, pXSprite, &tentacleSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 2503, AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &tentacleChase);
+        }
+        break;
+    }
+}
+
+void aiSetTarget(XSPRITE *pXSprite, int x, int y, int z)
+{
+    pXSprite->target = -1;
+    pXSprite->targetX = x;
+    pXSprite->targetY = y;
+    pXSprite->targetZ = z;
+}
+
+void aiSetTarget(XSPRITE *pXSprite, int nTarget)
+{
+    dassert(nTarget >= 0 && nTarget < kMaxSprites);
+    spritetype *pTarget = &sprite[nTarget];
+    if (pTarget->type >= kDudeBase && pTarget->type < kDudeMax)
+    {
+        if (actSpriteOwnerToSpriteId(&sprite[pXSprite->reference]) != nTarget)
+        {
+            pXSprite->target = nTarget;
+            DUDEINFO *pDudeInfo = &dudeInfo[pTarget->type-kDudeBase];
+            pXSprite->targetX = pTarget->x;
+            pXSprite->targetY = pTarget->y;
+            pXSprite->targetZ = pTarget->z-((pDudeInfo->eyeHeight*pTarget->yrepeat)<<2);
+        }
+    }
+}
+
+
+int aiDamageSprite(spritetype *pSprite, XSPRITE *pXSprite, int nSource, DAMAGE_TYPE nDmgType, int nDamage)
+{
+    dassert(nSource < kMaxSprites);
+    if (!pXSprite->health)
+        return 0;
+    pXSprite->health = ClipLow(pXSprite->health - nDamage, 0);
+    cumulDamage[pSprite->extra] += nDamage;
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int nSprite = pXSprite->reference;
+    if (nSource >= 0)
+    {
+        spritetype *pSource = &sprite[nSource];
+        if (pSprite == pSource)
+            return 0;
+        if (pXSprite->target == -1 || (nSource != pXSprite->target && Chance(pSprite->type == pSource->type ? nDamage*pDudeInfo->changeTargetKin : nDamage*pDudeInfo->changeTarget)))
+        {
+            aiSetTarget(pXSprite, nSource);
+            aiActivateDude(pSprite, pXSprite);
+        }
+        if (nDmgType == DAMAGE_TYPE_6)
+        {
+            DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
+            pDudeExtra->at4 = 1;
+        }
+        switch (pSprite->type)
+        {
+        case 201:
+        case 202:
+        case 247:
+        case 248:
+            if (nDmgType != DAMAGE_TYPE_1)
+            {
+                if (!sub_5BDA8(pSprite, 14) && !pXSprite->medium)
+                    aiNewState(pSprite, pXSprite, &cultistDodge);
+                else if (sub_5BDA8(pSprite, 14) && !pXSprite->medium)
+                    aiNewState(pSprite, pXSprite, &cultistProneDodge);
+                else if (sub_5BDA8(pSprite, 13) && (pXSprite->medium == 1 || pXSprite->medium == 2))
+                    aiNewState(pSprite, pXSprite, &cultistSwimDodge);
+            }
+            else if (nDmgType == DAMAGE_TYPE_1 && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/)
+            {
+                pSprite->type = 240;
+                aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
+                aiPlay3DSound(pSprite, 1031+Random(2), AI_SFX_PRIORITY_2, -1);
+                gDudeExtra[pSprite->extra].at0 = gFrameClock+360;
+                actHealDude(pXSprite, dudeInfo[40].startHealth, dudeInfo[40].startHealth);
+                evKill(nSprite, 3, CALLBACK_ID_0);
+            }
+            break;
+        case 245: // innocent
+            if (nDmgType == DAMAGE_TYPE_1 && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/)
+            {
+                pSprite->type = 239;
+                aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
+                gDudeExtra[pSprite->extra].at0 = gFrameClock+360;
+                actHealDude(pXSprite, dudeInfo[39].startHealth, dudeInfo[39].startHealth);
+                evKill(nSprite, 3, CALLBACK_ID_0);
+            }
+            break;
+        case 240:
+            if (Chance(0x4000) && gDudeExtra[pSprite->extra].at0 < gFrameClock)
+            {
+                aiPlay3DSound(pSprite, 1031+Random(2), AI_SFX_PRIORITY_2, -1);
+                gDudeExtra[pSprite->extra].at0 = gFrameClock+360;
+            }
+            if (Chance(0x600) && (pXSprite->medium == 1 || pXSprite->medium == 2))
+            {
+                pSprite->type = 201;
+                pXSprite->burnTime = 0;
+                aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+            }
+            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+            {
+                pSprite->type = 202;
+                pXSprite->burnTime = 0;
+                aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+            }
+            break;
+        case 206:
+            aiNewState(pSprite, pXSprite, &gargoyleFChase);
+            break;
+        case 204:
+            if (nDmgType == DAMAGE_TYPE_1 && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth)
+            {
+                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
+                aiPlay3DSound(pSprite, 1202, AI_SFX_PRIORITY_2, -1);
+                pSprite->type = 242;
+                aiNewState(pSprite, pXSprite, &zombieFBurnGoto);
+                actHealDude(pXSprite, dudeInfo[42].startHealth, dudeInfo[42].startHealth);
+                evKill(nSprite, 3, CALLBACK_ID_0);
+            }
+            break;
+        case 250: // tiny Caleb
+            if (nDmgType == DAMAGE_TYPE_1 && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/)
+            {
+                pSprite->type = 239;
+                if (!VanillaMode())
+                    pXSprite->scale = -4; // need to change this to 64 later
+                aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
+                gDudeExtra[pSprite->extra].at0 = gFrameClock+360;
+                actHealDude(pXSprite, dudeInfo[39].startHealth, dudeInfo[39].startHealth);
+                evKill(nSprite, 3, CALLBACK_ID_0);
+            }
+            break;
+        case kGDXGenDudeBurning:
+            if (Chance(0x2000) && gDudeExtra[pSprite->extra].at0 < gFrameClock) {
+                sfxPlayGDXGenDudeSound(pSprite, 3, pXSprite->data3);
+                gDudeExtra[pSprite->extra].at0 = gFrameClock + 360;
+            }
+            if (pXSprite->burnTime == 0) pXSprite->burnTime = 2400;
+            if (spriteIsUnderwater(pSprite, false)) {
+                pSprite->type = kGDXDudeUniversalCultist;
+                pXSprite->burnTime = 0;
+                pXSprite->health = 1; // so it can be killed with flame weapons while underwater and if already was burning dude before.
+                aiNewState(pSprite, pXSprite, &GDXGenDudeGotoW);
+            }
+            break;
+        case kGDXDudeUniversalCultist:
+        {
+            if (nDmgType == DAMAGE_TYPE_1) {
+                if (pXSprite->health <= pDudeInfo->fleeHealth) {
+                    if (getNextIncarnation(pXSprite) == NULL) {
+                        removeDudeStuff(pSprite);
+                        
+                        if (pXSprite->data1 >= 459 && pXSprite->data1 < (459 + kExplodeMax) - 1)
+                            doExplosion(pSprite, pXSprite->data1 - 459);
+                        
+                        if (spriteIsUnderwater(pSprite, false)) {
+                            pXSprite->health = 0;
+                            break;
+                        }
+                        
+                        if (pXSprite->burnTime <= 0) 
+                            pXSprite->burnTime = 1200;
+
+                        if ((gSysRes.Lookup(pXSprite->data2 + 15, "SEQ") || gSysRes.Lookup(pXSprite->data2 + 16, "SEQ"))
+                            && gSysRes.Lookup(pXSprite->data2 + 3, "SEQ")) {
+                            
+                            aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
+                            sfxPlayGDXGenDudeSound(pSprite, 3, pXSprite->data3);
+                            pSprite->type = kGDXGenDudeBurning;
+
+                            if (pXSprite->data2 == 11520) // don't inherit palette for burning if using default animation
+                                pSprite->pal = 0;
+
+                            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnGoto);
+                            actHealDude(pXSprite, dudeInfo[55].startHealth, dudeInfo[55].startHealth);
+                            gDudeExtra[pSprite->extra].at0 = gFrameClock + 360;
+                            evKill(nSprite, 3, CALLBACK_ID_0);
+
+                        }
+
+                    } else {
+                        actKillDude(nSource, pSprite, nDmgType, nDamage);
+                    }
+                }
+            } else if (pXSprite->aiState != &GDXGenDudeDodgeDmgD && pXSprite->aiState != &GDXGenDudeDodgeDmgL
+                 && pXSprite->aiState != &GDXGenDudeDodgeDmgW) {
+                    
+                if (Chance(getDodgeChance(pSprite))) {
+                    if (!spriteIsUnderwater(pSprite, false)) {
+                        if (!sub_5BDA8(pSprite, 14)) aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeDmgL);
+                        else aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeDmgD);
+                    }
+                    else if (sub_5BDA8(pSprite, 13) && spriteIsUnderwater(pSprite, false))
+                        aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeDmgW);
+                }
+            }
+
+            break;
+        }
+        case 249:
+            if (pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth)
+            {
+                pSprite->type = 251;
+                aiPlay3DSound(pSprite, 9008, AI_SFX_PRIORITY_1, -1);
+                aiNewState(pSprite, pXSprite, &beastMorphFromCultist);
+                actHealDude(pXSprite, dudeInfo[51].startHealth, dudeInfo[51].startHealth);
+            }
+            break;
+        case 203:
+        case 205:
+            if (nDmgType == DAMAGE_TYPE_1 && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth)
+            {
+                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
+                aiPlay3DSound(pSprite, 1106, AI_SFX_PRIORITY_2, -1);
+                pSprite->type = 241;
+                aiNewState(pSprite, pXSprite, &zombieABurnGoto);
+                actHealDude(pXSprite, dudeInfo[41].startHealth, dudeInfo[41].startHealth);
+                evKill(nSprite, 3, CALLBACK_ID_0);
+            }
+            break;
+        }
+    }
+    return nDamage;
+}
+
+void RecoilDude(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    char v4 = Chance(0x8000);
+    DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
+    if (pSprite->statnum == 6 && (pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+    {
+        DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+        switch (pSprite->type)
+        {
+        case kGDXDudeUniversalCultist:
+        {
+            int mass = getDudeMassBySpriteSize(pSprite); int chance3 = getRecoilChance(pSprite);
+            if ((mass < 155 && !spriteIsUnderwater(pSprite, false) && pDudeExtra->at4) || (mass > 155 && Chance(chance3)))
+            {
+                sfxPlayGDXGenDudeSound(pSprite, 1, pXSprite->data3);
+                
+                if (gSysRes.Lookup(pXSprite->data2 + 4, "SEQ")) aiNewState(pSprite, pXSprite, &GDXGenDudeRTesla);
+                else if (!v4 || (v4 && gGameOptions.nDifficulty == 0)) aiNewState(pSprite, pXSprite, &GDXGenDudeRecoilD);
+                else if (spriteIsUnderwater(pSprite, false)) aiNewState(pSprite, pXSprite, &GDXGenDudeRecoilW);
+                else aiNewState(pSprite, pXSprite, &GDXGenDudeRecoilL);
+
+                return;
+            }
+
+            if (pXSprite->aiState == &GDXGenDudeDodgeDmgW || pXSprite->aiState == &GDXGenDudeDodgeDmgD 
+                || pXSprite->aiState == &GDXGenDudeDodgeDmgL) {
+                    if (Chance(chance3)) sfxPlayGDXGenDudeSound(pSprite, 1, pXSprite->data3);
+                    return;
+            }
+
+            if ((!dudeIsMelee(pXSprite) && mass < 155) || Chance(chance3)) {
+
+                sfxPlayGDXGenDudeSound(pSprite, 1, pXSprite->data3);
+
+                if (!v4 || (v4 && gGameOptions.nDifficulty == 0)) aiNewState(pSprite, pXSprite, &GDXGenDudeRecoilD);
+                else if (spriteIsUnderwater(pSprite, false)) aiNewState(pSprite, pXSprite, &GDXGenDudeRecoilW);
+                else aiNewState(pSprite, pXSprite, &GDXGenDudeRecoilL);
+
+            }
+
+            break;
+        }
+        case 201:
+        case 202:
+        case 247:
+        case 248:
+        case 249:
+            if (pSprite->type == 201)
+                aiPlay3DSound(pSprite, 4013+Random(2), AI_SFX_PRIORITY_2, -1);
+            else
+                aiPlay3DSound(pSprite, 1013+Random(2), AI_SFX_PRIORITY_2, -1);
+            if (!v4 && pXSprite->medium == 0)
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &cultistTeslaRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &cultistRecoil);
+            }
+            else if (v4 && pXSprite->medium == 0)
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &cultistTeslaRecoil);
+                else if (gGameOptions.nDifficulty > 0)
+                    aiNewState(pSprite, pXSprite, &cultistProneRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &cultistRecoil);
+            }
+            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                aiNewState(pSprite, pXSprite, &cultistSwimRecoil);
+            else
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &cultistTeslaRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &cultistRecoil);
+            }
+            break;
+        case 240:
+            aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+            break;
+        case kGDXGenDudeBurning:
+            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnGoto);
+            break;
+        case 204:
+            aiPlay3DSound(pSprite, 1202, AI_SFX_PRIORITY_2, -1);
+            if (pDudeExtra->at4)
+                aiNewState(pSprite, pXSprite, &zombieFTeslaRecoil);
+            else
+                aiNewState(pSprite, pXSprite, &zombieFRecoil);
+            break;
+        case 203:
+        case 205:
+            aiPlay3DSound(pSprite, 1106, AI_SFX_PRIORITY_2, -1);
+            if (pDudeExtra->at4 && pXSprite->cumulDamage > pDudeInfo->startHealth/3)
+                aiNewState(pSprite, pXSprite, &zombieATeslaRecoil);
+            else if (pXSprite->cumulDamage > pDudeInfo->startHealth/3)
+                aiNewState(pSprite, pXSprite, &zombieARecoil2);
+            else
+                aiNewState(pSprite, pXSprite, &zombieARecoil);
+            break;
+        case 241:
+            aiPlay3DSound(pSprite, 1106, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &zombieABurnGoto);
+            break;
+        case 242:
+            aiPlay3DSound(pSprite, 1202, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &zombieFBurnGoto);
+            break;
+        case 206:
+        case 207:
+            aiPlay3DSound(pSprite, 1402, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &gargoyleFRecoil);
+            break;
+        case 227:
+            aiPlay3DSound(pSprite, 2302+Random(2), AI_SFX_PRIORITY_2, -1);
+            if (pDudeExtra->at4 && pXSprite->cumulDamage > pDudeInfo->startHealth/3)
+                aiNewState(pSprite, pXSprite, &cerberusTeslaRecoil);
+            else
+                aiNewState(pSprite, pXSprite, &cerberusRecoil);
+            break;
+        case 228:
+            aiPlay3DSound(pSprite, 2302+Random(2), AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &cerberus2Recoil);
+            break;
+        case 211:
+            aiPlay3DSound(pSprite, 1302, AI_SFX_PRIORITY_2, -1);
+            if (pDudeExtra->at4)
+                aiNewState(pSprite, pXSprite, &houndTeslaRecoil);
+            else
+                aiNewState(pSprite, pXSprite, &houndRecoil);
+            break;
+        case 229:
+            aiPlay3DSound(pSprite, 2370+Random(2), AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &tchernobogRecoil);
+            break;
+        case 212:
+            aiPlay3DSound(pSprite, 1902, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &handRecoil);
+            break;
+        case 220:
+            aiPlay3DSound(pSprite, 2102, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &ratRecoil);
+            break;
+        case 219:
+            aiPlay3DSound(pSprite, 2002, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &batRecoil);
+            break;
+        case 218:
+            aiPlay3DSound(pSprite, 1502, AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &eelRecoil);
+            break;
+        case 217:
+        {
+            XSECTOR *pXSector = NULL;
+            if (sector[pSprite->sectnum].extra > 0)
+                pXSector = &xsector[sector[pSprite->sectnum].extra];
+            aiPlay3DSound(pSprite, 1702, AI_SFX_PRIORITY_2, -1);
+            if (pXSector && pXSector->Underwater)
+                aiNewState(pSprite, pXSprite, &gillBeastSwimRecoil);
+            else
+                aiNewState(pSprite, pXSprite, &gillBeastRecoil);
+            break;
+        }
+        case 210:
+            aiPlay3DSound(pSprite, 1602, AI_SFX_PRIORITY_2, -1);
+            if (pDudeExtra->at4)
+                aiNewState(pSprite, pXSprite, &ghostTeslaRecoil);
+            else
+                aiNewState(pSprite, pXSprite, &ghostRecoil);
+            break;
+        case 213:
+        case 214:
+        case 215:
+            aiPlay3DSound(pSprite, 1802+Random(1), AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &spidDodge);
+            break;
+        case 216:
+            aiPlay3DSound(pSprite, 1851+Random(1), AI_SFX_PRIORITY_2, -1);
+            aiNewState(pSprite, pXSprite, &spidDodge);
+            break;
+        case 245:
+            aiPlay3DSound(pSprite, 7007+Random(2), AI_SFX_PRIORITY_2, -1);
+            if (pDudeExtra->at4)
+                aiNewState(pSprite, pXSprite, &innocentTeslaRecoil);
+            else
+                aiNewState(pSprite, pXSprite, &innocentRecoil);
+            break;
+        case 250:
+            if (pXSprite->medium == 0)
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &tinycalebTeslaRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &tinycalebRecoil);
+            }
+            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                aiNewState(pSprite, pXSprite, &tinycalebSwimRecoil);
+            else
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &tinycalebTeslaRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &tinycalebRecoil);
+            }
+            break;
+        case 251:
+            aiPlay3DSound(pSprite, 9004+Random(2), AI_SFX_PRIORITY_2, -1);
+            if (pXSprite->medium == 0)
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &beastTeslaRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &beastRecoil);
+            }
+            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                aiNewState(pSprite, pXSprite, &beastSwimRecoil);
+            else
+            {
+                if (pDudeExtra->at4)
+                    aiNewState(pSprite, pXSprite, &beastTeslaRecoil);
+                else
+                    aiNewState(pSprite, pXSprite, &beastRecoil);
+            }
+            break;
+        case 221:
+        case 223:
+            aiNewState(pSprite, pXSprite, &podRecoil);
+            break;
+        case 222:
+        case 224:
+            aiNewState(pSprite, pXSprite, &tentacleRecoil);
+            break;
+        default:
+            aiNewState(pSprite, pXSprite, &genRecoil);
+            break;
+        }
+        pDudeExtra->at4 = 0;
+    }
+}
+
+void aiThinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pSprite->owner == pPlayer->at5b || pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+        }
+    }
+}
+
+void sub_5F15C(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pSprite->owner == pPlayer->at5b || pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+        }
+        if (pXSprite->state)
+        {
+            char va4[(kMaxSectors+7)>>3];
+            gAffectedSectors[0] = 0;
+            gAffectedXWalls[0] = 0;
+            GetClosestSpriteSectors(pSprite->sectnum, pSprite->x, pSprite->y, 400, gAffectedSectors, va4, gAffectedXWalls);
+            for (int nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+            {
+                spritetype *pSprite2 = &sprite[nSprite2];
+                int dx = pSprite2->x-pSprite->x;
+                int dy = pSprite2->y-pSprite->y;
+                int nDist = approxDist(dx, dy);
+                if (pSprite2->type == 245)
+                {
+                    DUDEINFO *pDudeInfo = &dudeInfo[pSprite2->type-kDudeBase];
+                    if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                        continue;
+                    int UNUSED(nAngle) = getangle(dx,dy);
+                    aiSetTarget(pXSprite, pSprite2->index);
+                    aiActivateDude(pSprite, pXSprite);
+                    return;
+                }
+            }
+        }
+    }
+}
+
+void aiProcessDudes(void)
+{
+    for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag&32)
+            continue;
+        int nXSprite = pSprite->extra;
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+        if (pSprite->type >= kDudePlayer1 && pSprite->type <= kDudePlayer8)
+            continue;
+        if (pXSprite->health == 0)
+            continue;
+        pXSprite->stateTimer = ClipLow(pXSprite->stateTimer-4, 0);
+        if (pXSprite->aiState->at10)
+            pXSprite->aiState->at10(pSprite, pXSprite);
+        if (pXSprite->aiState->at14 && (gFrame&3) == (nSprite&3))
+            pXSprite->aiState->at14(pSprite, pXSprite);
+        if (pXSprite->stateTimer == 0 && pXSprite->aiState->at18)
+        {
+            if (pXSprite->aiState->at8 > 0)
+                aiNewState(pSprite, pXSprite, pXSprite->aiState->at18);
+            else if (seqGetStatus(3, nXSprite) < 0)
+                aiNewState(pSprite, pXSprite, pXSprite->aiState->at18);
+        }
+        if (pXSprite->health > 0 && ((pDudeInfo->hinderDamage<<4) <= cumulDamage[nXSprite]))
+        {
+            pXSprite->cumulDamage = cumulDamage[nXSprite];
+            RecoilDude(pSprite, pXSprite);
+        }
+    }
+    memset(cumulDamage, 0, sizeof(cumulDamage));
+}
+
+void aiInit(void)
+{
+    for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        aiInitSprite(&sprite[nSprite]);
+    }
+}
+
+void aiInitSprite(spritetype *pSprite)
+{
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSector = pSprite->sectnum;
+    int nXSector = sector[nSector].extra;
+    XSECTOR *pXSector = NULL;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
+    pDudeExtra->at4 = 0;
+    pDudeExtra->at0 = 0;
+    switch (pSprite->type)
+    {
+    case kGDXDudeUniversalCultist:
+    {
+        DUDEEXTRA_at6_u1* pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        pDudeExtraE->at4 = 0;
+        aiNewState(pSprite, pXSprite, &GDXGenDudeIdleL);
+        break;
+    }
+    case kGDXGenDudeBurning:
+        aiNewState(pSprite, pXSprite, &GDXGenDudeBurnGoto);
+        pXSprite->burnTime = 1200;
+        break;
+    case 201:
+    case 202:
+    case 247:
+    case 248:
+    case 249:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &cultistIdle);
+        break;
+    }
+    case 230:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &fanaticProneIdle);
+        break;
+    }
+    case 246:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &cultistProneIdle);
+        break;
+    }
+    case 204:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u2;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &zombieFIdle);
+        break;
+    }
+    case 203:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u2;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &zombieAIdle);
+        break;
+    }
+    case 244:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u2;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &zombieSIdle);
+        pSprite->hitag &= ~1;
+        break;
+    }
+    case 205:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u2;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &zombieEIdle);
+        break;
+    }
+    case 206:
+    case 207:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &gargoyleFIdle);
+        break;
+    }
+    case 208:
+    case 209:
+        aiNewState(pSprite, pXSprite, &gargoyleStatueIdle);
+        break;
+    case 227:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u2;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &cerberusIdle);
+        break;
+    }
+    case 211:
+        aiNewState(pSprite, pXSprite, &houndIdle);
+        break;
+    case 212:
+        aiNewState(pSprite, pXSprite, &handIdle);
+        break;
+    case 210:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &ghostIdle);
+        break;
+    }
+    case 245:
+        aiNewState(pSprite, pXSprite, &innocentIdle);
+        break;
+    case 220:
+        aiNewState(pSprite, pXSprite, &ratIdle);
+        break;
+    case 218:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &eelIdle);
+        break;
+    }
+    case 217:
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &gillBeastIdle);
+        else
+            aiNewState(pSprite, pXSprite, &gillBeastIdle);
+        break;
+    case 219:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &batIdle);
+        break;
+    }
+    case 213:
+    case 214:
+    case 215:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &spidIdle);
+        break;
+    }
+    case 216:
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &spidIdle);
+        break;
+    }
+    case 229:
+    {
+        DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[nXSprite].at6.u2;
+        pDudeExtraE->at4 = 0;
+        pDudeExtraE->at0 = 0;
+        aiNewState(pSprite, pXSprite, &tchernobogIdle);
+        break;
+    }
+    case 250:
+        aiNewState(pSprite, pXSprite, &tinycalebIdle);
+        break;
+    case 251:
+        aiNewState(pSprite, pXSprite, &beastIdle);
+        break;
+    case 221:
+    case 223:
+        aiNewState(pSprite, pXSprite, &podIdle);
+        break;
+    case 222:
+    case 224:
+        aiNewState(pSprite, pXSprite, &tentacleIdle);
+        break;
+    default:
+        aiNewState(pSprite, pXSprite, &genIdle);
+        break;
+    }
+    aiSetTarget(pXSprite, 0, 0, 0);
+    pXSprite->stateTimer = 0;
+    switch (pSprite->type)
+    {
+    case 213:
+    case 214:
+    case 215:
+        if (pSprite->cstat&8)
+            pSprite->hitag |= 9;
+        else
+            pSprite->hitag = 15;
+        break;
+    case 206:
+    case 207:
+    case 210:
+    case 218:
+    case 219:
+        pSprite->hitag |= 9;
+        break;
+    case 217:
+        if (pXSector && pXSector->Underwater)
+            pSprite->hitag |= 9;
+        else
+            pSprite->hitag = 15;
+        break;
+    case 205:
+    case 244:
+        pSprite->hitag = 7;
+        break;
+    // By NoOne: Allow put pods and tentacles on ceilings if sprite is y-flipped.
+    case 221:
+    case 222:
+    case 223:
+    case 224:
+    case 225:
+    case 226:
+        if ((pSprite->cstat & kSprFlipY) != 0) {
+            if (!(pSprite->hitag & kHitagExtBit)) // don't add autoaim for player if hitag 1 specified in editor.
+                pSprite->hitag = kHitagAutoAim;
+            break;
+        }
+        fallthrough__;
+    // go default
+    default:
+        pSprite->hitag = 15;
+        break;
+    }
+}
+
+class AILoadSave : public LoadSave
+{
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+void AILoadSave::Load(void)
+{
+    Read(cumulDamage, sizeof(cumulDamage));
+    Read(gDudeSlope, sizeof(gDudeSlope));
+}
+
+void AILoadSave::Save(void)
+{
+    Write(cumulDamage, sizeof(cumulDamage));
+    Write(gDudeSlope, sizeof(gDudeSlope));
+}
+
+static AILoadSave *myLoadSave;
+
+void AILoadSaveConstruct(void)
+{
+    myLoadSave = new AILoadSave();
+}
+
diff --git a/source/blood/src/ai.h b/source/blood/src/ai.h
new file mode 100644
index 000000000..b69e83644
--- /dev/null
+++ b/source/blood/src/ai.h
@@ -0,0 +1,105 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "compat.h"
+#include "common_game.h"
+#include "actor.h"
+#include "db.h"
+
+struct AISTATE {
+    int stateType; // By NoOne: current type of state. Basically required for kGDXDudeTargetChanger, but can be used for something else.
+    int at0; // seq
+    int at4; // seq callback
+    int at8;
+    void(*atc)(spritetype *, XSPRITE *);
+    void(*at10)(spritetype *, XSPRITE *);
+    void(*at14)(spritetype *, XSPRITE *);
+    AISTATE *at18; // next state ?
+};
+extern AISTATE aiState[];
+
+enum AI_SFX_PRIORITY {
+    AI_SFX_PRIORITY_0 = 0,
+    AI_SFX_PRIORITY_1,
+    AI_SFX_PRIORITY_2,
+};
+
+
+struct DUDEEXTRA_at6_u1
+{
+    int at0;
+    int at4;
+    char at8;
+};
+
+struct DUDEEXTRA_at6_u2
+{
+    int at0;
+    char at4;
+};
+
+struct DUDEEXTRA
+{
+    int at0;
+    char at4;
+    AI_SFX_PRIORITY at5;
+    union
+    {
+        DUDEEXTRA_at6_u1 u1;
+        DUDEEXTRA_at6_u2 u2;
+    } at6;
+    //DUDEEXTRA_at6 at6;
+};
+
+struct TARGETTRACK {
+    int at0;
+    int at4;
+    int at8; // view angle
+    int atc;
+    int at10; // Move predict
+};
+
+extern int dword_138BB0[5];
+extern DUDEEXTRA gDudeExtra[];
+extern int gDudeSlope[];
+
+bool sub_5BDA8(spritetype *pSprite, int nSeq);
+void aiPlay3DSound(spritetype *pSprite, int a2, AI_SFX_PRIORITY a3, int a4);
+void aiNewState(spritetype *pSprite, XSPRITE *pXSprite, AISTATE *pAIState);
+void aiChooseDirection(spritetype *pSprite, XSPRITE *pXSprite, int a3);
+void aiMoveForward(spritetype *pSprite, XSPRITE *pXSprite);
+void aiMoveTurn(spritetype *pSprite, XSPRITE *pXSprite);
+void aiMoveDodge(spritetype *pSprite, XSPRITE *pXSprite);
+void aiActivateDude(spritetype *pSprite, XSPRITE *pXSprite);
+void aiSetTarget(XSPRITE *pXSprite, int x, int y, int z);
+void aiSetTarget(XSPRITE *pXSprite, int nTarget);
+int aiDamageSprite(spritetype *pSprite, XSPRITE *pXSprite, int nSource, DAMAGE_TYPE nDmgType, int nDamage);
+void aiThinkTarget(spritetype *pSprite, XSPRITE *pXSprite);
+void sub_5F15C(spritetype *pSprite, XSPRITE *pXSprite);
+void aiProcessDudes(void);
+void aiInit(void);
+void aiInitSprite(spritetype *pSprite);
+
+// By NoOne: this function required for kGDXDudeTargetChanger
+void aiSetGenIdleState(spritetype* pSprite, XSPRITE* pXSprite);
diff --git a/source/blood/src/aibat.cpp b/source/blood/src/aibat.cpp
new file mode 100644
index 000000000..f138c9cbd
--- /dev/null
+++ b/source/blood/src/aibat.cpp
@@ -0,0 +1,437 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aibat.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "player.h"
+#include "seq.h"
+#include "trig.h"
+
+static void BiteSeqCallback(int, int);
+static void thinkTarget(spritetype *, XSPRITE *);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkPonder(spritetype *, XSPRITE *);
+static void MoveDodgeUp(spritetype *, XSPRITE *);
+static void MoveDodgeDown(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void MoveForward(spritetype *, XSPRITE *);
+static void MoveSwoop(spritetype *, XSPRITE *);
+static void MoveFly(spritetype *, XSPRITE *);
+static void MoveToCeil(spritetype *, XSPRITE *);
+
+static int nBiteClient = seqRegisterClient(BiteSeqCallback);
+
+AISTATE batIdle = {kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE batFlyIdle = {kAiStateIdle, 6, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE batChase = {kAiStateChase, 6, -1, 0, NULL, MoveForward, thinkChase, &batFlyIdle };
+AISTATE batPonder = {kAiStateOther, 6, -1, 0, NULL, NULL, thinkPonder, NULL };
+AISTATE batGoto = {kAiStateMove, 6, -1, 600, NULL, MoveForward, thinkGoto, &batFlyIdle };
+AISTATE batBite = {kAiStateChase, 7, nBiteClient, 60, NULL, NULL, NULL, &batPonder };
+AISTATE batRecoil = {kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &batChase };
+AISTATE batSearch = {kAiStateSearch, 6, -1, 120, NULL, MoveForward, thinkSearch, &batFlyIdle };
+AISTATE batSwoop = {kAiStateOther, 6, -1, 60, NULL, MoveSwoop, thinkChase, &batChase };
+AISTATE batFly = { kAiStateMove, 6, -1, 0, NULL, MoveFly, thinkChase, &batChase };
+AISTATE batTurn = {kAiStateMove, 6, -1, 60, NULL, aiMoveTurn, NULL, &batChase };
+AISTATE batHide = {kAiStateOther, 6, -1, 0, NULL, MoveToCeil, MoveForward, NULL };
+AISTATE batDodgeUp = {kAiStateMove, 6, -1, 120, NULL, MoveDodgeUp, 0, &batChase };
+AISTATE batDodgeUpRight = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeUp, 0, &batChase };
+AISTATE batDodgeUpLeft = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeUp, 0, &batChase };
+AISTATE batDodgeDown = {kAiStateMove, 6, -1, 120, NULL, MoveDodgeDown, 0, &batChase };
+AISTATE batDodgeDownRight = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeDown, 0, &batChase };
+AISTATE batDodgeDownLeft = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeDown, 0, &batChase };
+
+static void BiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    spritetype *pSprite = &sprite[pXSprite->reference];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int dx = Cos(pSprite->ang) >> 16;
+    int dy = Sin(pSprite->ang) >> 16;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type-kDudeBase];
+    int height = (pSprite->yrepeat*pDudeInfo->eyeHeight)<<2;
+    int height2 = (pTarget->yrepeat*pDudeInfoT->eyeHeight)<<2;
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    actFireVector(pSprite, 0, 0, dx, dy, height2-height, VECTOR_TYPE_6);
+}
+
+static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+    if (pDudeExtraE->at8 && pDudeExtraE->at4 < 10)
+        pDudeExtraE->at4++;
+    else if (pDudeExtraE->at4 >= 10 && pDudeExtraE->at8)
+    {
+        pDudeExtraE->at4 = 0;
+        pXSprite->goalAng += 256;
+        POINT3D *pTarget = &baseSprite[pSprite->index];
+        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
+        aiNewState(pSprite, pXSprite, &batTurn);
+        return;
+    }
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else
+                continue;
+            break;
+        }
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    thinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &batSearch);
+    thinkTarget(pSprite, pXSprite);
+}
+
+static void thinkPonder(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &batSearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &batSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        int height2 = (dudeInfo[pTarget->type-kDudeBase].eyeHeight*pTarget->yrepeat)<<2;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            aiSetTarget(pXSprite, pXSprite->target);
+            if (height2-height < 0x3000 && nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &batDodgeUp);
+            else if (height2-height > 0x5000 && nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &batDodgeDown);
+            else if (height2-height < 0x2000 && nDist < 0x200 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &batDodgeUp);
+            else if (height2-height > 0x6000 && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &batDodgeDown);
+            else if (height2-height < 0x2000 && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &batDodgeUp);
+            else if (height2-height < 0x2000 && klabs(nDeltaAngle) < 85 && nDist > 0x1400)
+                aiNewState(pSprite, pXSprite, &batDodgeUp);
+            else if (height2-height > 0x4000)
+                aiNewState(pSprite, pXSprite, &batDodgeDown);
+            else
+                aiNewState(pSprite, pXSprite, &batDodgeUp);
+            return;
+        }
+    }
+    aiNewState(pSprite, pXSprite, &batGoto);
+    pXSprite->target = -1;
+}
+
+static void MoveDodgeUp(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -0x52aaa;
+}
+
+static void MoveDodgeDown(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (pXSprite->dodgeDir == 0)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = 0x44444;
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &batGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &batSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &batSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        // Should be dudeInfo[pTarget->type-kDudeBase]
+        int height2 = (pDudeInfo->eyeHeight*pTarget->yrepeat)<<2;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int floorZ = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                if (height2-height < 0x2000 && nDist < 0x200 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &batBite);
+                else if ((height2-height > 0x5000 || floorZ-bottom > 0x5000) && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &batSwoop);
+                else if ((height2-height < 0x3000 || floorZ-bottom < 0x3000) && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &batFly);
+                return;
+            }
+        }
+        else
+        {
+            aiNewState(pSprite, pXSprite, &batFly);
+            return;
+        }
+    }
+
+    pXSprite->target = -1;
+    aiNewState(pSprite, pXSprite, &batHide);
+}
+
+static void MoveForward(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if ((unsigned int)Random(64) < 32 && nDist <= 0x200)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void MoveSwoop(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x200)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = 0x44444;
+}
+
+static void MoveFly(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pSprite->ang = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x200)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -0x2d555;
+}
+
+void MoveToCeil(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = pSprite->z;
+    int nSector = pSprite->sectnum;
+    if (z - pXSprite->targetZ < 0x1000)
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pSprite->hitag = 0;
+        aiNewState(pSprite, pXSprite, &batIdle);
+    }
+    else
+        aiSetTarget(pXSprite, x, y, sector[nSector].ceilingz);
+}
diff --git a/source/blood/src/aibat.h b/source/blood/src/aibat.h
new file mode 100644
index 000000000..464644610
--- /dev/null
+++ b/source/blood/src/aibat.h
@@ -0,0 +1,43 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE batIdle;
+extern AISTATE batFlyIdle;
+extern AISTATE batChase;
+extern AISTATE batPonder;
+extern AISTATE batGoto;
+extern AISTATE batBite;
+extern AISTATE batRecoil;
+extern AISTATE batSearch;
+extern AISTATE batSwoop;
+extern AISTATE batFly;
+extern AISTATE batTurn;
+extern AISTATE batHide;
+extern AISTATE batDodgeUp;
+extern AISTATE batDodgeUpRight;
+extern AISTATE batDodgeUpLeft;
+extern AISTATE batDodgeDown;
+extern AISTATE batDodgeDownRight;
+extern AISTATE batDodgeDownLeft;
diff --git a/source/blood/src/aibeast.cpp b/source/blood/src/aibeast.cpp
new file mode 100644
index 000000000..01f6bc010
--- /dev/null
+++ b/source/blood/src/aibeast.cpp
@@ -0,0 +1,581 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aibeast.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void SlashSeqCallback(int, int);
+static void StompSeqCallback(int, int);
+static void MorphToBeast(spritetype *, XSPRITE *);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void thinkSwimGoto(spritetype *, XSPRITE *);
+static void thinkSwimChase(spritetype *, XSPRITE *);
+static void MoveForward(spritetype *, XSPRITE *);
+static void sub_628A0(spritetype *, XSPRITE *);
+static void sub_62AE0(spritetype *, XSPRITE *);
+static void sub_62D7C(spritetype *, XSPRITE *);
+
+static int nSlashClient = seqRegisterClient(SlashSeqCallback);
+static int nStompClient = seqRegisterClient(StompSeqCallback);
+
+AISTATE beastIdle = {kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE beastChase = {kAiStateChase, 8, -1, 0, NULL, MoveForward, thinkChase, NULL };
+AISTATE beastDodge = { kAiStateMove, 8, -1, 60, NULL, aiMoveDodge, NULL, &beastChase };
+AISTATE beastGoto = { kAiStateMove, 8, -1, 600, NULL, MoveForward, thinkGoto, &beastIdle };
+AISTATE beastSlash = { kAiStateChase, 6, nSlashClient, 120, NULL, NULL, NULL, &beastChase };
+AISTATE beastStomp = { kAiStateChase, 7, nStompClient, 120, NULL, NULL, NULL, &beastChase };
+AISTATE beastSearch = { kAiStateSearch, 8, -1, 120, NULL, MoveForward, thinkSearch, &beastIdle };
+AISTATE beastRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &beastDodge };
+AISTATE beastTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &beastDodge };
+AISTATE beastSwimIdle = {kAiStateIdle, 9, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE beastSwimChase = { kAiStateChase, 9, -1, 0, NULL, sub_628A0, thinkSwimChase, NULL };
+AISTATE beastSwimDodge = { kAiStateMove, 9, -1, 90, NULL, aiMoveDodge, NULL, &beastSwimChase };
+AISTATE beastSwimGoto = { kAiStateMove, 9, -1, 600, NULL, MoveForward, thinkSwimGoto, &beastSwimIdle };
+AISTATE beastSwimSearch = { kAiStateSearch, 9, -1, 120, NULL, MoveForward, thinkSearch, &beastSwimIdle };
+AISTATE beastSwimSlash = { kAiStateChase, 9, nSlashClient, 0, NULL, NULL, thinkSwimChase, &beastSwimChase };
+AISTATE beastSwimRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &beastSwimDodge };
+AISTATE beastMorphToBeast = { kAiStateOther, -1, -1, 0, MorphToBeast, NULL, NULL, &beastIdle };
+AISTATE beastMorphFromCultist = { kAiStateOther, 2576, -1, 0, NULL, NULL, NULL, &beastMorphToBeast };
+AISTATE beast138FB4 = { kAiStateOther, 9, -1, 120, NULL, sub_62AE0, thinkSwimChase, &beastSwimChase };
+AISTATE beast138FD0 = { kAiStateOther, 9, -1, 0, NULL, sub_62D7C, thinkSwimChase, &beastSwimChase };
+AISTATE beast138FEC = { kAiStateOther, 9, -1, 120, NULL, aiMoveTurn, NULL, &beastSwimChase };
+
+static void SlashSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    // Correct ?
+    int dz = pSprite->z-pTarget->z;
+    dx += Random3(4000-700*gGameOptions.nDifficulty);
+    dy += Random3(4000-700*gGameOptions.nDifficulty);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_13);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_13);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_13);
+    sfxPlay3DSound(pSprite, 9012+Random(2), -1, 0);
+}
+
+static void StompSeqCallback(int, int nXSprite)
+{
+    char vb8[(kMaxSectors+7)>>3];
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = pSprite->z;
+    int vc = 400;
+    int nSector = pSprite->sectnum;
+    int v1c = 5+2*gGameOptions.nDifficulty;
+    int v10 = 25+30*gGameOptions.nDifficulty;
+    gAffectedSectors[0] = -1;
+    gAffectedXWalls[0] = -1;
+    GetClosestSpriteSectors(nSector, x, y, vc, gAffectedSectors, vb8, gAffectedXWalls);
+    char v4 = 0;
+    int v34 = -1;
+    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+    actHitcodeToData(hit, &gHitInfo, &v34, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
+    if (hit == 3 && v34 >= 0)
+    {
+        if (sprite[v34].statnum == 6)
+            v4 = 0;
+    }
+    vc <<= 4;
+    for (int nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        if (nSprite != nSprite2 || v4)
+        {
+            spritetype *pSprite2 = &sprite[nSprite2];
+            if (pSprite2->extra > 0 && pSprite2->extra < kMaxXSprites)
+            {
+                if (pSprite2->type == 251)
+                    continue;
+                if (pSprite2->hitag&32)
+                    continue;
+                if (TestBitString(vb8, pSprite2->sectnum) && CheckProximity(pSprite2, x, y, z, nSector, vc))
+                {
+                    int top, bottom;
+                    GetSpriteExtents(pSprite, &top, &bottom);
+                    if (klabs(bottom-sector[nSector].floorz) == 0)
+                    {
+                        int dx = klabs(pSprite->x-pSprite2->x);
+                        int dy = klabs(pSprite->y-pSprite2->y);
+                        int nDist2 = ksqrt(dx*dx + dy*dy);
+                        if (nDist2 <= vc)
+                        {
+                            int nDamage;
+                            if (!nDist2)
+                                nDamage = v1c + v10;
+                            else
+                                nDamage = v1c + ((vc-nDist2)*v10)/vc;
+                            if (IsPlayerSprite(pSprite2))
+                                gPlayer[pSprite2->type-kDudePlayer1].at37f += nDamage*4;
+                            actDamageSprite(nSprite, pSprite2, DAMAGE_TYPE_0, nDamage<<4);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    for (int nSprite2 = headspritestat[4]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite2->hitag&32)
+            continue;
+        if (TestBitString(vb8, pSprite2->sectnum) && CheckProximity(pSprite2, x, y, z, nSector, vc))
+        {
+            XSPRITE *pXSprite = &xsprite[pSprite2->extra];
+            if (pXSprite->locked)
+                continue;
+            int dx = klabs(pSprite->x-pSprite2->x);
+            int dy = klabs(pSprite->y-pSprite2->y);
+            int nDist2 = ksqrt(dx*dx + dy*dy);
+            if (nDist2 <= vc)
+            {
+                int nDamage;
+                if (!nDist2)
+                    nDamage = v1c + v10;
+                else
+                    nDamage = v1c + ((vc-nDist2)*v10)/vc;
+                if (IsPlayerSprite(pSprite2))
+                    gPlayer[pSprite2->type-kDudePlayer1].at37f += nDamage*4;
+                actDamageSprite(nSprite, pSprite2, DAMAGE_TYPE_0, nDamage<<4);
+            }
+        }
+    }
+    sfxPlay3DSound(pSprite, 9015+Random(2), -1, 0);
+}
+
+static void MorphToBeast(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    actHealDude(pXSprite, dudeInfo[51].startHealth, dudeInfo[51].startHealth);
+    pSprite->type = 251;
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    XSECTOR *pXSector;
+    int nXSector = sector[pSprite->sectnum].extra;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    else
+        pXSector = NULL;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &beastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &beastSearch);
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &beastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &beastSearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &beastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &beastSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &beastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &beastSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int nXSprite = sprite[pXSprite->reference].extra;
+                gDudeSlope[nXSprite] = divscale(pTarget->z-pSprite->z, nDist, 10);
+                if (nDist < 0x1400 && nDist > 0xa00 && klabs(nDeltaAngle) < 85 && (pTarget->hitag&2)
+                    && IsPlayerSprite(pTarget) && Chance(0x8000))
+                {
+                    XSECTOR *pXSector;
+                    int nXSector = sector[pSprite->sectnum].extra;
+                    if (nXSector > 0)
+                        pXSector = &xsector[nXSector];
+                    else
+                        pXSector = NULL;
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    if (pXTarget->health > gPlayerTemplate[0].startHealth/2)
+                    {
+                        switch (hit)
+                        {
+                        case -1:
+                            if (!pXSector || !pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &beastStomp);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                            {
+                                if (!pXSector || !pXSector->Underwater)
+                                    aiNewState(pSprite, pXSprite, &beastStomp);
+                            }
+                            else
+                            {
+                                if (pXSector && pXSector->Underwater)
+                                    aiNewState(pSprite, pXSprite, &beastSwimDodge);
+                                else
+                                    aiNewState(pSprite, pXSprite, &beastDodge);
+                            }
+                            break;
+                        default:
+                            if (!pXSector || !pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &beastStomp);
+                            break;
+                        }
+                    }
+                }
+                if (nDist < 921 && klabs(nDeltaAngle) < 28)
+                {
+                    XSECTOR *pXSector;
+                    int nXSector = sector[pSprite->sectnum].extra;
+                    if (nXSector > 0)
+                        pXSector = &xsector[nXSector];
+                    else
+                        pXSector = NULL;
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (hit)
+                    {
+                    case -1:
+                        if (pXSector && pXSector->Underwater)
+                            aiNewState(pSprite, pXSprite, &beastSwimSlash);
+                        else
+                            aiNewState(pSprite, pXSprite, &beastSlash);
+                        break;
+                    case 3:
+                        if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                        {
+                            if (pXSector && pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &beastSwimSlash);
+                            else
+                                aiNewState(pSprite, pXSprite, &beastSlash);
+                        }
+                        else
+                        {
+                            if (pXSector && pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &beastSwimDodge);
+                            else
+                                aiNewState(pSprite, pXSprite, &beastDodge);
+                        }
+                        break;
+                    default:
+                        if (pXSector && pXSector->Underwater)
+                            aiNewState(pSprite, pXSprite, &beastSwimSlash);
+                        else
+                            aiNewState(pSprite, pXSprite, &beastSlash);
+                        break;
+                    }
+                }
+            }
+            return;
+        }
+    }
+
+    XSECTOR *pXSector;
+    int nXSector = sector[pSprite->sectnum].extra;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    else
+        pXSector = NULL;
+    if (pXSector && pXSector->Underwater)
+        aiNewState(pSprite, pXSprite, &beastSwimGoto);
+    else
+        aiNewState(pSprite, pXSprite, &beastGoto);
+    pXSprite->target = -1;
+}
+
+static void thinkSwimGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &beastSwimSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkSwimChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &beastSwimGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &beastSwimSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &beastSwimSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = pDudeInfo->eyeHeight+pSprite->z;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int UNUSED(floorZ) = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &beastSwimSlash);
+                else
+                {
+                    aiPlay3DSound(pSprite, 9009+Random(2), AI_SFX_PRIORITY_1, -1);
+                    aiNewState(pSprite, pXSprite, &beast138FD0);
+                }
+            }
+        }
+        else
+            aiNewState(pSprite, pXSprite, &beast138FD0);
+        return;
+    }
+    aiNewState(pSprite, pXSprite, &beastSwimGoto);
+    pXSprite->target = -1;
+}
+
+static void MoveForward(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (klabs(nAng) > 341)
+        return;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (nDist <= 0x400 && Random(64) < 32)
+        return;
+    xvel[nSprite] += mulscale30(pDudeInfo->frontSpeed, Cos(pSprite->ang));
+    yvel[nSprite] += mulscale30(pDudeInfo->frontSpeed, Sin(pSprite->ang));
+}
+
+static void sub_628A0(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Random(64) < 32 && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>2;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void sub_62AE0(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int z = pSprite->z + dudeInfo[pSprite->type - kDudeBase].eyeHeight;
+    int z2 = pTarget->z + dudeInfo[pTarget->type - kDudeBase].eyeHeight;
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int dz = z2 - z;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -dz;
+}
+
+static void sub_62D7C(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int z = pSprite->z + dudeInfo[pSprite->type - kDudeBase].eyeHeight;
+    int z2 = pTarget->z + dudeInfo[pTarget->type - kDudeBase].eyeHeight;
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pSprite->ang = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int dz = (z2 - z)<<3;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = dz;
+}
diff --git a/source/blood/src/aibeast.h b/source/blood/src/aibeast.h
new file mode 100644
index 000000000..66451dbbb
--- /dev/null
+++ b/source/blood/src/aibeast.h
@@ -0,0 +1,46 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE beastIdle;
+extern AISTATE beastChase;
+extern AISTATE beastDodge;
+extern AISTATE beastGoto;
+extern AISTATE beastSlash;
+extern AISTATE beastStomp;
+extern AISTATE beastSearch;
+extern AISTATE beastRecoil;
+extern AISTATE beastTeslaRecoil;
+extern AISTATE beastSwimIdle;
+extern AISTATE beastSwimChase;
+extern AISTATE beastSwimDodge;
+extern AISTATE beastSwimGoto;
+extern AISTATE beastSwimSearch;
+extern AISTATE beastSwimSlash;
+extern AISTATE beastSwimRecoil;
+extern AISTATE beastMorphToBeast;
+extern AISTATE beastMorphFromCultist;
+extern AISTATE beast138FB4;
+extern AISTATE beast138FD0;
+extern AISTATE beast138FEC;
diff --git a/source/blood/src/aiboneel.cpp b/source/blood/src/aiboneel.cpp
new file mode 100644
index 000000000..35e8921ef
--- /dev/null
+++ b/source/blood/src/aiboneel.cpp
@@ -0,0 +1,442 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aiboneel.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void BiteSeqCallback(int, int);
+static void thinkTarget(spritetype *, XSPRITE *);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkPonder(spritetype *, XSPRITE *);
+static void MoveDodgeUp(spritetype *, XSPRITE *);
+static void MoveDodgeDown(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void MoveForward(spritetype *, XSPRITE *);
+static void MoveSwoop(spritetype *, XSPRITE *);
+static void MoveAscend(spritetype *pSprite, XSPRITE *pXSprite);
+static void MoveToCeil(spritetype *, XSPRITE *);
+
+static int nBiteClient = seqRegisterClient(BiteSeqCallback);
+
+AISTATE eelIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE eelFlyIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE eelChase = { kAiStateChase, 0, -1, 0, NULL, MoveForward, thinkChase, &eelIdle };
+AISTATE eelPonder = { kAiStateOther, 0, -1, 0, NULL, NULL, thinkPonder, NULL };
+AISTATE eelGoto = { kAiStateMove, 0, -1, 600, NULL, NULL, thinkGoto, &eelIdle };
+AISTATE eelBite = { kAiStateChase, 7, nBiteClient, 60, NULL, NULL, NULL, &eelChase };
+AISTATE eelRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &eelChase };
+AISTATE eelSearch = { kAiStateSearch, 0, -1, 120, NULL, MoveForward, thinkSearch, &eelIdle };
+AISTATE eelSwoop = { kAiStateOther, 0, -1, 60, NULL, MoveSwoop, thinkChase, &eelChase };
+AISTATE eelFly = { kAiStateMove, 0, -1, 0, NULL, MoveAscend, thinkChase, &eelChase };
+AISTATE eelTurn = { kAiStateMove, 0, -1, 60, NULL, aiMoveTurn, NULL, &eelChase };
+AISTATE eelHide = { kAiStateOther, 0, -1, 0, NULL, MoveToCeil, MoveForward, NULL };
+AISTATE eelDodgeUp = { kAiStateMove, 0, -1, 120, NULL, MoveDodgeUp, NULL, &eelChase };
+AISTATE eelDodgeUpRight = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeUp, NULL, &eelChase };
+AISTATE eelDodgeUpLeft = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeUp, NULL, &eelChase };
+AISTATE eelDodgeDown = { kAiStateMove, 0, -1, 120, NULL, MoveDodgeDown, NULL, &eelChase };
+AISTATE eelDodgeDownRight = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeDown, NULL, &eelChase };
+AISTATE eelDodgeDownLeft = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeDown, NULL, &eelChase };
+
+static void BiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    spritetype *pSprite = &sprite[pXSprite->reference];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int dx = Cos(pSprite->ang) >> 16;
+    int dy = Sin(pSprite->ang) >> 16;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type-kDudeBase];
+    int height = (pSprite->yrepeat*pDudeInfo->eyeHeight)<<2;
+    int height2 = (pTarget->yrepeat*pDudeInfoT->eyeHeight)<<2;
+    /*
+     * workaround for 
+     * pXSprite->target >= 0 && pXSprite->target < kMaxSprites in file NBlood/source/blood/src/aiboneel.cpp at line 86
+     * The value of pXSprite->target is -1. 
+     * copied from lines 177:181
+     * resolves this case, but may cause other issues? 
+     */
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &eelSearch);
+        return;
+    }
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    actFireVector(pSprite, 0, 0, dx, dy, height2-height, VECTOR_TYPE_7);
+}
+
+static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+    if (pDudeExtraE->at8 && pDudeExtraE->at4 < 10)
+        pDudeExtraE->at4++;
+    else if (pDudeExtraE->at4 >= 10 && pDudeExtraE->at8)
+    {
+        pDudeExtraE->at4 = 0;
+        pXSprite->goalAng += 256;
+        POINT3D *pTarget = &baseSprite[pSprite->index];
+        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
+        aiNewState(pSprite, pXSprite, &eelTurn);
+        return;
+    }
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                pDudeExtraE->at4 = 0;
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                pDudeExtraE->at4 = 0;
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else
+                continue;
+            break;
+        }
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    thinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &eelSearch);
+    thinkTarget(pSprite, pXSprite);
+}
+
+static void thinkPonder(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &eelSearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &eelSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        int height2 = (dudeInfo[pTarget->type-kDudeBase].eyeHeight*pTarget->yrepeat)<<2;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            aiSetTarget(pXSprite, pXSprite->target);
+            if (height2-height < -0x2000 && nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &eelDodgeUp);
+            else if (height2-height > 0xccc && nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &eelDodgeDown);
+            else if (height2-height < 0xccc && nDist < 0x399 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &eelDodgeUp);
+            else if (height2-height > 0xccc && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &eelDodgeDown);
+            else if (height2-height < -0x2000 && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
+                aiNewState(pSprite, pXSprite, &eelDodgeUp);
+            else if (height2-height < -0x2000 && klabs(nDeltaAngle) < 85 && nDist > 0x1400)
+                aiNewState(pSprite, pXSprite, &eelDodgeUp);
+            else if (height2-height > 0xccc)
+                aiNewState(pSprite, pXSprite, &eelDodgeDown);
+            else
+                aiNewState(pSprite, pXSprite, &eelDodgeUp);
+            return;
+        }
+    }
+    aiNewState(pSprite, pXSprite, &eelGoto);
+    pXSprite->target = -1;
+}
+
+static void MoveDodgeUp(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -0x8000;
+}
+
+static void MoveDodgeDown(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (pXSprite->dodgeDir == 0)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = 0x44444;
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &eelGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &eelSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &eelSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        int top2, bottom2;
+        GetSpriteExtents(pTarget, &top2, &bottom2);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x399 && top2 > top && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &eelSwoop);
+                else if (nDist <= 0x399 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &eelBite);
+                else if (bottom2 > top && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &eelSwoop);
+                else if (top2 < top && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &eelFly);
+            }
+        }
+        return;
+    }
+
+    pXSprite->target = -1;
+    aiNewState(pSprite, pXSprite, &eelSearch);
+}
+
+static void MoveForward(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = (pDudeInfo->frontSpeed-(((4-gGameOptions.nDifficulty)<<26)/120)/120)<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (nDist <= 0x399)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void MoveSwoop(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = (pDudeInfo->frontSpeed-(((4-gGameOptions.nDifficulty)<<26)/120)/120)<<2;
+    if (klabs(nAng) > 341)
+        return;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x8000) && nDist <= 0x399)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = 0x22222;
+}
+
+static void MoveAscend(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = (pDudeInfo->frontSpeed-(((4-gGameOptions.nDifficulty)<<26)/120)/120)<<2;
+    if (klabs(nAng) > 341)
+        return;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x399)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -0x8000;
+}
+
+void MoveToCeil(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = pSprite->z;
+    int nSector = pSprite->sectnum;
+    if (z - pXSprite->targetZ < 0x1000)
+    {
+        DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+        pDudeExtraE->at8 = 0;
+        pSprite->hitag = 0;
+        aiNewState(pSprite, pXSprite, &eelIdle);
+    }
+    else
+        aiSetTarget(pXSprite, x, y, sector[nSector].ceilingz);
+}
diff --git a/source/blood/src/aiboneel.h b/source/blood/src/aiboneel.h
new file mode 100644
index 000000000..b73cc1a7c
--- /dev/null
+++ b/source/blood/src/aiboneel.h
@@ -0,0 +1,44 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE eelIdle;
+extern AISTATE eelFlyIdle;
+extern AISTATE eelChase;
+extern AISTATE eelPonder;
+extern AISTATE eelGoto;
+extern AISTATE eelBite;
+extern AISTATE eelRecoil;
+extern AISTATE eelSearch;
+extern AISTATE eelSwoop;
+extern AISTATE eelFly;
+extern AISTATE eelTurn;
+extern AISTATE eelHide;
+extern AISTATE eelDodgeUp;
+extern AISTATE eelDodgeUpRight;
+extern AISTATE eelDodgeUpLeft;
+extern AISTATE eelDodgeDown;
+extern AISTATE eelDodgeDownRight;
+extern AISTATE eelDodgeDownLeft;
+
diff --git a/source/blood/src/aiburn.cpp b/source/blood/src/aiburn.cpp
new file mode 100644
index 000000000..307561bee
--- /dev/null
+++ b/source/blood/src/aiburn.cpp
@@ -0,0 +1,267 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aiburn.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void BurnSeqCallback(int, int);
+static void thinkSearch(spritetype*, XSPRITE*);
+static void thinkGoto(spritetype*, XSPRITE*);
+static void thinkChase(spritetype*, XSPRITE*);
+
+static int nBurnClient = seqRegisterClient(BurnSeqCallback);
+
+AISTATE cultistBurnIdle = { kAiStateIdle, 3, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE cultistBurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE cultistBurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &cultistBurnSearch };
+AISTATE cultistBurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, &cultistBurnSearch };
+AISTATE cultistBurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &cultistBurnChase };
+
+AISTATE zombieABurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE zombieABurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &zombieABurnSearch };
+AISTATE zombieABurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, NULL };
+AISTATE zombieABurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &zombieABurnChase };
+
+AISTATE zombieFBurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE zombieFBurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &zombieFBurnSearch };
+AISTATE zombieFBurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, NULL };
+AISTATE zombieFBurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &zombieFBurnChase };
+
+AISTATE innocentBurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE innocentBurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &zombieFBurnSearch };
+AISTATE innocentBurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, NULL };
+AISTATE innocentBurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &zombieFBurnChase };
+
+AISTATE beastBurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE beastBurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &beastBurnSearch };
+AISTATE beastBurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, &beastBurnSearch };
+AISTATE beastBurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &beastBurnChase };
+
+AISTATE tinycalebBurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE tinycalebBurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &tinycalebBurnSearch };
+AISTATE tinycalebBurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, &tinycalebBurnSearch };
+AISTATE tinycalebBurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &tinycalebBurnChase };
+
+AISTATE GDXGenDudeBurnIdle = { kAiStateIdle, 3, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE GDXGenDudeBurnChase = { kAiStateChase, 3, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE GDXGenDudeBurnGoto = { kAiStateMove, 3, -1, 3600, NULL, aiMoveForward, thinkGoto, &GDXGenDudeBurnSearch };
+AISTATE GDXGenDudeBurnSearch = { kAiStateSearch, 3, -1, 3600, NULL, aiMoveForward, thinkSearch, &GDXGenDudeBurnSearch };
+AISTATE GDXGenDudeBurnAttack = { kAiStateChase, 3, nBurnClient, 120, NULL, NULL, NULL, &GDXGenDudeBurnChase };
+
+static void BurnSeqCallback(int, int)
+{
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        switch (pSprite->type)
+        {
+        case 240:
+            aiNewState(pSprite, pXSprite, &cultistBurnSearch);
+            break;
+        case 241:
+            aiNewState(pSprite, pXSprite, &zombieABurnSearch);
+            break;
+        case 242:
+            aiNewState(pSprite, pXSprite, &zombieFBurnSearch);
+            break;
+        case 239:
+            aiNewState(pSprite, pXSprite, &innocentBurnSearch);
+            break;
+        case 253:
+            aiNewState(pSprite, pXSprite, &beastBurnSearch);
+            break;
+        case 252:
+            aiNewState(pSprite, pXSprite, &tinycalebBurnSearch);
+            break;
+        case kGDXGenDudeBurning:
+            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnSearch);
+            break;
+        }
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        switch (pSprite->type)
+        {
+        case 240:
+            aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+            break;
+        case 241:
+            aiNewState(pSprite, pXSprite, &zombieABurnGoto);
+            break;
+        case 242:
+            aiNewState(pSprite, pXSprite, &zombieFBurnGoto);
+            break;
+        case 239:
+            aiNewState(pSprite, pXSprite, &innocentBurnGoto);
+            break;
+        case 253:
+            aiNewState(pSprite, pXSprite, &beastBurnGoto);
+            break;
+        case 252:
+            aiNewState(pSprite, pXSprite, &tinycalebBurnGoto);
+            break;
+        case kGDXGenDudeBurning:
+            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnGoto);
+            break;
+        }
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        switch (pSprite->type)
+        {
+        case 240:
+            aiNewState(pSprite, pXSprite, &cultistBurnSearch);
+            break;
+        case 241:
+            aiNewState(pSprite, pXSprite, &zombieABurnSearch);
+            break;
+        case 242:
+            aiNewState(pSprite, pXSprite, &zombieFBurnSearch);
+            break;
+        case 239:
+            aiNewState(pSprite, pXSprite, &innocentBurnSearch);
+            break;
+        case 253:
+            aiNewState(pSprite, pXSprite, &beastBurnSearch);
+            break;
+        case 252:
+            aiNewState(pSprite, pXSprite, &tinycalebBurnSearch);
+            break;
+        case kGDXGenDudeBurning:
+            aiNewState(pSprite, pXSprite, &GDXGenDudeBurnSearch);
+            break;
+        }
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x333 && klabs(nDeltaAngle) < 85)
+                {
+                    switch (pSprite->type)
+                    {
+                    case 240:
+                        aiNewState(pSprite, pXSprite, &cultistBurnAttack);
+                        break;
+                    case 241:
+                        aiNewState(pSprite, pXSprite, &zombieABurnAttack);
+                        break;
+                    case 242:
+                        aiNewState(pSprite, pXSprite, &zombieFBurnAttack);
+                        break;
+                    case 239:
+                        aiNewState(pSprite, pXSprite, &innocentBurnAttack);
+                        break;
+                    case 253:
+                        aiNewState(pSprite, pXSprite, &beastBurnAttack);
+                        break;
+                    case 252:
+                        aiNewState(pSprite, pXSprite, &tinycalebBurnAttack);
+                        break;
+                    case kGDXGenDudeBurning:
+                        aiNewState(pSprite, pXSprite, &GDXGenDudeBurnSearch);
+                        break;
+                    }
+                }
+                return;
+            }
+        }
+    }
+    
+    switch (pSprite->type)
+    {
+    case 240:
+        aiNewState(pSprite, pXSprite, &cultistBurnGoto);
+        break;
+    case 241:
+        aiNewState(pSprite, pXSprite, &zombieABurnGoto);
+        break;
+    case 242:
+        aiNewState(pSprite, pXSprite, &zombieFBurnGoto);
+        break;
+    case 239:
+        aiNewState(pSprite, pXSprite, &innocentBurnGoto);
+        break;
+    case 253:
+        aiNewState(pSprite, pXSprite, &beastBurnGoto);
+        break;
+    case 252:
+        aiNewState(pSprite, pXSprite, &tinycalebBurnGoto);
+        break;
+    case kGDXGenDudeBurning:
+        aiNewState(pSprite, pXSprite, &GDXGenDudeBurnSearch);
+        break;
+    }
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aiburn.h b/source/blood/src/aiburn.h
new file mode 100644
index 000000000..ff4cde645
--- /dev/null
+++ b/source/blood/src/aiburn.h
@@ -0,0 +1,56 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE cultistBurnIdle;
+extern AISTATE cultistBurnChase;
+extern AISTATE cultistBurnGoto;
+extern AISTATE cultistBurnSearch;
+extern AISTATE cultistBurnAttack;
+extern AISTATE zombieABurnChase;
+extern AISTATE zombieABurnGoto;
+extern AISTATE zombieABurnSearch;
+extern AISTATE zombieABurnAttack;
+extern AISTATE zombieFBurnChase;
+extern AISTATE zombieFBurnGoto;
+extern AISTATE zombieFBurnSearch;
+extern AISTATE zombieFBurnAttack;
+extern AISTATE innocentBurnChase;
+extern AISTATE innocentBurnGoto;
+extern AISTATE innocentBurnSearch;
+extern AISTATE innocentBurnAttack;
+extern AISTATE beastBurnChase;
+extern AISTATE beastBurnGoto;
+extern AISTATE beastBurnSearch;
+extern AISTATE beastBurnAttack;
+extern AISTATE tinycalebBurnChase;
+extern AISTATE tinycalebBurnGoto;
+extern AISTATE tinycalebBurnSearch;
+extern AISTATE tinycalebBurnAttack;
+extern AISTATE GDXGenDudeBurnIdle;
+extern AISTATE GDXGenDudeBurnChase;
+extern AISTATE GDXGenDudeBurnGoto;
+extern AISTATE GDXGenDudeBurnSearch;
+extern AISTATE GDXGenDudeBurnAttack;
+
diff --git a/source/blood/src/aicaleb.cpp b/source/blood/src/aicaleb.cpp
new file mode 100644
index 000000000..5ad414ded
--- /dev/null
+++ b/source/blood/src/aicaleb.cpp
@@ -0,0 +1,423 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aicaleb.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void SeqAttackCallback(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void thinkSwimGoto(spritetype *, XSPRITE *);
+static void thinkSwimChase(spritetype *, XSPRITE *);
+static void sub_65D04(spritetype *, XSPRITE *);
+static void sub_65F44(spritetype *, XSPRITE *);
+static void sub_661E0(spritetype *, XSPRITE *);
+
+static int nAttackClient = seqRegisterClient(SeqAttackCallback);
+
+AISTATE tinycalebIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE tinycalebChase = { kAiStateChase, 6, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE tinycalebDodge = { kAiStateMove, 6, -1, 90, NULL, aiMoveDodge, NULL, &tinycalebChase };
+AISTATE tinycalebGoto = { kAiStateMove, 6, -1, 600, NULL, aiMoveForward, thinkGoto, &tinycalebIdle };
+AISTATE tinycalebAttack = { kAiStateChase, 0, nAttackClient, 120, NULL, NULL, NULL, &tinycalebChase };
+AISTATE tinycalebSearch = { kAiStateSearch, 6, -1, 120, NULL, aiMoveForward, thinkSearch, &tinycalebIdle };
+AISTATE tinycalebRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &tinycalebDodge };
+AISTATE tinycalebTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &tinycalebDodge };
+AISTATE tinycalebSwimIdle = { kAiStateIdle, 10, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE tinycalebSwimChase = { kAiStateChase, 8, -1, 0, NULL, sub_65D04, thinkSwimChase, NULL };
+AISTATE tinycalebSwimDodge = { kAiStateMove, 8, -1, 90, NULL, aiMoveDodge, NULL, &tinycalebSwimChase };
+AISTATE tinycalebSwimGoto = { kAiStateMove, 8, -1, 600, NULL, aiMoveForward, thinkSwimGoto, &tinycalebSwimIdle };
+AISTATE tinycalebSwimSearch = { kAiStateSearch, 8, -1, 120, NULL, aiMoveForward, thinkSearch, &tinycalebSwimIdle };
+AISTATE tinycalebSwimAttack = { kAiStateChase, 10, nAttackClient, 0, NULL, NULL, NULL, &tinycalebSwimChase };
+AISTATE tinycalebSwimRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &tinycalebSwimDodge };
+AISTATE tinycaleb139660 = { kAiStateOther, 8, -1, 120, NULL, sub_65F44, thinkSwimChase, &tinycalebSwimChase };
+AISTATE tinycaleb13967C = { kAiStateOther, 8, -1, 0, NULL, sub_661E0, thinkSwimChase, &tinycalebSwimChase };
+AISTATE tinycaleb139698 = { kAiStateOther, 8, -1, 120, NULL, aiMoveTurn, NULL, &tinycalebSwimChase };
+
+static void SeqAttackCallback(int, int nXSprite)
+{
+    int nSprite = xsprite[nXSprite].reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    int dz = gDudeSlope[nXSprite];
+    dx += Random2(1500);
+    dy += Random2(1500);
+    dz += Random2(1500);
+    for (int i = 0; i < 2; i++)
+    {
+        int r1 = Random3(500);
+        int r2 = Random3(1000);
+        int r3 = Random3(1000);
+        actFireVector(pSprite, 0, 0, dx+r3, dy+r2, dz+r1, VECTOR_TYPE_1);
+    }
+    if (Chance(0x8000))
+        sfxPlay3DSound(pSprite, 10000+Random(5), -1, 0);
+    if (Chance(0x8000))
+        sfxPlay3DSound(pSprite, 1001, -1, 0);
+    else
+        sfxPlay3DSound(pSprite, 1002, -1, 0);
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    XSECTOR *pXSector;
+    int nXSector = sector[pSprite->sectnum].extra;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    else
+        pXSector = NULL;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &tinycalebSearch);
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &tinycalebSearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+        else
+        {
+            aiPlay3DSound(pSprite, 11000+Random(4), AI_SFX_PRIORITY_1, -1);
+            aiNewState(pSprite, pXSprite, &tinycalebSearch);
+        }
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &tinycalebSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int nXSprite = sprite[pXSprite->reference].extra;
+                gDudeSlope[nXSprite] = divscale(pTarget->z-pSprite->z, nDist, 10);
+                if (nDist < 0x599 && klabs(nDeltaAngle) < 28)
+                {
+                    XSECTOR *pXSector;
+                    int nXSector = sector[pSprite->sectnum].extra;
+                    if (nXSector > 0)
+                        pXSector = &xsector[nXSector];
+                    else
+                        pXSector = NULL;
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (hit)
+                    {
+                    case -1:
+                        if (pXSector && pXSector->Underwater)
+                            aiNewState(pSprite, pXSprite, &tinycalebSwimAttack);
+                        else
+                            aiNewState(pSprite, pXSprite, &tinycalebAttack);
+                        break;
+                    case 3:
+                        if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                        {
+                            if (pXSector && pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &tinycalebSwimAttack);
+                            else
+                                aiNewState(pSprite, pXSprite, &tinycalebAttack);
+                        }
+                        else
+                        {
+                            if (pXSector && pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &tinycalebSwimDodge);
+                            else
+                                aiNewState(pSprite, pXSprite, &tinycalebDodge);
+                        }
+                        break;
+                    default:
+                        if (pXSector && pXSector->Underwater)
+                            aiNewState(pSprite, pXSprite, &tinycalebSwimAttack);
+                        else
+                            aiNewState(pSprite, pXSprite, &tinycalebAttack);
+                        break;
+                    }
+                }
+            }
+            return;
+        }
+    }
+
+    XSECTOR *pXSector;
+    int nXSector = sector[pSprite->sectnum].extra;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    else
+        pXSector = NULL;
+    if (pXSector && pXSector->Underwater)
+        aiNewState(pSprite, pXSprite, &tinycalebSwimGoto);
+    else
+        aiNewState(pSprite, pXSprite, &tinycalebGoto);
+    if (Chance(0x2000))
+        sfxPlay3DSound(pSprite, 10000 + Random(5), -1, 0);
+    pXSprite->target = -1;
+}
+
+static void thinkSwimGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkSwimChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &tinycalebSwimGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = pDudeInfo->eyeHeight+pSprite->z;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int UNUSED(floorZ) = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &tinycalebSwimAttack);
+                else
+                    aiNewState(pSprite, pXSprite, &tinycaleb13967C);
+            }
+        }
+        return;
+    }
+    aiNewState(pSprite, pXSprite, &tinycalebSwimGoto);
+    pXSprite->target = -1;
+}
+
+static void sub_65D04(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Random(64) < 32 && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>2;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void sub_65F44(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int z = pSprite->z + dudeInfo[pSprite->type - kDudeBase].eyeHeight;
+    int z2 = pTarget->z + dudeInfo[pTarget->type - kDudeBase].eyeHeight;
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int dz = z2 - z;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -dz;
+}
+
+static void sub_661E0(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int z = pSprite->z + dudeInfo[pSprite->type - kDudeBase].eyeHeight;
+    int z2 = pTarget->z + dudeInfo[pTarget->type - kDudeBase].eyeHeight;
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pSprite->ang = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int dz = (z2 - z)<<3;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = dz;
+}
diff --git a/source/blood/src/aicaleb.h b/source/blood/src/aicaleb.h
new file mode 100644
index 000000000..969070bda
--- /dev/null
+++ b/source/blood/src/aicaleb.h
@@ -0,0 +1,43 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE tinycalebIdle;
+extern AISTATE tinycalebChase;
+extern AISTATE tinycalebDodge;
+extern AISTATE tinycalebGoto;
+extern AISTATE tinycalebAttack;
+extern AISTATE tinycalebSearch;
+extern AISTATE tinycalebRecoil;
+extern AISTATE tinycalebTeslaRecoil;
+extern AISTATE tinycalebSwimIdle;
+extern AISTATE tinycalebSwimChase;
+extern AISTATE tinycalebSwimDodge;
+extern AISTATE tinycalebSwimGoto;
+extern AISTATE tinycalebSwimSearch;
+extern AISTATE tinycalebSwimAttack;
+extern AISTATE tinycalebSwimRecoil;
+extern AISTATE tinycaleb139660;
+extern AISTATE tinycaleb13967C;
+extern AISTATE tinycaleb139698;
diff --git a/source/blood/src/aicerber.cpp b/source/blood/src/aicerber.cpp
new file mode 100644
index 000000000..8d509bf9b
--- /dev/null
+++ b/source/blood/src/aicerber.cpp
@@ -0,0 +1,489 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aicerber.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void BiteSeqCallback(int, int);
+static void BurnSeqCallback(int, int);
+static void BurnSeqCallback2(int, int);
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite);
+static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite);
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite);
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite);
+
+static int nBiteClient = seqRegisterClient(BiteSeqCallback);
+static int nBurnClient = seqRegisterClient(BurnSeqCallback);
+static int nBurnClient2 = seqRegisterClient(BurnSeqCallback2);
+
+AISTATE cerberusIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE cerberusSearch = { kAiStateSearch, 7, -1, 1800, NULL, aiMoveForward, thinkSearch, &cerberusIdle };
+AISTATE cerberusChase = { kAiStateChase, 7, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE cerberusRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cerberusSearch };
+AISTATE cerberusTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &cerberusSearch };
+AISTATE cerberusGoto = { kAiStateMove, 7, -1, 600, NULL, aiMoveForward, thinkGoto, &cerberusIdle };
+AISTATE cerberusBite = { kAiStateChase, 6, nBiteClient, 60, NULL, NULL, NULL, &cerberusChase };
+AISTATE cerberusBurn = { kAiStateChase, 6, nBurnClient, 60, NULL, NULL, NULL, &cerberusChase };
+AISTATE cerberus3Burn = { kAiStateChase, 6, nBurnClient2, 60, NULL, NULL, NULL, &cerberusChase };
+AISTATE cerberus2Idle = { kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE cerberus2Search = { kAiStateSearch, 7, -1, 1800, NULL, aiMoveForward, thinkSearch, &cerberus2Idle };
+AISTATE cerberus2Chase = { kAiStateChase, 7, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE cerberus2Recoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cerberus2Search };
+AISTATE cerberus2Goto = { kAiStateMove, 7, -1, 600, NULL, aiMoveForward, thinkGoto, &cerberus2Idle };
+AISTATE cerberus2Bite = { kAiStateChase, 6, nBiteClient, 60, NULL, NULL, NULL, &cerberus2Chase };
+AISTATE cerberus2Burn = { kAiStateChase, 6, nBurnClient, 60, NULL, NULL, NULL, &cerberus2Chase };
+AISTATE cerberus4Burn = { kAiStateChase, 6, nBurnClient2, 60, NULL, NULL, NULL, &cerberus2Chase };
+AISTATE cerberus139890 = { kAiStateOther, 7, -1, 120, NULL, aiMoveTurn, NULL, &cerberusChase };
+AISTATE cerberus1398AC = { kAiStateOther, 7, -1, 120, NULL, aiMoveTurn, NULL, &cerberusChase };
+
+static void BiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int dz = pTarget->z-pSprite->z;
+    actFireVector(pSprite, 350, -100, dx, dy, dz, VECTOR_TYPE_14);
+    actFireVector(pSprite, -350, 0, dx, dy, dz, VECTOR_TYPE_14);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_14);
+}
+
+static void BurnSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int height = pDudeInfo->eyeHeight*pSprite->yrepeat;
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = height; // ???
+    TARGETTRACK tt1 = { 0x10000, 0x10000, 0x100, 0x55, 0x1aaaaa };
+    Aim aim;
+    aim.dx = Cos(pSprite->ang)>>16;
+    aim.dy = Sin(pSprite->ang)>>16;
+    aim.dz = gDudeSlope[nXSprite];
+    int nClosest = 0x7fffffff;
+    for (short nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite == pSprite2 || !(pSprite2->hitag&8))
+            continue;
+        int x2 = pSprite2->x;
+        int y2 = pSprite2->y;
+        int z2 = pSprite2->z;
+        int nDist = approxDist(x2-x, y2-y);
+        if (nDist == 0 || nDist > 0x2800)
+            continue;
+        if (tt1.at10)
+        {
+            int t = divscale(nDist, tt1.at10, 12);
+            x2 += (xvel[nSprite2]*t)>>12;
+            y2 += (yvel[nSprite2]*t)>>12;
+            z2 += (zvel[nSprite2]*t)>>8;
+        }
+        int tx = x+mulscale30(Cos(pSprite->ang), nDist);
+        int ty = y+mulscale30(Sin(pSprite->ang), nDist);
+        int tz = z+mulscale(gDudeSlope[nXSprite], nDist, 10);
+        int tsr = mulscale(9460, nDist, 10);
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (tz-tsr > bottom || tz+tsr < top)
+            continue;
+        int dx = (tx-x2)>>4;
+        int dy = (ty-y2)>>4;
+        int dz = (tz-z2)>>8;
+        int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+        if (nDist2 < nClosest)
+        {
+            int nAngle = getangle(x2-x, y2-y);
+            int nDeltaAngle = ((nAngle-pSprite->ang+1024)&2047)-1024;
+            if (klabs(nDeltaAngle) <= tt1.at8)
+            {
+                int tz = pSprite2->z-pSprite->z;
+                if (cansee(x, y, z, pSprite->sectnum, x2, y2, z2, pSprite2->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(nAngle)>>16;
+                    aim.dy = Sin(nAngle)>>16;
+                    aim.dz = divscale(tz, nDist, 10);
+                }
+                else
+                    aim.dz = tz;
+            }
+        }
+    }
+    switch (pSprite->type)
+    {
+    case 227:
+        actFireMissile(pSprite, -350, 0, aim.dx, aim.dy, aim.dz, 313);
+        actFireMissile(pSprite, 350, -100, aim.dx, aim.dy, aim.dz, 313);
+        break;
+    case 228:
+        actFireMissile(pSprite, 350, -100, aim.dx, aim.dy, aim.dz, 313);
+        break;
+    }
+}
+
+static void BurnSeqCallback2(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int height = pDudeInfo->eyeHeight*pSprite->yrepeat;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = height; // ???
+    TARGETTRACK tt1 = { 0x10000, 0x10000, 0x100, 0x55, 0x1aaaaa };
+    Aim aim;
+    int ax, ay, az;
+    aim.dx = ax = Cos(pSprite->ang)>>16;
+    aim.dy = ay = Sin(pSprite->ang)>>16;
+    aim.dz = gDudeSlope[nXSprite];
+    az = 0;
+    int nClosest = 0x7fffffff;
+    for (short nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite == pSprite2 || !(pSprite2->hitag&8))
+            continue;
+        int x2 = pSprite2->x;
+        int y2 = pSprite2->y;
+        int z2 = pSprite2->z;
+        int nDist = approxDist(x2-x, y2-y);
+        if (nDist == 0 || nDist > 0x2800)
+            continue;
+        if (tt1.at10)
+        {
+            int t = divscale(nDist, tt1.at10, 12);
+            x2 += (xvel[nSprite2]*t)>>12;
+            y2 += (yvel[nSprite2]*t)>>12;
+            z2 += (zvel[nSprite2]*t)>>8;
+        }
+        int tx = x+mulscale30(Cos(pSprite->ang), nDist);
+        int ty = y+mulscale30(Sin(pSprite->ang), nDist);
+        int tz = z+mulscale(gDudeSlope[nXSprite], nDist, 10);
+        int tsr = mulscale(9460, nDist, 10);
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (tz-tsr > bottom || tz+tsr < top)
+            continue;
+        int dx = (tx-x2)>>4;
+        int dy = (ty-y2)>>4;
+        int dz = (tz-z2)>>8;
+        int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+        if (nDist2 < nClosest)
+        {
+            int nAngle = getangle(x2-x, y2-y);
+            int nDeltaAngle = ((nAngle-pSprite->ang+1024)&2047)-1024;
+            if (klabs(nDeltaAngle) <= tt1.at8)
+            {
+                DUDEINFO *pDudeInfo2 = &dudeInfo[pSprite2->type - kDudeBase];
+                int height = (pDudeInfo2->aimHeight*pSprite2->yrepeat)<<2;
+                int tz = (z2-height)-z;
+                if (cansee(x, y, z, pSprite->sectnum, x2, y2, z2, pSprite2->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(nAngle)>>16;
+                    aim.dy = Sin(nAngle)>>16;
+                    aim.dz = divscale(tz, nDist, 10);
+                }
+                else
+                    aim.dz = tz;
+            }
+        }
+    }
+    switch (pSprite->type)
+    {
+    case 227:
+        actFireMissile(pSprite, 350, -100, aim.dx, aim.dy, -aim.dz, 308);
+        actFireMissile(pSprite, -350, 0, ax, ay, az, 308);
+        break;
+    case 228:
+        actFireMissile(pSprite, 350, -100, aim.dx, aim.dy, -aim.dz, 308);
+        break;
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+    if (pDudeExtraE->at8 && pDudeExtraE->at4 < 10)
+        pDudeExtraE->at4++;
+    else if (pDudeExtraE->at4 >= 10 && pDudeExtraE->at8)
+    {
+        pXSprite->goalAng += 256;
+        POINT3D *pTarget = &baseSprite[pSprite->index];
+        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
+        if (pSprite->type == 227)
+            aiNewState(pSprite, pXSprite, &cerberus139890);
+        else
+            aiNewState(pSprite, pXSprite, &cerberus1398AC);
+        return;
+    }
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                pDudeExtraE->at0 = 0;
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                pDudeExtraE->at0 = 0;
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else
+                continue;
+            break;
+        }
+    }
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        switch (pSprite->type)
+        {
+        case 227:
+            aiNewState(pSprite, pXSprite, &cerberusSearch);
+            break;
+        case 228:
+            aiNewState(pSprite, pXSprite, &cerberus2Search);
+            break;
+        }
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        switch (pSprite->type)
+        {
+        case 227:
+            aiNewState(pSprite, pXSprite, &cerberusGoto);
+            break;
+        case 228:
+            aiNewState(pSprite, pXSprite, &cerberus2Goto);
+            break;
+        }
+        return;
+    }
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        switch (pSprite->type)
+        {
+        case 227:
+            aiNewState(pSprite, pXSprite, &cerberusSearch);
+            break;
+        case 228:
+            aiNewState(pSprite, pXSprite, &cerberus2Search);
+            break;
+        }
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        switch (pSprite->type)
+        {
+        case 227:
+            aiNewState(pSprite, pXSprite, &cerberusSearch);
+            break;
+        case 228:
+            aiNewState(pSprite, pXSprite, &cerberus2Search);
+            break;
+        }
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x1b00 && nDist > 0xd00 && klabs(nDeltaAngle) < 85)
+                {
+                    switch (pSprite->type)
+                    {
+                    case 227:
+                        aiNewState(pSprite, pXSprite, &cerberusBurn);
+                        break;
+                    case 228:
+                        aiNewState(pSprite, pXSprite, &cerberus2Burn);
+                        break;
+                    }
+                }
+                else if (nDist < 0xb00 && nDist > 0x500 && klabs(nDeltaAngle) < 85)
+                {
+                    switch (pSprite->type)
+                    {
+                    case 227:
+                        aiNewState(pSprite, pXSprite, &cerberus3Burn);
+                        break;
+                    case 228:
+                        aiNewState(pSprite, pXSprite, &cerberus4Burn);
+                        break;
+                    }
+                }
+                else if (nDist < 0x200 && klabs(nDeltaAngle) < 85)
+                {
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (pSprite->type)
+                    {
+                    case 227:
+                        switch (hit)
+                        {
+                        case -1:
+                            aiNewState(pSprite, pXSprite, &cerberusBite);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 211)
+                                aiNewState(pSprite, pXSprite, &cerberusBite);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cerberusBite);
+                            break;
+                        }
+                        break;
+                    case 228:
+                        switch (hit)
+                        {
+                        case -1:
+                            aiNewState(pSprite, pXSprite, &cerberus2Bite);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 211)
+                                aiNewState(pSprite, pXSprite, &cerberus2Bite);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cerberus2Bite);
+                            break;
+                        }
+                        break;
+                    }
+                }
+                return;
+            }
+        }
+    }
+
+    switch (pSprite->type)
+    {
+    case 227:
+        aiNewState(pSprite, pXSprite, &cerberusGoto);
+        break;
+    case 228:
+        aiNewState(pSprite, pXSprite, &cerberus2Goto);
+        break;
+    }
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aicerber.h b/source/blood/src/aicerber.h
new file mode 100644
index 000000000..5c416e31a
--- /dev/null
+++ b/source/blood/src/aicerber.h
@@ -0,0 +1,44 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE cerberusIdle;
+extern AISTATE cerberusSearch;
+extern AISTATE cerberusChase;
+extern AISTATE cerberusRecoil;
+extern AISTATE cerberusTeslaRecoil;
+extern AISTATE cerberusGoto;
+extern AISTATE cerberusBite;
+extern AISTATE cerberusBurn;
+extern AISTATE cerberus3Burn;
+extern AISTATE cerberus2Idle;
+extern AISTATE cerberus2Search;
+extern AISTATE cerberus2Chase;
+extern AISTATE cerberus2Recoil;
+extern AISTATE cerberus2Goto;
+extern AISTATE cerberus2Bite;
+extern AISTATE cerberus2Burn;
+extern AISTATE cerberus4Burn;
+extern AISTATE cerberus139890;
+extern AISTATE cerberus1398AC;
diff --git a/source/blood/src/aicult.cpp b/source/blood/src/aicult.cpp
new file mode 100644
index 000000000..b64311e43
--- /dev/null
+++ b/source/blood/src/aicult.cpp
@@ -0,0 +1,659 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aicult.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void TommySeqCallback(int, int);
+static void TeslaSeqCallback(int, int);
+static void ShotSeqCallback(int, int);
+static void ThrowSeqCallback(int, int);
+static void sub_68170(int, int);
+static void sub_68230(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+
+static int nTommyClient = seqRegisterClient(TommySeqCallback);
+static int nTeslaClient = seqRegisterClient(TeslaSeqCallback);
+static int nShotClient = seqRegisterClient(ShotSeqCallback);
+static int nThrowClient = seqRegisterClient(ThrowSeqCallback);
+static int n68170Client = seqRegisterClient(sub_68170);
+static int n68230Client = seqRegisterClient(sub_68230);
+
+AISTATE cultistIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE cultistProneIdle = { kAiStateIdle, 17, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE fanaticProneIdle = { kAiStateIdle, 17, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE cultistProneIdle3 = { kAiStateIdle, 17, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE cultistChase = { kAiStateChase, 9, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE fanaticChase = { kAiStateChase, 0, -1, 0, NULL, aiMoveTurn, thinkChase, NULL };
+AISTATE cultistDodge = { kAiStateMove, 9, -1, 90, NULL, aiMoveDodge, NULL, &cultistChase };
+AISTATE cultistGoto = { kAiStateMove, 9, -1, 600, NULL, aiMoveForward, thinkGoto, &cultistIdle };
+AISTATE cultistProneChase = { kAiStateChase, 14, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE cultistProneDodge = { kAiStateMove, 14, -1, 90, NULL, aiMoveDodge, NULL, &cultistProneChase };
+AISTATE cultistTThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistTFire };
+AISTATE cultistSThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistSFire };
+AISTATE cultistTsThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistTsFire };
+AISTATE cultistDThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistChase };
+AISTATE cultist139A78 = { kAiStateChase, 7, n68170Client, 120, NULL, NULL, NULL, &cultistChase };
+AISTATE cultist139A94 = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, NULL, &cultistIdle };
+AISTATE cultist139AB0 = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, thinkSearch, &cultist139A94 };
+AISTATE cultist139ACC = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, thinkSearch, &cultist139AB0 };
+AISTATE cultist139AE8 = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, thinkSearch, &cultist139AE8 };
+AISTATE cultistSearch = { kAiStateSearch, 9, -1, 1800, NULL, aiMoveForward, thinkSearch, &cultistIdle };
+AISTATE cultistSFire = { kAiStateChase, 6, nShotClient, 60, NULL, NULL, NULL, &cultistChase };
+AISTATE cultistTFire = { kAiStateChase, 6, nTommyClient, 0, NULL, aiMoveTurn, thinkChase, &cultistTFire };
+AISTATE cultistTsFire = { kAiStateChase, 6, nTeslaClient, 0, NULL, aiMoveTurn, thinkChase, &cultistChase };
+AISTATE cultistSProneFire = { kAiStateChase, 8, nShotClient, 60, NULL, NULL, NULL, &cultistProneChase };
+AISTATE cultistTProneFire = { kAiStateChase, 8, nTommyClient, 0, NULL, aiMoveTurn, thinkChase, &cultistTProneFire };
+AISTATE cultistTsProneFire = { kAiStateChase, 8, nTeslaClient, 0, NULL, aiMoveTurn, NULL, &cultistTsProneFire };
+AISTATE cultistRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cultistDodge };
+AISTATE cultistProneRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cultistProneDodge };
+AISTATE cultistTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &cultistDodge };
+AISTATE cultistSwimIdle = { kAiStateIdle, 13, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE cultistSwimChase = { kAiStateChase, 13, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE cultistSwimDodge = { kAiStateMove, 13, -1, 90, NULL, aiMoveDodge, NULL, &cultistSwimChase };
+AISTATE cultistSwimGoto = { kAiStateMove, 13, -1, 600, NULL, aiMoveForward, thinkGoto, &cultistSwimIdle };
+AISTATE cultistSwimSearch = { kAiStateSearch, 13, -1, 1800, NULL, aiMoveForward, thinkSearch, &cultistSwimIdle };
+AISTATE cultistSSwimFire = { kAiStateChase, 8, nShotClient, 60, NULL, NULL, NULL, &cultistSwimChase };
+AISTATE cultistTSwimFire = { kAiStateChase, 8, nTommyClient, 0, NULL, aiMoveTurn, thinkChase, &cultistTSwimFire };
+AISTATE cultistTsSwimFire = { kAiStateChase, 8, nTeslaClient, 0, NULL, aiMoveTurn, thinkChase, &cultistTsSwimFire };
+AISTATE cultistSwimRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cultistSwimDodge };
+
+static void TommySeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang) >> 16;
+    int dy = Sin(pSprite->ang) >> 16;
+    int dz = gDudeSlope[nXSprite];
+    dx += Random3((5-gGameOptions.nDifficulty)*1000);
+    dy += Random3((5-gGameOptions.nDifficulty)*1000);
+    dz += Random3((5-gGameOptions.nDifficulty)*500);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_2);
+    sfxPlay3DSound(pSprite, 4001, -1, 0);
+}
+
+static void TeslaSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    if (Chance(dword_138BB0[gGameOptions.nDifficulty]))
+    {
+        int dx = Cos(pSprite->ang) >> 16;
+        int dy = Sin(pSprite->ang) >> 16;
+        int dz = gDudeSlope[nXSprite];
+        dx += Random3((5-gGameOptions.nDifficulty)*1000);
+        dy += Random3((5-gGameOptions.nDifficulty)*1000);
+        dz += Random3((5-gGameOptions.nDifficulty)*500);
+        actFireMissile(pSprite, 0, 0, dx, dy, dz, 306);
+        sfxPlay3DSound(pSprite, 470, -1, 0);
+    }
+}
+
+static void ShotSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang) >> 16;
+    int dy = Sin(pSprite->ang) >> 16;
+    int dz = gDudeSlope[nXSprite];
+    dx += Random2((5-gGameOptions.nDifficulty)*1000-500);
+    dy += Random2((5-gGameOptions.nDifficulty)*1000-500);
+    dz += Random2((5-gGameOptions.nDifficulty)*500);
+    for (int i = 0; i < 8; i++)
+    {
+        int r1 = Random3(500);
+        int r2 = Random3(1000);
+        int r3 = Random3(1000);
+        actFireVector(pSprite, 0, 0, dx+r3, dy+r2, dz+r1, VECTOR_TYPE_1);
+    }
+    if (Chance(0x8000))
+        sfxPlay3DSound(pSprite, 1001, -1, 0);
+    else
+        sfxPlay3DSound(pSprite, 1002, -1, 0);
+}
+
+static void ThrowSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int nMissile = 418;
+    if (gGameOptions.nDifficulty > 2)
+        nMissile = 419;
+    char v4 = Chance(0x6000);
+    sfxPlay3DSound(pSprite, 455, -1, 0);
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    int dx = pTarget->x - pSprite->x;
+    int dy = pTarget->y - pSprite->y;
+    int dz = pTarget->z - pSprite->z;
+    int nDist = approxDist(dx, dy);
+    int nDist2 = nDist / 540;
+    if (nDist > 0x1e00)
+        v4 = 0;
+    spritetype *pMissile = actFireThing(pSprite, 0, 0, dz/128-14500, nMissile, (nDist2<<23)/120);
+    if (v4)
+        xsprite[pMissile->extra].Impact = 1;
+    else
+        evPost(pMissile->index, 3, 120*(1+Random(2)), COMMAND_ID_1);
+}
+
+static void sub_68170(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int nMissile = 418;
+    if (gGameOptions.nDifficulty > 2)
+        nMissile = 419;
+    sfxPlay3DSound(pSprite, 455, -1, 0);
+    spritetype *pMissile = actFireThing(pSprite, 0, 0, gDudeSlope[nXSprite]-9460, nMissile, 0x133333);
+    evPost(pMissile->index, 3, 120*(2+Random(2)), COMMAND_ID_1);
+}
+
+static void sub_68230(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int nMissile = 418;
+    if (gGameOptions.nDifficulty > 2)
+        nMissile = 419;
+    sfxPlay3DSound(pSprite, 455, -1, 0);
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    int dx = pTarget->x - pSprite->x;
+    int dy = pTarget->y - pSprite->y;
+    int dz = pTarget->z - pSprite->z;
+    int nDist = approxDist(dx, dy);
+    int nDist2 = nDist / 540;
+    spritetype *pMissile = actFireThing(pSprite, 0, 0, dz/128-14500, nMissile, (nDist2<<17)/120);
+    xsprite[pMissile->extra].Impact = 1;
+}
+
+static char TargetNearExplosion(spritetype *pSprite)
+{
+    for (short nSprite = headspritesect[pSprite->sectnum]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        if (sprite[nSprite].type == 418 || sprite[nSprite].statnum == 2)
+            return 1;
+    }
+    return 0;
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    sub_5F15C(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 5120 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        switch (pXSprite->medium)
+        {
+        case 0:
+            aiNewState(pSprite, pXSprite, &cultistSearch);
+            break;
+        case 1:
+        case 2:
+            aiNewState(pSprite, pXSprite, &cultistSwimSearch);
+            break;
+        }
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        switch (pXSprite->medium)
+        {
+        case 0:
+            aiNewState(pSprite, pXSprite, &cultistGoto);
+            break;
+        case 1:
+        case 2:
+            aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+            break;
+        }
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        switch (pXSprite->medium)
+        {
+        case 0:
+            aiNewState(pSprite, pXSprite, &cultistSearch);
+            if (pSprite->type == 201)
+                aiPlay3DSound(pSprite, 4021+Random(4), AI_SFX_PRIORITY_1, -1);
+            else
+                aiPlay3DSound(pSprite, 1021+Random(4), AI_SFX_PRIORITY_1, -1);
+            break;
+        case 1:
+        case 2:
+            aiNewState(pSprite, pXSprite, &cultistSwimSearch);
+            break;
+        }
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        switch (pXSprite->medium)
+        {
+        case 0:
+            aiNewState(pSprite, pXSprite, &cultistSearch);
+            break;
+        case 1:
+        case 2:
+            aiNewState(pSprite, pXSprite, &cultistSwimSearch);
+            break;
+        }
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int nXSprite = sprite[pXSprite->reference].extra;
+                gDudeSlope[nXSprite] = divscale(pTarget->z-pSprite->z, nDist, 10);
+                switch (pSprite->type)
+                {
+                case 201:
+                    if (nDist < 0x1e00 && nDist > 0xe00 && klabs(nDeltaAngle) < 85 && !TargetNearExplosion(pTarget)
+                        && (pTarget->hitag&2) && gGameOptions.nDifficulty > 2 && IsPlayerSprite(pTarget) && gPlayer[pTarget->type-kDudePlayer1].at2e
+                        && Chance(0x8000))
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistTThrow);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202 && pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistTThrow);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cultistTThrow);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x4600 && klabs(nDeltaAngle) < 28)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTProneFire);
+                            else if (sub_5BDA8(pSprite, 13) && (pXSprite->medium == 1 || pXSprite->medium == 2))
+                                aiNewState(pSprite, pXSprite, &cultistTSwimFire);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202)
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistTFire);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistTProneFire);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistTSwimFire);
+                            }
+                            else
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistDodge);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistProneDodge);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistSwimDodge);
+                            }
+                            break;
+                        default:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistTSwimFire);
+                            break;
+                        }
+                    }
+                    break;
+                case 202:
+                    if (nDist < 0x2c00 && nDist > 0x1400 && !TargetNearExplosion(pTarget)
+                        && (pTarget->hitag&2) && gGameOptions.nDifficulty >= 2 && IsPlayerSprite(pTarget) && !gPlayer[pTarget->type-kDudePlayer1].at2e
+                        && Chance(0x8000))
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistSThrow);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202 && pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistSThrow);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cultistSThrow);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x3200 && klabs(nDeltaAngle) < 28)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistSSwimFire);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 201)
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistSFire);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistSProneFire);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistSSwimFire);
+                            }
+                            else
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistDodge);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistProneDodge);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistSwimDodge);
+                            }
+                            break;
+                        default:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistSSwimFire);
+                            break;
+                        }
+                    }
+                    break;
+                case 247:
+                    if (nDist < 0x1e00 && nDist > 0xe00 && !TargetNearExplosion(pTarget)
+                        && (pTarget->hitag&2) && gGameOptions.nDifficulty > 2 && IsPlayerSprite(pTarget) && gPlayer[pTarget->type-kDudePlayer1].at2e
+                        && Chance(0x8000))
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistTsThrow);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202 && pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistTsThrow);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cultistTsThrow);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x3200 && klabs(nDeltaAngle) < 28)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTsFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTsProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistTsSwimFire);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 201)
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistTsFire);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistTsProneFire);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistTsSwimFire);
+                            }
+                            else
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistDodge);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistProneDodge);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistSwimDodge);
+                            }
+                            break;
+                        default:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTsFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistTsProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistTsSwimFire);
+                            break;
+                        }
+                    }
+                    break;
+                case 248:
+                    if (nDist < 0x2c00 && nDist > 0x1400 && klabs(nDeltaAngle) < 85
+                        && (pTarget->hitag&2) && IsPlayerSprite(pTarget))
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistDThrow);
+                            break;
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202 && pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistDThrow);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cultistDThrow);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x1400 && klabs(nDeltaAngle) < 85
+                        && (pTarget->hitag&2) && IsPlayerSprite(pTarget))
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultist139A78);
+                            break;
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202 && pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultist139A78);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cultist139A78);
+                            break;
+                        }
+                    }
+                    break;
+                case 249:
+                    if (nDist < 0x1e00 && nDist > 0xe00 && !TargetNearExplosion(pTarget)
+                        && (pTarget->hitag&2) && gGameOptions.nDifficulty > 2 && IsPlayerSprite(pTarget) && gPlayer[pTarget->type-kDudePlayer1].at2e
+                        && Chance(0x8000))
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistSThrow);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 202 && pXSprite->medium != 1 && pXSprite->medium != 2)
+                                aiNewState(pSprite, pXSprite, &cultistSThrow);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &cultistSThrow);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x3200 && klabs(nDeltaAngle) < 28)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistSSwimFire);
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 201)
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistSFire);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistSProneFire);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistSSwimFire);
+                            }
+                            else
+                            {
+                                if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistDodge);
+                                else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                    aiNewState(pSprite, pXSprite, &cultistProneDodge);
+                                else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                    aiNewState(pSprite, pXSprite, &cultistSwimDodge);
+                            }
+                            break;
+                        default:
+                            if (!sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSFire);
+                            else if (sub_5BDA8(pSprite, 14) && pXSprite->medium == 0)
+                                aiNewState(pSprite, pXSprite, &cultistSProneFire);
+                            else if (pXSprite->medium == 1 || pXSprite->medium == 2)
+                                aiNewState(pSprite, pXSprite, &cultistSSwimFire);
+                            break;
+                        }
+                    }
+                    break;
+                }
+                return;
+            }
+        }
+    }
+    switch (pXSprite->medium)
+    {
+    case 0:
+        aiNewState(pSprite, pXSprite, &cultistGoto);
+        break;
+    case 1:
+    case 2:
+        aiNewState(pSprite, pXSprite, &cultistSwimGoto);
+        break;
+    }
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aicult.h b/source/blood/src/aicult.h
new file mode 100644
index 000000000..0a09c3deb
--- /dev/null
+++ b/source/blood/src/aicult.h
@@ -0,0 +1,63 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE cultistIdle;
+extern AISTATE cultistProneIdle;
+extern AISTATE fanaticProneIdle;
+extern AISTATE cultistProneIdle3;
+extern AISTATE cultistChase;
+extern AISTATE fanaticChase;
+extern AISTATE cultistDodge;
+extern AISTATE cultistGoto;
+extern AISTATE cultistProneChase;
+extern AISTATE cultistProneDodge;
+extern AISTATE cultistTThrow;
+extern AISTATE cultistSThrow;
+extern AISTATE cultistTsThrow;
+extern AISTATE cultistDThrow;
+extern AISTATE cultist139A78;
+extern AISTATE cultist139A94;
+extern AISTATE cultist139AB0;
+extern AISTATE cultist139ACC;
+extern AISTATE cultist139AE8;
+extern AISTATE cultistSearch;
+extern AISTATE cultistSFire;
+extern AISTATE cultistTFire;
+extern AISTATE cultistTsFire;
+extern AISTATE cultistSProneFire;
+extern AISTATE cultistTProneFir;
+extern AISTATE cultistTsProneFire;
+extern AISTATE cultistRecoil;
+extern AISTATE cultistProneRecoil;
+extern AISTATE cultistTeslaRecoil;
+extern AISTATE cultistSwimIdle;
+extern AISTATE cultistSwimChase;
+extern AISTATE cultistSwimDodge;
+extern AISTATE cultistSwimGoto;
+extern AISTATE cultistSwimSearch;
+extern AISTATE cultistSSwimFire;
+extern AISTATE cultistTSwimFire;
+extern AISTATE cultistTsSwimFire;
+extern AISTATE cultistSwimRecoil;
diff --git a/source/blood/src/aigarg.cpp b/source/blood/src/aigarg.cpp
new file mode 100644
index 000000000..ddb7042dd
--- /dev/null
+++ b/source/blood/src/aigarg.cpp
@@ -0,0 +1,704 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aigarg.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void SlashFSeqCallback(int, int);
+static void ThrowFSeqCallback(int, int);
+static void BlastSSeqCallback(int, int);
+static void ThrowSSeqCallback(int, int);
+static void thinkTarget(spritetype *, XSPRITE *);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void MoveDodgeUp(spritetype *, XSPRITE *);
+static void MoveDodgeDown(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void entryFStatue(spritetype *, XSPRITE *);
+static void entrySStatue(spritetype *, XSPRITE *);
+static void MoveForward(spritetype *, XSPRITE *);
+static void MoveSlow(spritetype *, XSPRITE *);
+static void MoveSwoop(spritetype *, XSPRITE *);
+static void MoveFly(spritetype *, XSPRITE *);
+static void playStatueBreakSnd(spritetype*,XSPRITE*);
+
+static int nSlashFClient = seqRegisterClient(SlashFSeqCallback);
+static int nThrowFClient = seqRegisterClient(ThrowFSeqCallback);
+static int nThrowSClient = seqRegisterClient(ThrowSSeqCallback);
+static int nBlastSClient = seqRegisterClient(BlastSSeqCallback);
+
+AISTATE gargoyleFIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE gargoyleStatueIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, NULL, NULL };
+AISTATE gargoyleFChase = { kAiStateChase, 0, -1, 0, NULL, MoveForward, thinkChase, &gargoyleFIdle };
+AISTATE gargoyleFGoto = { kAiStateMove, 0, -1, 600, NULL, MoveForward, thinkGoto, &gargoyleFIdle };
+AISTATE gargoyleFSlash = { kAiStateChase, 6, nSlashFClient, 120, NULL, NULL, NULL, &gargoyleFChase };
+AISTATE gargoyleFThrow = { kAiStateChase, 6, nThrowFClient, 120, NULL, NULL, NULL, &gargoyleFChase };
+AISTATE gargoyleSThrow = { kAiStateChase, 6, nThrowSClient, 120, NULL, MoveForward, NULL, &gargoyleFChase };
+AISTATE gargoyleSBlast = { kAiStateChase, 7, nBlastSClient, 60, NULL, MoveSlow, NULL, &gargoyleFChase };
+AISTATE gargoyleFRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &gargoyleFChase };
+AISTATE gargoyleFSearch = { kAiStateSearch, 0, -1, 120, NULL, MoveForward, thinkSearch, &gargoyleFIdle };
+AISTATE gargoyleFMorph2 = { kAiStateOther, -1, -1, 0, entryFStatue, NULL, NULL, &gargoyleFIdle };
+AISTATE gargoyleFMorph = { kAiStateOther, 6, -1, 0, NULL, NULL, NULL, &gargoyleFMorph2 };
+AISTATE gargoyleSMorph2 = { kAiStateOther, -1, -1, 0, entrySStatue, NULL, NULL, &gargoyleFIdle };
+AISTATE gargoyleSMorph = { kAiStateOther, 6, -1, 0, NULL, NULL, NULL, &gargoyleSMorph2 };
+AISTATE gargoyleSwoop = { kAiStateOther, 0, -1, 120, NULL, MoveSwoop, thinkChase, &gargoyleFChase };
+AISTATE gargoyleFly = { kAiStateMove, 0, -1, 120, NULL, MoveFly, thinkChase, &gargoyleFChase };
+AISTATE gargoyleTurn = { kAiStateMove, 0, -1, 120, NULL, aiMoveTurn, NULL, &gargoyleFChase };
+AISTATE gargoyleDodgeUp = { kAiStateMove, 0, -1, 60, NULL, MoveDodgeUp, NULL, &gargoyleFChase };
+AISTATE gargoyleFDodgeUpRight = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeUp, NULL, &gargoyleFChase };
+AISTATE gargoyleFDodgeUpLeft = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeUp, NULL, &gargoyleFChase };
+AISTATE gargoyleDodgeDown = { kAiStateMove, 0, -1, 120, NULL, MoveDodgeDown, NULL, &gargoyleFChase };
+AISTATE gargoyleFDodgeDownRight = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeDown, NULL, &gargoyleFChase };
+AISTATE gargoyleFDodgeDownLeft = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeDown, NULL, &gargoyleFChase };
+
+AISTATE statueFBreakSEQ = { kAiStateOther, 5, -1, 0, entryFStatue, NULL, playStatueBreakSnd, &gargoyleFMorph2};
+AISTATE statueSBreakSEQ = { kAiStateOther, 5, -1, 0, entrySStatue, NULL, playStatueBreakSnd, &gargoyleSMorph2};
+
+static void playStatueBreakSnd(spritetype* pSprite, XSPRITE* pXSprite) {
+    UNREFERENCED_PARAMETER(pXSprite);
+    aiPlay3DSound(pSprite, 313, AI_SFX_PRIORITY_1, -1);
+}
+
+static void SlashFSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type - kDudeBase];
+    int height = (pSprite->yrepeat*pDudeInfo->eyeHeight)<<2;
+    int height2 = (pTarget->yrepeat*pDudeInfoT->eyeHeight)<<2;
+    int dz = height-height2;
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_13);
+    int r1 = Random(50);
+    int r2 = Random(50);
+    actFireVector(pSprite, 0, 0, dx+r2, dy-r1, dz, VECTOR_TYPE_13);
+    r1 = Random(50);
+    r2 = Random(50);
+    actFireVector(pSprite, 0, 0, dx-r2, dy+r1, dz, VECTOR_TYPE_13);
+}
+
+static void ThrowFSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    actFireThing(&sprite[nSprite], 0, 0, gDudeSlope[nXSprite]-7500, 421, 0xeeeee);
+}
+
+static void BlastSSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    wrand(); // ???
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int height = (pSprite->yrepeat*dudeInfo[pSprite->type-kDudeBase].eyeHeight) << 2;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nDist) = approxDist(dx, dy);
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = height;
+    TARGETTRACK tt = { 0x10000, 0x10000, 0x100, 0x55, 0x1aaaaa };
+    Aim aim;
+    aim.dx = Cos(pSprite->ang)>>16;
+    aim.dy = Sin(pSprite->ang)>>16;
+    aim.dz = gDudeSlope[nXSprite];
+    int nClosest = 0x7fffffff;
+    for (short nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite == pSprite2 || !(pSprite2->hitag&8))
+            continue;
+        int x2 = pSprite2->x;
+        int y2 = pSprite2->y;
+        int z2 = pSprite2->z;
+        int nDist = approxDist(x2-x, y2-y);
+        if (nDist == 0 || nDist > 0x2800)
+            continue;
+        if (tt.at10)
+        {
+            int t = divscale(nDist, tt.at10, 12);
+            x2 += (xvel[nSprite2]*t)>>12;
+            y2 += (yvel[nSprite2]*t)>>12;
+            z2 += (zvel[nSprite2]*t)>>8;
+        }
+        int tx = x+mulscale30(Cos(pSprite->ang), nDist);
+        int ty = y+mulscale30(Sin(pSprite->ang), nDist);
+        int tz = z+mulscale(gDudeSlope[nXSprite], nDist, 10);
+        int tsr = mulscale(9460, nDist, 10);
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (tz-tsr > bottom || tz+tsr < top)
+            continue;
+        int dx = (tx-x2)>>4;
+        int dy = (ty-y2)>>4;
+        int dz = (tz-z2)>>8;
+        int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+        if (nDist2 < nClosest)
+        {
+            int nAngle = getangle(x2-x, y2-y);
+            int nDeltaAngle = ((nAngle-pSprite->ang+1024)&2047)-1024;
+            if (klabs(nDeltaAngle) <= tt.at8)
+            {
+                int tz = pSprite2->z-pSprite->z;
+                if (cansee(x, y, z, pSprite->sectnum, x2, y2, z2, pSprite2->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(nAngle)>>16;
+                    aim.dy = Sin(nAngle)>>16;
+                    aim.dz = divscale(tz, nDist, 10);
+                    if (tz > -0x333)
+                        aim.dz = divscale(tz, nDist, 10);
+                    else if (tz < -0x333 && tz > -0xb33)
+                        aim.dz = divscale(tz, nDist, 10)+9460;
+                    else if (tz < -0xb33 && tz > -0x3000)
+                        aim.dz = divscale(tz, nDist, 10)+9460;
+                    else if (tz < -0x3000)
+                        aim.dz = divscale(tz, nDist, 10)-7500;
+                    else
+                        aim.dz = divscale(tz, nDist, 10);
+                }
+                else
+                    aim.dz = divscale(tz, nDist, 10);
+            }
+        }
+    }
+    if (IsPlayerSprite(pTarget) || !VanillaMode()) // By NoOne: allow to fire missile in non-player targets
+    {
+        actFireMissile(pSprite, -120, 0, aim.dx, aim.dy, aim.dz, 311);
+        actFireMissile(pSprite, 120, 0, aim.dx, aim.dy, aim.dz, 311);
+    }
+}
+
+static void ThrowSSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    actFireThing(pSprite, 0, 0, gDudeSlope[nXSprite]-7500, 421, Chance(0x6000) ? 0x133333 : 0x111111);
+}
+
+static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+    if (pDudeExtraE->at8 && pDudeExtraE->at4 < 10)
+        pDudeExtraE->at4++;
+    else if (pDudeExtraE->at4 >= 10 && pDudeExtraE->at8)
+    {
+        pXSprite->goalAng += 256;
+        POINT3D *pTarget = &baseSprite[pSprite->index];
+        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
+        aiNewState(pSprite, pXSprite, &gargoyleTurn);
+        return;
+    }
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                pDudeExtraE->at4 = 0;
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                pDudeExtraE->at4 = 0;
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else
+                continue;
+            break;
+        }
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    sub_5F15C(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &gargoyleFSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void MoveDodgeUp(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -0x1d555;
+}
+
+static void MoveDodgeDown(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (pXSprite->dodgeDir == 0)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = 0x44444;
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &gargoyleFGoto);
+        return;
+    }
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &gargoyleFSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &gargoyleFSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        // Should be dudeInfo[pTarget->type-kDudeBase]
+        int height2 = (pDudeInfo->eyeHeight*pTarget->yrepeat)<<2;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int floorZ = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                switch (pSprite->type)
+                {
+                case 206:
+                    if (nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            sfxPlay3DSound(pSprite, 1408, 0, 0);
+                            aiNewState(pSprite, pXSprite, &gargoyleFThrow);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 207)
+                            {
+                                sfxPlay3DSound(pSprite, 1408, 0, 0);
+                                aiNewState(pSprite, pXSprite, &gargoyleFThrow);
+                            }
+                            break;
+                        default:
+                            sfxPlay3DSound(pSprite, 1408, 0, 0);
+                            aiNewState(pSprite, pXSprite, &gargoyleFThrow);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            sfxPlay3DSound(pSprite, 1406, 0, 0);
+                            aiNewState(pSprite, pXSprite, &gargoyleFSlash);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 207)
+                            {
+                                sfxPlay3DSound(pSprite, 1406, 0, 0);
+                                aiNewState(pSprite, pXSprite, &gargoyleFSlash);
+                            }
+                            break;
+                        default:
+                            sfxPlay3DSound(pSprite, 1406, 0, 0);
+                            aiNewState(pSprite, pXSprite, &gargoyleFSlash);
+                            break;
+                        }
+                    }
+                    else if ((height2-height > 0x2000 || floorZ-bottom > 0x2000) && nDist < 0x1400 && nDist > 0xa00)
+                    {
+                        aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);
+                        aiNewState(pSprite, pXSprite, &gargoyleSwoop);
+                    }
+                    else if ((height2-height < 0x2000 || floorZ-bottom < 0x2000) && klabs(nDeltaAngle) < 85)
+                        aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);
+                    break;
+                case 207:
+                    if (nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            sfxPlay3DSound(pSprite, 1457, 0, 0);
+                            aiNewState(pSprite, pXSprite, &gargoyleSBlast);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 206)
+                            {
+                                sfxPlay3DSound(pSprite, 1457, 0, 0);
+                                aiNewState(pSprite, pXSprite, &gargoyleSBlast);
+                            }
+                            break;
+                        default:
+                            sfxPlay3DSound(pSprite, 1457, 0, 0);
+                            aiNewState(pSprite, pXSprite, &gargoyleSBlast);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            aiNewState(pSprite, pXSprite, &gargoyleFSlash);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 206)
+                                aiNewState(pSprite, pXSprite, &gargoyleFSlash);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &gargoyleFSlash);
+                            break;
+                        }
+                    }
+                    else if ((height2-height > 0x2000 || floorZ-bottom > 0x2000) && nDist < 0x1400 && nDist > 0x800)
+                    {
+                        if (pSprite->type == 206)
+                            aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);
+                        else
+                            aiPlay3DSound(pSprite, 1450, AI_SFX_PRIORITY_1, -1);
+                        aiNewState(pSprite, pXSprite, &gargoyleSwoop);
+                    }
+                    else if ((height2-height < 0x2000 || floorZ-bottom < 0x2000) && klabs(nDeltaAngle) < 85)
+                        aiPlay3DSound(pSprite, 1450, AI_SFX_PRIORITY_1, -1);
+                    break;
+                }
+            }
+            return;
+        }
+        else
+        {
+            aiNewState(pSprite, pXSprite, &gargoyleFly);
+            return;
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &gargoyleFGoto);
+    pXSprite->target = -1;
+}
+
+static void entryFStatue(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    DUDEINFO *pDudeInfo = &dudeInfo[6];
+    actHealDude(pXSprite, pDudeInfo->startHealth, pDudeInfo->startHealth);
+    pSprite->type = 206;
+}
+
+static void entrySStatue(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    DUDEINFO *pDudeInfo = &dudeInfo[7];
+    actHealDude(pXSprite, pDudeInfo->startHealth, pDudeInfo->startHealth);
+    pSprite->type = 207;
+}
+
+static void MoveForward(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if ((unsigned int)Random(64) < 32 && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void MoveSlow(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 = nAccel>>1;
+    t2 >>= 1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    switch (pSprite->type)
+    {
+    case 206:
+        zvel[nSprite] = 0x44444;
+        break;
+    case 207:
+        zvel[nSprite] = 0x35555;
+        break;
+    }
+}
+
+static void MoveSwoop(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    switch (pSprite->type)
+    {
+    case 206:
+        zvel[nSprite] = t1;
+        break;
+    case 207:
+        zvel[nSprite] = t1;
+        break;
+    }
+}
+
+static void MoveFly(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pSprite->ang = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    switch (pSprite->type)
+    {
+    case 206:
+        zvel[nSprite] = -t1;
+        break;
+    case 207:
+        zvel[nSprite] = -t1;
+        break;
+    }
+    klabs(zvel[nSprite]);
+}
diff --git a/source/blood/src/aigarg.h b/source/blood/src/aigarg.h
new file mode 100644
index 000000000..3112cf85e
--- /dev/null
+++ b/source/blood/src/aigarg.h
@@ -0,0 +1,50 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE gargoyleFIdle;
+extern AISTATE gargoyleStatueIdle;
+extern AISTATE gargoyleFChase;
+extern AISTATE gargoyleFGoto;
+extern AISTATE gargoyleFSlash;
+extern AISTATE gargoyleFThrow;
+extern AISTATE gargoyleSThrow;
+extern AISTATE gargoyleSBlast;
+extern AISTATE gargoyleFRecoil;
+extern AISTATE gargoyleFSearch;
+extern AISTATE gargoyleFMorph2;
+extern AISTATE gargoyleFMorph;
+extern AISTATE gargoyleSMorph2;
+extern AISTATE gargoyleSMorph;
+extern AISTATE gargoyleSwoop;
+extern AISTATE gargoyleFly;
+extern AISTATE gargoyleTurn;
+extern AISTATE gargoyleDodgeUp;
+extern AISTATE gargoyleFDodgeUpRight;
+extern AISTATE gargoyleFDodgeUpLeft;
+extern AISTATE gargoyleDodgeDown;
+extern AISTATE gargoyleFDodgeDownRight;
+extern AISTATE gargoyleFDodgeDownLeft;
+extern AISTATE statueFBreakSEQ;
+extern AISTATE statueSBreakSEQ;
diff --git a/source/blood/src/aighost.cpp b/source/blood/src/aighost.cpp
new file mode 100644
index 000000000..502d5768f
--- /dev/null
+++ b/source/blood/src/aighost.cpp
@@ -0,0 +1,587 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aighost.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void SlashSeqCallback(int, int);
+static void ThrowSeqCallback(int, int);
+static void BlastSeqCallback(int, int);
+static void thinkTarget(spritetype *, XSPRITE *);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void MoveDodgeUp(spritetype *, XSPRITE *);
+static void MoveDodgeDown(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void MoveForward(spritetype *, XSPRITE *);
+static void MoveSlow(spritetype *, XSPRITE *);
+static void MoveSwoop(spritetype *, XSPRITE *);
+static void MoveFly(spritetype *, XSPRITE *);
+
+static int nSlashClient = seqRegisterClient(SlashSeqCallback);
+static int nThrowClient = seqRegisterClient(ThrowSeqCallback);
+static int nBlastClient = seqRegisterClient(BlastSeqCallback);
+
+AISTATE ghostIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
+AISTATE ghostChase = { kAiStateChase, 0, -1, 0, NULL, MoveForward, thinkChase, &ghostIdle };
+AISTATE ghostGoto = { kAiStateMove, 0, -1, 600, NULL, MoveForward, thinkGoto, &ghostIdle };
+AISTATE ghostSlash = { kAiStateChase, 6, nSlashClient, 120, NULL, NULL, NULL, &ghostChase };
+AISTATE ghostThrow = { kAiStateChase, 6, nThrowClient, 120, NULL, NULL, NULL, &ghostChase };
+AISTATE ghostBlast = { kAiStateChase, 6, nBlastClient, 120, NULL, MoveSlow, NULL, &ghostChase };
+AISTATE ghostRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &ghostChase };
+AISTATE ghostTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &ghostChase };
+AISTATE ghostSearch = { kAiStateSearch, 0, -1, 120, NULL, MoveForward, thinkSearch, &ghostIdle };
+AISTATE ghostSwoop = { kAiStateOther, 0, -1, 120, NULL, MoveSwoop, thinkChase, &ghostChase };
+AISTATE ghostFly = { kAiStateMove, 0, -1, 0, NULL, MoveFly, thinkChase, &ghostChase };
+AISTATE ghostTurn = { kAiStateMove, 0, -1, 120, NULL, aiMoveTurn, NULL, &ghostChase };
+AISTATE ghostDodgeUp = { kAiStateMove, 0, -1, 60, NULL, MoveDodgeUp, NULL, &ghostChase };
+AISTATE ghostDodgeUpRight = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeUp, NULL, &ghostChase };
+AISTATE ghostDodgeUpLeft = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeUp, NULL, &ghostChase };
+AISTATE ghostDodgeDown = { kAiStateMove, 0, -1, 120, NULL, MoveDodgeDown, NULL, &ghostChase };
+AISTATE ghostDodgeDownRight = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeDown, NULL, &ghostChase };
+AISTATE ghostDodgeDownLeft = { kAiStateMove, 0, -1, 90, NULL, MoveDodgeDown, NULL, &ghostChase };
+
+static void SlashSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type-kDudeBase];
+    int height = (pSprite->yrepeat*pDudeInfo->eyeHeight)<<2;
+    int height2 = (pTarget->yrepeat*pDudeInfoT->eyeHeight)<<2;
+    int dz = height-height2;
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    sfxPlay3DSound(pSprite, 1406, 0, 0);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_12);
+    int r1 = Random(50);
+    int r2 = Random(50);
+    actFireVector(pSprite, 0, 0, dx+r2, dy-r1, dz, VECTOR_TYPE_12);
+    r1 = Random(50);
+    r2 = Random(50);
+    actFireVector(pSprite, 0, 0, dx-r2, dy+r1, dz, VECTOR_TYPE_12);
+}
+
+static void ThrowSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    actFireThing(&sprite[nSprite], 0, 0, gDudeSlope[nXSprite]-7500, 421, 0xeeeee);
+}
+
+static void BlastSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    wrand(); // ???
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int height = (pSprite->yrepeat*dudeInfo[pSprite->type-kDudeBase].eyeHeight) << 2;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nDist) = approxDist(dx, dy);
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = height;
+    TARGETTRACK tt = { 0x10000, 0x10000, 0x100, 0x55, 0x1aaaaa };
+    Aim aim;
+    aim.dx = Cos(pSprite->ang)>>16;
+    aim.dy = Sin(pSprite->ang)>>16;
+    aim.dz = gDudeSlope[nXSprite];
+    int nClosest = 0x7fffffff;
+    for (short nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite == pSprite2 || !(pSprite2->hitag&8))
+            continue;
+        int x2 = pSprite2->x;
+        int y2 = pSprite2->y;
+        int z2 = pSprite2->z;
+        int nDist = approxDist(x2-x, y2-y);
+        if (nDist == 0 || nDist > 0x2800)
+            continue;
+        if (tt.at10)
+        {
+            int t = divscale(nDist, tt.at10, 12);
+            x2 += (xvel[nSprite2]*t)>>12;
+            y2 += (yvel[nSprite2]*t)>>12;
+            z2 += (zvel[nSprite2]*t)>>8;
+        }
+        int tx = x+mulscale30(Cos(pSprite->ang), nDist);
+        int ty = y+mulscale30(Sin(pSprite->ang), nDist);
+        int tz = z+mulscale(gDudeSlope[nXSprite], nDist, 10);
+        int tsr = mulscale(9460, nDist, 10);
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (tz-tsr > bottom || tz+tsr < top)
+            continue;
+        int dx = (tx-x2)>>4;
+        int dy = (ty-y2)>>4;
+        int dz = (tz-z2)>>8;
+        int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+        if (nDist2 < nClosest)
+        {
+            int nAngle = getangle(x2-x, y2-y);
+            int nDeltaAngle = ((nAngle-pSprite->ang+1024)&2047)-1024;
+            if (klabs(nDeltaAngle) <= tt.at8)
+            {
+                int tz = pSprite2->z-pSprite->z;
+                if (cansee(x, y, z, pSprite->sectnum, x2, y2, z2, pSprite2->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(nAngle)>>16;
+                    aim.dy = Sin(nAngle)>>16;
+                    aim.dz = divscale(tz, nDist, 10);
+                    if (tz > -0x333)
+                        aim.dz = divscale(tz, nDist, 10);
+                    else if (tz < -0x333 && tz > -0xb33)
+                        aim.dz = divscale(tz, nDist, 10)+9460;
+                    else if (tz < -0xb33 && tz > -0x3000)
+                        aim.dz = divscale(tz, nDist, 10)+9460;
+                    else if (tz < -0x3000)
+                        aim.dz = divscale(tz, nDist, 10)-7500;
+                    else
+                        aim.dz = divscale(tz, nDist, 10);
+                }
+                else
+                    aim.dz = divscale(tz, nDist, 10);
+            }
+        }
+    }
+    if (IsPlayerSprite(pTarget) || !VanillaMode()) // By NoOne: allow fire missile in non-player targets if not a demo
+    {
+        sfxPlay3DSound(pSprite, 489, 0, 0);
+        actFireMissile(pSprite, 0, 0, aim.dx, aim.dy, aim.dz, 307);
+    }
+}
+
+static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+    if (pDudeExtraE->at8 && pDudeExtraE->at4 < 10)
+        pDudeExtraE->at4++;
+    else if (pDudeExtraE->at4 >= 10 && pDudeExtraE->at8)
+    {
+        pXSprite->goalAng += 256;
+        POINT3D *pTarget = &baseSprite[pSprite->index];
+        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
+        aiNewState(pSprite, pXSprite, &ghostTurn);
+        return;
+    }
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                pDudeExtraE->at4 = 0;
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                pDudeExtraE->at4 = 0;
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+        }
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &ghostSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void MoveDodgeUp(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -0x1d555;
+}
+
+static void MoveDodgeDown(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    if (pXSprite->dodgeDir == 0)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int dx = xvel[nSprite];
+    int dy = yvel[nSprite];
+    int t1 = dmulscale30(dx, nCos, dy, nSin);
+    int t2 = dmulscale30(dx, nSin, -dy, nCos);
+    if (pXSprite->dodgeDir > 0)
+        t2 += pDudeInfo->sideSpeed;
+    else
+        t2 -= pDudeInfo->sideSpeed;
+
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = 0x44444;
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &ghostGoto);
+        return;
+    }
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &ghostSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &ghostSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        // Should be dudeInfo[pTarget->type-kDudeBase]
+        int height2 = (pDudeInfo->eyeHeight*pTarget->yrepeat)<<2;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int floorZ = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                switch (pSprite->type)
+                {
+                case 210:
+                    if (nDist < 0x2000 && nDist > 0x1000 && klabs(nDeltaAngle) < 85)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            aiNewState(pSprite, pXSprite, &ghostBlast);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 210)
+                                aiNewState(pSprite, pXSprite, &ghostBlast);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &ghostBlast);
+                            break;
+                        }
+                    }
+                    else if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    {
+                        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                        switch (hit)
+                        {
+                        case -1:
+                            aiNewState(pSprite, pXSprite, &ghostSlash);
+                            break;
+                        case 0:
+                        case 4:
+                            break;
+                        case 3:
+                            if (pSprite->type != sprite[gHitInfo.hitsprite].type && sprite[gHitInfo.hitsprite].type != 210)
+                                aiNewState(pSprite, pXSprite, &ghostSlash);
+                            break;
+                        default:
+                            aiNewState(pSprite, pXSprite, &ghostSlash);
+                            break;
+                        }
+                    }
+                    else if ((height2-height > 0x2000 || floorZ-bottom > 0x2000) && nDist < 0x1400 && nDist > 0x800)
+                    {
+                        aiPlay3DSound(pSprite, 1600, AI_SFX_PRIORITY_1, -1);
+                        aiNewState(pSprite, pXSprite, &ghostSwoop);
+                    }
+                    else if ((height2-height < 0x2000 || floorZ-bottom < 0x2000) && klabs(nDeltaAngle) < 85)
+                        aiPlay3DSound(pSprite, 1600, AI_SFX_PRIORITY_1, -1);
+                    break;
+                }
+            }
+            return;
+        }
+        else
+        {
+            aiNewState(pSprite, pXSprite, &ghostFly);
+            return;
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &ghostGoto);
+    pXSprite->target = -1;
+}
+
+static void MoveForward(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if ((unsigned int)Random(64) < 32 && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void MoveSlow(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 = nAccel>>1;
+    t2 >>= 1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    switch (pSprite->type)
+    {
+    case 210:
+        zvel[nSprite] = 0x44444;
+        break;
+    }
+}
+
+static void MoveSwoop(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    switch (pSprite->type)
+    {
+    case 210:
+        zvel[nSprite] = t1;
+        break;
+    }
+}
+
+static void MoveFly(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = pDudeInfo->frontSpeed<<2;
+    if (klabs(nAng) > 341)
+    {
+        pSprite->ang = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    switch (pSprite->type)
+    {
+    case 210:
+        zvel[nSprite] = -t1;
+        break;
+    }
+}
diff --git a/source/blood/src/aighost.h b/source/blood/src/aighost.h
new file mode 100644
index 000000000..b45504f9b
--- /dev/null
+++ b/source/blood/src/aighost.h
@@ -0,0 +1,42 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+extern AISTATE ghostIdle;
+extern AISTATE ghostChase;
+extern AISTATE ghostGoto;
+extern AISTATE ghostSlash;
+extern AISTATE ghostThrow;
+extern AISTATE ghostBlast;
+extern AISTATE ghostRecoil;
+extern AISTATE ghostTeslaRecoil;
+extern AISTATE ghostSearch;
+extern AISTATE ghostSwoop;
+extern AISTATE ghostFly;
+extern AISTATE ghostTurn;
+extern AISTATE ghostDodgeUp;
+extern AISTATE ghostDodgeUpRight;
+extern AISTATE ghostDodgeUpLeft;
+extern AISTATE ghostDodgeDown;
+extern AISTATE ghostDodgeDownRight;
+extern AISTATE ghostDodgeDownLeft;
diff --git a/source/blood/src/aigilbst.cpp b/source/blood/src/aigilbst.cpp
new file mode 100644
index 000000000..ea31fccad
--- /dev/null
+++ b/source/blood/src/aigilbst.cpp
@@ -0,0 +1,415 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aigilbst.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void GillBiteSeqCallback(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void thinkSwimGoto(spritetype *, XSPRITE *);
+static void thinkSwimChase(spritetype *, XSPRITE *);
+static void sub_6CB00(spritetype *, XSPRITE *);
+static void sub_6CD74(spritetype *, XSPRITE *);
+static void sub_6D03C(spritetype *, XSPRITE *);
+
+static int nGillBiteClient = seqRegisterClient(GillBiteSeqCallback);
+
+AISTATE gillBeastIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE gillBeastChase = { kAiStateChase, 9, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE gillBeastDodge = { kAiStateMove, 9, -1, 90, NULL, aiMoveDodge, NULL, &gillBeastChase };
+AISTATE gillBeastGoto = { kAiStateMove, 9, -1, 600, NULL, aiMoveForward, thinkGoto, &gillBeastIdle };
+AISTATE gillBeastBite = { kAiStateChase, 6, nGillBiteClient, 120, NULL, NULL, NULL, &gillBeastChase };
+AISTATE gillBeastSearch = { kAiStateMove, 9, -1, 120, NULL, aiMoveForward, thinkSearch, &gillBeastIdle };
+AISTATE gillBeastRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &gillBeastDodge };
+AISTATE gillBeastSwimIdle = { kAiStateIdle, 10, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE gillBeastSwimChase = { kAiStateChase, 10, -1, 0, NULL, sub_6CB00, thinkSwimChase, NULL };
+AISTATE gillBeastSwimDodge = { kAiStateMove, 10, -1, 90, NULL, aiMoveDodge, NULL, &gillBeastSwimChase };
+AISTATE gillBeastSwimGoto = { kAiStateMove, 10, -1, 600, NULL, aiMoveForward, thinkSwimGoto, &gillBeastSwimIdle };
+AISTATE gillBeastSwimSearch = { kAiStateSearch, 10, -1, 120, NULL, aiMoveForward, thinkSearch, &gillBeastSwimIdle };
+AISTATE gillBeastSwimBite = { kAiStateChase, 7, nGillBiteClient, 0, NULL, NULL, thinkSwimChase, &gillBeastSwimChase };
+AISTATE gillBeastSwimRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &gillBeastSwimDodge };
+AISTATE gillBeast13A138 = { kAiStateOther, 10, -1, 120, NULL, sub_6CD74, thinkSwimChase, &gillBeastSwimChase };
+AISTATE gillBeast13A154 = { kAiStateOther, 10, -1, 0, NULL, sub_6D03C, thinkSwimChase, &gillBeastSwimChase };
+AISTATE gillBeast13A170 = { kAiStateOther, 10, -1, 120, NULL, NULL, aiMoveTurn, &gillBeastSwimChase };
+
+static void GillBiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    int dz = pSprite->z-pTarget->z;
+    dx += Random3(2000);
+    dy += Random3(2000);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_8);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_8);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_8);
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    XSECTOR *pXSector;
+    int nXSector = sector[pSprite->sectnum].extra;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    else
+        pXSector = NULL;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &gillBeastSearch);
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &gillBeastSearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &gillBeastSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        XSECTOR *pXSector;
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0)
+            pXSector = &xsector[nXSector];
+        else
+            pXSector = NULL;
+        if (pXSector && pXSector->Underwater)
+            aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        else
+            aiNewState(pSprite, pXSprite, &gillBeastSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int nXSprite = sprite[pXSprite->reference].extra;
+                gDudeSlope[nXSprite] = divscale(pTarget->z-pSprite->z, nDist, 10);
+                if (nDist < 921 && klabs(nDeltaAngle) < 28)
+                {
+                    XSECTOR *pXSector;
+                    int nXSector = sector[pSprite->sectnum].extra;
+                    if (nXSector > 0)
+                        pXSector = &xsector[nXSector];
+                    else
+                        pXSector = NULL;
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (hit)
+                    {
+                    case -1:
+                        if (pXSector && pXSector->Underwater)
+                            aiNewState(pSprite, pXSprite, &gillBeastSwimBite);
+                        else
+                            aiNewState(pSprite, pXSprite, &gillBeastBite);
+                        break;
+                    case 3:
+                        if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                        {
+                            if (pXSector && pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &gillBeastSwimBite);
+                            else
+                                aiNewState(pSprite, pXSprite, &gillBeastBite);
+                        }
+                        else
+                        {
+                            if (pXSector && pXSector->Underwater)
+                                aiNewState(pSprite, pXSprite, &gillBeastSwimDodge);
+                            else
+                                aiNewState(pSprite, pXSprite, &gillBeastDodge);
+                        }
+                        break;
+                    default:
+                        if (pXSector && pXSector->Underwater)
+                            aiNewState(pSprite, pXSprite, &gillBeastSwimBite);
+                        else
+                            aiNewState(pSprite, pXSprite, &gillBeastBite);
+                        break;
+                    }
+                }
+            }
+            return;
+        }
+    }
+
+    XSECTOR *pXSector;
+    int nXSector = sector[pSprite->sectnum].extra;
+    if (nXSector > 0)
+        pXSector = &xsector[nXSector];
+    else
+        pXSector = NULL;
+    if (pXSector && pXSector->Underwater)
+        aiNewState(pSprite, pXSprite, &gillBeastSwimGoto);
+    else
+        aiNewState(pSprite, pXSprite, &gillBeastGoto);
+    sfxPlay3DSound(pSprite, 1701, -1, 0);
+    pXSprite->target = -1;
+}
+
+static void thinkSwimGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkSwimChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = pDudeInfo->eyeHeight+pSprite->z;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                int UNUSED(floorZ) = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &gillBeastSwimBite);
+                else
+                {
+                    aiPlay3DSound(pSprite, 1700, AI_SFX_PRIORITY_1, -1);
+                    aiNewState(pSprite, pXSprite, &gillBeast13A154);
+                }
+            }
+        }
+        else
+            aiNewState(pSprite, pXSprite, &gillBeast13A154);
+        return;
+    }
+    aiNewState(pSprite, pXSprite, &gillBeastSwimGoto);
+    pXSprite->target = -1;
+}
+
+static void sub_6CB00(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = (pDudeInfo->frontSpeed-(((4-gGameOptions.nDifficulty)<<27)/120)/120)<<2;
+    if (klabs(nAng) > 341)
+        return;
+    if (pXSprite->target == -1)
+        pSprite->ang = (pSprite->ang+256)&2047;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Random(64) < 32 && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    if (pXSprite->target == -1)
+        t1 += nAccel;
+    else
+        t1 += nAccel>>2;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+}
+
+static void sub_6CD74(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int z = pSprite->z + dudeInfo[pSprite->type - kDudeBase].eyeHeight;
+    int z2 = pTarget->z + dudeInfo[pTarget->type - kDudeBase].eyeHeight;
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = (pDudeInfo->frontSpeed-(((4-gGameOptions.nDifficulty)<<27)/120)/120)<<2;
+    if (klabs(nAng) > 341)
+    {
+        pXSprite->goalAng = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int dz = z2 - z;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x600) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = -dz;
+}
+
+static void sub_6D03C(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    int nSprite = pSprite->index;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    int z = pSprite->z + dudeInfo[pSprite->type - kDudeBase].eyeHeight;
+    int z2 = pTarget->z + dudeInfo[pTarget->type - kDudeBase].eyeHeight;
+    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
+    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
+    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
+    int nAccel = (pDudeInfo->frontSpeed-(((4-gGameOptions.nDifficulty)<<27)/120)/120)<<2;
+    if (klabs(nAng) > 341)
+    {
+        pSprite->ang = (pSprite->ang+512)&2047;
+        return;
+    }
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int dz = (z2 - z)<<3;
+    int UNUSED(nAngle) = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    if (Chance(0x4000) && nDist <= 0x400)
+        return;
+    int nCos = Cos(pSprite->ang);
+    int nSin = Sin(pSprite->ang);
+    int vx = xvel[nSprite];
+    int vy = yvel[nSprite];
+    int t1 = dmulscale30(vx, nCos, vy, nSin);
+    int t2 = dmulscale30(vx, nSin, -vy, nCos);
+    t1 += nAccel>>1;
+    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
+    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
+    zvel[nSprite] = dz;
+}
diff --git a/source/blood/src/aigilbst.h b/source/blood/src/aigilbst.h
new file mode 100644
index 000000000..e20610e04
--- /dev/null
+++ b/source/blood/src/aigilbst.h
@@ -0,0 +1,43 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE gillBeastIdle;
+extern AISTATE gillBeastChase;
+extern AISTATE gillBeastDodge;
+extern AISTATE gillBeastGoto;
+extern AISTATE gillBeastBite;
+extern AISTATE gillBeastSearch;
+extern AISTATE gillBeastRecoil;
+extern AISTATE gillBeastSwimIdle;
+extern AISTATE gillBeastSwimChase;
+extern AISTATE gillBeastSwimDodge;
+extern AISTATE gillBeastSwimGoto;
+extern AISTATE gillBeastSwimSearch;
+extern AISTATE gillBeastSwimBite;
+extern AISTATE gillBeastSwimRecoil;
+extern AISTATE gillBeast13A138;
+extern AISTATE gillBeast13A154;
+extern AISTATE gillBeast13A170;
+
diff --git a/source/blood/src/aihand.cpp b/source/blood/src/aihand.cpp
new file mode 100644
index 000000000..f611f12c5
--- /dev/null
+++ b/source/blood/src/aihand.cpp
@@ -0,0 +1,138 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aihand.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void HandJumpSeqCallback(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+
+static int nJumpClient = seqRegisterClient(HandJumpSeqCallback);
+
+AISTATE handIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE hand13A3B4 = { kAiStateOther, 0, -1, 0, NULL, NULL, NULL, NULL };
+AISTATE handSearch = { kAiStateMove, 6, -1, 600, NULL, aiMoveForward, thinkSearch, &handIdle };
+AISTATE handChase = { kAiStateChase, 6, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE handRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &handSearch };
+AISTATE handGoto = { kAiStateMove, 6, -1, 1800, NULL, aiMoveForward, thinkGoto, &handIdle };
+AISTATE handJump = { kAiStateChase, 7, nJumpClient, 120, NULL, NULL, NULL, &handChase };
+
+static void HandJumpSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    if (IsPlayerSprite(pTarget))
+    {
+        PLAYER *pPlayer = &gPlayer[pTarget->type-kDudePlayer1];
+        if (!pPlayer->at376)
+        {
+            pPlayer->at376 = 1;
+            actPostSprite(pSprite->index, kStatFree);
+        }
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &handSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &handGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &handSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &handSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x233 && klabs(nDeltaAngle) < 85 && gGameOptions.nGameType == 0)
+                    aiNewState(pSprite, pXSprite, &handJump);
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &handGoto);
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aihand.h b/source/blood/src/aihand.h
new file mode 100644
index 000000000..8e4b03ea7
--- /dev/null
+++ b/source/blood/src/aihand.h
@@ -0,0 +1,32 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE handIdle;
+extern AISTATE hand13A3B4;
+extern AISTATE handSearch;
+extern AISTATE handChase;
+extern AISTATE handRecoil;
+extern AISTATE handGoto;
+extern AISTATE handJump;
\ No newline at end of file
diff --git a/source/blood/src/aihound.cpp b/source/blood/src/aihound.cpp
new file mode 100644
index 000000000..ab60ae2e8
--- /dev/null
+++ b/source/blood/src/aihound.cpp
@@ -0,0 +1,160 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aihound.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void BiteSeqCallback(int, int);
+static void BurnSeqCallback(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+
+static int nBiteClient = seqRegisterClient(BiteSeqCallback);
+static int nBurnClient = seqRegisterClient(BurnSeqCallback);
+
+AISTATE houndIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE houndSearch = { kAiStateMove, 8, -1, 1800, NULL, aiMoveForward, thinkSearch, &houndIdle };
+AISTATE houndChase = { kAiStateChase, 8, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE houndRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &houndSearch };
+AISTATE houndTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &houndSearch };
+AISTATE houndGoto = { kAiStateMove, 8, -1, 600, NULL, aiMoveForward, thinkGoto, &houndIdle };
+AISTATE houndBite = { kAiStateChase, 6, nBiteClient, 60, NULL, NULL, NULL, &houndChase };
+AISTATE houndBurn = { kAiStateChase, 7, nBurnClient, 60, NULL, NULL, NULL, &houndChase };
+
+static void BiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax)) 
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    if (IsPlayerSprite(pTarget) || !VanillaMode()) // allow to hit non-player targets if not a demo
+        actFireVector(pSprite, 0, 0, dx, dy, pTarget->z-pSprite->z, VECTOR_TYPE_15);
+}
+
+static void BurnSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    actFireMissile(pSprite, 0, 0, Cos(pSprite->ang)>>16, Sin(pSprite->ang)>>16, 0, 308);
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &houndSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &houndGoto);
+        return;
+    }
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &houndSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &houndSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0xb00 && nDist > 0x500 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &houndBurn);
+                else if(nDist < 0x266 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &houndBite);
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &houndGoto);
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aihound.h b/source/blood/src/aihound.h
new file mode 100644
index 000000000..156a82cd0
--- /dev/null
+++ b/source/blood/src/aihound.h
@@ -0,0 +1,33 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE houndIdle;
+extern AISTATE houndSearch;
+extern AISTATE houndChase;
+extern AISTATE houndRecoil;
+extern AISTATE houndTeslaRecoil;
+extern AISTATE houndGoto;
+extern AISTATE houndBite;
+extern AISTATE houndBurn;
diff --git a/source/blood/src/aiinnoc.cpp b/source/blood/src/aiinnoc.cpp
new file mode 100644
index 000000000..360551b97
--- /dev/null
+++ b/source/blood/src/aiinnoc.cpp
@@ -0,0 +1,119 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aiinnoc.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+
+AISTATE innocentIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE innocentSearch = { kAiStateSearch, 6, -1, 1800, NULL, aiMoveForward, thinkSearch, &innocentIdle };
+AISTATE innocentChase = { kAiStateChase, 6, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE innocentRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &innocentChase };
+AISTATE innocentTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &innocentChase };
+AISTATE innocentGoto = { kAiStateMove, 6, -1, 600, NULL, aiMoveForward, thinkGoto, &innocentIdle };
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &innocentSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &innocentGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &innocentSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget))
+    {
+        aiNewState(pSprite, pXSprite, &innocentSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x666 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &innocentIdle);
+                return;
+            }
+        }
+    }
+
+    aiPlay3DSound(pSprite, 7000+Random(6), AI_SFX_PRIORITY_1, -1);
+    aiNewState(pSprite, pXSprite, &innocentGoto);
+    pXSprite->target = -1;
+}
+
diff --git a/source/blood/src/aiinnoc.h b/source/blood/src/aiinnoc.h
new file mode 100644
index 000000000..e121b220c
--- /dev/null
+++ b/source/blood/src/aiinnoc.h
@@ -0,0 +1,31 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE innocentIdle;
+extern AISTATE innocentSearch;
+extern AISTATE innocentChase;
+extern AISTATE innocentRecoil;
+extern AISTATE innocentTeslaRecoil;
+extern AISTATE innocentGoto;
\ No newline at end of file
diff --git a/source/blood/src/aipod.cpp b/source/blood/src/aipod.cpp
new file mode 100644
index 000000000..ac69d947d
--- /dev/null
+++ b/source/blood/src/aipod.cpp
@@ -0,0 +1,282 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aipod.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void sub_6FF08(int, int);
+static void sub_6FF54(int, int);
+static void sub_6FFA0(int, int);
+static void sub_70284(int, int);
+static void sub_7034C(spritetype *, XSPRITE *);
+static void sub_70380(spritetype *, XSPRITE *);
+static void sub_704D8(spritetype *, XSPRITE *);
+
+static int dword_279B34 = seqRegisterClient(sub_6FFA0);
+static int dword_279B38 = seqRegisterClient(sub_70284);
+static int dword_279B3C = seqRegisterClient(sub_6FF08);
+static int dword_279B40 = seqRegisterClient(sub_6FF54);
+
+AISTATE podIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE pod13A600 = { kAiStateMove, 7, -1, 3600, NULL, aiMoveTurn, sub_70380, &podSearch };
+AISTATE podSearch = { kAiStateSearch, 0, -1, 3600, NULL, aiMoveTurn, sub_7034C, &podSearch };
+AISTATE pod13A638 = { kAiStateChase, 8, dword_279B34, 600, NULL, NULL, NULL, &podChase };
+AISTATE podRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &podChase };
+AISTATE podChase = { kAiStateChase, 6, -1, 0, NULL, aiMoveTurn, sub_704D8, NULL };
+AISTATE tentacleIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE tentacle13A6A8 = { kAiStateOther, 7, dword_279B3C, 0, NULL, NULL, NULL, &tentacle13A6C4 };
+AISTATE tentacle13A6C4 = { kAiStateOther, -1, -1, 0, NULL, NULL, NULL, &tentacleChase };
+AISTATE tentacle13A6E0 = { kAiStateOther, 8, dword_279B40, 0, NULL, NULL, NULL, &tentacle13A6FC };
+AISTATE tentacle13A6FC = { kAiStateOther, -1, -1, 0, NULL, NULL, NULL, &tentacleIdle };
+AISTATE tentacle13A718 = { kAiStateOther, 8, -1, 3600, NULL, aiMoveTurn, sub_70380, &tentacleSearch };
+AISTATE tentacleSearch = { kAiStateOther, 0, -1, 3600, NULL, aiMoveTurn, sub_7034C, NULL };
+AISTATE tentacle13A750 = { kAiStateOther, 6, dword_279B38, 120, NULL, NULL, NULL, &tentacleChase };
+AISTATE tentacleRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &tentacleChase };
+AISTATE tentacleChase = { kAiStateChase, 6, -1, 0, NULL, aiMoveTurn, sub_704D8, NULL };
+
+static void sub_6FF08(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    sfxPlay3DSound(&sprite[nSprite], 2503, -1, 0);
+}
+
+static void sub_6FF54(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    sfxPlay3DSound(&sprite[nSprite], 2500, -1, 0);
+}
+
+static void sub_6FFA0(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int x = pTarget->x-pSprite->x;
+    int y = pTarget->y-pSprite->y;
+    int dz = pTarget->z-pSprite->z;
+    x += Random2(1000);
+    y += Random2(1000);
+    int nDist = approxDist(x, y);
+    int nDist2 = nDist / 540;
+    spritetype *pMissile = NULL;
+    switch (pSprite->type)
+    {
+    case 221:
+        dz += 8000;
+        if (pDudeInfo->seeDist*0.1 < nDist)
+        {
+            if (Chance(0x8000))
+                sfxPlay3DSound(pSprite, 2474, -1, 0);
+            else
+                sfxPlay3DSound(pSprite, 2475, -1, 0);
+            pMissile = actFireThing(pSprite, 0, -8000, dz/128-14500, 430, (nDist2<<23)/120);
+        }
+        if (pMissile)
+            seqSpawn(68, 3, pMissile->extra, -1);
+        break;
+    case 223:
+        dz += 8000;
+        if (pDudeInfo->seeDist*0.1 < nDist)
+        {
+            sfxPlay3DSound(pSprite, 2454, -1, 0);
+            pMissile = actFireThing(pSprite, 0, -8000, dz/128-14500, 429, (nDist2<<23)/120);
+        }
+        if (pMissile)
+            seqSpawn(22, 3, pMissile->extra, -1);
+        break;
+    }
+    for (int i = 0; i < 4; i++)
+        sub_746D4(pSprite, 240);
+}
+
+static void sub_70284(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    sfxPlay3DSound(pSprite, 2502, -1, 0);
+    int nDist, nBurn;
+    DAMAGE_TYPE dmgType;
+    switch (pSprite->type)
+    {
+    case 222:
+    default:
+        nBurn = 0;
+        dmgType = DAMAGE_TYPE_2;
+        nDist = 50;
+        break;
+    case 224:
+        nBurn = (gGameOptions.nDifficulty*120)>>2;
+        dmgType = DAMAGE_TYPE_3;
+        nDist = 75;
+        break;
+    }
+    sub_2A620(nSprite, pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, nDist, 1, 5*(1+gGameOptions.nDifficulty), dmgType, 2, nBurn, 0, 0);
+}
+
+static void sub_7034C(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void sub_70380(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+    {
+        switch (pSprite->type)
+        {
+        case 221:
+        case 223:
+            aiNewState(pSprite, pXSprite, &podSearch);
+            break;
+        case 222:
+        case 224:
+            aiNewState(pSprite, pXSprite, &tentacleSearch);
+            break;
+        }
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void sub_704D8(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        switch (pSprite->type)
+        {
+        case 221:
+        case 223:
+            aiNewState(pSprite, pXSprite, &pod13A600);
+            break;
+        case 222:
+        case 224:
+            aiNewState(pSprite, pXSprite, &tentacle13A718);
+            break;
+        }
+        return;
+    }
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        switch (pSprite->type)
+        {
+        case 221:
+        case 223:
+            aiNewState(pSprite, pXSprite, &podSearch);
+            break;
+        case 222:
+        case 224:
+            aiNewState(pSprite, pXSprite, &tentacleSearch);
+            break;
+        }
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (klabs(nDeltaAngle) < 85 && pTarget->type != 221 && pTarget->type != 223)
+                {
+                    switch (pSprite->type)
+                    {
+                    case 221:
+                    case 223:
+                        aiNewState(pSprite, pXSprite, &pod13A638);
+                        break;
+                    case 222:
+                    case 224:
+                        aiNewState(pSprite, pXSprite, &tentacle13A750);
+                        break;
+                    }
+                }
+                return;
+            }
+        }
+    }
+    
+    switch (pSprite->type)
+    {
+    case 221:
+    case 223:
+        aiNewState(pSprite, pXSprite, &pod13A600);
+        break;
+    case 222:
+    case 224:
+        aiNewState(pSprite, pXSprite, &tentacle13A718);
+        break;
+    }
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aipod.h b/source/blood/src/aipod.h
new file mode 100644
index 000000000..5ef41fa66
--- /dev/null
+++ b/source/blood/src/aipod.h
@@ -0,0 +1,41 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE podIdle;
+extern AISTATE pod13A600;
+extern AISTATE podSearch;
+extern AISTATE pod13A638;
+extern AISTATE podRecoil;
+extern AISTATE podChase;
+extern AISTATE tentacleIdle;
+extern AISTATE tentacle13A6A8;
+extern AISTATE tentacle13A6C4;
+extern AISTATE tentacle13A6E0;
+extern AISTATE tentacle13A6FC;
+extern AISTATE tentacle13A718;
+extern AISTATE tentacleSearch;
+extern AISTATE tentacle13A750;
+extern AISTATE tentacleRecoil;
+extern AISTATE tentacleChase;
diff --git a/source/blood/src/airat.cpp b/source/blood/src/airat.cpp
new file mode 100644
index 000000000..ac3884044
--- /dev/null
+++ b/source/blood/src/airat.cpp
@@ -0,0 +1,135 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "airat.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void BiteSeqCallback(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+
+static int nBiteClient = seqRegisterClient(BiteSeqCallback);
+
+AISTATE ratIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE ratSearch = { kAiStateSearch, 7, -1, 1800, NULL, aiMoveForward, thinkSearch, &ratIdle };
+AISTATE ratChase = { kAiStateChase, 7, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE ratDodge = { kAiStateMove, 7, -1, 0, NULL, NULL, NULL, &ratChase };
+AISTATE ratRecoil = { kAiStateRecoil, 7, -1, 0, NULL, NULL, NULL, &ratDodge };
+AISTATE ratGoto = { kAiStateMove, 7, -1, 600, NULL, aiMoveForward, thinkGoto, &ratIdle };
+AISTATE ratBite = { kAiStateChase, 6, nBiteClient, 120, NULL, NULL, NULL, &ratChase };
+
+static void BiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    if (IsPlayerSprite(pTarget))
+        actFireVector(pSprite, 0, 0, dx, dy, pTarget->z-pSprite->z, VECTOR_TYPE_16);
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &ratSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &ratGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &ratSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &ratSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x399 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &ratBite);
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &ratGoto);
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/airat.h b/source/blood/src/airat.h
new file mode 100644
index 000000000..6c469c7a2
--- /dev/null
+++ b/source/blood/src/airat.h
@@ -0,0 +1,32 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE ratIdle;
+extern AISTATE ratSearch;
+extern AISTATE ratChase;
+extern AISTATE ratDodge;
+extern AISTATE ratRecoil;
+extern AISTATE ratGoto;
+extern AISTATE ratBit;
\ No newline at end of file
diff --git a/source/blood/src/aispid.cpp b/source/blood/src/aispid.cpp
new file mode 100644
index 000000000..5b619b213
--- /dev/null
+++ b/source/blood/src/aispid.cpp
@@ -0,0 +1,290 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aispid.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "endgame.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void SpidBiteSeqCallback(int, int);
+static void SpidJumpSeqCallback(int, int);
+static void sub_71370(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+
+static int nBiteClient = seqRegisterClient(SpidBiteSeqCallback);
+static int nJumpClient = seqRegisterClient(SpidJumpSeqCallback);
+static int dword_279B50 = seqRegisterClient(sub_71370);
+
+AISTATE spidIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE spidChase = { kAiStateChase, 7, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE spidDodge = { kAiStateMove, 7, -1, 90, NULL, aiMoveDodge, NULL, &spidChase };
+AISTATE spidGoto = { kAiStateMove, 7, -1, 600, NULL, aiMoveForward, thinkGoto, &spidIdle };
+AISTATE spidSearch = { kAiStateSearch, 7, -1, 1800, NULL, aiMoveForward, thinkSearch, &spidIdle };
+AISTATE spidBite = { kAiStateChase, 6, nBiteClient, 60, NULL, NULL, NULL, &spidChase };
+AISTATE spidJump = { kAiStateChase, 8, nJumpClient, 60, NULL, aiMoveForward, NULL, &spidChase };
+AISTATE spid13A92C = { kAiStateOther, 0, dword_279B50, 60, NULL, NULL, NULL, &spidIdle };
+
+static char sub_70D30(XSPRITE *pXDude, int a2, int a3)
+{
+    dassert(pXDude != NULL);
+    int nDude = pXDude->reference;
+    spritetype *pDude = &sprite[nDude];
+    if (IsPlayerSprite(pDude))
+    {
+        a2 <<= 4;
+        a3 <<= 4;
+        if (IsPlayerSprite(pDude))
+        {
+            PLAYER *pPlayer = &gPlayer[pDude->type-kDudePlayer1];
+            if (a3 > pPlayer->at36a)
+            {
+                pPlayer->at36a = ClipHigh(pPlayer->at36a+a2, a3);
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+static void SpidBiteSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    dx += Random2(2000);
+    dy += Random2(2000);
+    int dz = Random2(2000);
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    if (IsPlayerSprite(pTarget))
+    {
+        int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+        if (hit == 3)
+        {
+            if (sprite[gHitInfo.hitsprite].type <= kDudePlayer8 && sprite[gHitInfo.hitsprite].type >= kDudePlayer1)
+            {
+                dz += pTarget->z-pSprite->z;
+                if (pTarget->type >= kDudePlayer1 && pTarget->type <= kDudePlayer8)
+                {
+                    PLAYER *pPlayer = &gPlayer[pTarget->type-kDudePlayer1];
+                    switch (pSprite->type)
+                    {
+                    case 213:
+                        actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_17);
+                        if (IsPlayerSprite(pTarget) && !pPlayer->at31a && powerupCheck(pPlayer, 14) <= 0
+                            && Chance(0x4000))
+                            powerupActivate(pPlayer, 28);
+                        break;
+                    case 214:
+                        actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_17);
+                        if (Chance(0x5000))
+                            sub_70D30(pXTarget, 4, 16);
+                        break;
+                    case 215:
+                        actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_17);
+                        sub_70D30(pXTarget, 8, 16);
+                        break;
+                    case 216:
+                    {
+                        actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_17);
+                        dx += Random2(2000);
+                        dy += Random2(2000);
+                        dz += Random2(2000);
+                        actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_17);
+                        sub_70D30(pXTarget, 8, 16);
+                        break;
+                    }
+                    }
+                }
+            }
+        }
+    }
+}
+
+static void SpidJumpSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    int dx = Cos(pSprite->ang)>>16;
+    int dy = Sin(pSprite->ang)>>16;
+    dx += Random2(200);
+    dy += Random2(200);
+    int dz = Random2(200);
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    if (IsPlayerSprite(pTarget))
+    {
+        dz += pTarget->z-pSprite->z;
+        if (pTarget->type >= kDudePlayer1 && pTarget->type <= kDudePlayer8)
+        {
+            switch (pSprite->type)
+            {
+            case 213:
+            case 214:
+            case 215:
+                xvel[nSprite] = dx << 16;
+                yvel[nSprite] = dy << 16;
+                zvel[nSprite] = dz << 16;
+                break;
+            }
+        }
+    }
+}
+
+static void sub_71370(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    DUDEEXTRA_at6_u1 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u1;
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    spritetype *pSpawn = NULL;
+    if (IsPlayerSprite(pTarget) && pDudeExtraE->at4 < 10)
+    {
+        if (nDist < 0x1a00 && nDist > 0x1400 && klabs(pSprite->ang-nAngle) < pDudeInfo->periphery)
+            pSpawn = actSpawnDude(pSprite, 214, pSprite->clipdist, 0);
+        else if (nDist < 0x1400 && nDist > 0xc00 && klabs(pSprite->ang-nAngle) < pDudeInfo->periphery)
+            pSpawn = actSpawnDude(pSprite, 213, pSprite->clipdist, 0);
+        else if (nDist < 0xc00 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+            pSpawn = actSpawnDude(pSprite, 213, pSprite->clipdist, 0);
+        if (pSpawn)
+        {
+            pDudeExtraE->at4++;
+            pSpawn->owner = nSprite;
+            gKillMgr.sub_263E0(1);
+        }
+    }
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &spidSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &spidGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &spidSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &spidSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                switch (pSprite->type)
+                {
+                case 214:
+                    if (nDist < 0x399 && klabs(nDeltaAngle) < 85)
+                        aiNewState(pSprite, pXSprite, &spidBite);
+                    break;
+                case 213:
+                case 215:
+                    if (nDist < 0x733 && nDist > 0x399 && klabs(nDeltaAngle) < 85)
+                        aiNewState(pSprite, pXSprite, &spidJump);
+                    else if (nDist < 0x399 && klabs(nDeltaAngle) < 85)
+                        aiNewState(pSprite, pXSprite, &spidBite);
+                    break;
+                case 216:
+                    if (nDist < 0x733 && nDist > 0x399 && klabs(nDeltaAngle) < 85)
+                        aiNewState(pSprite, pXSprite, &spidJump);
+                    else if (Chance(0x8000))
+                        aiNewState(pSprite, pXSprite, &spid13A92C);
+                    break;
+                }
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &spidGoto);
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aispid.h b/source/blood/src/aispid.h
new file mode 100644
index 000000000..152d15199
--- /dev/null
+++ b/source/blood/src/aispid.h
@@ -0,0 +1,33 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE spidIdle;
+extern AISTATE spidChase;
+extern AISTATE spidDodge;
+extern AISTATE spidGoto;
+extern AISTATE spidSearch;
+extern AISTATE spidBite;
+extern AISTATE spidJump;
+extern AISTATE spid13A92C;
diff --git a/source/blood/src/aitchern.cpp b/source/blood/src/aitchern.cpp
new file mode 100644
index 000000000..715e40a0b
--- /dev/null
+++ b/source/blood/src/aitchern.cpp
@@ -0,0 +1,357 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aitchern.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void sub_71A90(int, int);
+static void sub_71BD4(int, int);
+static void sub_720AC(int, int);
+static void sub_72580(spritetype *, XSPRITE *);
+static void sub_725A4(spritetype *, XSPRITE *);
+static void sub_72850(spritetype *, XSPRITE *);
+static void sub_72934(spritetype *, XSPRITE *);
+
+static int dword_279B54 = seqRegisterClient(sub_71BD4);
+static int dword_279B58 = seqRegisterClient(sub_720AC);
+static int dword_279B5C = seqRegisterClient(sub_71A90);
+
+AISTATE tchernobogIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, sub_725A4, NULL };
+AISTATE tchernobogSearch = { kAiStateSearch, 8, -1, 1800, NULL, aiMoveForward, sub_72580, &tchernobogIdle };
+AISTATE tchernobogChase = { kAiStateChase, 8, -1, 0, NULL, aiMoveForward, sub_72934, NULL };
+AISTATE tchernobogRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &tchernobogSearch };
+AISTATE tcherno13A9B8 = { kAiStateMove, 8, -1, 600, NULL, aiMoveForward, sub_72850, &tchernobogIdle };
+AISTATE tcherno13A9D4 = { kAiStateMove, 6, dword_279B54, 60, NULL, NULL, NULL, &tchernobogChase };
+AISTATE tcherno13A9F0 = { kAiStateChase, 6, dword_279B58, 60, NULL, NULL, NULL, &tchernobogChase };
+AISTATE tcherno13AA0C = { kAiStateChase, 7, dword_279B5C, 60, NULL, NULL, NULL, &tchernobogChase };
+AISTATE tcherno13AA28 = { kAiStateChase, 8, -1, 60, NULL, aiMoveTurn, NULL, &tchernobogChase };
+
+static void sub_71A90(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int nTarget = pTarget->index;
+    int nOwner = actSpriteIdToOwnerId(nSprite);
+    if (pXTarget->burnTime == 0)
+        evPost(nTarget, 3, 0, CALLBACK_ID_0);
+    actBurnSprite(nOwner, pXTarget, 40);
+    if (Chance(0x6000))
+        aiNewState(pSprite, pXSprite, &tcherno13A9D4);
+}
+
+static void sub_71BD4(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int height = pSprite->yrepeat*pDudeInfo->eyeHeight;
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = height;
+    TARGETTRACK tt = { 0x10000, 0x10000, 0x100, 0x55, 0x100000 };
+    Aim aim;
+    aim.dx = Cos(pSprite->ang)>>16;
+    aim.dy = Sin(pSprite->ang)>>16;
+    aim.dz = gDudeSlope[nXSprite];
+    int nClosest = 0x7fffffff;
+    for (short nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite == pSprite2 || !(pSprite2->hitag&8))
+            continue;
+        int x2 = pSprite2->x;
+        int y2 = pSprite2->y;
+        int z2 = pSprite2->z;
+        int nDist = approxDist(x2-x, y2-y);
+        if (nDist == 0 || nDist > 0x2800)
+            continue;
+        if (tt.at10)
+        {
+            int t = divscale(nDist, tt.at10, 12);
+            x2 += (xvel[nSprite2]*t)>>12;
+            y2 += (yvel[nSprite2]*t)>>12;
+            z2 += (zvel[nSprite2]*t)>>8;
+        }
+        int tx = x+mulscale30(Cos(pSprite->ang), nDist);
+        int ty = y+mulscale30(Sin(pSprite->ang), nDist);
+        int tz = z+mulscale(gDudeSlope[nXSprite], nDist, 10);
+        int tsr = mulscale(9460, nDist, 10);
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (tz-tsr > bottom || tz+tsr < top)
+            continue;
+        int dx = (tx-x2)>>4;
+        int dy = (ty-y2)>>4;
+        int dz = (tz-z2)>>8;
+        int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+        if (nDist2 < nClosest)
+        {
+            int nAngle = getangle(x2-x, y2-y);
+            int nDeltaAngle = ((nAngle-pSprite->ang+1024)&2047)-1024;
+            if (klabs(nDeltaAngle) <= tt.at8)
+            {
+                int tz = pSprite2->z-pSprite->z;
+                if (cansee(x, y, z, pSprite->sectnum, x2, y2, z2, pSprite2->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(nAngle)>>16;
+                    aim.dy = Sin(nAngle)>>16;
+                    aim.dz = divscale(tz, nDist, 10);
+                }
+                else
+                    aim.dz = tz;
+            }
+        }
+    }
+    actFireMissile(pSprite, -350, 0, aim.dx, aim.dy, aim.dz, 314);
+    actFireMissile(pSprite, 350, 0, aim.dx, aim.dy, aim.dz, 314);
+}
+
+static void sub_720AC(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int height = pSprite->yrepeat*pDudeInfo->eyeHeight;
+    int ax, ay, az;
+    ax = Cos(pSprite->ang)>>16;
+    ay = Sin(pSprite->ang)>>16;
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = height;
+    TARGETTRACK tt = { 0x10000, 0x10000, 0x100, 0x55, 0x100000 };
+    Aim aim;
+    aim.dx = ax;
+    aim.dy = ay;
+    aim.dz = gDudeSlope[nXSprite];
+    int nClosest = 0x7fffffff;
+    az = 0;
+    for (short nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
+    {
+        spritetype *pSprite2 = &sprite[nSprite2];
+        if (pSprite == pSprite2 || !(pSprite2->hitag&8))
+            continue;
+        int x2 = pSprite2->x;
+        int y2 = pSprite2->y;
+        int z2 = pSprite2->z;
+        int nDist = approxDist(x2-x, y2-y);
+        if (nDist == 0 || nDist > 0x2800)
+            continue;
+        if (tt.at10)
+        {
+            int t = divscale(nDist, tt.at10, 12);
+            x2 += (xvel[nSprite2]*t)>>12;
+            y2 += (yvel[nSprite2]*t)>>12;
+            z2 += (zvel[nSprite2]*t)>>8;
+        }
+        int tx = x+mulscale30(Cos(pSprite->ang), nDist);
+        int ty = y+mulscale30(Sin(pSprite->ang), nDist);
+        int tz = z+mulscale(gDudeSlope[nXSprite], nDist, 10);
+        int tsr = mulscale(9460, nDist, 10);
+        int top, bottom;
+        GetSpriteExtents(pSprite2, &top, &bottom);
+        if (tz-tsr > bottom || tz+tsr < top)
+            continue;
+        int dx = (tx-x2)>>4;
+        int dy = (ty-y2)>>4;
+        int dz = (tz-z2)>>8;
+        int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+        if (nDist2 < nClosest)
+        {
+            int nAngle = getangle(x2-x, y2-y);
+            int nDeltaAngle = ((nAngle-pSprite->ang+1024)&2047)-1024;
+            if (klabs(nDeltaAngle) <= tt.at8)
+            {
+                int tz = pSprite2->z-pSprite->z;
+                if (cansee(x, y, z, pSprite->sectnum, x2, y2, z2, pSprite2->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(nAngle)>>16;
+                    aim.dy = Sin(nAngle)>>16;
+                    aim.dz = divscale(tz, nDist, 10);
+                }
+                else
+                    aim.dz = tz;
+            }
+        }
+    }
+    actFireMissile(pSprite, 350, 0, aim.dx, aim.dy, -aim.dz, 314);
+    actFireMissile(pSprite, -350, 0, ax, ay, az, 314);
+}
+
+static void sub_72580(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void sub_725A4(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEEXTRA_at6_u2 *pDudeExtraE = &gDudeExtra[pSprite->extra].at6.u2;
+    if (pDudeExtraE->at4 && pDudeExtraE->at0 < 10)
+        pDudeExtraE->at0++;
+    else if (pDudeExtraE->at0 >= 10 && pDudeExtraE->at4)
+    {
+        pXSprite->goalAng += 256;
+        POINT3D *pTarget = &baseSprite[pSprite->index];
+        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
+        aiNewState(pSprite, pXSprite, &tcherno13AA28);
+        return;
+    }
+    if (Chance(pDudeInfo->alertChance))
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+                continue;
+            int x = pPlayer->pSprite->x;
+            int y = pPlayer->pSprite->y;
+            int z = pPlayer->pSprite->z;
+            int nSector = pPlayer->pSprite->sectnum;
+            int dx = x-pSprite->x;
+            int dy = y-pSprite->y;
+            int nDist = approxDist(dx, dy);
+            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+                continue;
+            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+                continue;
+            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                pDudeExtraE->at0 = 0;
+                aiSetTarget(pXSprite, pPlayer->at5b);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else if (nDist < pDudeInfo->hearDist)
+            {
+                pDudeExtraE->at0 = 0;
+                aiSetTarget(pXSprite, x, y, z);
+                aiActivateDude(pSprite, pXSprite);
+            }
+            else
+                continue;
+            break;
+        }
+    }
+}
+
+static void sub_72850(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &tchernobogSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void sub_72934(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &tcherno13A9B8);
+        return;
+    }
+    if (!(pSprite->type >= kDudeBase && pSprite->type < kDudeMax))
+        return;
+    //dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+    //dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &tchernobogSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0)
+    {
+        aiNewState(pSprite, pXSprite, &tchernobogSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x1f00 && nDist > 0xd00 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &tcherno13AA0C);
+                else if (nDist < 0xd00 && nDist > 0xb00 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &tcherno13A9D4);
+                else if (nDist < 0xb00 && nDist > 0x500 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &tcherno13A9F0);
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &tcherno13A9B8);
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aitchern.h b/source/blood/src/aitchern.h
new file mode 100644
index 000000000..6065e2ccf
--- /dev/null
+++ b/source/blood/src/aitchern.h
@@ -0,0 +1,34 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE tchernobogIdle;
+extern AISTATE tchernobogSearch;
+extern AISTATE tchernobogChase;
+extern AISTATE tchernobogRecoil;
+extern AISTATE tcherno13A9B8;
+extern AISTATE tcherno13A9D4;
+extern AISTATE tcherno13A9F0;
+extern AISTATE tcherno13AA0C;
+extern AISTATE tcherno13AA28;
diff --git a/source/blood/src/aiunicult.cpp b/source/blood/src/aiunicult.cpp
new file mode 100644
index 000000000..e8a4fefd4
--- /dev/null
+++ b/source/blood/src/aiunicult.cpp
@@ -0,0 +1,1309 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+Copyright (C) NoOne
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aiunicult.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+#include "triggers.h"
+#include "endgame.h"
+
+static void GDXCultistAttack1(int, int);
+static void punchCallback(int, int);
+static void ThrowCallback1(int, int);
+static void ThrowCallback2(int, int);
+static void ThrowThing(int, bool);
+static void thinkSearch(spritetype*, XSPRITE*);
+static void thinkGoto(spritetype*, XSPRITE*);
+static void thinkChase(spritetype*, XSPRITE*);
+static void forcePunch(spritetype*, XSPRITE*);
+
+static int nGDXGenDudeAttack1 = seqRegisterClient(GDXCultistAttack1);
+static int nGDXGenDudePunch = seqRegisterClient(punchCallback);
+static int nGDXGenDudeThrow1 = seqRegisterClient(ThrowCallback1);
+static int nGDXGenDudeThrow2 = seqRegisterClient(ThrowCallback2);
+
+static bool gGDXGenDudePunch = false;
+
+//public static final int kSlopeThrow = -8192;
+AISTATE GDXGenDudeIdleL = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE GDXGenDudeIdleW = { kAiStateIdle, 13, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE GDXGenDudeSearchL = { kAiStateSearch, 9, -1, 600, NULL, aiGenDudeMoveForward, thinkSearch, &GDXGenDudeIdleL };
+AISTATE GDXGenDudeSearchW= { kAiStateSearch, 13, -1, 600, NULL, aiGenDudeMoveForward, thinkSearch, &GDXGenDudeIdleW };
+AISTATE GDXGenDudeGotoL = { kAiStateMove, 9, -1, 600, NULL, aiGenDudeMoveForward, thinkGoto, &GDXGenDudeIdleL };
+AISTATE GDXGenDudeGotoW = { kAiStateMove, 13, -1, 600, NULL, aiGenDudeMoveForward, thinkGoto, &GDXGenDudeIdleW };
+AISTATE GDXGenDudeDodgeL = { kAiStateMove, 9, -1, 90, NULL,	aiMoveDodge,	NULL, &GDXGenDudeChaseL };
+AISTATE GDXGenDudeDodgeD = { kAiStateMove, 14, -1, 90, NULL, aiMoveDodge,	NULL, &GDXGenDudeChaseD };
+AISTATE GDXGenDudeDodgeW = { kAiStateMove, 13, -1, 90, NULL, aiMoveDodge,	NULL, &GDXGenDudeChaseW };
+// Dodge when get damage
+AISTATE GDXGenDudeDodgeDmgL = { kAiStateMove, 9, -1, 90, NULL,	aiMoveDodge,	NULL, &GDXGenDudeChaseL };
+AISTATE GDXGenDudeDodgeDmgD = { kAiStateMove, 14, -1, 90, NULL, aiMoveDodge,	NULL, &GDXGenDudeChaseD };
+AISTATE GDXGenDudeDodgeDmgW = { kAiStateMove, 13, -1, 90, NULL, aiMoveDodge,	NULL, &GDXGenDudeChaseW };
+// ---------------------
+AISTATE GDXGenDudeChaseL = { kAiStateChase, 9, -1, 0, NULL,	aiGenDudeMoveForward, thinkChase, NULL };
+AISTATE GDXGenDudeChaseD = { kAiStateChase, 14, -1, 0, NULL,	aiGenDudeMoveForward, thinkChase, NULL };
+AISTATE GDXGenDudeChaseW = { kAiStateChase, 13, -1, 0, NULL,	aiGenDudeMoveForward, thinkChase, NULL };
+AISTATE GDXGenDudeFireL = { kAiStateChase, 6, nGDXGenDudeAttack1, 0, NULL, aiMoveTurn, thinkChase, &GDXGenDudeFireL };
+AISTATE GDXGenDudeFireD = { kAiStateChase, 8, nGDXGenDudeAttack1, 0, NULL, aiMoveTurn, thinkChase, &GDXGenDudeFireD };
+AISTATE GDXGenDudeFireW = { kAiStateChase, 8, nGDXGenDudeAttack1, 0, NULL, aiMoveTurn, thinkChase, &GDXGenDudeFireW };
+AISTATE GDXGenDudeFire2L = { kAiStateChase, 6, nGDXGenDudeAttack1, 0, NULL, NULL, thinkChase, &GDXGenDudeFire2L };
+AISTATE GDXGenDudeFire2D = { kAiStateChase, 8, nGDXGenDudeAttack1, 0, NULL, aiMoveTurn, NULL, &GDXGenDudeFire2D };
+AISTATE GDXGenDudeFire2W = { kAiStateChase, 8, nGDXGenDudeAttack1, 0, NULL, aiMoveTurn, NULL, &GDXGenDudeFire2W };
+AISTATE GDXGenDudeRecoilL = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &GDXGenDudeChaseL };
+AISTATE GDXGenDudeRecoilD = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &GDXGenDudeChaseD };
+AISTATE GDXGenDudeRecoilW = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &GDXGenDudeChaseW };
+AISTATE GDXGenDudeThrow = { kAiStateChase, 7, nGDXGenDudeThrow1, 0, NULL, NULL, NULL, &GDXGenDudeChaseL };
+AISTATE GDXGenDudeThrow2 = { kAiStateChase, 7, nGDXGenDudeThrow2, 0, NULL, NULL, NULL, &GDXGenDudeChaseL };
+AISTATE GDXGenDudeRTesla = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &GDXGenDudeDodgeL };
+AISTATE GDXGenDudeProne = { kAiStateIdle, 13, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE GDXGenDudePunch = { kAiStateChase,10, nGDXGenDudePunch, 0, NULL, NULL, forcePunch, &GDXGenDudeChaseL };
+AISTATE GDXGenDudeTurn = { kAiStateChase, 0, -1, 0, NULL, aiMoveTurn, thinkChase, NULL };
+
+
+static void forcePunch(spritetype* pSprite, XSPRITE* pXSprite) {
+    // Required for those who don't have fire trigger in punch seq and for default animation
+    if (gGDXGenDudePunch == false && seqGetStatus(3, pSprite->extra) == -1) {
+        int nXSprite = pSprite->extra;
+        punchCallback(0,nXSprite);
+    }
+    
+    gGDXGenDudePunch = false;
+}
+
+
+static void punchCallback(int, int nXIndex){
+        XSPRITE* pXSprite = &xsprite[nXIndex];
+        int nSprite = pXSprite->reference;
+        spritetype* pSprite = &sprite[nSprite];
+
+        int nAngle = getangle(pXSprite->targetX - pSprite->x, pXSprite->targetY - pSprite->y);
+        int nZOffset1 = dudeInfo[pSprite->type - kDudeBase].eyeHeight/* * pSprite->yrepeat << 2*/;
+        int nZOffset2 = 0;
+        if(pXSprite->target != -1) {
+            spritetype* pTarget = &sprite[pXSprite->target];
+            if(IsDudeSprite(pTarget))
+                nZOffset2 = dudeInfo[pTarget->type - kDudeBase].eyeHeight/* * pTarget->yrepeat << 2*/;
+
+            int dx = Cos(nAngle) >> 16;
+            int dy = Sin(nAngle) >> 16;
+            int dz = nZOffset1 - nZOffset2;
+                
+            if (!sfxPlayGDXGenDudeSound(pSprite,9,pXSprite->data3))
+                sfxPlay3DSound(pSprite, 530, 1, 0);
+                
+            actFireVector(pSprite, 0, 0, dx, dy, dz,VECTOR_TYPE_22);
+        }
+
+        gGDXGenDudePunch = true;
+    }
+
+static void GDXCultistAttack1(int, int nXIndex) {
+    XSPRITE* pXSprite = &xsprite[nXIndex];
+    int nSprite = pXSprite->reference;
+    spritetype* pSprite = &sprite[nSprite];
+    int dx, dy, dz; int weapon = pXSprite->data1;
+    if (weapon >= 0 && weapon < kVectorMax) {
+
+        int vector = pXSprite->data1;
+        dx = Cos(pSprite->ang) >> 16;
+        dy = Sin(pSprite->ang) >> 16;
+        dz = gDudeSlope[nXIndex];
+
+        VECTORDATA* pVectorData = &gVectorData[vector];
+        int vdist = pVectorData->maxDist;
+            
+        // dispersal modifiers here in case if non-melee enemy
+        if (vdist <= 0 || vdist > 1280) {
+            dx += Random3(3000 - 1000 * gGameOptions.nDifficulty);
+            dy += Random3(3000 - 1000 * gGameOptions.nDifficulty);
+            dz += Random3(1000 - 500 * gGameOptions.nDifficulty);
+        }
+
+        actFireVector(pSprite, 0, 0, dx, dy, dz,(VECTOR_TYPE)vector);
+        if (!sfxPlayGDXGenDudeSound(pSprite,7,pXSprite->data3))
+            sfxPlayVectorSound(pSprite,vector);
+            
+    } else if (weapon >= kDudeBase && weapon < kDudeMax) {
+
+        spritetype* pSpawned = NULL; int dist = pSprite->clipdist * 6;
+        if ((pSpawned = actSpawnDude(pSprite, weapon, dist, 0)) == NULL)
+            return;
+
+        gDudeExtra[pSprite->extra].at6.u1.at4++;
+        pSpawned->owner = nSprite;
+        pSpawned->x += dist + (Random3(dist));
+        //pSpawned->z = pSprite->z;
+        //pSpawned->y = pSprite->y;
+        if (pSpawned->extra > -1) {
+            xsprite[pSpawned->extra].target = pXSprite->target;
+            if (pXSprite->target > -1)
+                aiActivateDude(pSpawned, &xsprite[pSpawned->extra]);
+        }
+        gKillMgr.sub_263E0(1);
+        
+        if (!sfxPlayGDXGenDudeSound(pSprite, 7, pXSprite->data3))
+            sfxPlay3DSoundCP(pSprite, 379, 1, 0, 0x10000 - Random3(0x3000));
+
+        /*spritetype* pEffect = gFX.fxSpawn((FX_ID)52, pSpawned->sectnum, pSpawned->x, pSpawned->y, pSpawned->z, pSpawned->ang);
+        if (pEffect != NULL) {
+
+            pEffect->cstat = kSprOriginAlign | kSprFace;
+            pEffect->shade = -127;
+            switch (Random(3)) {
+            case 0:
+                pEffect->pal = 0;
+                break;
+            case 1:
+                pEffect->pal = 5;
+                break;
+            case 2:
+                pEffect->pal = 9;
+                break;
+            case 3:
+                pEffect->shade = 127;
+                pEffect->pal = 1;
+            default:
+                pEffect->pal = 6;
+                break;
+            }
+            int repeat = 64 + Random(50);
+            pEffect->xrepeat = repeat;
+            pEffect->yrepeat = repeat;
+        }*/
+
+        if (Chance(0x3500)) {
+            int state = checkAttackState(pSprite, pXSprite);
+            switch (state) {
+            case 1:
+                aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeW);
+                break;
+            case 2:
+                aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeD);
+                break;
+            default:
+                aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeL);
+                break;
+            }
+        }
+    } else if (weapon >= kMissileBase && weapon < kMissileMax) {
+
+        dx = Cos(pSprite->ang) >> 16;
+        dy = Sin(pSprite->ang) >> 16;
+        dz = gDudeSlope[nXIndex];
+        //dz = 0;
+
+        // dispersal modifiers here
+        dx += Random3(3000 - 1000 * gGameOptions.nDifficulty);
+        dy += Random3(3000 - 1000 * gGameOptions.nDifficulty);
+        dz += Random3(1000 - 500 * gGameOptions.nDifficulty);
+
+        actFireMissile(pSprite, 0, 0, dx, dy, dz, weapon);
+        if (!sfxPlayGDXGenDudeSound(pSprite,7,pXSprite->data3))
+            sfxPlayMissileSound(pSprite, weapon);
+    }
+}
+
+static void ThrowCallback1(int, int nXIndex) {
+    ThrowThing(nXIndex, true);
+}
+
+static void ThrowCallback2(int, int nXIndex) {
+    ThrowThing(nXIndex, true);
+}
+
+static void ThrowThing(int nXIndex, bool impact) {
+    XSPRITE* pXSprite = &xsprite[nXIndex];
+    int nSprite = pXSprite->reference;
+    spritetype* pSprite = &sprite[nSprite];
+
+    if (!(pXSprite->target >= 0 && pXSprite->target < kMaxSprites))
+        return;
+
+    spritetype * pTarget = &sprite[pXSprite->target];
+    if (!(pTarget->type >= kDudeBase && pTarget->type < kDudeMax))
+        return;
+
+
+    int thingType = pXSprite->data1;
+    if (thingType >= kThingBase && thingType < kThingMax) {
+
+        THINGINFO* pThinkInfo = &thingInfo[thingType - kThingBase];
+        if (pThinkInfo->allowThrow == 1) {
+
+            if (!sfxPlayGDXGenDudeSound(pSprite, 8, pXSprite->data3))
+                sfxPlay3DSound(pSprite, 455, -1, 0);
+
+            int dx = pTarget->x - pSprite->x;
+            int dy = pTarget->y - pSprite->y;
+            int dz = pTarget->z - pSprite->z;
+
+            int dist = approxDist(dx, dy);	int zThrow = 14500;
+            spritetype* pThing = NULL; spritetype* pLeech = NULL; XSPRITE* pXLeech = NULL;
+            if (thingType == kGDXThingCustomDudeLifeLeech) {
+                if ((pLeech = leechIsDropped(pSprite)) != NULL) {
+                    // pickup life leech before throw it again
+                    pXLeech = &xsprite[pLeech->extra];
+                    removeLeech(pLeech);
+                }
+
+                zThrow = 5000;
+            }
+
+            pThing = actFireThing(pSprite, 0, 0, (dz / 128) - zThrow, thingType, divscale(dist / 540, 120, 23));
+            if (pThing == NULL) return;
+
+            if (pThinkInfo->at11 < 0 && pThing->type != kGDXThingThrowableRock) pThing->picnum = 0;
+            pThing->owner = pSprite->xvel;
+            switch (thingType) {
+            case 428:
+                impact = true;
+                pThing->xrepeat = 24;
+                pThing->yrepeat = 24;
+                xsprite[pThing->extra].data4 = 3 + gGameOptions.nDifficulty;
+                break;
+
+            case kGDXThingThrowableRock:
+                int sPics[6];
+                sPics[0] = 2406;	sPics[1] = 2280;
+                sPics[2] = 2185;	sPics[3] = 2155;
+                sPics[4] = 2620;	sPics[5] = 3135;
+
+                pThing->picnum = sPics[Random(5)];
+                pThing->pal = 5;
+                pThing->xrepeat = 24 + Random(42);
+                pThing->yrepeat = 24 + Random(42);
+                pThing->cstat |= 0x0001;
+
+                if (Chance(0x3000)) pThing->cstat |= 0x0004;
+                if (Chance(0x3000)) pThing->cstat |= 0x0008;
+
+                if (pThing->xrepeat > 60) xsprite[pThing->extra].data1 = 43;
+                else if (pThing->xrepeat > 40) xsprite[pThing->extra].data1 = 33;
+                else if (pThing->xrepeat > 30) xsprite[pThing->extra].data1 = 23;
+                else xsprite[pThing->extra].data1 = 12;
+
+                impact = false;
+                return;
+            case 400:
+            case 401:
+            case 420:
+                impact = false;
+                break;
+            case kGDXThingTNTProx:
+                xsprite[pThing->extra].state = 0;
+                xsprite[pThing->extra].Proximity = true;
+                return;
+            case 431:
+            case kGDXThingCustomDudeLifeLeech:
+                XSPRITE* pXThing = &xsprite[pThing->extra];
+                if (pLeech != NULL) pXThing->health = pXLeech->health;
+                else pXThing->health = 300 * gGameOptions.nDifficulty;
+
+                sfxPlay3DSound(pSprite, 490, -1, 0);
+
+                if (gGameOptions.nDifficulty <= 2) pXThing->data3 = 32700;
+                else pXThing->data3 = Random(10);
+                pThing->pal = 6;
+                pXThing->target = pTarget->xvel;
+                pXThing->Proximity = true;
+                pXThing->stateTimer = 1;
+                evPost(pThing->xvel, 3, 80, CALLBACK_ID_20);
+                return;
+            }
+
+            if (impact == true && dist <= 7680) xsprite[pThing->extra].Impact = true;
+            else {
+                xsprite[pThing->extra].Impact = false;
+                evPost(pThing->xvel, 3, 120 * Random(2) + 120, COMMAND_ID_1);
+            }
+            return;
+        }
+
+    }
+}
+
+static void thinkSearch( spritetype* pSprite, XSPRITE* pXSprite )
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    sub_5F15C(pSprite, pXSprite);
+        
+}
+
+static void thinkGoto( spritetype* pSprite, XSPRITE* pXSprite )
+{
+    int dx, dy, dist;
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->lotag - kDudeBase];
+
+    dx = pXSprite->targetX - pSprite->x;
+    dy = pXSprite->targetY - pSprite->y;
+
+    int nAngle = getangle(dx, dy);
+    dist = approxDist(dx, dy);
+
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+
+    // if reached target, change to search mode
+    if ( /*dist < M2X(10.0)*/ dist < 5120 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery ) {
+        if(spriteIsUnderwater(pSprite,false))
+            aiNewState(pSprite, pXSprite, &GDXGenDudeSearchW);
+        else
+            aiNewState(pSprite, pXSprite, &GDXGenDudeSearchL);
+    }
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase( spritetype* pSprite, XSPRITE* pXSprite )
+{
+
+
+    if (Chance(0x3000)) GDXGenDudeRecoilL.at18 = &GDXGenDudeDodgeD;
+    else GDXGenDudeRecoilL.at18 = &GDXGenDudeChaseL;
+
+    if (Chance(0x3000)) GDXGenDudeRecoilW.at18 = &GDXGenDudeDodgeW;
+    else GDXGenDudeRecoilW.at18 = &GDXGenDudeChaseW;
+
+    if (Chance(0x3000)) GDXGenDudeRecoilD.at18 = &GDXGenDudeDodgeL;
+    else GDXGenDudeRecoilD.at18 = &GDXGenDudeChaseL;
+
+    if (pXSprite->target == -1) {
+        if(spriteIsUnderwater(pSprite,false))
+            aiNewState(pSprite, pXSprite, &GDXGenDudeGotoW);
+        else
+            aiNewState(pSprite, pXSprite, &GDXGenDudeGotoL);
+        return;
+    }
+
+    int dx, dy, dist;
+
+
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype* pTarget = &sprite[pXSprite->target];
+    XSPRITE* pXTarget = pTarget->extra > 0 ? &xsprite[pTarget->extra] : NULL;
+    if(!IsDudeSprite(pTarget))
+        pXTarget = NULL;
+
+    // check target
+    dx = pTarget->x - pSprite->x;
+    dy = pTarget->y - pSprite->y;
+
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+
+    if ( pXTarget == NULL || pXTarget->health <= 0 )
+    {
+        // target is dead
+        if(spriteIsUnderwater(pSprite,false))
+            aiNewState(pSprite, pXSprite, &GDXGenDudeSearchW);
+        else {
+            aiNewState(pSprite, pXSprite, &GDXGenDudeSearchL);
+            sfxPlayGDXGenDudeSound(pSprite,5,pXSprite->data3);
+        }
+        
+        return;
+    }
+
+    if ( IsPlayerSprite( pTarget ) )
+    {
+        PLAYER* pPlayer = &gPlayer[ pTarget->type - kDudePlayer1 ];
+        if ( powerupCheck( pPlayer, 13 ) > 0 )
+        {
+            if(spriteIsUnderwater(pSprite,false))
+                aiNewState(pSprite, pXSprite, &GDXGenDudeSearchW);
+            else
+                aiNewState(pSprite, pXSprite, &GDXGenDudeSearchL);
+            return;
+        }
+    }
+    
+    dist = approxDist(dx, dy);
+    if ( dist <= pDudeInfo->seeDist ) {
+        int nAngle = getangle(dx, dy);
+        int losAngle = ((1024 + nAngle - pSprite->ang) & 2047) - 1024;
+        int eyeAboveZ = (pDudeInfo->eyeHeight * pSprite->yrepeat) << 2;
+        VECTORDATA* meleeVector = &gVectorData[22];
+
+        // is there a line of sight to the target?
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum,
+            pSprite->x, pSprite->y, pSprite->z - eyeAboveZ, pSprite->sectnum)){
+            // is the target visible?
+            
+            if (dist < pDudeInfo->seeDist && klabs(losAngle) <= pDudeInfo->periphery) {
+                aiSetTarget(pXSprite, pXSprite->target);
+                
+                if ((gFrameClock & 64) == 0 && Chance(0x1000) && !spriteIsUnderwater(pSprite,false))
+                    sfxPlayGDXGenDudeSound(pSprite,6,pXSprite->data3);
+
+                if (dist <= 1500) gDudeSlope[sprite[pXSprite->reference].extra] = divscale(pTarget->z - pSprite->z, dist, 13);
+                else if (dist <= 3000) gDudeSlope[sprite[pXSprite->reference].extra] = divscale(pTarget->z - pSprite->z, dist, 11);
+                else if (dist > 0) gDudeSlope[sprite[pXSprite->reference].extra] = divscale(pTarget->z - pSprite->z, dist, 10);
+                    
+                spritetype* pLeech = NULL;
+                if (pXSprite->data1 >= kThingBase && pXSprite->data1 < kThingMax) {
+                    if (pXSprite->data1 == 431) pXSprite->data1 = kGDXThingCustomDudeLifeLeech;
+                    if (klabs(losAngle) < kAng15) {
+                        if (dist < 12264 && dist > 7680 && !spriteIsUnderwater(pSprite,false) && pXSprite->data1 != kGDXThingCustomDudeLifeLeech){
+                            int pHit = HitScan(pSprite, pSprite->z, dx, dy, 0, 16777280, 0);
+                            switch(pHit){
+                                case 3:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeThrow);
+                                    return;
+
+                                case 0:
+                                case 4:
+                                    return;
+                                case -1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeThrow);
+                                    return;
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeThrow);
+                                    return;
+                            }
+
+                        } else if (dist > 4072 && dist <= 9072 && !spriteIsUnderwater(pSprite,false) && pSprite->owner != kMaxSprites){
+                                if (pXSprite->data1 != kGDXThingCustomDudeLifeLeech && pXSprite->data1 != kGDXThingThrowableRock) {
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeThrow2);
+                                } else {
+                                        
+                                    if (pXSprite->data1 == kGDXThingCustomDudeLifeLeech) {
+                                        if ((pLeech = leechIsDropped(pSprite)) == NULL){
+                                            aiNewState(pSprite, pXSprite, &GDXGenDudeThrow2);
+                                            GDXGenDudeThrow2.at18 = &GDXGenDudeDodgeL;
+                                            return;
+                                        }
+                                            
+                                        XSPRITE* pXLeech = &xsprite[pLeech->extra];
+                                        int ldist = getTargetDist(pTarget,pDudeInfo,pLeech);
+                                        if (ldist > 3 || !cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum,
+                                                            pLeech->x, pLeech->y, pLeech->z, pLeech->sectnum) || pXLeech->target == -1) {
+                                                                    
+                                            aiNewState(pSprite, pXSprite, &GDXGenDudeThrow2);
+                                            GDXGenDudeThrow2.at18 = &GDXGenDudeDodgeL;
+                                                
+                                        } else {
+                                            GDXGenDudeThrow2.at18 = &GDXGenDudeChaseL;
+                                            if (pXLeech->target != pXSprite->target) pXLeech->target = pXSprite->target;
+                                            if (dist > 5072 && Chance(0x3000)) {
+                                                if (Chance(0x2000))
+                                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeL);
+                                                else
+                                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeD);
+                                            } else {
+                                                aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                                            }
+                                        }
+                                            
+                                    } else if (pXSprite->data1 == kGDXThingThrowableRock){
+                                        if (Chance(0x2000))
+                                            aiNewState(pSprite, pXSprite, &GDXGenDudeThrow2);
+                                        else
+                                            sfxPlayGDXGenDudeSound(pSprite,0,pXSprite->data3);
+                                    } 
+                                }
+
+                                return;
+
+                        } else if (dist <= meleeVector->maxDist) {
+                            if (spriteIsUnderwater(pSprite,false)) {
+                                if (Chance(0x7000))
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudePunch);
+                                else
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeW);
+
+                            } else {
+                                if (Chance(0x7000))
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudePunch);
+                                else
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeL);
+
+                            }
+                            return;
+
+                        } else {
+                            int state = checkAttackState(pSprite, pXSprite);
+                                
+                            if(state == 1) 
+                                aiNewState(pSprite, pXSprite, &GDXGenDudeChaseW);
+                                
+                            if(state == 2) {
+                                if (Chance(0x3000))
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseD);
+                                else
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                            }
+                                
+                            if(state == 3) 
+                                aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                                
+                            return;
+                        }
+                    }
+
+                } else {
+                    
+                    int defDist = 17920; int vdist = defDist;
+                    if (pXSprite->data1 > 0 && pXSprite->data1 < kVectorMax) {
+
+                        switch (pXSprite->data1) {
+                        case 19:
+                            pXSprite->data1 = 2;
+                            break;
+                        }
+
+                        VECTORDATA* pVectorData = &gVectorData[pXSprite->data1];
+                        vdist = pVectorData->maxDist;
+                        if (vdist <= 0 || vdist > defDist)
+                            vdist = defDist;
+
+                    } else if (pXSprite->data1 >= kDudeBase && pXSprite->data1 < kDudeMax && pXSprite->data1 != kGDXDudeUniversalCultist) {
+
+                            if (gDudeExtra[pSprite->extra].at6.u1.at4 > 0) {
+
+                                updateTargetOfSlaves(pSprite);
+
+                                if (pXSprite->target >= 0 && sprite[pXSprite->target].owner == pSprite->xvel) {
+                                    aiSetTarget(pXSprite, pSprite->x, pSprite->y, pSprite->z);
+                                    return;
+                                }
+                            }
+
+                            if (gDudeExtra[pSprite->extra].at6.u1.at4 <= gGameOptions.nDifficulty && dist > meleeVector->maxDist) {
+                                vdist = (vdist / 2) + Random(vdist / 2);
+                            }
+                            else if (dist <= meleeVector->maxDist) {
+                                aiNewState(pSprite, pXSprite, &GDXGenDudePunch);
+                                return;
+                            }
+                            else {
+                                int state = checkAttackState(pSprite, pXSprite);
+                                switch (state) {
+                                case 1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseW);
+                                    return;
+                                case 2:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseD);
+                                    return;
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                                    return;
+                                }
+                            }
+                    } else if (pXSprite->data1 >= kMissileBase && pXSprite->data1 < kMissileMax){
+                        // special handling for flame, explosive and life leech missiles
+                        int mdist = 2500;
+                        if (pXSprite->data1 != 303) mdist = 3000;
+                        switch(pXSprite->data1){
+                            case 315:
+                                // Pickup life leech if it was thrown previously
+                                if ((pLeech = leechIsDropped(pSprite)) != NULL)
+                                    removeLeech(pLeech);
+                                break;
+                            case 303:
+                            case 305:
+                            case 312:
+                            case 313:
+                            case 314:
+                            {
+                                if (dist > mdist || pXSprite->locked == 1) break;
+                                int state = checkAttackState(pSprite, pXSprite);
+                                if (dist <= meleeVector->maxDist && Chance(0x7000)) {
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudePunch);
+                                    return;
+                                } else {
+                                    switch (state) {
+                                    case 1:
+                                        aiNewState(pSprite, pXSprite, &GDXGenDudeChaseW);
+                                        return;
+                                    case 2:
+                                        aiNewState(pSprite, pXSprite, &GDXGenDudeChaseD);
+                                        return;
+                                    default:
+                                        aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                                        return;
+                                    }
+                                }
+                            }
+                            case 304:
+                            case 308:
+                            {
+                                if (spriteIsUnderwater(pSprite, false)) {
+                                    if (dist <= meleeVector->maxDist) {
+                                        if (Chance(0x4000)) {
+                                            aiNewState(pSprite, pXSprite, &GDXGenDudePunch);
+                                        }
+                                        else {
+                                            aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeW);
+                                        }
+                                    }
+                                    else {
+                                        aiNewState(pSprite, pXSprite, &GDXGenDudeChaseW);
+                                    }
+                                    return;
+                                }
+
+                                vdist = 4200;
+                                if ((gFrameClock & 16) == 0)
+                                    vdist += Random(800);
+                                break;
+                            }
+                        }
+                    } else if (pXSprite->data1 >= 459 && pXSprite->data1 < (459 + kExplodeMax) - 1) {
+
+                        int nType = pXSprite->data1 - 459; EXPLOSION* pExpl = &explodeInfo[nType];
+                        if (pExpl != NULL &&
+                            CheckProximity(pSprite, pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pExpl->at3 / 2) &&
+                            doExplosion(pSprite, nType)) {
+
+                            pXSprite->health = 1;
+                            actDamageSprite(pSprite->xvel, pSprite, DAMAGE_TYPE_3, 65535);
+
+                            xvel[pSprite->xvel] = 0;
+                            zvel[pSprite->xvel] = 0;
+                            yvel[pSprite->xvel] = 0;
+                        }
+                        return;
+
+                    } else {
+
+                        // Scared dude - no weapon. Still can punch you sometimes.
+                        int state = checkAttackState(pSprite, pXSprite);
+                        if (Chance(0x0500) && !spriteIsUnderwater(pSprite,false))
+                            sfxPlayGDXGenDudeSound(pSprite,6,pXSprite->data3);
+                            
+                        if (Chance(0x5000)){
+                            switch (state){
+                                case 1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeW);
+                                    break;
+                                case 2:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeD);
+                                    break;
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeDodgeL);
+                                    break;
+                            }
+
+                            pXSprite->target = -1;
+
+                        } else if (dist <= meleeVector->maxDist && Chance(0x7000)) {
+                            aiNewState(pSprite, pXSprite, &GDXGenDudePunch);
+                        } else {
+                            switch (state){
+                                case 1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeSearchW);
+                                    break;
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeSearchL);
+                                    break;
+                            }
+
+                            pXSprite->target = -1;
+                        }
+
+                        return;
+                    }
+
+                    int state = checkAttackState(pSprite, pXSprite);
+                    if (dist > vdist && pXSprite->aiState == &GDXGenDudeChaseD)
+                        aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                    
+                    if (dist < vdist && klabs(losAngle) < 35 /*&& klabs(losAngle) < kAngle5*/) {
+                        if (vdist > 1512){
+                            switch(state){
+                                case 1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeFireW);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFireW;
+                                    break;
+                                case 2:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeFireD);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFireD;
+                                    break;
+
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeFireL);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFireL;
+                                    break;
+                            }
+                        } else {
+                            switch(state){
+                                case 1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeFire2W);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFire2W;
+                                    break;
+                                case 2:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeFire2D);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFire2D;
+                                    break;
+
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeFire2L);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFire2L;
+                                    break;
+                            }
+                        }
+                    } else {
+                        int nSeq = 6; if (state < 3) nSeq = 8;
+                        if (seqGetID(3, pSprite->extra) == pDudeInfo->seqStartID + nSeq) {
+                            switch(state){
+                                case 1:
+                                    pXSprite->aiState->at18 = &GDXGenDudeChaseW;
+                                    break;
+                                case 2:
+                                    pXSprite->aiState->at18 = &GDXGenDudeChaseD;
+                                    break;
+                                default:
+                                    pXSprite->aiState->at18 = &GDXGenDudeChaseL;
+                                    break;
+                            }
+                        } else {
+                            if(pXSprite->aiState == &GDXGenDudeChaseL || pXSprite->aiState == &GDXGenDudeChaseD ||
+                            pXSprite->aiState == &GDXGenDudeChaseW || pXSprite->aiState == &GDXGenDudeFireL ||
+                            pXSprite->aiState == &GDXGenDudeFireD || pXSprite->aiState == &GDXGenDudeFireW)
+                                return;
+
+                            switch (state) {
+                                case 1:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseW);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFireW;
+                                    break;
+                                case 2:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseD);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFireD;
+                                    break;
+                                default:
+                                    aiNewState(pSprite, pXSprite, &GDXGenDudeChaseL);
+                                    pXSprite->aiState->at18 = &GDXGenDudeFireL;
+                                    break;
+                            }
+                        }
+                    }
+
+                }
+                return;
+            }
+        }
+    }
+
+    if(spriteIsUnderwater(pSprite,false)) {
+        aiNewState(pSprite, pXSprite, &GDXGenDudeGotoW);
+    } else {
+        aiNewState(pSprite, pXSprite, &GDXGenDudeGotoL);
+    }
+    pXSprite->target = -1;
+}
+
+int checkAttackState(spritetype* pSprite, XSPRITE* pXSprite) {
+    UNREFERENCED_PARAMETER(pXSprite);
+    if (sub_5BDA8(pSprite, 14) || spriteIsUnderwater(pSprite,false))
+    {
+        if ( !sub_5BDA8(pSprite, 14) || spriteIsUnderwater(pSprite,false))
+        {
+            if (spriteIsUnderwater(pSprite,false))
+            {
+                return 1; //water
+            }
+        }
+        else
+        {
+            return 2; //duck
+        }
+    }
+    else
+    {
+        return 3; //land
+    }
+    return 0;
+}
+
+/*bool sub_5BDA8(spritetype* pSprite, int nSeq)
+{
+    if (pSprite->statnum == 6 && pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
+    {
+        DUDEINFO* pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+        if (seqGetID(3, pSprite->extra) == pDudeInfo->seqStartID + nSeq && seqGetStatus(3, pSprite->extra) >= 0)
+            return true;
+    }
+    return false;
+}
+
+bool sub_57901(spritetype* pSprite, int nSeqID) {
+    if ( pSprite->statnum == 6 )
+    {
+        if ( IsDudeSprite(pSprite) )
+        {
+            SEQINST* pSeqInst = GetInstance(3, pSprite->extra); Seq* pSeq = pSeqInst->pSequence;
+            if ( pSeq == pSEQs.get(xsprite[pSprite->extra].data2 + nSeqID) && seqGetStatus(3, pSprite->extra) >= 0 )
+                return true;
+        }
+    }
+    return false;
+}*/
+
+bool TargetNearThing(spritetype* pSprite, int thingType) {
+    for ( int nSprite = headspritesect[pSprite->sectnum]; nSprite >= 0; nSprite = nextspritesect[nSprite] )
+    {
+        // check for required things or explosions in the same sector as the target
+        if ( sprite[nSprite].type == thingType || sprite[nSprite].statnum == 2 )
+            return true; // indicate danger
+    }
+    return false;
+}
+    
+///// For gen dude
+int getGenDudeMoveSpeed(spritetype* pSprite,int which, bool mul, bool shift) {
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    XSPRITE* pXSprite = &xsprite[pSprite->extra];
+    int speed = -1; int step = 2500; int maxSpeed = 146603;
+    switch(which){
+        case 0:
+            speed = pDudeInfo->frontSpeed;
+            break;
+        case 1:
+            speed = pDudeInfo->sideSpeed;
+            break;
+        case 2:
+            speed = pDudeInfo->backSpeed;
+            break;
+        case 3:
+            speed = pDudeInfo->angSpeed;
+            break;
+        default:
+            return -1;
+    }
+    if (pXSprite->busyTime > 0) speed /=3;
+    if (speed > 0 && mul) {
+            
+        //System.err.println(pXSprite.busyTime);
+        if (pXSprite->busyTime > 0)
+            speed += (step * pXSprite->busyTime);
+    }
+        
+    if (shift) speed *= 4 >> 4;
+    if (speed > maxSpeed) speed = maxSpeed;
+        
+    return speed;
+}
+    
+void aiGenDudeMoveForward(spritetype* pSprite, XSPRITE* pXSprite ) {
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    
+    int maxTurn = pDudeInfo->angSpeed * 4 >> 4;
+
+    int dang = ((kAng180 + pXSprite->goalAng - pSprite->ang) & 2047) - kAng180;
+    pSprite->ang = ((pSprite->ang + ClipRange(dang, -maxTurn, maxTurn)) & 2047);
+        
+    // don't move forward if trying to turn around
+    if ( klabs(dang) > kAng60 )
+        return;
+        
+    int sin = Sin(pSprite->ang);
+    int cos = Cos(pSprite->ang);
+
+    int frontSpeed = getGenDudeMoveSpeed(pSprite,0,true,false);
+    xvel[pSprite->xvel] += mulscale(cos, frontSpeed, 30);
+    yvel[pSprite->xvel] += mulscale(sin, frontSpeed, 30);
+}
+    
+bool sfxPlayGDXGenDudeSound(spritetype* pSprite, int mode, int data) {
+    int sndId = -1; int rand = 0; bool gotSnd = true;
+        
+    switch (mode){
+        // spot sound
+        case 0:
+            rand = 2; sndId = 1003;
+            if (data > 0)
+                sndId = data;
+            break;
+        // pain sound
+        case 1:
+            rand = 2; sndId = 1013;
+            if (data > 0)
+                sndId = data + 2;
+            break;
+        // death sound
+        case 2:
+            rand = 2; sndId = 1018;
+            if (data > 0)
+                sndId = data + 4;
+            break;
+        // burning state sound
+        case 3:
+            rand = 2; sndId = 1031;
+            if (data > 0)
+                sndId = data + 6;
+            break;
+        // explosive death or end of burning state sound
+        case 4:
+            rand = 2; sndId = 1018;
+            if (data > 0)
+                sndId = data + 8;
+            break;
+        // target of dude is dead
+        case 5:
+            rand = 2; sndId = 4021;
+            if (data > 0)
+                sndId = data + 10;
+            break;
+        // roam sounds
+        case 6:
+            rand = 2; sndId = 1005;
+            if (data > 0)
+                sndId = data + 12;
+            break;
+        // weapon attack
+        case 7:
+            if (data > 0)
+                sndId = data + 14;
+            break;
+        // throw attack
+        case 8:
+            if (data > 0)
+                sndId = data + 15;
+            break;
+        // melee attack
+        case 9:
+            if (data > 0)
+                sndId = data + 16;
+            break;
+        // transforming in other dude
+        case 10:
+            sndId = 9008;
+            if (data > 0)
+                sndId = data + 17;
+            break;
+        default:
+            return false;
+            
+    }
+        
+        
+    if (sndId < 0) return false;
+    else if (data <= 0) sndId = sndId + Random(rand);
+    else {
+            
+        int maxRetries = 5; gotSnd = false;
+        // Let's try to get random snd
+        while(maxRetries-- > 0){
+            int random = Random(rand);
+            if (gSysRes.Lookup(sndId + random, "SFX")){
+                sndId = sndId + random;
+                gotSnd = true;
+                break;
+            }
+        }
+            
+        // If no success in getting random snd, get first existing one
+        if (gotSnd == false){
+            int max = sndId + rand;
+            while(sndId++ <= max){
+                if (gSysRes.Lookup(sndId, "SFX")) {
+                    gotSnd = true;
+                    break;
+                }
+            }
+        }
+    }
+
+    if (gotSnd) {
+        switch (mode){
+           // case 1:
+            case 2:
+            case 4:
+            case 7:
+            case 8:
+            case 9:
+            case 10:
+                sfxPlay3DSound(pSprite, sndId, -1, 0);
+                break;
+            default:
+                aiPlay3DSound(pSprite, sndId, AI_SFX_PRIORITY_2, -1);
+                break;
+        }
+        return true;
+    }
+        
+    return false;
+}
+    
+    
+bool spriteIsUnderwater(spritetype* pSprite,bool oldWay) {
+    if (oldWay){
+        if (xsprite[pSprite->extra].medium == 1 || xsprite[pSprite->extra].medium == 2)
+            return true;
+        return false;
+    }
+        
+    if (sector[pSprite->sectnum].extra < 0) return false;
+    else if (xsector[sector[pSprite->sectnum].extra].Underwater)
+        return true;
+        
+    return false;
+}
+    
+spritetype* leechIsDropped(spritetype* pSprite) {
+    for (int nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        if (sprite[nSprite].lotag == kGDXThingCustomDudeLifeLeech && sprite[nSprite].owner == pSprite->xvel)
+            return &sprite[nSprite];
+    }
+        
+    return NULL;
+        
+}
+    
+void removeDudeStuff(spritetype* pSprite) {
+    for (short nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        if (sprite[nSprite].owner != pSprite->xvel) continue;
+        switch (sprite[nSprite].lotag) {
+        case 401:
+        case 402:
+        case 433:
+            deletesprite(nSprite);
+            break;
+        case kGDXThingCustomDudeLifeLeech:
+            killDudeLeech(&sprite[nSprite]);
+            break;
+        }
+    }
+
+    for (short nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        if (sprite[nSprite].owner != pSprite->xvel) continue;
+        actDamageSprite(sprite[nSprite].owner, &sprite[nSprite], (DAMAGE_TYPE) 0, 65535);
+    }
+}
+    
+void removeLeech(spritetype* pLeech, bool delSprite) {
+    if (pLeech != NULL) {
+        spritetype* pEffect = gFX.fxSpawn((FX_ID)52,pLeech->sectnum,pLeech->x,pLeech->y,pLeech->z,pLeech->ang);
+        if (pEffect != NULL) {
+            pEffect->cstat = kSprFace;
+            pEffect->pal = 6;
+            int repeat = 64 + Random(50);
+            pEffect->xrepeat = repeat;
+            pEffect->yrepeat = repeat;
+        }
+        sfxPlay3DSoundCP(pLeech, 490, -1, 0,60000);
+        if (delSprite)
+            actPostSprite(pLeech->index, kStatFree);
+    }
+}
+    
+void killDudeLeech(spritetype* pLeech) {
+    if (pLeech != NULL) {
+        //removeLeech(pLeech, false);
+        actDamageSprite(pLeech->owner, pLeech, DAMAGE_TYPE_3, 65535);
+        sfxPlay3DSoundCP(pLeech, 522, -1, 0, 60000);
+    }
+}
+    
+XSPRITE* getNextIncarnation(XSPRITE* pXSprite) {
+    if (pXSprite->txID > 0) {
+        for (short nSprite = headspritestat[7]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+            if (!IsDudeSprite(&sprite[nSprite]) || sprite[nSprite].extra < 0) continue;
+            if (xsprite[sprite[nSprite].extra].rxID == pXSprite->txID && xsprite[sprite[nSprite].extra].health > 0)
+                return &xsprite[sprite[nSprite].extra];
+        }
+    }
+    return NULL;
+}
+
+bool dudeIsMelee(XSPRITE* pXSprite) {
+    int meleeDist = 2048; int vdist = meleeDist;
+    if (pXSprite->data1 >= 0 && pXSprite->data1 < kVectorMax) {
+        int vector = pXSprite->data1; if (vector <= 0) vector = 2;
+        VECTORDATA pVectorData = gVectorData[vector];
+        vdist = pVectorData.maxDist;
+
+        if (vdist > 0 && vdist <= meleeDist)
+            return true;
+
+    }
+    else {
+
+        if (pXSprite->data1 >= 459 && pXSprite->data1 < (459 + kExplodeMax) - 1)
+            return true;
+
+        switch (pXSprite->data1) {
+        case 304:
+        case 308:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    return false;
+}
+
+int getRecoilChance(spritetype* pSprite) {
+    int mass = getDudeMassBySpriteSize(pSprite);
+    int cumulDmg = 0; int baseChance = 0x4000;
+    if (pSprite->extra >= 0) {
+        XSPRITE pXSprite = xsprite[pSprite->extra];
+        baseChance += (pXSprite.burnTime / 2);
+        cumulDmg = pXSprite.cumulDamage;
+        if (dudeIsMelee(&pXSprite))
+            baseChance = 0x500;
+    }
+
+    baseChance += cumulDmg;
+    int chance = ((baseChance / mass) << 7);
+    return chance;
+
+}
+
+int getDodgeChance(spritetype* pSprite) {
+    int mass = getDudeMassBySpriteSize(pSprite); int baseChance = 0x1000;
+    if (pSprite->extra >= 0) {
+        XSPRITE pXSprite = xsprite[pSprite->extra];
+        baseChance += pXSprite.burnTime;
+        if (dudeIsMelee(&pXSprite))
+            baseChance = 0x200;
+    }
+
+    int chance = ((baseChance / mass) << 7);
+    return chance;
+}
+
+void dudeLeechOperate(spritetype* pSprite, XSPRITE* pXSprite, EVENT a3)
+{
+    if (a3.cmd == COMMAND_ID_0) {
+        actPostSprite(pSprite->xvel, kStatFree);
+        return;
+    }
+    
+    int nTarget = pXSprite->target;
+    if (nTarget >= 0 && nTarget < kMaxSprites) {
+        spritetype* pTarget = &sprite[nTarget];
+        if (pTarget->statnum == 6 && !(pTarget->hitag & 32) && pTarget->extra > 0 && pTarget->extra < kMaxXSprites && !pXSprite->stateTimer)
+        {
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            int nType = pTarget->type - kDudeBase;
+            DUDEINFO* pDudeInfo = &dudeInfo[nType];
+            int z1 = (top - pSprite->z) - 256;
+            int x = pTarget->x; int y = pTarget->y; int z = pTarget->z;
+            int nDist = approxDist(x - pSprite->x, y - pSprite->y);
+            
+            if (nDist != 0 && cansee(pSprite->x, pSprite->y, top, pSprite->sectnum, x, y, z, pTarget->sectnum))
+            {
+                int t = divscale(nDist, 0x1aaaaa, 12);
+                x += (xvel[nTarget] * t) >> 12;
+                y += (yvel[nTarget] * t) >> 12;
+                int angBak = pSprite->ang;
+                pSprite->ang = getangle(x - pSprite->x, y - pSprite->y);
+                int dx = Cos(pSprite->ang) >> 16;
+                int dy = Sin(pSprite->ang) >> 16;
+                int tz = pTarget->z - (pTarget->yrepeat * pDudeInfo->aimHeight) * 4;
+                int dz = divscale(tz - top - 256, nDist, 10);
+                int nMissileType = 316 + (pXSprite->data3 ? 1 : 0);
+                int t2;
+                
+                if (!pXSprite->data3) t2 = 120 / 10.0;
+                else t2 = (3 * 120) / 10.0;
+
+                spritetype * pMissile = actFireMissile(pSprite, 0, z1, dx, dy, dz, nMissileType);
+                if (pMissile)
+                {
+                    pMissile->owner = pSprite->owner;
+                    pXSprite->stateTimer = 1;
+                    evPost(pSprite->index, 3, t2, CALLBACK_ID_20);
+                    pXSprite->data3 = ClipLow(pXSprite->data3 - 1, 0);
+                }
+                pSprite->ang = angBak;
+            }
+        }
+        
+    }
+}
+
+bool doExplosion(spritetype* pSprite, int nType) {
+    spritetype* pExplosion = actSpawnSprite(pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 2, true);
+    int nSeq = 4; int nSnd = 304; EXPLOSION* pExpl = &explodeInfo[nType];
+
+    pExplosion->yrepeat = pExpl->at0;
+    pExplosion->xrepeat = pExpl->at0;
+    pExplosion->lotag = nType;
+    pExplosion->cstat |= kSprInvisible | kSprOriginAlign;
+    pExplosion->owner = pSprite->xvel;
+
+    if (pExplosion->extra >= 0) {
+        xsprite[pExplosion->extra].target = 0;
+        xsprite[pExplosion->extra].data1 = pExpl->atf;
+        xsprite[pExplosion->extra].data2 = pExpl->at13;
+        xsprite[pExplosion->extra].data3 = pExpl->at17;
+
+
+        if (nType == 0) { nSeq = 3; nSnd = 303; pExplosion->z = pSprite->z; }
+        else if (nType == 2) { nSeq = 4; nSnd = 305; }
+        else if (nType == 3) { nSeq = 9; nSnd = 307; }
+        else if (nType == 4) { nSeq = 5; nSnd = 307; }
+        else if (nType <= 6) { nSeq = 4; nSnd = 303; }
+        else if (nType == 7) { nSeq = 4; nSnd = 303; }
+
+
+        if (fileExistsRFF(nSeq, "SEQ")) seqSpawn(nSeq, 3, pExplosion->extra, -1);
+        sfxPlay3DSound(pExplosion, nSnd, -1, 0);
+
+        return true;
+    }
+
+    return false;
+}
+
+
+void updateTargetOfSlaves(spritetype* pSprite) {
+    for (short nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        if (sprite[nSprite].owner != pSprite->xvel || sprite[nSprite].extra < 0 || !IsDudeSprite(&sprite[nSprite])) continue;
+        else if (xsprite[pSprite->extra].target != xsprite[sprite[nSprite].extra].target
+            && IsDudeSprite(&sprite[xsprite[pSprite->extra].target])) {
+            aiSetTarget(&xsprite[sprite[nSprite].extra], xsprite[pSprite->extra].target);
+        }
+
+        if (xsprite[sprite[nSprite].extra].target >= 0) {
+            // don't attack mates
+            if (sprite[xsprite[sprite[nSprite].extra].target].owner == sprite[nSprite].owner)
+                aiSetTarget(&xsprite[sprite[nSprite].extra], pSprite->x, pSprite->y, pSprite->z);
+        }
+
+        if (!isActive(sprite[nSprite].xvel) && xsprite[sprite[nSprite].extra].target >= 0)
+            aiActivateDude(&sprite[nSprite], &xsprite[sprite[nSprite].extra]);
+    }
+
+    return;
+}
+//////////
diff --git a/source/blood/src/aiunicult.h b/source/blood/src/aiunicult.h
new file mode 100644
index 000000000..da9a64284
--- /dev/null
+++ b/source/blood/src/aiunicult.h
@@ -0,0 +1,75 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+Copyright (C) NoOne
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+#include "eventq.h"
+
+extern AISTATE GDXGenDudeIdleL;
+extern AISTATE GDXGenDudeIdleW;
+extern AISTATE GDXGenDudeSearchL;
+extern AISTATE GDXGenDudeSearchW;
+extern AISTATE GDXGenDudeGotoL;
+extern AISTATE GDXGenDudeGotoW;
+extern AISTATE GDXGenDudeDodgeL;
+extern AISTATE GDXGenDudeDodgeD;
+extern AISTATE GDXGenDudeDodgeW;
+extern AISTATE GDXGenDudeDodgeDmgL;
+extern AISTATE GDXGenDudeDodgeDmgD;
+extern AISTATE GDXGenDudeDodgeDmgW;
+extern AISTATE GDXGenDudeChaseL;
+extern AISTATE GDXGenDudeChaseD;
+extern AISTATE GDXGenDudeChaseW;
+extern AISTATE GDXGenDudeFireL;
+extern AISTATE GDXGenDudeFireD;
+extern AISTATE GDXGenDudeFireW;
+extern AISTATE GDXGenDudeFire2L;
+extern AISTATE GDXGenDudeFire2D;
+extern AISTATE GDXGenDudeFire2W;
+extern AISTATE GDXGenDudeRecoilL;
+extern AISTATE GDXGenDudeRecoilD;
+extern AISTATE GDXGenDudeRecoilW;
+extern AISTATE GDGenDudeThrow;
+extern AISTATE GDGenDudeThrow2;
+extern AISTATE GDXGenDudePunch;
+extern AISTATE GDXGenDudeRTesla;
+extern AISTATE GDXGenDudeProne;
+extern AISTATE GDXGenDudeTurn;
+
+XSPRITE* getNextIncarnation(XSPRITE* pXSprite);
+void killDudeLeech(spritetype* pLeech);
+void removeLeech(spritetype* pLeech, bool delSprite = true);
+void removeDudeStuff(spritetype* pSprite);
+spritetype* leechIsDropped(spritetype* pSprite);
+bool spriteIsUnderwater(spritetype* pSprite, bool oldWay);
+bool sfxPlayGDXGenDudeSound(spritetype* pSprite, int mode, int data);
+void aiGenDudeMoveForward(spritetype* pSprite, XSPRITE* pXSprite);
+int getGenDudeMoveSpeed(spritetype* pSprite, int which, bool mul, bool shift);
+bool TargetNearThing(spritetype* pSprite, int thingType);
+int checkAttackState(spritetype* pSprite, XSPRITE* pXSprite);
+bool doExplosion(spritetype* pSprite, int nType);
+void dudeLeechOperate(spritetype* pSprite, XSPRITE* pXSprite, EVENT a3);
+int getDodgeChance(spritetype* pSprite);
+int getRecoilChance(spritetype* pSprite);
+bool dudeIsMelee(XSPRITE* pXSprite);
+void updateTargetOfSlaves(spritetype* pSprite);
\ No newline at end of file
diff --git a/source/blood/src/aizomba.cpp b/source/blood/src/aizomba.cpp
new file mode 100644
index 000000000..dab725a83
--- /dev/null
+++ b/source/blood/src/aizomba.cpp
@@ -0,0 +1,281 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aizomba.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void HackSeqCallback(int, int);
+static void StandSeqCallback(int, int);
+static void thinkSearch(spritetype *, XSPRITE *);
+static void thinkGoto(spritetype *, XSPRITE *);
+static void thinkChase(spritetype *, XSPRITE *);
+static void thinkPonder(spritetype *, XSPRITE *);
+static void myThinkTarget(spritetype *, XSPRITE *);
+static void myThinkSearch(spritetype *, XSPRITE *);
+static void entryEZombie(spritetype *, XSPRITE *);
+static void entryAIdle(spritetype *, XSPRITE *);
+static void entryEStand(spritetype *, XSPRITE *);
+
+static int nHackClient = seqRegisterClient(HackSeqCallback);
+static int nStandClient = seqRegisterClient(StandSeqCallback);
+
+AISTATE zombieAIdle = { kAiStateIdle, 0, -1, 0, entryAIdle, NULL, aiThinkTarget, NULL };
+AISTATE zombieAChase = { kAiStateChase, 8, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE zombieAPonder = { kAiStateOther, 0, -1, 0, NULL, aiMoveTurn, thinkPonder, NULL };
+AISTATE zombieAGoto = { kAiStateMove, 8, -1, 1800, NULL, aiMoveForward, thinkGoto, &zombieAIdle };
+AISTATE zombieAHack = { kAiStateChase, 6, nHackClient, 80, NULL, NULL, NULL, &zombieAPonder };
+AISTATE zombieASearch = { kAiStateSearch, 8, -1, 1800, NULL, aiMoveForward, thinkSearch, &zombieAIdle };
+AISTATE zombieARecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &zombieAPonder };
+AISTATE zombieATeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &zombieAPonder };
+AISTATE zombieARecoil2 = { kAiStateRecoil, 1, -1, 360, NULL, NULL, NULL, &zombieAStand };
+AISTATE zombieAStand = { kAiStateMove, 11, nStandClient, 0, NULL, NULL, NULL, &zombieAPonder };
+AISTATE zombieEIdle = { kAiStateIdle, 12, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE zombieEUp2 = { kAiStateMove, 0, -1, 1, entryEZombie, NULL, NULL, &zombieASearch };
+AISTATE zombieEUp = { kAiStateMove, 9, -1, 180, entryEStand, NULL, NULL, &zombieEUp2 };
+AISTATE zombie2Idle = { kAiStateIdle, 0, -1, 0, entryAIdle, NULL, myThinkTarget, NULL };
+AISTATE zombie2Search = { kAiStateSearch, 8, -1, 1800, NULL, NULL, myThinkSearch, &zombie2Idle };
+AISTATE zombieSIdle = { kAiStateIdle, 10, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE zombie13AC2C = { kAiStateOther, 11, nStandClient, 0, entryEZombie, NULL, NULL, &zombieAPonder };
+
+static void HackSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type-kDudeBase];
+    int tx = pXSprite->targetX-pSprite->x;
+    int ty = pXSprite->targetY-pSprite->y;
+    int UNUSED(nDist) = approxDist(tx, ty);
+    int nAngle = getangle(tx, ty);
+    int height = (pSprite->yrepeat*pDudeInfo->eyeHeight)<<2;
+    int height2 = (pTarget->yrepeat*pDudeInfoT->eyeHeight)<<2;
+    int dz = height-height2;
+    int dx = Cos(nAngle)>>16;
+    int dy = Sin(nAngle)>>16;
+    sfxPlay3DSound(pSprite, 1101, 1, 0);
+    actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_10);
+}
+
+static void StandSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    sfxPlay3DSound(&sprite[nSprite], 1102, -1, 0);
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    sub_5F15C(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 921 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &zombieASearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &zombieASearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &zombieASearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && (powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0 || powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 31) > 0))
+    {
+        aiNewState(pSprite, pXSprite, &zombieAGoto);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                    aiNewState(pSprite, pXSprite, &zombieAHack);
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &zombieAGoto);
+    pXSprite->target = -1;
+}
+
+static void thinkPonder(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &zombieASearch);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &zombieASearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && (powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0 || powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 31) > 0))
+    {
+        aiNewState(pSprite, pXSprite, &zombieAGoto);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x400)
+                {
+                    if (klabs(nDeltaAngle) < 85)
+                    {
+                        sfxPlay3DSound(pSprite, 1101, 1, 0);
+                        aiNewState(pSprite, pXSprite, &zombieAHack);
+                    }
+                    return;
+                }
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &zombieAChase);
+}
+
+static void myThinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+    {
+        PLAYER *pPlayer = &gPlayer[p];
+        int nOwner = (pSprite->owner & 0x1000) ? (pSprite->owner&0xfff) : -1;
+        if (nOwner == pPlayer->at5b || pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, 13) > 0)
+            continue;
+        int x = pPlayer->pSprite->x;
+        int y = pPlayer->pSprite->y;
+        int z = pPlayer->pSprite->z;
+        int nSector = pPlayer->pSprite->sectnum;
+        int dx = x-pSprite->x;
+        int dy = y-pSprite->y;
+        int nDist = approxDist(dx, dy);
+        if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
+            continue;
+        if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
+            continue;
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
+        {
+            aiSetTarget(pXSprite, pPlayer->at5b);
+            aiActivateDude(pSprite, pXSprite);
+        }
+        else if (nDist < pDudeInfo->hearDist)
+        {
+            aiSetTarget(pXSprite, x, y, z);
+            aiActivateDude(pSprite, pXSprite);
+        }
+        else
+            continue;
+        break;
+    }
+}
+
+static void myThinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    myThinkTarget(pSprite, pXSprite);
+}
+
+static void entryEZombie(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    UNREFERENCED_PARAMETER(pXSprite);
+    pSprite->type = 203;
+    pSprite->hitag |= 1;
+}
+
+static void entryAIdle(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    UNREFERENCED_PARAMETER(pSprite);
+    pXSprite->target = -1;
+}
+
+static void entryEStand(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    sfxPlay3DSound(pSprite, 1100, -1, 0);
+    pSprite->ang = getangle(pXSprite->targetX-pSprite->x, pXSprite->targetY-pSprite->y);
+}
diff --git a/source/blood/src/aizomba.h b/source/blood/src/aizomba.h
new file mode 100644
index 000000000..443553a87
--- /dev/null
+++ b/source/blood/src/aizomba.h
@@ -0,0 +1,42 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE zombieAIdle;
+extern AISTATE zombieAChase;
+extern AISTATE zombieAPonder;
+extern AISTATE zombieAGoto;
+extern AISTATE zombieAHack;
+extern AISTATE zombieASearch;
+extern AISTATE zombieARecoil;
+extern AISTATE zombieATeslaRecoil;
+extern AISTATE zombieARecoil2;
+extern AISTATE zombieAStand;
+extern AISTATE zombieEIdle;
+extern AISTATE zombieEUp2;
+extern AISTATE zombieEUp;
+extern AISTATE zombie2Idle;
+extern AISTATE zombie2Search;
+extern AISTATE zombieSIdle;
+extern AISTATE zombie13AC2C;
diff --git a/source/blood/src/aizombf.cpp b/source/blood/src/aizombf.cpp
new file mode 100644
index 000000000..d41b1604f
--- /dev/null
+++ b/source/blood/src/aizombf.cpp
@@ -0,0 +1,230 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "aizombf.h"
+#include "blood.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "trig.h"
+
+static void HackSeqCallback(int, int);
+static void PukeSeqCallback(int, int);
+static void ThrowSeqCallback(int, int);
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite);
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite);
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite);
+
+static int nHackClient = seqRegisterClient(HackSeqCallback);
+static int nPukeClient = seqRegisterClient(PukeSeqCallback);
+static int nThrowClient = seqRegisterClient(ThrowSeqCallback);
+
+AISTATE zombieFIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL };
+AISTATE zombieFChase = { kAiStateChase, 8, -1, 0, NULL, aiMoveForward, thinkChase, NULL };
+AISTATE zombieFGoto = { kAiStateMove, 8, -1, 600, NULL, aiMoveForward, thinkGoto, &zombieFIdle };
+AISTATE zombieFDodge = { kAiStateMove, 8, -1, 0, NULL, aiMoveDodge, thinkChase, &zombieFChase };
+AISTATE zombieFHack = { kAiStateChase, 6, nHackClient, 120, NULL, NULL, NULL, &zombieFChase };
+AISTATE zombieFPuke = { kAiStateChase, 9, nPukeClient, 120, NULL, NULL, NULL, &zombieFChase };
+AISTATE zombieFThrow = { kAiStateChase, 6, nThrowClient, 120, NULL, NULL, NULL, &zombieFChase };
+AISTATE zombieFSearch = { kAiStateSearch, 8, -1, 1800, NULL, aiMoveForward, thinkSearch, &zombieFIdle };
+AISTATE zombieFRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &zombieFChase };
+AISTATE zombieFTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &zombieFChase };
+
+static void HackSeqCallback(int, int nXSprite)
+{
+    if (nXSprite <= 0 || nXSprite >= kMaxXSprites)
+        return;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    if (nXSprite < 0 || nXSprite >= kMaxSprites)
+        return;
+    spritetype *pSprite = &sprite[nSprite];
+    if (pSprite->type != 204)
+        return;
+    spritetype *pTarget = &sprite[pXSprite->target];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int height = (pDudeInfo->eyeHeight*pSprite->yrepeat);
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type-kDudeBase];
+    int height2 = (pDudeInfoT->eyeHeight*pTarget->yrepeat);
+    actFireVector(pSprite, 0, 0, Cos(pSprite->ang)>>16, Sin(pSprite->ang)>>16, height-height2, VECTOR_TYPE_11);
+}
+
+static void PukeSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pTarget = &sprite[pXSprite->target];
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    DUDEINFO *pDudeInfoT = &dudeInfo[pTarget->type-kDudeBase];
+    int height = (pDudeInfo->eyeHeight*pSprite->yrepeat);
+    int height2 = (pDudeInfoT->eyeHeight*pTarget->yrepeat);
+    int tx = pXSprite->targetX-pSprite->x;
+    int ty = pXSprite->targetY-pSprite->y;
+    int UNUSED(nDist) = approxDist(tx, ty);
+    int nAngle = getangle(tx, ty);
+    int dx = Cos(nAngle)>>16;
+    int dy = Sin(nAngle)>>16;
+    sfxPlay3DSound(pSprite, 1203, 1, 0);
+    actFireMissile(pSprite, 0, -(height-height2), dx, dy, 0, 309);
+}
+
+static void ThrowSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    actFireMissile(pSprite, 0, -dudeInfo[pSprite->type-kDudeBase].eyeHeight, Cos(pSprite->ang)>>16, Sin(pSprite->ang)>>16, 0, 300);
+}
+
+static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    int dx = pXSprite->targetX-pSprite->x;
+    int dy = pXSprite->targetY-pSprite->y;
+    int nAngle = getangle(dx, dy);
+    int nDist = approxDist(dx, dy);
+    aiChooseDirection(pSprite, pXSprite, nAngle);
+    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
+        aiNewState(pSprite, pXSprite, &zombieFSearch);
+    aiThinkTarget(pSprite, pXSprite);
+}
+
+static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
+{
+    if (pXSprite->target == -1)
+    {
+        aiNewState(pSprite, pXSprite, &zombieFGoto);
+        return;
+    }
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type - kDudeBase];
+    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
+    spritetype *pTarget = &sprite[pXSprite->target];
+    XSPRITE *pXTarget = &xsprite[pTarget->extra];
+    int dx = pTarget->x-pSprite->x;
+    int dy = pTarget->y-pSprite->y;
+    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
+    if (pXTarget->health == 0)
+    {
+        aiNewState(pSprite, pXSprite, &zombieFSearch);
+        return;
+    }
+    if (IsPlayerSprite(pTarget) && (powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 13) > 0 || powerupCheck(&gPlayer[pTarget->type-kDudePlayer1], 31) > 0))
+    {
+        aiNewState(pSprite, pXSprite, &zombieFSearch);
+        return;
+    }
+    int nDist = approxDist(dx, dy);
+    if (nDist <= pDudeInfo->seeDist)
+    {
+        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
+        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
+        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
+        {
+            if (klabs(nDeltaAngle) <= pDudeInfo->periphery)
+            {
+                aiSetTarget(pXSprite, pXSprite->target);
+                if (nDist < 0x1400 && nDist > 0xe00 && klabs(nDeltaAngle) < 85)
+                {
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (hit)
+                    {
+                    case -1:
+                        aiNewState(pSprite, pXSprite, &zombieFThrow);
+                        break;
+                    case 3:
+                        if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                            aiNewState(pSprite, pXSprite, &zombieFThrow);
+                        else
+                            aiNewState(pSprite, pXSprite, &zombieFDodge);
+                        break;
+                    default:
+                        aiNewState(pSprite, pXSprite, &zombieFThrow);
+                        break;
+                    }
+                }
+                else if (nDist < 0x1400 && nDist > 0x600 && klabs(nDeltaAngle) < 85)
+                {
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (hit)
+                    {
+                    case -1:
+                        aiNewState(pSprite, pXSprite, &zombieFPuke);
+                        break;
+                    case 3:
+                        if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                            aiNewState(pSprite, pXSprite, &zombieFPuke);
+                        else
+                            aiNewState(pSprite, pXSprite, &zombieFDodge);
+                        break;
+                    default:
+                        aiNewState(pSprite, pXSprite, &zombieFPuke);
+                        break;
+                    }
+                }
+                else if (nDist < 0x400 && klabs(nDeltaAngle) < 85)
+                {
+                    int hit = HitScan(pSprite, pSprite->z, dx, dy, 0, CLIPMASK1, 0);
+                    switch (hit)
+                    {
+                    case -1:
+                        aiNewState(pSprite, pXSprite, &zombieFHack);
+                        break;
+                    case 3:
+                        if (pSprite->type != sprite[gHitInfo.hitsprite].type)
+                            aiNewState(pSprite, pXSprite, &zombieFHack);
+                        else
+                            aiNewState(pSprite, pXSprite, &zombieFDodge);
+                        break;
+                    default:
+                        aiNewState(pSprite, pXSprite, &zombieFHack);
+                        break;
+                    }
+                }
+                return;
+            }
+        }
+    }
+
+    aiNewState(pSprite, pXSprite, &zombieFSearch);
+    pXSprite->target = -1;
+}
diff --git a/source/blood/src/aizombf.h b/source/blood/src/aizombf.h
new file mode 100644
index 000000000..ba112cc93
--- /dev/null
+++ b/source/blood/src/aizombf.h
@@ -0,0 +1,35 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "ai.h"
+
+extern AISTATE zombieFIdle;
+extern AISTATE zombieFChase;
+extern AISTATE zombieFGoto;
+extern AISTATE zombieFDodge;
+extern AISTATE zombieFHack;
+extern AISTATE zombieFPuke;
+extern AISTATE zombieFThrow;
+extern AISTATE zombieFSearch;
+extern AISTATE zombieFRecoil;
+extern AISTATE zombieFTeslaRecoil;
diff --git a/source/blood/src/asound.cpp b/source/blood/src/asound.cpp
new file mode 100644
index 000000000..ccf86f932
--- /dev/null
+++ b/source/blood/src/asound.cpp
@@ -0,0 +1,158 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "fx_man.h"
+#include "common_game.h"
+#include "blood.h"
+#include "config.h"
+#include "db.h"
+#include "player.h"
+#include "resource.h"
+#include "sound.h"
+
+#define kMaxAmbChannel 64
+
+struct AMB_CHANNEL
+{
+    int at0;
+    int at4;
+    int at8;
+    DICTNODE *atc;
+    char *at10;
+    int at14;
+    int at18;
+};
+
+AMB_CHANNEL ambChannels[kMaxAmbChannel];
+int nAmbChannels = 0;
+
+void ambProcess(void)
+{
+    if (!SoundToggle)
+        return;
+    for (int nSprite = headspritestat[12]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->owner < 0 || pSprite->owner >= kMaxAmbChannel)
+            continue;
+        int nXSprite = pSprite->extra;
+        if (nXSprite > 0 && nXSprite < kMaxXSprites)
+        {
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (pXSprite->state)
+            {
+                int dx = pSprite->x-gMe->pSprite->x;
+                int dy = pSprite->y-gMe->pSprite->y;
+                int dz = pSprite->z-gMe->pSprite->z;
+                dx >>= 4;
+                dy >>= 4;
+                dz >>= 8;
+                int nDist = ksqrt(dx*dx+dy*dy+dz*dz);
+                int vs = mulscale16(pXSprite->data4, pXSprite->busy);
+                ambChannels[pSprite->owner].at4 += ClipRange(scale(nDist, pXSprite->data1, pXSprite->data2, vs, 0), 0, vs);
+            }
+        }
+    }
+    AMB_CHANNEL *pChannel = ambChannels;
+    for (int i = 0; i < nAmbChannels; i++, pChannel++)
+    {
+        if (pChannel->at0 > 0)
+            FX_SetPan(pChannel->at0, pChannel->at4, pChannel->at4, pChannel->at4);
+        else
+        {
+            int end = ClipLow(pChannel->at14-1, 0);
+            pChannel->at0 = FX_PlayLoopedRaw(pChannel->at10, pChannel->at14, pChannel->at10, pChannel->at10+end, sndGetRate(pChannel->at18), 0,
+                pChannel->at4, pChannel->at4, pChannel->at4, pChannel->at4, 1.f, (intptr_t)&pChannel->at0);
+        }
+        pChannel->at4 = 0;
+    }
+}
+
+void ambKillAll(void)
+{
+    AMB_CHANNEL *pChannel = ambChannels;
+    for (int i = 0; i < nAmbChannels; i++, pChannel++)
+    {
+        if (pChannel->at0 > 0)
+        {
+            FX_EndLooping(pChannel->at0);
+            FX_StopSound(pChannel->at0);
+        }
+        if (pChannel->atc)
+        {
+            gSoundRes.Unlock(pChannel->atc);
+            pChannel->atc = NULL;
+        }
+    }
+    nAmbChannels = 0;
+}
+
+void ambInit(void)
+{
+    ambKillAll();
+    memset(ambChannels, 0, sizeof(ambChannels));
+    for (int nSprite = headspritestat[12]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        int nXSprite = pSprite->extra;
+        if (nXSprite > 0 && nXSprite < kMaxXSprites)
+        {
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (pXSprite->data1 < pXSprite->data2)
+            {
+                int i;
+                AMB_CHANNEL *pChannel = ambChannels;
+                for (i = 0; i < nAmbChannels; i++, pChannel++)
+                    if (pXSprite->data3 == pChannel->at8)
+                        break;
+                if (i == nAmbChannels)
+                {
+                    if (i >= kMaxAmbChannel)
+                    {
+                        pSprite->owner = -1;
+                        continue;
+                    }
+                    int nSFX = pXSprite->data3;
+                    DICTNODE *pSFXNode = gSoundRes.Lookup(nSFX, "SFX");
+                    if (!pSFXNode)
+                        ThrowError("Missing sound #%d used in ambient sound generator %d\n", nSFX);
+                    SFX *pSFX = (SFX*)gSoundRes.Load(pSFXNode);
+                    DICTNODE *pRAWNode = gSoundRes.Lookup(pSFX->rawName, "RAW");
+                    if (!pRAWNode)
+                        ThrowError("Missing RAW sound \"%s\" used in ambient sound generator %d\n", pSFX->rawName, nSFX);
+                    if (pRAWNode->size > 0)
+                    {
+                        pChannel->at14 = pRAWNode->size;
+                        pChannel->at8 = nSFX;
+                        pChannel->atc = pRAWNode;
+                        pChannel->at14 = pRAWNode->size;
+                        pChannel->at10 = (char*)gSoundRes.Lock(pRAWNode);
+                        pChannel->at18 = pSFX->format;
+                        nAmbChannels++;
+                    }
+                }
+                pSprite->owner = i;
+            }
+        }
+    }
+}
diff --git a/source/blood/src/asound.h b/source/blood/src/asound.h
new file mode 100644
index 000000000..a442dcbda
--- /dev/null
+++ b/source/blood/src/asound.h
@@ -0,0 +1,27 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+void ambProcess(void);
+void ambKillAll(void);
+void ambInit(void);
\ No newline at end of file
diff --git a/source/blood/src/blood.cpp b/source/blood/src/blood.cpp
new file mode 100644
index 000000000..c1d0f7e60
--- /dev/null
+++ b/source/blood/src/blood.cpp
@@ -0,0 +1,2438 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "mmulti.h"
+#include "compat.h"
+#include "renderlayer.h"
+#include "fx_man.h"
+#include "common.h"
+#include "common_game.h"
+#include "gamedefs.h"
+
+#include "asound.h"
+#include "db.h"
+#include "blood.h"
+#include "choke.h"
+#include "config.h"
+#include "controls.h"
+#include "credits.h"
+#include "demo.h"
+#include "dude.h"
+#include "endgame.h"
+#include "eventq.h"
+#include "fire.h"
+#include "fx.h"
+#include "getopt.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "menu.h"
+#include "mirrors.h"
+#include "music.h"
+#include "network.h"
+#include "osdcmds.h"
+#include "replace.h"
+#include "resource.h"
+#include "qheap.h"
+#include "screen.h"
+#include "sectorfx.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "tile.h"
+#include "trig.h"
+#include "triggers.h"
+#include "view.h"
+#include "warp.h"
+#include "weapon.h"
+
+#ifdef _WIN32
+# include <shellapi.h>
+# define UPDATEINTERVAL 604800 // 1w
+# include "winbits.h"
+#else
+# ifndef GEKKO
+#  include <sys/ioctl.h>
+# endif
+#endif /* _WIN32 */
+
+const char* AppProperName = APPNAME;
+const char* AppTechnicalName = APPBASENAME;
+
+char qsprite_filler[kMaxSprites], qsector_filler[kMaxSectors];
+
+ud_setup_t gSetup;
+char SetupFilename[BMAX_PATH] = SETUPFILENAME;
+int32_t gNoSetup = 0, gCommandSetup = 0;
+
+Resource gSysRes, gGuiRes;
+
+INPUT_MODE gInputMode;
+
+unsigned int nMaxAlloc = 0x4000000;
+
+bool bCustomName = false;
+char bAddUserMap = false;
+bool bNoDemo = false;
+bool bQuickStart = true;
+bool bNoAutoLoad = false;
+
+bool bVanilla = false;
+
+int gMusicPrevLoadedEpisode = -1;
+int gMusicPrevLoadedLevel = -1;
+
+char gUserMapFilename[BMAX_PATH];
+char gPName[MAXPLAYERNAME];
+
+short BloodVersion = 0x115;
+
+int gNetPlayers;
+
+char *pUserTiles = NULL;
+char *pUserSoundRFF = NULL;
+char *pUserRFF = NULL;
+
+int gChokeCounter = 0;
+
+double g_gameUpdateTime, g_gameUpdateAndDrawTime;
+double g_gameUpdateAvgTime = 0.001;
+
+enum gametokens
+{
+    T_INCLUDE = 0,
+    T_INTERFACE = 0,
+    T_LOADGRP = 1,
+    T_MODE = 1,
+    T_CACHESIZE = 2,
+    T_ALLOW = 2,
+    T_NOAUTOLOAD,
+    T_INCLUDEDEFAULT,
+    T_MUSIC,
+    T_SOUND,
+    T_FILE,
+    //T_CUTSCENE,
+    //T_ANIMSOUNDS,
+    //T_NOFLOORPALRANGE,
+    T_ID,
+    T_MINPITCH,
+    T_MAXPITCH,
+    T_PRIORITY,
+    T_TYPE,
+    T_DISTANCE,
+    T_VOLUME,
+    T_DELAY,
+    T_RENAMEFILE,
+    T_GLOBALGAMEFLAGS,
+    T_ASPECT,
+    T_FORCEFILTER,
+    T_FORCENOFILTER,
+    T_TEXTUREFILTER,
+    T_RFFDEFINEID,
+};
+
+int blood_globalflags;
+
+void app_crashhandler(void)
+{
+    // NUKE-TODO:
+}
+
+void G_Polymer_UnInit(void)
+{
+    // NUKE-TODO:
+}
+
+void M32RunScript(const char *s)
+{
+    UNREFERENCED_PARAMETER(s);
+}
+
+static const char *_module;
+static int _line;
+
+void _SetErrorLoc(const char *pzFile, int nLine)
+{
+    _module = pzFile;
+    _line = nLine;
+}
+
+void _ThrowError(const char *pzFormat, ...)
+{
+    char buffer[256];
+    va_list args;
+    va_start(args, pzFormat);
+    vsprintf(buffer, pzFormat, args);
+    initprintf("%s(%i): %s\n", _module, _line, buffer);
+
+    char titlebuf[256];
+    Bsprintf(titlebuf, APPNAME " %s", s_buildRev);
+    wm_msgbox(titlebuf, "%s(%i): %s\n", _module, _line, buffer);
+
+    Bfflush(NULL);
+    QuitGame();
+}
+
+void __dassert(const char * pzExpr, const char * pzFile, int nLine)
+{
+    initprintf("Assertion failed: %s in file %s at line %i\n", pzExpr, pzFile, nLine);
+
+    char titlebuf[256];
+    Bsprintf(titlebuf, APPNAME " %s", s_buildRev);
+    wm_msgbox(titlebuf, "Assertion failed: %s in file %s at line %i\n", pzExpr, pzFile, nLine);
+
+    Bfflush(NULL);
+    exit(0);
+}
+
+void ShutDown(void)
+{
+    if (!in3dmode())
+        return;
+    CONFIG_WriteSetup(0);
+    netDeinitialize();
+    sndTerm();
+    sfxTerm();
+    scrUnInit();
+    CONTROL_Shutdown();
+    KB_Shutdown();
+    OSD_Cleanup();
+    // PORT_TODO: Check argument
+    if (syncstate)
+        printf("A packet was lost! (syncstate)\n");
+    for (int i = 0; i < 10; i++)
+    {
+        if (gSaveGamePic[i])
+            Resource::Free(gSaveGamePic[i]);
+    }
+    DO_FREE_AND_NULL(pUserTiles);
+    DO_FREE_AND_NULL(pUserSoundRFF);
+    DO_FREE_AND_NULL(pUserRFF);
+}
+
+void QuitGame(void)
+{
+    ShutDown();
+    exit(0);
+}
+
+int nPrecacheCount;
+
+void PrecacheDude(spritetype *pSprite)
+{
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    seqPrecacheId(pDudeInfo->seqStartID);
+    seqPrecacheId(pDudeInfo->seqStartID+5);
+    seqPrecacheId(pDudeInfo->seqStartID+1);
+    seqPrecacheId(pDudeInfo->seqStartID+2);
+    switch (pSprite->type)
+    {
+    case 201:
+    case 202:
+    case 247:
+    case 248:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        seqPrecacheId(pDudeInfo->seqStartID+8);
+        seqPrecacheId(pDudeInfo->seqStartID+9);
+        seqPrecacheId(pDudeInfo->seqStartID+13);
+        seqPrecacheId(pDudeInfo->seqStartID+14);
+        seqPrecacheId(pDudeInfo->seqStartID+15);
+        break;
+    case 204:
+    case 217:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        seqPrecacheId(pDudeInfo->seqStartID+8);
+        seqPrecacheId(pDudeInfo->seqStartID+9);
+        seqPrecacheId(pDudeInfo->seqStartID+10);
+        seqPrecacheId(pDudeInfo->seqStartID+11);
+        break;
+    case 208:
+    case 209:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        fallthrough__;
+    case 206:
+    case 207:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        seqPrecacheId(pDudeInfo->seqStartID+8);
+        seqPrecacheId(pDudeInfo->seqStartID+9);
+        break;
+    case 210:
+    case 211:
+    case 213:
+    case 214:
+    case 215:
+    case 216:
+    case 229:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        seqPrecacheId(pDudeInfo->seqStartID+8);
+        break;
+    case 227:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        fallthrough__;
+    case 212:
+    case 218:
+    case 219:
+    case 220:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        break;
+    case 249:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        break;
+    case 205:
+        seqPrecacheId(pDudeInfo->seqStartID+12);
+        seqPrecacheId(pDudeInfo->seqStartID+9);
+        fallthrough__;
+    case 244:
+        seqPrecacheId(pDudeInfo->seqStartID+10);
+        fallthrough__;
+    case 203:
+        seqPrecacheId(pDudeInfo->seqStartID+6);
+        seqPrecacheId(pDudeInfo->seqStartID+7);
+        seqPrecacheId(pDudeInfo->seqStartID+8);
+        seqPrecacheId(pDudeInfo->seqStartID+11);
+        seqPrecacheId(pDudeInfo->seqStartID+13);
+        seqPrecacheId(pDudeInfo->seqStartID+14);
+        break;
+    }
+}
+
+void PrecacheThing(spritetype *pSprite)
+{
+    switch (pSprite->type)
+    {
+    case 406:
+    case 407:
+        seqPrecacheId(12);
+        break;
+    case 410:
+        seqPrecacheId(15);
+        break;
+    case 411:
+        seqPrecacheId(21);
+        break;
+    case 412:
+        seqPrecacheId(25);
+        seqPrecacheId(26);
+        break;
+    case 413:
+        seqPrecacheId(38);
+        seqPrecacheId(40);
+        seqPrecacheId(28);
+        break;
+    case 416:
+        break;
+    default:
+        tilePreloadTile(pSprite->picnum);
+        break;
+    }
+    seqPrecacheId(3);
+    seqPrecacheId(4);
+    seqPrecacheId(5);
+    seqPrecacheId(9);
+}
+
+void PreloadTiles(void)
+{
+    int skyTile = -1;
+    memset(gotpic,0,sizeof(gotpic));
+    for (int i = 0; i < numsectors; i++)
+    {
+        tilePrecacheTile(sector[i].floorpicnum, 0);
+        tilePrecacheTile(sector[i].ceilingpicnum, 0);
+        if ((sector[i].ceilingstat&1) != 0 && skyTile == -1)
+            skyTile = sector[i].ceilingpicnum;
+    }
+    for (int i = 0; i < numwalls; i++)
+    {
+        tilePrecacheTile(wall[i].picnum, 0);
+        if (wall[i].overpicnum >= 0)
+            tilePrecacheTile(wall[i].overpicnum, 0);
+    }
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        if (sprite[i].statnum < kMaxStatus)
+        {
+            spritetype *pSprite = &sprite[i];
+            switch (pSprite->statnum)
+            {
+            case 6:
+                PrecacheDude(pSprite);
+                break;
+            case 4:
+                PrecacheThing(pSprite);
+                break;
+            default:
+                tilePrecacheTile(pSprite->picnum);
+                break;
+            }
+        }
+    }
+    if (numplayers > 1)
+    {
+        seqPrecacheId(dudeInfo[31].seqStartID+6);
+        seqPrecacheId(dudeInfo[31].seqStartID+7);
+        seqPrecacheId(dudeInfo[31].seqStartID+8);
+        seqPrecacheId(dudeInfo[31].seqStartID+9);
+        seqPrecacheId(dudeInfo[31].seqStartID+10);
+        seqPrecacheId(dudeInfo[31].seqStartID+14);
+        seqPrecacheId(dudeInfo[31].seqStartID+15);
+        seqPrecacheId(dudeInfo[31].seqStartID+12);
+        seqPrecacheId(dudeInfo[31].seqStartID+16);
+        seqPrecacheId(dudeInfo[31].seqStartID+17);
+        seqPrecacheId(dudeInfo[31].seqStartID+18);
+    }
+    if (skyTile > -1 && skyTile < kMaxTiles)
+    {
+        for (int i = 1; i < gSkyCount; i++)
+            tilePrecacheTile(skyTile+i, 0);
+    }
+    G_HandleAsync();
+}
+
+char precachehightile[2][(MAXTILES+7)>>3];
+#ifdef USE_OPENGL
+void PrecacheExtraTextureMaps(int nTile)
+{
+    // PRECACHE
+    if (useprecache && bpp > 8)
+    {
+        for (int type = 0; type < 2 && !KB_KeyPressed(sc_Space); type++)
+        {
+            if (TestBitString(precachehightile[type], nTile))
+            {
+                for (int k = 0; k < MAXPALOOKUPS - RESERVEDPALS && !KB_KeyPressed(sc_Space); k++)
+                {
+                    // this is the CROSSHAIR_PAL, see screens.cpp
+                    if (k == MAXPALOOKUPS - RESERVEDPALS - 1)
+                        break;
+#ifdef POLYMER
+                    if (videoGetRenderMode() != REND_POLYMER || !polymer_havehighpalookup(0, k))
+#endif
+                        polymost_precache(nTile, k, type);
+                }
+
+#ifdef USE_GLEXT
+                if (r_detailmapping)
+                    polymost_precache(nTile, DETAILPAL, type);
+
+                if (r_glowmapping)
+                    polymost_precache(nTile, GLOWPAL, type);
+#endif
+#ifdef POLYMER
+                if (videoGetRenderMode() == REND_POLYMER)
+                {
+                    if (pr_specularmapping)
+                        polymost_precache(nTile, SPECULARPAL, type);
+
+                    if (pr_normalmapping)
+                        polymost_precache(nTile, NORMALPAL, type);
+                }
+#endif
+            }
+        }
+    }
+}
+#endif
+
+void PreloadCache(void)
+{
+    char tempbuf[128];
+    if (gDemo.at1)
+        return;
+    if (MusicRestartsOnLoadToggle)
+        sndTryPlaySpecialMusic(MUS_LOADING);
+    gSoundRes.PrecacheSounds();
+    PreloadTiles();
+    int clock = totalclock;
+    int cnt = 0;
+    int percentDisplayed = -1;
+
+    for (int i=0; i<kMaxTiles && !KB_KeyPressed(sc_Space); i++)
+    {
+        if (TestBitString(gotpic, i))
+        {
+            if (waloff[i] == 0)
+                tileLoad((int16_t)i);
+
+#ifdef USE_OPENGL
+            PrecacheExtraTextureMaps(i);
+#endif
+
+            MUSIC_Update();
+
+            if ((++cnt & 7) == 0)
+                G_HandleAsync();
+
+            if (videoGetRenderMode() != REND_CLASSIC && totalclock - clock > (kTicRate>>2))
+            {
+                int const percentComplete = min(100, tabledivide32_noinline(100 * cnt, nPrecacheCount));
+
+                // this just prevents the loading screen percentage bar from making large jumps
+                while (percentDisplayed < percentComplete)
+                {
+                    Bsprintf(tempbuf, "Loaded %d%% (%d/%d textures)\n", percentDisplayed, cnt, nPrecacheCount);
+                    viewLoadingScreenUpdate(tempbuf, percentDisplayed);
+                    timerUpdate();
+
+                    if (totalclock - clock >= 1)
+                    {
+                        clock = totalclock;
+                        percentDisplayed++;
+                    }
+                }
+
+                clock = totalclock;
+            }
+        }
+    }
+    memset(gotpic,0,sizeof(gotpic));
+}
+
+void EndLevel(void)
+{
+    gViewPos = VIEWPOS_0;
+    gGameMessageMgr.Clear();
+    sndKillAllSounds();
+    sfxKillAllSounds();
+    ambKillAll();
+    seqKillAll();
+}
+
+PLAYER gPlayerTemp[kMaxPlayers];
+int gHealthTemp[kMaxPlayers];
+
+vec3_t startpos;
+int16_t startang, startsectnum;
+
+void StartLevel(GAMEOPTIONS *gameOptions)
+{
+    EndLevel();
+    gStartNewGame = 0;
+    ready2send = 0;
+    gMusicPrevLoadedEpisode = gGameOptions.nEpisode;
+    gMusicPrevLoadedLevel = gGameOptions.nLevel;
+    if (gDemo.at0 && gGameStarted)
+        gDemo.Close();
+    netWaitForEveryone(0);
+    if (gGameOptions.nGameType == 0)
+    {
+        if (!(gGameOptions.uGameFlags&1))
+            levelSetupOptions(gGameOptions.nEpisode, gGameOptions.nLevel);
+        if (gEpisodeInfo[gGameOptions.nEpisode].cutALevel == gGameOptions.nLevel
+            && gEpisodeInfo[gGameOptions.nEpisode].at8f08)
+            gGameOptions.uGameFlags |= 4;
+        if ((gGameOptions.uGameFlags&4) && gDemo.at1 == 0)
+            levelPlayIntroScene(gGameOptions.nEpisode);
+    }
+    else if (gGameOptions.nGameType > 0 && !(gGameOptions.uGameFlags&1))
+    {
+        gGameOptions.nEpisode = gPacketStartGame.episodeId;
+        gGameOptions.nLevel = gPacketStartGame.levelId;
+        gGameOptions.nGameType = gPacketStartGame.gameType;
+        gGameOptions.nDifficulty = gPacketStartGame.difficulty;
+        gGameOptions.nMonsterSettings = gPacketStartGame.monsterSettings;
+        gGameOptions.nWeaponSettings = gPacketStartGame.weaponSettings;
+        gGameOptions.nItemSettings = gPacketStartGame.itemSettings;
+        gGameOptions.nRespawnSettings = gPacketStartGame.respawnSettings;
+        if (gPacketStartGame.userMap)
+            levelAddUserMap(gPacketStartGame.userMapName);
+        else
+            levelSetupOptions(gGameOptions.nEpisode, gGameOptions.nLevel);
+    }
+    if (gameOptions->uGameFlags&1)
+    {
+        for (int i = connecthead; i >= 0; i = connectpoint2[i])
+        {
+            memcpy(&gPlayerTemp[i],&gPlayer[i],sizeof(PLAYER));
+            gHealthTemp[i] = xsprite[gPlayer[i].pSprite->extra].health;
+        }
+    }
+    bVanilla = gDemo.at1 && gDemo.m_bLegacy;
+    //blooddemohack = 1;//bVanilla;
+    memset(xsprite,0,sizeof(xsprite));
+    memset(sprite,0,kMaxSprites*sizeof(spritetype));
+    drawLoadingScreen();
+    dbLoadMap(gameOptions->zLevelName,(int*)&startpos.x,(int*)&startpos.y,(int*)&startpos.z,&startang,&startsectnum,(unsigned int*)&gameOptions->uMapCRC);
+    wsrand(gameOptions->uMapCRC);
+    gKillMgr.Clear();
+    gSecretMgr.Clear();
+    gLevelTime = 0;
+    automapping = 1;
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        spritetype *pSprite = &sprite[i];
+        if (pSprite->statnum < kMaxStatus && pSprite->extra > 0)
+        {
+            XSPRITE *pXSprite = &xsprite[pSprite->extra];
+            if ((pXSprite->lSkill & (1 << gameOptions->nDifficulty)) || (pXSprite->lS && gameOptions->nGameType == 0)
+                || (pXSprite->lB && gameOptions->nGameType == 2) || (pXSprite->lT && gameOptions->nGameType == 3)
+                || (pXSprite->lC && gameOptions->nGameType == 1)) {
+                
+                DeleteSprite(i);
+                continue;
+            }
+
+            if (sprite[i].lotag == kGDXDudeTargetChanger)
+                InsertSpriteStat(i, kStatGDXDudeTargetChanger);
+        }
+    }
+    scrLoadPLUs();
+    startpos.z = getflorzofslope(startsectnum,startpos.x,startpos.y);
+    for (int i = 0; i < kMaxPlayers; i++)
+    {
+        gStartZone[i].x = startpos.x;
+        gStartZone[i].y = startpos.y;
+        gStartZone[i].z = startpos.z;
+        gStartZone[i].sectnum = startsectnum;
+        gStartZone[i].ang = startang;
+
+        // By NoOne: Create spawn zones for players in teams mode.
+        if (i <= kMaxPlayers / 2) {
+            gStartZoneTeam1[i].x = startpos.x;
+            gStartZoneTeam1[i].y = startpos.y;
+            gStartZoneTeam1[i].z = startpos.z;
+            gStartZoneTeam1[i].sectnum = startsectnum;
+            gStartZoneTeam1[i].ang = startang;
+
+            gStartZoneTeam2[i].x = startpos.x;
+            gStartZoneTeam2[i].y = startpos.y;
+            gStartZoneTeam2[i].z = startpos.z;
+            gStartZoneTeam2[i].sectnum = startsectnum;
+            gStartZoneTeam2[i].ang = startang;
+        }
+    }
+    InitSectorFX();
+    warpInit();
+    actInit();
+    evInit();
+    for (int i = connecthead; i >= 0; i = connectpoint2[i])
+    {
+        if (!(gameOptions->uGameFlags&1))
+        {
+            if (numplayers == 1)
+            {
+                gProfile[i].skill = gSkill;
+                gProfile[i].nAutoAim = gAutoAim;
+                gProfile[i].nWeaponSwitch = gWeaponSwitch;
+            }
+            playerInit(i,0);
+        }
+        playerStart(i);
+    }
+    if (gameOptions->uGameFlags&1)
+    {
+        for (int i = connecthead; i >= 0; i = connectpoint2[i])
+        {
+            PLAYER *pPlayer = &gPlayer[i];
+            pPlayer->pXSprite->health &= 0xf000;
+            pPlayer->pXSprite->health |= gHealthTemp[i];
+            pPlayer->at26 = gPlayerTemp[i].at26;
+            pPlayer->atbd = gPlayerTemp[i].atbd;
+            pPlayer->atc3 = gPlayerTemp[i].atc3;
+            pPlayer->atc7 = gPlayerTemp[i].atc7;
+            pPlayer->at2a = gPlayerTemp[i].at2a;
+            pPlayer->at1b1 = gPlayerTemp[i].at1b1;
+            pPlayer->atbf = gPlayerTemp[i].atbf;
+            pPlayer->atbe = gPlayerTemp[i].atbe;
+        }
+    }
+    gameOptions->uGameFlags &= ~3;
+    scrSetDac();
+    PreloadCache();
+    InitMirrors();
+    gFrameClock = 0;
+    trInit();
+    if (!bVanilla && !gMe->packInfo[1].at0) // if diving suit is not active, turn off reverb sound effect
+        sfxSetReverb(0);
+    ambInit();
+    sub_79760();
+    gCacheMiss = 0;
+    gFrame = 0;
+    gChokeCounter = 0;
+    if (!gDemo.at1)
+        gGameMenuMgr.Deactivate();
+    levelTryPlayMusicOrNothing(gGameOptions.nEpisode, gGameOptions.nLevel);
+    // viewSetMessage("");
+    viewSetErrorMessage("");
+    viewResizeView(gViewSize);
+    if (gGameOptions.nGameType == 3)
+        gGameMessageMgr.SetCoordinates(gViewX0S+1,gViewY0S+15);
+    netWaitForEveryone(0);
+    gGameClock = 0;
+    gPaused = 0;
+    gGameStarted = 1;
+    ready2send = 1;
+}
+
+void StartNetworkLevel(void)
+{
+    if (gDemo.at0)
+        gDemo.Close();
+    if (!(gGameOptions.uGameFlags&1))
+    {
+        gGameOptions.nEpisode = gPacketStartGame.episodeId;
+        gGameOptions.nLevel = gPacketStartGame.levelId;
+        gGameOptions.nGameType = gPacketStartGame.gameType;
+        gGameOptions.nDifficulty = gPacketStartGame.difficulty;
+        gGameOptions.nMonsterSettings = gPacketStartGame.monsterSettings;
+        gGameOptions.nWeaponSettings = gPacketStartGame.weaponSettings;
+        gGameOptions.nItemSettings = gPacketStartGame.itemSettings;
+        gGameOptions.nRespawnSettings = gPacketStartGame.respawnSettings;
+        if (gPacketStartGame.userMap)
+            levelAddUserMap(gPacketStartGame.userMapName);
+        else
+            levelSetupOptions(gGameOptions.nEpisode, gGameOptions.nLevel);
+    }
+    StartLevel(&gGameOptions);
+}
+
+void LocalKeys(void)
+{
+    char alt = keystatus[sc_LeftAlt] | keystatus[sc_RightAlt];
+    char ctrl = keystatus[sc_LeftControl] | keystatus[sc_RightControl];
+    char shift = keystatus[sc_LeftShift] | keystatus[sc_RightShift];
+    if (BUTTON(gamefunc_See_Chase_View) && !alt && !shift)
+    {
+        CONTROL_ClearButton(gamefunc_See_Chase_View);
+        if (gViewPos > VIEWPOS_0)
+            gViewPos = VIEWPOS_0;
+        else
+            gViewPos = VIEWPOS_1;
+    }
+    if (BUTTON(gamefunc_See_Coop_View))
+    {
+        CONTROL_ClearButton(gamefunc_See_Coop_View);
+        if (gGameOptions.nGameType == 1)
+        {
+            gViewIndex = connectpoint2[gViewIndex];
+            if (gViewIndex == -1)
+                gViewIndex = connecthead;
+            gView = &gPlayer[gViewIndex];
+        }
+        else if (gGameOptions.nGameType == 3)
+        {
+            int oldViewIndex = gViewIndex;
+            do
+            {
+                gViewIndex = connectpoint2[gViewIndex];
+                if (gViewIndex == -1)
+                    gViewIndex = connecthead;
+                if (oldViewIndex == gViewIndex || gMe->at2ea == gPlayer[gViewIndex].at2ea)
+                    break;
+            } while (oldViewIndex != gViewIndex);
+            gView = &gPlayer[gViewIndex];
+        }
+    }
+    char key;
+    if ((key = keyGetScan()) != 0)
+    {
+        if ((alt || shift) && gGameOptions.nGameType > 0 && key >= 0x3b && key <= 0x44)
+        {
+            char fk = key - 0x3b;
+            if (alt)
+            {
+                netBroadcastTaunt(myconnectindex, fk);
+            }
+            else
+            {
+                gPlayerMsg.Set(CommbatMacro[fk]);
+                gPlayerMsg.Send();
+            }
+            keyFlushScans();
+            keystatus[key] = 0;
+            CONTROL_ClearButton(41);
+            return;
+        }
+        switch (key)
+        {
+        case 0x53:
+        case 0xd3:
+            if (ctrl && alt)
+            {
+                gQuitGame = 1;
+                return;
+            }
+            break;
+        case 0x01:
+            keyFlushScans();
+            if (gGameStarted && gPlayer[myconnectindex].pXSprite->health != 0)
+            {
+                if (!gGameMenuMgr.m_bActive)
+                    gGameMenuMgr.Push(&menuMainWithSave,-1);
+            }
+            else
+            {
+                if (!gGameMenuMgr.m_bActive)
+                    gGameMenuMgr.Push(&menuMain,-1);
+            }
+            return;
+        case 0x3b:
+            keyFlushScans();
+            if (gGameOptions.nGameType == 0)
+                gGameMenuMgr.Push(&menuOrder,-1);
+            break;
+        case 0x3c:
+            keyFlushScans();
+            if (!gGameMenuMgr.m_bActive && gGameOptions.nGameType == 0)
+                gGameMenuMgr.Push(&menuSaveGame,-1);
+            break;
+        case 0x3d:
+            keyFlushScans();
+            if (!gGameMenuMgr.m_bActive && gGameOptions.nGameType == 0)
+                gGameMenuMgr.Push(&menuLoadGame,-1);
+            break;
+        case 0x3e:
+            keyFlushScans();
+            if (!gGameMenuMgr.m_bActive)
+                gGameMenuMgr.Push(&menuOptionsSound,-1);
+            return;
+        case 0x3f:
+            keyFlushScans();
+            if (!gGameMenuMgr.m_bActive)
+                gGameMenuMgr.Push(&menuOptions,-1);
+            return;
+        case 0x40:
+            keyFlushScans();
+            if (gGameStarted && !gGameMenuMgr.m_bActive && gPlayer[myconnectindex].pXSprite->health != 0)
+            {
+                if (gQuickSaveSlot != -1)
+                {
+                    QuickSaveGame();
+                    return;
+                }
+                gGameMenuMgr.Push(&menuSaveGame,-1);
+            }
+            break;
+        case 0x42:
+            keyFlushScans();
+            gGameMenuMgr.Push(&menuOptions,-1);
+            break;
+        case 0x43:
+            keyFlushScans();
+            if (!gGameMenuMgr.m_bActive)
+            {
+                if (gQuickLoadSlot != -1)
+                {
+                    QuickLoadGame();
+                    return;
+                }
+                if (gQuickLoadSlot == -1 && gQuickSaveSlot != -1)
+                {
+                    gQuickLoadSlot = gQuickSaveSlot;
+                    QuickLoadGame();
+                    return;
+                }
+                gGameMenuMgr.Push(&menuLoadGame,-1);
+            }
+            break;
+        case 0x44:
+            keyFlushScans();
+            if (!gGameMenuMgr.m_bActive)
+                gGameMenuMgr.Push(&menuQuit,-1);
+            break;
+        case 0x57:
+            break;
+        case 0x58:
+            videoCaptureScreen("blud0000.tga", 0);
+            break;
+        }
+    }
+}
+
+bool gRestartGame = false;
+
+void ProcessFrame(void)
+{
+    char buffer[128];
+    for (int i = connecthead; i >= 0; i = connectpoint2[i])
+    {
+        gPlayer[i].atc.buttonFlags = gFifoInput[gNetFifoTail&255][i].buttonFlags;
+        gPlayer[i].atc.keyFlags.word |= gFifoInput[gNetFifoTail&255][i].keyFlags.word;
+        gPlayer[i].atc.useFlags.byte |= gFifoInput[gNetFifoTail&255][i].useFlags.byte;
+        if (gFifoInput[gNetFifoTail&255][i].newWeapon)
+            gPlayer[i].atc.newWeapon = gFifoInput[gNetFifoTail&255][i].newWeapon;
+        gPlayer[i].atc.forward = gFifoInput[gNetFifoTail&255][i].forward;
+        gPlayer[i].atc.q16turn = gFifoInput[gNetFifoTail&255][i].q16turn;
+        gPlayer[i].atc.strafe = gFifoInput[gNetFifoTail&255][i].strafe;
+        gPlayer[i].atc.q16mlook = gFifoInput[gNetFifoTail&255][i].q16mlook;
+    }
+    gNetFifoTail++;
+    if (!(gFrame&((gSyncRate<<3)-1)))
+    {
+        CalcGameChecksum();
+        memcpy(gCheckFifo[gCheckHead[myconnectindex]&255][myconnectindex], gChecksum, sizeof(gChecksum));
+        gCheckHead[myconnectindex]++;
+    }
+    for (int i = connecthead; i >= 0; i = connectpoint2[i])
+    {
+        if (gPlayer[i].atc.keyFlags.quit)
+        {
+            gPlayer[i].atc.keyFlags.quit = 0;
+            netBroadcastPlayerLogoff(i);
+            if (i == myconnectindex)
+            {
+                // netBroadcastMyLogoff(gQuitRequest == 2);
+                gQuitGame = true;
+                gRestartGame = gQuitRequest == 2;
+                netDeinitialize();
+                netResetToSinglePlayer();
+                return;
+            }
+        }
+        if (gPlayer[i].atc.keyFlags.restart)
+        {
+            gPlayer[i].atc.keyFlags.restart = 0;
+            levelRestart();
+            return;
+        }
+        if (gPlayer[i].atc.keyFlags.pause)
+        {
+            gPlayer[i].atc.keyFlags.pause = 0;
+            gPaused = !gPaused;
+            if (gPaused && gGameOptions.nGameType > 0 && numplayers > 1)
+            {
+                sprintf(buffer,"%s paused the game",gProfile[i].name);
+                viewSetMessage(buffer);
+            }
+        }
+    }
+    viewClearInterpolations();
+    if (!gDemo.at1)
+    {
+        if (gPaused || gEndGameMgr.at0 || (gGameOptions.nGameType == 0 && gGameMenuMgr.m_bActive))
+            return;
+        if (gDemo.at0)
+            gDemo.Write(gFifoInput[(gNetFifoTail-1)&255]);
+    }
+    for (int i = connecthead; i >= 0; i = connectpoint2[i])
+    {
+        viewBackupView(i);
+        playerProcess(&gPlayer[i]);
+    }
+    trProcessBusy();
+    evProcess(gFrameClock);
+    seqProcess(4);
+    DoSectorPanning();
+    actProcessSprites();
+    actPostProcess();
+    viewCorrectPrediction();
+    sndProcess();
+    ambProcess();
+    viewUpdateDelirium();
+    viewUpdateShake();
+    sfxUpdate3DSounds();
+    if (gMe->at376 == 1)
+    {
+#define CHOKERATE 8
+#define TICRATE 30
+        gChokeCounter += CHOKERATE;
+        while (gChokeCounter >= TICRATE)
+        {
+            gChoke.at1c(gMe);
+            gChokeCounter -= TICRATE;
+        }
+    }
+    gLevelTime++;
+    gFrame++;
+    gFrameClock += 4;
+    if ((gGameOptions.uGameFlags&1) != 0 && !gStartNewGame)
+    {
+        ready2send = 0;
+        if (gNetPlayers > 1 && gNetMode == NETWORK_SERVER && gPacketMode == PACKETMODE_1 && myconnectindex == connecthead)
+        {
+            while (gNetFifoMasterTail < gNetFifoTail)
+            {
+                G_HandleAsync();
+                netMasterUpdate();
+            }
+        }
+        if (gDemo.at0)
+            gDemo.Close();
+        sndFadeSong(4000);
+        seqKillAll();
+        if (gGameOptions.uGameFlags&2)
+        {
+            if (gGameOptions.nGameType == 0)
+            {
+                if (gGameOptions.uGameFlags&8)
+                    levelPlayEndScene(gGameOptions.nEpisode);
+                gGameMenuMgr.Deactivate();
+                gGameMenuMgr.Push(&menuCredits,-1);
+            }
+            gGameOptions.uGameFlags &= ~3;
+            gRestartGame = 1;
+            gQuitGame = 1;
+        }
+        else
+        {
+            gEndGameMgr.Setup();
+            viewResizeView(gViewSize);
+        }
+    }
+}
+
+SWITCH switches[] = {
+    { "?", 0, 0 },
+    { "help", 0, 0 },
+    { "broadcast", 1, 0 },
+    { "map", 2, 1 },
+    { "masterslave", 3, 0 },
+    //{ "net", 4, 1 },
+    { "nodudes", 5, 1 },
+    { "playback", 6, 1 },
+    { "record", 7, 1 },
+    { "robust", 8, 0 },
+    { "setupfile", 9, 1 },
+    { "skill", 10, 1 },
+    //{ "nocd", 11, 0 },
+    //{ "8250", 12, 0 },
+    { "ini", 13, 1 },
+    { "noaim", 14, 0 },
+    { "f", 15, 1 },
+    { "control", 16, 1 },
+    { "vector", 17, 1 },
+    { "quick", 18, 0 },
+    //{ "getopt", 19, 1 },
+    //{ "auto", 20, 1 },
+    { "pname", 21, 1 },
+    { "noresend", 22, 0 },
+    { "silentaim", 23, 0 },
+    { "nodemo", 25, 0 },
+    { "art", 26, 1 },
+    { "snd", 27, 1 },
+    { "rff", 28, 1 },
+    { "maxalloc", 29, 1 },
+    { "server", 30, 1 },
+    { "client", 31, 1 },
+    { "noautoload", 32, 0 },
+    { "usecwd", 33, 0 },
+    { "cachesize", 34, 1 },
+    { "g", 35, 1 },
+    { "grp", 35, 1 },
+    { "game_dir", 36, 1 },
+    { "cfg", 9, 1 },
+    { "setup", 37, 0 },
+    { "nosetup", 38, 0 },
+    { "port", 39, 1 },
+    { "h", 40, 1 },
+    { "mh", 41, 1 },
+    { "j", 42, 1 },
+    { NULL, 0, 0 }
+};
+
+void PrintHelp(void)
+{
+    char tempbuf[128];
+    static char const s[] = "Usage: " APPBASENAME " [files] [options]\n"
+        "Example: " APPBASENAME " -usecwd -cfg myconfig.cfg -map nukeland.map\n\n"
+        "Files can be of type [grp|zip|map|def]\n"
+        "\n"
+        "-art [file.art]\tSpecify an art base file name\n"
+        "-cachesize #\tSet cache size in kB\n"
+        "-cfg [file.cfg]\tUse an alternate configuration file\n"
+        "-client [host]\tConnect to a multiplayer game\n"
+        "-game_dir [dir]\tSpecify game data directory\n"
+        "-g [file.grp]\tLoad additional game data\n"
+        "-h [file.def]\tLoad an alternate definitions file\n"
+        "-ini [file.ini]\tSpecify an INI file name (default is blood.ini)\n"
+        "-j [dir]\t\tAdd a directory to " APPNAME "'s search list\n"
+        "-map [file.map]\tLoad an external map file\n"
+        "-mh [file.def]\tInclude an additional definitions module\n"
+        "-noautoload\tDisable loading from autoload directory\n"
+        "-nodemo\t\tNo Demos\n"
+        "-nodudes\tNo monsters\n"
+        "-playback\tPlay back a demo\n"
+        "-pname\t\tOverride player name setting from config file\n"
+        "-record\t\tRecord demo\n"
+        "-rff\t\tSpecify an RFF file for Blood game resources\n"
+        "-server [players]\tStart a multiplayer server\n"
+#ifdef STARTUP_SETUP_WINDOW
+        "-setup/nosetup\tEnable or disable startup window\n"
+#endif
+        "-skill\t\tSet player handicap; Range:0..4; Default:2; (NOT difficulty level.)\n"
+        "-snd\t\tSpecify an RFF Sound file name\n"
+        "-usecwd\t\tRead data and configuration from current directory\n"
+        ;
+#ifdef WM_MSGBOX_WINDOW
+    Bsnprintf(tempbuf, sizeof(tempbuf), APPNAME " %s", s_buildRev);
+    wm_msgbox(tempbuf, s);
+#else
+    initprintf("%s\n", s);
+#endif
+#if 0
+    puts("Blood Command-line Options:");
+    // NUKE-TODO:
+    puts("-?            This help");
+    //puts("-8250         Enforce obsolete UART I/O");
+    //puts("-auto         Automatic Network start. Implies -quick");
+    //puts("-getopt       Use network game options from file.  Implies -auto");
+    puts("-broadcast    Set network to broadcast packet mode");
+    puts("-masterslave  Set network to master/slave packet mode");
+    //puts("-net          Net mode game");
+    //puts("-noaim        Disable auto-aiming");
+    //puts("-nocd         Disable CD audio");
+    puts("-nodudes      No monsters");
+    puts("-nodemo       No Demos");
+    puts("-robust       Robust network sync checking");
+    puts("-skill        Set player handicap; Range:0..4; Default:2; (NOT difficulty level.)");
+    puts("-quick        Skip Intro screens and get right to the game");
+    puts("-pname        Override player name setting from config file");
+    puts("-map          Specify a user map");
+    puts("-playback     Play back a demo");
+    puts("-record       Record a demo");
+    puts("-art          Specify an art base file name");
+    puts("-snd          Specify an RFF Sound file name");
+    puts("-RFF          Specify an RFF file for Blood game resources");
+    puts("-ini          Specify an INI file name (default is blood.ini)");
+#endif
+    exit(0);
+}
+
+void ParseOptions(void)
+{
+    int option;
+    while ((option = GetOptions(switches)) != -1)
+    {
+        switch (option)
+        {
+        case -3:
+            ThrowError("Invalid argument: %s", OptFull);
+            fallthrough__;
+        case 29:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            nMaxAlloc = atoi(OptArgv[0]);
+            if (!nMaxAlloc)
+                nMaxAlloc = 0x2000000;
+            break;
+        case 0:
+            PrintHelp();
+            break;
+        //case 19:
+        //    byte_148eec = 1;
+        //case 20:
+        //    if (OptArgc < 1)
+        //        ThrowError("Missing argument");
+        //    strncpy(byte_148ef0, OptArgv[0], 13);
+        //    byte_148ef0[12] = 0;
+        //    bQuickStart = 1;
+        //    byte_148eeb = 1;
+        //    if (gGameOptions.gameType == 0)
+        //        gGameOptions.gameType = 2;
+        //    break;
+        case 25:
+            bNoDemo = 1;
+            break;
+        case 18:
+            bQuickStart = 1;
+            break;
+        //case 12:
+        //    EightyTwoFifty = 1;
+        //    break;
+        case 1:
+            gPacketMode = PACKETMODE_2;
+            break;
+        case 21:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            strcpy(gPName, OptArgv[0]);
+            bCustomName = 1;
+            break;
+        case 2:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            strcpy(gUserMapFilename, OptArgv[0]);
+            bAddUserMap = 1;
+            bNoDemo = 1;
+            break;
+        case 3:
+            if (gSyncRate == 1)
+                gPacketMode = PACKETMODE_2;
+            else
+                gPacketMode = PACKETMODE_1;
+            break;
+        case 4:
+            //if (OptArgc < 1)
+            //    ThrowError("Missing argument");
+            //if (gGameOptions.nGameType == 0)
+            //    gGameOptions.nGameType = 2;
+            break;
+        case 30:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            gNetPlayers = ClipRange(atoi(OptArgv[0]), 1, kMaxPlayers);
+            gNetMode = NETWORK_SERVER;
+            break;
+        case 31:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            gNetMode = NETWORK_CLIENT;
+            strncpy(gNetAddress, OptArgv[0], sizeof(gNetAddress)-1);
+            break;
+        case 14:
+            gAutoAim = 0;
+            break;
+        case 22:
+            bNoResend = 0;
+            break;
+        case 23:
+            bSilentAim = 1;
+            break;
+        case 5:
+            gGameOptions.nMonsterSettings = 0;
+            break;
+        case 6:
+            if (OptArgc < 1)
+                gDemo.SetupPlayback(NULL);
+            else
+                gDemo.SetupPlayback(OptArgv[0]);
+            break;
+        case 7:
+            if (OptArgc < 1)
+                gDemo.Create(NULL);
+            else
+                gDemo.Create(OptArgv[0]);
+            break;
+        case 8:
+            gRobust = 1;
+            break;
+        case 13:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            levelOverrideINI(OptArgv[0]);
+            bNoDemo = 1;
+            break;
+        case 26:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            pUserTiles = (char*)malloc(strlen(OptArgv[0])+1);
+            if (!pUserTiles)
+                return;
+            strcpy(pUserTiles, OptArgv[0]);
+            break;
+        case 27:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            pUserSoundRFF = (char*)malloc(strlen(OptArgv[0])+1);
+            if (!pUserSoundRFF)
+                return;
+            strcpy(pUserSoundRFF, OptArgv[0]);
+            break;
+        case 28:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            pUserRFF = (char*)malloc(strlen(OptArgv[0])+1);
+            if (!pUserRFF)
+                return;
+            strcpy(pUserRFF, OptArgv[0]);
+            break;
+        case 9:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            strcpy(SetupFilename, OptArgv[0]);
+            break;
+        case 10:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            gSkill = strtoul(OptArgv[0], NULL, 0);
+            if (gSkill < 0)
+                gSkill = 0;
+            else if (gSkill > 4)
+                gSkill = 4;
+            break;
+        case 15:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            gSyncRate = ClipRange(strtoul(OptArgv[0], NULL, 0), 1, 4);
+            if (gPacketMode == PACKETMODE_1)
+                gSyncRate = 1;
+            else if (gPacketMode == PACKETMODE_3)
+                gSyncRate = 1;
+            break;
+        case -2:
+        {
+            const char *k = strrchr(OptFull, '.');
+            if (k)
+            {
+                if (!Bstrcasecmp(k, ".map"))
+                {
+                    strcpy(gUserMapFilename, OptFull);
+                    bAddUserMap = 1;
+                    bNoDemo = 1;
+                }
+                else if (!Bstrcasecmp(k, ".grp") || !Bstrcasecmp(k, ".zip") || !Bstrcasecmp(k, ".pk3") || !Bstrcasecmp(k, ".pk4"))
+                {
+                    G_AddGroup(OptFull);
+                }
+                else if (!Bstrcasecmp(k, ".def"))
+                {
+                    clearDefNamePtr();
+                    g_defNamePtr = dup_filename(OptFull);
+                    initprintf("Using DEF file \"%s\".\n", g_defNamePtr);
+                    continue;
+                }
+            }
+            else
+            {
+                strcpy(gUserMapFilename, OptFull);
+                bAddUserMap = 1;
+                bNoDemo = 1;
+            }
+            break;
+        }
+        case 11:
+            //bNoCDAudio = 1;
+            break;
+        case 32:
+            initprintf("Autoload disabled\n");
+            bNoAutoLoad = true;
+            break;
+        case 33:
+            g_useCwd = true;
+            break;
+        case 34:
+        {
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            uint32_t j = strtoul(OptArgv[0], NULL, 0);
+            MAXCACHE1DSIZE = j<<10;
+            initprintf("Cache size: %dkB\n", j);
+            break;
+        }
+        case 35:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            G_AddGroup(OptArgv[0]);
+            break;
+        case 36:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            Bstrncpyz(g_modDir, OptArgv[0], sizeof(g_modDir));
+            G_AddPath(OptArgv[0]);
+            break;
+        case 37:
+            gCommandSetup = true;
+            break;
+        case 38:
+            gNoSetup = true;
+            gCommandSetup = false;
+            break;
+        case 39:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            gNetPort = strtoul(OptArgv[0], NULL, 0);
+            break;
+        case 40:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            G_AddDef(OptArgv[0]);
+            break;
+        case 41:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            G_AddDefModule(OptArgv[0]);
+            break;
+        case 42:
+            if (OptArgc < 1)
+                ThrowError("Missing argument");
+            G_AddPath(OptArgv[0]);
+            break;
+        }
+    }
+#if 0
+    if (bAddUserMap)
+    {
+        char zNode[BMAX_PATH];
+        char zDir[BMAX_PATH];
+        char zFName[BMAX_PATH];
+        _splitpath(gUserMapFilename, zNode, zDir, zFName, NULL);
+        strcpy(g_modDir, zNode);
+        strcat(g_modDir, zDir);
+        strcpy(gUserMapFilename, zFName);
+    }
+#endif
+}
+
+void ClockStrobe()
+{
+    gGameClock++;
+}
+
+#if defined(_WIN32) && defined(DEBUGGINGAIDS)
+// See FILENAME_CASE_CHECK in cache1d.c
+static int32_t check_filename_casing(void)
+{
+    return 1;
+}
+#endif
+
+int app_main(int argc, char const * const * argv)
+{
+    char buffer[BMAX_PATH];
+    margc = argc;
+    margv = argv;
+#ifdef _WIN32
+    if (!G_CheckCmdSwitch(argc, argv, "-noinstancechecking") && win_checkinstance())
+    {
+        if (!wm_ynbox(APPNAME, "Another Build game is currently running. "
+                      "Do you wish to continue starting this copy?"))
+            return 3;
+    }
+
+    backgroundidle = 0;
+
+    G_ExtPreInit(argc, argv);
+
+#ifdef DEBUGGINGAIDS
+    extern int32_t (*check_filename_casing_fn)(void);
+    check_filename_casing_fn = check_filename_casing;
+#endif
+#endif
+
+    OSD_SetLogFile(APPBASENAME ".log");
+
+    OSD_SetFunctions(NULL,
+                     NULL,
+                     NULL,
+                     NULL,
+                     NULL,
+                     GAME_clearbackground,
+                     BGetTime,
+                     GAME_onshowosd);
+
+    wm_setapptitle(APPNAME);
+
+    initprintf(APPNAME " %s\n", s_buildRev);
+    PrintBuildInfo();
+
+    memcpy(&gGameOptions, &gSingleGameOptions, sizeof(GAMEOPTIONS));
+    ParseOptions();
+    G_ExtInit();
+
+    if (!g_useCwd)
+        G_AddSearchPaths();
+
+    // used with binds for fast function lookup
+    hash_init(&h_gamefuncs);
+    for (bssize_t i=NUMGAMEFUNCTIONS-1; i>=0; i--)
+    {
+        if (gamefunctions[i][0] == '\0')
+            continue;
+
+        char *str = Bstrtolower(Xstrdup(gamefunctions[i]));
+        hash_add(&h_gamefuncs,gamefunctions[i],i,0);
+        hash_add(&h_gamefuncs,str,i,0);
+        Bfree(str);
+    }
+    
+#ifdef STARTUP_SETUP_WINDOW
+    int const readSetup =
+#endif
+    CONFIG_ReadSetup();
+    if (bCustomName)
+        strcpy(szPlayerName, gPName);
+
+    if (enginePreInit())
+    {
+        wm_msgbox("Build Engine Initialization Error",
+                  "There was a problem initializing the Build engine: %s", engineerrstr);
+        ERRprintf("app_main: There was a problem initializing the Build engine: %s\n", engineerrstr);
+        Bexit(2);
+    }
+
+    if (Bstrcmp(SetupFilename, SETUPFILENAME))
+        initprintf("Using config file \"%s\".\n", SetupFilename);
+
+    ScanINIFiles();
+
+#ifdef STARTUP_SETUP_WINDOW
+    if (readSetup < 0 || (!gNoSetup && (configversion != BYTEVERSION || gSetup.forcesetup)) || gCommandSetup)
+    {
+        if (quitevent || !startwin_run())
+        {
+            engineUnInit();
+            Bexit(0);
+        }
+    }
+#endif
+
+    G_LoadGroups(!bNoAutoLoad && !gSetup.noautoload);
+
+    //if (!g_useCwd)
+    //    G_CleanupSearchPaths();
+
+    initprintf("Initializing OSD...\n");
+
+    //Bsprintf(tempbuf, HEAD2 " %s", s_buildRev);
+    OSD_SetVersion("Blood", 10, 0);
+    OSD_SetParameters(0, 0, 0, 12, 2, 12, OSD_ERROR, OSDTEXT_RED, gamefunctions[gamefunc_Show_Console][0] == '\0' ? OSD_PROTECTED : 0);
+    registerosdcommands();
+
+    char *const setupFileName = Xstrdup(SetupFilename);
+    char *const p = strtok(setupFileName, ".");
+
+    if (!p || !Bstrcmp(SetupFilename, SETUPFILENAME))
+        Bsprintf(buffer, "settings.cfg");
+    else
+        Bsprintf(buffer, "%s_settings.cfg", p);
+
+    Bfree(setupFileName);
+
+    OSD_Exec(buffer);
+    OSD_Exec("autoexec.cfg");
+
+    // Not neccessary ?
+    // CONFIG_SetDefaultKeys(keydefaults, true);
+
+    system_getcvars();
+
+    Resource::heap = new QHeap(nMaxAlloc);
+    gSysRes.Init(pUserRFF ? pUserRFF : "BLOOD.RFF");
+    gGuiRes.Init("GUI.RFF");
+    gSoundRes.Init(pUserSoundRFF ? pUserSoundRFF : "SOUNDS.RFF");
+
+
+    { // Replace
+        void qinitspritelists();
+        int32_t qinsertsprite(int16_t nSector, int16_t nStat);
+        int32_t qdeletesprite(int16_t nSprite);
+        int32_t qchangespritesect(int16_t nSprite, int16_t nSector);
+        int32_t qchangespritestat(int16_t nSprite, int16_t nStatus);
+        animateoffs_replace = qanimateoffs;
+        paletteLoadFromDisk_replace = qloadpalette;
+        getpalookup_replace = qgetpalookup;
+        initspritelists_replace = qinitspritelists;
+        insertsprite_replace = qinsertsprite;
+        deletesprite_replace = qdeletesprite;
+        changespritesect_replace = qchangespritesect;
+        changespritestat_replace = qchangespritestat;
+        loadvoxel_replace = qloadvoxel;
+        bloodhack = true;
+    }
+
+    initprintf("Initializing Build 3D engine\n");
+    scrInit();
+    
+    initprintf("Loading tiles\n");
+    if (pUserTiles)
+    {
+        strcpy(buffer,pUserTiles);
+        strcat(buffer,"%03i.ART");
+        if (!tileInit(0,buffer))
+            ThrowError("User specified ART files not found");
+    }
+    else
+    {
+        if (!tileInit(0,NULL))
+            ThrowError("TILES###.ART files not found");
+    }
+
+    LoadExtraArts();
+
+    levelLoadDefaults();
+    const char *defsfile = G_DefFile();
+    uint32_t stime = timerGetTicks();
+    if (!loaddefinitionsfile(defsfile))
+    {
+        uint32_t etime = timerGetTicks();
+        initprintf("Definitions file \"%s\" loaded in %d ms.\n", defsfile, etime-stime);
+    }
+    loaddefinitions_game(defsfile, FALSE);
+    powerupInit();
+    initprintf("Loading cosine table\n");
+    trigInit(gSysRes);
+    initprintf("Initializing view subsystem\n");
+    viewInit();
+    initprintf("Initializing dynamic fire\n");
+    FireInit();
+    initprintf("Initializing weapon animations\n");
+    WeaponInit();
+    LoadSaveSetup();
+    LoadSavedInfo();
+    gDemo.LoadDemoInfo();
+    initprintf("There are %d demo(s) in the loop\n", gDemo.at59ef);
+    initprintf("Loading control setup\n");
+    ctrlInit();
+    timerInit(120);
+    timerSetCallback(ClockStrobe);
+    // PORT-TODO: CD audio init
+
+    initprintf("Initializing network users\n");
+    netInitialize(true);
+    scrSetGameMode(gSetup.fullscreen, gSetup.xdim, gSetup.ydim, gSetup.bpp);
+    scrSetGamma(gGamma);
+    viewResizeView(gViewSize);
+    initprintf("Initializing sound system\n");
+    sndInit();
+    sfxInit();
+    gChoke.sub_83ff0(518, sub_84230);
+    if (bAddUserMap)
+    {
+        levelAddUserMap(gUserMapFilename);
+        gStartNewGame = 1;
+    }
+    SetupMenus();
+    videoSetViewableArea(0, 0, xdim - 1, ydim - 1);
+    if (!bQuickStart)
+        credLogosDos();
+    scrSetDac();
+RESTART:
+    sub_79760();
+    gViewIndex = myconnectindex;
+    gMe = gView = &gPlayer[myconnectindex];
+    netBroadcastPlayerInfo(myconnectindex);
+    initprintf("Waiting for network players!\n");
+    netWaitForEveryone(0);
+    if (gRestartGame)
+    {
+        // Network error
+        gQuitGame = false;
+        gRestartGame = false;
+        netDeinitialize();
+        netResetToSinglePlayer();
+        goto RESTART;
+    }
+    UpdateNetworkMenus();
+    if (!gDemo.at0 && gDemo.at59ef > 0 && gGameOptions.nGameType == 0 && !bNoDemo)
+        gDemo.SetupPlayback(NULL);
+    viewSetCrosshairColor(CrosshairColors.r, CrosshairColors.g, CrosshairColors.b);
+    gQuitGame = 0;
+    gRestartGame = 0;
+    if (gGameOptions.nGameType > 0)
+    {
+        KB_ClearKeysDown();
+        KB_FlushKeyboardQueue();
+        keyFlushScans();
+    }
+    else if (gDemo.at1 && !bAddUserMap && !bNoDemo)
+        gDemo.Playback();
+    if (gDemo.at59ef > 0)
+        gGameMenuMgr.Deactivate();
+    if (!bAddUserMap && !gGameStarted)
+        gGameMenuMgr.Push(&menuMain, -1);
+    ready2send = 1;
+    while (!gQuitGame)
+    {
+        if (handleevents() && quitevent)
+        {
+            KB_KeyDown[sc_Escape] = 1;
+            quitevent = 0;
+        }
+        netUpdate();
+        MUSIC_Update();
+        CONTROL_BindsEnabled = gInputMode == INPUT_MODE_0;
+        switch (gInputMode)
+        {
+        case INPUT_MODE_1:
+            if (gGameMenuMgr.m_bActive)
+                gGameMenuMgr.Process();
+            break;
+        case INPUT_MODE_0:
+            LocalKeys();
+            break;
+        default:
+            break;
+        }
+        if (gQuitGame)
+            continue;
+
+        OSD_DispatchQueued();
+        
+        bool bDraw;
+        if (gGameStarted)
+        {
+            char gameUpdate = false;
+            double const gameUpdateStartTime = timerGetHiTicks();
+            G_HandleAsync();
+            while (gPredictTail < gNetFifoHead[myconnectindex] && !gPaused)
+            {
+                viewUpdatePrediction(&gFifoInput[gPredictTail&255][myconnectindex]);
+            }
+            if (numplayers == 1)
+                gBufferJitter = 0;
+            while (gGameClock >= gNetFifoClock && ready2send)
+            {
+                netGetInput();
+                gNetFifoClock += 4;
+                while (gNetFifoHead[myconnectindex]-gNetFifoTail > gBufferJitter && !gStartNewGame && !gQuitGame)
+                {
+                    int i;
+                    for (i = connecthead; i >= 0; i = connectpoint2[i])
+                        if (gNetFifoHead[i] == gNetFifoTail)
+                            break;
+                    if (i >= 0)
+                        break;
+                    faketimerhandler();
+                    ProcessFrame();
+                    timerUpdate();
+                    gameUpdate = true;
+                }
+                timerUpdate();
+            }
+            if (gameUpdate)
+            {
+                g_gameUpdateTime = timerGetHiTicks() - gameUpdateStartTime;
+                if (g_gameUpdateAvgTime < 0.f)
+                    g_gameUpdateAvgTime = g_gameUpdateTime;
+                g_gameUpdateAvgTime = ((GAMEUPDATEAVGTIMENUMSAMPLES-1.f)*g_gameUpdateAvgTime+g_gameUpdateTime)/((float) GAMEUPDATEAVGTIMENUMSAMPLES);
+            }
+            bDraw = viewFPSLimit() != 0;
+            if (gQuitRequest && gQuitGame)
+                videoClearScreen(0);
+            else
+            {
+                netCheckSync();
+                if (bDraw)
+                {
+                    viewDrawScreen();
+                    g_gameUpdateAndDrawTime = timerGetHiTicks() - gameUpdateStartTime;
+                }
+            }
+        }
+        else
+        {
+            bDraw = viewFPSLimit() != 0;
+            if (bDraw)
+            {
+                videoClearScreen(0);
+                rotatesprite(160<<16,100<<16,65536,0,2518,0,0,0x4a,0,0,xdim-1,ydim-1);
+            }
+            G_HandleAsync();
+            if (gQuitRequest && !gQuitGame)
+                netBroadcastMyLogoff(gQuitRequest == 2);
+        }
+        if (bDraw)
+        {
+            switch (gInputMode)
+            {
+            case INPUT_MODE_1:
+                if (gGameMenuMgr.m_bActive)
+                    gGameMenuMgr.Draw();
+                break;
+            case INPUT_MODE_2:
+                gPlayerMsg.ProcessKeys();
+                gPlayerMsg.Draw();
+                break;
+            case INPUT_MODE_3:
+                gEndGameMgr.ProcessKeys();
+                gEndGameMgr.Draw();
+                break;
+            default:
+                break;
+            }
+        }
+        //scrNextPage();
+        if (TestBitString(gotpic, 2342))
+        {
+            FireProcess();
+            ClearBitString(gotpic, 2342);
+        }
+        //if (byte_148e29 && gStartNewGame)
+        //{
+        //	gStartNewGame = 0;
+        //	gQuitGame = 1;
+        //}
+        if (gStartNewGame)
+            StartLevel(&gGameOptions);
+    }
+    ready2send = 0;
+    if (gDemo.at0)
+        gDemo.Close();
+    if (gRestartGame)
+    {
+        UpdateDacs(0, true);
+        sndStopSong();
+        FX_StopAllSounds();
+        gQuitGame = 0;
+        gQuitRequest = 0;
+        gRestartGame = 0;
+        gGameStarted = 0;
+        levelSetupOptions(0,0);
+        while (gGameMenuMgr.m_bActive)
+        {
+            gGameMenuMgr.Process();
+            G_HandleAsync();
+            if (viewFPSLimit())
+            {
+                videoClearScreen(0);
+                gGameMenuMgr.Draw();
+            }
+        }
+        if (gGameOptions.nGameType != 0)
+        {
+            if (!gDemo.at0 && gDemo.at59ef > 0 && gGameOptions.nGameType == 0 && !bNoDemo)
+                gDemo.NextDemo();
+            videoSetViewableArea(0,0,xdim-1,ydim-1);
+            if (!bQuickStart)
+                credLogosDos();
+            scrSetDac();
+        }
+        goto RESTART;
+    }
+    ShutDown();
+
+    return 0;
+}
+
+static int32_t S_DefineAudioIfSupported(char *fn, const char *name)
+{
+#if !defined HAVE_FLAC || !defined HAVE_VORBIS
+    const char *extension = Bstrrchr(name, '.');
+# if !defined HAVE_FLAC
+    if (extension && !Bstrcasecmp(extension, ".flac"))
+        return -2;
+# endif
+# if !defined HAVE_VORBIS
+    if (extension && !Bstrcasecmp(extension, ".ogg"))
+        return -2;
+# endif
+#endif
+    Bstrncpy(fn, name, BMAX_PATH);
+    return 0;
+}
+
+// Returns:
+//   0: all OK
+//  -1: ID declaration was invalid:
+static int32_t S_DefineMusic(const char *ID, const char *name)
+{
+    int32_t sel = MUS_FIRST_SPECIAL;
+
+    Bassert(ID != NULL);
+
+    if (!Bstrcmp(ID,"intro"))
+    {
+        sel = MUS_INTRO;
+    }
+    else if (!Bstrcmp(ID,"loading"))
+    {
+        sel = MUS_LOADING;
+    }
+    else
+    {
+        sel = levelGetMusicIdx(ID);
+        if (sel < 0)
+            return -1;
+    }
+
+    int nEpisode = sel/kMaxLevels;
+    int nLevel = sel%kMaxLevels;
+    return S_DefineAudioIfSupported(gEpisodeInfo[nEpisode].at28[nLevel].atd0, name);
+}
+
+static int parsedefinitions_game(scriptfile *, int);
+
+static void parsedefinitions_game_include(const char *fileName, scriptfile *pScript, const char *cmdtokptr, int const firstPass)
+{
+    scriptfile *included = scriptfile_fromfile(fileName);
+
+    if (!included)
+    {
+        if (!Bstrcasecmp(cmdtokptr,"null") || pScript == NULL) // this is a bit overboard to prevent unused parameter warnings
+            {
+           // initprintf("Warning: Failed including %s as module\n", fn);
+            }
+/*
+        else
+            {
+            initprintf("Warning: Failed including %s on line %s:%d\n",
+                       fn, script->filename,scriptfile_getlinum(script,cmdtokptr));
+            }
+*/
+    }
+    else
+    {
+        parsedefinitions_game(included, firstPass);
+        scriptfile_close(included);
+    }
+}
+
+#if 0
+static void parsedefinitions_game_animsounds(scriptfile *pScript, const char * blockEnd, char const * fileName, dukeanim_t * animPtr)
+{
+    Bfree(animPtr->sounds);
+
+    size_t numPairs = 0, allocSize = 4;
+
+    animPtr->sounds = (animsound_t *)Xmalloc(allocSize * sizeof(animsound_t));
+    animPtr->numsounds = 0;
+
+    int defError = 1;
+    uint16_t lastFrameNum = 1;
+
+    while (pScript->textptr < blockEnd)
+    {
+        int32_t frameNum;
+        int32_t soundNum;
+
+        // HACK: we've reached the end of the list
+        //  (hack because it relies on knowledge of
+        //   how scriptfile_* preprocesses the text)
+        if (blockEnd - pScript->textptr == 1)
+            break;
+
+        // would produce error when it encounters the closing '}'
+        // without the above hack
+        if (scriptfile_getnumber(pScript, &frameNum))
+            break;
+
+        defError = 1;
+
+        if (scriptfile_getsymbol(pScript, &soundNum))
+            break;
+
+        // frame numbers start at 1 for us
+        if (frameNum <= 0)
+        {
+            initprintf("Error: frame number must be greater zero on line %s:%d\n", pScript->filename,
+                       scriptfile_getlinum(pScript, pScript->ltextptr));
+            break;
+        }
+
+        if (frameNum < lastFrameNum)
+        {
+            initprintf("Error: frame numbers must be in (not necessarily strictly)"
+                       " ascending order (line %s:%d)\n",
+                       pScript->filename, scriptfile_getlinum(pScript, pScript->ltextptr));
+            break;
+        }
+
+        lastFrameNum = frameNum;
+
+        if ((unsigned)soundNum >= MAXSOUNDS && soundNum != -1)
+        {
+            initprintf("Error: sound number #%d invalid on line %s:%d\n", soundNum, pScript->filename,
+                       scriptfile_getlinum(pScript, pScript->ltextptr));
+            break;
+        }
+
+        if (numPairs >= allocSize)
+        {
+            allocSize *= 2;
+            animPtr->sounds = (animsound_t *)Xrealloc(animPtr->sounds, allocSize * sizeof(animsound_t));
+        }
+
+        defError = 0;
+
+        animsound_t & sound = animPtr->sounds[numPairs];
+        sound.frame = frameNum;
+        sound.sound = soundNum;
+
+        ++numPairs;
+    }
+
+    if (!defError)
+    {
+        animPtr->numsounds = numPairs;
+        // initprintf("Defined sound sequence for hi-anim \"%s\" with %d frame/sound pairs\n",
+        //           hardcoded_anim_tokens[animnum].text, numpairs);
+    }
+    else
+    {
+        DO_FREE_AND_NULL(animPtr->sounds);
+        initprintf("Failed defining sound sequence for anim \"%s\".\n", fileName);
+    }
+}
+
+#endif
+
+static int parsedefinitions_game(scriptfile *pScript, int firstPass)
+{
+    int   token;
+    char *pToken;
+
+    static const tokenlist tokens[] =
+    {
+        { "include",         T_INCLUDE          },
+        { "#include",        T_INCLUDE          },
+        { "includedefault",  T_INCLUDEDEFAULT   },
+        { "#includedefault", T_INCLUDEDEFAULT   },
+        { "loadgrp",         T_LOADGRP          },
+        { "cachesize",       T_CACHESIZE        },
+        { "noautoload",      T_NOAUTOLOAD       },
+        { "music",           T_MUSIC            },
+        { "sound",           T_SOUND            },
+        //{ "cutscene",        T_CUTSCENE         },
+        //{ "animsounds",      T_ANIMSOUNDS       },
+        { "renamefile",      T_RENAMEFILE       },
+        { "globalgameflags", T_GLOBALGAMEFLAGS  },
+        { "rffdefineid",     T_RFFDEFINEID      },
+    };
+
+    static const tokenlist soundTokens[] =
+    {
+        { "id",       T_ID },
+        { "file",     T_FILE },
+        { "minpitch", T_MINPITCH },
+        { "maxpitch", T_MAXPITCH },
+        { "priority", T_PRIORITY },
+        { "type",     T_TYPE },
+        { "distance", T_DISTANCE },
+        { "volume",   T_VOLUME },
+    };
+
+#if 0
+    static const tokenlist animTokens [] =
+    {
+        { "delay",         T_DELAY },
+        { "aspect",        T_ASPECT },
+        { "sounds",        T_SOUND },
+        { "forcefilter",   T_FORCEFILTER },
+        { "forcenofilter", T_FORCENOFILTER },
+        { "texturefilter", T_TEXTUREFILTER },
+    };
+#endif
+
+    do
+    {
+        token  = getatoken(pScript, tokens, ARRAY_SIZE(tokens));
+        pToken = pScript->ltextptr;
+
+        switch (token)
+        {
+        case T_LOADGRP:
+        {
+            char *fileName;
+
+            pathsearchmode = 1;
+            if (!scriptfile_getstring(pScript,&fileName) && firstPass)
+            {
+                if (initgroupfile(fileName) == -1)
+                    initprintf("Could not find file \"%s\".\n", fileName);
+                else
+                {
+                    initprintf("Using file \"%s\" as game data.\n", fileName);
+                    if (!bNoAutoLoad && !gSetup.noautoload)
+                        G_DoAutoload(fileName);
+                }
+            }
+
+            pathsearchmode = 0;
+        }
+        break;
+        case T_CACHESIZE:
+        {
+            int32_t cacheSize;
+
+            if (scriptfile_getnumber(pScript, &cacheSize) || !firstPass)
+                break;
+
+            if (cacheSize > 0)
+                MAXCACHE1DSIZE = cacheSize << 10;
+        }
+        break;
+        case T_INCLUDE:
+        {
+            char *fileName;
+
+            if (!scriptfile_getstring(pScript, &fileName))
+                parsedefinitions_game_include(fileName, pScript, pToken, firstPass);
+
+            break;
+        }
+        case T_INCLUDEDEFAULT:
+        {
+            parsedefinitions_game_include(G_DefaultDefFile(), pScript, pToken, firstPass);
+            break;
+        }
+        case T_NOAUTOLOAD:
+            if (firstPass)
+                bNoAutoLoad = true;
+            break;
+        case T_MUSIC:
+        {
+            char *tokenPtr = pScript->ltextptr;
+            char *musicID  = NULL;
+            char *fileName = NULL;
+            char *musicEnd;
+
+            if (scriptfile_getbraces(pScript, &musicEnd))
+                break;
+
+            while (pScript->textptr < musicEnd)
+            {
+                switch (getatoken(pScript, soundTokens, ARRAY_SIZE(soundTokens)))
+                {
+                    case T_ID: scriptfile_getstring(pScript, &musicID); break;
+                    case T_FILE: scriptfile_getstring(pScript, &fileName); break;
+                }
+            }
+
+            if (!firstPass)
+            {
+                if (musicID==NULL)
+                {
+                    initprintf("Error: missing ID for music definition near line %s:%d\n",
+                               pScript->filename, scriptfile_getlinum(pScript,tokenPtr));
+                    break;
+                }
+
+                if (fileName == NULL || check_file_exist(fileName))
+                    break;
+
+                if (S_DefineMusic(musicID, fileName) == -1)
+                    initprintf("Error: invalid music ID on line %s:%d\n", pScript->filename, scriptfile_getlinum(pScript, tokenPtr));
+            }
+        }
+        break;
+
+        case T_RFFDEFINEID:
+        {
+            char *resName = NULL;
+            char *resType = NULL;
+            char *rffName = NULL;
+            int resID;
+
+            if (scriptfile_getstring(pScript, &resName))
+                break;
+
+            if (scriptfile_getstring(pScript, &resType))
+                break;
+
+            if (scriptfile_getnumber(pScript, &resID))
+                break;
+
+            if (scriptfile_getstring(pScript, &rffName))
+                break;
+
+            if (!firstPass)
+            {
+                if (!Bstrcasecmp(rffName, "SYSTEM"))
+                    gSysRes.AddExternalResource(resName, resType, resID);
+                else if (!Bstrcasecmp(rffName, "SOUND"))
+                    gSoundRes.AddExternalResource(resName, resType, resID);
+            }
+        }
+        break;
+
+#if 0
+        case T_CUTSCENE:
+        {
+            char *fileName = NULL;
+
+            scriptfile_getstring(pScript, &fileName);
+
+            char *animEnd;
+
+            if (scriptfile_getbraces(pScript, &animEnd))
+                break;
+
+            if (!firstPass)
+            {
+                dukeanim_t *animPtr = Anim_Find(fileName);
+
+                if (!animPtr)
+                {
+                    animPtr = Anim_Create(fileName);
+                    animPtr->framedelay = 10;
+                    animPtr->frameflags = 0;
+                }
+
+                int32_t temp;
+
+                while (pScript->textptr < animEnd)
+                {
+                    switch (getatoken(pScript, animTokens, ARRAY_SIZE(animTokens)))
+                    {
+                        case T_DELAY:
+                            scriptfile_getnumber(pScript, &temp);
+                            animPtr->framedelay = temp;
+                            break;
+                        case T_ASPECT:
+                        {
+                            double dtemp, dtemp2;
+                            scriptfile_getdouble(pScript, &dtemp);
+                            scriptfile_getdouble(pScript, &dtemp2);
+                            animPtr->frameaspect1 = dtemp;
+                            animPtr->frameaspect2 = dtemp2;
+                            break;
+                        }
+                        case T_SOUND:
+                        {
+                            char *animSoundsEnd = NULL;
+                            if (scriptfile_getbraces(pScript, &animSoundsEnd))
+                                break;
+                            parsedefinitions_game_animsounds(pScript, animSoundsEnd, fileName, animPtr);
+                            break;
+                        }
+                        case T_FORCEFILTER:
+                            animPtr->frameflags |= CUTSCENE_FORCEFILTER;
+                            break;
+                        case T_FORCENOFILTER:
+                            animPtr->frameflags |= CUTSCENE_FORCENOFILTER;
+                            break;
+                        case T_TEXTUREFILTER:
+                            animPtr->frameflags |= CUTSCENE_TEXTUREFILTER;
+                            break;
+                    }
+                }
+            }
+            else
+                pScript->textptr = animEnd;
+        }
+        break;
+        case T_ANIMSOUNDS:
+        {
+            char *tokenPtr     = pScript->ltextptr;
+            char *fileName     = NULL;
+
+            scriptfile_getstring(pScript, &fileName);
+            if (!fileName)
+                break;
+
+            char *animSoundsEnd = NULL;
+
+            if (scriptfile_getbraces(pScript, &animSoundsEnd))
+                break;
+
+            if (firstPass)
+            {
+                pScript->textptr = animSoundsEnd;
+                break;
+            }
+
+            dukeanim_t *animPtr = Anim_Find(fileName);
+
+            if (!animPtr)
+            {
+                initprintf("Error: expected animation filename on line %s:%d\n",
+                    pScript->filename, scriptfile_getlinum(pScript, tokenPtr));
+                break;
+            }
+
+            parsedefinitions_game_animsounds(pScript, animSoundsEnd, fileName, animPtr);
+        }
+        break;
+        case T_SOUND:
+        {
+            char *tokenPtr = pScript->ltextptr;
+            char *fileName = NULL;
+            char *musicEnd;
+
+            double volume = 1.0;
+
+            int32_t soundNum = -1;
+            int32_t maxpitch = 0;
+            int32_t minpitch = 0;
+            int32_t priority = 0;
+            int32_t type     = 0;
+            int32_t distance = 0;
+
+            if (scriptfile_getbraces(pScript, &musicEnd))
+                break;
+
+            while (pScript->textptr < musicEnd)
+            {
+                switch (getatoken(pScript, soundTokens, ARRAY_SIZE(soundTokens)))
+                {
+                    case T_ID:       scriptfile_getsymbol(pScript, &soundNum); break;
+                    case T_FILE:     scriptfile_getstring(pScript, &fileName); break;
+                    case T_MINPITCH: scriptfile_getsymbol(pScript, &minpitch); break;
+                    case T_MAXPITCH: scriptfile_getsymbol(pScript, &maxpitch); break;
+                    case T_PRIORITY: scriptfile_getsymbol(pScript, &priority); break;
+                    case T_TYPE:     scriptfile_getsymbol(pScript, &type);     break;
+                    case T_DISTANCE: scriptfile_getsymbol(pScript, &distance); break;
+                    case T_VOLUME:   scriptfile_getdouble(pScript, &volume);   break;
+                }
+            }
+
+            if (!firstPass)
+            {
+                if (soundNum==-1)
+                {
+                    initprintf("Error: missing ID for sound definition near line %s:%d\n", pScript->filename, scriptfile_getlinum(pScript,tokenPtr));
+                    break;
+                }
+
+                if (fileName == NULL || check_file_exist(fileName))
+                    break;
+
+                // maybe I should have just packed this into a sound_t and passed a reference...
+                if (S_DefineSound(soundNum, fileName, minpitch, maxpitch, priority, type, distance, volume) == -1)
+                    initprintf("Error: invalid sound ID on line %s:%d\n", pScript->filename, scriptfile_getlinum(pScript,tokenPtr));
+            }
+        }
+        break;
+#endif
+        case T_GLOBALGAMEFLAGS: scriptfile_getnumber(pScript, &blood_globalflags); break;
+        case T_EOF: return 0;
+        default: break;
+        }
+    }
+    while (1);
+
+    return 0;
+}
+
+int loaddefinitions_game(const char *fileName, int32_t firstPass)
+{
+    scriptfile *pScript = scriptfile_fromfile(fileName);
+
+    if (pScript)
+        parsedefinitions_game(pScript, firstPass);
+
+    for (char const * m : g_defModules)
+        parsedefinitions_game_include(m, NULL, "null", firstPass);
+
+    if (pScript)
+        scriptfile_close(pScript);
+
+    scriptfile_clearsymbols();
+
+    return 0;
+}
+
+INICHAIN *pINIChain;
+INICHAIN const*pINISelected;
+int nINICount = 0;
+
+const char *pzCrypticArts[] = {
+    "CPART07.AR_", "CPART15.AR_"
+};
+
+INIDESCRIPTION gINIDescription[] = {
+    { "BLOOD: One Unit Whole Blood", "BLOOD.INI", NULL, 0 },
+    { "Cryptic passage", "CRYPTIC.INI", pzCrypticArts, ARRAY_SSIZE(pzCrypticArts) },
+};
+
+bool AddINIFile(const char *pzFile, bool bForce = false)
+{
+    char *pzFN;
+    struct Bstat st;
+    static INICHAIN *pINIIter = NULL;
+    if (!bForce)
+    {
+        if (findfrompath(pzFile, &pzFN)) return false; // failed to resolve the filename
+        if (Bstat(pzFN, &st))
+        {
+            Bfree(pzFN);
+            return false;
+        } // failed to stat the file
+        Bfree(pzFN);
+        IniFile *pTempIni = new IniFile(pzFile);
+        if (!pTempIni->FindSection("Episode1"))
+        {
+            delete pTempIni;
+            return false;
+        }
+        delete pTempIni;
+    }
+    if (!pINIChain)
+        pINIIter = pINIChain = new INICHAIN;
+    else
+        pINIIter = pINIIter->pNext = new INICHAIN;
+    pINIIter->pNext = NULL;
+    pINIIter->pDescription = NULL;
+    Bstrncpy(pINIIter->zName, pzFile, BMAX_PATH);
+    for (int i = 0; i < ARRAY_SSIZE(gINIDescription); i++)
+    {
+        if (!Bstrncasecmp(pINIIter->zName, gINIDescription[i].pzFilename, BMAX_PATH))
+        {
+            pINIIter->pDescription = &gINIDescription[i];
+            break;
+        }
+    }
+    return true;
+}
+
+void ScanINIFiles(void)
+{
+    nINICount = 0;
+    CACHE1D_FIND_REC *pINIList = klistpath("/", "*.ini", CACHE1D_FIND_FILE);
+    pINIChain = NULL;
+
+    if (bINIOverride || !pINIList)
+    {
+        AddINIFile(BloodIniFile, true);
+    }
+
+    for (auto pIter = pINIList; pIter; pIter = pIter->next)
+    {
+        AddINIFile(pIter->name);
+    }
+    klistfree(pINIList);
+    pINISelected = pINIChain;
+    for (auto pIter = pINIChain; pIter; pIter = pIter->pNext)
+    {
+        if (!Bstrncasecmp(BloodIniFile, pIter->zName, BMAX_PATH))
+        {
+            pINISelected = pIter;
+            break;
+        }
+    }
+}
+
+bool LoadArtFile(const char *pzFile)
+{
+    int hFile = kopen4loadfrommod(pzFile, 0);
+    if (hFile == -1)
+    {
+        initprintf("Can't open extra art file:\"%s\"\n", pzFile);
+        return false;
+    }
+    artheader_t artheader;
+    int nStatus = artReadHeader(hFile, pzFile, &artheader);
+    if (nStatus != 0)
+    {
+        kclose(hFile);
+        initprintf("Error reading extra art file:\"%s\"\n", pzFile);
+        return false;
+    }
+    for (int i = artheader.tilestart; i <= artheader.tileend; i++)
+        tileDelete(i);
+    artReadManifest(hFile, &artheader);
+    artPreloadFile(hFile, &artheader);
+    for (int i = artheader.tilestart; i <= artheader.tileend; i++)
+        tileUpdatePicSiz(i);
+    kclose(hFile);
+    return true;
+}
+
+void LoadExtraArts(void)
+{
+    if (!pINISelected->pDescription)
+        return;
+    for (int i = 0; i < pINISelected->pDescription->nArts; i++)
+    {
+        LoadArtFile(pINISelected->pDescription->pzArts[i]);
+    }
+}
+
+bool DemoRecordStatus(void) {
+    return gDemo.at0;
+}
+
+bool VanillaMode() {
+    return gDemo.m_bLegacy && gDemo.at1;
+}
+
+bool fileExistsRFF(int id, const char *ext) {
+    return gSysRes.Lookup(id, ext);
+}
\ No newline at end of file
diff --git a/source/blood/src/blood.h b/source/blood/src/blood.h
new file mode 100644
index 000000000..c310bcd7d
--- /dev/null
+++ b/source/blood/src/blood.h
@@ -0,0 +1,249 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "levels.h"
+#include "resource.h"
+
+#define TILTBUFFER 4078
+
+#define kExplodeMax 8
+#define kDudeBase 200
+#define kDudePlayer1 231
+#define kDudePlayer8 238
+#define kDudeMax 260
+#define kMissileBase 300
+#define kMissileMax 318
+#define kThingBase 400
+#define kThingMax 436
+
+#define kMaxPowerUps 51
+
+#define kStatRespawn 8
+#define kStatMarker 10
+#define kStatGDXDudeTargetChanger 20
+#define kStatFree 1024
+
+#define kLensSize 80
+#define kViewEffectMax 19
+
+#define kNoTile -1
+
+
+// defined by NoOne:
+// -------------------------------
+#define kMaxPAL 5
+
+#define kWeaponItemBase 40
+#define kItemMax 151
+
+// marker sprite types
+#define kMarkerSPStart 1
+#define kMarkerMPStart 2
+#define kMarkerOff 3
+#define kMarkerOn 4
+#define kMarkerAxis 5
+#define kMarkerLowLink 6
+#define kMarkerUpLink 7
+#define kMarkerWarpDest 8
+#define kMarkerUpWater 9
+#define kMarkerLowWater 10
+#define kMarkerUpStack 11
+#define kMarkerLowStack 12
+#define kMarkerUpGoo 13
+#define kMarkerLowGoo 14
+#define kMarkerPath 15
+
+// sprite cstat
+#define kSprBlock 0x0001
+#define kSprTrans 0x0002
+#define kSprFlipX 0x0004
+#define kSprFlipY 0x0008
+#define kSprFace 0x0000
+#define kSprWall 0x0010
+#define kSprFloor 0x0020
+#define kSprSpin 0x0030
+#define kSprRMask 0x0030
+#define kSprOneSided 0x0040
+#define kSprOriginAlign 0x0080
+#define kSprHitscan 0x0100
+#define kSprTransR 0x0200
+#define kSprPushable 0x1000
+#define kSprMoveMask 0x6000
+#define kSprMoveNone 0x0000
+#define kSprMoveForward 0x2000
+#define kSprMoveFloor 0x2000
+#define kSprMoveReverse 0x4000
+#define kSprMoveCeiling 0x4000
+#define kSprInvisible 0x8000
+
+// sprite attributes
+#define kHitagMovePhys 0x0001 // affected by movement physics
+#define kHitagGravityPhys 0x0002 // affected by gravity
+#define kHitagFalling 0x0004 // currently in z-motion
+#define kHitagAutoAim 0x0008
+#define kHitagRespawn 0x0010
+#define kHitagFree 0x0020
+#define kHitagSmoke 0x0100
+#define kHitagExtBit 0x8000 // NoOne's extension bit(Note: it's bit 0 in editor!)
+
+// sector types 
+#define kSecBase 600
+#define kSecZMotion kSectorBase
+#define kSecZSprite 602
+#define kSecWarp 603
+#define kSecTeleport 604
+#define kSecPath 612
+#define kSecRotateStep 613
+#define kSecSlideMarked 614
+#define kSecRotateMarked 615
+#define kSecSlide 616
+#define kSecRotate 617
+#define kSecDamage 618
+#define kSecCounter 619
+#define kSecMax 620
+
+// switch types
+#define kSwitchBase 20
+#define kSwitchToggle 20
+#define kSwitchOneWay 21
+#define kSwitchCombo 22
+#define kSwitchPadlock 23
+#define kSwitchMax 24
+
+// projectile types
+#define kProjectileEctoSkull 307
+
+// custom level end
+#define kGDXChannelEndLevelCustom 6
+
+// GDX types
+#define kGDXTypeBase 24
+#define kGDXCustomDudeSpawn 24
+#define kGDXRandomTX 25
+#define kGDXSequentialTX 26
+#define kGDXSeqSpawner 27
+#define kGDXObjPropertiesChanger 28
+#define kGDXObjPicnumChanger 29
+#define kGDXObjSizeChanger 31
+#define kGDXDudeTargetChanger 33
+#define kGDXSectorFXChanger 34
+#define kGDXObjDataChanger 35
+#define kGDXSpriteDamager 36
+#define kGDXObjDataAccumulator 37
+#define kGDXEffectSpawner 38
+#define kGDXWindGenerator 39
+
+#define kGDXThingTNTProx 433 // detects only players
+#define kGDXThingThrowableRock 434 // does small damage if hits target
+#define kGDXThingCustomDudeLifeLeech 435 // the same as normal, except it aims in specified target
+#define kGDXDudeUniversalCultist 254
+#define kGDXGenDudeBurning 255
+
+#define kGDXItemMapLevel 150 // once picked up, draws whole minimap
+
+// ai state types
+#define kAiStateOther -1
+#define kAiStateIdle 0
+#define kAiStateGenIdle 1
+#define kAiStateMove 2
+#define kAiStateSearch 3
+#define kAiStateChase 4
+#define kAiStateRecoil 5
+
+
+#define kAng5 28
+#define kAng15 85
+#define kAng30 170
+#define kAng45 256
+#define kAng60 341
+#define kAng90 512
+#define kAng120 682
+#define kAng180 1024
+#define kAng360 2048
+
+
+// -------------------------------
+
+struct INIDESCRIPTION {
+    const char *pzName;
+    const char *pzFilename;
+    const char **pzArts;
+    int nArts;
+};
+
+struct INICHAIN {
+    INICHAIN *pNext;
+    char zName[BMAX_PATH];
+    INIDESCRIPTION *pDescription;
+};
+
+extern INICHAIN *pINIChain;
+extern INICHAIN const*pINISelected;
+
+typedef struct {
+    int32_t usejoystick;
+    int32_t usemouse;
+    int32_t fullscreen;
+    int32_t xdim;
+    int32_t ydim;
+    int32_t bpp;
+    int32_t forcesetup;
+    int32_t noautoload;
+} ud_setup_t;
+
+enum INPUT_MODE {
+    INPUT_MODE_0 = 0,
+    INPUT_MODE_1,
+    INPUT_MODE_2,
+    INPUT_MODE_3,
+};
+
+extern Resource gSysRes, gGuiRes;
+extern INPUT_MODE gInputMode;
+extern ud_setup_t gSetup;
+extern char SetupFilename[BMAX_PATH];
+extern int32_t gNoSetup;
+extern short BloodVersion;
+extern int gNetPlayers;
+extern bool gRestartGame;
+#define GAMEUPDATEAVGTIMENUMSAMPLES 100
+extern double g_gameUpdateTime, g_gameUpdateAndDrawTime;
+extern double g_gameUpdateAvgTime;
+extern int blood_globalflags;
+extern bool bVanilla;
+extern int gMusicPrevLoadedEpisode;
+extern int gMusicPrevLoadedLevel;
+
+extern int gFrameClock;
+
+void QuitGame(void);
+void PreloadCache(void);
+void StartLevel(GAMEOPTIONS *gameOptions);
+void ProcessFrame(void);
+void ScanINIFiles(void);
+bool LoadArtFile(const char *pzFile);
+void LoadExtraArts(void);
+bool DemoRecordStatus(void);
+bool VanillaMode(void);
+bool fileExistsRFF(int id, const char* ext);
\ No newline at end of file
diff --git a/source/blood/src/callback.cpp b/source/blood/src/callback.cpp
new file mode 100644
index 000000000..223645a3f
--- /dev/null
+++ b/source/blood/src/callback.cpp
@@ -0,0 +1,731 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#include "build.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "ai.h"
+#include "blood.h"
+#include "callback.h"
+#include "config.h"
+#include "db.h"
+#include "dude.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gameutil.h"
+#include "levels.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "trig.h"
+#include "triggers.h"
+#include "view.h"
+
+
+void sub_74C20(int nSprite) // 7
+{
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_15, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        xvel[pFX->index] = xvel[nSprite] + Random2(0x10000);
+        yvel[pFX->index] = yvel[nSprite] + Random2(0x10000);
+        zvel[pFX->index] = zvel[nSprite] - Random(0x1aaaa);
+    }
+    evPost(nSprite, 3, 3, CALLBACK_ID_7);
+}
+
+void sub_74D04(int nSprite) // 15
+{
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_49, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        xvel[pFX->index] = xvel[nSprite] + Random2(0x1aaaa);
+        yvel[pFX->index] = yvel[nSprite] + Random2(0x1aaaa);
+        zvel[pFX->index] = zvel[nSprite] - Random(0x1aaaa);
+    }
+    evPost(nSprite, 3, 3, CALLBACK_ID_15);
+}
+
+void FinishHim(int nSprite) // 13
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    if (playerSeqPlaying(&gPlayer[pSprite->type-kDudePlayer1], 16) && pXSprite->target == gMe->at5b)
+        sndStartSample(3313, -1, 1, 0);
+}
+
+void FlameLick(int nSprite) // 0
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    for (int i = 0; i < 3; i++)
+    {
+        int nDist = (pSprite->xrepeat*(tilesiz[pSprite->picnum].x/2))>>3;
+        int nAngle = Random(2048);
+        int dx = mulscale30(nDist, Cos(nAngle));
+        int dy = mulscale30(nDist, Sin(nAngle));
+        int x = pSprite->x + dx;
+        int y = pSprite->y + dy;
+        int z = bottom-Random(bottom-top);
+        spritetype *pFX = gFX.fxSpawn(FX_32, pSprite->sectnum, x, y, z, 0);
+        if (pFX)
+        {
+            xvel[pFX->index] = xvel[nSprite] + Random2(-dx);
+            yvel[pFX->index] = yvel[nSprite] + Random2(-dy);
+            zvel[pFX->index] = zvel[nSprite] - Random(0x1aaaa);
+        }
+    }
+    if (pXSprite->burnTime > 0)
+        evPost(nSprite, 3, 5, CALLBACK_ID_0);
+}
+
+void Remove(int nSprite) // 1
+{
+    spritetype *pSprite = &sprite[nSprite];
+    evKill(nSprite, 3);
+    if (pSprite->extra > 0)
+        seqKill(3, pSprite->extra);
+    sfxKill3DSound(pSprite, 0, -1);
+    DeleteSprite(nSprite);
+}
+
+void FlareBurst(int nSprite) // 2
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    spritetype *pSprite = &sprite[nSprite];
+    int nAngle = getangle(xvel[nSprite], yvel[nSprite]);
+    int nRadius = 0x55555;
+    for (int i = 0; i < 8; i++)
+    {
+        spritetype *pSpawn = actSpawnSprite(pSprite, 5);
+        pSpawn->picnum = 2424;
+        pSpawn->shade = -128;
+        pSpawn->xrepeat = pSpawn->yrepeat = 32;
+        pSpawn->type = 303;
+        pSpawn->clipdist = 2;
+        pSpawn->owner = pSprite->owner;
+        int nAngle2 = (i<<11)/8;
+        int dx = 0;
+        int dy = mulscale30r(nRadius, Sin(nAngle2));
+        int dz = mulscale30r(nRadius, -Cos(nAngle2));
+        if (i&1)
+        {
+            dy >>= 1;
+            dz >>= 1;
+        }
+        RotateVector(&dx, &dy, nAngle);
+        xvel[pSpawn->index] += dx;
+        yvel[pSpawn->index] += dy;
+        zvel[pSpawn->index] += dz;
+        evPost(pSpawn->index, 3, 960, CALLBACK_ID_3);
+    }
+    evPost(nSprite, 3, 0, CALLBACK_ID_1);
+}
+
+void FlareSpark(int nSprite) // 3
+{
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_28, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        xvel[pFX->index] = xvel[nSprite] + Random2(0x1aaaa);
+        yvel[pFX->index] = yvel[nSprite] + Random2(0x1aaaa);
+        zvel[pFX->index] = zvel[nSprite] - Random(0x1aaaa);
+    }
+    evPost(nSprite, 3, 4, CALLBACK_ID_3);
+}
+
+void FlareSparkLite(int nSprite) // 4
+{
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_28, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        xvel[pFX->index] = xvel[nSprite] + Random2(0x1aaaa);
+        yvel[pFX->index] = yvel[nSprite] + Random2(0x1aaaa);
+        zvel[pFX->index] = zvel[nSprite] - Random(0x1aaaa);
+    }
+    evPost(nSprite, 3, 12, CALLBACK_ID_4);
+}
+
+void ZombieSpurt(int nSprite) // 5
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    spritetype *pSprite = &sprite[nSprite];
+    int nXSprite = pSprite->extra;
+    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    spritetype *pFX = gFX.fxSpawn(FX_27, pSprite->sectnum, pSprite->x, pSprite->y, top, 0);
+    if (pFX)
+    {
+        xvel[pFX->index] = xvel[nSprite] + Random2(0x11111);
+        yvel[pFX->index] = yvel[nSprite] + Random2(0x11111);
+        zvel[pFX->index] = zvel[nSprite] - 0x6aaaa;
+    }
+    if (pXSprite->data1 > 0)
+    {
+        evPost(nSprite, 3, 4, CALLBACK_ID_5);
+        pXSprite->data1 -= 4;
+    }
+    else if (pXSprite->data2 > 0)
+    {
+        evPost(nSprite, 3, 60, CALLBACK_ID_5);
+        pXSprite->data1 = 40;
+        pXSprite->data2--;
+    }
+}
+
+void BloodSpurt(int nSprite) // 6
+{
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX = gFX.fxSpawn(FX_27, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        pFX->ang = 0;
+        xvel[pFX->index] = xvel[nSprite]>>8;
+        yvel[pFX->index] = yvel[nSprite]>>8;
+        zvel[pFX->index] = zvel[nSprite]>>8;
+    }
+    evPost(nSprite, 3, 6, CALLBACK_ID_6);
+}
+
+void DynPuff(int nSprite) // 8
+{
+    spritetype *pSprite = &sprite[nSprite];
+    if (zvel[nSprite])
+    {
+        int nDist = (pSprite->xrepeat*(tilesiz[pSprite->picnum].x/2))>>2;
+        int x = pSprite->x + mulscale30(nDist, Cos(pSprite->ang-512));
+        int y = pSprite->y + mulscale30(nDist, Sin(pSprite->ang-512));
+        int z = pSprite->z;
+        spritetype *pFX = gFX.fxSpawn(FX_7, pSprite->sectnum, x, y, z, 0);
+        if (pFX)
+        {
+            xvel[pFX->index] = xvel[nSprite];
+            yvel[pFX->index] = yvel[nSprite];
+            zvel[pFX->index] = zvel[nSprite];
+        }
+    }
+    evPost(nSprite, 3, 12, CALLBACK_ID_8);
+}
+
+void Respawn(int nSprite) // 9
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int nXSprite = pSprite->extra;
+    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    if (pSprite->statnum != 8 && pSprite->statnum != 4)
+        ThrowError("Sprite %d is not on Respawn or Thing list\n", nSprite);
+    if (!(pSprite->hitag&16))
+        ThrowError("Sprite %d does not have the respawn attribute\n", nSprite);
+    switch (pXSprite->respawnPending)
+    {
+    case 1:
+    {
+        int nTime = mulscale16(actGetRespawnTime(pSprite), 0x4000);
+        pXSprite->respawnPending = 2;
+        evPost(nSprite, 3, nTime, CALLBACK_ID_9);
+        break;
+    }
+    case 2:
+    {
+        int nTime = mulscale16(actGetRespawnTime(pSprite), 0x2000);
+        pXSprite->respawnPending = 3;
+        evPost(nSprite, 3, nTime, CALLBACK_ID_9);
+        break;
+    }
+    case 3:
+    {
+        dassert(pSprite->owner != kStatRespawn);
+        dassert(pSprite->owner >= 0 && pSprite->owner < kMaxStatus);
+        ChangeSpriteStat(nSprite, pSprite->owner);
+        pSprite->type = pSprite->zvel;
+        pSprite->owner = -1;
+        pSprite->hitag &= ~16;
+        xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+        pXSprite->respawnPending = 0;
+        pXSprite->burnTime = 0;
+        pXSprite->isTriggered = 0;
+        if (pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
+        {
+            int nType = pSprite->type-kDudeBase;
+            pSprite->x = baseSprite[nSprite].x;
+            pSprite->y = baseSprite[nSprite].y;
+            pSprite->z = baseSprite[nSprite].z;
+            pSprite->cstat |= 0x1101;
+            pSprite->clipdist = dudeInfo[nType].clipdist;
+            pXSprite->health = dudeInfo[nType].startHealth<<4;
+            if (gSysRes.Lookup(dudeInfo[nType].seqStartID, "SEQ"))
+                seqSpawn(dudeInfo[nType].seqStartID, 3, pSprite->extra, -1);
+            aiInitSprite(pSprite);
+            pXSprite->key = 0;
+        }
+        if (pSprite->type == 400)
+        {
+            pSprite->cstat |= 257;
+            pSprite->cstat &= (unsigned short)~32768;
+        }
+        gFX.fxSpawn(FX_29, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+        sfxPlay3DSound(pSprite, 350, -1, 0);
+        break;
+    }
+    default:
+        ThrowError("Unexpected respawnPending value = %d", pXSprite->respawnPending);
+        break;
+    }
+}
+
+void PlayerBubble(int nSprite) // 10
+{
+    spritetype *pSprite = &sprite[nSprite];
+    if (IsPlayerSprite(pSprite))
+    {
+        PLAYER *pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+        dassert(pPlayer != NULL);
+        if (!pPlayer->at302)
+            return;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        for (int i = 0; i < (pPlayer->at302>>6); i++)
+        {
+            int nDist = (pSprite->xrepeat*(tilesiz[pSprite->picnum].x/2))>>2;
+            int nAngle = Random(2048);
+            int x = pSprite->x + mulscale30(nDist, Cos(nAngle));
+            int y = pSprite->y + mulscale30(nDist, Sin(nAngle));
+            int z = bottom-Random(bottom-top);
+            spritetype *pFX = gFX.fxSpawn((FX_ID)(FX_23+Random(3)), pSprite->sectnum, x, y, z, 0);
+            if (pFX)
+            {
+                xvel[pFX->index] = xvel[nSprite] + Random2(0x1aaaa);
+                yvel[pFX->index] = yvel[nSprite] + Random2(0x1aaaa);
+                zvel[pFX->index] = zvel[nSprite] + Random2(0x1aaaa);
+            }
+        }
+        evPost(nSprite, 3, 4, CALLBACK_ID_10);
+    }
+}
+
+void EnemyBubble(int nSprite) // 11
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    for (int i = 0; i < (klabs(zvel[nSprite])>>18); i++)
+    {
+        int nDist = (pSprite->xrepeat*(tilesiz[pSprite->picnum].x/2))>>2;
+        int nAngle = Random(2048);
+        int x = pSprite->x + mulscale30(nDist, Cos(nAngle));
+        int y = pSprite->y + mulscale30(nDist, Sin(nAngle));
+        int z = bottom-Random(bottom-top);
+        spritetype *pFX = gFX.fxSpawn((FX_ID)(FX_23+Random(3)), pSprite->sectnum, x, y, z, 0);
+        if (pFX)
+        {
+            xvel[pFX->index] = xvel[nSprite] + Random2(0x1aaaa);
+            yvel[pFX->index] = yvel[nSprite] + Random2(0x1aaaa);
+            zvel[pFX->index] = zvel[nSprite] + Random2(0x1aaaa);
+        }
+    }
+    evPost(nSprite, 3, 4, CALLBACK_ID_11);
+}
+
+void CounterCheck(int nSector) // 12
+{
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    sectortype *pSector = &sector[nSector];
+    // By NoOne: edits for counter sector new features.
+    // remove check below, so every sector can be counter if command 12 (this callback) received.
+    //if (pSector->lotag != 619) return;
+    int nXSprite = pSector->extra;
+    if (nXSprite > 0)
+    {
+        XSECTOR *pXSector = &xsector[nXSprite];
+        int nReq = pXSector->waitTimeA;
+        int nType = pXSector->data;
+        if (nType && nReq)
+        {
+            int nCount = 0;
+            for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+            {
+                if (sprite[nSprite].type == nType)
+                    nCount++;
+            }
+            if (nCount >= nReq)
+            {
+
+                //pXSector->waitTimeA = 0; //do not reset necessary objects counter to zero
+                trTriggerSector(nSector, pXSector, 1);
+                pXSector->locked = 1; //lock sector, so it can be opened again later
+            }
+            else
+                evPost(nSector, 6, 5, CALLBACK_ID_12);
+        }
+    }
+}
+
+void sub_76140(int nSprite) // 14
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int ceilZ, ceilHit, floorZ, floorHit;
+    GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist, CLIPMASK0);
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    pSprite->z += floorZ-bottom;
+    int nAngle = Random(2048);
+    int nDist = Random(16)<<4;
+    int x = pSprite->x+mulscale28(nDist, Cos(nAngle));
+    int y = pSprite->y+mulscale28(nDist, Sin(nAngle));
+    gFX.fxSpawn(FX_48, pSprite->sectnum, x, y, pSprite->z, 0);
+    if (pSprite->ang == 1024)
+    {
+        int nChannel = 28+(pSprite->index&2);
+        dassert(nChannel < 32);
+        sfxPlay3DSound(pSprite, 385, nChannel, 1);
+    }
+    if (Chance(0x5000))
+    {
+        spritetype *pFX = gFX.fxSpawn(FX_36, pSprite->sectnum, x, y, floorZ-64, 0);
+        if (pFX)
+            pFX->ang = nAngle;
+    }
+    gFX.sub_73FFC(nSprite);
+}
+
+void sub_7632C(spritetype *pSprite)
+{
+    xvel[pSprite->index] = yvel[pSprite->index] = zvel[pSprite->index] = 0;
+    if (pSprite->extra > 0)
+        seqKill(3, pSprite->extra);
+    sfxKill3DSound(pSprite, -1, -1);
+    switch (pSprite->type)
+    {
+    case 37:
+    case 38:
+    case 39:
+        pSprite->picnum = 2465;
+        break;
+    case 40:
+    case 41:
+    case 42:
+        pSprite->picnum = 2464;
+        break;
+    }
+    pSprite->type = 51;
+    pSprite->xrepeat = pSprite->yrepeat = 10;
+}
+
+int dword_13B32C[] = { 608, 609, 611 };
+int dword_13B338[] = { 610, 612 };
+
+void sub_763BC(int nSprite) // 16
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int ceilZ, ceilHit, floorZ, floorHit;
+    GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist, CLIPMASK0);
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    pSprite->z += floorZ-bottom;
+    int zv = zvel[nSprite]-velFloor[pSprite->sectnum];
+    if (zv > 0)
+    {
+        actFloorBounceVector((int*)&xvel[nSprite], (int*)&yvel[nSprite], &zv, pSprite->sectnum, 0x9000);
+        zvel[nSprite] = zv;
+        if (velFloor[pSprite->sectnum] == 0 && klabs(zvel[nSprite]) < 0x20000)
+        {
+            sub_7632C(pSprite);
+            return;
+        }
+        int nChannel = 28+(pSprite->index&2);
+        dassert(nChannel < 32);
+        if (pSprite->type >= 37 && pSprite->type <= 39)
+        {
+            Random(3);
+            sfxPlay3DSound(pSprite, 608+Random(2), nChannel, 1);
+        }
+        else
+            sfxPlay3DSound(pSprite, dword_13B338[Random(2)], nChannel, 1);
+    }
+    else if (zvel[nSprite] == 0)
+        sub_7632C(pSprite);
+}
+
+void sub_765B8(int nSprite) // 17
+{
+    spritetype *pSprite = &sprite[nSprite];
+    if (pSprite->owner >= 0 && pSprite->owner < kMaxSprites)
+    {
+        spritetype *pOwner = &sprite[pSprite->owner];
+        XSPRITE *pXOwner = &xsprite[pOwner->extra];
+        switch (pSprite->type)
+        {
+        case 147:
+            trTriggerSprite(pOwner->index, pXOwner, 1);
+            sndStartSample(8003, 255, 2, 0);
+            viewSetMessage("Blue Flag returned to base.");
+            break;
+        case 148:
+            trTriggerSprite(pOwner->index, pXOwner, 1);
+            sndStartSample(8002, 255, 2, 0);
+            viewSetMessage("Red Flag returned to base.");
+            break;
+        }
+    }
+    evPost(pSprite->index, 3, 0, CALLBACK_ID_1);
+}
+
+void sub_766B8(int nSprite) // 19
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int ceilZ, ceilHit, floorZ, floorHit;
+    GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist, CLIPMASK0);
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    pSprite->z += floorZ-bottom;
+    int nAngle = Random(2048);
+    int nDist = Random(16)<<4;
+    int x = pSprite->x+mulscale28(nDist, Cos(nAngle));
+    int y = pSprite->y+mulscale28(nDist, Sin(nAngle));
+    if (pSprite->ang == 1024)
+    {
+        int nChannel = 28+(pSprite->index&2);
+        dassert(nChannel < 32);
+        sfxPlay3DSound(pSprite, 385, nChannel, 1);
+    }
+    spritetype *pFX = NULL;
+    if (pSprite->type == 53 || pSprite->type == 430)
+    {
+        if (Chance(0x500) || pSprite->type == 430)
+            pFX = gFX.fxSpawn(FX_55, pSprite->sectnum, x, y, floorZ-64, 0);
+        if (pFX)
+            pFX->ang = nAngle;
+    }
+    else
+    {
+        pFX = gFX.fxSpawn(FX_32, pSprite->sectnum, x, y, floorZ-64, 0);
+        if (pFX)
+            pFX->ang = nAngle;
+    }
+    gFX.sub_73FFC(nSprite);
+}
+
+void sub_768E8(int nSprite) // 18
+{
+    spritetype *pSprite = &sprite[nSprite];
+    spritetype *pFX;
+    if (pSprite->type == 53)
+        pFX = gFX.fxSpawn(FX_53, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    else
+        pFX = gFX.fxSpawn(FX_54, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pFX)
+    {
+        pFX->ang = 0;
+        xvel[pFX->index] = xvel[nSprite]>>8;
+        yvel[pFX->index] = yvel[nSprite]>>8;
+        zvel[pFX->index] = zvel[nSprite]>>8;
+    }
+    evPost(nSprite, 3, 6, CALLBACK_ID_18);
+}
+
+void sub_769B4(int nSprite) // 19
+{
+    spritetype *pSprite = &sprite[nSprite];
+    if (pSprite->statnum == 4 && !(pSprite->hitag & 32)) {
+        switch (pSprite->lotag) {
+            case 431:
+            case kGDXThingCustomDudeLifeLeech:
+                xsprite[pSprite->extra].stateTimer = 0;
+                break;
+        }
+    }
+}
+
+void sub_76A08(spritetype *pSprite, spritetype *pSprite2, PLAYER *pPlayer)
+{
+    int top, bottom;
+    int nSprite = pSprite->index;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    pSprite->x = pSprite2->x;
+    pSprite->y = pSprite2->y;
+    pSprite->z = sector[pSprite2->sectnum].floorz-(bottom-pSprite->z);
+    pSprite->ang = pSprite2->ang;
+    ChangeSpriteSect(nSprite, pSprite2->sectnum);
+    sfxPlay3DSound(pSprite2, 201, -1, 0);
+    xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+    viewBackupSpriteLoc(nSprite, pSprite);
+    if (pPlayer)
+    {
+        playerResetInertia(pPlayer);
+        pPlayer->at6b = pPlayer->at73 = 0;
+    }
+}
+
+void sub_76B78(int nSprite)
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int nOwner = actSpriteOwnerToSpriteId(pSprite);
+    if (nOwner < 0 || nOwner >= kMaxSprites)
+    {
+        evPost(nSprite, 3, 0, CALLBACK_ID_1);
+        return;
+    }
+    spritetype *pOwner = &sprite[nOwner];
+    PLAYER *pPlayer;
+    if (IsPlayerSprite(pOwner))
+        pPlayer = &gPlayer[pOwner->type-kDudePlayer1];
+    else
+        pPlayer = NULL;
+    if (!pPlayer)
+    {
+        evPost(nSprite, 3, 0, CALLBACK_ID_1);
+        return;
+    }
+    pSprite->ang = getangle(pOwner->x-pSprite->x, pOwner->y-pSprite->y);
+    int nXSprite = pSprite->extra;
+    if (nXSprite > 0)
+    {
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        if (pXSprite->data1 == 0)
+        {
+            evPost(nSprite, 3, 0, CALLBACK_ID_1);
+            return;
+        }
+        int nSprite2, nNextSprite;
+        for (nSprite2 = headspritestat[6]; nSprite2 >= 0; nSprite2 = nNextSprite)
+        {
+            nNextSprite = nextspritestat[nSprite2];
+            if (nOwner == nSprite2)
+                continue;
+            spritetype *pSprite2 = &sprite[nSprite2];
+            int nXSprite2 = pSprite2->extra;
+            if (nXSprite2 > 0 && nXSprite2 < kMaxXSprites)
+            {
+                XSPRITE *pXSprite2 = &xsprite[nXSprite2];
+                PLAYER *pPlayer2;
+                if (IsPlayerSprite(pSprite2))
+                    pPlayer2 = &gPlayer[pSprite2->type-kDudePlayer1];
+                else
+                    pPlayer2 = NULL;
+                if (pXSprite2->health > 0 && (pPlayer2 || pXSprite2->key == 0))
+                {
+                    if (pPlayer2)
+                    {
+                        if (gGameOptions.nGameType == 1)
+                            continue;
+                        if (gGameOptions.nGameType == 3 && pPlayer->at2ea == pPlayer2->at2ea)
+                            continue;
+                        int t = 0x8000/ClipLow(gNetPlayers-1, 1);
+                        if (!powerupCheck(pPlayer2, 14))
+                            t += ((3200-pPlayer2->at33e[2])<<15)/3200;
+                        if (Chance(t) || nNextSprite < 0)
+                        {
+                            int nDmg = actDamageSprite(nOwner, pSprite2, DAMAGE_TYPE_5, pXSprite->data1<<4);
+                            pXSprite->data1 = ClipLow(pXSprite->data1-nDmg, 0);
+                            sub_76A08(pSprite2, pSprite, pPlayer2);
+                            evPost(nSprite, 3, 0, CALLBACK_ID_1);
+                            return;
+                        }
+                    }
+                    else
+                    {
+                        int vd = 0x2666;
+                        switch (pSprite2->type)
+                        {
+                        case 218:
+                        case 219:
+                        case 220:
+                        case 250:
+                        case 251:
+                            vd = 0x147;
+                            break;
+                        case 205:
+                        case 221:
+                        case 222:
+                        case 223:
+                        case 224:
+                        case 225:
+                        case 226:
+                        case 227:
+                        case 228:
+                        case 229:
+                        case 239:
+                        case 240:
+                        case 241:
+                        case 242:
+                        case 243:
+                        case 244:
+                        case 245:
+                        case 252:
+                        case 253:
+                            vd = 0;
+                            break;
+                        }
+                        if (vd && (Chance(vd) || nNextSprite < 0))
+                        {
+                            sub_76A08(pSprite2, pSprite, NULL);
+                            evPost(nSprite, 3, 0, CALLBACK_ID_1);
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+        pXSprite->data1 = ClipLow(pXSprite->data1-1, 0);
+        evPost(nSprite, 3, 0, CALLBACK_ID_1);
+    }
+}
+
+void(*gCallback[kCallbackMax])(int) =
+{
+    FlameLick,
+    Remove,
+    FlareBurst,
+    FlareSpark,
+    FlareSparkLite,
+    ZombieSpurt,
+    BloodSpurt,
+    sub_74C20,
+    DynPuff,
+    Respawn,
+    PlayerBubble,
+    EnemyBubble,
+    CounterCheck,
+    FinishHim,
+    sub_76140,
+    sub_74D04,
+    sub_763BC,
+    sub_765B8,
+    sub_768E8,
+    sub_766B8,
+    sub_769B4,
+    sub_76B78
+};
diff --git a/source/blood/src/callback.h b/source/blood/src/callback.h
new file mode 100644
index 000000000..c6ad73654
--- /dev/null
+++ b/source/blood/src/callback.h
@@ -0,0 +1,53 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+
+enum CALLBACK_ID {
+    CALLBACK_ID_NONE = -1,
+    CALLBACK_ID_0 = 0,
+    CALLBACK_ID_1,
+    CALLBACK_ID_2,
+    CALLBACK_ID_3,
+    CALLBACK_ID_4,
+    CALLBACK_ID_5,
+    CALLBACK_ID_6,
+    CALLBACK_ID_7,
+    CALLBACK_ID_8,
+    CALLBACK_ID_9,
+    CALLBACK_ID_10,
+    CALLBACK_ID_11,
+    CALLBACK_ID_12,
+    CALLBACK_ID_13,
+    CALLBACK_ID_14,
+    CALLBACK_ID_15,
+    CALLBACK_ID_16,
+    CALLBACK_ID_17,
+    CALLBACK_ID_18,
+    CALLBACK_ID_19,
+    CALLBACK_ID_20,
+    CALLBACK_ID_21,
+    kCallbackMax
+};
+
+extern void (*gCallback[kCallbackMax])(int);
\ No newline at end of file
diff --git a/source/blood/src/choke.cpp b/source/blood/src/choke.cpp
new file mode 100644
index 000000000..bf10091e9
--- /dev/null
+++ b/source/blood/src/choke.cpp
@@ -0,0 +1,136 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#include "common_game.h"
+#include "blood.h"
+#include "choke.h"
+#include "globals.h"
+#include "levels.h"
+#include "player.h"
+#include "qav.h"
+#include "resource.h"
+
+
+void CChoke::sub_83F54(char *a1, int _x, int _y, void (*a2)(PLAYER*))
+{
+    at14 = _x;
+    at18 = _y;
+    at0 = a1;
+    at1c = a2;
+    if (!at4 && at0)
+    {
+        at4 = gSysRes.Lookup(at0, "QAV");
+        if (!at4)
+            ThrowError("Could not load QAV %s\n", at0);
+        at8 = (QAV*)gSysRes.Lock(at4);
+        at8->nSprite = -1;
+        at8->x = at14;
+        at8->y = at18;
+        at8->Preload();
+        sub_84218();
+    }
+}
+
+void CChoke::sub_83ff0(int a1, void(*a2)(PLAYER*))
+{
+    at0 = NULL;
+    at1c = a2;
+    if (!at4 && a1 != -1)
+    {
+        at4 = gSysRes.Lookup(a1, "QAV");
+        if (!at4)
+            ThrowError("Could not load QAV %d\n", a1);
+        at8 = (QAV*)gSysRes.Lock(at4);
+        at8->nSprite = -1;
+        at8->x = at14;
+        at8->y = at18;
+        at8->Preload();
+        sub_84218();
+    }
+}
+
+void CChoke::sub_84080(char *a1, void(*a2)(PLAYER*))
+{
+    at0 = a1;
+    at1c = a2;
+    if (!at4 && at0)
+    {
+        at4 = gSysRes.Lookup(at0, "QAV");
+        if (!at4)
+            ThrowError("Could not load QAV %s\n", at0);
+        at8 = (QAV*)gSysRes.Lock(at4);
+        at8->nSprite = -1;
+        at8->x = at14;
+        at8->y = at18;
+        at8->Preload();
+        sub_84218();
+    }
+}
+
+void CChoke::sub_84110(int x, int y)
+{
+    if (!at4)
+        return;
+    int v4 = gFrameClock;
+    gFrameClock = gGameClock;
+    at8->x = x;
+    at8->y = y;
+    int vd = totalclock-at10;
+    at10 = totalclock;
+    atc -= vd;
+    if (atc <= 0 || atc > at8->at10)
+        atc = at8->at10;
+    int vdi = at8->at10-atc;
+    at8->Play(vdi-vd, vdi, -1, NULL);
+    int vb = windowxy1.x;
+    int v10 = windowxy1.y;
+    int vc = windowxy2.x;
+    int v8 = windowxy2.y;
+    windowxy1.x = windowxy1.y = 0;
+    windowxy2.x = xdim-1;
+    windowxy2.y = ydim-1;
+    at8->Draw(vdi, 10, 0, 0);
+    windowxy1.x = vb;
+    windowxy1.y = v10;
+    windowxy2.x = vc;
+    windowxy2.y = v8;
+    gFrameClock = v4;
+}
+
+void CChoke::sub_84218()
+{
+    atc = at8->at10;
+    at10 = totalclock;
+}
+
+void sub_84230(PLAYER *pPlayer)
+{
+    int t = gGameOptions.nDifficulty+2;
+    if (pPlayer->at372 < 64)
+        pPlayer->at372 = ClipHigh(pPlayer->at372+t, 64);
+    if (pPlayer->at372 > (7-gGameOptions.nDifficulty)*5)
+        pPlayer->at36a = ClipHigh(pPlayer->at36a+t*4, 128);
+}
+
+CChoke gChoke;
\ No newline at end of file
diff --git a/source/blood/src/choke.h b/source/blood/src/choke.h
new file mode 100644
index 000000000..34f7c5f54
--- /dev/null
+++ b/source/blood/src/choke.h
@@ -0,0 +1,61 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "common_game.h"
+#include "player.h"
+#include "qav.h"
+#include "resource.h"
+
+class CChoke
+{
+public:
+    CChoke()
+    {
+        at0 = NULL;
+        at4 = NULL;
+        at8 = NULL;
+        atc = 0;
+        at10 = 0;
+        at1c = NULL;
+        at14 = 0;
+        at18 = 0;
+    };
+    void sub_83F54(char *a1, int _x, int _y, void(*a2)(PLAYER*));
+    void sub_83ff0(int a1, void(*a2)(PLAYER*));
+    void sub_84080(char *a1, void(*a2)(PLAYER*));
+    void sub_84110(int x, int y);
+    void sub_84218();
+    char *at0;
+    DICTNODE *at4;
+    QAV *at8;
+    int atc;
+    int at10;
+    int at14;
+    int at18;
+    void(*at1c)(PLAYER *);
+};
+
+void sub_84230(PLAYER*);
+
+extern CChoke gChoke;
diff --git a/source/blood/src/common.cpp b/source/blood/src/common.cpp
new file mode 100644
index 000000000..37b5164e9
--- /dev/null
+++ b/source/blood/src/common.cpp
@@ -0,0 +1,748 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+//
+// Common non-engine code/data for EDuke32 and Mapster32
+//
+
+#include "compat.h"
+#include "build.h"
+#include "baselayer.h"
+#include "palette.h"
+
+#ifdef _WIN32
+# define NEED_SHLWAPI_H
+# include "windows_inc.h"
+# include "winbits.h"
+# ifndef KEY_WOW64_64KEY
+#  define KEY_WOW64_64KEY 0x0100
+# endif
+# ifndef KEY_WOW64_32KEY
+#  define KEY_WOW64_32KEY 0x0200
+# endif
+#elif defined __APPLE__
+# include "osxbits.h"
+#endif
+
+#include "common.h"
+#include "common_game.h"
+
+// g_grpNamePtr can ONLY point to a malloc'd block (length BMAX_PATH)
+char *g_grpNamePtr = NULL;
+
+const char *G_DefaultGrpFile(void)
+{
+    return "nblood.pk3";
+}
+
+const char *G_DefaultDefFile(void)
+{
+    return "blood.def";
+}
+
+const char *G_GrpFile(void)
+{
+    return (g_grpNamePtr == NULL) ? G_DefaultGrpFile() : g_grpNamePtr;
+}
+
+const char *G_DefFile(void)
+{
+    return (g_defNamePtr == NULL) ? G_DefaultDefFile() : g_defNamePtr;
+}
+
+static char g_rootDir[BMAX_PATH];
+
+int g_useCwd;
+int32_t g_groupFileHandle;
+
+void G_ExtPreInit(int32_t argc,char const * const * argv)
+{
+    g_useCwd = G_CheckCmdSwitch(argc, argv, "-usecwd");
+
+#ifdef _WIN32
+    GetModuleFileName(NULL,g_rootDir,BMAX_PATH);
+    Bcorrectfilename(g_rootDir,1);
+    //chdir(g_rootDir);
+#else
+    getcwd(g_rootDir,BMAX_PATH);
+    strcat(g_rootDir,"/");
+#endif
+}
+
+void G_ExtInit(void)
+{
+    char cwd[BMAX_PATH];
+
+#ifdef EDUKE32_OSX
+    char *appdir = Bgetappdir();
+    addsearchpath(appdir);
+    Bfree(appdir);
+#endif
+
+    if (getcwd(cwd,BMAX_PATH) && Bstrcmp(cwd,"/") != 0)
+        addsearchpath(cwd);
+
+    if (CommandPaths)
+    {
+        int32_t i;
+        struct strllist *s;
+        while (CommandPaths)
+        {
+            s = CommandPaths->next;
+            i = addsearchpath(CommandPaths->str);
+            if (i < 0)
+            {
+                initprintf("Failed adding %s for game data: %s\n", CommandPaths->str,
+                           i==-1 ? "not a directory" : "no such directory");
+            }
+
+            Bfree(CommandPaths->str);
+            Bfree(CommandPaths);
+            CommandPaths = s;
+        }
+    }
+
+#if defined(_WIN32)
+    if (!access("user_profiles_enabled", F_OK))
+#else
+    if (g_useCwd == 0 && access("user_profiles_disabled", F_OK))
+#endif
+    {
+        char *homedir;
+        int32_t asperr;
+
+        if ((homedir = Bgethomedir()))
+        {
+            Bsnprintf(cwd,sizeof(cwd),"%s/"
+#if defined(_WIN32)
+                      APPNAME
+#elif defined(GEKKO)
+                      "apps/" APPBASENAME
+#else
+                      ".config/" APPBASENAME
+#endif
+                      ,homedir);
+            asperr = addsearchpath(cwd);
+            if (asperr == -2)
+            {
+                if (Bmkdir(cwd,S_IRWXU) == 0) asperr = addsearchpath(cwd);
+                else asperr = -1;
+            }
+            if (asperr == 0)
+                Bchdir(cwd);
+            Bfree(homedir);
+        }
+    }
+}
+
+static int32_t G_TryLoadingGrp(char const * const grpfile)
+{
+    int32_t i;
+
+    if ((i = initgroupfile(grpfile)) == -1)
+        initprintf("Warning: could not find main data file \"%s\"!\n", grpfile);
+    else
+        initprintf("Using \"%s\" as main game data file.\n", grpfile);
+
+    return i;
+}
+
+void G_LoadGroups(int32_t autoload)
+{
+    if (g_modDir[0] != '/')
+    {
+        char cwd[BMAX_PATH];
+
+        Bstrcat(g_rootDir, g_modDir);
+        addsearchpath(g_rootDir);
+        //        addsearchpath(mod_dir);
+
+        char path[BMAX_PATH];
+
+        if (getcwd(cwd, BMAX_PATH))
+        {
+            Bsnprintf(path, sizeof(path), "%s/%s", cwd, g_modDir);
+            if (!Bstrcmp(g_rootDir, path))
+            {
+                if (addsearchpath(path) == -2)
+                    if (Bmkdir(path, S_IRWXU) == 0)
+                        addsearchpath(path);
+            }
+        }
+
+    }
+    const char *grpfile = G_GrpFile();
+    G_TryLoadingGrp(grpfile);
+
+    if (autoload)
+    {
+        G_LoadGroupsInDir("autoload");
+
+        //if (i != -1)
+        //    G_DoAutoload(grpfile);
+    }
+
+    if (g_modDir[0] != '/')
+        G_LoadGroupsInDir(g_modDir);
+
+    if (g_defNamePtr == NULL)
+    {
+        const char *tmpptr = getenv("BLOODDEF");
+        if (tmpptr)
+        {
+            clearDefNamePtr();
+            g_defNamePtr = dup_filename(tmpptr);
+            initprintf("Using \"%s\" as definitions file\n", g_defNamePtr);
+        }
+    }
+
+    loaddefinitions_game(G_DefFile(), TRUE);
+
+    struct strllist *s;
+
+    int const bakpathsearchmode = pathsearchmode;
+    pathsearchmode = 1;
+
+    while (CommandGrps)
+    {
+        int32_t j;
+
+        s = CommandGrps->next;
+
+        if ((j = initgroupfile(CommandGrps->str)) == -1)
+            initprintf("Could not find file \"%s\".\n", CommandGrps->str);
+        else
+        {
+            g_groupFileHandle = j;
+            initprintf("Using file \"%s\" as game data.\n", CommandGrps->str);
+            if (autoload)
+                G_DoAutoload(CommandGrps->str);
+        }
+
+        Bfree(CommandGrps->str);
+        Bfree(CommandGrps);
+        CommandGrps = s;
+    }
+    pathsearchmode = bakpathsearchmode;
+}
+
+#if defined _WIN32
+static int G_ReadRegistryValue(char const * const SubKey, char const * const Value, char * const Output, DWORD * OutputSize)
+{
+    // KEY_WOW64_32KEY gets us around Wow6432Node on 64-bit builds
+    REGSAM const wow64keys[] = { KEY_WOW64_32KEY, KEY_WOW64_64KEY };
+
+    for (auto &wow64key : wow64keys)
+    {
+        HKEY hkey;
+        LONG keygood = RegOpenKeyEx(HKEY_LOCAL_MACHINE, NULL, 0, KEY_READ | wow64key, &hkey);
+
+        if (keygood != ERROR_SUCCESS)
+            continue;
+
+        LONG retval = SHGetValueA(hkey, SubKey, Value, NULL, Output, OutputSize);
+
+        RegCloseKey(hkey);
+
+        if (retval == ERROR_SUCCESS)
+            return 1;
+    }
+
+    return 0;
+}
+#endif
+
+#ifndef EDUKE32_TOUCH_DEVICES
+#if defined EDUKE32_OSX || defined __linux__ || defined EDUKE32_BSD
+static void G_AddSteamPaths(const char *basepath)
+{
+    UNREFERENCED_PARAMETER(basepath);
+#if 0
+    char buf[BMAX_PATH];
+
+    // PORT-TODO:
+    // Duke Nukem 3D: Megaton Edition (Steam)
+    Bsnprintf(buf, sizeof(buf), "%s/steamapps/common/Duke Nukem 3D/gameroot", basepath);
+    addsearchpath(buf);
+    Bsnprintf(buf, sizeof(buf), "%s/steamapps/common/Duke Nukem 3D/gameroot/addons/dc", basepath);
+    addsearchpath_user(buf, SEARCHPATH_REMOVE);
+    Bsnprintf(buf, sizeof(buf), "%s/steamapps/common/Duke Nukem 3D/gameroot/addons/nw", basepath);
+    addsearchpath_user(buf, SEARCHPATH_REMOVE);
+    Bsnprintf(buf, sizeof(buf), "%s/steamapps/common/Duke Nukem 3D/gameroot/addons/vacation", basepath);
+    addsearchpath_user(buf, SEARCHPATH_REMOVE);
+
+    // Duke Nukem 3D (3D Realms Anthology (Steam) / Kill-A-Ton Collection 2015)
+#if defined EDUKE32_OSX
+    Bsnprintf(buf, sizeof(buf), "%s/steamapps/common/Duke Nukem 3D/Duke Nukem 3D.app/drive_c/Program Files/Duke Nukem 3D", basepath);
+    addsearchpath_user(buf, SEARCHPATH_REMOVE);
+#endif
+#endif
+}
+
+// A bare-bones "parser" for Valve's KeyValues VDF format.
+// There is no guarantee this will function properly with ill-formed files.
+static void KeyValues_SkipWhitespace(char **vdfbuf, char * const vdfbufend)
+{
+    while (((*vdfbuf)[0] == ' ' || (*vdfbuf)[0] == '\n' || (*vdfbuf)[0] == '\r' || (*vdfbuf)[0] == '\t' || (*vdfbuf)[0] == '\0') && *vdfbuf < vdfbufend)
+        (*vdfbuf)++;
+
+    // comments
+    if ((*vdfbuf) + 2 < vdfbufend && (*vdfbuf)[0] == '/' && (*vdfbuf)[1] == '/')
+    {
+        while ((*vdfbuf)[0] != '\n' && (*vdfbuf)[0] != '\r' && *vdfbuf < vdfbufend)
+            (*vdfbuf)++;
+
+        KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+    }
+}
+static void KeyValues_SkipToEndOfQuotedToken(char **vdfbuf, char * const vdfbufend)
+{
+    (*vdfbuf)++;
+    while ((*vdfbuf)[0] != '\"' && (*vdfbuf)[-1] != '\\' && *vdfbuf < vdfbufend)
+        (*vdfbuf)++;
+}
+static void KeyValues_SkipToEndOfUnquotedToken(char **vdfbuf, char * const vdfbufend)
+{
+    while ((*vdfbuf)[0] != ' ' && (*vdfbuf)[0] != '\n' && (*vdfbuf)[0] != '\r' && (*vdfbuf)[0] != '\t' && (*vdfbuf)[0] != '\0' && *vdfbuf < vdfbufend)
+        (*vdfbuf)++;
+}
+static void KeyValues_SkipNextWhatever(char **vdfbuf, char * const vdfbufend)
+{
+    KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+
+    if (*vdfbuf == vdfbufend)
+        return;
+
+    if ((*vdfbuf)[0] == '{')
+    {
+        (*vdfbuf)++;
+        do
+        {
+            KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
+        }
+        while ((*vdfbuf)[0] != '}');
+        (*vdfbuf)++;
+    }
+    else if ((*vdfbuf)[0] == '\"')
+        KeyValues_SkipToEndOfQuotedToken(vdfbuf, vdfbufend);
+    else if ((*vdfbuf)[0] != '}')
+        KeyValues_SkipToEndOfUnquotedToken(vdfbuf, vdfbufend);
+
+    KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+}
+static char* KeyValues_NormalizeToken(char **vdfbuf, char * const vdfbufend)
+{
+    char *token = *vdfbuf;
+
+    if ((*vdfbuf)[0] == '\"' && *vdfbuf < vdfbufend)
+    {
+        token++;
+
+        KeyValues_SkipToEndOfQuotedToken(vdfbuf, vdfbufend);
+        (*vdfbuf)[0] = '\0';
+
+        // account for escape sequences
+        char *writeseeker = token, *readseeker = token;
+        while (readseeker <= *vdfbuf)
+        {
+            if (readseeker[0] == '\\')
+                readseeker++;
+
+            writeseeker[0] = readseeker[0];
+
+            writeseeker++;
+            readseeker++;
+        }
+
+        return token;
+    }
+
+    KeyValues_SkipToEndOfUnquotedToken(vdfbuf, vdfbufend);
+    (*vdfbuf)[0] = '\0';
+
+    return token;
+}
+static void KeyValues_FindKey(char **vdfbuf, char * const vdfbufend, const char *token)
+{
+    char *ParentKey = KeyValues_NormalizeToken(vdfbuf, vdfbufend);
+    if (token != NULL) // pass in NULL to find the next key instead of a specific one
+        while (Bstrcmp(ParentKey, token) != 0 && *vdfbuf < vdfbufend)
+        {
+            KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
+            ParentKey = KeyValues_NormalizeToken(vdfbuf, vdfbufend);
+        }
+
+    KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+}
+static int32_t KeyValues_FindParentKey(char **vdfbuf, char * const vdfbufend, const char *token)
+{
+    KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+
+    // end of scope
+    if ((*vdfbuf)[0] == '}')
+        return 0;
+
+    KeyValues_FindKey(vdfbuf, vdfbufend, token);
+
+    // ignore the wrong type
+    while ((*vdfbuf)[0] != '{' && *vdfbuf < vdfbufend)
+    {
+        KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
+        KeyValues_FindKey(vdfbuf, vdfbufend, token);
+    }
+
+    if (*vdfbuf == vdfbufend)
+        return 0;
+
+    return 1;
+}
+static char* KeyValues_FindKeyValue(char **vdfbuf, char * const vdfbufend, const char *token)
+{
+    KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+
+    // end of scope
+    if ((*vdfbuf)[0] == '}')
+        return NULL;
+
+    KeyValues_FindKey(vdfbuf, vdfbufend, token);
+
+    // ignore the wrong type
+    while ((*vdfbuf)[0] == '{' && *vdfbuf < vdfbufend)
+    {
+        KeyValues_SkipNextWhatever(vdfbuf, vdfbufend);
+        KeyValues_FindKey(vdfbuf, vdfbufend, token);
+    }
+
+    KeyValues_SkipWhitespace(vdfbuf, vdfbufend);
+
+    if (*vdfbuf == vdfbufend)
+        return NULL;
+
+    return KeyValues_NormalizeToken(vdfbuf, vdfbufend);
+}
+
+static void G_ParseSteamKeyValuesForPaths(const char *vdf)
+{
+    int32_t fd = Bopen(vdf, BO_RDONLY);
+    int32_t size = Bfilelength(fd);
+    char *vdfbufstart, *vdfbuf, *vdfbufend;
+
+    if (size <= 0)
+        return;
+
+    vdfbufstart = vdfbuf = (char*)Xmalloc(size);
+    size = (int32_t)Bread(fd, vdfbuf, size);
+    Bclose(fd);
+    vdfbufend = vdfbuf + size;
+
+    if (KeyValues_FindParentKey(&vdfbuf, vdfbufend, "LibraryFolders"))
+    {
+        char *result;
+        vdfbuf++;
+        while ((result = KeyValues_FindKeyValue(&vdfbuf, vdfbufend, NULL)) != NULL)
+            G_AddSteamPaths(result);
+    }
+
+    Bfree(vdfbufstart);
+}
+#endif
+#endif
+
+void G_AddSearchPaths(void)
+{
+#ifndef EDUKE32_TOUCH_DEVICES
+#if defined __linux__ || defined EDUKE32_BSD
+    char buf[BMAX_PATH];
+    char *homepath = Bgethomedir();
+
+    Bsnprintf(buf, sizeof(buf), "%s/.steam/steam", homepath);
+    G_AddSteamPaths(buf);
+
+    Bsnprintf(buf, sizeof(buf), "%s/.steam/steam/steamapps/libraryfolders.vdf", homepath);
+    G_ParseSteamKeyValuesForPaths(buf);
+
+    Bfree(homepath);
+
+    addsearchpath("/usr/share/games/nblood");
+    addsearchpath("/usr/local/share/games/nblood");
+#elif defined EDUKE32_OSX
+    char buf[BMAX_PATH];
+    int32_t i;
+    char *applications[] = { osx_getapplicationsdir(0), osx_getapplicationsdir(1) };
+    char *support[] = { osx_getsupportdir(0), osx_getsupportdir(1) };
+
+    for (i = 0; i < 2; i++)
+    {
+        Bsnprintf(buf, sizeof(buf), "%s/Steam", support[i]);
+        G_AddSteamPaths(buf);
+
+        Bsnprintf(buf, sizeof(buf), "%s/Steam/steamapps/libraryfolders.vdf", support[i]);
+        G_ParseSteamKeyValuesForPaths(buf);
+
+#if 0
+        // Duke Nukem 3D: Atomic Edition (GOG.com)
+        Bsnprintf(buf, sizeof(buf), "%s/Duke Nukem 3D.app/Contents/Resources/Duke Nukem 3D.boxer/C.harddisk", applications[i]);
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+#endif
+    }
+
+    for (i = 0; i < 2; i++)
+    {
+        Bsnprintf(buf, sizeof(buf), "%s/NBlood", support[i]);
+        addsearchpath(buf);
+    }
+
+    for (i = 0; i < 2; i++)
+    {
+        Bfree(applications[i]);
+        Bfree(support[i]);
+    }
+#elif defined (_WIN32)
+    char buf[BMAX_PATH] = {0};
+    DWORD bufsize;
+    bool found = false;
+
+    // Blood: One Unit Whole Blood (Steam)
+    bufsize = sizeof(buf);
+    if (!found && G_ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 299030)", "InstallLocation", buf, &bufsize))
+    {
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+        found = true;
+    }
+
+    // Blood: One Unit Whole Blood (GOG.com)
+    bufsize = sizeof(buf);
+    if (!found && G_ReadRegistryValue("SOFTWARE\\GOG.com\\GOGONEUNITONEBLOOD", "PATH", buf, &bufsize))
+    {
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+        found = true;
+    }
+
+    // Blood: Fresh Supply (Steam)
+    bufsize = sizeof(buf);
+    if (!found && G_ReadRegistryValue(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 1010750)", "InstallLocation", buf, &bufsize))
+    {
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+        strncat(buf, R"(\addons\Cryptic Passage)", 23);
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+        found = true;
+    }
+
+    // Blood: Fresh Supply (GOG.com)
+    bufsize = sizeof(buf);
+    if (!found && G_ReadRegistryValue(R"(SOFTWARE\Wow6432Node\GOG.com\Games\1374469660)", "path", buf, &bufsize))
+    {
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+        strncat(buf, R"(\addons\Cryptic Passage)", 23);
+        addsearchpath_user(buf, SEARCHPATH_REMOVE);
+        found = true;
+    }
+#endif
+#endif
+}
+
+void G_CleanupSearchPaths(void)
+{
+    removesearchpaths_withuser(SEARCHPATH_REMOVE);
+}
+
+//////////
+
+struct strllist *CommandPaths, *CommandGrps;
+
+void G_AddGroup(const char *buffer)
+{
+    char buf[BMAX_PATH];
+
+    struct strllist *s = (struct strllist *)Xcalloc(1,sizeof(struct strllist));
+
+    Bstrcpy(buf, buffer);
+
+    if (Bstrchr(buf,'.') == 0)
+        Bstrcat(buf,".grp");
+
+    s->str = Xstrdup(buf);
+
+    if (CommandGrps)
+    {
+        struct strllist *t;
+        for (t = CommandGrps; t->next; t=t->next) ;
+        t->next = s;
+        return;
+    }
+    CommandGrps = s;
+}
+
+void G_AddPath(const char *buffer)
+{
+    struct strllist *s = (struct strllist *)Xcalloc(1,sizeof(struct strllist));
+    s->str = Xstrdup(buffer);
+
+    if (CommandPaths)
+    {
+        struct strllist *t;
+        for (t = CommandPaths; t->next; t=t->next) ;
+        t->next = s;
+        return;
+    }
+    CommandPaths = s;
+}
+
+//////////
+
+// loads all group (grp, zip, pk3/4) files in the given directory
+void G_LoadGroupsInDir(const char *dirname)
+{
+    static const char *extensions[] = { "*.grp", "*.zip", "*.ssi", "*.pk3", "*.pk4" };
+    char buf[BMAX_PATH];
+    fnlist_t fnlist = FNLIST_INITIALIZER;
+
+    for (auto & extension : extensions)
+    {
+        CACHE1D_FIND_REC *rec;
+
+        fnlist_getnames(&fnlist, dirname, extension, -1, 0);
+
+        for (rec=fnlist.findfiles; rec; rec=rec->next)
+        {
+            Bsnprintf(buf, sizeof(buf), "%s/%s", dirname, rec->name);
+            initprintf("Using group file \"%s\".\n", buf);
+            initgroupfile(buf);
+        }
+
+        fnlist_clearnames(&fnlist);
+    }
+}
+
+void G_DoAutoload(const char *dirname)
+{
+    char buf[BMAX_PATH];
+
+    Bsnprintf(buf, sizeof(buf), "autoload/%s", dirname);
+    G_LoadGroupsInDir(buf);
+}
+
+//////////
+
+#ifdef FORMAT_UPGRADE_ELIGIBLE
+
+static int32_t S_TryFormats(char * const testfn, char * const fn_suffix, char const searchfirst)
+{
+#ifdef HAVE_FLAC
+    {
+        Bstrcpy(fn_suffix, ".flac");
+        int32_t const fp = kopen4loadfrommod(testfn, searchfirst);
+        if (fp >= 0)
+            return fp;
+    }
+#endif
+
+#ifdef HAVE_VORBIS
+    {
+        Bstrcpy(fn_suffix, ".ogg");
+        int32_t const fp = kopen4loadfrommod(testfn, searchfirst);
+        if (fp >= 0)
+            return fp;
+    }
+#endif
+
+    return -1;
+}
+
+static int32_t S_TryExtensionReplacements(char * const testfn, char const searchfirst, uint8_t const ismusic)
+{
+    char * extension = Bstrrchr(testfn, '.');
+    char * const fn_end = Bstrchr(testfn, '\0');
+
+    // ex: grabbag.voc --> grabbag_voc.*
+    if (extension != NULL)
+    {
+        *extension = '_';
+
+        int32_t const fp = S_TryFormats(testfn, fn_end, searchfirst);
+        if (fp >= 0)
+            return fp;
+    }
+    else
+    {
+        extension = fn_end;
+    }
+
+    // ex: grabbag.mid --> grabbag.*
+    if (ismusic)
+    {
+        int32_t const fp = S_TryFormats(testfn, extension, searchfirst);
+        if (fp >= 0)
+            return fp;
+    }
+
+    return -1;
+}
+
+int32_t S_OpenAudio(const char *fn, char searchfirst, uint8_t const ismusic)
+{
+    int32_t const     origfp       = kopen4loadfrommod(fn, searchfirst);
+    char const *const origparent   = origfp != -1 ? kfileparent(origfp) : NULL;
+    uint32_t const    parentlength = origparent != NULL ? Bstrlen(origparent) : 0;
+
+    auto testfn = (char *)Xmalloc(Bstrlen(fn) + 12 + parentlength); // "music/" + overestimation of parent minus extension + ".flac" + '\0'
+
+    // look in ./
+    // ex: ./grabbag.mid
+    Bstrcpy(testfn, fn);
+    int32_t fp = S_TryExtensionReplacements(testfn, searchfirst, ismusic);
+    if (fp >= 0)
+        goto success;
+
+    // look in ./music/<file's parent GRP name>/
+    // ex: ./music/duke3d/grabbag.mid
+    // ex: ./music/nwinter/grabbag.mid
+    if (origparent != NULL)
+    {
+        char const * const parentextension = Bstrrchr(origparent, '.');
+        uint32_t const namelength = parentextension != NULL ? (unsigned)(parentextension - origparent) : parentlength;
+
+        Bsprintf(testfn, "music/%.*s/%s", namelength, origparent, fn);
+        fp = S_TryExtensionReplacements(testfn, searchfirst, ismusic);
+        if (fp >= 0)
+            goto success;
+    }
+
+    // look in ./music/
+    // ex: ./music/grabbag.mid
+    Bsprintf(testfn, "music/%s", fn);
+    fp = S_TryExtensionReplacements(testfn, searchfirst, ismusic);
+    if (fp >= 0)
+        goto success;
+
+    fp = origfp;
+success:
+    Bfree(testfn);
+    if (fp != origfp)
+        kclose(origfp);
+
+    return fp;
+}
+
+#endif
+
diff --git a/source/blood/src/common_game.h b/source/blood/src/common_game.h
new file mode 100644
index 000000000..3aab66ce9
--- /dev/null
+++ b/source/blood/src/common_game.h
@@ -0,0 +1,499 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "baselayer.h"
+#include "build.h"
+#include "cache1d.h"
+#include "common.h"
+#include "pragmas.h"
+#include "misc.h"
+#include "network.h"
+
+extern int g_useCwd;
+
+#ifndef APPNAME
+#define APPNAME "NBlood"
+#endif
+
+#ifndef APPBASENAME
+#define APPBASENAME "nblood"
+#endif
+
+#define BYTEVERSION 102
+#define EXEVERSION 101
+
+void _SetErrorLoc(const char *pzFile, int nLine);
+void _ThrowError(const char *pzFormat, ...);
+void __dassert(const char *pzExpr, const char *pzFile, int nLine);
+
+#define ThrowError(...) \
+	{ \
+		_SetErrorLoc(__FILE__,__LINE__); \
+		_ThrowError(__VA_ARGS__); \
+	}
+
+#define dassert(x) if (!(x)) __dassert(#x,__FILE__,__LINE__)
+
+#define kMaxSectors 1024
+#define kMaxWalls 8192
+#define kMaxSprites 4096
+
+#define kMaxTiles MAXTILES
+#define kMaxStatus MAXSTATUS
+#define kMaxPlayers 8
+#define kMaxViewSprites maxspritesonscreen
+
+#define kMaxVoxels MAXVOXELS
+
+#define kTicRate 120
+#define kTicsPerFrame 4
+#define kTicsPerSec (kTicRate/kTicsPerFrame)
+
+// NUKE-TODO:
+#define OSDTEXT_DEFAULT   "^00"
+#define OSDTEXT_DARKRED   "^00"
+#define OSDTEXT_GREEN     "^00"
+#define OSDTEXT_RED       "^00"
+#define OSDTEXT_YELLOW    "^00"
+
+#define OSDTEXT_BRIGHT    "^S0"
+
+#define OSD_ERROR OSDTEXT_DARKRED OSDTEXT_BRIGHT
+
+enum BLOOD_GLOBALFLAGS {
+    BLOOD_FORCE_WIDELOADSCREEN = 1<<0,
+};
+
+enum searchpathtypes_t {
+    SEARCHPATH_REMOVE = 1<<0,
+};
+
+extern char *g_grpNamePtr;
+
+extern int loaddefinitions_game(const char *fn, int32_t preload);
+
+extern void G_AddSearchPaths(void);
+extern void G_CleanupSearchPaths(void);
+
+extern void G_ExtPreInit(int32_t argc, char const * const * argv);
+extern void G_ExtInit(void);
+
+void G_LoadGroupsInDir(const char *dirname);
+void G_DoAutoload(const char *dirname);
+extern void G_LoadGroups(int32_t autoload);
+
+#define G_ModDirSnprintf(buf, size, basename, ...)                                                                                          \
+    (((g_modDir[0] != '/') ? Bsnprintf(buf, size, "%s/" basename, g_modDir, ##__VA_ARGS__) : Bsnprintf(buf, size, basename, ##__VA_ARGS__)) \
+     >= ((int32_t)size) - 1)
+
+#define G_ModDirSnprintfLite(buf, size, basename) \
+    ((g_modDir[0] != '/') ? Bsnprintf(buf, size, "%s/%s", g_modDir, basename) : Bsnprintf(buf, size, "%s", basename))
+
+static inline void G_HandleAsync(void)
+{
+    handleevents();
+    netGetPackets();
+}
+
+#if defined HAVE_FLAC || defined HAVE_VORBIS
+# define FORMAT_UPGRADE_ELIGIBLE
+extern int32_t S_OpenAudio(const char *fn, char searchfirst, uint8_t ismusic);
+#else
+# define S_OpenAudio(fn, searchfirst, ismusic) kopen4loadfrommod(fn, searchfirst)
+#endif
+
+#pragma pack(push,1)
+
+#if 0
+struct sectortype
+{
+    short wallptr, wallnum;
+    int ceilingz, floorz;
+    unsigned short ceilingstat, floorstat;
+    short ceilingpicnum, ceilingheinum;
+    signed char ceilingshade;
+    char ceilingpal, ceilingxpanning, ceilingypanning;
+    short floorpicnum, floorheinum;
+    signed char floorshade;
+    char floorpal, floorxpanning, floorypanning;
+    char visibility, filler;
+    unsigned short lotag;
+    short hitag, extra;
+};
+
+struct walltype
+{
+    int x, y;
+    short point2, nextwall, nextsector;
+    unsigned short cstat;
+    short picnum, overpicnum;
+    signed char shade;
+    char pal, xrepeat, yrepeat, xpanning, ypanning;
+    short lotag, hitag, extra;
+};
+
+struct spritetype
+{
+    int x, y, z;
+    short cstat, picnum;
+    signed char shade;
+    char pal, clipdist, filler;
+    unsigned char xrepeat, yrepeat;
+    signed char xoffset, yoffset;
+    short sectnum, statnum;
+    short ang, owner, index, yvel, zvel;
+    short type, hitag, extra;
+};
+
+struct PICANM {
+    unsigned int animframes : 5;
+    unsigned int at0_5 : 1;
+    unsigned int animtype : 2;
+    signed int xoffset : 8;
+    signed int yoffset : 8;
+    unsigned int animspeed : 4;
+    unsigned int at3_4 : 3; // type
+    unsigned int at3_7 : 1; // filler
+};
+#endif
+
+struct LOCATION {
+    int x, y, z;
+    int ang;
+};
+
+struct POINT2D {
+    int x, y;
+};
+
+struct POINT3D {
+    int x, y, z;
+};
+
+struct VECTOR2D {
+    int dx, dy;
+};
+
+struct Aim {
+    int dx, dy, dz;
+};
+
+#pragma pack(pop)
+
+extern char qsprite_filler[], qsector_filler[];
+
+inline int ksgnf(float f)
+{
+    if (f < 0)
+        return -1;
+    if (f > 0)
+        return 1;
+    return 0;
+}
+
+inline int IncBy(int a, int b)
+{
+    a += b;
+    int q = a % b;
+    a -= q;
+    if (q < 0)
+        a -= b;
+    return a;
+}
+
+inline int DecBy(int a, int b)
+{
+    a--;
+    int q = a % b;
+    a -= q;
+    if (q < 0)
+        a -= b;
+    return a;
+}
+
+#if 0
+inline float IncByF(float a, float b)
+{
+    a += b;
+    float q = fmod(a, b);
+    a -= q;
+    if (q < 0)
+        a -= b;
+    return a;
+}
+
+inline float DecByF(float a, float b)
+{
+    //a--;
+    a -= fabs(b)*0.001;
+    float q = fmod(a, b);
+    a -= q;
+    if (q < 0)
+        a -= b;
+    return a;
+}
+#endif
+
+inline int ClipLow(int a, int b)
+{
+    if (a < b)
+        return b;
+    return a;
+}
+
+inline int ClipHigh(int a, int b)
+{
+    if (a >= b)
+        return b;
+    return a;
+}
+
+inline int ClipRange(int a, int b, int c)
+{
+    if (a < b)
+        return b;
+    if (a > c)
+        return c;
+    return a;
+}
+
+inline float ClipRangeF(float a, float b, float c)
+{
+    if (a < b)
+        return b;
+    if (a > c)
+        return c;
+    return a;
+}
+
+inline int interpolate(int a, int b, int c)
+{
+    return a+mulscale16(b-a,c);
+}
+
+inline int interpolateang(int a, int b, int c)
+{
+    return a+mulscale16(((b-a+1024)&2047)-1024, c);
+}
+
+inline fix16_t interpolateangfix16(fix16_t a, fix16_t b, int c)
+{
+    return a+mulscale16(((b-a+0x4000000)&0x7ffffff)-0x4000000, c);
+}
+
+inline char Chance(int a1)
+{
+    return wrand() < (a1>>1);
+}
+
+inline unsigned int Random(int a1)
+{
+    return mulscale(wrand(), a1, 15);
+}
+
+inline int Random2(int a1)
+{
+    return mulscale(wrand(), a1, 14)-a1;
+}
+
+inline int Random3(int a1)
+{
+    return mulscale(wrand()+wrand(), a1, 15) - a1;
+}
+
+inline unsigned int QRandom(int a1)
+{
+    return mulscale(qrand(), a1, 15);
+}
+
+inline int QRandom2(int a1)
+{
+    return mulscale(qrand(), a1, 14)-a1;
+}
+
+inline void SetBitString(char *pArray, int nIndex)
+{
+    pArray[nIndex>>3] |= 1<<(nIndex&7);
+}
+
+inline void ClearBitString(char *pArray, int nIndex)
+{
+    pArray[nIndex >> 3] &= ~(1 << (nIndex & 7));
+}
+
+inline char TestBitString(char *pArray, int nIndex)
+{
+    return pArray[nIndex>>3] & (1<<(nIndex&7));
+}
+
+inline int scale(int a1, int a2, int a3, int a4, int a5)
+{
+    return a4 + (a5-a4) * (a1-a2) / (a3-a2);
+}
+
+inline int mulscale16r(int a, int b)
+{
+    int64_t acc = 1<<(16-1);
+    acc += ((int64_t)a) * b;
+    return (int)(acc>>16);
+}
+
+inline int mulscale30r(int a, int b)
+{
+    int64_t acc = 1<<(30-1);
+    acc += ((int64_t)a) * b;
+    return (int)(acc>>30);
+}
+
+inline int dmulscale30r(int a, int b, int c, int d)
+{
+    int64_t acc = 1<<(30-1);
+    acc += ((int64_t)a) * b;
+    acc += ((int64_t)c) * d;
+    return (int)(acc>>30);
+}
+
+inline int approxDist(int dx, int dy)
+{
+    dx = klabs(dx);
+    dy = klabs(dy);
+    if (dx > dy)
+        dy = (3*dy)>>3;
+    else
+        dx = (3*dx)>>3;
+    return dx+dy;
+}
+
+class Rect {
+public:
+    int x1, y1, x2, y2;
+    Rect(int _x1, int _y1, int _x2, int _y2)
+    {
+        x1 = _x1; y1 = _y1; x2 = _x2; y2 = _y2;
+    }
+    bool isValid(void) const
+    {
+        return x1 < x2 && y1 < y2;
+    }
+    char isEmpty(void) const
+    {
+        return !(x1 < x2 && y1 < y2);
+    }
+    bool operator!(void) const
+    {
+        return isEmpty();
+    }
+
+    Rect & operator&=(Rect &pOther)
+    {
+        x1 = ClipLow(x1, pOther.x1);
+        y1 = ClipLow(y1, pOther.y1);
+        x2 = ClipHigh(x2, pOther.x2);
+        y2 = ClipHigh(y2, pOther.y2);
+        return *this;
+    }
+};
+
+class BitReader {
+public:
+    int nBitPos;
+    int nSize;
+    char *pBuffer;
+    BitReader(char *_pBuffer, int _nSize, int _nBitPos) { pBuffer = _pBuffer; nSize = _nSize; nBitPos = _nBitPos; nSize -= nBitPos>>3; }
+    BitReader(char *_pBuffer, int _nSize) { pBuffer = _pBuffer; nSize = _nSize; nBitPos = 0; }
+    int readBit()
+    {
+        if (nSize <= 0)
+            ThrowError("Buffer overflow");
+        int bit = ((*pBuffer)>>nBitPos)&1;
+        if (++nBitPos >= 8)
+        {
+            nBitPos = 0;
+            pBuffer++;
+            nSize--;
+        }
+        return bit;
+    }
+    void skipBits(int nBits)
+    {
+        nBitPos += nBits;
+        pBuffer += nBitPos>>3;
+        nSize -= nBitPos>>3;
+        nBitPos &= 7;
+        if ((nSize == 0 && nBitPos > 0) || nSize < 0)
+            ThrowError("Buffer overflow");
+    }
+    unsigned int readUnsigned(int nBits)
+    {
+        unsigned int n = 0;
+        dassert(nBits <= 32);
+        for (int i = 0; i < nBits; i++)
+            n += readBit()<<i;
+        return n;
+    }
+    int readSigned(int nBits)
+    {
+        dassert(nBits <= 32);
+        int n = (int)readUnsigned(nBits);
+        n <<= 32-nBits;
+        n >>= 32-nBits;
+        return n;
+    }
+};
+
+class BitWriter {
+public:
+    int nBitPos;
+    int nSize;
+    char *pBuffer;
+    BitWriter(char *_pBuffer, int _nSize, int _nBitPos) { pBuffer = _pBuffer; nSize = _nSize; nBitPos = _nBitPos; memset(pBuffer, 0, nSize); nSize -= nBitPos>>3; }
+    BitWriter(char *_pBuffer, int _nSize) { pBuffer = _pBuffer; nSize = _nSize; nBitPos = 0; memset(pBuffer, 0, nSize); }
+    void writeBit(int bit)
+    {
+        if (nSize <= 0)
+            ThrowError("Buffer overflow");
+        *pBuffer |= bit<<nBitPos;
+        if (++nBitPos >= 8)
+        {
+            nBitPos = 0;
+            pBuffer++;
+            nSize--;
+        }
+    }
+    void skipBits(int nBits)
+    {
+        nBitPos += nBits;
+        pBuffer += nBitPos>>3;
+        nSize -= nBitPos>>3;
+        nBitPos &= 7;
+        if ((nSize == 0 && nBitPos > 0) || nSize < 0)
+            ThrowError("Buffer overflow");
+    }
+    void write(int nValue, int nBits)
+    {
+        dassert(nBits <= 32);
+        for (int i = 0; i < nBits; i++)
+            writeBit((nValue>>i)&1);
+    }
+};
+
diff --git a/source/blood/src/config.cpp b/source/blood/src/config.cpp
new file mode 100644
index 000000000..fd81aeb2b
--- /dev/null
+++ b/source/blood/src/config.cpp
@@ -0,0 +1,1079 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#include "baselayer.h"
+#include "common_game.h"
+#include "build.h"
+#include "cache1d.h"
+#include "hash.h"
+#include "scriplib.h"
+#include "renderlayer.h"
+#include "function.h"
+#include "blood.h"
+#include "gamedefs.h"
+#include "config.h"
+#include "view.h"
+
+#ifdef __ANDROID__
+# include "android.h"
+#endif
+
+#if defined RENDERTYPESDL && defined SDL_TARGET && SDL_TARGET > 1
+# include "sdl_inc.h"
+#endif
+
+// we load this in to get default button and key assignments
+// as well as setting up function mappings
+
+#define __SETUP__   // JBF 20031211
+#include "_functio.h"
+
+hashtable_t h_gamefuncs    = { NUMGAMEFUNCTIONS<<1, NULL };
+
+int32_t MAXCACHE1DSIZE = (96*1024*1024);
+
+int32_t MouseDeadZone, MouseBias;
+int32_t SmoothInput;
+int32_t MouseFunctions[MAXMOUSEBUTTONS][2];
+int32_t MouseDigitalFunctions[MAXMOUSEAXES][2];
+int32_t MouseAnalogueAxes[MAXMOUSEAXES];
+int32_t MouseAnalogueScale[MAXMOUSEAXES];
+int32_t JoystickFunctions[MAXJOYBUTTONSANDHATS][2];
+int32_t JoystickDigitalFunctions[MAXJOYAXES][2];
+int32_t JoystickAnalogueAxes[MAXJOYAXES];
+int32_t JoystickAnalogueScale[MAXJOYAXES];
+int32_t JoystickAnalogueDead[MAXJOYAXES];
+int32_t JoystickAnalogueSaturate[MAXJOYAXES];
+uint8_t KeyboardKeys[NUMGAMEFUNCTIONS][2];
+int32_t scripthandle;
+int32_t setupread;
+int32_t SoundToggle;
+int32_t MusicToggle;
+int32_t MusicRestartsOnLoadToggle;
+int32_t CDAudioToggle;
+int32_t FXVolume;
+int32_t MusicVolume;
+int32_t CDVolume;
+int32_t NumVoices;
+int32_t NumChannels;
+int32_t NumBits;
+int32_t MixRate;
+int32_t ReverseStereo;
+int32_t MusicDevice;
+int32_t configversion;
+int32_t CheckForUpdates;
+int32_t LastUpdateCheck;
+int32_t useprecache;
+char CommbatMacro[MAXRIDECULE][MAXRIDECULELENGTH];
+char szPlayerName[MAXPLAYERNAME];
+int32_t gTurnSpeed;
+int32_t gDetail;
+int32_t gMouseAim;
+int32_t gAutoAim;
+int32_t gWeaponSwitch;
+int32_t gAutoRun;
+int32_t gViewInterpolate;
+int32_t gViewHBobbing;
+int32_t gViewVBobbing;
+int32_t gFollowMap;
+int32_t gOverlayMap;
+int32_t gRotateMap;
+int32_t gAimReticle;
+int32_t gSlopeTilting;
+int32_t gMessageState;
+int32_t gMessageCount;
+int32_t gMessageTime;
+int32_t gMessageFont;
+int32_t gbAdultContent;
+char gzAdultPassword[9];
+int32_t gDoppler;
+int32_t gShowWeapon;
+int32_t gMouseSensitivity;
+int32_t gMouseAiming;
+int32_t gMouseAimingFlipped;
+int32_t gRunKeyMode;
+bool gNoClip;
+bool gInfiniteAmmo;
+bool gFullMap;
+int32_t gUpscaleFactor;
+int32_t gBrightness;
+int32_t gLevelStats;
+int32_t gPowerupDuration;
+int32_t gShowMapTitle;
+int32_t gFov;
+int32_t gCenterHoriz;
+int32_t gDeliriumBlur;
+
+int32_t CONFIG_FunctionNameToNum(const char *func)
+{
+    int32_t i;
+
+    if (!func)
+        return -1;
+
+    i = hash_find(&h_gamefuncs,func);
+
+    if (i < 0)
+    {
+        char *str = Bstrtolower(Xstrdup(func));
+        i = hash_find(&h_gamefuncs,str);
+        Bfree(str);
+
+        return i;
+    }
+
+    return i;
+}
+
+
+char *CONFIG_FunctionNumToName(int32_t func)
+{
+    if ((unsigned)func >= (unsigned)NUMGAMEFUNCTIONS)
+        return NULL;
+    return gamefunctions[func];
+}
+
+
+int32_t CONFIG_AnalogNameToNum(const char *func)
+{
+    if (!func)
+        return -1;
+
+    if (!Bstrcasecmp(func,"analog_turning"))
+    {
+        return analog_turning;
+    }
+    if (!Bstrcasecmp(func,"analog_strafing"))
+    {
+        return analog_strafing;
+    }
+    if (!Bstrcasecmp(func,"analog_moving"))
+    {
+        return analog_moving;
+    }
+    if (!Bstrcasecmp(func,"analog_lookingupanddown"))
+    {
+        return analog_lookingupanddown;
+    }
+
+    return -1;
+}
+
+
+const char *CONFIG_AnalogNumToName(int32_t func)
+{
+    switch (func)
+    {
+    case analog_turning:
+        return "analog_turning";
+    case analog_strafing:
+        return "analog_strafing";
+    case analog_moving:
+        return "analog_moving";
+    case analog_lookingupanddown:
+        return "analog_lookingupanddown";
+    }
+
+    return NULL;
+}
+
+
+void CONFIG_SetDefaultKeys(const char (*keyptr)[MAXGAMEFUNCLEN], bool lazy/*=false*/)
+{
+    static char const s_gamefunc_[] = "gamefunc_";
+    int constexpr strlen_gamefunc_ = ARRAY_SIZE(s_gamefunc_) - 1;
+
+    if (!lazy)
+    {
+        Bmemset(KeyboardKeys, 0xff, sizeof(KeyboardKeys));
+        CONTROL_ClearAllBinds();
+    }
+
+    for (int i=0; i < ARRAY_SSIZE(gamefunctions); ++i)
+    {
+        if (gamefunctions[i][0] == '\0')
+            continue;
+
+        auto &key = KeyboardKeys[i];
+
+        int const default0 = KB_StringToScanCode(keyptr[i<<1]);
+        int const default1 = KB_StringToScanCode(keyptr[(i<<1)+1]);
+
+        // skip the function if the default key is already used
+        // or the function is assigned to another key
+        if (lazy && (key[0] != 0xff || (CONTROL_KeyIsBound(default0) && Bstrlen(CONTROL_KeyBinds[default0].cmdstr) > strlen_gamefunc_
+                        && CONFIG_FunctionNameToNum(CONTROL_KeyBinds[default0].cmdstr + strlen_gamefunc_) >= 0)))
+        {
+#if 0 // defined(DEBUGGINGAIDS)
+            if (key[0] != 0xff)
+                initprintf("Skipping %s bound to %s\n", keyptr[i<<1], CONTROL_KeyBinds[default0].cmdstr);
+#endif
+            continue;
+        }
+
+        key[0] = default0;
+        key[1] = default1;
+
+        if (key[0])
+            CONTROL_FreeKeyBind(key[0]);
+
+        if (key[1])
+            CONTROL_FreeKeyBind(key[1]);
+
+        if (i == gamefunc_Show_Console)
+            OSD_CaptureKey(key[0]);
+        else
+            CONFIG_MapKey(i, key[0], 0, key[1], 0);
+    }
+}
+
+
+void CONFIG_SetDefaults(void)
+{
+    scripthandle = -1;
+
+#ifdef __ANDROID__
+    droidinput.forward_sens = 5.f;
+    droidinput.gameControlsAlpha = 0.5;
+    droidinput.hideStick = 0;
+    droidinput.pitch_sens = 5.f;
+    droidinput.quickSelectWeapon = 1;
+    droidinput.strafe_sens = 5.f;
+    droidinput.toggleCrouch = 1;
+    droidinput.yaw_sens = 5.f;
+
+    gSetup.xdim = droidinfo.screen_width;
+    gSetup.ydim = droidinfo.screen_height;
+#else
+# if defined RENDERTYPESDL && SDL_MAJOR_VERSION > 1
+    uint32_t inited = SDL_WasInit(SDL_INIT_VIDEO);
+    if (inited == 0)
+        SDL_Init(SDL_INIT_VIDEO);
+    else if (!(inited & SDL_INIT_VIDEO))
+        SDL_InitSubSystem(SDL_INIT_VIDEO);
+
+    SDL_DisplayMode dm;
+    if (SDL_GetDesktopDisplayMode(0, &dm) == 0)
+    {
+        gSetup.xdim = dm.w;
+        gSetup.ydim = dm.h;
+    }
+    else
+# endif
+    {
+        gSetup.xdim = 1024;
+        gSetup.ydim = 768;
+    }
+#endif
+
+#ifdef USE_OPENGL
+    gSetup.bpp = 32;
+#else
+    gSetup.bpp = 8;
+#endif
+
+#if defined(_WIN32)
+    MixRate = 44100;
+#elif defined __ANDROID__
+    MixRate = droidinfo.audio_sample_rate;
+#else
+    MixRate = 48000;
+#endif
+
+#if defined GEKKO || defined __OPENDINGUX__
+    NumVoices = 32;
+#else
+    NumVoices = 64;
+#endif
+
+#ifdef GEKKO
+    gSetup.usejoystick = 1;
+#else
+    gSetup.usejoystick = 0;
+#endif
+
+    gSetup.forcesetup       = 1;
+    gSetup.noautoload       = 1;
+    gSetup.fullscreen       = 1;
+    gSetup.usemouse         = 1;
+
+    //ud.config.AmbienceToggle  = 1;
+    //ud.config.AutoAim         = 1;
+    CheckForUpdates = 1;
+    FXVolume        = 255;
+    MouseBias       = 0;
+    MouseDeadZone   = 0;
+    MusicToggle     = 1;
+    MusicRestartsOnLoadToggle = 0;
+    MusicVolume     = 195;
+    NumBits         = 16;
+    NumChannels     = 2;
+    ReverseStereo   = 0;
+    gBrightness = 8;
+    //ud.config.ShowWeapons     = 0;
+    SmoothInput     = 1;
+    SoundToggle     = 1;
+    CDAudioToggle = 0;
+    MusicDevice = 0;
+    //ud.config.VoiceToggle     = 5;  // bitfield, 1 = local, 2 = dummy, 4 = other players in DM
+    useprecache     = 1;
+    configversion          = 0;
+    //ud.crosshair              = 1;
+    //ud.crosshairscale         = 50;
+    //ud.default_skill          = 1;
+    //ud.democams               = 1;
+    gUpscaleFactor = 0;
+    //ud.display_bonus_screen   = 1;
+    //ud.drawweapon             = 1;
+    //ud.hudontop               = 0;
+    //ud.idplayers              = 1;
+    gLevelStats = 0;
+    gPowerupDuration = 1;
+    gShowMapTitle = 1;
+    //ud.lockout                = 0;
+    //ud.m_marker               = 1;
+    //ud.maxautosaves           = 5;
+    //ud.menu_scrollbartilenum  = -1;
+    //ud.menu_scrollbarz        = 65536;
+    //ud.menu_scrollcursorz     = 65536;
+    //ud.menu_slidebarmargin    = 65536;
+    //ud.menu_slidebarz         = 65536;
+    //ud.menu_slidecursorz      = 65536;
+    //ud.menubackground         = 1;
+    //ud.mouseaiming            = 0;
+    //ud.mouseflip              = 1;
+    //ud.msgdisptime            = 120;
+    //ud.obituaries             = 1;
+    //ud.pwlockout[0]           = '\0';
+    gRunKeyMode            = 0;
+    //ud.screen_size            = 4;
+    //ud.screen_tilting         = 1;
+    //ud.screenfade             = 1;
+    //ud.shadow_pal             = 4;
+    //ud.shadows                = 1;
+    //ud.show_level_text        = 1;
+    //ud.slidebar_paldisabled   = 1;
+    //ud.statusbarflags         = STATUSBAR_NOSHRINK;
+    //ud.statusbarmode          = 1;
+    //ud.statusbarscale         = 100;
+    //ud.team                   = 0;
+    //ud.textscale              = 200;
+    //ud.viewbob                = 1;
+    //ud.weaponscale            = 100;
+    //ud.weaponsway             = 1;
+    //ud.weaponswitch           = 3;  // new+empty
+    gFov = 90;
+    gCenterHoriz = 0;
+    gDeliriumBlur = 1;
+    gViewSize = 2;
+    gTurnSpeed = 92;
+    gDetail = 4;
+    gAutoRun = 0;
+    gViewInterpolate = 1;
+    gViewHBobbing = 1;
+    gViewVBobbing = 1;
+    gFollowMap = 1;
+    gOverlayMap = 0;
+    gRotateMap = 0;
+    gAimReticle = 0;
+    gSlopeTilting = 0;
+    gMessageState = 1;
+    gMessageCount = 4;
+    gMessageTime = 5;
+    gMessageFont = 0;
+    gbAdultContent = 0;
+    gDoppler = 1;
+    gShowWeapon = 0;
+    gzAdultPassword[0] = 0;
+
+    gMouseAimingFlipped = 0;
+    gMouseAim = 1;
+    gAutoAim = 1;
+    gWeaponSwitch = 1;
+
+    Bstrcpy(szPlayerName, "Player");
+
+    Bstrcpy(CommbatMacro[0], "I love the smell of napalm...");
+    Bstrcpy(CommbatMacro[1], "Is that gasoline I smell?");
+    Bstrcpy(CommbatMacro[2], "Ta da!");
+    Bstrcpy(CommbatMacro[3], "Who wants some, huh? Who's next?");
+    Bstrcpy(CommbatMacro[4], "I have something for you.");
+    Bstrcpy(CommbatMacro[5], "You just gonna stand there...");
+    Bstrcpy(CommbatMacro[6], "That'll teach ya!");
+    Bstrcpy(CommbatMacro[7], "Ooh, that wasn't a bit nice.");
+    Bstrcpy(CommbatMacro[8], "Amateurs!");
+    Bstrcpy(CommbatMacro[9], "Fool! You are already dead.");
+
+    CONFIG_SetDefaultKeys(keydefaults);
+
+    memset(MouseFunctions, -1, sizeof(MouseFunctions));
+    memset(MouseDigitalFunctions, -1, sizeof(MouseDigitalFunctions));
+    memset(JoystickFunctions, -1, sizeof(JoystickFunctions));
+    memset(JoystickDigitalFunctions, -1, sizeof(JoystickDigitalFunctions));
+
+    CONTROL_MouseSensitivity = DEFAULTMOUSESENSITIVITY;
+
+    for (int i=0; i<MAXMOUSEBUTTONS; i++)
+    {
+        MouseFunctions[i][0] = CONFIG_FunctionNameToNum(mousedefaults[i]);
+        CONTROL_MapButton(MouseFunctions[i][0], i, 0, controldevice_mouse);
+        if (i>=4) continue;
+        MouseFunctions[i][1] = CONFIG_FunctionNameToNum(mouseclickeddefaults[i]);
+        CONTROL_MapButton(MouseFunctions[i][1], i, 1, controldevice_mouse);
+    }
+
+    for (int i=0; i<MAXMOUSEAXES; i++)
+    {
+        MouseAnalogueScale[i] = DEFAULTMOUSEANALOGUESCALE;
+        CONTROL_SetAnalogAxisScale(i, MouseAnalogueScale[i], controldevice_mouse);
+
+        MouseDigitalFunctions[i][0] = CONFIG_FunctionNameToNum(mousedigitaldefaults[i*2]);
+        MouseDigitalFunctions[i][1] = CONFIG_FunctionNameToNum(mousedigitaldefaults[i*2+1]);
+        CONTROL_MapDigitalAxis(i, MouseDigitalFunctions[i][0], 0, controldevice_mouse);
+        CONTROL_MapDigitalAxis(i, MouseDigitalFunctions[i][1], 1, controldevice_mouse);
+
+        MouseAnalogueAxes[i] = CONFIG_AnalogNameToNum(mouseanalogdefaults[i]);
+        CONTROL_MapAnalogAxis(i, MouseAnalogueAxes[i], controldevice_mouse);
+    }
+
+    for (int i=0; i<MAXJOYBUTTONSANDHATS; i++)
+    {
+        JoystickFunctions[i][0] = CONFIG_FunctionNameToNum(joystickdefaults[i]);
+        JoystickFunctions[i][1] = CONFIG_FunctionNameToNum(joystickclickeddefaults[i]);
+        CONTROL_MapButton(JoystickFunctions[i][0], i, 0, controldevice_joystick);
+        CONTROL_MapButton(JoystickFunctions[i][1], i, 1, controldevice_joystick);
+    }
+
+    for (int i=0; i<MAXJOYAXES; i++)
+    {
+        JoystickAnalogueScale[i] = DEFAULTJOYSTICKANALOGUESCALE;
+        JoystickAnalogueDead[i] = DEFAULTJOYSTICKANALOGUEDEAD;
+        JoystickAnalogueSaturate[i] = DEFAULTJOYSTICKANALOGUESATURATE;
+        CONTROL_SetAnalogAxisScale(i, JoystickAnalogueScale[i], controldevice_joystick);
+
+        JoystickDigitalFunctions[i][0] = CONFIG_FunctionNameToNum(joystickdigitaldefaults[i*2]);
+        JoystickDigitalFunctions[i][1] = CONFIG_FunctionNameToNum(joystickdigitaldefaults[i*2+1]);
+        CONTROL_MapDigitalAxis(i, JoystickDigitalFunctions[i][0], 0, controldevice_joystick);
+        CONTROL_MapDigitalAxis(i, JoystickDigitalFunctions[i][1], 1, controldevice_joystick);
+
+        JoystickAnalogueAxes[i] = CONFIG_AnalogNameToNum(joystickanalogdefaults[i]);
+        CONTROL_MapAnalogAxis(i, JoystickAnalogueAxes[i], controldevice_joystick);
+    }
+}
+
+
+// wrapper for CONTROL_MapKey(), generates key bindings to reflect changes to keyboard setup
+void CONFIG_MapKey(int which, kb_scancode key1, kb_scancode oldkey1, kb_scancode key2, kb_scancode oldkey2)
+{
+    int const keys[] = { key1, key2, oldkey1, oldkey2 };
+    char buf[2*MAXGAMEFUNCLEN];
+    char tempbuf[128];
+
+    if (which == gamefunc_Show_Console)
+        OSD_CaptureKey(key1);
+
+    for (int k = 0; (unsigned)k < ARRAY_SIZE(keys); k++)
+    {
+        if (keys[k] == 0xff || !keys[k])
+            continue;
+
+        int match = 0;
+
+        for (; sctokeylut[match].key; match++)
+        {
+            if (keys[k] == sctokeylut[match].sc)
+                break;
+        }
+
+        tempbuf[0] = 0;
+
+        for (int i=NUMGAMEFUNCTIONS-1; i>=0; i--)
+        {
+            if (KeyboardKeys[i][0] == keys[k] || KeyboardKeys[i][1] == keys[k])
+            {
+                Bsprintf(buf, "gamefunc_%s; ", CONFIG_FunctionNumToName(i));
+                Bstrcat(tempbuf,buf);
+            }
+        }
+
+        int const len = Bstrlen(tempbuf);
+
+        if (len >= 2)
+        {
+            tempbuf[len-2] = 0;  // cut off the trailing "; "
+            CONTROL_BindKey(keys[k], tempbuf, 1, sctokeylut[match].key ? sctokeylut[match].key : "<?>");
+        }
+        else
+        {
+            CONTROL_FreeKeyBind(keys[k]);
+        }
+    }
+}
+
+
+void CONFIG_SetupMouse(void)
+{
+    if (scripthandle < 0)
+        return;
+
+    char str[80];
+    char temp[80];
+
+    for (int i=0; i<MAXMOUSEBUTTONS; i++)
+    {
+        Bsprintf(str,"MouseButton%d",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle,"Controls", str,temp))
+            MouseFunctions[i][0] = CONFIG_FunctionNameToNum(temp);
+
+        Bsprintf(str,"MouseButtonClicked%d",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle,"Controls", str,temp))
+            MouseFunctions[i][1] = CONFIG_FunctionNameToNum(temp);
+    }
+
+    // map over the axes
+    for (int i=0; i<MAXMOUSEAXES; i++)
+    {
+        Bsprintf(str,"MouseAnalogAxes%d",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle, "Controls", str,temp))
+            if (CONFIG_AnalogNameToNum(temp) != -1 || (!temp[0] && CONFIG_FunctionNameToNum(temp) != -1))
+                MouseAnalogueAxes[i] = CONFIG_AnalogNameToNum(temp);
+
+        Bsprintf(str,"MouseDigitalAxes%d_0",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle, "Controls", str,temp))
+            if (CONFIG_FunctionNameToNum(temp) != -1 || (!temp[0] && CONFIG_FunctionNameToNum(temp) != -1))
+                MouseDigitalFunctions[i][0] = CONFIG_FunctionNameToNum(temp);
+
+        Bsprintf(str,"MouseDigitalAxes%d_1",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle, "Controls", str,temp))
+            if (CONFIG_FunctionNameToNum(temp) != -1 || (!temp[0] && CONFIG_FunctionNameToNum(temp) != -1))
+                MouseDigitalFunctions[i][1] = CONFIG_FunctionNameToNum(temp);
+
+        Bsprintf(str,"MouseAnalogScale%d",i);
+        int32_t scale = MouseAnalogueScale[i];
+        SCRIPT_GetNumber(scripthandle, "Controls", str, &scale);
+        MouseAnalogueScale[i] = scale;
+    }
+
+    for (int i=0; i<MAXMOUSEBUTTONS; i++)
+    {
+        CONTROL_MapButton(MouseFunctions[i][0], i, 0, controldevice_mouse);
+        CONTROL_MapButton(MouseFunctions[i][1], i, 1,  controldevice_mouse);
+    }
+    for (int i=0; i<MAXMOUSEAXES; i++)
+    {
+        CONTROL_MapAnalogAxis(i, MouseAnalogueAxes[i], controldevice_mouse);
+        CONTROL_MapDigitalAxis(i, MouseDigitalFunctions[i][0], 0,controldevice_mouse);
+        CONTROL_MapDigitalAxis(i, MouseDigitalFunctions[i][1], 1,controldevice_mouse);
+        CONTROL_SetAnalogAxisScale(i, MouseAnalogueScale[i], controldevice_mouse);
+    }
+}
+
+
+void CONFIG_SetupJoystick(void)
+{
+    int32_t i;
+    char str[80];
+    char temp[80];
+    int32_t scale;
+
+    if (scripthandle < 0) return;
+
+    for (i=0; i<MAXJOYBUTTONSANDHATS; i++)
+    {
+        Bsprintf(str,"JoystickButton%d",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle,"Controls", str,temp))
+            JoystickFunctions[i][0] = CONFIG_FunctionNameToNum(temp);
+
+        Bsprintf(str,"JoystickButtonClicked%d",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle,"Controls", str,temp))
+            JoystickFunctions[i][1] = CONFIG_FunctionNameToNum(temp);
+    }
+
+    // map over the axes
+    for (i=0; i<MAXJOYAXES; i++)
+    {
+        Bsprintf(str,"JoystickAnalogAxes%d",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle, "Controls", str,temp))
+            if (CONFIG_AnalogNameToNum(temp) != -1 || (!temp[0] && CONFIG_FunctionNameToNum(temp) != -1))
+                JoystickAnalogueAxes[i] = CONFIG_AnalogNameToNum(temp);
+
+        Bsprintf(str,"JoystickDigitalAxes%d_0",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle, "Controls", str,temp))
+            if (CONFIG_FunctionNameToNum(temp) != -1 || (!temp[0] && CONFIG_FunctionNameToNum(temp) != -1))
+                JoystickDigitalFunctions[i][0] = CONFIG_FunctionNameToNum(temp);
+
+        Bsprintf(str,"JoystickDigitalAxes%d_1",i);
+        temp[0] = 0;
+        if (!SCRIPT_GetString(scripthandle, "Controls", str,temp))
+            if (CONFIG_FunctionNameToNum(temp) != -1 || (!temp[0] && CONFIG_FunctionNameToNum(temp) != -1))
+                JoystickDigitalFunctions[i][1] = CONFIG_FunctionNameToNum(temp);
+
+        Bsprintf(str,"JoystickAnalogScale%d",i);
+        scale = JoystickAnalogueScale[i];
+        SCRIPT_GetNumber(scripthandle, "Controls", str,&scale);
+        JoystickAnalogueScale[i] = scale;
+
+        Bsprintf(str,"JoystickAnalogDead%d",i);
+        scale = JoystickAnalogueDead[i];
+        SCRIPT_GetNumber(scripthandle, "Controls", str,&scale);
+        JoystickAnalogueDead[i] = scale;
+
+        Bsprintf(str,"JoystickAnalogSaturate%d",i);
+        scale = JoystickAnalogueSaturate[i];
+        SCRIPT_GetNumber(scripthandle, "Controls", str,&scale);
+        JoystickAnalogueSaturate[i] = scale;
+    }
+
+    for (i=0; i<MAXJOYBUTTONSANDHATS; i++)
+    {
+        CONTROL_MapButton(JoystickFunctions[i][0], i, 0, controldevice_joystick);
+        CONTROL_MapButton(JoystickFunctions[i][1], i, 1,  controldevice_joystick);
+    }
+    for (i=0; i<MAXJOYAXES; i++)
+    {
+        CONTROL_MapAnalogAxis(i, JoystickAnalogueAxes[i], controldevice_joystick);
+        CONTROL_MapDigitalAxis(i, JoystickDigitalFunctions[i][0], 0, controldevice_joystick);
+        CONTROL_MapDigitalAxis(i, JoystickDigitalFunctions[i][1], 1, controldevice_joystick);
+        CONTROL_SetAnalogAxisScale(i, JoystickAnalogueScale[i], controldevice_joystick);
+    }
+}
+
+
+int CONFIG_ReadSetup(void)
+{
+    char tempbuf[1024];
+
+    CONTROL_ClearAssignments();
+    CONFIG_SetDefaults();
+
+    setupread = 1;
+    pathsearchmode = 1;
+
+    if (scripthandle < 0)
+    {
+		if (buildvfs_exists(SetupFilename))  // JBF 20031211
+			scripthandle = SCRIPT_Load(SetupFilename);
+#if !defined(EDUKE32_TOUCH_DEVICES) && !defined(EDUKE32_STANDALONE)
+		else if (buildvfs_exists(SETUPFILENAME))
+		{
+			int const i = wm_ynbox("Import Configuration Settings",
+				"The configuration file \"%s\" was not found. "
+				"Import configuration data from \"%s\"?",
+				SetupFilename, SETUPFILENAME);
+			if (i)
+				scripthandle = SCRIPT_Load(SETUPFILENAME);
+		}
+#endif
+    }
+
+    pathsearchmode = 0;
+
+    if (scripthandle < 0)
+        return -1;
+
+    char commmacro[] = "CommbatMacro# ";
+
+    for (int i = 0; i < MAXRIDECULE; i++)
+    {
+        commmacro[13] = i+'0';
+        SCRIPT_GetString(scripthandle, "Comm Setup",commmacro,&CommbatMacro[i][0]);
+    }
+
+    Bmemset(tempbuf, 0, sizeof(tempbuf));
+    SCRIPT_GetString(scripthandle, "Comm Setup","PlayerName",&tempbuf[0]);
+
+    char nameBuf[64];
+
+    while (Bstrlen(OSD_StripColors(nameBuf, tempbuf)) > 10)
+        tempbuf[Bstrlen(tempbuf) - 1] = '\0';
+
+    Bstrncpyz(szPlayerName, tempbuf, sizeof(szPlayerName));
+
+    //SCRIPT_GetString(scripthandle, "Comm Setup","RTSName",&ud.rtsname[0]);
+
+    SCRIPT_GetNumber(scripthandle, "Setup", "ConfigVersion", &configversion);
+    SCRIPT_GetNumber(scripthandle, "Setup", "ForceSetup", &gSetup.forcesetup);
+    SCRIPT_GetNumber(scripthandle, "Setup", "NoAutoLoad", &gSetup.noautoload);
+
+    int32_t cachesize;
+    SCRIPT_GetNumber(scripthandle, "Setup", "CacheSize", &cachesize);
+
+    if (cachesize > MAXCACHE1DSIZE)
+        MAXCACHE1DSIZE = cachesize;
+
+    if (gNoSetup == 0 && g_modDir[0] == '/')
+    {
+        struct Bstat st;
+        SCRIPT_GetString(scripthandle, "Setup","ModDir",&g_modDir[0]);
+
+        if (Bstat(g_modDir, &st))
+        {
+            if ((st.st_mode & S_IFDIR) != S_IFDIR)
+            {
+                initprintf("Invalid mod dir in cfg!\n");
+                Bsprintf(g_modDir,"/");
+            }
+        }
+    }
+
+    //if (g_grpNamePtr == NULL && g_addonNum == 0)
+    //{
+    //    SCRIPT_GetStringPtr(scripthandle, "Setup", "SelectedGRP", &g_grpNamePtr);
+    //    if (g_grpNamePtr && !Bstrlen(g_grpNamePtr))
+    //        g_grpNamePtr = dup_filename(G_DefaultGrpFile());
+    //}
+    //
+    //if (!NAM_WW2GI)
+    //{
+    //    SCRIPT_GetNumber(scripthandle, "Screen Setup", "Out", &ud.lockout);
+    //    SCRIPT_GetString(scripthandle, "Screen Setup", "Password", &ud.pwlockout[0]);
+    //}
+
+    windowx = -1;
+    windowy = -1;
+
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "MaxRefreshFreq", (int32_t *)&maxrefreshfreq);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "ScreenBPP", &gSetup.bpp);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "ScreenHeight", &gSetup.ydim);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "ScreenMode", &gSetup.fullscreen);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "ScreenWidth", &gSetup.xdim);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "WindowPosX", (int32_t *)&windowx);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "WindowPosY", (int32_t *)&windowy);
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "WindowPositioning", (int32_t *)&windowpos);
+
+    if (gSetup.bpp < 8) gSetup.bpp = 32;
+
+#ifdef POLYMER
+    int32_t rendmode = 0;
+    SCRIPT_GetNumber(scripthandle, "Screen Setup", "Polymer", &rendmode);
+    glrendmode = (rendmode > 0) ? REND_POLYMER : REND_POLYMOST;
+#endif
+
+    //SCRIPT_GetNumber(scripthandle, "Misc", "Executions", &ud.executions);
+
+#ifdef _WIN32
+    SCRIPT_GetNumber(scripthandle, "Updates", "CheckForUpdates", &CheckForUpdates);
+    SCRIPT_GetNumber(scripthandle, "Updates", "LastUpdateCheck", &LastUpdateCheck);
+#endif
+
+    setupread = 1;
+    return 0;
+}
+
+
+void CONFIG_WriteSettings(void) // save binds and aliases to <cfgname>_settings.cfg
+{
+    char *ptr = Xstrdup(SetupFilename);
+    char filename[BMAX_PATH];
+
+    if (!Bstrcmp(SetupFilename, SETUPFILENAME))
+        Bsprintf(filename, "settings.cfg");
+    else
+        Bsprintf(filename, "%s_settings.cfg", strtok(ptr, "."));
+
+    BFILE *fp = Bfopen(filename, "wt");
+
+    if (fp)
+    {
+        Bfprintf(fp,"// this file is automatically generated by %s\n", AppProperName);
+        Bfprintf(fp,"unbindall\n");
+
+        for (int i=0; i<MAXBOUNDKEYS+MAXMOUSEBUTTONS; i++)
+        {
+            if (CONTROL_KeyIsBound(i))
+            {
+                Bfprintf(fp, "bind \"%s\"%s \"%s\"\n", CONTROL_KeyBinds[i].key,
+                                                       CONTROL_KeyBinds[i].repeat ? "" : " norepeat",
+                                                       CONTROL_KeyBinds[i].cmdstr);
+            }
+        }
+
+        OSD_WriteAliases(fp);
+
+        if (g_isAlterDefaultCrosshair)
+            Bfprintf(fp, "crosshaircolor %d %d %d\n", CrosshairColors.r, CrosshairColors.g, CrosshairColors.b);
+
+        OSD_WriteCvars(fp);
+
+        Bfclose(fp);
+        Bfree(ptr);
+
+        OSD_Printf("Wrote %s\n", filename);
+
+        return;
+    }
+
+    OSD_Printf("Error writing %s: %s\n", filename, strerror(errno));
+
+    Bfree(ptr);
+}
+
+void CONFIG_WriteSetup(uint32_t flags)
+{
+    char buf[128];
+    if (!setupread) return;
+
+    if (scripthandle < 0)
+        scripthandle = SCRIPT_Init(SetupFilename);
+
+    //SCRIPT_PutNumber(scripthandle, "Misc", "Executions", ud.executions, FALSE, FALSE);
+
+    SCRIPT_PutNumber(scripthandle, "Setup", "CacheSize", MAXCACHE1DSIZE, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Setup", "ConfigVersion", BYTEVERSION, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Setup", "ForceSetup", gSetup.forcesetup, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Setup", "NoAutoLoad", gSetup.noautoload, FALSE, FALSE);
+
+#ifdef POLYMER
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "Polymer", glrendmode == REND_POLYMER, FALSE, FALSE);
+#endif
+
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "ScreenBPP", gSetup.bpp, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "ScreenHeight", gSetup.ydim, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "ScreenMode", gSetup.fullscreen, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "ScreenWidth", gSetup.xdim, FALSE, FALSE);
+
+    //if (g_grpNamePtr && !g_addonNum)
+    //    SCRIPT_PutString(scripthandle, "Setup", "SelectedGRP", g_grpNamePtr);
+
+#ifdef STARTUP_SETUP_WINDOW
+    if (gNoSetup == 0)
+        SCRIPT_PutString(scripthandle, "Setup", "ModDir", &g_modDir[0]);
+#endif
+    // exit early after only updating the values that can be changed from the startup window
+    if (flags & 1)
+    {
+        SCRIPT_Save(scripthandle, SetupFilename);
+        SCRIPT_Free(scripthandle);
+        return;
+    }
+
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "MaxRefreshFreq", maxrefreshfreq, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "WindowPosX", windowx, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "WindowPosY", windowy, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Screen Setup", "WindowPositioning", windowpos, FALSE, FALSE);
+
+    //if (!NAM_WW2GI)
+    //{
+    //    SCRIPT_PutNumber(scripthandle, "Screen Setup", "Out",ud.lockout,FALSE,FALSE);
+    //    SCRIPT_PutString(scripthandle, "Screen Setup", "Password",ud.pwlockout);
+    //}
+
+#ifdef _WIN32
+    SCRIPT_PutNumber(scripthandle, "Updates", "CheckForUpdates", CheckForUpdates, FALSE, FALSE);
+    SCRIPT_PutNumber(scripthandle, "Updates", "LastUpdateCheck", LastUpdateCheck, FALSE, FALSE);
+#endif
+
+    if (gSetup.usemouse)
+    {
+        for (int i=0; i<MAXMOUSEBUTTONS; i++)
+        {
+            if (CONFIG_FunctionNumToName(MouseFunctions[i][0]))
+            {
+                Bsprintf(buf, "MouseButton%d", i);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(MouseFunctions[i][0]));
+            }
+
+            if (i >= (MAXMOUSEBUTTONS-2)) continue;
+
+            if (CONFIG_FunctionNumToName(MouseFunctions[i][1]))
+            {
+                Bsprintf(buf, "MouseButtonClicked%d", i);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(MouseFunctions[i][1]));
+            }
+        }
+
+        for (int i=0; i<MAXMOUSEAXES; i++)
+        {
+            if (CONFIG_AnalogNumToName(MouseAnalogueAxes[i]))
+            {
+                Bsprintf(buf, "MouseAnalogAxes%d", i);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_AnalogNumToName(MouseAnalogueAxes[i]));
+            }
+
+            if (CONFIG_FunctionNumToName(MouseDigitalFunctions[i][0]))
+            {
+                Bsprintf(buf, "MouseDigitalAxes%d_0", i);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(MouseDigitalFunctions[i][0]));
+            }
+
+            if (CONFIG_FunctionNumToName(MouseDigitalFunctions[i][1]))
+            {
+                Bsprintf(buf, "MouseDigitalAxes%d_1", i);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(MouseDigitalFunctions[i][1]));
+            }
+
+            if (MouseAnalogueScale[i] != DEFAULTMOUSEANALOGUESCALE)
+            {
+                Bsprintf(buf, "MouseAnalogScale%d", i);
+                SCRIPT_PutNumber(scripthandle, "Controls", buf, MouseAnalogueScale[i], FALSE, FALSE);
+            }
+        }
+    }
+
+    if (gSetup.usejoystick)
+    {
+        for (int dummy=0; dummy<MAXJOYBUTTONSANDHATS; dummy++)
+        {
+            if (CONFIG_FunctionNumToName(JoystickFunctions[dummy][0]))
+            {
+                Bsprintf(buf, "JoystickButton%d", dummy);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(JoystickFunctions[dummy][0]));
+            }
+
+            if (CONFIG_FunctionNumToName(JoystickFunctions[dummy][1]))
+            {
+                Bsprintf(buf, "JoystickButtonClicked%d", dummy);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(JoystickFunctions[dummy][1]));
+            }
+        }
+        for (int dummy=0; dummy<MAXJOYAXES; dummy++)
+        {
+            if (CONFIG_AnalogNumToName(JoystickAnalogueAxes[dummy]))
+            {
+                Bsprintf(buf, "JoystickAnalogAxes%d", dummy);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_AnalogNumToName(JoystickAnalogueAxes[dummy]));
+            }
+
+            if (CONFIG_FunctionNumToName(JoystickDigitalFunctions[dummy][0]))
+            {
+                Bsprintf(buf, "JoystickDigitalAxes%d_0", dummy);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(JoystickDigitalFunctions[dummy][0]));
+            }
+
+            if (CONFIG_FunctionNumToName(JoystickDigitalFunctions[dummy][1]))
+            {
+                Bsprintf(buf, "JoystickDigitalAxes%d_1", dummy);
+                SCRIPT_PutString(scripthandle, "Controls", buf, CONFIG_FunctionNumToName(JoystickDigitalFunctions[dummy][1]));
+            }
+
+            if (JoystickAnalogueScale[dummy] != DEFAULTJOYSTICKANALOGUESCALE)
+            {
+                Bsprintf(buf, "JoystickAnalogScale%d", dummy);
+                SCRIPT_PutNumber(scripthandle, "Controls", buf, JoystickAnalogueScale[dummy], FALSE, FALSE);
+            }
+
+            if (JoystickAnalogueDead[dummy] != DEFAULTJOYSTICKANALOGUEDEAD)
+            {
+                Bsprintf(buf, "JoystickAnalogDead%d", dummy);
+                SCRIPT_PutNumber(scripthandle, "Controls", buf, JoystickAnalogueDead[dummy], FALSE, FALSE);
+            }
+
+            if (JoystickAnalogueSaturate[dummy] != DEFAULTJOYSTICKANALOGUESATURATE)
+            {
+                Bsprintf(buf, "JoystickAnalogSaturate%d", dummy);
+                SCRIPT_PutNumber(scripthandle, "Controls", buf, JoystickAnalogueSaturate[dummy], FALSE, FALSE);
+            }
+        }
+    }
+
+    SCRIPT_PutString(scripthandle, "Comm Setup","PlayerName",&szPlayerName[0]);
+
+    //SCRIPT_PutString(scripthandle, "Comm Setup","RTSName",&ud.rtsname[0]);
+
+    char commmacro[] = "CommbatMacro# ";
+
+    for (int dummy = 0; dummy < MAXRIDECULE; dummy++)
+    {
+        commmacro[13] = dummy+'0';
+        SCRIPT_PutString(scripthandle, "Comm Setup",commmacro,&CommbatMacro[dummy][0]);
+    }
+
+    SCRIPT_Save(scripthandle, SetupFilename);
+
+    if ((flags & 2) == 0)
+        SCRIPT_Free(scripthandle);
+
+    OSD_Printf("Wrote %s\n",SetupFilename);
+    CONFIG_WriteSettings();
+    Bfflush(NULL);
+}
+
+#if 0
+static const char *CONFIG_GetMapEntryName(char m[], char const * const mapname)
+{
+    strcpy(m, mapname);
+
+    char *p = strrchr(m, '/');
+    if (!p) p = strrchr(m, '\\');
+    if (p) Bmemmove(m, p, Bstrlen(p)+1);
+    for (p=m; *p; p++) *p = tolower(*p);
+
+    // cheap hack because SCRIPT_GetNumber doesn't like the slashes
+    p = m;
+    while (*p == '/') p++;
+
+    return p;
+}
+
+static void CONFIG_GetMD4EntryName(char m[], uint8_t const * const md4)
+{
+    sprintf(m, "MD4_%08x%08x%08x%08x",
+            B_BIG32(B_UNBUF32(&md4[0])), B_BIG32(B_UNBUF32(&md4[4])),
+            B_BIG32(B_UNBUF32(&md4[8])), B_BIG32(B_UNBUF32(&md4[12])));
+}
+
+int32_t CONFIG_GetMapBestTime(char const * const mapname, uint8_t const * const mapmd4)
+{
+    if (!setupread || scripthandle < 0)
+        return -1;
+
+    char m[37];
+
+    CONFIG_GetMD4EntryName(m, mapmd4);
+
+    int32_t t = -1;
+    if (SCRIPT_GetNumber(scripthandle, "MapTimes", m, &t))
+    {
+        // fall back to map filenames
+        char m2[BMAX_PATH];
+        auto p = CONFIG_GetMapEntryName(m2, mapname);
+
+        SCRIPT_GetNumber(scripthandle, "MapTimes", p, &t);
+    }
+
+    return t;
+}
+
+int CONFIG_SetMapBestTime(uint8_t const * const mapmd4, int32_t tm)
+{
+    if (scripthandle < 0 && (scripthandle = SCRIPT_Init(SetupFilename)) < 0)
+        return -1;
+
+    char m[37];
+
+    CONFIG_GetMD4EntryName(m, mapmd4);
+    SCRIPT_PutNumber(scripthandle, "MapTimes", m, tm, FALSE, FALSE);
+
+    return 0;
+}
+
+#endif
diff --git a/source/blood/src/config.h b/source/blood/src/config.h
new file mode 100644
index 000000000..12d839388
--- /dev/null
+++ b/source/blood/src/config.h
@@ -0,0 +1,128 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#ifndef config_public_h_
+#define config_public_h_
+
+#include "keyboard.h"
+#include "function.h"
+#include "control.h"
+#include "_control.h"
+#include "hash.h"
+
+#define MAXRIDECULE 10
+#define MAXRIDECULELENGTH 40
+#define MAXPLAYERNAME 16
+
+extern int32_t MouseDeadZone, MouseBias;
+extern int32_t SmoothInput;
+extern int32_t MouseFunctions[MAXMOUSEBUTTONS][2];
+extern int32_t MouseDigitalFunctions[MAXMOUSEAXES][2];
+extern int32_t MouseAnalogueAxes[MAXMOUSEAXES];
+extern int32_t MouseAnalogueScale[MAXMOUSEAXES];
+extern int32_t JoystickFunctions[MAXJOYBUTTONSANDHATS][2];
+extern int32_t JoystickDigitalFunctions[MAXJOYAXES][2];
+extern int32_t JoystickAnalogueAxes[MAXJOYAXES];
+extern int32_t JoystickAnalogueScale[MAXJOYAXES];
+extern int32_t JoystickAnalogueDead[MAXJOYAXES];
+extern int32_t JoystickAnalogueSaturate[MAXJOYAXES];
+extern uint8_t KeyboardKeys[NUMGAMEFUNCTIONS][2];
+extern int32_t scripthandle;
+extern int32_t setupread;
+extern int32_t SoundToggle;
+extern int32_t MusicToggle;
+extern int32_t MusicRestartsOnLoadToggle;
+extern int32_t CDAudioToggle;
+extern int32_t FXVolume;
+extern int32_t MusicVolume;
+extern int32_t CDVolume;
+extern int32_t NumVoices;
+extern int32_t NumChannels;
+extern int32_t NumBits;
+extern int32_t MixRate;
+extern int32_t ReverseStereo;
+extern int32_t MusicDevice;
+extern int32_t configversion;
+extern int32_t CheckForUpdates;
+extern int32_t LastUpdateCheck;
+extern int32_t useprecache;
+extern char CommbatMacro[MAXRIDECULE][MAXRIDECULELENGTH];
+extern char szPlayerName[MAXPLAYERNAME];
+extern int32_t gTurnSpeed;
+extern int32_t gDetail;
+extern int32_t gAutoAim;
+extern int32_t gWeaponSwitch;
+extern int32_t gAutoRun;
+extern int32_t gViewInterpolate;
+extern int32_t gViewHBobbing;
+extern int32_t gViewVBobbing;
+extern int32_t gFollowMap;
+extern int32_t gOverlayMap;
+extern int32_t gRotateMap;
+extern int32_t gAimReticle;
+extern int32_t gSlopeTilting;
+extern int32_t gMessageState;
+extern int32_t gMessageCount;
+extern int32_t gMessageTime;
+extern int32_t gMessageFont;
+extern int32_t gbAdultContent;
+extern char gzAdultPassword[9];
+extern int32_t gDoppler;
+extern int32_t gShowWeapon;
+extern int32_t gMouseSensitivity;
+extern int32_t gMouseAiming;
+extern int32_t gMouseAimingFlipped;
+extern int32_t gRunKeyMode;
+extern bool gNoClip;
+extern bool gInfiniteAmmo;
+extern bool gFullMap;
+extern hashtable_t h_gamefuncs;
+extern int32_t gUpscaleFactor;
+extern int32_t gBrightness;
+extern int32_t gLevelStats;
+extern int32_t gPowerupDuration;
+extern int32_t gShowMapTitle;
+extern int32_t MAXCACHE1DSIZE;
+extern int32_t gFov;
+extern int32_t gCenterHoriz;
+extern int32_t gDeliriumBlur;
+
+int  CONFIG_ReadSetup(void);
+void CONFIG_WriteSetup(uint32_t flags);
+void CONFIG_SetDefaults(void);
+void CONFIG_SetupMouse(void);
+void CONFIG_SetupJoystick(void);
+void CONFIG_SetDefaultKeys(const char (*keyptr)[MAXGAMEFUNCLEN], bool lazy=false);
+
+int32_t CONFIG_GetMapBestTime(char const *mapname, uint8_t const *mapmd4);
+int     CONFIG_SetMapBestTime(uint8_t const *mapmd4, int32_t tm);
+
+int32_t CONFIG_FunctionNameToNum(const char *func);
+char *  CONFIG_FunctionNumToName(int32_t func);
+
+int32_t     CONFIG_AnalogNameToNum(const char *func);
+const char *CONFIG_AnalogNumToName(int32_t func);
+
+void CONFIG_MapKey(int which, kb_scancode key1, kb_scancode oldkey1, kb_scancode key2, kb_scancode oldkey2);
+
+#endif
diff --git a/source/blood/src/controls.cpp b/source/blood/src/controls.cpp
new file mode 100644
index 000000000..3c5cfe5ce
--- /dev/null
+++ b/source/blood/src/controls.cpp
@@ -0,0 +1,491 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "baselayer.h"
+#include "keyboard.h"
+#include "mouse.h"
+#include "joystick.h"
+#include "control.h"
+#include "function.h"
+#include "common_game.h"
+#include "blood.h"
+#include "config.h"
+#include "controls.h"
+#include "globals.h"
+#include "levels.h"
+#include "map2d.h"
+#include "view.h"
+
+
+int32_t ctrlCheckAllInput(void)
+{
+    return (
+        KB_KeyWaiting() ||
+        MOUSE_GetButtons() ||
+        JOYSTICK_GetButtons()
+        );
+}
+
+void ctrlClearAllInput(void)
+{
+    KB_FlushKeyboardQueue();
+    KB_ClearKeysDown();
+    MOUSE_ClearAllButtons();
+    JOYSTICK_ClearAllButtons();
+}
+
+GINPUT gInput;
+bool bSilentAim = false;
+
+int iTurnCount = 0;
+
+int32_t GetTime(void)
+{
+    return gGameClock;
+}
+
+void ctrlInit(void)
+{
+    KB_ClearKeysDown();
+    KB_FlushKeyboardQueue();
+    KB_FlushKeyboardQueueScans();
+    CONTROL_Startup(controltype_keyboardandmouse, &GetTime, 120);
+    CONFIG_SetupMouse();
+    CONFIG_SetupJoystick();
+
+    CONTROL_JoystickEnabled = (gSetup.usejoystick && CONTROL_JoyPresent);
+    CONTROL_MouseEnabled = (gSetup.usemouse && CONTROL_MousePresent);
+    CONTROL_SmoothMouse = SmoothInput;
+
+    // JBF 20040215: evil and nasty place to do this, but joysticks are evil and nasty too
+    for (int i = 0; i < joystick.numAxes; i++)
+        joySetDeadZone(i, JoystickAnalogueDead[i], JoystickAnalogueSaturate[i]);
+    CONTROL_DefineFlag(gamefunc_Move_Forward, false);
+    CONTROL_DefineFlag(gamefunc_Move_Backward, false);
+    CONTROL_DefineFlag(gamefunc_Turn_Left, false);
+    CONTROL_DefineFlag(gamefunc_Turn_Right, false);
+    CONTROL_DefineFlag(gamefunc_Turn_Around, false);
+    CONTROL_DefineFlag(gamefunc_Strafe, false);
+    CONTROL_DefineFlag(gamefunc_Strafe_Left, false);
+    CONTROL_DefineFlag(gamefunc_Strafe_Right, false);
+    CONTROL_DefineFlag(gamefunc_Jump, false);
+    CONTROL_DefineFlag(gamefunc_Crouch, false);
+    CONTROL_DefineFlag(gamefunc_Run, false);
+    CONTROL_DefineFlag(gamefunc_AutoRun, false);
+    CONTROL_DefineFlag(gamefunc_Open, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_Fire, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_Special_Fire, false);
+    CONTROL_DefineFlag(gamefunc_Aim_Up, false);
+    CONTROL_DefineFlag(gamefunc_Aim_Down, false);
+    CONTROL_DefineFlag(gamefunc_Aim_Center, false);
+    CONTROL_DefineFlag(gamefunc_Look_Up, false);
+    CONTROL_DefineFlag(gamefunc_Look_Down, false);
+    CONTROL_DefineFlag(gamefunc_Tilt_Left, false);
+    CONTROL_DefineFlag(gamefunc_Tilt_Right, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_1, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_2, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_3, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_4, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_5, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_6, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_7, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_8, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_9, false);
+    CONTROL_DefineFlag(gamefunc_Weapon_10, false);
+    CONTROL_DefineFlag(gamefunc_Inventory_Use, false);
+    CONTROL_DefineFlag(gamefunc_Inventory_Left, false);
+    CONTROL_DefineFlag(gamefunc_Inventory_Right, false);
+    CONTROL_DefineFlag(gamefunc_Map_Toggle, false);
+    CONTROL_DefineFlag(gamefunc_Map_Follow_Mode, false);
+    CONTROL_DefineFlag(gamefunc_Shrink_Screen, false);
+    CONTROL_DefineFlag(gamefunc_Enlarge_Screen, false);
+    CONTROL_DefineFlag(gamefunc_Send_Message, false);
+    CONTROL_DefineFlag(gamefunc_See_Coop_View, false);
+    CONTROL_DefineFlag(gamefunc_See_Chase_View, false);
+    CONTROL_DefineFlag(gamefunc_Mouse_Aiming, false);
+    CONTROL_DefineFlag(gamefunc_Toggle_Crosshair, false);
+    CONTROL_DefineFlag(gamefunc_Next_Weapon, false);
+    CONTROL_DefineFlag(gamefunc_Previous_Weapon, false);
+    CONTROL_DefineFlag(gamefunc_Holster_Weapon, false);
+    CONTROL_DefineFlag(gamefunc_Show_Opponents_Weapon, false);
+    CONTROL_DefineFlag(gamefunc_BeastVision, false);
+    CONTROL_DefineFlag(gamefunc_CrystalBall, false);
+    CONTROL_DefineFlag(gamefunc_JumpBoots, false);
+    CONTROL_DefineFlag(gamefunc_MedKit, false);
+    CONTROL_DefineFlag(gamefunc_ProximityBombs, false);
+    CONTROL_DefineFlag(gamefunc_RemoteBombs, false);
+}
+
+void ctrlTerm(void)
+{
+    CONTROL_Shutdown();
+}
+
+int32_t mouseyaxismode = -1;
+
+void ctrlGetInput(void)
+{
+    ControlInfo info;
+    int forward = 0, strafe = 0;
+    fix16_t turn = 0;
+    memset(&gInput, 0, sizeof(gInput));
+
+    if (!gGameStarted || gInputMode != INPUT_MODE_0)
+    {
+        CONTROL_GetInput(&info);
+        return;
+    }
+
+    CONTROL_ProcessBinds();
+
+    if (gMouseAiming)
+        gMouseAim = 0;
+
+    if (BUTTON(gamefunc_Mouse_Aiming))
+    {
+        if (gMouseAiming)
+            gMouseAim = 1;
+        else
+        {
+            CONTROL_ClearButton(gamefunc_Mouse_Aiming);
+            gMouseAim = !gMouseAim;
+            if (gMouseAim)
+            {
+                if (!bSilentAim)
+                    viewSetMessage("Mouse aiming ON");
+            }
+            else
+            {
+                if (!bSilentAim)
+                    viewSetMessage("Mouse aiming OFF");
+                gInput.keyFlags.lookCenter = 1;
+            }
+        }
+    }
+    else if (gMouseAiming)
+        gInput.keyFlags.lookCenter = 1;
+
+    int32_t const aimMode = (gMouseAim) ? (int32_t)analog_lookingupanddown : MouseAnalogueAxes[1];
+
+    if (aimMode != mouseyaxismode)
+    {
+        CONTROL_MapAnalogAxis(1, aimMode, controldevice_mouse);
+        mouseyaxismode = aimMode;
+    }
+
+    CONTROL_GetInput(&info);
+
+    if (MouseDeadZone)
+    {
+        if (info.dpitch > 0)
+            info.dpitch = max(info.dpitch - MouseDeadZone, 0);
+        else if (info.dpitch < 0)
+            info.dpitch = min(info.dpitch + MouseDeadZone, 0);
+
+        if (info.dyaw > 0)
+            info.dyaw = max(info.dyaw - MouseDeadZone, 0);
+        else if (info.dyaw < 0)
+            info.dyaw = min(info.dyaw + MouseDeadZone, 0);
+    }
+
+    if (MouseBias)
+    {
+        if (klabs(info.dyaw) > klabs(info.dpitch))
+            info.dpitch = tabledivide32_noinline(info.dpitch, MouseBias);
+        else info.dyaw = tabledivide32_noinline(info.dyaw, MouseBias);
+    }
+
+    if (gQuitRequest)
+        gInput.keyFlags.quit = 1;
+
+    if (gGameStarted && gInputMode != INPUT_MODE_2 && gInputMode != INPUT_MODE_1
+        && BUTTON(gamefunc_Send_Message))
+    {
+        CONTROL_ClearButton(gamefunc_Send_Message);
+        keyFlushScans();
+        gInputMode = INPUT_MODE_2;
+    }
+
+    if (BUTTON(gamefunc_AutoRun))
+    {
+        CONTROL_ClearButton(gamefunc_AutoRun);
+        gAutoRun = !gAutoRun;
+        if (gAutoRun)
+            viewSetMessage("Auto run ON");
+        else
+            viewSetMessage("Auto run OFF");
+    }
+
+    if (BUTTON(gamefunc_Map_Toggle))
+    {
+        CONTROL_ClearButton(gamefunc_Map_Toggle);
+        viewToggle(gViewMode);
+    }
+
+    if (BUTTON(gamefunc_Map_Follow_Mode))
+    {
+        CONTROL_ClearButton(gamefunc_Map_Follow_Mode);
+        gFollowMap = !gFollowMap;
+        gViewMap.FollowMode(gFollowMap);
+    }
+
+    if (BUTTON(gamefunc_Shrink_Screen))
+    {
+        if (gViewMode == 3)
+        {
+            CONTROL_ClearButton(gamefunc_Shrink_Screen);
+            viewResizeView(gViewSize + 1);
+        }
+        if (gViewMode == 2 || gViewMode == 4)
+        {
+            gZoom = ClipLow(gZoom - (gZoom >> 4), 64);
+            gViewMap.nZoom = gZoom;
+        }
+    }
+
+    if (BUTTON(gamefunc_Enlarge_Screen))
+    {
+        if (gViewMode == 3)
+        {
+            CONTROL_ClearButton(gamefunc_Enlarge_Screen);
+            viewResizeView(gViewSize - 1);
+        }
+        if (gViewMode == 2 || gViewMode == 4)
+        {
+            gZoom = ClipHigh(gZoom + (gZoom >> 4), 4096);
+            gViewMap.nZoom = gZoom;
+        }
+    }
+
+    if (BUTTON(gamefunc_Toggle_Crosshair))
+    {
+        CONTROL_ClearButton(gamefunc_Toggle_Crosshair);
+        gAimReticle = !gAimReticle;
+    }
+
+    if (BUTTON(gamefunc_Next_Weapon))
+    {
+        CONTROL_ClearButton(gamefunc_Next_Weapon);
+        gInput.keyFlags.nextWeapon = 1;
+    }
+
+    if (BUTTON(gamefunc_Previous_Weapon))
+    {
+        CONTROL_ClearButton(gamefunc_Previous_Weapon);
+        gInput.keyFlags.prevWeapon = 1;
+    }
+
+    if (BUTTON(gamefunc_Show_Opponents_Weapon))
+    {
+        CONTROL_ClearButton(gamefunc_Show_Opponents_Weapon);
+        gShowWeapon = !gShowWeapon;
+    }
+
+    if (BUTTON(gamefunc_Jump))
+        gInput.buttonFlags.jump = 1;
+
+    if (BUTTON(gamefunc_Crouch))
+        gInput.buttonFlags.crouch = 1;
+
+    if (BUTTON(gamefunc_Weapon_Fire))
+        gInput.buttonFlags.shoot = 1;
+
+    if (BUTTON(gamefunc_Weapon_Special_Fire))
+        gInput.buttonFlags.shoot2 = 1;
+
+    if (BUTTON(gamefunc_Open))
+    {
+        CONTROL_ClearButton(gamefunc_Open);
+        gInput.keyFlags.action = 1;
+    }
+
+    gInput.buttonFlags.lookUp = BUTTON(gamefunc_Look_Up);
+    gInput.buttonFlags.lookDown = BUTTON(gamefunc_Look_Down);
+
+    if (gInput.buttonFlags.lookUp || gInput.buttonFlags.lookDown)
+        gInput.keyFlags.lookCenter = 1;
+    else
+    {
+        gInput.buttonFlags.lookUp = BUTTON(gamefunc_Aim_Up);
+        gInput.buttonFlags.lookDown = BUTTON(gamefunc_Aim_Down);
+    }
+
+    if (BUTTON(gamefunc_Aim_Center))
+    {
+        CONTROL_ClearButton(gamefunc_Aim_Center);
+        gInput.keyFlags.lookCenter = 1;
+    }
+
+    gInput.keyFlags.spin180 = BUTTON(gamefunc_Turn_Around);
+
+    if (BUTTON(gamefunc_Inventory_Left))
+    {
+        CONTROL_ClearButton(gamefunc_Inventory_Left);
+        gInput.keyFlags.prevItem = 1;
+    }
+
+    if (BUTTON(gamefunc_Inventory_Right))
+    {
+        CONTROL_ClearButton(gamefunc_Inventory_Right);
+        gInput.keyFlags.nextItem = 1;
+    }
+
+    if (BUTTON(gamefunc_Inventory_Use))
+    {
+        CONTROL_ClearButton(gamefunc_Inventory_Use);
+        gInput.keyFlags.useItem = 1;
+    }
+
+    if (BUTTON(gamefunc_BeastVision))
+    {
+        CONTROL_ClearButton(gamefunc_BeastVision);
+        gInput.useFlags.useBeastVision = 1;
+    }
+
+    if (BUTTON(gamefunc_CrystalBall))
+    {
+        CONTROL_ClearButton(gamefunc_CrystalBall);
+        gInput.useFlags.useCrystalBall = 1;
+    }
+
+    if (BUTTON(gamefunc_JumpBoots))
+    {
+        CONTROL_ClearButton(gamefunc_JumpBoots);
+        gInput.useFlags.useJumpBoots = 1;
+    }
+
+    if (BUTTON(gamefunc_MedKit))
+    {
+        CONTROL_ClearButton(gamefunc_MedKit);
+        gInput.useFlags.useMedKit = 1;
+    }
+
+    for (int i = 0; i < 10; i++)
+    {
+        if (BUTTON(gamefunc_Weapon_1 + i))
+        {
+            CONTROL_ClearButton(gamefunc_Weapon_1 + i);
+            gInput.newWeapon = 1 + i;
+        }
+    }
+
+    if (BUTTON(gamefunc_ProximityBombs))
+    {
+        CONTROL_ClearButton(gamefunc_ProximityBombs);
+        gInput.newWeapon = 11;
+    }
+
+    if (BUTTON(gamefunc_RemoteBombs))
+    {
+        CONTROL_ClearButton(gamefunc_RemoteBombs);
+        gInput.newWeapon = 12;
+    }
+
+    if (BUTTON(gamefunc_Holster_Weapon))
+    {
+        CONTROL_ClearButton(gamefunc_Holster_Weapon);
+        gInput.keyFlags.holsterWeapon = 1;
+    }
+
+    char run = gRunKeyMode ? (BUTTON(gamefunc_Run) | gAutoRun) : (BUTTON(gamefunc_Run) ^ gAutoRun);
+    char run2 = BUTTON(gamefunc_Run);
+
+    gInput.syncFlags.run = run;
+
+    if (BUTTON(gamefunc_Move_Forward))
+        forward += (1+run)<<10;
+
+    if (BUTTON(gamefunc_Move_Backward))
+        forward -= (1+run)<<10;
+
+    char turnLeft = 0, turnRight = 0;
+
+    if (BUTTON(gamefunc_Strafe))
+    {
+        if (BUTTON(gamefunc_Turn_Left))
+            strafe += (1 + run) << 10;
+        if (BUTTON(gamefunc_Turn_Right))
+            strafe -= (1 + run) << 10;
+    }
+    else
+    {
+        if (BUTTON(gamefunc_Strafe_Left))
+            strafe += (1 + run) << 10;
+        if (BUTTON(gamefunc_Strafe_Right))
+            strafe -= (1 + run) << 10;
+        if (BUTTON(gamefunc_Turn_Left))
+            turnLeft = 1;
+        if (BUTTON(gamefunc_Turn_Right))
+            turnRight = 1;
+    }
+
+    if (turnLeft || turnRight)
+        iTurnCount += 4;
+    else
+        iTurnCount = 0;
+
+    if (turnLeft)
+        turn -= fix16_from_int(ClipHigh(12 * iTurnCount, gTurnSpeed))>>2;
+    if (turnRight)
+        turn += fix16_from_int(ClipHigh(12 * iTurnCount, gTurnSpeed))>>2;
+
+    if ((run2 || run) && iTurnCount > 24)
+        turn <<= 1;
+
+    if (BUTTON(gamefunc_Strafe))
+        strafe = ClipRange(strafe - info.dyaw, -2048, 2048);
+    else
+        turn = fix16_clamp(turn + fix16_div(fix16_from_int(info.dyaw), F16(32)), F16(-1024)>>2, F16(1024)>>2);
+
+    strafe = ClipRange(strafe-(info.dx<<5), -2048, 2048);
+
+#if 0
+    if (info.dz < 0)
+        gInput.mlook = ClipRange((info.dz+127)>>7, -127, 127);
+    else
+        gInput.mlook = ClipRange(info.dz>>7, -127, 127);
+#endif
+    gInput.q16mlook = fix16_clamp(fix16_div(fix16_from_int(info.dpitch), F16(256)), F16(-127)>>2, F16(127)>>2);
+    if (!gMouseAimingFlipped)
+        gInput.q16mlook = -gInput.q16mlook;
+    forward = ClipRange(forward - info.dz, -2048, 2048);
+
+    if (KB_KeyPressed(sc_Pause)) // 0xc5 in disassembly
+    {
+        gInput.keyFlags.pause = 1;
+        KB_ClearKeyDown(sc_Pause);
+    }
+
+    if (!gViewMap.bFollowMode && gViewMode == 4)
+    {
+        gViewMap.turn = fix16_to_int(turn<<2);
+        gViewMap.forward = forward>>8;
+        gViewMap.strafe = strafe>>8;
+        turn = 0;
+        forward = 0;
+        strafe = 0;
+    }
+    gInput.forward = forward;
+    gInput.q16turn = turn;
+    gInput.strafe = strafe;
+}
diff --git a/source/blood/src/controls.h b/source/blood/src/controls.h
new file mode 100644
index 000000000..94f008f86
--- /dev/null
+++ b/source/blood/src/controls.h
@@ -0,0 +1,112 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#pragma pack(push, 1)
+
+union BUTTONFLAGS
+{
+    int8_t byte;
+    struct
+    {
+        unsigned int jump : 1;
+        unsigned int crouch : 1;
+        unsigned int shoot : 1;
+        unsigned int shoot2 : 1;
+        unsigned int lookUp : 1;
+        unsigned int lookDown : 1;
+    };
+};
+
+union KEYFLAGS
+{
+    int16_t word;
+    struct
+    {
+        unsigned int action : 1;
+        unsigned int jab : 1;
+        unsigned int prevItem : 1;
+        unsigned int nextItem : 1;
+        unsigned int useItem : 1;
+        unsigned int prevWeapon : 1;
+        unsigned int nextWeapon : 1;
+        unsigned int holsterWeapon : 1;
+        unsigned int lookCenter : 1;
+        unsigned int lookLeft : 1;
+        unsigned int lookRight : 1;
+        unsigned int spin180 : 1;
+        unsigned int pause : 1;
+        unsigned int quit : 1;
+        unsigned int restart : 1;
+    };
+};
+
+union USEFLAGS
+{
+    uint8_t byte;
+    struct
+    {
+        unsigned int useBeastVision : 1;
+        unsigned int useCrystalBall : 1;
+        unsigned int useJumpBoots : 1;
+        unsigned int useMedKit : 1;
+    };
+};
+
+union SYNCFLAGS
+{
+    uint8_t byte;
+    struct
+    {
+        unsigned int buttonChange : 1;
+        unsigned int keyChange : 1;
+        unsigned int useChange : 1;
+        unsigned int weaponChange : 1;
+        unsigned int mlookChange : 1;
+        unsigned int run : 1;
+    };
+};
+struct GINPUT
+{
+    SYNCFLAGS syncFlags;
+    int16_t forward;
+    fix16_t q16turn;
+    int16_t strafe;
+    BUTTONFLAGS buttonFlags;
+    KEYFLAGS keyFlags;
+    USEFLAGS useFlags;
+    uint8_t newWeapon;
+    fix16_t q16mlook;
+};
+
+#pragma pack(pop)
+
+extern GINPUT gInput;
+extern bool bSilentAim;
+extern int32_t gMouseAim; // Should be an int32 due to being passed to OSD
+
+int32_t ctrlCheckAllInput(void);
+void ctrlClearAllInput(void);
+void ctrlInit();
+void ctrlGetInput();
+
diff --git a/source/blood/src/credits.cpp b/source/blood/src/credits.cpp
new file mode 100644
index 000000000..a88abcdb1
--- /dev/null
+++ b/source/blood/src/credits.cpp
@@ -0,0 +1,284 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#include "SmackerDecoder.h"
+#include "fx_man.h"
+#include "keyboard.h"
+#include "common_game.h"
+#include "blood.h"
+#include "config.h"
+#include "controls.h"
+#include "globals.h"
+#include "resource.h"
+#include "screen.h"
+#include "sound.h"
+#include "view.h"
+
+char exitCredits = 0;
+
+char Wait(int nTicks)
+{
+    gGameClock = 0;
+    while (gGameClock < nTicks)
+    {
+        timerUpdate();
+        char key = keyGetScan();
+        if (key)
+        {
+            if (key == sc_Escape) // sc_Escape
+                exitCredits = 1;
+            return 0;
+        }
+    }
+    return 1;
+}
+
+char DoFade(char r, char g, char b, int nTicks)
+{
+    dassert(nTicks > 0);
+    scrSetupFade(r, g, b);
+    gGameClock = gFrameClock = 0;
+    do
+    {
+        while (gGameClock < gFrameClock) { timerUpdate();};
+        gFrameClock += 2;
+        scrNextPage();
+        scrFadeAmount(divscale16(ClipHigh(gGameClock, nTicks), nTicks));
+        if (keyGetScan())
+            return 0;
+    } while (gGameClock <= nTicks);
+    return 1;
+}
+
+char DoUnFade(int nTicks)
+{
+    dassert(nTicks > 0);
+    scrSetupUnfade();
+    gGameClock = gFrameClock = 0;
+    do
+    {
+        while (gGameClock < gFrameClock) { timerUpdate(); };
+        scrNextPage();
+        scrFadeAmount(0x10000-divscale16(ClipHigh(gGameClock, nTicks), nTicks));
+        if (keyGetScan())
+            return 0;
+    } while (gGameClock <= nTicks);
+    return 1;
+}
+
+void credLogosDos(void)
+{
+    char bShift = keystatus[sc_LeftShift] | keystatus[sc_RightShift];
+    videoSetViewableArea(0, 0, xdim-1, ydim-1);
+    DoUnFade(1);
+    videoClearScreen(0);
+    if (bShift)
+        return;
+    {
+        //CSMKPlayer smkPlayer;
+        //if (smkPlayer.PlaySMKWithWAV("LOGO.SMK", 300) == 1)
+        //{
+            rotatesprite(160<<16, 100<<16, 65536, 0, 2050, 0, 0, 0x4a, 0, 0, xdim-1, ydim-1);
+            sndStartSample("THUNDER2", 128, -1);
+            scrNextPage();
+            if (!Wait(360))
+                return;
+            if (!DoFade(0, 0, 0, 60))
+                return;
+        //}
+        //if (smkPlayer.PlaySMKWithWAV("GTI.SMK", 301) == 1)
+        //{
+            videoClearScreen(0);
+            rotatesprite(160<<16, 100<<16, 65536, 0, 2052, 0, 0, 0x0a, 0, 0, xdim-1, ydim-1);
+            scrNextPage();
+            DoUnFade(1);
+            sndStartSample("THUNDER2", 128, -1);
+            if (!Wait(360))
+                return;
+        //}
+    }
+    sndPlaySpecialMusicOrNothing(MUS_INTRO);
+    sndStartSample("THUNDER2", 128, -1);
+    if (!DoFade(0, 0, 0, 60))
+        return;
+    videoClearScreen(0);
+    scrNextPage();
+    if (!DoUnFade(1))
+        return;
+    videoClearScreen(0);
+    rotatesprite(160<<16, 100<<16, 65536, 0, 2518, 0, 0, 0x4a, 0, 0, xdim-1, ydim-1);
+    scrNextPage();
+    Wait(360);
+    sndFadeSong(4000);
+}
+
+void credReset(void)
+{
+    videoClearScreen(0);
+    scrNextPage();
+    DoFade(0,0,0,1);
+    scrSetupUnfade();
+    DoUnFade(1);
+}
+
+int credKOpen4Load(char *&pzFile)
+{
+    int nLen = strlen(pzFile);
+    for (int i = 0; i < nLen; i++)
+    {
+        if (pzFile[i] == '\\')
+            pzFile[i] = '/';
+    }
+    int nHandle = kopen4loadfrommod(pzFile, 0);
+    if (nHandle == -1)
+    {
+        // Hack
+        if (nLen >= 3 && isalpha(pzFile[0]) && pzFile[1] == ':' && pzFile[2] == '/')
+        {
+            pzFile += 3;
+            nHandle = kopen4loadfrommod(pzFile, 0);
+        }
+    }
+    return nHandle;
+}
+
+#define kSMKPal 5
+#define kSMKTile (MAXTILES-1)
+
+void credPlaySmk(const char *_pzSMK, const char *_pzWAV, int nWav)
+{
+#if 0
+    CSMKPlayer smkPlayer;
+    if (dword_148E14 >= 0)
+    {
+        if (toupper(*pzSMK) == 'A'+dword_148E14)
+        {
+            if (Redbook.sub_82258() == 0 || Redbook.sub_82258() > 20)
+                return;
+        }
+        Redbook.sub_82554();
+    }
+    smkPlayer.sub_82E6C(pzSMK, pzWAV);
+#endif
+    if (Bstrlen(_pzSMK) == 0)
+        return;
+    char *pzSMK = Xstrdup(_pzSMK);
+    char *pzWAV = Xstrdup(_pzWAV);
+    char *pzSMK_ = pzSMK;
+    char *pzWAV_ = pzWAV;
+    int nHandleSMK = credKOpen4Load(pzSMK);
+    if (nHandleSMK == -1)
+    {
+        Bfree(pzSMK_);
+        Bfree(pzWAV_);
+        return;
+    }
+    kclose(nHandleSMK);
+    SmackerHandle hSMK = Smacker_Open(pzSMK);
+    if (!hSMK.isValid)
+    {
+        Bfree(pzSMK_);
+        Bfree(pzWAV_);
+        return;
+    }
+    uint32_t nWidth, nHeight;
+    Smacker_GetFrameSize(hSMK, nWidth, nHeight);
+    uint8_t palette[768];
+    uint8_t *pFrame = (uint8_t*)Xmalloc(nWidth*nHeight);
+    waloff[kSMKTile] = (intptr_t)pFrame;
+    tilesiz[kSMKTile].y = nWidth;
+    tilesiz[kSMKTile].x = nHeight;
+    if (!pFrame)
+    {
+        Smacker_Close(hSMK);
+        Bfree(pzSMK_);
+        Bfree(pzWAV_);
+        return;
+    }
+    int nFrameRate = Smacker_GetFrameRate(hSMK);
+    int nFrames = Smacker_GetNumFrames(hSMK);
+
+    Smacker_GetPalette(hSMK, palette);
+    paletteSetColorTable(kSMKPal, palette);
+    videoSetPalette(gBrightness>>2, kSMKPal, 8+2);
+
+    int nScale;
+
+    if ((nWidth / (nHeight * 1.2f)) > (1.f * xdim / ydim))
+        nScale = divscale16(320 * xdim * 3, nWidth * ydim * 4);
+    else
+        nScale = divscale16(200, nHeight);
+
+    if (nWav)
+        sndStartWavID(nWav, FXVolume);
+    else
+    {
+        int nHandleWAV = credKOpen4Load(pzWAV);
+        if (nHandleWAV != -1)
+        {
+            kclose(nHandleWAV);
+            sndStartWavDisk(pzWAV, FXVolume);
+        }
+    }
+
+    UpdateDacs(0, true);
+
+    timerUpdate();
+    int32_t nStartTime = totalclock;
+
+    ctrlClearAllInput();
+    
+    int nFrame = 0;
+    do
+    {
+        G_HandleAsync();
+        if (scale(totalclock-nStartTime, nFrameRate, kTicRate) < nFrame)
+            continue;
+
+        if (ctrlCheckAllInput())
+            break;
+
+        videoClearScreen(0);
+        Smacker_GetPalette(hSMK, palette);
+        paletteSetColorTable(kSMKPal, palette);
+        videoSetPalette(gBrightness >> 2, kSMKPal, 0);
+        tileInvalidate(kSMKTile, 0, 1 << 4);  // JBF 20031228
+        Smacker_GetFrame(hSMK, pFrame);
+        rotatesprite_fs(160<<16, 100<<16, nScale, 512, kSMKTile, 0, 0, 2|4|8|64);
+
+        videoNextPage();
+
+        ctrlClearAllInput();
+        nFrame++;
+        Smacker_GetNextFrame(hSMK);
+    } while(nFrame < nFrames);
+
+    Smacker_Close(hSMK);
+    ctrlClearAllInput();
+    FX_StopAllSounds();
+    videoSetPalette(gBrightness >> 2, 0, 8+2);
+    Bfree(pFrame);
+    Bfree(pzSMK_);
+    Bfree(pzWAV_);
+}
diff --git a/source/blood/src/credits.h b/source/blood/src/credits.h
new file mode 100644
index 000000000..3e1bbaaa9
--- /dev/null
+++ b/source/blood/src/credits.h
@@ -0,0 +1,27 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+void credLogosDos(void);
+void credReset(void);
+void credPlaySmk(const char *pzSMK, const char *pzWAV, int nWAV);
\ No newline at end of file
diff --git a/source/blood/src/db.cpp b/source/blood/src/db.cpp
new file mode 100644
index 000000000..97c9308f2
--- /dev/null
+++ b/source/blood/src/db.cpp
@@ -0,0 +1,1240 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#ifdef POLYMER
+#include "polymer.h"
+#endif
+#include "compat.h"
+#include "common_game.h"
+#include "crc32.h"
+
+#include "actor.h"
+#include "blood.h"
+#include "db.h"
+#include "iob.h"
+
+unsigned short gStatCount[kMaxStatus + 1];
+
+XSPRITE xsprite[kMaxXSprites];
+XSECTOR xsector[kMaxXSectors];
+XWALL xwall[kMaxXWalls];
+
+int xvel[kMaxSprites], yvel[kMaxSprites], zvel[kMaxSprites];
+
+int gVisibility;
+
+const char *gItemText[] = {
+    "Skull Key",
+    "Eye Key",
+    "Fire Key",
+    "Dagger Key",
+    "Spider Key",
+    "Moon Key",
+    "Key 7",
+    "Doctor's Bag",
+    "Medicine Pouch",
+    "Life Essence",
+    "Life Seed",
+    "Red Potion",
+    "Feather Fall",
+    "Limited Invisibility",
+    "INVULNERABILITY",
+    "Boots of Jumping",
+    "Raven Flight",
+    "Guns Akimbo",
+    "Diving Suit",
+    "Gas mask",
+    "Clone",
+    "Crystal Ball",
+    "Decoy",
+    "Doppleganger",
+    "Reflective shots",
+    "Beast Vision",
+    "ShadowCloak",
+    "Rage shroom",
+    "Delirium Shroom",
+    "Grow shroom",
+    "Shrink shroom",
+    "Death mask",
+    "Wine Goblet",
+    "Wine Bottle",
+    "Skull Grail",
+    "Silver Grail",
+    "Tome",
+    "Black Chest",
+    "Wooden Chest",
+    "Asbestos Armor",
+    "Basic Armor",
+    "Body Armor",
+    "Fire Armor",
+    "Spirit Armor",
+    "Super Armor",
+    "Blue Team Base",
+    "Red Team Base",
+    "Blue Flag",
+    "Red Flag",
+    "DUMMY",
+    "Level map",
+};
+
+const char *gAmmoText[] = {
+    "Spray can",
+    "Bundle of TNT*",
+    "Bundle of TNT",
+    "Case of TNT",
+    "Proximity Detonator",
+    "Remote Detonator",
+    "Trapped Soul",
+    "4 shotgun shells",
+    "Box of shotgun shells",
+    "A few bullets",
+    "Voodoo Doll",
+    "OBSOLETE",
+    "Full drum of bullets",
+    "Tesla Charge",
+    "OBSOLETE",
+    "OBSOLETE",
+    "Flares",
+    "OBSOLETE",
+    "OBSOLETE",
+    "Gasoline Can",
+    NULL,
+};
+
+const char *gWeaponText[] = {
+    "RANDOM",
+    "Sawed-off",
+    "Tommy Gun",
+    "Flare Pistol",
+    "Voodoo Doll",
+    "Tesla Cannon",
+    "Napalm Launcher",
+    "Pitchfork",
+    "Spray Can",
+    "Dynamite",
+    "Life Leech",
+};
+
+
+
+void dbCrypt(char *pPtr, int nLength, int nKey)
+{
+    for (int i = 0; i < nLength; i++)
+    {
+        pPtr[i] = pPtr[i] ^ nKey;
+        nKey++;
+    }
+}
+
+void InsertSpriteSect(int nSprite, int nSector)
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    int nOther = headspritesect[nSector];
+    if (nOther >= 0)
+    {
+        prevspritesect[nSprite] = prevspritesect[nOther];
+        nextspritesect[nSprite] = -1;
+        nextspritesect[prevspritesect[nOther]] = nSprite;
+        prevspritesect[nOther] = nSprite;
+    }
+    else
+    {
+        prevspritesect[nSprite] = nSprite;
+        nextspritesect[nSprite] = -1;
+        headspritesect[nSector] = nSprite;
+    }
+    sprite[nSprite].sectnum = nSector;
+}
+
+void RemoveSpriteSect(int nSprite)
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    int nSector = sprite[nSprite].sectnum;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    int nOther = nextspritesect[nSprite];
+    if (nOther < 0)
+    {
+        nOther = headspritesect[nSector];
+    }
+    prevspritesect[nOther] = prevspritesect[nSprite];
+    if (headspritesect[nSector] != nSprite)
+    {
+        nextspritesect[prevspritesect[nSprite]] = nextspritesect[nSprite];
+    }
+    else
+    {
+        headspritesect[nSector] = nextspritesect[nSprite];
+    }
+    sprite[nSprite].sectnum = -1;
+}
+
+void InsertSpriteStat(int nSprite, int nStat)
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    dassert(nStat >= 0 && nStat <= kMaxStatus);
+    int nOther = headspritestat[nStat];
+    if (nOther >= 0)
+    {
+        prevspritestat[nSprite] = prevspritestat[nOther];
+        nextspritestat[nSprite] = -1;
+        nextspritestat[prevspritestat[nOther]] = nSprite;
+        prevspritestat[nOther] = nSprite;
+    }
+    else
+    {
+        prevspritestat[nSprite] = nSprite;
+        nextspritestat[nSprite] = -1;
+        headspritestat[nStat] = nSprite;
+    }
+    sprite[nSprite].statnum = nStat;
+    gStatCount[nStat]++;
+}
+
+void RemoveSpriteStat(int nSprite)
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    int nStat = sprite[nSprite].statnum;
+    dassert(nStat >= 0 && nStat <= kMaxStatus);
+    int nOther = nextspritestat[nSprite];
+    if (nOther < 0)
+    {
+        nOther = headspritestat[nStat];
+    }
+    prevspritestat[nOther] = prevspritestat[nSprite];
+    if (headspritestat[nStat] != nSprite)
+    {
+        nextspritestat[prevspritestat[nSprite]] = nextspritestat[nSprite];
+    }
+    else
+    {
+        headspritestat[nStat] = nextspritestat[nSprite];
+    }
+    sprite[nSprite].statnum = -1;
+    gStatCount[nStat]--;
+}
+
+void qinitspritelists(void) // Replace
+{
+    for (short i = 0; i <= kMaxSectors; i++)
+    {
+        headspritesect[i] = -1;
+    }
+    for (short i = 0; i <= kMaxStatus; i++)
+    {
+        headspritestat[i] = -1;
+    }
+    for (short i = 0; i < kMaxSprites; i++)
+    {
+        sprite[i].sectnum = -1;
+        sprite[i].index = -1;
+        InsertSpriteStat(i, kMaxStatus);
+    }
+    memset(gStatCount, 0, sizeof(gStatCount));
+}
+
+int InsertSprite(int nSector, int nStat)
+{
+    int nSprite = headspritestat[kMaxStatus];
+    dassert(nSprite < kMaxSprites);
+    if (nSprite < 0)
+    {
+        return nSprite;
+    }
+    RemoveSpriteStat(nSprite);
+    spritetype *pSprite = &sprite[nSprite];
+    memset(&sprite[nSprite], 0, sizeof(spritetype));
+    InsertSpriteStat(nSprite, nStat);
+    InsertSpriteSect(nSprite, nSector);
+    pSprite->cstat = 128;
+    pSprite->clipdist = 32;
+    pSprite->xrepeat = pSprite->yrepeat = 64;
+    pSprite->owner = -1;
+    pSprite->extra = -1;
+    pSprite->index = nSprite;
+    xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+
+    return nSprite;
+}
+
+int qinsertsprite(short nSector, short nStat) // Replace
+{
+    return InsertSprite(nSector, nStat);
+}
+
+int DeleteSprite(int nSprite)
+{
+    if (sprite[nSprite].extra > 0)
+    {
+        dbDeleteXSprite(sprite[nSprite].extra);
+    }
+    dassert(sprite[nSprite].statnum >= 0 && sprite[nSprite].statnum < kMaxStatus);
+    RemoveSpriteStat(nSprite);
+    dassert(sprite[nSprite].sectnum >= 0 && sprite[nSprite].sectnum < kMaxSectors);
+    RemoveSpriteSect(nSprite);
+    InsertSpriteStat(nSprite, kMaxStatus);
+
+    return nSprite;
+}
+
+int qdeletesprite(short nSprite) // Replace
+{
+    return DeleteSprite(nSprite);
+}
+
+int ChangeSpriteSect(int nSprite, int nSector)
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    dassert(sprite[nSprite].sectnum >= 0 && sprite[nSprite].sectnum < kMaxSectors);
+    RemoveSpriteSect(nSprite);
+    InsertSpriteSect(nSprite, nSector);
+    return 0;
+}
+
+int qchangespritesect(short nSprite, short nSector)
+{
+    return ChangeSpriteSect(nSprite, nSector);
+}
+
+int ChangeSpriteStat(int nSprite, int nStatus)
+{
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    dassert(nStatus >= 0 && nStatus < kMaxStatus);
+    dassert(sprite[nSprite].statnum >= 0 && sprite[nSprite].statnum < kMaxStatus);
+    dassert(sprite[nSprite].sectnum >= 0 && sprite[nSprite].sectnum < kMaxSectors);
+    RemoveSpriteStat(nSprite);
+    InsertSpriteStat(nSprite, nStatus);
+    return 0;
+}
+
+int qchangespritestat(short nSprite, short nStatus)
+{
+    return ChangeSpriteStat(nSprite, nStatus);
+}
+
+unsigned short nextXSprite[kMaxXSprites];
+unsigned short nextXWall[kMaxXWalls];
+unsigned short nextXSector[kMaxXSectors];
+
+void InitFreeList(unsigned short *pList, int nCount)
+{
+    for (int i = 1; i < nCount; i++)
+    {
+        pList[i] = i-1;
+    }
+    pList[0] = nCount - 1;
+}
+
+void InsertFree(unsigned short *pList, int nIndex)
+{
+    pList[nIndex] = pList[0];
+    pList[0] = nIndex;
+}
+
+unsigned short dbInsertXSprite(int nSprite)
+{
+    int nXSprite = nextXSprite[0];
+    nextXSprite[0] = nextXSprite[nXSprite];
+    if (nXSprite == 0)
+    {
+        ThrowError("Out of free XSprites");
+    }
+    memset(&xsprite[nXSprite], 0, sizeof(XSPRITE));
+    if (!bVanilla)
+        memset(&gSpriteHit[nXSprite], 0, sizeof(SPRITEHIT));
+    xsprite[nXSprite].reference = nSprite;
+    sprite[nSprite].extra = nXSprite;
+    return nXSprite;
+}
+
+void dbDeleteXSprite(int nXSprite)
+{
+    dassert(xsprite[nXSprite].reference >= 0);
+    dassert(sprite[xsprite[nXSprite].reference].extra == nXSprite);
+    InsertFree(nextXSprite, nXSprite);
+    sprite[xsprite[nXSprite].reference].extra = -1;
+    xsprite[nXSprite].reference = -1;
+}
+
+unsigned short dbInsertXWall(int nWall)
+{
+    int nXWall = nextXWall[0];
+    nextXWall[0] = nextXWall[nXWall];
+    if (nXWall == 0)
+    {
+        ThrowError("Out of free XWalls");
+    }
+    memset(&xwall[nXWall], 0, sizeof(XWALL));
+    xwall[nXWall].reference = nWall;
+    wall[nWall].extra = nXWall;
+    return nXWall;
+}
+
+void dbDeleteXWall(int nXWall)
+{
+    dassert(xwall[nXWall].reference >= 0);
+    InsertFree(nextXWall, nXWall);
+    wall[xwall[nXWall].reference].extra = -1;
+    xwall[nXWall].reference = -1;
+}
+
+unsigned short dbInsertXSector(int nSector)
+{
+    int nXSector = nextXSector[0];
+    nextXSector[0] = nextXSector[nXSector];
+    if (nXSector == 0)
+    {
+        ThrowError("Out of free XSectors");
+    }
+    memset(&xsector[nXSector], 0, sizeof(XSECTOR));
+    xsector[nXSector].reference = nSector;
+    sector[nSector].extra = nXSector;
+    return nXSector;
+}
+
+void dbDeleteXSector(int nXSector)
+{
+    dassert(xsector[nXSector].reference >= 0);
+    InsertFree(nextXSector, nXSector);
+    sector[xsector[nXSector].reference].extra = -1;
+    xsector[nXSector].reference = -1;
+}
+
+void dbXSpriteClean(void)
+{
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        int nXSprite = sprite[i].extra;
+        if (nXSprite == 0)
+        {
+            sprite[i].extra = -1;
+        }
+        if (sprite[i].statnum < kMaxStatus && nXSprite > 0)
+        {
+            dassert(nXSprite < kMaxXSprites);
+            if (xsprite[nXSprite].reference != i)
+            {
+                int nXSprite2 = dbInsertXSprite(i);
+                memcpy(&xsprite[nXSprite2], &xsprite[nXSprite], sizeof(XSPRITE));
+                xsprite[nXSprite2].reference = i;
+            }
+        }
+    }
+    for (int i = 1; i < kMaxXSprites; i++)
+    {
+        int nSprite = xsprite[i].reference;
+        if (nSprite >= 0)
+        {
+            dassert(nSprite < kMaxSprites);
+            if (sprite[nSprite].statnum >= kMaxStatus || sprite[nSprite].extra != i)
+            {
+                InsertFree(nextXSprite, i);
+                xsprite[i].reference = -1;
+            }
+        }
+    }
+}
+
+void dbXWallClean(void)
+{
+    for (int i = 0; i < numwalls; i++)
+    {
+        int nXWall = wall[i].extra;
+        if (nXWall == 0)
+        {
+            wall[i].extra = -1;
+        }
+        if (nXWall > 0)
+        {
+            dassert(nXWall < kMaxXWalls);
+            if (xwall[nXWall].reference == -1)
+            {
+                wall[i].extra = -1;
+            }
+            else
+            {
+                xwall[nXWall].reference = i;
+            }
+        }
+    }
+    for (int i = 0; i < numwalls; i++)
+    {
+        int nXWall = wall[i].extra;
+        if (nXWall > 0)
+        {
+            dassert(nXWall < kMaxXWalls);
+            if (xwall[nXWall].reference != i)
+            {
+                int nXWall2 = dbInsertXWall(i);
+                memcpy(&xwall[nXWall2], &xwall[nXWall], sizeof(XWALL));
+                xwall[nXWall2].reference = i;
+            }
+        }
+    }
+    for (int i = 1; i < kMaxXWalls; i++)
+    {
+        int nWall = xwall[i].reference;
+        if (nWall >= 0)
+        {
+            dassert(nWall < kMaxWalls);
+            if (nWall >= numwalls || wall[nWall].extra != i)
+            {
+                InsertFree(nextXWall, i);
+                xwall[i].reference = -1;
+            }
+        }
+    }
+}
+
+void dbXSectorClean(void)
+{
+    for (int i = 0; i < numsectors; i++)
+    {
+        int nXSector = sector[i].extra;
+        if (nXSector == 0)
+        {
+            sector[i].extra = -1;
+        }
+        if (nXSector > 0)
+        {
+            dassert(nXSector < kMaxXSectors);
+            if (xsector[nXSector].reference == -1)
+            {
+                sector[i].extra = -1;
+            }
+            else
+            {
+                xsector[nXSector].reference = i;
+            }
+        }
+    }
+    for (int i = 0; i < numsectors; i++)
+    {
+        int nXSector = sector[i].extra;
+        if (nXSector > 0)
+        {
+            dassert(nXSector < kMaxXSectors);
+            if (xsector[nXSector].reference != i)
+            {
+                int nXSector2 = dbInsertXSector(i);
+                memcpy(&xsector[nXSector2], &xsector[nXSector], sizeof(XSECTOR));
+                xsector[nXSector2].reference = i;
+            }
+        }
+    }
+    for (int i = 1; i < kMaxXSectors; i++)
+    {
+        int nSector = xsector[i].reference;
+        if (nSector >= 0)
+        {
+            dassert(nSector < kMaxSectors);
+            if (nSector >= numsectors || sector[nSector].extra != i)
+            {
+                InsertFree(nextXSector, i);
+                xsector[i].reference = -1;
+            }
+        }
+    }
+}
+
+void dbInit(void)
+{
+    InitFreeList(nextXSprite, kMaxXSprites);
+    for (int i = 1; i < kMaxXSprites; i++)
+    {
+        xsprite[i].reference = -1;
+    }
+    InitFreeList(nextXWall, kMaxXWalls);
+    for (int i = 1; i < kMaxXWalls; i++)
+    {
+        xwall[i].reference = -1;
+    }
+    InitFreeList(nextXSector, kMaxXSectors);
+    for (int i = 1; i < kMaxXSectors; i++)
+    {
+        xsector[i].reference = -1;
+    }
+    initspritelists();
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        sprite[i].cstat = 128;
+    }
+}
+
+void PropagateMarkerReferences(void)
+{
+    int nSprite, nNextSprite;
+    for (nSprite = headspritestat[10]; nSprite != -1; nSprite = nNextSprite)
+    {
+        nNextSprite = nextspritestat[nSprite];
+        switch (sprite[nSprite].type)
+        {
+        case 8:
+        {
+            int nOwner = sprite[nSprite].owner;
+            if (nOwner >= 0 && nOwner < numsectors)
+            {
+                int nXSector = sector[nOwner].extra;
+                if (nXSector > 0 && nXSector < kMaxXSectors)
+                {
+                    xsector[nXSector].at2c_0 = nSprite;
+                    continue;
+                }
+            }
+            break;
+        }
+        case 3:
+        {
+            int nOwner = sprite[nSprite].owner;
+            if (nOwner >= 0 && nOwner < numsectors)
+            {
+                int nXSector = sector[nOwner].extra;
+                if (nXSector > 0 && nXSector < kMaxXSectors)
+                {
+                    xsector[nXSector].at2c_0 = nSprite;
+                    continue;
+                }
+            }
+            break;
+        }
+        case 4:
+        {
+            int nOwner = sprite[nSprite].owner;
+            if (nOwner >= 0 && nOwner < numsectors)
+            {
+                int nXSector = sector[nOwner].extra;
+                if (nXSector > 0 && nXSector < kMaxXSectors)
+                {
+                    xsector[nXSector].at2e_0 = nSprite;
+                    continue;
+                }
+            }
+            break;
+        }
+        case 5:
+        {
+            int nOwner = sprite[nSprite].owner;
+            if (nOwner >= 0 && nOwner < numsectors)
+            {
+                int nXSector = sector[nOwner].extra;
+                if (nXSector > 0 && nXSector < kMaxXSectors)
+                {
+                    xsector[nXSector].at2c_0 = nSprite;
+                    continue;
+                }
+            }
+            break;
+        }
+        }
+        DeleteSprite(nSprite);
+    }
+}
+
+bool byte_1A76C6, byte_1A76C7, byte_1A76C8;
+
+MAPHEADER2 byte_19AE44;
+
+unsigned int dbReadMapCRC(const char *pPath)
+{
+    byte_1A76C7 = 0;
+    byte_1A76C8 = 0;
+    DICTNODE *pNode = gSysRes.Lookup(pPath, "MAP");
+    if (!pNode)
+    {
+        ThrowError("Error opening map file %s", pPath);
+    }
+    char *pData = (char*)gSysRes.Lock(pNode);
+    int nSize = pNode->size;
+    MAPSIGNATURE header;
+    IOBuffer(nSize, pData).Read(&header, 6);
+#if B_BIG_ENDIAN == 1
+    header.version = B_LITTLE16(header.version);
+#endif
+    if (memcmp(header.signature, "BLM\x1a", 4))
+    {
+        ThrowError("Map file corrupted");
+    }
+    if ((header.version & 0xff00) == 0x600)
+    {
+    }
+    else if ((header.version & 0xff00) == 0x700)
+    {
+        byte_1A76C8 = 1;
+    }
+    else
+    {
+        ThrowError("Map file is wrong version");
+    }
+    unsigned int nCRC = *(unsigned int*)(pData+nSize-4);
+    gSysRes.Unlock(pNode);
+    return nCRC;
+}
+
+int gMapRev, gSongId, gSkyCount;
+//char byte_19AE44[128];
+
+void dbLoadMap(const char *pPath, int *pX, int *pY, int *pZ, short *pAngle, short *pSector, unsigned int *pCRC)
+{
+    int16_t tpskyoff[256];
+    memset(show2dsector, 0, sizeof(show2dsector));
+    memset(show2dwall, 0, sizeof(show2dwall));
+    memset(show2dsprite, 0, sizeof(show2dsprite));
+#ifdef USE_OPENGL
+    Polymost_prepare_loadboard();
+#endif
+    {
+        char name2[BMAX_PATH];
+        Bstrncpy(name2, pPath, BMAX_PATH);
+        Bstrupr(name2);
+        DICTNODE* pNode = *gSysRes.Probe(name2, "MAP");
+        if (pNode && pNode->flags & DICT_EXTERNAL)
+        {
+            gSysRes.RemoveNode(pNode);
+        }
+    }
+    DICTNODE *pNode = gSysRes.Lookup(pPath, "MAP");
+    if (!pNode)
+    {
+        ThrowError("Error opening map file %s", pPath);
+    }
+    char *pData = (char*)gSysRes.Lock(pNode);
+    int nSize = pNode->size;
+    MAPSIGNATURE header;
+    IOBuffer IOBuffer1 = IOBuffer(nSize, pData);
+    IOBuffer1.Read(&header, 6);
+#if B_BIG_ENDIAN == 1
+    header.version = B_LITTLE16(header.version);
+#endif
+    if (memcmp(header.signature, "BLM\x1a", 4))
+    {
+        ThrowError("Map file corrupted");
+    }
+    byte_1A76C8 = 0;
+    if ((header.version & 0xff00) == 0x600)
+    {
+    }
+    else if ((header.version & 0xff00) == 0x700)
+    {
+        byte_1A76C8 = 1;
+    }
+    else
+    {
+        ThrowError("Map file is wrong version");
+    }
+    MAPHEADER mapHeader;
+    IOBuffer1.Read(&mapHeader,37/* sizeof(mapHeader)*/);
+    if (mapHeader.at16 != 0 && mapHeader.at16 != 0x7474614d && mapHeader.at16 != 0x4d617474)
+    {
+        dbCrypt((char*)&mapHeader, sizeof(mapHeader), 0x7474614d);
+        byte_1A76C7 = 1;
+    }
+#if B_BIG_ENDIAN == 1
+    mapHeader.at0 = B_LITTLE32(mapHeader.at0);
+    mapHeader.at4 = B_LITTLE32(mapHeader.at4);
+    mapHeader.at8 = B_LITTLE32(mapHeader.at8);
+    mapHeader.atc = B_LITTLE16(mapHeader.atc);
+    mapHeader.ate = B_LITTLE16(mapHeader.ate);
+    mapHeader.at10 = B_LITTLE16(mapHeader.at10);
+    mapHeader.at12 = B_LITTLE32(mapHeader.at12);
+    mapHeader.at16 = B_LITTLE32(mapHeader.at16);
+    mapHeader.at1b = B_LITTLE32(mapHeader.at1b);
+    mapHeader.at1f = B_LITTLE16(mapHeader.at1f);
+    mapHeader.at21 = B_LITTLE16(mapHeader.at21);
+    mapHeader.at23 = B_LITTLE16(mapHeader.at23);
+#endif
+
+    psky_t *pSky = tileSetupSky(0);
+    pSky->horizfrac = 65536;
+
+    *pX = mapHeader.at0;
+    *pY = mapHeader.at4;
+    *pZ = mapHeader.at8;
+    *pAngle = mapHeader.atc;
+    *pSector = mapHeader.ate;
+    pSky->lognumtiles = mapHeader.at10;
+    gVisibility = g_visibility = mapHeader.at12;
+    gSongId = mapHeader.at16;
+    if (byte_1A76C8)
+    {
+        if (mapHeader.at16 == 0x7474614d || mapHeader.at16 == 0x4d617474)
+        {
+            byte_1A76C6 = 1;
+        }
+        else if (!mapHeader.at16)
+        {
+            byte_1A76C6 = 0;
+        }
+        else
+        {
+            ThrowError("Corrupted Map file");
+        }
+    }
+    else if (mapHeader.at16)
+    {
+        ThrowError("Corrupted Map file");
+    }
+    parallaxtype = mapHeader.at1a;
+    gMapRev = mapHeader.at1b;
+    numsectors = mapHeader.at1f;
+    numwalls = mapHeader.at21;
+    dbInit();
+    if (byte_1A76C8)
+    {
+        IOBuffer1.Read(&byte_19AE44, 128);
+        dbCrypt((char*)&byte_19AE44, 128, numwalls);
+#if B_BIG_ENDIAN == 1
+        byte_19AE44.at40 = B_LITTLE32(byte_19AE44.at40);
+        byte_19AE44.at44 = B_LITTLE32(byte_19AE44.at44);
+        byte_19AE44.at48 = B_LITTLE32(byte_19AE44.at48);
+#endif
+    }
+    else
+    {
+        memset(&byte_19AE44, 0, 128);
+    }
+    gSkyCount = 1<<pSky->lognumtiles;
+    IOBuffer1.Read(tpskyoff, gSkyCount*sizeof(tpskyoff[0]));
+    if (byte_1A76C8)
+    {
+        dbCrypt((char*)tpskyoff, gSkyCount*sizeof(tpskyoff[0]), gSkyCount*2);
+    }
+    for (int i = 0; i < ClipHigh(gSkyCount, MAXPSKYTILES); i++)
+    {
+        pSky->tileofs[i] = B_LITTLE16(tpskyoff[i]);
+    }
+    for (int i = 0; i < numsectors; i++)
+    {
+        sectortype *pSector = &sector[i];
+        IOBuffer1.Read(pSector, sizeof(sectortype));
+        if (byte_1A76C8)
+        {
+            dbCrypt((char*)pSector, sizeof(sectortype), gMapRev*sizeof(sectortype));
+        }
+#if B_BIG_ENDIAN == 1
+        pSector->wallptr = B_LITTLE16(pSector->wallptr);
+        pSector->wallnum = B_LITTLE16(pSector->wallnum);
+        pSector->ceilingz = B_LITTLE32(pSector->ceilingz);
+        pSector->floorz = B_LITTLE32(pSector->floorz);
+        pSector->ceilingstat = B_LITTLE16(pSector->ceilingstat);
+        pSector->floorstat = B_LITTLE16(pSector->floorstat);
+        pSector->ceilingpicnum = B_LITTLE16(pSector->ceilingpicnum);
+        pSector->ceilingheinum = B_LITTLE16(pSector->ceilingheinum);
+        pSector->floorpicnum = B_LITTLE16(pSector->floorpicnum);
+        pSector->floorheinum = B_LITTLE16(pSector->floorheinum);
+        pSector->lotag = B_LITTLE16(pSector->lotag);
+        pSector->hitag = B_LITTLE16(pSector->hitag);
+        pSector->extra = B_LITTLE16(pSector->extra);
+#endif
+        qsector_filler[i] = pSector->fogpal;
+        pSector->fogpal = 0;
+        if (sector[i].extra > 0)
+        {
+            const int nXSectorSize = 60;
+            char pBuffer[nXSectorSize];
+            int nXSector = dbInsertXSector(i);
+            XSECTOR *pXSector = &xsector[nXSector];
+            memset(pXSector, 0, sizeof(XSECTOR));
+            int nCount;
+            if (!byte_1A76C8)
+            {
+                nCount = nXSectorSize;
+            }
+            else
+            {
+                nCount = byte_19AE44.at48;
+            }
+            dassert(nCount <= nXSectorSize);
+            IOBuffer1.Read(pBuffer, nCount);
+            BitReader bitReader(pBuffer, nCount);
+            pXSector->reference = bitReader.readSigned(14);
+            pXSector->state = bitReader.readUnsigned(1);
+            pXSector->busy = bitReader.readUnsigned(17);
+            pXSector->data = bitReader.readUnsigned(16);
+            pXSector->txID = bitReader.readUnsigned(10);
+            pXSector->at7_2 = bitReader.readUnsigned(3);
+            pXSector->at7_5 = bitReader.readUnsigned(3);
+            pXSector->rxID = bitReader.readUnsigned(10);
+            pXSector->command = bitReader.readUnsigned(8);
+            pXSector->triggerOn = bitReader.readUnsigned(1);
+            pXSector->triggerOff = bitReader.readUnsigned(1);
+            pXSector->busyTimeA = bitReader.readUnsigned(12);
+            pXSector->waitTimeA = bitReader.readUnsigned(12);
+            pXSector->atd_4 = bitReader.readUnsigned(1);
+            pXSector->interruptable = bitReader.readUnsigned(1);
+            pXSector->amplitude = bitReader.readSigned(8);
+            pXSector->freq = bitReader.readUnsigned(8);
+            pXSector->atf_6 = bitReader.readUnsigned(1);
+            pXSector->atf_7 = bitReader.readUnsigned(1);
+            pXSector->phase = bitReader.readUnsigned(8);
+            pXSector->wave = bitReader.readUnsigned(4);
+            pXSector->shadeAlways = bitReader.readUnsigned(1);
+            pXSector->shadeFloor = bitReader.readUnsigned(1);
+            pXSector->shadeCeiling = bitReader.readUnsigned(1);
+            pXSector->shadeWalls = bitReader.readUnsigned(1);
+            pXSector->shade = bitReader.readSigned(8);
+            pXSector->panAlways = bitReader.readUnsigned(1);
+            pXSector->panFloor = bitReader.readUnsigned(1);
+            pXSector->panCeiling = bitReader.readUnsigned(1);
+            pXSector->Drag = bitReader.readUnsigned(1);
+            pXSector->Underwater = bitReader.readUnsigned(1);
+            pXSector->Depth = bitReader.readUnsigned(3);
+            pXSector->panVel = bitReader.readUnsigned(8);
+            pXSector->panAngle = bitReader.readUnsigned(11);
+            pXSector->at16_3 = bitReader.readUnsigned(1);
+            pXSector->decoupled = bitReader.readUnsigned(1);
+            pXSector->triggerOnce = bitReader.readUnsigned(1);
+            pXSector->at16_6 = bitReader.readUnsigned(1);
+            pXSector->Key = bitReader.readUnsigned(3);
+            pXSector->Push = bitReader.readUnsigned(1);
+            pXSector->Vector = bitReader.readUnsigned(1);
+            pXSector->Reserved = bitReader.readUnsigned(1);
+            pXSector->Enter = bitReader.readUnsigned(1);
+            pXSector->Exit = bitReader.readUnsigned(1);
+            pXSector->Wallpush = bitReader.readUnsigned(1);
+            pXSector->color = bitReader.readUnsigned(1);
+            pXSector->at18_1 = bitReader.readUnsigned(1);
+            pXSector->busyTimeB = bitReader.readUnsigned(12);
+            pXSector->waitTimeB = bitReader.readUnsigned(12);
+            pXSector->at1b_2 = bitReader.readUnsigned(1);
+            pXSector->at1b_3 = bitReader.readUnsigned(1);
+            pXSector->ceilpal = bitReader.readUnsigned(4);
+            pXSector->at1c_0 = bitReader.readSigned(32);
+            pXSector->at20_0 = bitReader.readSigned(32);
+            pXSector->at24_0 = bitReader.readSigned(32);
+            pXSector->at28_0 = bitReader.readSigned(32);
+            pXSector->at2c_0 = bitReader.readUnsigned(16);
+            pXSector->at2e_0 = bitReader.readUnsigned(16);
+            pXSector->Crush = bitReader.readUnsigned(1);
+            pXSector->at30_1 = bitReader.readUnsigned(8);
+            pXSector->at31_1 = bitReader.readUnsigned(8);
+            pXSector->at32_1 = bitReader.readUnsigned(8);
+            pXSector->damageType = bitReader.readUnsigned(3);
+            pXSector->floorpal = bitReader.readUnsigned(4);
+            pXSector->at34_0 = bitReader.readUnsigned(8);
+            pXSector->locked = bitReader.readUnsigned(1);
+            pXSector->windVel = bitReader.readUnsigned(10);
+            pXSector->windAng = bitReader.readUnsigned(11);
+            pXSector->windAlways = bitReader.readUnsigned(1);
+            pXSector->at37_7 = bitReader.readUnsigned(1);
+            pXSector->bobTheta = bitReader.readUnsigned(11);
+            pXSector->bobZRange = bitReader.readUnsigned(5);
+            pXSector->bobSpeed = bitReader.readSigned(12);
+            pXSector->bobAlways = bitReader.readUnsigned(1);
+            pXSector->bobFloor = bitReader.readUnsigned(1);
+            pXSector->bobCeiling = bitReader.readUnsigned(1);
+            pXSector->bobRotate = bitReader.readUnsigned(1);
+            xsector[sector[i].extra].reference = i;
+            xsector[sector[i].extra].busy = xsector[sector[i].extra].state<<16;
+        }
+    }
+    for (int i = 0; i < numwalls; i++)
+    {
+        walltype *pWall = &wall[i];
+        IOBuffer1.Read(pWall, sizeof(walltype));
+        if (byte_1A76C8)
+        {
+            dbCrypt((char*)pWall, sizeof(walltype), (gMapRev*sizeof(sectortype)) | 0x7474614d);
+        }
+#if B_BIG_ENDIAN == 1
+        pWall->x = B_LITTLE32(pWall->x);
+        pWall->y = B_LITTLE32(pWall->y);
+        pWall->point2 = B_LITTLE16(pWall->point2);
+        pWall->nextwall = B_LITTLE16(pWall->nextwall);
+        pWall->nextsector = B_LITTLE16(pWall->nextsector);
+        pWall->cstat = B_LITTLE16(pWall->cstat);
+        pWall->picnum = B_LITTLE16(pWall->picnum);
+        pWall->overpicnum = B_LITTLE16(pWall->overpicnum);
+        pWall->lotag = B_LITTLE16(pWall->lotag);
+        pWall->hitag = B_LITTLE16(pWall->hitag);
+        pWall->extra = B_LITTLE16(pWall->extra);
+#endif
+        if (wall[i].extra > 0)
+        {
+            const int nXWallSize = 24;
+            char pBuffer[nXWallSize];
+            int nXWall = dbInsertXWall(i);
+            XWALL *pXWall = &xwall[nXWall];
+            memset(pXWall, 0, sizeof(XWALL));
+            int nCount;
+            if (!byte_1A76C8)
+            {
+                nCount = nXWallSize;
+            }
+            else
+            {
+                nCount = byte_19AE44.at44;
+            }
+            dassert(nCount <= nXWallSize);
+            IOBuffer1.Read(pBuffer, nCount);
+            BitReader bitReader(pBuffer, nCount);
+            pXWall->reference = bitReader.readSigned(14);
+            pXWall->state = bitReader.readUnsigned(1);
+            pXWall->busy = bitReader.readUnsigned(17);
+            pXWall->data = bitReader.readSigned(16);
+            pXWall->txID = bitReader.readUnsigned(10);
+            pXWall->at7_2 = bitReader.readUnsigned(6);
+            pXWall->rxID = bitReader.readUnsigned(10);
+            pXWall->command = bitReader.readUnsigned(8);
+            pXWall->triggerOn = bitReader.readUnsigned(1);
+            pXWall->triggerOff = bitReader.readUnsigned(1);
+            pXWall->busyTime = bitReader.readUnsigned(12);
+            pXWall->waitTime = bitReader.readUnsigned(12);
+            pXWall->restState = bitReader.readUnsigned(1);
+            pXWall->interruptable = bitReader.readUnsigned(1);
+            pXWall->panAlways = bitReader.readUnsigned(1);
+            pXWall->panXVel = bitReader.readSigned(8);
+            pXWall->panYVel = bitReader.readSigned(8);
+            pXWall->decoupled = bitReader.readUnsigned(1);
+            pXWall->triggerOnce = bitReader.readUnsigned(1);
+            pXWall->isTriggered = bitReader.readUnsigned(1);
+            pXWall->key = bitReader.readUnsigned(3);
+            pXWall->triggerPush = bitReader.readUnsigned(1);
+            pXWall->triggerVector = bitReader.readUnsigned(1);
+            pXWall->triggerReserved = bitReader.readUnsigned(1);
+            pXWall->at11_0 = bitReader.readUnsigned(2);
+            pXWall->xpanFrac = bitReader.readUnsigned(8);
+            pXWall->ypanFrac = bitReader.readUnsigned(8);
+            pXWall->locked = bitReader.readUnsigned(1);
+            pXWall->dudeLockout = bitReader.readUnsigned(1);
+            pXWall->at13_4 = bitReader.readUnsigned(4);
+            pXWall->at14_0 = bitReader.readUnsigned(32);
+            xwall[wall[i].extra].reference = i;
+            xwall[wall[i].extra].busy = xwall[wall[i].extra].state << 16;
+        }
+    }
+    initspritelists();
+    for (int i = 0; i < mapHeader.at23; i++)
+    {
+        RemoveSpriteStat(i);
+        spritetype *pSprite = &sprite[i];
+        IOBuffer1.Read(pSprite, sizeof(spritetype));
+        if (byte_1A76C8)
+        {
+            dbCrypt((char*)pSprite, sizeof(spritetype), (gMapRev*sizeof(spritetype)) | 0x7474614d);
+        }
+#if B_BIG_ENDIAN == 1
+        pSprite->x = B_LITTLE32(pSprite->x);
+        pSprite->y = B_LITTLE32(pSprite->y);
+        pSprite->z = B_LITTLE32(pSprite->z);
+        pSprite->cstat = B_LITTLE16(pSprite->cstat);
+        pSprite->picnum = B_LITTLE16(pSprite->picnum);
+        pSprite->sectnum = B_LITTLE16(pSprite->sectnum);
+        pSprite->statnum = B_LITTLE16(pSprite->statnum);
+        pSprite->ang = B_LITTLE16(pSprite->ang);
+        pSprite->owner = B_LITTLE16(pSprite->owner);
+        pSprite->xvel = B_LITTLE16(pSprite->xvel);
+        pSprite->yvel = B_LITTLE16(pSprite->yvel);
+        pSprite->zvel = B_LITTLE16(pSprite->zvel);
+        pSprite->lotag = B_LITTLE16(pSprite->lotag);
+        pSprite->hitag = B_LITTLE16(pSprite->hitag);
+        pSprite->extra = B_LITTLE16(pSprite->extra);
+#endif
+        // NoOne's extension bit
+        if (pSprite->hitag&1)
+        {
+            pSprite->hitag &= ~1;
+            pSprite->hitag |= kHitagExtBit;
+        }
+        InsertSpriteSect(i, sprite[i].sectnum);
+        InsertSpriteStat(i, sprite[i].statnum);
+        sprite[i].index = i;
+        qsprite_filler[i] = pSprite->blend;
+        pSprite->blend = 0;
+        if (sprite[i].extra > 0)
+        {
+            const int nXSpriteSize = 56;
+            char pBuffer[nXSpriteSize];
+            int nXSprite = dbInsertXSprite(i);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            memset(pXSprite, 0, sizeof(XSPRITE));
+            int nCount;
+            if (!byte_1A76C8)
+            {
+                nCount = nXSpriteSize;
+            }
+            else
+            {
+                nCount = byte_19AE44.at40;
+            }
+            dassert(nCount <= nXSpriteSize);
+            IOBuffer1.Read(pBuffer, nCount);
+            BitReader bitReader(pBuffer, nCount);
+            pXSprite->reference = bitReader.readSigned(14);
+            pXSprite->state = bitReader.readUnsigned(1);
+            pXSprite->busy = bitReader.readUnsigned(17);
+            pXSprite->txID = bitReader.readUnsigned(10);
+            pXSprite->rxID = bitReader.readUnsigned(10);
+            pXSprite->command = bitReader.readUnsigned(8);
+            pXSprite->triggerOn = bitReader.readUnsigned(1);
+            pXSprite->triggerOff = bitReader.readUnsigned(1);
+            pXSprite->wave = bitReader.readUnsigned(2);
+            pXSprite->busyTime = bitReader.readUnsigned(12);
+            pXSprite->waitTime = bitReader.readUnsigned(12);
+            pXSprite->restState = bitReader.readUnsigned(1);
+            pXSprite->Interrutable = bitReader.readUnsigned(1);
+            pXSprite->atb_2 = bitReader.readUnsigned(2);
+            pXSprite->respawnPending = bitReader.readUnsigned(2);
+            pXSprite->atb_6 = bitReader.readUnsigned(1);
+            pXSprite->lT = bitReader.readUnsigned(1);
+            pXSprite->dropMsg = bitReader.readUnsigned(8);
+            pXSprite->Decoupled = bitReader.readUnsigned(1);
+            pXSprite->triggerOnce = bitReader.readUnsigned(1);
+            pXSprite->isTriggered = bitReader.readUnsigned(1);
+            pXSprite->key = bitReader.readUnsigned(3);
+            pXSprite->Push = bitReader.readUnsigned(1);
+            pXSprite->Vector = bitReader.readUnsigned(1);
+            pXSprite->Impact = bitReader.readUnsigned(1);
+            pXSprite->Pickup = bitReader.readUnsigned(1);
+            pXSprite->Touch = bitReader.readUnsigned(1);
+            pXSprite->Sight = bitReader.readUnsigned(1);
+            pXSprite->Proximity = bitReader.readUnsigned(1);
+            pXSprite->ate_5 = bitReader.readUnsigned(2);
+            pXSprite->lSkill = bitReader.readUnsigned(5);
+            pXSprite->lS = bitReader.readUnsigned(1);
+            pXSprite->lB = bitReader.readUnsigned(1);
+            pXSprite->lC = bitReader.readUnsigned(1);
+            pXSprite->DudeLockout = bitReader.readUnsigned(1);
+            pXSprite->data1 = bitReader.readSigned(16);
+            pXSprite->data2 = bitReader.readSigned(16);
+            pXSprite->data3 = bitReader.readSigned(16);
+            pXSprite->goalAng = bitReader.readUnsigned(11);
+            pXSprite->dodgeDir = bitReader.readSigned(2);
+            pXSprite->locked = bitReader.readUnsigned(1);
+            pXSprite->medium = bitReader.readUnsigned(2);
+            pXSprite->respawn = bitReader.readUnsigned(2);
+            pXSprite->data4 = bitReader.readUnsigned(16);
+            pXSprite->at1a_2 = bitReader.readUnsigned(6);
+            pXSprite->lockMsg = bitReader.readUnsigned(8);
+            pXSprite->health = bitReader.readUnsigned(12);
+            pXSprite->dudeDeaf = bitReader.readUnsigned(1);
+            pXSprite->dudeAmbush = bitReader.readUnsigned(1);
+            pXSprite->dudeGuard = bitReader.readUnsigned(1);
+            pXSprite->dudeFlag4 = bitReader.readUnsigned(1);
+            pXSprite->target = bitReader.readSigned(16);
+            pXSprite->targetX = bitReader.readSigned(32);
+            pXSprite->targetY = bitReader.readSigned(32);
+            pXSprite->targetZ = bitReader.readSigned(32);
+            pXSprite->burnTime = bitReader.readUnsigned(16);
+            pXSprite->burnSource = bitReader.readSigned(16);
+            pXSprite->height = bitReader.readUnsigned(16);
+            pXSprite->stateTimer = bitReader.readUnsigned(16);
+            pXSprite->aiState = NULL;
+            bitReader.skipBits(32);
+            xsprite[sprite[i].extra].reference = i;
+            xsprite[sprite[i].extra].busy = xsprite[sprite[i].extra].state << 16;
+            if (!byte_1A76C8)
+            {
+                xsprite[sprite[i].extra].lT |= xsprite[sprite[i].extra].lB;
+            }
+        }
+        if ((sprite[i].cstat & 0x30) == 0x30)
+        {
+            sprite[i].cstat &= ~0x30;
+        }
+    }
+    unsigned int nCRC;
+    IOBuffer1.Read(&nCRC, 4);
+#if B_BIG_ENDIAN == 1
+    nCRC = B_LITTLE32(nCRC);
+#endif
+    if (Bcrc32(pData, nSize-4, 0) != nCRC)
+    {
+        ThrowError("Map File does not match CRC");
+    }
+    *pCRC = nCRC;
+    gSysRes.Unlock(pNode);
+    PropagateMarkerReferences();
+    if (byte_1A76C8)
+    {
+        if (gSongId == 0x7474614d || gSongId == 0x4d617474)
+        {
+            byte_1A76C6 = 1;
+        }
+        else if (!gSongId)
+        {
+            byte_1A76C6 = 0;
+        }
+        else
+        {
+            ThrowError("Corrupted Map file");
+        }
+    }
+    else if (gSongId != 0)
+    {
+        ThrowError("Corrupted Shareware Map file");
+    }
+
+#ifdef POLYMER
+    if (videoGetRenderMode() == REND_POLYMER)
+        polymer_loadboard();
+#endif
+
+    if ((header.version & 0xff00) == 0x600)
+    {
+        switch (header.version&0xff)
+        {
+        case 0:
+            for (int i = 0; i < numsectors; i++)
+            {
+                sectortype *pSector = &sector[i];
+                if (pSector->extra > 0)
+                {
+                    XSECTOR *pXSector = &xsector[pSector->extra];
+                    pXSector->busyTimeB = pXSector->busyTimeA;
+                    if (pXSector->busyTimeA > 0)
+                    {
+                        if (!pXSector->atd_4)
+                        {
+                            pXSector->atf_6 = 1;
+                        }
+                        else
+                        {
+                            pXSector->waitTimeB = pXSector->busyTimeA;
+                            pXSector->waitTimeA = 0;
+                            pXSector->atf_7 = 1;
+                        }
+                    }
+                }
+            }
+            fallthrough__;
+        case 1:
+            for (int i = 0; i < numsectors; i++)
+            {
+                sectortype *pSector = &sector[i];
+                if (pSector->extra > 0)
+                {
+                    XSECTOR *pXSector = &xsector[pSector->extra];
+                    pXSector->freq >>= 1;
+                }
+            }
+            fallthrough__;
+        case 2:
+            for (int i = 0; i < kMaxSprites; i++)
+            {
+            }
+            break;
+            
+        }
+    }
+}
diff --git a/source/blood/src/db.h b/source/blood/src/db.h
new file mode 100644
index 000000000..b08c51ed0
--- /dev/null
+++ b/source/blood/src/db.h
@@ -0,0 +1,299 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#define kMaxXSprites 2048
+#define kMaxXWalls 512
+#define kMaxXSectors 512
+
+#pragma pack(push, 1)
+
+struct AISTATE;
+
+struct XSPRITE {
+    //int at0;
+    unsigned int atb_2 : 2; // unused	//
+    unsigned int atb_6 : 1; // unused	// let's use these to add more data 
+    unsigned int ate_5 : 2; // unused	// fields in the future? must be signed also
+    unsigned int at1a_2 : 6; // unused	//
+
+    signed   int reference : 14; // at0_0
+    unsigned int state : 1;  // State 0
+    unsigned int busy : 17;
+    unsigned int txID : 10; // TX ID
+    unsigned int rxID : 10; // RX ID
+    unsigned int command : 8; // Cmd
+    unsigned int triggerOn : 1; // going ON
+    unsigned int triggerOff : 1; // going OFF
+    unsigned int busyTime : 12; // busyTime
+    unsigned int waitTime : 12; // waitTime
+    unsigned int restState : 1; // restState
+    unsigned int Interrutable : 1; // Interruptable
+
+    unsigned int respawnPending : 2; // respawnPending
+
+    signed int dropMsg : 10; // Drop Item
+    unsigned int Decoupled : 1; // Decoupled
+    unsigned int triggerOnce : 1; // 1-shot
+    unsigned int isTriggered : 1; // works in case if triggerOnce selected
+
+    unsigned int key : 3; // Key
+    unsigned int wave : 2; // Wave
+    unsigned int Push: 1; // Push
+    unsigned int Vector : 1; // Vector
+    unsigned int Impact : 1; // Impact
+    unsigned int Pickup : 1; // Pickup
+    unsigned int Touch : 1; // Touch
+    unsigned int Sight : 1; // Sight
+    unsigned int Proximity : 1; // Proximity
+    unsigned int lSkill : 5; // Launch 12345
+    unsigned int lS : 1; // Single
+    unsigned int lB : 1; // Bloodbath
+    unsigned int lT : 1; // Launch Team
+    unsigned int lC : 1; // Coop
+    unsigned int DudeLockout : 1; // DudeLockout
+    signed   int data1 : 16; // Data 1
+    signed   int data2 : 16; // Data 2
+    signed   int data3 : 16; // Data 3
+    unsigned int data4 : 16; // Data 4
+    unsigned int locked : 1; // Locked
+    unsigned int medium : 2; // medium
+    unsigned int respawn : 2; // Respawn option
+    unsigned int lockMsg : 8; // Lock msg
+    unsigned int health : 20; // 1c_0
+    unsigned int dudeDeaf : 1; // dudeDeaf
+    unsigned int dudeAmbush : 1; // dudeAmbush
+    unsigned int dudeGuard : 1; // dudeGuard
+    unsigned int dudeFlag4 : 1; // DF reserved
+    signed   int target : 16; // target sprite
+    signed   int targetX : 32; // target x
+    signed   int targetY : 32; // target y
+    signed   int targetZ : 32; // target z
+    unsigned int goalAng : 11; // Dude goal ang
+    signed   int dodgeDir : 2; // Dude dodge direction
+    unsigned int burnTime : 16;
+    signed   int burnSource : 16;
+    unsigned int height : 16;
+    unsigned int stateTimer : 16; // ai timer
+    AISTATE *aiState; // ai
+    signed int txIndex : 10; // used by kGDXSequentialTX to keep current TX ID index
+    signed int cumulDamage : 16; // for dudes
+    signed int scale; // used for scaling SEQ size on sprites
+};
+
+struct XSECTOR {
+    signed int reference : 14;
+    unsigned int state : 1; // State 0
+    unsigned int busy : 17;
+    unsigned int data : 16; // Data
+    unsigned int txID : 10; // TX ID
+    unsigned int rxID : 10; // RX ID
+    unsigned int at7_2 : 3; // OFF->ON wave
+    unsigned int at7_5 : 3; // ON->OFF wave
+
+    unsigned int command : 8; // Cmd 0
+    unsigned int triggerOn : 1; // Send at ON
+    unsigned int triggerOff : 1; // Send at OFF
+    unsigned int busyTimeA : 12; // OFF->ON busyTime
+    unsigned int waitTimeA : 12; // OFF->ON waitTime
+    unsigned int atd_4 : 1;
+    unsigned int interruptable : 1; // Interruptable
+
+    unsigned int atf_6 : 1; // OFF->ON wait
+    unsigned int atf_7 : 1; // ON->OFF wait
+    signed int amplitude : 8; // Lighting amplitude
+    unsigned int freq : 8; // Lighting freq
+    unsigned int phase : 8; // Lighting phase
+    unsigned int wave : 4; // Lighting wave
+    unsigned int shadeAlways : 1; // Lighting shadeAlways
+    unsigned int shadeFloor : 1; // Lighting floor
+    unsigned int shadeCeiling : 1; // Lighting ceiling
+    unsigned int shadeWalls : 1; // Lighting walls
+    signed int shade : 8; // Lighting value
+    unsigned int panAlways : 1; // Pan always
+    unsigned int panFloor : 1; // Pan floor
+    unsigned int panCeiling : 1; // Pan ceiling
+    unsigned int Drag : 1; // Pan drag
+    unsigned int panVel : 8; // Motion speed
+    unsigned int panAngle : 11; // Motion angle
+    unsigned int Underwater : 1; // Underwater
+    unsigned int Depth : 3; // Depth
+    unsigned int at16_3 : 1;
+    unsigned int decoupled : 1; // Decoupled
+    unsigned int triggerOnce : 1; // 1-shot
+    unsigned int at16_6 : 1;
+    unsigned int Key : 3; // Key
+    unsigned int Push : 1; // Push
+    unsigned int Vector : 1; // Vector
+    unsigned int Reserved : 1; // Reserved
+    unsigned int Enter : 1; // Enter
+    unsigned int Exit : 1; // Exit
+    unsigned int Wallpush : 1; // WallPush
+    unsigned int color : 1; // Color Lights
+    unsigned int at18_1 : 1;
+    unsigned int busyTimeB : 12; // ON->OFF busyTime
+    unsigned int waitTimeB : 12; // ON->OFF waitTime
+    unsigned int at1b_2 : 1;
+    unsigned int at1b_3 : 1;
+    unsigned int ceilpal : 4; // Ceil pal2
+    signed int at1c_0 : 32;
+    signed int at20_0 : 32;
+    signed int at24_0 : 32;
+    signed int at28_0 : 32;
+    unsigned int at2c_0 : 16;
+    unsigned int at2e_0 : 16;
+    unsigned int Crush : 1; // Crush
+    unsigned int at30_1 : 8; // Ceiling x panning frac
+    unsigned int at31_1 : 8; // Ceiling y panning frac
+    unsigned int at32_1 : 8; // Floor x panning frac
+    unsigned int damageType : 3; // DamageType
+    unsigned int floorpal : 4; // Floor pal2
+    unsigned int at34_0 : 8; // Floor y panning frac
+    unsigned int locked : 1; // Locked
+    unsigned int windVel; // Wind vel (by NoOne: changed from 10 bit to use higher velocity values)
+    unsigned int windAng : 11; // Wind ang
+    unsigned int windAlways : 1; // Wind always
+    unsigned int at37_7 : 1;
+    unsigned int bobTheta : 11; // Motion Theta
+    unsigned int bobZRange : 5; // Motion Z range
+    signed int bobSpeed : 12; // Motion speed
+    unsigned int bobAlways : 1; // Motion always
+    unsigned int bobFloor : 1; // Motion bob floor
+    unsigned int bobCeiling : 1; // Motion bob ceiling
+    unsigned int bobRotate : 1; // Motion rotate
+}; // 60(0x3c) bytes
+
+struct XWALL {
+    signed int reference : 14;
+    unsigned int state : 1; // State
+    unsigned int busy : 17;
+    signed int data : 16; // Data
+    unsigned int txID : 10; // TX ID
+    unsigned int at7_2 : 6; // unused
+    unsigned int rxID : 10; // RX ID
+    unsigned int command : 8; // Cmd
+    unsigned int triggerOn : 1; // going ON
+    unsigned int triggerOff : 1; // going OFF
+    unsigned int busyTime : 12; // busyTime
+    unsigned int waitTime : 12; // waitTime
+    unsigned int restState : 1; // restState
+    unsigned int interruptable : 1; // Interruptable
+    unsigned int panAlways : 1; // panAlways
+    signed   int panXVel : 8; // panX
+    signed   int panYVel : 8; // panY
+    unsigned int decoupled : 1; // Decoupled
+    unsigned int triggerOnce : 1; // 1-shot
+    unsigned int isTriggered : 1;
+    unsigned int key : 3; // Key 
+    unsigned int triggerPush : 1; // Push
+    unsigned int triggerVector : 1; // Vector
+    unsigned int triggerReserved : 1; // Reserved
+    unsigned int at11_0 : 2; // unused
+    unsigned int xpanFrac : 8; // x panning frac
+    unsigned int ypanFrac : 8; // y panning frac
+    unsigned int locked : 1; // Locked
+    unsigned int dudeLockout : 1; // DudeLockout
+    unsigned int at13_4 : 4; // unused;
+    unsigned int at14_0 : 32; // unused
+}; // 24(0x18) bytes
+
+struct MAPSIGNATURE {
+    char signature[4];
+    short version;
+};
+
+struct MAPHEADER  {
+    int at0; // x
+    int at4; // y
+    int at8; // z
+    short atc; // ang
+    short ate; // sect
+    short at10; // pskybits
+    int at12; // visibility
+    int at16; // song id, Matt
+    char at1a; // parallaxtype
+    int at1b; // map revision
+    short at1f; // numsectors
+    short at21; // numwalls
+    short at23; // numsprites
+};
+
+struct MAPHEADER2 {
+    char at0[64];
+    int at40; // xsprite size
+    int at44; // xwall size
+    int at48; // xsector size
+    char pad[52];
+};
+
+#pragma pack(pop)
+
+extern unsigned short gStatCount[kMaxStatus + 1];;
+
+extern bool byte_1A76C6, byte_1A76C7, byte_1A76C8;
+extern MAPHEADER2 byte_19AE44;
+
+extern XSPRITE xsprite[kMaxXSprites];
+extern XSECTOR xsector[kMaxXSectors];
+extern XWALL xwall[kMaxXWalls];
+
+extern int xvel[kMaxSprites], yvel[kMaxSprites], zvel[kMaxSprites];
+
+extern int gVisibility;
+extern int gMapRev, gSongId, gSkyCount;
+extern const char *gItemText[];
+extern const char *gAmmoText[];
+extern const char *gWeaponText[];
+
+extern unsigned short nextXSprite[kMaxXSprites];
+extern unsigned short nextXWall[kMaxXWalls];
+extern unsigned short nextXSector[kMaxXSectors];
+
+void InsertSpriteSect(int nSprite, int nSector);
+void RemoveSpriteSect(int nSprite);
+void InsertSpriteStat(int nSprite, int nStat);
+void RemoveSpriteStat(int nSprite);
+void qinitspritelists(void);
+int InsertSprite(int nSector, int nStat);
+int qinsertsprite(short nSector, short nStat);
+int DeleteSprite(int nSprite);
+int qdeletesprite(short nSprite);
+int ChangeSpriteSect(int nSprite, int nSector);
+int qchangespritesect(short nSprite, short nSector);
+int ChangeSpriteStat(int nSprite, int nStatus);
+int qchangespritestat(short nSprite, short nStatus);
+void InitFreeList(unsigned short *pList, int nCount);
+void InsertFree(unsigned short *pList, int nIndex);
+unsigned short dbInsertXSprite(int nSprite);
+void dbDeleteXSprite(int nXSprite);
+unsigned short dbInsertXWall(int nWall);
+void dbDeleteXWall(int nXWall);
+unsigned short dbInsertXSector(int nSector);
+void dbDeleteXSector(int nXSector);
+void dbXSpriteClean(void);
+void dbXWallClean(void);
+void dbXSectorClean(void);
+void dbInit(void);
+void PropagateMarkerReferences(void);
+unsigned int dbReadMapCRC(const char *pPath);
+void dbLoadMap(const char *pPath, int *pX, int *pY, int *pZ, short *pAngle, short *pSector, unsigned int *pCRC);
diff --git a/source/blood/src/demo.cpp b/source/blood/src/demo.cpp
new file mode 100644
index 000000000..44d0525e4
--- /dev/null
+++ b/source/blood/src/demo.cpp
@@ -0,0 +1,625 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <string.h>
+#include "common.h"
+#include "common_game.h"
+#include "keyboard.h"
+#include "control.h"
+#include "osd.h"
+#include "mmulti.h"
+
+#include "blood.h"
+#include "controls.h"
+#include "demo.h"
+#include "fire.h"
+#include "gamemenu.h"
+#include "globals.h"
+#include "levels.h"
+#include "menu.h"
+#include "messages.h"
+#include "misc.h"
+#include "music.h"
+#include "network.h"
+#include "player.h"
+#include "screen.h"
+#include "view.h"
+
+int nBuild = 0;
+
+void ReadGameOptionsLegacy(GAMEOPTIONS &gameOptions, GAMEOPTIONSLEGACY &gameOptionsLegacy)
+{
+    gameOptions.nGameType = gameOptionsLegacy.nGameType;
+    gameOptions.nDifficulty = gameOptionsLegacy.nDifficulty;
+    gameOptions.nEpisode = gameOptionsLegacy.nEpisode;
+    gameOptions.nLevel = gameOptionsLegacy.nLevel;
+    strcpy(gameOptions.zLevelName, gameOptionsLegacy.zLevelName);
+    strcpy(gameOptions.zLevelSong, gameOptionsLegacy.zLevelSong);
+    gameOptions.nTrackNumber = gameOptionsLegacy.nTrackNumber;
+    strcpy(gameOptions.szSaveGameName, gameOptionsLegacy.szSaveGameName);
+    strcpy(gameOptions.szUserGameName, gameOptionsLegacy.szUserGameName);
+    gameOptions.nSaveGameSlot = gameOptionsLegacy.nSaveGameSlot;
+    gameOptions.picEntry = gameOptionsLegacy.picEntry;
+    gameOptions.uMapCRC = gameOptionsLegacy.uMapCRC;
+    gameOptions.nMonsterSettings = gameOptionsLegacy.nMonsterSettings;
+    gameOptions.uGameFlags = gameOptionsLegacy.uGameFlags;
+    gameOptions.uNetGameFlags = gameOptionsLegacy.uNetGameFlags;
+    gameOptions.nWeaponSettings = gameOptionsLegacy.nWeaponSettings;
+    gameOptions.nItemSettings = gameOptionsLegacy.nItemSettings;
+    gameOptions.nRespawnSettings = gameOptionsLegacy.nRespawnSettings;
+    gameOptions.nTeamSettings = gameOptionsLegacy.nTeamSettings;
+    gameOptions.nMonsterRespawnTime = gameOptionsLegacy.nMonsterRespawnTime;
+    gameOptions.nWeaponRespawnTime = gameOptionsLegacy.nWeaponRespawnTime;
+    gameOptions.nItemRespawnTime = gameOptionsLegacy.nItemRespawnTime;
+    gameOptions.nSpecialRespawnTime = gameOptionsLegacy.nSpecialRespawnTime;
+}
+
+CDemo gDemo;
+
+CDemo::CDemo()
+{
+    nBuild = 4;
+    at0 = 0;
+    at1 = 0;
+    at3 = 0;
+    hPFile = -1;
+    hRFile = NULL;
+    atb = 0;
+    pFirstDemo = NULL;
+    pCurrentDemo = NULL;
+    at59ef = 0;
+    at2 = 0;
+    memset(&atf, 0, sizeof(atf));
+    m_bLegacy = false;
+}
+
+CDemo::~CDemo()
+{
+    at0 = 0;
+    at1 = 0;
+    at3 = 0;
+    atb = 0;
+    memset(&atf, 0, sizeof(atf));
+    if (hPFile >= 0)
+    {
+        kclose(hPFile);
+        hPFile = -1;
+    }
+    if (hRFile != NULL)
+    {
+        fclose(hRFile);
+        hRFile = NULL;
+    }
+    auto pNextDemo = pFirstDemo;
+    for (auto pDemo = pFirstDemo; pDemo != NULL; pDemo = pNextDemo)
+    {
+        pNextDemo = pDemo->pNext;
+        delete pDemo;
+    }
+    pFirstDemo = NULL;
+    pCurrentDemo = NULL;
+    at59ef = 0;
+    m_bLegacy = false;
+}
+
+bool CDemo::Create(const char *pzFile)
+{
+    char buffer[BMAX_PATH];
+    char vc = 0;
+    if (at0 || at1)
+        ThrowError("CDemo::Create called during demo record/playback process.");
+    if (!pzFile)
+    {
+        for (int i = 0; i < 8 && !vc; i++)
+        {
+            G_ModDirSnprintf(buffer, BMAX_PATH, "%s0%02d.dem", BloodIniPre, i);
+            if (access(buffer, F_OK) != -1)
+                vc = 1;
+        }
+        if (vc == 1)
+        {
+            hRFile = fopen(buffer, "wb");
+            if (hRFile == NULL)
+                return false;
+        }
+    }
+    else
+    {
+        G_ModDirSnprintfLite(buffer, BMAX_PATH, pzFile);
+        hRFile = fopen(buffer, "wb");
+        if (hRFile == NULL)
+            return false;
+    }
+    at0 = 1;
+    atb = 0;
+    return true;
+}
+
+void CDemo::Write(GINPUT *pPlayerInputs)
+{
+    dassert(pPlayerInputs != NULL);
+    if (!at0)
+        return;
+    if (atb == 0)
+    {
+        atf.signature = 0x1a4d4445; // '\x1aMDE';
+        atf.nVersion = BYTEVERSION;
+        atf.nBuild = nBuild;
+        atf.nInputCount = 0;
+        atf.nNetPlayers = gNetPlayers;
+        atf.nMyConnectIndex = myconnectindex;
+        atf.nConnectHead = connecthead;
+        memcpy(atf.connectPoints, connectpoint2, sizeof(atf.connectPoints));
+        memcpy(&m_gameOptions, &gGameOptions, sizeof(gGameOptions));
+        fwrite(&atf, sizeof(DEMOHEADER), 1, hRFile);
+        fwrite(&m_gameOptions, sizeof(GAMEOPTIONS), 1, hRFile);
+    }
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+    {
+        memcpy(&at1aa[atb&1023], &pPlayerInputs[p], sizeof(GINPUT));
+        atb++;
+        if((atb&(kInputBufferSize-1))==0)
+            FlushInput(kInputBufferSize);
+    }
+}
+
+void CDemo::Close(void)
+{
+    if (at0)
+    {
+        if (atb&(kInputBufferSize-1))
+            FlushInput(atb&(kInputBufferSize-1));
+        atf.nInputCount = atb;
+        fseek(hRFile, 0, SEEK_SET);
+        fwrite(&atf, sizeof(DEMOHEADER), 1, hRFile);
+        fwrite(&m_gameOptions, sizeof(GAMEOPTIONS), 1, hRFile);
+    }
+    if (hPFile >= 0)
+    {
+        kclose(hPFile);
+        hPFile = -1;
+    }
+    if (hRFile != NULL)
+    {
+        fclose(hRFile);
+        hRFile = NULL;
+    }
+    at0 = 0;
+    at1 = 0;
+}
+
+bool CDemo::SetupPlayback(const char *pzFile)
+{
+    at0 = 0;
+    at1 = 0;
+    if (pzFile)
+    {
+        hPFile = kopen4loadfrommod(pzFile, 0);
+        if (hPFile == -1)
+            return false;
+    }
+    else
+    {
+        if (!pCurrentDemo)
+            return false;
+        hPFile = kopen4loadfrommod(pCurrentDemo->zName, 0);
+        if (hPFile == -1)
+            return false;
+    }
+    kread(hPFile, &atf, sizeof(DEMOHEADER));
+#if B_BIG_ENDIAN == 1
+    atf.signature = B_LITTLE32(atf.signature);
+    atf.nVersion = B_LITTLE16(atf.nVersion);
+    atf.nBuild = B_LITTLE32(atf.nBuild);
+    atf.nInputCount = B_LITTLE32(atf.nInputCount);
+    atf.nNetPlayers = B_LITTLE32(atf.nNetPlayers);
+    atf.nMyConnectIndex = B_LITTLE16(atf.nMyConnectIndex);
+    atf.nConnectHead = B_LITTLE16(atf.nConnectHead);
+    atf.nMyConnectIndex = B_LITTLE16(atf.nMyConnectIndex);
+    for (int i = 0; i < 8; i++)
+        atf.connectPoints[i] = B_LITTLE16(atf.connectPoints[i]);
+#endif
+    // if (aimHeight.signature != '\x1aMED' && aimHeight.signature != '\x1aMDE')
+    if (atf.signature != 0x1a4d4544 && atf.signature != 0x1a4d4445)
+        return 0;
+    m_bLegacy = atf.signature == 0x1a4d4544;
+    if (m_bLegacy)
+    {
+        GAMEOPTIONSLEGACY gameOptions;
+        if (BloodVersion != atf.nVersion)
+            return 0;
+        kread(hPFile, &gameOptions, sizeof(GAMEOPTIONSLEGACY));
+        ReadGameOptionsLegacy(m_gameOptions, gameOptions);
+    }
+    else
+    {
+        if (BYTEVERSION != atf.nVersion)
+            return 0;
+        kread(hPFile, &m_gameOptions, sizeof(GAMEOPTIONS));
+    }
+#if B_BIG_ENDIAN == 1
+    m_gameOptions.nEpisode = B_LITTLE32(m_gameOptions.nEpisode);
+    m_gameOptions.nLevel = B_LITTLE32(m_gameOptions.nLevel);
+    m_gameOptions.nTrackNumber = B_LITTLE32(m_gameOptions.nTrackNumber);
+    m_gameOptions.nSaveGameSlot = B_LITTLE16(m_gameOptions.nSaveGameSlot);
+    m_gameOptions.picEntry = B_LITTLE32(m_gameOptions.picEntry);
+    m_gameOptions.uMapCRC = B_LITTLE32(m_gameOptions.uMapCRC);
+    m_gameOptions.uGameFlags = B_LITTLE32(m_gameOptions.uGameFlags);
+    m_gameOptions.uNetGameFlags = B_LITTLE32(m_gameOptions.uNetGameFlags);
+    m_gameOptions.nMonsterRespawnTime = B_LITTLE32(m_gameOptions.nMonsterRespawnTime);
+    m_gameOptions.nWeaponRespawnTime = B_LITTLE32(m_gameOptions.nWeaponRespawnTime);
+    m_gameOptions.nItemRespawnTime = B_LITTLE32(m_gameOptions.nItemRespawnTime);
+    m_gameOptions.nSpecialRespawnTime = B_LITTLE32(m_gameOptions.nSpecialRespawnTime);
+#endif
+    at0 = 0;
+    at1 = 1;
+    return 1;
+}
+
+void CDemo::ProcessKeys(void)
+{
+    switch (gInputMode)
+    {
+    case INPUT_MODE_1:
+        gGameMenuMgr.Process();
+        break;
+    case INPUT_MODE_2:
+        gPlayerMsg.ProcessKeys();
+        break;
+    case INPUT_MODE_0:
+    {
+        char nKey;
+        while ((nKey = keyGetScan()) != 0)
+        {
+	        char UNUSED(alt) = keystatus[0x38] | keystatus[0xb8];
+	        char UNUSED(ctrl) = keystatus[0x1d] | keystatus[0x9d];
+            switch (nKey)
+            {
+            case 1:
+                if (!CGameMenuMgr::m_bActive)
+                {
+                    gGameMenuMgr.Push(&menuMain, -1);
+                    at2 = 1;
+                }
+                break;
+            case 0x58:
+                gViewIndex = connectpoint2[gViewIndex];
+                if (gViewIndex == -1)
+                    gViewIndex = connecthead;
+                gView = &gPlayer[gViewIndex];
+                break;
+            }
+        }
+        break;
+    default:
+        gInputMode = INPUT_MODE_0;
+        break;
+    }
+    }
+}
+
+void CDemo::Playback(void)
+{
+    CONTROL_BindsEnabled = false;
+    ready2send = 0;
+    int v4 = 0;
+    if (!CGameMenuMgr::m_bActive)
+    {
+        gGameMenuMgr.Push(&menuMain, -1);
+        at2 = 1;
+    }
+    gNetFifoClock = gGameClock;
+    gViewMode = 3;
+_DEMOPLAYBACK:
+    while (at1 && !gQuitGame)
+    {
+        if (handleevents() && quitevent)
+        {
+            KB_KeyDown[sc_Escape] = 1;
+            quitevent = 0;
+        }
+        MUSIC_Update();
+        while (gGameClock >= gNetFifoClock && !gQuitGame)
+        {
+            if (!v4)
+            {
+                viewResizeView(gViewSize);
+                viewSetMessage("");
+                gNetPlayers = atf.nNetPlayers;
+                atb = atf.nInputCount;
+                myconnectindex = atf.nMyConnectIndex;
+                connecthead = atf.nConnectHead;
+                for (int i = 0; i < 8; i++)
+                    connectpoint2[i] = atf.connectPoints[i];
+                memset(gNetFifoHead, 0, sizeof(gNetFifoHead));
+                gNetFifoTail = 0;
+                //memcpy(connectpoint2, aimHeight.connectPoints, sizeof(aimHeight.connectPoints));
+                memcpy(&gGameOptions, &m_gameOptions, sizeof(GAMEOPTIONS));
+                gSkill = gGameOptions.nDifficulty;
+                for (int i = 0; i < 8; i++)
+                    playerInit(i, 0);
+                StartLevel(&gGameOptions);
+                for (int i = 0; i < 8; i++)
+                {
+                    gProfile[i].nAutoAim = 1;
+                    gProfile[i].nWeaponSwitch = 1;
+                }
+            }
+            ready2send = 0;
+            OSD_DispatchQueued();
+            if (!gDemo.at1)
+                break;
+            ProcessKeys();
+            for (int p = connecthead; p >= 0; p = connectpoint2[p])
+            {
+                if ((v4&1023) == 0)
+                {
+                    unsigned int nSize = atb-v4;
+                    if (nSize > kInputBufferSize)
+                        nSize = kInputBufferSize;
+                    ReadInput(nSize);
+                }
+                memcpy(&gFifoInput[gNetFifoHead[p]&255], &at1aa[v4&1023], sizeof(GINPUT));
+                gNetFifoHead[p]++;
+                v4++;
+                if (v4 >= atf.nInputCount)
+                {
+                    ready2send = 0;
+                    if (at59ef != 1)
+                    {
+                        v4 = 0;
+                        Close();
+                        NextDemo();
+                        gNetFifoClock = gGameClock;
+                        goto _DEMOPLAYBACK;
+                    }
+                    else
+                    {
+                        int const nOffset = sizeof(DEMOHEADER)+(m_bLegacy ? sizeof(GAMEOPTIONSLEGACY) : sizeof(GAMEOPTIONS));
+                        klseek(hPFile, nOffset, SEEK_SET);
+                        v4 = 0;
+                    }
+                }
+            }
+            gNetFifoClock += 4;
+            if (!gQuitGame)
+                ProcessFrame();
+            ready2send = 0;
+        }
+        if (viewFPSLimit())
+        {
+            viewDrawScreen();
+            if (gInputMode == INPUT_MODE_1 && CGameMenuMgr::m_bActive)
+                gGameMenuMgr.Draw();
+        }
+        if (TestBitString(gotpic, 2342))
+        {
+            FireProcess();
+            ClearBitString(gotpic, 2342);
+        }
+    }
+    Close();
+}
+
+void CDemo::StopPlayback(void)
+{
+    at1 = 0;
+}
+
+void CDemo::LoadDemoInfo(void)
+{
+    auto pDemo = &pFirstDemo;
+    const int opsm = pathsearchmode;
+    at59ef = 0;
+    pathsearchmode = 0;
+    char zFN[BMAX_PATH];
+    Bsnprintf(zFN, BMAX_PATH, "%s*.dem", BloodIniPre);
+    auto pList = klistpath("/", zFN, CACHE1D_FIND_FILE);
+    auto pIterator = pList;
+    while (pIterator != NULL)
+    {
+        int hFile = kopen4loadfrommod(pIterator->name, 0);
+        if (hFile == -1)
+            ThrowError("Error loading demo file header.");
+        kread(hFile, &atf, sizeof(atf));
+        kclose(hFile);
+#if B_BIG_ENDIAN == 1
+        atf.signature = B_LITTLE32(atf.signature);
+        atf.nVersion = B_LITTLE16(atf.nVersion);
+#endif
+        if ((atf.signature == 0x1a4d4544 /* '\x1aMED' */&& atf.nVersion == BloodVersion)
+            || (atf.signature == 0x1a4d4445 /* '\x1aMDE' */ && atf.nVersion == BYTEVERSION))
+        {
+            *pDemo = new DEMOCHAIN;
+            (*pDemo)->pNext = NULL;
+            Bstrncpy((*pDemo)->zName, pIterator->name, BMAX_PATH);
+            at59ef++;
+            pDemo = &(*pDemo)->pNext;
+        }
+        pIterator = pIterator->next;
+    }
+    klistfree(pList);
+    pathsearchmode = opsm;
+    pCurrentDemo = pFirstDemo;
+}
+
+void CDemo::NextDemo(void)
+{
+    pCurrentDemo = pCurrentDemo->pNext ? pCurrentDemo->pNext : pFirstDemo;
+    SetupPlayback(NULL);
+}
+
+const int nInputSize = 17;
+const int nInputSizeLegacy = 22;
+
+void CDemo::FlushInput(int nCount)
+{
+    char pBuffer[nInputSize*kInputBufferSize];
+    BitWriter bitWriter(pBuffer, sizeof(pBuffer));
+    for (int i = 0; i < nCount; i++)
+    {
+        GINPUT *pInput = &at1aa[i];
+        bitWriter.writeBit(pInput->syncFlags.buttonChange);
+        bitWriter.writeBit(pInput->syncFlags.keyChange);
+        bitWriter.writeBit(pInput->syncFlags.useChange);
+        bitWriter.writeBit(pInput->syncFlags.weaponChange);
+        bitWriter.writeBit(pInput->syncFlags.mlookChange);
+        bitWriter.writeBit(pInput->syncFlags.run);
+        bitWriter.write(pInput->forward, 16);
+        bitWriter.write(pInput->q16turn, 32);
+        bitWriter.write(pInput->strafe, 16);
+        bitWriter.writeBit(pInput->buttonFlags.jump);
+        bitWriter.writeBit(pInput->buttonFlags.crouch);
+        bitWriter.writeBit(pInput->buttonFlags.shoot);
+        bitWriter.writeBit(pInput->buttonFlags.shoot2);
+        bitWriter.writeBit(pInput->buttonFlags.lookUp);
+        bitWriter.writeBit(pInput->buttonFlags.lookDown);
+        bitWriter.writeBit(pInput->keyFlags.action);
+        bitWriter.writeBit(pInput->keyFlags.jab);
+        bitWriter.writeBit(pInput->keyFlags.prevItem);
+        bitWriter.writeBit(pInput->keyFlags.nextItem);
+        bitWriter.writeBit(pInput->keyFlags.useItem);
+        bitWriter.writeBit(pInput->keyFlags.prevWeapon);
+        bitWriter.writeBit(pInput->keyFlags.nextWeapon);
+        bitWriter.writeBit(pInput->keyFlags.holsterWeapon);
+        bitWriter.writeBit(pInput->keyFlags.lookCenter);
+        bitWriter.writeBit(pInput->keyFlags.lookLeft);
+        bitWriter.writeBit(pInput->keyFlags.lookRight);
+        bitWriter.writeBit(pInput->keyFlags.spin180);
+        bitWriter.writeBit(pInput->keyFlags.pause);
+        bitWriter.writeBit(pInput->keyFlags.quit);
+        bitWriter.writeBit(pInput->keyFlags.restart);
+        bitWriter.writeBit(pInput->useFlags.useBeastVision);
+        bitWriter.writeBit(pInput->useFlags.useCrystalBall);
+        bitWriter.writeBit(pInput->useFlags.useJumpBoots);
+        bitWriter.writeBit(pInput->useFlags.useMedKit);
+        bitWriter.write(pInput->newWeapon, 8);
+        bitWriter.write(pInput->q16mlook, 32);
+        bitWriter.skipBits(1);
+    }
+    fwrite(pBuffer, 1, nInputSize*nCount, hRFile);
+}
+
+void CDemo::ReadInput(int nCount)
+{
+    if (m_bLegacy)
+    {
+        char pBuffer[nInputSizeLegacy*kInputBufferSize];
+        kread(hPFile, pBuffer, nInputSizeLegacy*nCount);
+        BitReader bitReader(pBuffer, sizeof(pBuffer));
+        memset(at1aa, 0, nCount * sizeof(GINPUT));
+        for (int i = 0; i < nCount; i++)
+        {
+            GINPUT *pInput = &at1aa[i];
+            pInput->syncFlags.buttonChange = bitReader.readBit();
+            pInput->syncFlags.keyChange = bitReader.readBit();
+            pInput->syncFlags.useChange = bitReader.readBit();
+            pInput->syncFlags.weaponChange = bitReader.readBit();
+            pInput->syncFlags.mlookChange = bitReader.readBit();
+            pInput->syncFlags.run = bitReader.readBit();
+            bitReader.skipBits(26);
+            pInput->forward = bitReader.readSigned(8) << 8;
+            pInput->q16turn = fix16_from_int(bitReader.readSigned(16) >> 2);
+            pInput->strafe = bitReader.readSigned(8) << 8;
+            pInput->buttonFlags.jump = bitReader.readBit();
+            pInput->buttonFlags.crouch = bitReader.readBit();
+            pInput->buttonFlags.shoot = bitReader.readBit();
+            pInput->buttonFlags.shoot2 = bitReader.readBit();
+            pInput->buttonFlags.lookUp = bitReader.readBit();
+            pInput->buttonFlags.lookDown = bitReader.readBit();
+            bitReader.skipBits(26);
+            pInput->keyFlags.action = bitReader.readBit();
+            pInput->keyFlags.jab = bitReader.readBit();
+            pInput->keyFlags.prevItem = bitReader.readBit();
+            pInput->keyFlags.nextItem = bitReader.readBit();
+            pInput->keyFlags.useItem = bitReader.readBit();
+            pInput->keyFlags.prevWeapon = bitReader.readBit();
+            pInput->keyFlags.nextWeapon = bitReader.readBit();
+            pInput->keyFlags.holsterWeapon = bitReader.readBit();
+            pInput->keyFlags.lookCenter = bitReader.readBit();
+            pInput->keyFlags.lookLeft = bitReader.readBit();
+            pInput->keyFlags.lookRight = bitReader.readBit();
+            pInput->keyFlags.spin180 = bitReader.readBit();
+            pInput->keyFlags.pause = bitReader.readBit();
+            pInput->keyFlags.quit = bitReader.readBit();
+            pInput->keyFlags.restart = bitReader.readBit();
+            bitReader.skipBits(17);
+            pInput->useFlags.useBeastVision = bitReader.readBit();
+            pInput->useFlags.useCrystalBall = bitReader.readBit();
+            pInput->useFlags.useJumpBoots = bitReader.readBit();
+            pInput->useFlags.useMedKit = bitReader.readBit();
+            bitReader.skipBits(28);
+            pInput->newWeapon = bitReader.readUnsigned(8);
+            int mlook = bitReader.readSigned(8);
+            pInput->q16mlook = fix16_from_int(mlook / 4);
+        }
+    }
+    else
+    {
+        char pBuffer[nInputSize*kInputBufferSize];
+        kread(hPFile, pBuffer, nInputSize*nCount);
+        BitReader bitReader(pBuffer, sizeof(pBuffer));
+        memset(at1aa, 0, nCount * sizeof(GINPUT));
+        for (int i = 0; i < nCount; i++)
+        {
+            GINPUT *pInput = &at1aa[i];
+            pInput->syncFlags.buttonChange = bitReader.readBit();
+            pInput->syncFlags.keyChange = bitReader.readBit();
+            pInput->syncFlags.useChange = bitReader.readBit();
+            pInput->syncFlags.weaponChange = bitReader.readBit();
+            pInput->syncFlags.mlookChange = bitReader.readBit();
+            pInput->syncFlags.run = bitReader.readBit();
+            pInput->forward = bitReader.readSigned(16);
+            pInput->q16turn = bitReader.readSigned(32);
+            pInput->strafe = bitReader.readSigned(16);
+            pInput->buttonFlags.jump = bitReader.readBit();
+            pInput->buttonFlags.crouch = bitReader.readBit();
+            pInput->buttonFlags.shoot = bitReader.readBit();
+            pInput->buttonFlags.shoot2 = bitReader.readBit();
+            pInput->buttonFlags.lookUp = bitReader.readBit();
+            pInput->buttonFlags.lookDown = bitReader.readBit();
+            pInput->keyFlags.action = bitReader.readBit();
+            pInput->keyFlags.jab = bitReader.readBit();
+            pInput->keyFlags.prevItem = bitReader.readBit();
+            pInput->keyFlags.nextItem = bitReader.readBit();
+            pInput->keyFlags.useItem = bitReader.readBit();
+            pInput->keyFlags.prevWeapon = bitReader.readBit();
+            pInput->keyFlags.nextWeapon = bitReader.readBit();
+            pInput->keyFlags.holsterWeapon = bitReader.readBit();
+            pInput->keyFlags.lookCenter = bitReader.readBit();
+            pInput->keyFlags.lookLeft = bitReader.readBit();
+            pInput->keyFlags.lookRight = bitReader.readBit();
+            pInput->keyFlags.spin180 = bitReader.readBit();
+            pInput->keyFlags.pause = bitReader.readBit();
+            pInput->keyFlags.quit = bitReader.readBit();
+            pInput->keyFlags.restart = bitReader.readBit();
+            pInput->useFlags.useBeastVision = bitReader.readBit();
+            pInput->useFlags.useCrystalBall = bitReader.readBit();
+            pInput->useFlags.useJumpBoots = bitReader.readBit();
+            pInput->useFlags.useMedKit = bitReader.readBit();
+            pInput->newWeapon = bitReader.readUnsigned(8);
+            pInput->q16mlook = bitReader.readSigned(32);
+            bitReader.skipBits(1);
+        }
+    }
+}
diff --git a/source/blood/src/demo.h b/source/blood/src/demo.h
new file mode 100644
index 000000000..03f0a6729
--- /dev/null
+++ b/source/blood/src/demo.h
@@ -0,0 +1,110 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "controls.h"
+#include "levels.h"
+
+#define kInputBufferSize 1024
+
+#pragma pack(push, 1)
+
+struct GAMEOPTIONSLEGACY {
+    char nGameType;
+    char nDifficulty;
+    int nEpisode;
+    int nLevel;
+    char zLevelName[144];
+    char zLevelSong[144];
+    int nTrackNumber; //at12a;
+    char szSaveGameName[16];
+    char szUserGameName[16];
+    short nSaveGameSlot;
+    int picEntry;
+    unsigned int uMapCRC;
+    char nMonsterSettings;
+    int uGameFlags;
+    int uNetGameFlags;
+    char nWeaponSettings;
+    char nItemSettings;
+    char nRespawnSettings;
+    char nTeamSettings;
+    int nMonsterRespawnTime;
+    int nWeaponRespawnTime;
+    int nItemRespawnTime;
+    int nSpecialRespawnTime;
+};
+
+struct DEMOHEADER
+{
+    int signature;
+    short nVersion;
+    int nBuild;
+    int nInputCount;
+    int nNetPlayers;
+    short nMyConnectIndex;
+    short nConnectHead;
+    short connectPoints[8];
+};
+
+#pragma pack(pop)
+
+struct DEMOCHAIN
+{
+    DEMOCHAIN *pNext;
+    char zName[BMAX_PATH];
+};
+
+class CDemo {
+public:
+    CDemo();
+    ~CDemo();
+    bool Create(const char *);
+    void Write(GINPUT *);
+    void Close(void);
+    bool SetupPlayback(const char *);
+    void ProcessKeys(void);
+    void Playback(void);
+    void StopPlayback(void);
+    void LoadDemoInfo(void);
+    void NextDemo(void);
+    void FlushInput(int nCount);
+    void ReadInput(int nCount);
+    bool at0; // record
+    bool at1; // playback
+    bool m_bLegacy;
+    char at2;
+    int at3;
+    int hPFile;
+    FILE *hRFile;
+    int atb;
+    DEMOHEADER atf;
+    GAMEOPTIONS m_gameOptions;
+    GINPUT at1aa[kInputBufferSize];
+    const char **pzDemoFile;
+    DEMOCHAIN *pFirstDemo;
+    DEMOCHAIN *pCurrentDemo;
+    int at59ef;
+};
+
+extern CDemo gDemo;
diff --git a/source/blood/src/dude.cpp b/source/blood/src/dude.cpp
new file mode 100644
index 000000000..a282da9c1
--- /dev/null
+++ b/source/blood/src/dude.cpp
@@ -0,0 +1,1727 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "common.h"
+#include "blood.h"
+#include "dude.h"
+
+DUDEINFO dudeInfo[kDudeMax-kDudeBase] = 
+{
+    {
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4096, //seqStartId
+        40, // startHp
+        70, // mass
+        1200, // ???
+        48, // clipdist
+        41, // eye height
+        20, // aim height
+        10240, // hear dist
+        51200, // see dist
+        512, // periphery
+        0, // melee distance
+        10, // flee health
+        8, // hinder damage
+        256, // change target chance
+        16, // change target chance to same type
+        32768, // alert chance
+        1, // lockout
+        46603, // front speed
+        34952, // side speed
+        13981, // back speed
+        256, // ang speed
+        15, -1, -1, // gib type
+        256, 256, 96, 256, 256, 256, 192, // start damage
+        0, 0, 0, 0, 0, 0, 0, // real damage
+        0, // ???
+        0 // ???
+    },
+    {
+        11520,
+        40,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        5,
+        256,
+        16,
+        32768,
+        1,
+        34952,
+        34952,
+        13981,
+        256,
+        15, -1, -1,
+        256, 256, 128, 256, 256, 256, 192,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4352,
+        60,
+        70,
+        1200,
+        48,
+        46,
+        20,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        15, -1, -1,
+        256, 256, 112, 256, 256, 256, 160,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4608,
+        80,
+        200,
+        1200,
+        48,
+        128,
+        20,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        1,
+        23301,
+        23301,
+        13981,
+        256,
+        15, -1, -1,
+        256, 256, 32, 128, 256, 64, 128,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4352,
+        60,
+        70,
+        1200,
+        48,
+        46,
+        20,
+        5120,
+        0,
+        341,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        15, -1, -1,
+        256, 256, 112, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4864,
+        110,
+        120,
+        1200,
+        64,
+        13,
+        5,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        25,
+        256,
+        16,
+        32768,
+        1,
+        46603,
+        34952,
+        23301,
+        384,
+        30, -1, -1,
+        0, 128, 48, 208, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        5120,
+        200,
+        200,
+        1200,
+        84,
+        13,
+        5,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        20,
+        256,
+        16,
+        32768,
+        1,
+        46603,
+        34952,
+        23301,
+        256,
+        19, -1, -1,
+        0, 0, 10, 10, 0, 128, 64,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        11008,
+        100,
+        200,
+        1200,
+        64,
+        13,
+        5,
+        2048,
+        5120,
+        512,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        0,
+        -1, -1, -1,
+        0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        11264,
+        100,
+        200,
+        1200,
+        64,
+        13,
+        5,
+        2048,
+        5120,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        0,
+        -1, -1, -1,
+        0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        5376,
+        100,
+        70,
+        1200,
+        64,
+        25,
+        15,
+        10240,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        -1, -1, -1,
+        0, 0, 48, 0, 0, 16, 0,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        5632,
+        70,
+        120,
+        1200,
+        80,
+        6,
+        0,
+        10240,
+        51200,
+        682,
+        0,
+        10,
+        20,
+        256,
+        16,
+        32768,
+        0,
+        116508,
+        81555,
+        69905,
+        384,
+        29, -1, -1,
+        48, 0, 48, 48, 256, 128, 192,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        5888,
+        10,
+        70,
+        1200,
+        32,
+        0,
+        0,
+        5120,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        64, 256, 256, 256, 0, 64, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        6144,
+        10,
+        5,
+        1200,
+        32,
+        -5,
+        -5,
+        5120,
+        51200,
+        682,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        64, 256, 256, 96, 256, 64, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        6400,
+        25,
+        10,
+        1200,
+        32,
+        -5,
+        -5,
+        5120,
+        51200,
+        682,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        64, 128, 256, 96, 256, 64, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        6656,
+        75,
+        20,
+        1200,
+        32,
+        -5,
+        -5,
+        5120,
+        51200,
+        682,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        128, 256, 256, 96, 256, 64, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        6912,
+        100,
+        40,
+        1200,
+        32,
+        -5,
+        -5,
+        5120,
+        51200,
+        682,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        32, 16, 16, 16, 32, 32, 32,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        7168,
+        50,
+        200,
+        1200,
+        64,
+        37,
+        20,
+        5120,
+        51200,
+        682,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        48, 80, 64, 128, 0, 128, 48,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        7424,
+        25,
+        30,
+        1200,
+        32,
+        4,
+        0,
+        5120,
+        51200,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        34952,
+        23301,
+        23301,
+        128,
+        7, -1, -1,
+        256, 256, 256, 256, 0, 256, 192,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        7680,
+        10,
+        5,
+        1200,
+        32,
+        2,
+        0,
+        10240,
+        25600,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        23301,
+        23301,
+        13981,
+        384,
+        7, -1, -1,
+        256, 256, 256, 256, 256, 64, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        7936,
+        10,
+        5,
+        1200,
+        32,
+        3,
+        0,
+        12800,
+        51200,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        256, 256, 256, 256, 256, 128, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        8192,
+        50,
+        65535,
+        1200,
+        64,
+        40,
+        0,
+        2048,
+        11264,
+        1024,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        384,
+        7, -1, -1,
+        160, 160, 128, 160, 0, 0, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        8448,
+        10,
+        65535,
+        1200,
+        32,
+        0,
+        0,
+        2048,
+        5120,
+        1024,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        384,
+        7, -1, -1,
+        256, 256, 256, 80, 0, 0, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        8704,
+        100,
+        65535,
+        1200,
+        64,
+        40,
+        0,
+        2048,
+        15360,
+        1024,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        384,
+        7, -1, -1,
+        96, 0, 128, 64, 256, 64, 160,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        8960,
+        20,
+        65535,
+        1200,
+        32,
+        0,
+        0,
+        2048,
+        5120,
+        1024,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        384,
+        7, -1, -1,
+        128, 0, 128, 128, 0, 0, 128,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        9216,
+        200,
+        65535,
+        1200,
+        64,
+        40,
+        0,
+        2048,
+        51200,
+        1024,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        0,
+        7, -1, -1,
+        128, 256, 128, 256, 128, 128, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        9472,
+        50,
+        65535,
+        1200,
+        32,
+        0,
+        0,
+        2048,
+        51200,
+        1024,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        0,
+        7, -1, -1,
+        256, 256, 128, 256, 128, 128, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        9728,
+        200,
+        1000,
+        1200,
+        64,
+        29,
+        10,
+        40960,
+        102400,
+        682,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        69905,
+        58254,
+        46603,
+        384,
+        7, -1, -1,
+        16, 0, 16, 16, 0, 96, 48,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        9984,
+        100,
+        1000,
+        1200,
+        64,
+        29,
+        10,
+        20480,
+        51200,
+        682,
+        0,
+        10,
+        10,
+        256,
+        0,
+        32768,
+        0,
+        58254,
+        34952,
+        25631,
+        384,
+        7, -1, -1,
+        16, 0, 16, 16, 0, 96, 48,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        10240,
+        32, // 800,
+        1500,
+        1200,
+        128,
+        0,
+        0,
+        25600,
+        51200,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        58254,
+        34952,
+        384,
+        7, -1, -1,
+        3, 1, 4, 4, 0, 4, 3,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4096,
+        25,
+        20,
+        1200,
+        32,
+        0,
+        0,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        15, -1, -1,
+        256, 256, 96, 256, 256, 256, 192,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12032,
+        100,
+        70,
+        1200,
+        48,
+        0,
+        16,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12544,
+        25,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        10240,
+        51200,
+        341,
+        0,
+        100,
+        100,
+        0,
+        0,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        160,
+        7, 5, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4096,
+        30,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        10240,
+        51200,
+        341,
+        0,
+        100,
+        100,
+        0,
+        0,
+        32768,
+        0,
+        46603,
+        34952,
+        13981,
+        160,
+        7, 5, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4352,
+        12,
+        70,
+        1200,
+        48,
+        46,
+        20,
+        10240,
+        51200,
+        341,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        160,
+        7, 5, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4352,
+        25,
+        120,
+        1200,
+        48,
+        44,
+        20,
+        10240,
+        51200,
+        341,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        0,
+        39612,
+        27962,
+        13981,
+        100,
+        7, 5, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4096,
+        100,
+        70,
+        1200,
+        64,
+        38,
+        20,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        64,
+        15, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        4352,
+        60,
+        70,
+        1200,
+        48,
+        46,
+        20,
+        5120,
+        0,
+        341,
+        0,
+        10,
+        15,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        15, -1, -1,
+        256, 256, 112, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12544,
+        50,
+        70,
+        1200,
+        48,
+        46,
+        20,
+        2560,
+        0,
+        341,
+        0,
+        10,
+        8,
+        256,
+        16,
+        32768,
+        1,
+        58254,
+        46603,
+        34952,
+        384,
+        15, -1, -1,
+        288, 288, 288, 288, 288, 288, 288,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        11520,
+        25,
+        70,
+        1200,
+        32,
+        -5,
+        0,
+        2048,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        0,
+        0,
+        0,
+        64,
+        7, 5, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        12800,
+        40,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        8,
+        256,
+        16,
+        32768,
+        1,
+        46603,
+        34952,
+        13981,
+        256,
+        15, -1, -1,
+        256, 256, 96, 160, 256, 256, 12,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        13056,
+        40,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        8,
+        256,
+        16,
+        32768,
+        1,
+        46603,
+        34952,
+        13981,
+        256,
+        15, -1, -1,
+        256, 160, 96, 64, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        13312,
+        40,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        10240,
+        51200,
+        512,
+        0,
+        10,
+        12,
+        256,
+        16,
+        32768,
+        1,
+        46603,
+        34952,
+        13981,
+        256,
+        15, -1, -1,
+        128, 128, 16, 16, 0, 64, 48,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        13568,
+        10,
+        5,
+        1200,
+        32,
+        3,
+        0,
+        12800,
+        51200,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        160, 160, 160, 160, 256, 128, 288,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        10752,
+        120,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        12800,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        116508,
+        81555,
+        69905,
+        384,
+        7, -1, -1,
+        5, 5, 15, 8, 0, 15, 15,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        13568,
+        10,
+        5,
+        1200,
+        32,
+        3,
+        0,
+        12800,
+        51200,
+        512,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        0,
+        58254,
+        46603,
+        34952,
+        384,
+        7, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    {
+        10752,
+        25,
+        70,
+        1200,
+        48,
+        41,
+        20,
+        12800,
+        51200,
+        341,
+        0,
+        10,
+        10,
+        256,
+        16,
+        32768,
+        1,
+        116508,
+        81555,
+        69905,
+        384,
+        7, -1, -1,
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    //254 -  kGDXUniversalCultist
+    {
+        11520,		// start sequence ID
+        85,		// start health
+        75,			// mass
+        120,
+        48,			// clip distance
+        48,			// eye above z
+        20,
+        10240,		// hear distance
+        51200,	// seeing distance
+        kAng120,	// vision periphery
+        //			    0,
+        618,			// melee distance
+        5,			// flee health
+        12,			// hinder damage
+        0x0100,		// change target chance
+        0x0010,		// change target to kin chance
+        0x8000,		// alertChance
+        0,		// lockout
+        46603,		// frontSpeed
+        34952,		// sideSpeed
+        13981,		// backSpeed
+        256,		// angSpeed
+        //			    0,
+        7,	-1, 18,		// nGibType
+        256, 256, 128, 256, 256, 256, 192,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+    //255 -  kGDXGenDudeBurning
+    {
+        4096,		// start sequence ID
+        25,			// start health
+        5,			// mass
+        120,
+        48,			// clip distance
+        41,			// eye above z
+        20,
+        12800,		// hear distance
+        51200,		// seeing distance
+        kAng60,	// vision periphery
+        //						0,
+        0,			// melee distance
+        10,			// flee health
+        10,			// hinder damage
+        0x0100,		// change target chance
+        0x0010,		// change target to kin chance
+        0x8000,		// alertChance
+        true,		// lockout
+        58254,			// frontSpeed
+        46603,			// sideSpeed
+        34952,			// backSpeed
+        384,			// angSpeed
+        //              0,
+        7,	-1, -1,		// nGibType
+        256, 256, 256, 256, 256, 256, 256,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    }
+};
+
+DUDEINFO gPlayerTemplate[4] = 
+{
+    // normal human
+    {
+        0x2f00,
+        100,
+        70,
+        1200,
+        0x30,
+        0,
+        0x10,
+        0x800,
+        0xc800,
+        0x155,
+        0,
+        10,
+        10,
+        0x100,
+        0x10,
+        0x8000,
+        0x1,
+        0,
+        0,
+        0,
+        0x40,
+        15, -1, -1,
+        0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x120,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+
+    // normal beast
+    {
+        0x2900,
+        100,
+        70,
+        1200,
+        0x30,
+        0,
+        0x14,
+        0x800,
+        0xc800,
+        0x155,
+        0,
+        10,
+        10,
+        0x100,
+        0x10,
+        0x8000,
+        0x1,
+        0,
+        0,
+        0,
+        0x40,
+        7, -1, -1,
+        0x100, 0x100, 0x100, 0x100, 0x100, 0x100, 0x120,
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+
+    // shrink human
+    {
+        12032,
+        100,
+        10, // mass
+        1200,
+        16, // clipdist
+        0,
+        0x10,
+        0x800,
+        0xc800,
+        0x155,
+        0,
+        10,
+        10,
+        0x100,
+        0x10,
+        0x8000,
+        0x1,
+        0,
+        0,
+        0,
+        0x40,
+        15, -1, -1, // gib type
+        1024, 1024, 1024, 1024, 256, 1024, 1024, //damage shift
+        0, 0, 0, 0, 0, 0, 0,
+        0,
+        0
+    },
+
+     // grown human
+     {
+         12032,
+         100,
+         1100, // mass
+         1200,
+         100, // clipdist
+         0,
+         0x10,
+         0x800,
+         0xc800,
+         0x155,
+         0,
+         10,
+         10,
+         0x100,
+         0x10,
+         0x8000,
+         0x1,
+         0,
+         0,
+         0,
+         0x40,
+         15, 7, 7, // gib type
+         64, 64, 64, 64, 256, 64, 64, // damage shift
+         0, 0, 0, 0, 0, 0, 0,
+         0,
+         0
+     },
+};
diff --git a/source/blood/src/dude.h b/source/blood/src/dude.h
new file mode 100644
index 000000000..5f317e520
--- /dev/null
+++ b/source/blood/src/dude.h
@@ -0,0 +1,56 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "blood.h"
+// By NoOne: renamed dude struct
+struct DUDEINFO {
+    short seqStartID; // seq
+    short startHealth; // health
+    unsigned short mass; // mass
+    int at6; // unused?
+    unsigned char clipdist; // clipdist
+    int eyeHeight;
+    int aimHeight; // used by just Cerberus
+    int hearDist; // hear radius
+    int seeDist; // sight radius
+    int periphery; // periphery
+    int meleeDist; // unused?
+    int fleeHealth; // at which hp level enemy will turn in burning dude
+    int hinderDamage; // recoil damage
+    int changeTarget; // chance to change target when attacked someone else
+    int changeTargetKin; // chance to change target when attacked by same type
+    int alertChance;
+    char lockOut; // indicates if this dude can trigger something via trigger flags
+    int frontSpeed; // acceleration
+    int sideSpeed; // dodge
+    int backSpeed; // backward speed (unused)
+    int angSpeed; // turn speed
+    int nGibType[3]; // which gib used when explode dude
+    int startDamage[7]; // start damage shift
+    int at70[7]; // real damage? Hmm?
+    int at8c; // unused ?
+    int at90; // unused ?
+};
+
+extern DUDEINFO dudeInfo[kDudeMax-kDudeBase];
+extern DUDEINFO gPlayerTemplate[4];
\ No newline at end of file
diff --git a/source/blood/src/endgame.cpp b/source/blood/src/endgame.cpp
new file mode 100644
index 000000000..07f09e778
--- /dev/null
+++ b/source/blood/src/endgame.cpp
@@ -0,0 +1,277 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "common.h"
+#include "mmulti.h"
+#include "fx_man.h"
+#include "common_game.h"
+#include "blood.h"
+#include "endgame.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "menu.h"
+#include "network.h"
+#include "player.h"
+#include "sound.h"
+#include "view.h"
+
+CEndGameMgr::CEndGameMgr()
+{
+    at0 = 0;
+}
+
+void CEndGameMgr::Draw(void)
+{
+    viewLoadingScreenWide();
+    int nHeight;
+    gMenuTextMgr.GetFontInfo(1, NULL, NULL, &nHeight);
+    rotatesprite(160<<16, 20<<16, 65536, 0, 2038, -128, 0, 6, 0, 0, xdim-1, ydim-1);
+    int nY = 20 - nHeight / 2;
+    if (gGameOptions.nGameType == 0)
+    {
+        viewDrawText(1, "LEVEL STATS", 160, nY, -128, 0, 1, 0);
+        if (CCheatMgr::m_bPlayerCheated)
+        {
+            viewDrawText(3, ">>> YOU CHEATED! <<<", 160, 32, -128, 0, 1, 1);
+        }
+        gKillMgr.Draw();
+        gSecretMgr.Draw();
+    }
+    else
+    {
+        viewDrawText(1, "FRAG STATS", 160, nY, -128, 0, 1, 0);
+        gKillMgr.Draw();
+    }
+    if (/*dword_28E3D4 != 1 && */(gGameClock&32))
+    {
+        viewDrawText(3, "PRESS A KEY TO CONTINUE", 160, 134, -128, 0, 1, 1);
+    }
+}
+
+void CEndGameMgr::ProcessKeys(void)
+{
+    //if (dword_28E3D4 == 1)
+    //{
+    //    if (gGameOptions.gameType >= 0 || numplayers > 1)
+    //        netWaitForEveryone(0);
+    //    Finish();
+    //}
+    //else
+    {
+        char ch = keyGetScan();
+        if (!ch)
+            return;
+        if (gGameOptions.nGameType > 0 || numplayers > 1)
+            netWaitForEveryone(0);
+        Finish();
+    }
+}
+
+extern void EndLevel(void);
+
+void CEndGameMgr::Setup(void)
+{
+    at1 = gInputMode;
+    gInputMode = INPUT_MODE_3;
+    at0 = 1;
+    EndLevel();
+    sndStartSample(268, 128, -1, 1);
+    keyFlushScans();
+}
+
+//int gNextLevel;
+
+extern int gInitialNetPlayers;
+extern bool gStartNewGame;
+
+void CEndGameMgr::Finish(void)
+{
+    levelSetupOptions(gGameOptions.nEpisode, gNextLevel);
+    gInitialNetPlayers = numplayers;
+    //if (FXDevice != -1)
+        FX_StopAllSounds();
+    sndKillAllSounds();
+    gStartNewGame = 1;
+    gInputMode = (INPUT_MODE)at1;
+    at0 = 0;
+}
+
+CKillMgr::CKillMgr()
+{
+    Clear();
+}
+
+void CKillMgr::SetCount(int nCount)
+{
+    at0 = nCount;
+}
+
+void CKillMgr::sub_263E0(int nCount)
+{
+    at0 += nCount;
+}
+
+void CKillMgr::AddKill(spritetype *pSprite)
+{
+    if (pSprite->statnum == 6 && pSprite->type != 219 && pSprite->type != 220 && pSprite->type != 245 && pSprite->type != 239)
+        at4++;
+}
+
+void CKillMgr::sub_2641C(void)
+{
+    at0 = 0;
+    for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->type < kDudeBase || pSprite->type >= kDudeMax)
+            ThrowError("Non-enemy sprite (%d) in the enemy sprite list.", nSprite);
+        if (pSprite->statnum == 6 && pSprite->type != 219 && pSprite->type != 220 && pSprite->type != 245 && pSprite->type != 239)
+            at0++;
+    }
+}
+
+void CKillMgr::Draw(void)
+{
+    char pBuffer[40];
+    if (gGameOptions.nGameType == 0)
+    {
+        viewDrawText(1, "KILLS:", 75, 50, -128, 0, 0, 1);
+        sprintf(pBuffer, "%2d", at4);
+        viewDrawText(1, pBuffer, 160, 50, -128, 0, 0, 1);
+        viewDrawText(1, "OF", 190, 50, -128, 0, 0, 1);
+        sprintf(pBuffer, "%2d", at0);
+        viewDrawText(1, pBuffer, 220, 50, -128, 0, 0, 1);
+    }
+    else
+    {
+        viewDrawText(3, "#", 85, 35, -128, 0, 0, 1);
+        viewDrawText(3, "NAME", 100, 35, -128, 0, 0, 1);
+        viewDrawText(3, "FRAGS", 210, 35, -128, 0, 0, 1);
+        int nStart = 0;
+        int nEnd = gInitialNetPlayers;
+        //if (dword_28E3D4 == 1)
+        //{
+        //    nStart++;
+        //    nEnd++;
+        //}
+        for (int i = nStart; i < nEnd; i++)
+        {
+            sprintf(pBuffer, "%-2d", i);
+            viewDrawText(3, pBuffer, 85, 50+8*i, -128, 0, 0, 1);
+            sprintf(pBuffer, "%s", gProfile[i].name);
+            viewDrawText(3, pBuffer, 100, 50+8*i, -128, 0, 0, 1);
+            sprintf(pBuffer, "%d", gPlayer[i].at2c6);
+            viewDrawText(3, pBuffer, 210, 50+8*i, -128, 0, 0, 1);
+        }
+    }
+}
+
+void CKillMgr::Clear(void)
+{
+    at0 = at4 = 0;
+}
+
+CSecretMgr::CSecretMgr(void)
+{
+    Clear();
+}
+
+void CSecretMgr::SetCount(int nCount)
+{
+    at0 = nCount;
+}
+
+void CSecretMgr::Found(int nType)
+{
+    if (nType < 0)
+        ThrowError("Invalid secret type %d triggered.", nType);
+    if (nType == 0)
+        at4++;
+    else
+        at8++;
+    if (gGameOptions.nGameType == 0)
+    {
+        switch (Random(2))
+        {
+        case 0:
+            viewSetMessage("A secret is revealed.");
+            break;
+        case 1:
+            viewSetMessage("You found a secret.");
+            break;
+        }
+    }
+}
+
+void CSecretMgr::Draw(void)
+{
+    char pBuffer[40];
+    viewDrawText(1, "SECRETS:", 75, 70, -128, 0, 0, 1);
+    sprintf(pBuffer, "%2d", at4);
+    viewDrawText(1, pBuffer, 160, 70, -128, 0, 0, 1);
+    viewDrawText(1, "OF", 190, 70, -128, 0, 0, 1);
+    sprintf(pBuffer, "%2d", at0);
+    viewDrawText(1, pBuffer, 220, 70, -128, 0, 0, 1);
+    if (at8 > 0)
+        viewDrawText(1, "YOU FOUND A SUPER SECRET!", 160, 100, -128, 2, 1, 1);
+}
+
+void CSecretMgr::Clear(void)
+{
+    at0 = at4 = at8 = 0;
+}
+
+class EndGameLoadSave : public LoadSave {
+public:
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+void EndGameLoadSave::Load(void)
+{
+    Read(&gSecretMgr.at0, 4);
+    Read(&gSecretMgr.at4, 4);
+    Read(&gSecretMgr.at8, 4);
+    Read(&gKillMgr.at0, 4);
+    Read(&gKillMgr.at4, 4);
+}
+
+void EndGameLoadSave::Save(void)
+{
+    Write(&gSecretMgr.at0, 4);
+    Write(&gSecretMgr.at4, 4);
+    Write(&gSecretMgr.at8, 4);
+    Write(&gKillMgr.at0, 4);
+    Write(&gKillMgr.at4, 4);
+}
+
+CEndGameMgr gEndGameMgr;
+CSecretMgr gSecretMgr;
+CKillMgr gKillMgr;
+static EndGameLoadSave *myLoadSave;
+
+void EndGameLoadSaveConstruct(void)
+{
+    myLoadSave = new EndGameLoadSave();
+}
diff --git a/source/blood/src/endgame.h b/source/blood/src/endgame.h
new file mode 100644
index 000000000..8c66fdc53
--- /dev/null
+++ b/source/blood/src/endgame.h
@@ -0,0 +1,62 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common_game.h"
+
+class CEndGameMgr {
+public:
+    char at0;
+    char at1;
+    CEndGameMgr();
+    void Setup(void);
+    void ProcessKeys(void);
+    void Draw(void);
+    void Finish(void);
+};
+
+class CKillMgr {
+public:
+    int at0, at4;
+    CKillMgr();
+    void SetCount(int);
+    void sub_263E0(int);
+    void AddKill(spritetype *pSprite);
+    void sub_2641C(void);
+    void Clear(void);
+    void Draw(void);
+};
+
+class CSecretMgr {
+public:
+    int at0, at4, at8;
+    CSecretMgr();
+    void SetCount(int);
+    void Found(int);
+    void Clear(void);
+    void Draw(void);
+};
+
+extern CEndGameMgr gEndGameMgr;
+extern CSecretMgr gSecretMgr;
+extern CKillMgr gKillMgr;
diff --git a/source/blood/src/eventq.cpp b/source/blood/src/eventq.cpp
new file mode 100644
index 000000000..9a348539b
--- /dev/null
+++ b/source/blood/src/eventq.cpp
@@ -0,0 +1,611 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "build.h"
+#include "common_game.h"
+
+#include "callback.h"
+#include "db.h"
+#include "eventq.h"
+#include "globals.h"
+#include "loadsave.h"
+#include "pqueue.h"
+#include "triggers.h"
+
+class EventQueue
+{
+public:
+    PriorityQueue* PQueue;
+    EventQueue()
+    {
+        PQueue = NULL;
+    }
+    bool IsNotEmpty(unsigned int nTime)
+    {
+        return PQueue->Size() > 0 && nTime >= PQueue->LowestPriority();
+    }
+    EVENT ERemove(void)
+    {
+        unsigned int node = PQueue->Remove();
+        return *(EVENT*)&node;
+    }
+    void Kill(int, int);
+    void Kill(int, int, CALLBACK_ID);
+};
+
+EventQueue eventQ;
+void EventQueue::Kill(int a1, int a2)
+{
+    EVENT evn = { (unsigned int)a1, (unsigned int)a2, 0, 0 };
+    //evn.at0_0 = a1;
+    //evn.at1_5 = a2;
+
+    short vs = *(short*)&evn;
+    PQueue->Kill([=](unsigned int nItem)->bool {return !memcmp(&nItem, &vs, 2); });
+}
+
+void EventQueue::Kill(int a1, int a2, CALLBACK_ID a3)
+{
+    EVENT evn = { (unsigned int)a1, (unsigned int)a2, kCommandCallback, (unsigned int)a3 };
+    unsigned int vc = *(unsigned int*)&evn;
+    PQueue->Kill([=](unsigned int nItem)->bool {return nItem == vc; });
+}
+
+//struct RXBUCKET
+//{
+//    unsigned int at0_0 : 13;
+//    unsigned int at1_5 : 3;
+//};
+
+RXBUCKET rxBucket[kMaxChannels+1];
+
+int GetBucketChannel(const RXBUCKET *pRX)
+{
+    switch (pRX->type)
+    {
+    case 6:
+    {
+        int nIndex = pRX->index;
+        int nXIndex = sector[nIndex].extra;
+        dassert(nXIndex > 0);
+        return xsector[nXIndex].rxID;
+    }
+    case 0:
+    {
+        int nIndex = pRX->index;
+        int nXIndex = wall[nIndex].extra;
+        dassert(nXIndex > 0);
+        return xwall[nXIndex].rxID;
+    }
+    case 3:
+    {
+        int nIndex = pRX->index;
+        int nXIndex = sprite[nIndex].extra;
+        dassert(nXIndex > 0);
+        return xsprite[nXIndex].rxID;
+    }
+    default:
+        ThrowError("Unexpected rxBucket type %d, index %d", pRX->type, pRX->index);
+        break;
+    }
+    return 0;
+}
+
+#if 0
+int CompareChannels(const void *a, const void *b)
+{
+    return GetBucketChannel((const RXBUCKET*)a)-GetBucketChannel((const RXBUCKET*)b);
+}
+#else
+static int CompareChannels(RXBUCKET *a, RXBUCKET *b)
+{
+    return GetBucketChannel(a) - GetBucketChannel(b);
+}
+#endif
+
+static RXBUCKET *SortGetMiddle(RXBUCKET *a1, RXBUCKET *a2, RXBUCKET *a3)
+{
+    if (CompareChannels(a1, a2) > 0)
+    {
+        if (CompareChannels(a1, a3) > 0)
+        {
+            if (CompareChannels(a2, a3) > 0)
+                return a2;
+            return a3;
+        }
+        return a1;
+    }
+    else
+    {
+        if (CompareChannels(a1, a3) < 0)
+        {
+            if (CompareChannels(a2, a3) > 0)
+                return a3;
+            return a2;
+        }
+        return a1;
+    }
+}
+
+static void SortSwap(RXBUCKET *a, RXBUCKET *b)
+{
+    RXBUCKET t = *a;
+    *a = *b;
+    *b = t;
+}
+
+static void SortRXBucket(int nCount)
+{
+    RXBUCKET *v144[32];
+    int vc4[32];
+    int v14 = 0;
+    RXBUCKET *pArray = rxBucket;
+    while (true)
+    {
+        while (nCount > 1)
+        {
+            if (nCount < 16)
+            {
+                for (int nDist = 3; nDist > 0; nDist -= 2)
+                {
+                    for (RXBUCKET *pI = pArray+nDist; pI < pArray+nCount; pI += nDist)
+                    {
+                        for (RXBUCKET *pJ = pI; pJ > pArray && CompareChannels(pJ-nDist, pJ) > 0; pJ -= nDist)
+                        {
+                            SortSwap(pJ, pJ-nDist);
+                        }
+                    }
+                }
+                break;
+            }
+            RXBUCKET *v30, *vdi, *vsi;
+            vdi = pArray + nCount / 2;
+            if (nCount > 29)
+            {
+                v30 = pArray;
+                vsi = pArray + nCount-1;
+                if (nCount > 42)
+                {
+                    int v20 = nCount / 8;
+                    v30 = SortGetMiddle(v30, v30+v20, v30+v20*2);
+                    vdi = SortGetMiddle(vdi-v20, vdi, vdi+v20);
+                    vsi = SortGetMiddle(vsi-v20*2, vsi-v20, vsi);
+                }
+                vdi = SortGetMiddle(v30, vdi, vsi);
+            }
+            RXBUCKET v44 = *vdi;
+            RXBUCKET *vc = pArray;
+            RXBUCKET *v8 = pArray+nCount-1;
+            RXBUCKET *vbx = vc;
+            RXBUCKET *v4 = v8;
+            while (true)
+            {
+                while (vbx <= v4)
+                {
+                    int nCmp = CompareChannels(vbx, &v44);
+                    if (nCmp > 0)
+                        break;
+                    if (nCmp == 0)
+                    {
+                        SortSwap(vbx, vc);
+                        vc++;
+                    }
+                    vbx++;
+                }
+                while (vbx <= v4)
+                {
+                    int nCmp = CompareChannels(v4, &v44);
+                    if (nCmp < 0)
+                        break;
+                    if (nCmp == 0)
+                    {
+                        SortSwap(v4, v8);
+                        v8--;
+                    }
+                    v4--;
+                }
+                if (vbx > v4)
+                    break;
+                SortSwap(vbx, v4);
+                v4--;
+                vbx++;
+            }
+            RXBUCKET *v2c = pArray+nCount;
+            int vt = ClipHigh(vbx-vc, vc-pArray);
+            for (int i = 0; i < vt; i++)
+            {
+                SortSwap(&vbx[i-vt], &pArray[i]);
+            }
+            vt = ClipHigh(v8-v4, v2c-v8-1);
+            for (int i = 0; i < vt; i++)
+            {
+                SortSwap(&v2c[i-vt], &vbx[i]);
+            }
+            int vvsi = v8-v4;
+            int vvdi = vbx-vc;
+            if (vvsi >= vvdi)
+            {
+                vc4[v14] = vvsi;
+                v144[v14] = v2c-vvsi;
+                nCount = vvdi;
+                v14++;
+            }
+            else
+            {
+                vc4[v14] = vvdi;
+                v144[v14] = pArray;
+                nCount = vvsi;
+                pArray = v2c - vvsi;
+                v14++;
+            }
+        }
+        if (v14 == 0)
+            return;
+        v14--;
+        pArray = v144[v14];
+        nCount = vc4[v14];
+    }
+}
+
+unsigned short bucketHead[1024+1];
+
+void evInit(void)
+{
+    if (eventQ.PQueue)
+        delete eventQ.PQueue;
+    if (VanillaMode())
+        eventQ.PQueue = new VanillaPriorityQueue();
+    else
+        eventQ.PQueue = new StdPriorityQueue();
+    eventQ.PQueue->Clear();
+    int nCount = 0;
+    for (int i = 0; i < numsectors; i++)
+    {
+        int nXSector = sector[i].extra;
+        if (nXSector >= kMaxXSectors)
+            ThrowError("Invalid xsector reference in sector %d", i);
+        if (nXSector > 0 && xsector[nXSector].rxID > 0)
+        {
+            dassert(nCount < kMaxChannels);
+            rxBucket[nCount].type = 6;
+            rxBucket[nCount].index = i;
+            nCount++;
+        }
+    }
+    for (int i = 0; i < numwalls; i++)
+    {
+        int nXWall = wall[i].extra;
+        if (nXWall >= kMaxXWalls)
+            ThrowError("Invalid xwall reference in wall %d", i);
+        if (nXWall > 0 && xwall[nXWall].rxID > 0)
+        {
+            dassert(nCount < kMaxChannels);
+            rxBucket[nCount].type = 0;
+            rxBucket[nCount].index = i;
+            nCount++;
+        }
+    }
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        if (sprite[i].statnum < kMaxStatus)
+        {
+            int nXSprite = sprite[i].extra;
+            if (nXSprite >= kMaxXSprites)
+                ThrowError("Invalid xsprite reference in sprite %d", i);
+            if (nXSprite > 0 && xsprite[nXSprite].rxID > 0)
+            {
+                dassert(nCount < kMaxChannels);
+                rxBucket[nCount].type = 3;
+                rxBucket[nCount].index = i;
+                nCount++;
+            }
+        }
+    }
+    SortRXBucket(nCount);
+    int i, j = 0;
+    for (i = 0; i < 1024; i++)
+    {
+        bucketHead[i] = j;
+        while(j < nCount && GetBucketChannel(&rxBucket[j]) == i)
+            j++;
+    }
+    bucketHead[i] = j;
+}
+
+char evGetSourceState(int nType, int nIndex)
+{
+    switch (nType)
+    {
+    case 6:
+    {
+        int nXIndex = sector[nIndex].extra;
+        dassert(nXIndex > 0 && nXIndex < kMaxXSectors);
+        return xsector[nXIndex].state;
+    }
+    case 0:
+    {
+        int nXIndex = wall[nIndex].extra;
+        dassert(nXIndex > 0 && nXIndex < kMaxXWalls);
+        return xwall[nXIndex].state;
+    }
+    case 3:
+    {
+        int nXIndex = sprite[nIndex].extra;
+        dassert(nXIndex > 0 && nXIndex < kMaxXSprites);
+        return xsprite[nXIndex].state;
+    }
+    }
+    return 0;
+}
+
+void evSend(int nIndex, int nType, int rxId, COMMAND_ID command)
+{
+    if (command == COMMAND_ID_2)
+        command = evGetSourceState(nType, nIndex) ? COMMAND_ID_1 : COMMAND_ID_0;
+    else if (command == COMMAND_ID_4)
+        command = evGetSourceState(nType, nIndex) ? COMMAND_ID_0 : COMMAND_ID_1;
+    EVENT evn;
+    evn.index = nIndex;
+    evn.type = nType;
+    evn.cmd = command;
+    if (rxId > 0)
+    {
+        switch (rxId)
+        {
+        case 7:
+        case 10:
+            break;
+        case 3:
+            if (command < COMMAND_ID_64)
+                ThrowError("Invalid TextOver command by xobject %d(type %d)", nIndex, nType);
+            trTextOver(command-COMMAND_ID_64);
+            return;
+        case 4:
+            levelEndLevel(0);
+            return;
+        case 5:
+            levelEndLevel(1);
+            return;
+        // By NoOne: finished level and load custom level � via numbered command.
+        case kGDXChannelEndLevelCustom:
+            levelEndLevelCustom(command - 64);
+            return;
+        case 1:
+            if (command < COMMAND_ID_64)
+                ThrowError("Invalid SetupSecret command by xobject %d(type %d)", nIndex, nType);
+            levelSetupSecret(command - COMMAND_ID_64);
+            break;
+        case 2:
+            if (command < COMMAND_ID_64)
+                ThrowError("Invalid Secret command by xobject %d(type %d)", nIndex, nType);
+            levelTriggerSecret(command - COMMAND_ID_64);
+            break;
+        case 90:
+        case 91:
+        case 92:
+        case 93:
+        case 94:
+        case 95:
+        case 96:
+        case 97:
+            for (int nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+            {
+                spritetype *pSprite = &sprite[nSprite];
+                if (pSprite->hitag&32)
+                    continue;
+                int nXSprite = pSprite->extra;
+                if (nXSprite > 0)
+                {
+                    XSPRITE *pXSprite = &xsprite[nXSprite];
+                    if (pXSprite->rxID == rxId)
+                        trMessageSprite(nSprite, evn);
+                }
+            }
+            return;
+        case 80:
+        case 81:
+            for (int nSprite = headspritestat[3]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+            {
+                spritetype *pSprite = &sprite[nSprite];
+                if (pSprite->hitag&32)
+                    continue;
+                int nXSprite = pSprite->extra;
+                if (nXSprite > 0)
+                {
+                    XSPRITE *pXSprite = &xsprite[nXSprite];
+                    if (pXSprite->rxID == rxId)
+                        trMessageSprite(nSprite, evn);
+                }
+            }
+            return;
+        }
+    }
+    for (int i = bucketHead[rxId]; i < bucketHead[rxId+1]; i++)
+    {
+        if (evn.type != rxBucket[i].type || evn.index != rxBucket[i].index)
+        {
+            switch (rxBucket[i].type)
+            {
+            case 6:
+                trMessageSector(rxBucket[i].index, evn);
+                break;
+            case 0:
+                trMessageWall(rxBucket[i].index, evn);
+                break;
+            case 3:
+            {
+                int nSprite = rxBucket[i].index;
+                spritetype *pSprite = &sprite[nSprite];
+                if (pSprite->hitag&32)
+                    continue;
+                int nXSprite = pSprite->extra;
+                if (nXSprite > 0)
+                {
+                    XSPRITE *pXSprite = &xsprite[nXSprite];
+                    if (pXSprite->rxID > 0)
+                        trMessageSprite(nSprite, evn);
+                }
+                break;
+            }
+            }
+        }
+    }
+}
+
+void evPost(int nIndex, int nType, unsigned int nDelta, COMMAND_ID command)
+{
+    dassert(command != kCommandCallback);
+    if (command == COMMAND_ID_2)
+        command = evGetSourceState(nType, nIndex) ? COMMAND_ID_1 : COMMAND_ID_0;
+    else if (command == COMMAND_ID_4)
+        command = evGetSourceState(nType, nIndex) ? COMMAND_ID_0 : COMMAND_ID_1;
+    EVENT evn;
+    evn.index = nIndex;
+    evn.type = nType;
+    evn.cmd = command;
+    // Inlined?
+    eventQ.PQueue->Insert(gFrameClock+nDelta, *(unsigned int*)&evn);
+}
+
+void evPost(int nIndex, int nType, unsigned int nDelta, CALLBACK_ID a4)
+{
+    EVENT evn;
+    evn.index = nIndex;
+    evn.type = nType;
+    evn.cmd = kCommandCallback;
+    evn.funcID = a4;
+    eventQ.PQueue->Insert(gFrameClock+nDelta, *(unsigned int*)&evn);
+}
+
+void evProcess(unsigned int nTime)
+{
+#if 0
+    while (1)
+    {
+        // Inlined?
+        char bDone;
+        if (eventQ.fNodeCount > 0 && nTime >= eventQ.queueItems[1])
+            bDone = 1;
+        else
+            bDone = 0;
+        if (!bDone)
+            break;
+#endif
+    while(eventQ.IsNotEmpty(nTime))
+    {
+        EVENT event = eventQ.ERemove();
+        if (event.cmd == kCommandCallback)
+        {
+            dassert(event.funcID < kCallbackMax);
+            dassert(gCallback[event.funcID] != NULL);
+            gCallback[event.funcID](event.index);
+        }
+        else
+        {
+            switch (event.type)
+            {
+            case 6:
+                trMessageSector(event.index, event);
+                break;
+            case 0:
+                trMessageWall(event.index, event);
+                break;
+            case 3:
+                trMessageSprite(event.index, event);
+                break;
+            }
+        }
+    }
+}
+
+void evKill(int a1, int a2)
+{
+    eventQ.Kill(a1, a2);
+}
+
+void evKill(int a1, int a2, CALLBACK_ID a3)
+{
+    eventQ.Kill(a1, a2, a3);
+}
+
+class EventQLoadSave : public LoadSave
+{
+public:
+    virtual void Load();
+    virtual void Save();
+};
+
+void EventQLoadSave::Load()
+{
+    if (eventQ.PQueue)
+        delete eventQ.PQueue;
+    Read(&eventQ, sizeof(eventQ));
+    if (VanillaMode())
+        eventQ.PQueue = new VanillaPriorityQueue();
+    else
+        eventQ.PQueue = new StdPriorityQueue();
+    int nEvents;
+    Read(&nEvents, sizeof(nEvents));
+    for (int i = 0; i < nEvents; i++)
+    {
+        EVENT event;
+        unsigned int eventtime;
+        Read(&eventtime, sizeof(eventtime));
+        Read(&event, sizeof(event));
+        eventQ.PQueue->Insert(eventtime, *(unsigned int*)&event);
+    }
+    Read(rxBucket, sizeof(rxBucket));
+    Read(bucketHead, sizeof(bucketHead));
+}
+
+void EventQLoadSave::Save()
+{
+    EVENT events[1024];
+    unsigned int eventstime[1024];
+    Write(&eventQ, sizeof(eventQ));
+    int nEvents = eventQ.PQueue->Size();
+    Write(&nEvents, sizeof(nEvents));
+    for (int i = 0; i < nEvents; i++)
+    {
+        eventstime[i] = eventQ.PQueue->LowestPriority();
+        events[i] = eventQ.ERemove();
+        Write(&eventstime[i], sizeof(eventstime[i]));
+        Write(&events[i], sizeof(events[i]));
+    }
+    dassert(eventQ.PQueue->Size() == 0);
+    for (int i = 0; i < nEvents; i++)
+    {
+        eventQ.PQueue->Insert(eventstime[i], *(unsigned int*)&events[i]);
+    }
+    Write(rxBucket, sizeof(rxBucket));
+    Write(bucketHead, sizeof(bucketHead));
+}
+
+static EventQLoadSave *myLoadSave;
+
+void EventQLoadSaveConstruct(void)
+{
+    myLoadSave = new EventQLoadSave();
+}
diff --git a/source/blood/src/eventq.h b/source/blood/src/eventq.h
new file mode 100644
index 000000000..e47f1592a
--- /dev/null
+++ b/source/blood/src/eventq.h
@@ -0,0 +1,68 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "callback.h"
+#define kMaxChannels 4096
+
+struct RXBUCKET
+{
+    unsigned int index : 13;
+    unsigned int type : 3;
+};
+extern RXBUCKET rxBucket[];
+extern unsigned short bucketHead[];
+
+enum COMMAND_ID {
+    COMMAND_ID_0 = 0,
+    COMMAND_ID_1,
+    COMMAND_ID_2,
+    COMMAND_ID_3,
+    COMMAND_ID_4,
+    COMMAND_ID_5,
+    COMMAND_ID_6,
+    COMMAND_ID_7,
+    COMMAND_ID_8,
+    COMMAND_ID_9,
+
+    kCommandCallback = 20,
+    COMMAND_ID_21,
+    kGDXCommandPaste = 53, // used by some new GDX types
+    kGDXCommandSpriteDamage, // used by sprite damager GDX type
+    COMMAND_ID_64 = 64,
+};
+
+struct EVENT {
+    unsigned int index : 13; // index
+    unsigned int type : 3; // type
+    unsigned int cmd : 8; // cmd
+    unsigned int funcID : 8; // callback
+}; // <= 4 bytes
+
+void evInit(void);
+char evGetSourceState(int nType, int nIndex);
+void evSend(int nIndex, int nType, int rxId, COMMAND_ID command);
+void evPost(int nIndex, int nType, unsigned int nDelta, COMMAND_ID command);
+void evPost(int nIndex, int nType, unsigned int nDelta, CALLBACK_ID a4);
+void evProcess(unsigned int nTime);
+void evKill(int a1, int a2);
+void evKill(int a1, int a2, CALLBACK_ID a3);
\ No newline at end of file
diff --git a/source/blood/src/fire.cpp b/source/blood/src/fire.cpp
new file mode 100644
index 000000000..a471a34f2
--- /dev/null
+++ b/source/blood/src/fire.cpp
@@ -0,0 +1,124 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "build.h"
+#include "common_game.h"
+#include "blood.h"
+#include "fire.h"
+#include "globals.h"
+#include "misc.h"
+#include "tile.h"
+
+int fireSize = 128;
+int gDamping = 6;
+
+/*extern "C" */char CoolTable[1024];
+
+void CellularFrame(char *pFrame, int sizeX, int sizeY);
+
+char FrameBuffer[17280];
+char SeedBuffer[16][128];
+char *gCLU;
+
+void InitSeedBuffers(void)
+{
+    for (int i = 0; i < 16; i++)
+        for (int j = 0; j < fireSize; j += 2)
+            SeedBuffer[i][j] = SeedBuffer[i][j+1] = wrand();
+}
+
+void BuildCoolTable(void)
+{
+    for (int i = 0; i < 1024; i++)
+        CoolTable[i] = ClipLow((i-gDamping) / 4, 0);
+}
+
+void DoFireFrame(void)
+{
+    int nRand = qrand()&15;
+    for (int i = 0; i < 3; i++)
+    {
+        memcpy(FrameBuffer+16896+i*128, SeedBuffer[nRand], 128);
+    }
+    CellularFrame(FrameBuffer, 128, 132);
+    char *pData = tileLoadTile(2342);
+    char *pSource = FrameBuffer;
+    int x = fireSize;
+    do
+    {
+        int y = fireSize;
+        char *pDataBak = pData;
+        do
+        {
+            *pData = gCLU[*pSource];
+            pSource++;
+            pData += fireSize;
+        } while (--y);
+        pData = pDataBak + 1;
+    } while (--x);
+}
+
+void FireInit(void)
+{
+    memset(FrameBuffer, 0, sizeof(FrameBuffer));
+    BuildCoolTable();
+    InitSeedBuffers();
+    DICTNODE *pNode = gSysRes.Lookup("RFIRE", "CLU");
+    if (!pNode)
+        ThrowError("RFIRE.CLU not found");
+    gCLU = (char*)gSysRes.Lock(pNode);
+    for (int i = 0; i < 100; i++)
+        DoFireFrame();
+}
+
+void FireProcess(void)
+{
+    static int lastUpdate;
+    if (gGameClock < lastUpdate || lastUpdate + 2 < gGameClock)
+    {
+        DoFireFrame();
+        lastUpdate = gGameClock;
+        tileInvalidate(2342, -1, -1);
+    }
+}
+
+void CellularFrame(char *pFrame, int sizeX, int sizeY)
+{
+    int nSquare = sizeX * sizeY;
+    unsigned char *pPtr1 = (unsigned char*)pFrame;
+    while (nSquare--)
+    {
+        unsigned char *pPtr2 = pPtr1+sizeX;
+        int sum = *(pPtr2-1) + *pPtr2 + *(pPtr2+1) + *(pPtr2+sizeX);
+        if (*(pPtr2+sizeX) > 96)
+        {
+            pPtr2 += sizeX;
+            sum += *(pPtr2-1) + *pPtr2 + *(pPtr2+1) + *(pPtr2+sizeX);
+            sum >>= 1;
+        }
+        *pPtr1 = CoolTable[sum];
+        pPtr1++;
+    }
+}
diff --git a/source/blood/src/fire.h b/source/blood/src/fire.h
new file mode 100644
index 000000000..0fc76789b
--- /dev/null
+++ b/source/blood/src/fire.h
@@ -0,0 +1,26 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+void FireInit(void);
+void FireProcess(void);
\ No newline at end of file
diff --git a/source/blood/src/function.h b/source/blood/src/function.h
new file mode 100644
index 000000000..37a10d47b
--- /dev/null
+++ b/source/blood/src/function.h
@@ -0,0 +1,106 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+// function.h
+
+// file created by makehead.exe
+// these headers contain default key assignments, as well as
+// default button assignments and game function names
+// axis defaults are also included
+
+
+#ifndef function_public_h_
+#define function_public_h_
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define NUMGAMEFUNCTIONS 55
+#define MAXGAMEFUNCLEN 32
+
+extern char gamefunctions[NUMGAMEFUNCTIONS][MAXGAMEFUNCLEN];
+extern const char keydefaults[NUMGAMEFUNCTIONS*2][MAXGAMEFUNCLEN];
+extern const char oldkeydefaults[NUMGAMEFUNCTIONS*2][MAXGAMEFUNCLEN];
+
+enum GameFunction_t
+   {
+   gamefunc_Move_Forward,
+   gamefunc_Move_Backward,
+   gamefunc_Turn_Left,
+   gamefunc_Turn_Right,
+   gamefunc_Turn_Around,
+   gamefunc_Strafe,
+   gamefunc_Strafe_Left,
+   gamefunc_Strafe_Right,
+   gamefunc_Jump,
+   gamefunc_Crouch,
+   gamefunc_Run,
+   gamefunc_AutoRun,
+   gamefunc_Open,
+   gamefunc_Weapon_Fire,
+   gamefunc_Weapon_Special_Fire,
+   gamefunc_Aim_Up,
+   gamefunc_Aim_Down,
+   gamefunc_Aim_Center,
+   gamefunc_Look_Up,
+   gamefunc_Look_Down,
+   gamefunc_Tilt_Left,
+   gamefunc_Tilt_Right,
+   gamefunc_Weapon_1,
+   gamefunc_Weapon_2,
+   gamefunc_Weapon_3,
+   gamefunc_Weapon_4,
+   gamefunc_Weapon_5,
+   gamefunc_Weapon_6,
+   gamefunc_Weapon_7,
+   gamefunc_Weapon_8,
+   gamefunc_Weapon_9,
+   gamefunc_Weapon_10,
+   gamefunc_Inventory_Use,
+   gamefunc_Inventory_Left,
+   gamefunc_Inventory_Right,
+   gamefunc_Map_Toggle,
+   gamefunc_Map_Follow_Mode,
+   gamefunc_Shrink_Screen,
+   gamefunc_Enlarge_Screen,
+   gamefunc_Send_Message,
+   gamefunc_See_Coop_View,
+   gamefunc_See_Chase_View,
+   gamefunc_Mouse_Aiming,
+   gamefunc_Toggle_Crosshair,
+   gamefunc_Next_Weapon,
+   gamefunc_Previous_Weapon,
+   gamefunc_Holster_Weapon,
+   gamefunc_Show_Opponents_Weapon,
+   gamefunc_BeastVision,
+   gamefunc_CrystalBall,
+   gamefunc_JumpBoots,
+   gamefunc_MedKit,
+   gamefunc_ProximityBombs,
+   gamefunc_RemoteBombs,
+   gamefunc_Show_Console,
+   };
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/source/blood/src/fx.cpp b/source/blood/src/fx.cpp
new file mode 100644
index 000000000..30ee66b86
--- /dev/null
+++ b/source/blood/src/fx.cpp
@@ -0,0 +1,348 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#include "build.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "blood.h"
+#include "callback.h"
+#include "config.h"
+#include "db.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gameutil.h"
+#include "levels.h"
+#include "seq.h"
+#include "trig.h"
+#include "view.h"
+
+CFX gFX;
+
+struct FXDATA {
+    CALLBACK_ID funcID; // callback
+    char at1; // detail
+    short at2; // seq
+    short at4; // flags
+    int at6; // gravity
+    int ata; // air drag
+    int ate;
+    short at12; // picnum
+    unsigned char at14; // xrepeat
+    unsigned char at15; // yrepeat
+    short at16; // cstat
+    signed char at18; // shade
+    char at19; // pal
+};
+
+FXDATA gFXData[] = {
+    { CALLBACK_ID_NONE, 0, 49, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 51, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 52, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 7, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 44, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 45, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 46, 1, -128, 8192, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 42, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 43, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 48, 3, -256, 8192, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 60, 3, -256, 8192, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_14, 2, 0, 1, 46603, 2048, 480, 2154, 40, 40, 0, -12, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 3, 46603, 5120, 480, 2269, 24, 24, 0, -128, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 3, 46603, 5120, 480, 1720, 24, 24, 0, -128, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 1, 58254, 3072, 480, 2280, 48, 48, 0, -128, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 1, 58254, 3072, 480, 3135, 48, 48, 0, -128, 0 },
+    { CALLBACK_ID_NONE, 0, 0, 3, 58254, 1024, 480, 3261, 32, 32, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 58254, 1024, 480, 3265, 32, 32, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 58254, 1024, 480, 3269, 32, 32, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 58254, 1024, 480, 3273, 32, 32, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 58254, 1024, 480, 3277, 32, 32, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 1, -27962, 8192, 600, 1128, 16, 16, 514, -16, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 1, -18641, 8192, 600, 1128, 12, 12, 514, -16, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 1, -9320, 8192, 600, 1128, 8, 8, 514, -16, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 1, -18641, 8192, 600, 1131, 32, 32, 514, -16, 0 },
+    { CALLBACK_ID_14, 2, 0, 3, 27962, 4096, 480, 733, 32, 32, 0, -16, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 18641, 4096, 120, 2261, 12, 12, 0, -128, 0 },
+    { CALLBACK_ID_NONE, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 58254, 3328, 480, 2185, 48, 48, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 0, 0, 3, 58254, 1024, 480, 2620, 48, 48, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 55, 1, -13981, 5120, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 56, 1, -13981, 5120, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 57, 1, 0, 2048, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 58, 1, 0, 2048, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 0, 0, 0, 960, 956, 32, 32, 610, 0, 0 },
+    { CALLBACK_ID_16, 2, 62, 0, 46603, 1024, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_16, 2, 63, 0, 46603, 1024, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_16, 2, 64, 0, 46603, 1024, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_16, 2, 65, 0, 46603, 1024, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_16, 2, 66, 0, 46603, 1024, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_16, 2, 67, 0, 46603, 1024, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 0, 0, 0, 838, 16, 16, 80, -8, 0 },
+    { CALLBACK_ID_NONE, 0, 0, 3, 34952, 8192, 0, 2078, 64, 64, 0, -8, 0 },
+    { CALLBACK_ID_NONE, 0, 0, 3, 34952, 8192, 0, 1106, 64, 64, 0, -8, 0 },
+    { CALLBACK_ID_NONE, 0, 0, 3, 58254, 3328, 480, 2406, 48, 48, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 3, 46603, 4096, 480, 3511, 64, 64, 0, -128, 0 },
+    { CALLBACK_ID_NONE, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 11, 3, -256, 8192, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 2, 11, 3, 0, 8192, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { CALLBACK_ID_NONE, 1, 30, 3, 0, 0, 0, 0, 40, 40, 80, -8, 0 },
+    { CALLBACK_ID_19, 2, 0, 3, 27962, 4096, 480, 4023, 32, 32, 0, -16, 0 },
+    { CALLBACK_ID_19, 2, 0, 3, 27962, 4096, 480, 4028, 32, 32, 0, -16, 0 },
+    { CALLBACK_ID_NONE, 2, 0, 0, 0, 0, 480, 926, 32, 32, 610, -12, 0 },
+    { CALLBACK_ID_NONE, 1, 70, 1, -13981, 5120, 0, 0, 0, 0, 0, 0, 0 }
+};
+
+void CFX::sub_73FB0(int nSprite)
+{
+    if (nSprite < 0 || nSprite >= kMaxSprites)
+        return;
+    evKill(nSprite, 3);
+    if (sprite[nSprite].extra > 0)
+        seqKill(3, sprite[nSprite].extra);
+    DeleteSprite(nSprite);
+}
+
+void CFX::sub_73FFC(int nSprite)
+{
+    if (nSprite < 0 || nSprite >= kMaxSprites)
+        return;
+    spritetype *pSprite = &sprite[nSprite];
+    if (pSprite->extra > 0)
+        seqKill(3, pSprite->extra);
+    if (pSprite->statnum != kStatFree)
+        actPostSprite(nSprite, kStatFree);
+}
+
+spritetype * CFX::fxSpawn(FX_ID nFx, int nSector, int x, int y, int z, unsigned int a6)
+{
+    if (nSector < 0 || nSector >= numsectors)
+        return NULL;
+    int nSector2 = nSector;
+    if (!FindSector(x, y, z, &nSector2))
+        return NULL;
+    if (gbAdultContent && gGameOptions.nGameType <= 0)
+    {
+        switch (nFx)
+        {
+        case FX_0:
+        case FX_1:
+        case FX_2:
+        case FX_3:
+        case FX_13:
+        case FX_34:
+        case FX_35:
+        case FX_36:
+            return NULL;
+        default:
+            break;
+        }
+    }
+    if (nFx < 0 || nFx >= kFXMax)
+        return NULL;
+    FXDATA *pFX = &gFXData[nFx];
+    if (gStatCount[1] == 512)
+    {
+        int nSprite = headspritestat[1];;
+        while ((sprite[nSprite].hitag & 32) && nSprite != -1)
+            nSprite = nextspritestat[nSprite];
+        if (nSprite == -1)
+            return NULL;
+        sub_73FB0(nSprite);
+    }
+    spritetype *pSprite = actSpawnSprite(nSector, x, y, z, 1, 0);
+    pSprite->type = nFx;
+    pSprite->picnum = pFX->at12;
+    pSprite->cstat |= pFX->at16;
+    pSprite->shade = pFX->at18;
+    pSprite->pal = pFX->at19;
+    qsprite_filler[pSprite->index] = pFX->at1;
+    if (pFX->at14 > 0)
+        pSprite->xrepeat = pFX->at14;
+    if (pFX->at15 > 0)
+        pSprite->yrepeat = pFX->at15;
+    if ((pFX->at4 & 1) && Chance(0x8000))
+        pSprite->cstat |= 4;
+    if ((pFX->at4 & 2) && Chance(0x8000))
+        pSprite->cstat |= 8;
+    if (pFX->at2)
+    {
+        int nXSprite = dbInsertXSprite(pSprite->index);
+        seqSpawn(pFX->at2, 3, nXSprite, -1);
+    }
+    if (a6 == 0)
+        a6 = pFX->ate;
+    if (a6)
+        evPost((int)pSprite->index, 3, a6+Random2(a6>>1), CALLBACK_ID_1);
+    return pSprite;
+}
+
+void CFX::fxProcess(void)
+{
+    for (int nSprite = headspritestat[1]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        viewBackupSpriteLoc(nSprite, pSprite);
+        short nSector = pSprite->sectnum;
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        dassert(pSprite->type < kFXMax);
+        FXDATA *pFXData = &gFXData[pSprite->type];
+        actAirDrag(pSprite, pFXData->ata);
+        if (xvel[nSprite])
+            pSprite->x += xvel[nSprite]>>12;
+        if (yvel[nSprite])
+            pSprite->y += yvel[nSprite]>>12;
+        if (zvel[nSprite])
+            pSprite->z += zvel[nSprite]>>8;
+        // Weird...
+        if (xvel[nSprite] || (yvel[nSprite] && pSprite->z >= sector[pSprite->sectnum].floorz))
+        {
+            updatesector(pSprite->x, pSprite->y, &nSector);
+            if (nSector == -1)
+            {
+                sub_73FFC(nSprite);
+                continue;
+            }
+            if (getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y) <= pSprite->z)
+            {
+                if (pFXData->funcID < 0 || pFXData->funcID >= kCallbackMax)
+                {
+                    sub_73FFC(nSprite);
+                    continue;
+                }
+                dassert(gCallback[pFXData->funcID] != NULL);
+                gCallback[pFXData->funcID](nSprite);
+                continue;
+            }
+            if (nSector != pSprite->sectnum)
+            {
+                dassert(nSector >= 0 && nSector < kMaxSectors);
+                ChangeSpriteSect(nSprite, nSector);
+            }
+        }
+        if (xvel[nSprite] || yvel[nSprite] || zvel[nSprite])
+        {
+            int32_t floorZ, ceilZ;
+            getzsofslope(nSector, pSprite->x, pSprite->y, &ceilZ, &floorZ);
+            if (ceilZ > pSprite->z && !(sector[nSector].ceilingstat&1))
+            {
+                sub_73FFC(nSprite);
+                continue;
+            }
+            if (floorZ < pSprite->z)
+            {
+                if (pFXData->funcID < 0 || pFXData->funcID >= kCallbackMax)
+                {
+                    sub_73FFC(nSprite);
+                    continue;
+                }
+                dassert(gCallback[pFXData->funcID] != NULL);
+                gCallback[pFXData->funcID](nSprite);
+                continue;
+            }
+        }
+        zvel[nSprite] += pFXData->at6;
+    }
+}
+
+void fxSpawnBlood(spritetype *pSprite, int a2)
+{
+    UNREFERENCED_PARAMETER(a2);
+    if (pSprite->sectnum < 0 || pSprite->sectnum >= numsectors)
+        return;
+    int nSector = pSprite->sectnum;
+    if (!FindSector(pSprite->x, pSprite->y, pSprite->z, &nSector))
+        return;
+    if (gbAdultContent && gGameOptions.nGameType <= 0)
+        return;
+    spritetype *pBlood = gFX.fxSpawn(FX_27, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pBlood)
+    {
+        pBlood->ang = 1024;
+        xvel[pBlood->index] = Random2(0x6aaaa);
+        yvel[pBlood->index] = Random2(0x6aaaa);
+        zvel[pBlood->index] = -Random(0x10aaaa)-100;
+        evPost(pBlood->index, 3, 8, CALLBACK_ID_6);
+    }
+}
+
+void sub_746D4(spritetype *pSprite, int a2)
+{
+    UNREFERENCED_PARAMETER(a2);
+    if (pSprite->sectnum < 0 || pSprite->sectnum >= numsectors)
+        return;
+    int nSector = pSprite->sectnum;
+    if (!FindSector(pSprite->x, pSprite->y, pSprite->z, &nSector))
+        return;
+    if (gbAdultContent && gGameOptions.nGameType <= 0)
+        return;
+    spritetype *pSpawn;
+    if (pSprite->type == 221)
+        pSpawn = gFX.fxSpawn(FX_53, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    else
+        pSpawn = gFX.fxSpawn(FX_54, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+    if (pSpawn)
+    {
+        pSpawn->ang = 1024;
+        xvel[pSpawn->index] = Random2(0x6aaaa);
+        yvel[pSpawn->index] = Random2(0x6aaaa);
+        zvel[pSpawn->index] = -Random(0x10aaaa)-100;
+        evPost(pSpawn->index, 3, 8, CALLBACK_ID_18);
+    }
+}
+
+void sub_74818(spritetype *pSprite, int z, int a3, int a4)
+{
+    int x = pSprite->x+mulscale28(pSprite->clipdist-4, Cos(pSprite->ang));
+    int y = pSprite->y+mulscale28(pSprite->clipdist-4, Sin(pSprite->ang));
+    x += mulscale30(a3, Cos(pSprite->ang+512));
+    y += mulscale30(a3, Sin(pSprite->ang+512));
+    spritetype *pShell = gFX.fxSpawn((FX_ID)(FX_37+Random(3)), pSprite->sectnum, x, y, z, 0);
+    if (pShell)
+    {
+        int nDist = (a4<<18)/120+Random2(((a4/4)<<18)/120);
+        int nAngle = pSprite->ang+Random2(56)+512;
+        xvel[pShell->index] = mulscale30(nDist, Cos(nAngle));
+        yvel[pShell->index] = mulscale30(nDist, Sin(nAngle));
+        zvel[pShell->index] = zvel[pSprite->index]-(0x20000+(Random2(40)<<18)/120);
+    }
+}
+
+void sub_74A18(spritetype *pSprite, int z, int a3, int a4)
+{
+    int x = pSprite->x+mulscale28(pSprite->clipdist-4, Cos(pSprite->ang));
+    int y = pSprite->y+mulscale28(pSprite->clipdist-4, Sin(pSprite->ang));
+    x += mulscale30(a3, Cos(pSprite->ang+512));
+    y += mulscale30(a3, Sin(pSprite->ang+512));
+    spritetype *pShell = gFX.fxSpawn((FX_ID)(FX_40+Random(3)), pSprite->sectnum, x, y, z, 0);
+    if (pShell)
+    {
+        int nDist = (a4<<18)/120+Random2(((a4/4)<<18)/120);
+        int nAngle = pSprite->ang+Random2(56)+512;
+        xvel[pShell->index] = mulscale30(nDist, Cos(nAngle));
+        yvel[pShell->index] = mulscale30(nDist, Sin(nAngle));
+        zvel[pShell->index] = zvel[pSprite->index]-(0x20000+(Random2(20)<<18)/120);
+    }
+}
diff --git a/source/blood/src/fx.h b/source/blood/src/fx.h
new file mode 100644
index 000000000..5d6c8809e
--- /dev/null
+++ b/source/blood/src/fx.h
@@ -0,0 +1,102 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common_game.h"
+
+enum FX_ID {
+    FX_NONE = -1,
+    FX_0 = 0,
+    FX_1,
+    FX_2,
+    FX_3,
+    FX_4,
+    FX_5,
+    FX_6,
+    FX_7,
+    FX_8,
+    FX_9,
+    FX_10,
+    FX_11,
+    FX_12,
+    FX_13,
+    FX_14,
+    FX_15,
+    FX_16,
+    FX_17,
+    FX_18,
+    FX_19,
+    FX_20,
+    FX_21,
+    FX_22,
+    FX_23,
+    FX_24,
+    FX_25,
+    FX_26,
+    FX_27,
+    FX_28,
+    FX_29,
+    FX_30,
+    FX_31,
+    FX_32,
+    FX_33,
+    FX_34,
+    FX_35,
+    FX_36,
+    FX_37,
+    FX_38,
+    FX_39,
+    FX_40,
+    FX_41,
+    FX_42,
+    FX_43,
+    FX_44,
+    FX_45,
+    FX_46,
+    FX_47,
+    FX_48,
+    FX_49,
+    FX_50,
+    FX_51,
+    FX_52,
+    FX_53,
+    FX_54,
+    FX_55,
+    FX_56,
+    kFXMax
+};
+
+class CFX {
+public:
+    void sub_73FB0(int);
+    void sub_73FFC(int);
+    spritetype * fxSpawn(FX_ID, int, int, int, int, unsigned int);
+    void fxProcess(void);
+};
+
+void fxSpawnBlood(spritetype *pSprite, int a2);
+void sub_746D4(spritetype *pSprite, int a2);
+void sub_74818(spritetype *pSprite, int z, int a3, int a4);
+void sub_74A18(spritetype *pSprite, int z, int a3, int a4);
+
+extern CFX gFX;
diff --git a/source/blood/src/gamedefs.h b/source/blood/src/gamedefs.h
new file mode 100644
index 000000000..440531f17
--- /dev/null
+++ b/source/blood/src/gamedefs.h
@@ -0,0 +1,72 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+//****************************************************************************
+//
+// gamedefs.h
+//
+// common defines between the game and the setup program
+//
+//****************************************************************************
+
+#ifndef gamedefs_public_h_
+#define gamedefs_public_h_
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// config file name
+#define SETUPFILENAME APPBASENAME ".cfg"
+
+// KEEPINSYNC mact/include/_control.h, build/src/sdlayer.cpp
+#define MAXJOYBUTTONS 32
+#define MAXJOYBUTTONSANDHATS (MAXJOYBUTTONS+4)
+
+// KEEPINSYNC mact/include/_control.h, build/src/sdlayer.cpp
+#define MAXMOUSEAXES 2
+#define MAXMOUSEDIGITAL (MAXMOUSEAXES*2)
+
+// KEEPINSYNC mact/include/_control.h, build/src/sdlayer.cpp
+#define MAXJOYAXES 9
+#define MAXJOYDIGITAL (MAXJOYAXES*2)
+
+// default mouse scale
+#define DEFAULTMOUSEANALOGUESCALE           65536
+
+// default joystick settings
+
+#if defined(GEKKO)
+#define DEFAULTJOYSTICKANALOGUESCALE        16384
+#define DEFAULTJOYSTICKANALOGUEDEAD         1000
+#define DEFAULTJOYSTICKANALOGUESATURATE     9500
+#else
+#define DEFAULTJOYSTICKANALOGUESCALE        65536
+#define DEFAULTJOYSTICKANALOGUEDEAD         1000
+#define DEFAULTJOYSTICKANALOGUESATURATE     9500
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+
diff --git a/source/blood/src/gamemenu.cpp b/source/blood/src/gamemenu.cpp
new file mode 100644
index 000000000..e66c371d0
--- /dev/null
+++ b/source/blood/src/gamemenu.cpp
@@ -0,0 +1,3003 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#include "mouse.h"
+#include "common_game.h"
+#include "blood.h"
+#include "config.h"
+#include "gamemenu.h"
+#include "globals.h"
+#include "inifile.h"
+#include "levels.h"
+#include "menu.h"
+#include "qav.h"
+#include "resource.h"
+#include "view.h"
+
+CMenuTextMgr gMenuTextMgr;
+CGameMenuMgr gGameMenuMgr;
+
+extern CGameMenuItemPicCycle itemSorryPicCycle;
+extern CGameMenuItemQAV itemBloodQAV;
+
+CMenuTextMgr::CMenuTextMgr()
+{
+    at0 = -1;
+}
+
+static char buffer[21][45];
+
+void CMenuTextMgr::DrawText(const char *pString, int nFont, int x, int y, int nShade, int nPalette, bool shadow )
+{
+    viewDrawText(nFont, pString, x, y, nShade, nPalette, 0, shadow);
+}
+
+void CMenuTextMgr::GetFontInfo(int nFont, const char *pString, int *pXSize, int *pYSize)
+{
+    if (nFont < 0 || nFont >= 5)
+        return;
+    viewGetFontInfo(nFont, pString, pXSize, pYSize);
+}
+
+bool CGameMenuMgr::m_bInitialized = false;
+bool CGameMenuMgr::m_bActive = false;
+bool CGameMenuMgr::m_bFirstPush = true;
+
+CGameMenuMgr::CGameMenuMgr()
+{
+    dassert(!m_bInitialized);
+    m_bInitialized = true;
+    Clear();
+}
+
+CGameMenuMgr::~CGameMenuMgr()
+{
+    m_bInitialized = false;
+    Clear();
+}
+
+void CGameMenuMgr::InitializeMenu(void)
+{
+    if (pActiveMenu)
+    {
+        CGameMenuEvent event;
+        event.at0 = kMenuEventInit;
+        event.at2 = 0;
+        pActiveMenu->Event(event);
+    }
+}
+
+void CGameMenuMgr::DeInitializeMenu(void)
+{
+    if (pActiveMenu)
+    {
+        CGameMenuEvent event;
+        event.at0 = kMenuEventDeInit;
+        event.at2 = 0;
+        pActiveMenu->Event(event);
+    }
+}
+
+bool CGameMenuMgr::Push(CGameMenu *pMenu, int nItem)
+{
+    if (nMenuPointer == 0)
+    {
+        mouseReadAbs(&m_prevmousepos, &g_mouseAbs);
+        m_mouselastactivity = -M_MOUSETIMEOUT;
+        m_mousewake_watchpoint = 0;
+        mouseLockToWindow(0);
+        if (m_bFirstPush)
+            m_bFirstPush = false;
+        else
+            mouseMoveToCenter();
+    }
+    dassert(pMenu != NULL);
+    if (nMenuPointer == 8)
+        return false;
+    pActiveMenu = pMenuStack[nMenuPointer] = pMenu;
+    nMenuPointer++;
+    if (nItem >= 0)
+        pMenu->SetFocusItem(nItem);
+    m_bActive = true;
+    gInputMode = INPUT_MODE_1;
+    InitializeMenu();
+    m_menuchange_watchpoint = 1;
+    m_mousecaught = 1;
+    return true;
+}
+
+void CGameMenuMgr::Pop(void)
+{
+    if (nMenuPointer > 0)
+    {
+        DeInitializeMenu();
+        nMenuPointer--;
+        if (nMenuPointer == 0)
+            Deactivate();
+        else
+            pActiveMenu = pMenuStack[nMenuPointer-1];
+
+        m_menuchange_watchpoint = 1;
+    }
+    m_mousecaught = 1;
+}
+
+void CGameMenuMgr::PostPop(void)
+{
+    m_postPop = true;
+}
+
+void CGameMenuMgr::Draw(void)
+{
+    if (pActiveMenu)
+    {
+        pActiveMenu->Draw();
+        viewUpdatePages();
+    }
+
+    if (m_postPop)
+    {
+        Pop();
+        m_postPop = false;
+    }
+
+    int32_t mousestatus = mouseReadAbs(&m_mousepos, &g_mouseAbs);
+    if (mousestatus && g_mouseClickState == MOUSE_PRESSED)
+        m_mousedownpos = m_mousepos;
+
+    int16_t mousetile = 1043; // red arrow
+    if (tilesiz[mousetile].x > 0 && mousestatus)
+    {
+        if (!MOUSEACTIVECONDITION)
+            m_mousewake_watchpoint = 1;
+
+        if (MOUSEACTIVECONDITIONAL(mouseAdvanceClickState()) || m_mousepos.x != m_prevmousepos.x || m_mousepos.y != m_prevmousepos.y)
+        {
+            m_prevmousepos = m_mousepos;
+            m_mouselastactivity = totalclock;
+        }
+        else
+            m_mousewake_watchpoint = 0;
+
+        m_mousecaught = 0;
+    }
+    else
+    {
+        m_mouselastactivity = -M_MOUSETIMEOUT;
+
+        m_mousewake_watchpoint = 0;
+    }
+
+    // Display the mouse cursor, except on touch devices.
+    if (MOUSEACTIVECONDITION && !m_bFirstPush)
+    {
+        vec2_t cursorpos = { m_mousepos.x + (7 << 16), m_mousepos.y + (6 << 16) };
+
+        if ((unsigned) mousetile < MAXTILES)
+        {
+            int32_t scale = 65536;
+            int16_t rotate = 768;
+            uint32_t stat = 2|4|8;
+            int8_t alpha = MOUSEALPHA; //CURSORALPHA;
+            rotatesprite_fs_alpha(cursorpos.x, cursorpos.y, scale, rotate, mousetile, 0, 0, stat, alpha);
+        }
+    }
+    else
+        g_mouseClickState = MOUSE_IDLE;
+}
+
+void CGameMenuMgr::Clear(void)
+{
+    pActiveMenu = NULL;
+    memset(pMenuStack, 0, sizeof(pMenuStack));
+    nMenuPointer = 0;
+    m_postPop = false;
+}
+
+void CGameMenuMgr::Process(void)
+{
+    if (!pActiveMenu)
+        return;
+
+    if (m_menuchange_watchpoint > 0)
+        m_menuchange_watchpoint++;
+
+    CGameMenuEvent event;
+    event.at0 = 0;
+    event.at2 = 0;
+    char key;
+    if (!pActiveMenu->MouseEvent(event) && (key = keyGetScan()) != 0 )
+    {
+        keyFlushScans();
+        keyFlushChars();
+        event.at2 = key;
+        switch (key)
+        {
+        case sc_Escape:
+            event.at0 = kMenuEventEscape;
+            break;
+        case sc_Tab:
+            if (keystatus[sc_LeftShift] || keystatus[sc_RightShift])
+                event.at0 = kMenuEventUp;
+            else
+                event.at0 = kMenuEventDown;
+            break;
+        case sc_UpArrow:
+        case sc_kpad_8:
+            event.at0 = kMenuEventUp;
+            gGameMenuMgr.m_mouselastactivity = -M_MOUSETIMEOUT;
+            break;
+        case sc_DownArrow:
+        case sc_kpad_2:
+            event.at0 = kMenuEventDown;
+            gGameMenuMgr.m_mouselastactivity = -M_MOUSETIMEOUT;
+            break;
+        case sc_Enter:
+        case sc_kpad_Enter:
+            event.at0 = kMenuEventEnter;
+            break;
+        case sc_Space:
+            event.at0 = kMenuEventSpace;
+            break;
+        case sc_LeftArrow:
+        case sc_kpad_4:
+            event.at0 = kMenuEventLeft;
+            break;
+        case sc_RightArrow:
+        case sc_kpad_6:
+            event.at0 = kMenuEventRight;
+            break;
+        case sc_Delete:
+        case sc_kpad_Period:
+            event.at0 = kMenuEventDelete;
+            break;
+        case sc_BackSpace:
+            event.at0 = kMenuEventBackSpace;
+            break;
+        default:
+            event.at0 = kMenuEventKey;
+            break;
+        }
+    }
+    if (pActiveMenu->Event(event))
+        Pop();
+
+    if (m_menuchange_watchpoint >= 3)
+        m_menuchange_watchpoint = 0;
+}
+
+void CGameMenuMgr::Deactivate(void)
+{
+    Clear();
+    keyFlushScans();
+    keyFlushChars();
+    m_bActive = false;
+
+    mouseLockToWindow(1);
+    gInputMode = INPUT_MODE_0;
+}
+
+bool CGameMenuMgr::MouseOutsideBounds(vec2_t const * const pos, const int32_t x, const int32_t y, const int32_t width, const int32_t height)
+{
+    return pos->x < x || pos->x >= x + width || pos->y < y || pos->y >= y + height;
+}
+
+CGameMenu::CGameMenu()
+{
+    m_nItems = 0;
+    m_nFocus = at8 = -1;
+    atc = 0;
+}
+
+CGameMenu::CGameMenu(int unk)
+{
+    m_nItems = 0;
+    m_nFocus = at8 = -1;
+    atc = unk;
+}
+
+CGameMenu::~CGameMenu()
+{
+    if (!atc)
+        return;
+    for (int i = 0; i < m_nItems; i++)
+    {
+        if (pItemList[i] != &itemBloodQAV && pItemList[i] != &itemSorryPicCycle)
+            delete pItemList[i];
+    }
+}
+
+void CGameMenu::InitializeItems(CGameMenuEvent &event)
+{
+    for (int i = 0; i < m_nItems; i++)
+    {
+        pItemList[i]->Event(event);
+    }
+}
+
+void CGameMenu::Draw(void)
+{
+    for (int i = 0; i < m_nItems; i++)
+    {
+        if (pItemList[i]->pPreDrawCallback)
+            pItemList[i]->pPreDrawCallback(pItemList[i]);
+        if (i == m_nFocus || (i != m_nFocus && !pItemList[i]->bNoDraw))
+            pItemList[i]->Draw();
+    }
+}
+
+bool CGameMenu::Event(CGameMenuEvent &event)
+{
+    if (m_nItems <= 0)
+        return true;
+    switch (event.at0)
+    {
+    case kMenuEventInit:
+    case kMenuEventDeInit:
+        if (at8 >= 0)
+            m_nFocus = at8;
+        InitializeItems(event);
+        return false;
+    }
+    if (m_nFocus < 0)
+        return true;
+    return pItemList[m_nFocus]->Event(event);
+}
+
+void CGameMenu::Add(CGameMenuItem *pItem, bool active)
+{
+    dassert(pItem != NULL);
+    dassert(m_nItems < kMaxGameMenuItems);
+    pItemList[m_nItems] = pItem;
+    pItem->pMenu = this;
+    if (active)
+        m_nFocus = at8 = m_nItems;
+    m_nItems++;
+}
+
+void CGameMenu::SetFocusItem(int nItem)
+{
+    dassert(nItem >= 0 && nItem < m_nItems && nItem < kMaxGameMenuItems);
+    if (CanSelectItem(nItem))
+        m_nFocus = at8 = nItem;
+}
+
+void CGameMenu::SetFocusItem(CGameMenuItem *pItem)
+{
+    for (int i = 0; i < m_nItems; i++)
+        if (pItemList[i] == pItem)
+        {
+            SetFocusItem(i);
+            break;
+        }
+}
+
+bool CGameMenu::CanSelectItem(int nItem)
+{
+    dassert(nItem >= 0 && nItem < m_nItems && nItem < kMaxGameMenuItems);
+    return pItemList[nItem]->bCanSelect && pItemList[nItem]->bEnable;
+}
+
+void CGameMenu::FocusPrevItem(void)
+{
+    dassert(m_nFocus >= -1 && m_nFocus < m_nItems && m_nFocus < kMaxGameMenuItems);
+    int t = m_nFocus;
+    do
+    {
+        m_nFocus--;
+        if (m_nFocus < 0)
+            m_nFocus += m_nItems;
+        if (CanSelectItem(m_nFocus))
+            break;
+    } while(t != m_nFocus);
+}
+
+void CGameMenu::FocusNextItem(void)
+{
+    dassert(m_nFocus >= -1 && m_nFocus < m_nItems && m_nFocus < kMaxGameMenuItems);
+    int t = m_nFocus;
+    do
+    {
+        m_nFocus++;
+        if (m_nFocus >= m_nItems)
+            m_nFocus = 0;
+        if (CanSelectItem(m_nFocus))
+            break;
+    } while(t != m_nFocus);
+}
+
+bool CGameMenu::IsFocusItem(CGameMenuItem *pItem)
+{
+    if (m_nFocus < 0)
+        return false;
+    dassert(m_nFocus >= 0 && m_nFocus < m_nItems && m_nFocus < kMaxGameMenuItems);
+    return pItemList[m_nFocus] == pItem;
+}
+
+bool CGameMenu::MouseEvent(CGameMenuEvent &event)
+{
+    if (m_nItems <= 0 || m_nFocus < 0)
+        return true;
+    return pItemList[m_nFocus]->MouseEvent(event);
+}
+
+CGameMenuItem::CGameMenuItem()
+{
+    m_pzText = NULL;
+    m_nX = m_nY = m_nWidth = 0;
+    bCanSelect = 1;
+    bEnable = 1;
+    m_nFont = -1;
+    pMenu = NULL;
+    bNoDraw = 0;
+    pPreDrawCallback = NULL;
+}
+
+CGameMenuItem::~CGameMenuItem()
+{
+}
+
+bool CGameMenuItem::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEscape:
+        return true;
+    case kMenuEventUp:
+        pMenu->FocusPrevItem();
+        break;
+    case kMenuEventDown:
+        pMenu->FocusNextItem();
+        break;
+    }
+    return false;
+}
+
+bool CGameMenuItem::MouseEvent(CGameMenuEvent &event)
+{
+    event.at0 = kMenuEventNone;
+    if (MOUSEINACTIVECONDITIONAL(MOUSE_GetButtons()&LEFT_MOUSE))
+    {
+        event.at0 = kMenuEventEnter;
+        MOUSE_ClearButton(LEFT_MOUSE);
+    }
+    else if (MOUSE_GetButtons()&RIGHT_MOUSE)
+    {
+        event.at0 = kMenuEventEscape;
+        MOUSE_ClearButton(RIGHT_MOUSE);
+    }
+#if 0
+    else if (MOUSEINACTIVECONDITIONAL((MOUSE_GetButtons()&LEFT_MOUSE) && (MOUSE_GetButtons()&WHEELUP_MOUSE)))
+    {
+        MOUSE_ClearButton(WHEELUP_MOUSE);
+        event.bAutoAim = kMenuEventScrollLeft;
+    }
+    else if (MOUSEINACTIVECONDITIONAL((MOUSE_GetButtons()&LEFT_MOUSE) && (MOUSE_GetButtons()&WHEELDOWN_MOUSE)))
+    {
+        MOUSE_ClearButton(WHEELDOWN_MOUSE);
+        event.bAutoAim = kMenuEventScrollRight;
+    }
+#endif
+    else if (MOUSE_GetButtons()&WHEELUP_MOUSE)
+    {
+        MOUSE_ClearButton(WHEELUP_MOUSE);
+        event.at0 = kMenuEventUp;
+    }
+    else if (MOUSE_GetButtons()&WHEELDOWN_MOUSE)
+    {
+        MOUSE_ClearButton(WHEELDOWN_MOUSE);
+        event.at0 = kMenuEventDown;
+    }
+    return event.at0 != kMenuEventNone;
+}
+
+CGameMenuItemText::CGameMenuItemText()
+{
+    m_pzText = 0;
+    bEnable = 0;
+}
+
+CGameMenuItemText::CGameMenuItemText(const char *a1, int a2, int a3, int a4, int a5)
+{
+    m_nWidth = 0;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    at20 = a5;
+    bEnable = 0;
+}
+
+void CGameMenuItemText::Draw(void)
+{
+    if (m_pzText)
+    {
+        int width;
+        int x = m_nX;
+        switch (at20)
+        {
+        case 1:
+            gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+            x = m_nX-width/2;
+            break;
+        case 2:
+            gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+            x = m_nX-width;
+            break;
+        }
+        gMenuTextMgr.DrawText(m_pzText,m_nFont, x, m_nY, -128, 0, false);
+    }
+}
+
+CGameMenuItemTitle::CGameMenuItemTitle()
+{
+    m_pzText = 0;
+    bEnable = 0;
+}
+
+CGameMenuItemTitle::CGameMenuItemTitle(const char *a1, int a2, int a3, int a4, int a5)
+{
+    m_nWidth = 0;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    at20 = a5;
+    bEnable = 0;
+}
+
+void CGameMenuItemTitle::Draw(void)
+{
+    if (m_pzText)
+    {
+        int height;
+        gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+        if (at20 >= 0)
+            rotatesprite(320<<15, m_nY<<16, 65536, 0, at20, -128, 0, 78, 0, 0, xdim-1, ydim-1);
+        viewDrawText(m_nFont, m_pzText, m_nX, m_nY-height/2, -128, 0, 1, false);
+    }
+}
+
+CGameMenuItemZBool::CGameMenuItemZBool()
+{
+    at20 = false;
+    m_pzText = 0;
+    at21 = "On";
+    at25 = "Off";
+}
+
+CGameMenuItemZBool::CGameMenuItemZBool(const char *a1, int a2, int a3, int a4, int a5, bool a6, void(*a7)(CGameMenuItemZBool *), const char *a8, const char *a9)
+{
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    at20 = a6;
+    at29 = a7;
+    if (!a8)
+        at21 = "On";
+    else
+        at21 = a8; 
+    if (!a9)
+        at25 = "Off";
+    else
+        at25 = a9; 
+}
+
+void CGameMenuItemZBool::Draw(void)
+{
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    if (m_pzText)
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, m_nX, m_nY, shade, pal, false);
+    const char *value = at20 ? at21 : at25;
+    int width, height;
+    gMenuTextMgr.GetFontInfo(m_nFont, value, &width, &height);
+    gMenuTextMgr.DrawText(value, m_nFont, m_nWidth-1+m_nX-width, m_nY, shade, pal, false);
+    int mx = m_nX<<16;
+    int my = m_nY<<16;
+    int mw = m_nWidth<<16;
+    int mh = height<<16;
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, mx, my, mw, mh))
+        {
+            pMenu->SetFocusItem(this);
+
+            CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+            gGameMenuMgr.m_mousecaught = 1;
+
+            if (Event(event))
+                gGameMenuMgr.PostPop();
+        }
+    }
+}
+
+bool CGameMenuItemZBool::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEnter:
+    case kMenuEventSpace:
+        at20 = !at20;
+        if (at29)
+            at29(this);
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemChain::CGameMenuItemChain()
+{
+    m_pzText = NULL;
+    at24 = NULL;
+    at28 = -1;
+    at2c = NULL;
+    at30 = 0;
+}
+
+CGameMenuItemChain::CGameMenuItemChain(const char *a1, int a2, int a3, int a4, int a5, int a6, CGameMenu *a7, int a8, void(*a9)(CGameMenuItemChain *), int a10)
+{
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    at20 = a6;
+    at24 = a7;
+    at28 = a8;
+    at2c = a9;
+    at30 = a10;
+}
+
+void CGameMenuItemChain::Draw(void)
+{
+    if (!m_pzText) return;
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int width, height;
+    int x = m_nX;
+    int y = m_nY;
+    gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, &height);
+    switch (at20)
+    {
+    case 1:
+        x = m_nX+m_nWidth/2-width/2;
+        break;
+    case 2:
+        x = m_nX+m_nWidth-1-width;
+        break;
+    case 0:
+    default:
+        break;
+    }
+    gMenuTextMgr.DrawText(m_pzText, m_nFont, x, m_nY, shade, pal, true);
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, x<<16, y<<16, width<<16, height<<16)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, x<<16, y<<16, width<<16, height<<16)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, x<<16, y<<16, width<<16, height<<16))
+        {
+            pMenu->SetFocusItem(this);
+
+            CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+            gGameMenuMgr.m_mousecaught = 1;
+
+            if (Event(event))
+                gGameMenuMgr.PostPop();
+        }
+    }
+}
+
+bool CGameMenuItemChain::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEnter:
+        if (at2c)
+            at2c(this);
+        if (at24)
+            gGameMenuMgr.Push(at24, at28);
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItem7EA1C::CGameMenuItem7EA1C()
+{
+    m_pzText = NULL;
+    at24 = NULL;
+    at28 = -1;
+    at2c = NULL;
+    at30 = 0;
+    at34 = NULL;
+    at38[0] = 0;
+    at48[0] = 0;
+}
+
+CGameMenuItem7EA1C::CGameMenuItem7EA1C(const char *a1, int a2, int a3, int a4, int a5, const char *a6, const char *a7, int a8, int a9, void(*a10)(CGameMenuItem7EA1C *), int a11)
+{
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    at20 = a8;
+    at28 = a9;
+    at2c = a10;
+    at30 = a11;
+    strncpy(at38, a6, 15);
+    strncpy(at48, a7, 15);
+}
+
+void CGameMenuItem7EA1C::Draw(void)
+{
+    if (!m_pzText) return;
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int width;
+    int x = m_nX;
+    switch (at20)
+    {
+    case 1:
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+        x = m_nX+m_nWidth/2-width/2;
+        break;
+    case 2:
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+        x = m_nX+m_nWidth-1-width;
+        break;
+    case 0:
+    default:
+        break;
+    }
+    gMenuTextMgr.DrawText(m_pzText, m_nFont, x, m_nY, shade, pal, true);
+}
+
+void CGameMenuItem7EA1C::Setup(void)
+{
+    if (!at34 || !at24)
+        return;
+    if (!at34->SectionExists(at48))
+        return;
+    const char *title = at34->GetKeyString(at48, "Title", at48);
+    at24->Add(new CGameMenuItemTitle(title, 1, 160, 20, 2038), false);
+    at24->Add(&itemSorryPicCycle, true);
+    int y = 40;
+    for (int i = 0; i < 21; i++)
+    {
+        sprintf(buffer[i], "Line%d", i+1);
+        if (!at34->KeyExists(at48, buffer[i]))
+            break;
+        const char *line = at34->GetKeyString(at48, buffer[i], NULL);
+        if (line)
+        {
+            if (*line == 0)
+            {
+                y += 10;
+                continue;
+            }
+            at24->Add(new CGameMenuItemText(line, 1, 160, y, 1), false);
+            y += 20;
+        }
+    }
+    at24->Add(&itemBloodQAV, false);
+}
+
+bool CGameMenuItem7EA1C::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEnter:
+    {
+        if (at2c)
+            at2c(this);
+        if (at24)
+            delete at24;
+        at24 = new CGameMenu(1);
+        DICTNODE *pRes = gGuiRes.Lookup(at38, "MNU");
+        if (pRes)
+        {
+            at34 = new IniFile(gGuiRes.Load(pRes));
+            Setup();
+        }
+        if (at24)
+            gGameMenuMgr.Push(at24, at28);
+        return false;
+    }
+    case kMenuEventDeInit:
+        if (at34)
+        {
+            delete at34;
+            at34 = NULL;
+        }
+        if (at24)
+        {
+            delete at24;
+            at24 = NULL;
+        }
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItem7EE34::CGameMenuItem7EE34()
+{
+    m_pzText = NULL;
+    at28 = NULL;
+    at20 = -1;
+    at2c = NULL;
+}
+
+CGameMenuItem7EE34::CGameMenuItem7EE34(const char *a1, int a2, int a3, int a4, int a5, int a6)
+{
+    m_pzText = NULL;
+    at28 = NULL;
+    at20 = -1;
+    at2c = NULL;
+    m_nFont = a2;
+    m_nX = a3;
+    m_pzText = a1;
+    m_nY = a4;
+    m_nWidth = a5;
+    at24 = a6;
+}
+
+void CGameMenuItem7EE34::Draw(void)
+{
+    if (!m_pzText) return;
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int width;
+    int x = m_nX;
+    switch (at24)
+    {
+    case 1:
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+        x = m_nX+m_nWidth/2-width/2;
+        break;
+    case 2:
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+        x = m_nX+m_nWidth-1-width;
+        break;
+    case 0:
+    default:
+        break;
+    }
+    gMenuTextMgr.DrawText(m_pzText, m_nFont, x, m_nY, shade, pal, true);
+}
+
+extern void SetVideoModeOld(CGameMenuItemChain *pItem);
+
+void CGameMenuItem7EE34::Setup(void)
+{
+    if (!at28)
+        return;
+    at28->Add(new CGameMenuItemTitle("Video Mode", 1, 160, 20, 2038), false);
+    if (!at2c)
+    {
+        at2c = new CGameMenu(1);
+        at2c->Add(new CGameMenuItemTitle(" Mode Change ", 1, 160, 20, 2038), false);
+        at2c->Add(&itemSorryPicCycle, true);
+        CGameMenuItem *pItem1 = new CGameMenuItemText("VIDEO MODE WAS SET", 1, 160, 90, 1);
+        CGameMenuItem *pItem2 = new CGameMenuItemText("NOT ALL MODES Work correctly", 1, 160, 110, 1);
+        CGameMenuItem *pItem3 = new CGameMenuItemText("Press ESC to exit", 3, 160, 140, 1);
+        at2c->Add(pItem1, false);
+        pItem1->bEnable = 0;
+        at2c->Add(pItem2, false);
+        pItem2->bEnable = 0;
+        at2c->Add(pItem3, true);
+        pItem3->bEnable = 1;
+        at2c->Add(&itemBloodQAV, false);
+    }
+    sprintf(buffer[0], "640 x 480 (default)");
+    int y = 40;
+    at28->Add(new CGameMenuItemChain(buffer[0], 3, 0, y, 320, 1, at2c, -1, SetVideoModeOld, validmodecnt), true);
+    y += 20;
+    for (int i = 0; i < validmodecnt && i < 20; i++)
+    {
+        sprintf(buffer[i+1], "%d x %d", validmode[i].xdim, validmode[i].ydim);
+        at28->Add(new CGameMenuItemChain(buffer[i+1], 3, 0, y, 320, 1, at2c, -1, SetVideoModeOld, i), false);
+        if (validmodecnt > 10)
+            y += 7;
+        else
+            y += 15;
+    }
+    at28->Add(&itemBloodQAV, false);
+}
+
+bool CGameMenuItem7EE34::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEnter:
+        if (at28)
+            delete at28;
+        at28 = new CGameMenu(1);
+        Setup();
+        if (at28)
+            gGameMenuMgr.Push(at28, at20);
+        return false;
+    case kMenuEventDeInit:
+        if (at28)
+        {
+            delete at28;
+            at28 = 0;
+        }
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemChain7F2F0::CGameMenuItemChain7F2F0()
+{
+    at34 = -1;
+}
+
+CGameMenuItemChain7F2F0::CGameMenuItemChain7F2F0(char *a1, int a2, int a3, int a4, int a5, int a6, CGameMenu *a7, int a8, void(*a9)(CGameMenuItemChain *), int a10) :
+    CGameMenuItemChain(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
+{
+    at34 = a10;
+}
+
+bool CGameMenuItemChain7F2F0::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEnter:
+        if (at34 > -1)
+            gGameOptions.nEpisode = at34;
+        return CGameMenuItemChain::Event(event);
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemBitmap::CGameMenuItemBitmap()
+{
+    m_pzText = NULL;
+}
+
+CGameMenuItemBitmap::CGameMenuItemBitmap(const char *a1, int a2, int a3, int a4, int a5)
+{
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    at20 = a5;
+}
+
+void CGameMenuItemBitmap::Draw(void)
+{
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (bEnable && pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int x = m_nX;
+    int y = m_nY;
+    if (m_pzText)
+    {
+        int height;
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, x, y, shade, pal, false);
+        gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+        y += height + 2;
+    }
+    rotatesprite(x<<15,y<<15, 65536, 0, at20, 0, 0, 82, 0, 0, xdim-1,ydim-1);
+}
+
+bool CGameMenuItemBitmap::Event(CGameMenuEvent &event)
+{
+    if (bEnable && pMenu->IsFocusItem(this))
+        pMenu->FocusNextItem();
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemBitmapLS::CGameMenuItemBitmapLS()
+{
+    m_pzText = NULL;
+}
+
+CGameMenuItemBitmapLS::CGameMenuItemBitmapLS(const char *a1, int a2, int a3, int a4, int a5)
+{
+    at24 = -1;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    at28 = a5;
+}
+
+void CGameMenuItemBitmapLS::Draw(void)
+{
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (bEnable && pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int x = m_nX;
+    int y = m_nY;
+    if (m_pzText)
+    {
+        int height;
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, x, y, shade, pal, false);
+        gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+        y += height + 2;
+    }
+    char stat;
+    int16_t ang;
+    int picnum;
+    if (at24 == -1)
+    {
+        stat = 66;
+        ang = 0;
+        picnum = at28;
+    }
+    else
+    {
+        ang = 512;
+        stat = 70;
+        picnum = at24;
+    }
+    rotatesprite(200<<15,215<<15,32768, ang, picnum, 0, 0, stat, 0, 0, xdim-1, ydim-1);
+}
+
+bool CGameMenuItemBitmapLS::Event(CGameMenuEvent &event)
+{
+    if (bEnable && pMenu->IsFocusItem(this))
+        pMenu->FocusNextItem();
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemKeyList::CGameMenuItemKeyList()
+{
+    m_pzText = NULL;
+    m_nFont = 3;
+    m_nX = 0;
+    m_nY = 0;
+    nRows = 0;
+    nTopDelta = 0;
+    nFocus = 0;
+    nGameFuncs = 0;
+    bScan = false;
+}
+
+CGameMenuItemKeyList::CGameMenuItemKeyList(const char *a1, int a2, int a3, int a4, int a5, int a6, int a7, void(*a8)(CGameMenuItemKeyList *))
+{
+    nTopDelta = 0;
+    nFocus = 0;
+    bScan = false;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    nRows = a6;
+    pCallback = a8;
+    nGameFuncs = a7;
+}
+
+void CGameMenuItemKeyList::Scan(void)
+{
+    KB_FlushKeyboardQueue();
+    KB_FlushKeyboardQueueScans();
+    KB_ClearKeysDown();
+    KB_LastScan = 0;
+    bScan = true;
+}
+
+extern uint8_t KeyboardKeys[NUMGAMEFUNCTIONS][2];
+void CGameMenuItemKeyList::Draw(void)
+{
+    char buffer[40], buffer2[40];
+    int width, height;
+    int shade;
+    gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+    int y = m_nY;
+    int k = nFocus - nTopDelta;
+    int nNewFocus = nFocus;
+    bool bClick = false;
+    for (int i = 0; i < nRows; i++, y += height, k++)
+    {
+        char key1, key2;
+        key1 = KeyboardKeys[k][0];
+        key2 = KeyboardKeys[k][1];
+        const char *sKey1 = key1 == sc_Tilde ? "Tilde" : KB_ScanCodeToString(key1);
+        const char *sKey2 = key2 == sc_Tilde ? "Tilde" : KB_ScanCodeToString(key2);
+        sprintf(buffer, "%s", CONFIG_FunctionNumToName(k));
+        if (key2 == 0 || key2 == 0xff)
+        {
+            if (key1 == 0 || key1 == 0xff)
+                sprintf(buffer2, "????");
+            else
+                sprintf(buffer2, "%s", sKey1);
+        }
+        else
+            sprintf(buffer2, "%s or %s", sKey1, sKey2);
+        
+        if (k == nFocus)
+        {
+            shade = 32;
+            if (pMenu->IsFocusItem(this))
+                shade = 32-(totalclock&63);
+            viewDrawText(3, buffer, m_nX, y, shade, 0, 0, false);
+            const char *sVal;
+            if (bScan && (gGameClock & 32))
+                sVal = "____";
+            else
+                sVal = buffer2;
+            gMenuTextMgr.GetFontInfo(m_nFont, sVal, &width, 0);
+            viewDrawText(m_nFont, sVal, m_nX+m_nWidth-1-width, y, shade, 0, 0, false);
+        }
+        else
+        {
+            viewDrawText(3, buffer, m_nX, y, 24, 0, 0, false);
+            gMenuTextMgr.GetFontInfo(m_nFont, buffer2, &width, 0);
+            viewDrawText(m_nFont, buffer2, m_nX+m_nWidth-1-width, y, 24, 0, 0, false);
+        }
+        int mx = m_nX<<16;
+        int my = y<<16;
+        int mw = m_nWidth<<16;
+        int mh = height<<16;
+        if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+        {
+            if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+            {
+                nNewFocus = k;
+            }
+
+            if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, mx, my, mw, mh))
+            {
+                nNewFocus = k;
+                bClick = true;
+            }
+        }
+    }
+    nTopDelta += nNewFocus-nFocus;
+    nFocus = nNewFocus;
+    if (bClick)
+    {
+        CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+        gGameMenuMgr.m_mousecaught = 1;
+
+        if (Event(event))
+            gGameMenuMgr.PostPop();
+    }
+}
+
+bool CGameMenuItemKeyList::Event(CGameMenuEvent &event)
+{
+    if (bScan)
+    {
+        if (KB_LastScan && KB_LastScan != sc_Pause)
+        {
+            if (KB_KeyWaiting())
+                KB_GetCh();
+            char key1, key2;
+            extern uint8_t KeyboardKeys[NUMGAMEFUNCTIONS][2];
+            key1 = KeyboardKeys[nFocus][0];
+            key2 = KeyboardKeys[nFocus][1];
+            if (key1 > 0 && key2 != KB_LastScan)
+                key2 = key1;
+            key1 = KB_LastScan;
+            if (key1 == key2)
+                key2 = 0;
+            uint8_t oldKey[2];
+            oldKey[0] = KeyboardKeys[nFocus][0];
+            oldKey[1] = KeyboardKeys[nFocus][1];
+            KeyboardKeys[nFocus][0] = key1;
+            KeyboardKeys[nFocus][1] = key2;
+            CONFIG_MapKey(nFocus, key1, oldKey[0], key2, oldKey[1]);
+            KB_FlushKeyboardQueue();
+            KB_FlushKeyboardQueueScans();
+            KB_ClearKeysDown();
+            keyFlushScans();
+            keyFlushChars();
+            bScan = 0;
+        }
+        return false;
+    }
+    switch (event.at0)
+    {
+    case kMenuEventUp:
+        if (event.at2 == sc_Tab || nFocus == 0)
+        {
+            pMenu->FocusPrevItem();
+            return false;
+        }
+        nFocus--;
+        if (nTopDelta > 0)
+            nTopDelta--;
+        return false;
+    case kMenuEventDown:
+        if (event.at2 == sc_Tab || nFocus == nGameFuncs-1)
+        {
+            pMenu->FocusNextItem();
+            return false;
+        }
+        nFocus++;
+        if (nTopDelta+1 < nRows)
+            nTopDelta++;
+        return false;
+    case kMenuEventEnter:
+        if (pCallback)
+            pCallback(this);
+        Scan();
+        return false;
+    case kMenuEventDelete:
+        if (keystatus[sc_LeftControl] || keystatus[sc_RightControl])
+        {
+            uint8_t oldKey[2];
+            oldKey[0] = KeyboardKeys[nFocus][0];
+            oldKey[1] = KeyboardKeys[nFocus][1];
+            KeyboardKeys[nFocus][0] = 0;
+            KeyboardKeys[nFocus][1] = 0;
+            CONFIG_MapKey(nFocus, 0, oldKey[0], 0, oldKey[1]);
+        }
+        return false;
+    case kMenuEventScrollUp:
+        if (nFocus-nTopDelta > 0)
+        {
+            nTopDelta++;
+            if (nTopDelta>0)
+            {
+                nFocus--;
+                nTopDelta--;
+            }
+        }
+        return false;
+    case kMenuEventScrollDown:
+        if (nFocus-nTopDelta+nRows < nGameFuncs)
+        {
+            nTopDelta--;
+            if (nTopDelta+1 < nRows)
+            {
+                nFocus++;
+                nTopDelta++;
+            }
+        }
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+bool CGameMenuItemKeyList::MouseEvent(CGameMenuEvent &event)
+{
+    event.at0 = kMenuEventNone;
+    if (MOUSEACTIVECONDITIONAL(MOUSE_GetButtons()&WHEELUP_MOUSE))
+    {
+        gGameMenuMgr.m_mouselastactivity = totalclock;
+        MOUSE_ClearButton(WHEELUP_MOUSE);
+        event.at0 = kMenuEventScrollUp;
+    }
+    else if (MOUSEACTIVECONDITIONAL(MOUSE_GetButtons()&WHEELDOWN_MOUSE))
+    {
+        gGameMenuMgr.m_mouselastactivity = totalclock;
+        MOUSE_ClearButton(WHEELDOWN_MOUSE);
+        event.at0 = kMenuEventScrollDown;
+    }
+    else
+        return CGameMenuItem::MouseEvent(event);
+    return event.at0 != kMenuEventNone;
+}
+
+CGameMenuItemSlider::CGameMenuItemSlider()
+{
+    m_pzText = NULL;
+    m_nFont = -1;
+    m_nX = 0;
+    m_nY = 0;
+    nValue = 0;
+    nRangeLow = 0;
+    nStep = 0;
+    pCallback = NULL;
+    pValue = NULL;
+    nSliderTile = 2204;
+    nCursorTile = 2028;
+    nShowValue = kMenuSliderNone;
+}
+
+CGameMenuItemSlider::CGameMenuItemSlider(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, int _nValue, int _nRangeLow, int _nRangeHigh, int _nStep, void(*_pCallback)(CGameMenuItemSlider *), int _nSliderTile, int _nCursorTile, int _nShowValue)
+{
+    m_pzText = _pzText;
+    m_nFont = _nFont;
+    m_nX = _nX;
+    m_nY = _nY;
+    m_nWidth = _nWidth;
+    nRangeLow = _nRangeLow;
+    nRangeHigh = _nRangeHigh;
+    nStep = _nStep;
+    nValue = ClipRange(_nValue, nRangeLow, nRangeHigh);
+    pCallback = _pCallback;
+    nSliderTile = 2204;
+    nCursorTile = 2028;
+    if (_nSliderTile >= 0)
+        nSliderTile = _nSliderTile;
+    if (_nCursorTile >= 0)
+        nCursorTile = _nCursorTile;
+    nShowValue = _nShowValue;
+}
+
+CGameMenuItemSlider::CGameMenuItemSlider(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, int *pnValue, int _nRangeLow, int _nRangeHigh, int _nStep, void(*_pCallback)(CGameMenuItemSlider *), int _nSliderTile, int _nCursorTile, int _nShowValue)
+{
+    m_pzText = _pzText;
+    m_nFont = _nFont;
+    m_nX = _nX;
+    m_nY = _nY;
+    m_nWidth = _nWidth;
+    nRangeLow = _nRangeLow;
+    nRangeHigh = _nRangeHigh;
+    nStep = _nStep;
+    dassert(pnValue != NULL);
+    pValue = pnValue;
+    nValue = ClipRange(*pnValue, nRangeLow, nRangeHigh);
+    pCallback = _pCallback;
+    nSliderTile = 2204;
+    nCursorTile = 2028;
+    if (_nSliderTile >= 0)
+        nSliderTile = _nSliderTile;
+    if (_nCursorTile >= 0)
+        nCursorTile = _nCursorTile;
+    nShowValue = _nShowValue;
+}
+
+void CGameMenuItemSlider::Draw(void)
+{
+    char buffer[16];
+    int height;
+    nValue = pValue ? *pValue : nValue;
+    gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+    int shade = bEnable ? 32 : 48;
+    int shade2 = bEnable ? 0 : 16;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    if (m_pzText)
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, m_nX, m_nY, shade, pal, false);
+    int sliderX = m_nX+m_nWidth-1-tilesiz[nSliderTile].x/2;
+    rotatesprite(sliderX<<16, (m_nY+height/2)<<16, 65536, 0, nSliderTile, shade2, pal, 10, 0, 0, xdim-1, ydim-1);
+    int nRange = nRangeHigh - nRangeLow;
+    dassert(nRange > 0);
+    int value = nValue - nRangeLow;
+    int width = tilesiz[nSliderTile].x-8;
+    int cursorX = sliderX + ksgn(nStep)*(value * width / nRange - width / 2);
+    rotatesprite(cursorX<<16, (m_nY+height/2)<<16, 65536, 0, nCursorTile, shade2, pal, 10, 0, 0, xdim-1, ydim-1);
+
+    buffer[0] = 0;
+    switch (nShowValue)
+    {
+    case kMenuSliderNone:
+        break;
+    case kMenuSliderValue:
+        sprintf(buffer, "%i ", nValue);
+        break;
+    case kMenuSliderPercent:
+        sprintf(buffer, "%i%% ", roundscale(value, 100, nRange));
+        break;
+    case kMenuSliderQ16:
+        snprintf(buffer, 16, "%.3f ", nValue/65536.f);
+        break;
+    }
+    int valueWidth;
+    gMenuTextMgr.GetFontInfo(m_nFont, buffer, &valueWidth, NULL);
+    int valueX = m_nX+m_nWidth-1-tilesiz[nSliderTile].x-valueWidth;
+    gMenuTextMgr.DrawText(buffer, m_nFont, valueX, m_nY, 32, 0, false);
+
+    int mx = m_nX;
+    int my = m_nY;
+    int mw = m_nWidth;
+    int mh = height;
+    if (height < tilesiz[nSliderTile].y)
+    {
+        my -= (tilesiz[nSliderTile].y-height)/2;
+        height = tilesiz[nSliderTile].y;
+    }
+    mx <<= 16;
+    my <<= 16;
+    mw <<= 16;
+    mh <<= 16;
+
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && (g_mouseClickState == MOUSE_PRESSED || g_mouseClickState == MOUSE_HELD))
+        {
+            pMenu->SetFocusItem(this);
+
+            int sliderx = m_nX+m_nWidth-1-tilesiz[nSliderTile].x;
+            int sliderwidth = tilesiz[nSliderTile].x;
+            int regionwidth = sliderwidth-8;
+            int regionx = sliderx+(sliderwidth-regionwidth)/2;
+            sliderx <<= 16;
+            sliderwidth <<= 16;
+            regionwidth <<= 16;
+            regionx <<= 16;
+
+            // region between the x-midline of the slidepoint at the extremes slides proportionally
+            if (!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, regionx, my, regionwidth, mh))
+            {
+                int dx = (gGameMenuMgr.m_mousepos.x - (regionx+regionwidth/2))*ksgn(nStep);
+                nValue = nRangeLow + roundscale(dx+regionwidth/2, nRange, regionwidth);
+                nValue = ClipRange(nValue, nRangeLow, nRangeHigh);
+                if (pCallback)
+                    pCallback(this);
+                gGameMenuMgr.m_mousecaught = 1;
+            }
+            // region outside the x-midlines clamps to the extremes
+            else if (!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, sliderx, my, sliderwidth, mh))
+            {
+                if ((gGameMenuMgr.m_mousepos.x-(regionx+regionwidth/2))*ksgn(nStep) > 0)
+                    nValue = nRangeHigh;
+                else
+                    nValue = nRangeLow;
+                if (pCallback)
+                    pCallback(this);
+                gGameMenuMgr.m_mousecaught = 1;
+            }
+        }
+    }
+}
+
+bool CGameMenuItemSlider::Event(CGameMenuEvent &event)
+{
+    nValue = pValue ? *pValue : nValue;
+    switch (event.at0)
+    {
+    case kMenuEventUp:
+        pMenu->FocusPrevItem();
+        return false;
+    case kMenuEventDown:
+        pMenu->FocusNextItem();
+        return false;
+    case kMenuEventLeft:
+        if (nStep > 0)
+            nValue = DecBy(nValue, nStep);
+        else
+            nValue = IncBy(nValue, -nStep);
+        nValue = ClipRange(nValue, nRangeLow, nRangeHigh);
+        if (pCallback)
+            pCallback(this);
+        return false;
+    case kMenuEventRight:
+        if (nStep >= 0)
+            nValue = IncBy(nValue, nStep);
+        else
+            nValue = DecBy(nValue, -nStep);
+        nValue = ClipRange(nValue, nRangeLow, nRangeHigh);
+        if (pCallback)
+            pCallback(this);
+        return false;
+    case kMenuEventEnter:
+        if (pCallback)
+            pCallback(this);
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+bool CGameMenuItemSlider::MouseEvent(CGameMenuEvent &event)
+{
+    event.at0 = kMenuEventNone;
+    if (MOUSEINACTIVECONDITIONAL((MOUSE_GetButtons()&LEFT_MOUSE) && (MOUSE_GetButtons()&WHEELUP_MOUSE)))
+    {
+        MOUSE_ClearButton(WHEELUP_MOUSE);
+        event.at0 = kMenuEventLeft;
+    }
+    else if (MOUSEINACTIVECONDITIONAL((MOUSE_GetButtons()&LEFT_MOUSE) && (MOUSE_GetButtons()&WHEELDOWN_MOUSE)))
+    {
+        MOUSE_ClearButton(WHEELDOWN_MOUSE);
+        event.at0 = kMenuEventRight;
+    }
+    else if (MOUSE_GetButtons()&RIGHT_MOUSE)
+    {
+        MOUSE_ClearButton(RIGHT_MOUSE);
+        event.at0 = kMenuEventEscape;
+    }
+    else if (MOUSE_GetButtons()&WHEELUP_MOUSE)
+    {
+        MOUSE_ClearButton(WHEELUP_MOUSE);
+        MOUSE_ClearButton(LEFT_MOUSE);
+        event.at0 = kMenuEventUp;
+    }
+    else if (MOUSE_GetButtons()&WHEELDOWN_MOUSE)
+    {
+        MOUSE_ClearButton(WHEELDOWN_MOUSE);
+        MOUSE_ClearButton(LEFT_MOUSE);
+        event.at0 = kMenuEventDown;
+    }
+    return event.at0 != kMenuEventNone;
+}
+
+CGameMenuItemSliderFloat::CGameMenuItemSliderFloat()
+{
+    m_pzText = NULL;
+    m_nFont = -1;
+    m_nX = 0;
+    m_nY = 0;
+    fValue = 0;
+    fRangeLow = 0;
+    fStep = 0;
+    pCallback = NULL;
+    pValue = NULL;
+    nSliderTile = 2204;
+    nCursorTile = 2028;
+    nShowValue = kMenuSliderNone;
+}
+
+CGameMenuItemSliderFloat::CGameMenuItemSliderFloat(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, float _fValue, float _fRangeLow, float _fRangeHigh, float _fStep, void(*_pCallback)(CGameMenuItemSliderFloat *), int _nSliderTile, int _nCursorTile, int _nShowValue)
+{
+    m_pzText = _pzText;
+    m_nFont = _nFont;
+    m_nX = _nX;
+    m_nY = _nY;
+    m_nWidth = _nWidth;
+    fRangeLow = _fRangeLow;
+    fRangeHigh = _fRangeHigh;
+    fStep = _fStep;
+    fValue = ClipRangeF(_fValue, fRangeLow, fRangeHigh);
+    pCallback = _pCallback;
+    nSliderTile = 2204;
+    nCursorTile = 2028;
+    if (_nSliderTile >= 0)
+        nSliderTile = _nSliderTile;
+    if (_nCursorTile >= 0)
+        nCursorTile = _nCursorTile;
+    nShowValue = _nShowValue;
+}
+
+CGameMenuItemSliderFloat::CGameMenuItemSliderFloat(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, float *pnValue, float _fRangeLow, float _fRangeHigh, float _fStep, void(*_pCallback)(CGameMenuItemSliderFloat *), int _nSliderTile, int _nCursorTile, int _nShowValue)
+{
+    m_pzText = _pzText;
+    m_nFont = _nFont;
+    m_nX = _nX;
+    m_nY = _nY;
+    m_nWidth = _nWidth;
+    fRangeLow = _fRangeLow;
+    fRangeHigh = _fRangeHigh;
+    fStep = _fStep;
+    dassert(pnValue != NULL);
+    pValue = pnValue;
+    fValue = ClipRangeF(*pnValue, fRangeLow, fRangeHigh);
+    pCallback = _pCallback;
+    nSliderTile = 2204;
+    nCursorTile = 2028;
+    if (_nSliderTile >= 0)
+        nSliderTile = _nSliderTile;
+    if (_nCursorTile >= 0)
+        nCursorTile = _nCursorTile;
+    nShowValue = _nShowValue;
+}
+
+void CGameMenuItemSliderFloat::Draw(void)
+{
+    char buffer[16];
+    int height;
+
+    fValue = pValue ? *pValue : fValue;
+    gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+    int shade = bEnable ? 32 : 48;
+    int shade2 = bEnable ? 0 : 16;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    if (m_pzText)
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, m_nX, m_nY, shade, pal, false);
+    int sliderX = m_nX+m_nWidth-1-tilesiz[nSliderTile].x/2;
+    rotatesprite(sliderX<<16, (m_nY+height/2)<<16, 65536, 0, nSliderTile, shade2, pal, 10, 0, 0, xdim-1, ydim-1);
+    float fRange = fRangeHigh - fRangeLow;
+    dassert(fRange > 0);
+    float value = fValue - fRangeLow;
+    int width = tilesiz[nSliderTile].x-8;
+    int cursorX = sliderX + (int)(ksgnf(fStep)*(value * width / fRange - width / 2));
+    rotatesprite(cursorX<<16, (m_nY+height/2)<<16, 65536, 0, nCursorTile, shade2, pal, 10, 0, 0, xdim-1, ydim-1);
+
+    buffer[0] = 0;
+    switch (nShowValue)
+    {
+    case kMenuSliderNone:
+        break;
+    case kMenuSliderValue:
+        snprintf(buffer, 16, "%.3f ", fValue);
+        break;
+    case kMenuSliderPercent:
+        snprintf(buffer, 16, "%.3f%% ", value*100.f/fRange);
+        break;
+    }
+    int valueWidth;
+    gMenuTextMgr.GetFontInfo(m_nFont, buffer, &valueWidth, NULL);
+    int valueX = m_nX+m_nWidth-1-tilesiz[nSliderTile].x-valueWidth;
+    gMenuTextMgr.DrawText(buffer, m_nFont, valueX, m_nY, 32, 0, false);
+
+    int mx = m_nX;
+    int my = m_nY;
+    int mw = m_nWidth;
+    int mh = height;
+    if (height < tilesiz[nSliderTile].y)
+    {
+        my -= (tilesiz[nSliderTile].y-height)/2;
+        height = tilesiz[nSliderTile].y;
+    }
+    mx <<= 16;
+    my <<= 16;
+    mw <<= 16;
+    mh <<= 16;
+
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && (g_mouseClickState == MOUSE_PRESSED || g_mouseClickState == MOUSE_HELD))
+        {
+            pMenu->SetFocusItem(this);
+
+            int sliderx = m_nX+m_nWidth-1-tilesiz[nSliderTile].x;
+            int sliderwidth = tilesiz[nSliderTile].x;
+            int regionwidth = sliderwidth-8;
+            int regionx = sliderx+(sliderwidth-regionwidth)/2;
+            sliderx <<= 16;
+            sliderwidth <<= 16;
+            regionwidth <<= 16;
+            regionx <<= 16;
+
+            // region between the x-midline of the slidepoint at the extremes slides proportionally
+            if (!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, regionx, my, regionwidth, mh))
+            {
+                int dx = (gGameMenuMgr.m_mousepos.x - (regionx+regionwidth/2))*ksgnf(fStep);
+                fValue = fRangeLow + (dx+regionwidth/2) * fRange / regionwidth;
+                fValue = ClipRangeF(fValue, fRangeLow, fRangeHigh);
+                if (pCallback)
+                    pCallback(this);
+                gGameMenuMgr.m_mousecaught = 1;
+            }
+            // region outside the x-midlines clamps to the extremes
+            else if (!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, sliderx, my, sliderwidth, mh))
+            {
+                if ((gGameMenuMgr.m_mousepos.x-(regionx+regionwidth/2))*ksgnf(fStep) > 0)
+                    fValue = fRangeHigh;
+                else
+                    fValue = fRangeLow;
+                if (pCallback)
+                    pCallback(this);
+                gGameMenuMgr.m_mousecaught = 1;
+            }
+        }
+    }
+}
+
+bool CGameMenuItemSliderFloat::Event(CGameMenuEvent &event)
+{
+    fValue = pValue ? *pValue : fValue;
+    switch (event.at0)
+    {
+    case kMenuEventUp:
+        pMenu->FocusPrevItem();
+        return false;
+    case kMenuEventDown:
+        pMenu->FocusNextItem();
+        return false;
+    case kMenuEventLeft:
+        if (fStep > 0)
+            fValue -= fStep;
+        else
+            fValue += fStep;
+        fValue = ClipRangeF(fValue, fRangeLow, fRangeHigh);
+        if (pCallback)
+            pCallback(this);
+        return false;
+    case kMenuEventRight:
+        if (fStep >= 0)
+            fValue += fStep;
+        else
+            fValue -= fStep;
+        fValue = ClipRangeF(fValue, fRangeLow, fRangeHigh);
+        if (pCallback)
+            pCallback(this);
+        return false;
+    case kMenuEventEnter:
+        if (pCallback)
+            pCallback(this);
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemZEdit::CGameMenuItemZEdit()
+{
+    m_pzText = NULL;
+    m_nFont = -1;
+    m_nX = 0;
+    m_nY = 0;
+    at20 = NULL;
+    at24 = 0;
+    at32 = 0;
+    at2c = 0;
+    at30 = 0;
+    at28 = 0;
+    at31 = 1;
+}
+
+CGameMenuItemZEdit::CGameMenuItemZEdit(const char *a1, int a2, int a3, int a4, int a5, char *a6, int a7, char a8, void(*a9)(CGameMenuItemZEdit *, CGameMenuEvent *), int a10)
+{
+    at30 = 0;
+    at31 = 1;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    at20 = a6;
+    at24 = a7;
+    at32 = a8;
+    at2c = a9;
+    at28 = a10;
+}
+
+void CGameMenuItemZEdit::AddChar(char ch)
+{
+    int i = strlen(at20);
+    if (i + 1 < at24)
+    {
+        at20[i] = ch;
+        at20[i + 1] = 0;
+    }
+}
+
+void CGameMenuItemZEdit::BackChar(void)
+{
+    int i = strlen(at20);
+    if (i > 0)
+        at20[i - 1] = 0;
+}
+
+void CGameMenuItemZEdit::Draw(void)
+{
+    int height, width, textWidth = 0;
+    gMenuTextMgr.GetFontInfo(m_nFont, NULL, &width, &height);
+    if (at20)
+        gMenuTextMgr.GetFontInfo(m_nFont, at20, &textWidth, NULL);
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    if (at30)
+        shade = -128;
+    if (m_pzText)
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, m_nX, m_nY, shade, pal, false);
+    int x = m_nX+m_nWidth-1-textWidth;//(at24+1)*width;
+    if (at20 && *at20)
+    {
+        int width;
+        gMenuTextMgr.GetFontInfo(m_nFont, at20, &width, NULL);
+        int shade2;
+        if (at32)
+        {
+            if (at30)
+                shade2 = -128;
+            else
+                shade2 = shade;
+        }
+        else
+        {
+            if (at30)
+                shade2 = shade;
+            else
+                shade2 = 32;
+        }
+        gMenuTextMgr.DrawText(at20, m_nFont, x, m_nY, shade2, pal, false);
+        x += width;
+    }
+    if (at30 && (gGameClock & 32))
+        gMenuTextMgr.DrawText("_", m_nFont, x, m_nY, shade, 0, false);
+
+    int mx = m_nX<<16;
+    int my = m_nY<<16;
+    int mw = m_nWidth<<16;
+    int mh = height<<16;
+
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, mx, my, mw, mh))
+        {
+            pMenu->SetFocusItem(this);
+
+            CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+            gGameMenuMgr.m_mousecaught = 1;
+
+            if (Event(event))
+                gGameMenuMgr.PostPop();
+        }
+    }
+}
+
+bool CGameMenuItemZEdit::Event(CGameMenuEvent &event)
+{
+    static char buffer[256];
+    // Hack
+    if (event.at2 == sc_kpad_2 || event.at2 == sc_kpad_4 || event.at2 == sc_kpad_6 || event.at2 == sc_kpad_8)
+        event.at0 = kMenuEventKey;
+    switch (event.at0)
+    {
+    case kMenuEventEscape:
+        if (at30)
+        {
+            strncpy(at20, buffer, at24);
+            at20[at24-1] = 0;
+            at30 = 0;
+            return false;
+        }
+        return true;
+    case kMenuEventEnter:
+        if (!at31)
+        {
+            if (at2c)
+                at2c(this, &event);
+            return false;
+        }
+        if (at30)
+        {
+            if (at2c)
+                at2c(this, &event);
+            at30 = 0;
+            return false;
+        }
+        strncpy(buffer, at20, at24);
+        buffer[at24-1] = 0;
+        at30 = 1;
+        return false;
+    case kMenuEventBackSpace:
+        if (at30)
+            BackChar();
+        return false;
+    case kMenuEventKey:
+    case kMenuEventSpace:
+    {
+        char key;
+        if (event.at2 < 128)
+        {
+            if (keystatus[sc_LeftShift] || keystatus[sc_RightShift])
+                key = g_keyAsciiTableShift[event.at2];
+            else
+                key = g_keyAsciiTable[event.at2];
+            if (at30 && (isalnum(key) || ispunct(key) || isspace(key)))
+            {
+                AddChar(key);
+                return false;
+            }
+        }
+        return CGameMenuItem::Event(event);
+    }
+    case kMenuEventUp:
+        if (at30)
+            return false;
+        return CGameMenuItem::Event(event);
+    case kMenuEventDown:
+        if (at30)
+            return false;
+        return CGameMenuItem::Event(event);
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemZEditBitmap::CGameMenuItemZEditBitmap()
+{
+    m_pzText = NULL;
+    m_nFont = -1;
+    m_nX = 0;
+    m_nY = 0;
+    at20 = NULL;
+    at24 = 0;
+    at36 = 0;
+    at30 = NULL;
+    at2c = NULL;
+    at34 = 0;
+    at28 = 0;
+    at37 = 0;
+    at35 = 1;
+}
+
+CGameMenuItemZEditBitmap::CGameMenuItemZEditBitmap(char *a1, int a2, int a3, int a4, int a5, char *a6, int a7, char a8, void(*a9)(CGameMenuItemZEditBitmap *, CGameMenuEvent *), int a10)
+{
+    at2c = NULL;
+    at34 = 0;
+    at35 = 1;
+    at37 = 0;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    at20 = a6;
+    at24 = a7;
+    at36 = a8;
+    at30 = a9;
+    at28 = a10;
+}
+
+void CGameMenuItemZEditBitmap::AddChar(char ch)
+{
+    int i = strlen(at20);
+    if (i + 1 < at24)
+    {
+        at20[i] = ch;
+        at20[i + 1] = 0;
+    }
+}
+
+void CGameMenuItemZEditBitmap::BackChar(void)
+{
+    int i = strlen(at20);
+    if (i > 0)
+        at20[i - 1] = 0;
+}
+
+void CGameMenuItemZEditBitmap::Draw(void)
+{
+    int height, width;
+    gMenuTextMgr.GetFontInfo(m_nFont, NULL, &width, &height);
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    at2c->at24 = -1;
+    if (at34)
+        shade = -128;
+    if (m_pzText)
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, m_nX, m_nY, shade, pal, false);
+    int x = m_nX+m_nWidth-1-(at24+1)*width;
+    if (at20 && *at20)
+    {
+        int width;
+        gMenuTextMgr.GetFontInfo(m_nFont, at20, &width, NULL);
+        int shade2;
+        if (at36)
+        {
+            if (at34)
+                shade2 = -128;
+            else
+                shade2 = shade;
+        }
+        else
+        {
+            if (at34)
+                shade2 = shade;
+            else
+                shade2 = 32;
+        }
+        gMenuTextMgr.DrawText(at20, m_nFont, x, m_nY, shade2, 0, false);
+        x += width;
+    }
+    if (at34 && (gGameClock & 32))
+        gMenuTextMgr.DrawText("_", m_nFont, x, m_nY, shade, pal, false);
+
+    int mx = m_nX<<16;
+    int my = m_nY<<16;
+    int mw = m_nWidth<<16;
+    int mh = height<<16;
+
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, mx, my, mw, mh))
+        {
+            pMenu->SetFocusItem(this);
+
+            CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+            gGameMenuMgr.m_mousecaught = 1;
+
+            if (Event(event))
+                gGameMenuMgr.PostPop();
+        }
+    }
+}
+
+bool CGameMenuItemZEditBitmap::Event(CGameMenuEvent &event)
+{
+    static char buffer[256];
+    // Hack
+    if (event.at2 == sc_kpad_2 || event.at2 == sc_kpad_4 || event.at2 == sc_kpad_6 || event.at2 == sc_kpad_8)
+        event.at0 = kMenuEventKey;
+    switch (event.at0)
+    {
+    case kMenuEventEscape:
+        if (at34)
+        {
+            strncpy(at20, buffer, at24);
+            at20[at24-1] = 0;
+            at34 = 0;
+            gSaveGameActive = false;
+            return false;
+        }
+        gSaveGameActive = true;
+        return true;
+    case kMenuEventEnter:
+        if (!at35)
+        {
+            if (at30)
+                at30(this, &event);
+            gSaveGameActive = false;
+            return false;
+        }
+        if (at34)
+        {
+            if (at30)
+                at30(this, &event);
+            at34 = 0;
+            gSaveGameActive = false;
+            return false;
+        }
+        strncpy(buffer, at20, at24);
+        if (at37)
+            at20[0] = 0;
+        buffer[at24-1] = 0;
+        at34 = 1;
+        return false;
+    case kMenuEventBackSpace:
+        if (at34)
+            BackChar();
+        return false;
+    case kMenuEventKey:
+    case kMenuEventSpace:
+    {
+        char key;
+        if (event.at2 < 128)
+        {
+            if (keystatus[sc_LeftShift] || keystatus[sc_RightShift])
+                key = g_keyAsciiTableShift[event.at2];
+            else
+                key = g_keyAsciiTable[event.at2];
+            if (at30 && (isalnum(key) || ispunct(key) || isspace(key)))
+            {
+                AddChar(key);
+                return false;
+            }
+        }
+        return CGameMenuItem::Event(event);
+    }
+    case kMenuEventUp:
+        if (at34)
+            return false;
+        return CGameMenuItem::Event(event);
+    case kMenuEventDown:
+        if (at34)
+            return false;
+        return CGameMenuItem::Event(event);
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemQAV::CGameMenuItemQAV()
+{
+    at20 = NULL;
+    at24 = NULL;
+    at28 = 0;
+    bEnable = 0;
+}
+
+CGameMenuItemQAV::CGameMenuItemQAV(const char *a1, int a2, int a3, int a4, const char *a5, bool widescreen, bool clearbackground)
+{
+    m_nWidth = 0;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nY = a4;
+    at20 = a5;
+    m_nX = a3;
+    bEnable = 0;
+    bWideScreen = widescreen;
+    bClearBackground = clearbackground;
+}
+
+void CGameMenuItemQAV::Draw(void)
+{
+    if (bClearBackground)
+        videoClearScreen(0);
+    if (at24)
+    {
+        int backFC = gFrameClock;
+        gFrameClock = gGameClock;
+        int nTicks = totalclock - at30;
+        at30 = totalclock;
+        at2c -= nTicks;
+        if (at2c <= 0 || at2c > at28->at10)
+        {
+            at2c = at28->at10;
+        }
+        at28->Play(at28->at10 - at2c - nTicks, at28->at10 - at2c, -1, NULL);
+        int wx1, wy1, wx2, wy2;
+        wx1 = windowxy1.x;
+        wy1 = windowxy1.y;
+        wx2 = windowxy2.x;
+        wy2 = windowxy2.y;
+        windowxy1.x = 0;
+        windowxy1.y = 0;
+        windowxy2.x = xdim-1;
+        windowxy2.y = ydim-1;
+        if (bWideScreen)
+        {
+            int xdim43 = scale(ydim, 4, 3);
+            int nCount = (xdim+xdim43-1)/xdim43;
+            int backX = at28->x;
+            for (int i = 0; i < nCount; i++)
+            {
+                at28->Draw(at28->at10 - at2c, 10+kQavOrientationLeft, 0, 0);
+                at28->x += 320;
+            }
+            at28->x = backX;
+        }
+        else
+            at28->Draw(at28->at10 - at2c, 10, 0, 0);
+
+        windowxy1.x = wx1;
+        windowxy1.y = wy1;
+        windowxy2.x = wx2;
+        windowxy2.y = wy2;
+        gFrameClock = backFC;
+    }
+
+    if (bEnable && !gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED)
+    {
+        pMenu->SetFocusItem(this);
+
+        CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+        gGameMenuMgr.m_mousecaught = 1;
+
+        if (Event(event))
+            gGameMenuMgr.PostPop();
+    }
+}
+
+bool CGameMenuItemQAV::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventLeft:
+    case kMenuEventBackSpace:
+        pMenu->FocusPrevItem();
+        return false;
+    case kMenuEventRight:
+    case kMenuEventEnter:
+    case kMenuEventSpace:
+        pMenu->FocusNextItem();
+        return false;
+    case kMenuEventInit:
+        if (at20)
+        {
+            if (!at28)
+            {
+                at24 = gSysRes.Lookup(at20, "QAV");
+                if (!at24)
+                    ThrowError("Could not load QAV %s\n", at20);
+                at28 = (QAV*)gSysRes.Lock(at24);
+                at28->nSprite = -1;
+                at28->x = m_nX;
+                at28->y = m_nY;
+                at28->Preload();
+                at2c = at28->at10;
+                at30 = totalclock;
+                return false;
+            }
+            gSysRes.Lock(at24);
+        }
+        return false;
+    case kMenuEventDeInit:
+        if (at20 && at28)
+        {
+            gSysRes.Unlock(at24);
+            if (at24->lockCount == 0)
+                at28 = NULL;
+        }
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+void CGameMenuItemQAV::Reset(void)
+{
+    at2c = at28->at10;
+    at30 = totalclock;
+}
+
+CGameMenuItemZCycleSelect::CGameMenuItemZCycleSelect()
+{
+    m_pzText = NULL;
+    m_nFont = 3;
+    m_nX = 0;
+    m_nY = 0;
+    m_nRows = 0;
+    m_nTopDelta = 0;
+    m_nFocus = 0;
+    m_nItems = 0;
+    m_pzStrings = NULL;
+    m_pReturn = NULL;
+}
+
+CGameMenuItemZCycleSelect::CGameMenuItemZCycleSelect(const char *pzText, int nFont, int nX, int nY, int nWidth, int nRows, int nItems, const char **pzStrings, int *pReturn, void(*pCallback)(CGameMenuItemZCycleSelect *))
+{
+    m_nTopDelta = 0;
+    m_nFocus = 0;
+    m_pzText = pzText;
+    m_nFont = nFont;
+    m_nX = nX;
+    m_nY = nY;
+    m_nWidth = nWidth;
+    m_nRows = nRows;
+    m_pCallback = pCallback;
+    m_nItems = nItems;
+    m_pzStrings = pzStrings;
+    m_pReturn = pReturn;
+}
+
+void CGameMenuItemZCycleSelect::Draw(void)
+{
+    int height;
+    int shade;
+    gMenuTextMgr.GetFontInfo(m_nFont, NULL, NULL, &height);
+    int y = m_nY;
+    int k = m_nFocus - m_nTopDelta;
+    int nNewFocus = m_nFocus;
+    bool bClick = false;
+    for (int i = 0; i < m_nRows; i++, y += height, k++)
+    {
+        if (k == m_nFocus)
+        {
+            shade = 32;
+            if (pMenu->IsFocusItem(this))
+                shade = 32-(totalclock&63);
+            viewDrawText(3, m_pzStrings[k], m_nX, y, shade, 0, 0, false);
+        }
+        else
+        {
+            viewDrawText(3, m_pzStrings[k], m_nX, y, 24, 0, 0, false);
+        }
+        int mx = m_nX<<16;
+        int my = y<<16;
+        int mw = m_nWidth<<16;
+        int mh = height<<16;
+        if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, mx, my, mw, mh)))
+        {
+            if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, mx, my, mw, mh)))
+            {
+                nNewFocus = k;
+            }
+
+            if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, mx, my, mw, mh))
+            {
+                nNewFocus = k;
+                bClick = true;
+            }
+        }
+    }
+    m_nTopDelta += nNewFocus-m_nFocus;
+    m_nFocus = nNewFocus;
+    if (bClick)
+    {
+        CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+        gGameMenuMgr.m_mousecaught = 1;
+
+        if (Event(event))
+            gGameMenuMgr.PostPop();
+    }
+}
+
+bool CGameMenuItemZCycleSelect::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventUp:
+        if (event.at2 == sc_Tab || m_nFocus == 0)
+        {
+            pMenu->FocusPrevItem();
+            return false;
+        }
+        m_nFocus--;
+        if (m_nTopDelta > 0)
+            m_nTopDelta--;
+        return false;
+    case kMenuEventDown:
+        if (event.at2 == sc_Tab || m_nFocus == m_nItems-1)
+        {
+            pMenu->FocusNextItem();
+            return false;
+        }
+        m_nFocus++;
+        if (m_nTopDelta+1 < m_nRows)
+            m_nTopDelta++;
+        return false;
+    case kMenuEventEnter:
+        if (m_pCallback)
+            m_pCallback(this);
+        *m_pReturn = m_nFocus;
+        return true;
+    case kMenuEventScrollUp:
+        if (m_nFocus-m_nTopDelta > 0)
+        {
+            m_nTopDelta++;
+            if (m_nTopDelta>0)
+            {
+                m_nFocus--;
+                m_nTopDelta--;
+            }
+        }
+        return false;
+    case kMenuEventScrollDown:
+        if (m_nFocus-m_nTopDelta+m_nRows < m_nItems)
+        {
+            m_nTopDelta--;
+            if (m_nTopDelta+1 < m_nRows)
+            {
+                m_nFocus++;
+                m_nTopDelta++;
+            }
+        }
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+bool CGameMenuItemZCycleSelect::MouseEvent(CGameMenuEvent &event)
+{
+    event.at0 = kMenuEventNone;
+    if (MOUSEACTIVECONDITIONAL(MOUSE_GetButtons()&WHEELUP_MOUSE))
+    {
+        gGameMenuMgr.m_mouselastactivity = totalclock;
+        MOUSE_ClearButton(WHEELUP_MOUSE);
+        event.at0 = kMenuEventScrollUp;
+    }
+    else if (MOUSEACTIVECONDITIONAL(MOUSE_GetButtons()&WHEELDOWN_MOUSE))
+    {
+        gGameMenuMgr.m_mouselastactivity = totalclock;
+        MOUSE_ClearButton(WHEELDOWN_MOUSE);
+        event.at0 = kMenuEventScrollDown;
+    }
+    else
+        return CGameMenuItem::MouseEvent(event);
+    return event.at0 != kMenuEventNone;
+}
+
+CGameMenuItemZCycle::CGameMenuItemZCycle()
+{
+    m_pzText = NULL;
+    m_nFocus = 0;
+    m_nItems = 0;
+    m_pCallback = NULL;
+    m_pCallbackSelect = NULL;
+    m_pMenuSelect = NULL;
+    m_pItemSelectTitle = NULL;
+    m_pItemSelect = NULL;
+    m_nMenuSelectReturn = -1;
+}
+
+CGameMenuItemZCycle::CGameMenuItemZCycle(const char *a1, int a2, int a3, int a4, int a5, int a6, void(*a7)(CGameMenuItemZCycle *), const char **a8, int a9, int a10, bool bMenu, void(*pCallbackSelect)(CGameMenuItemZCycleSelect*))
+{
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nFocus = 0;
+    m_nWidth = a5;
+    m_nAlign = a6;
+    m_pCallback = a7;
+    m_pCallbackSelect = pCallbackSelect;
+    m_nItems = 0;
+    m_bMenu = bMenu;
+    m_pMenuSelect = NULL;
+    m_pItemSelectTitle = NULL;
+    m_pItemSelect = NULL;
+    m_nMenuSelectReturn = -1;
+    SetTextArray(a8, a9, a10);
+}
+
+CGameMenuItemZCycle::~CGameMenuItemZCycle()
+{
+    m_pzText = NULL;
+    m_nFocus = 0;
+    m_nItems = 0;
+    m_pCallback = NULL;
+    m_pCallbackSelect = NULL;
+    m_pMenuSelect = NULL;
+    m_pItemSelectTitle = NULL;
+    m_pItemSelect = NULL;
+    m_nMenuSelectReturn = -1;
+    memset(m_pzStrings, 0, sizeof(m_pzStrings));
+}
+
+void CGameMenuItemZCycle::Draw(void)
+{
+    int width = 0, height = 0;
+    int shade = bEnable ? 32 : 48;
+    int pal = bEnable ? 0 : 5;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int x = m_nX;
+    int y = m_nY;
+
+    if (m_nMenuSelectReturn != -1)
+    {
+        m_nFocus = m_nMenuSelectReturn;
+        if (m_pCallback)
+            m_pCallback(this);
+        m_nMenuSelectReturn = -1;
+    }
+
+    if (m_pzText)
+    {
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, &height);
+        switch (m_nAlign)
+        {
+        case 1:
+            x = m_nX+m_nWidth/2-width/2;
+            break;
+        case 2:
+            x = m_nX+m_nWidth-1-width;
+            break;
+        case 0:
+        default:
+            break;
+        }
+        gMenuTextMgr.DrawText(m_pzText, m_nFont, x, y, shade, pal, false);
+    }
+    const char *pzText;
+    if (!m_nItems)
+        pzText = "????";
+    else
+        pzText = m_pzStrings[m_nFocus];
+    dassert(pzText != NULL);
+    gMenuTextMgr.GetFontInfo(m_nFont, pzText, &width, NULL);
+    gMenuTextMgr.DrawText(pzText, m_nFont, m_nX + m_nWidth - 1 - width, y, shade, pal, false);
+    if (bEnable && MOUSEACTIVECONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousepos, x<<16, y<<16, m_nWidth<<16, height<<16)))
+    {
+        if (MOUSEWATCHPOINTCONDITIONAL(!gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_prevmousepos, x<<16, y<<16, m_nWidth<<16, height<<16)))
+        {
+            pMenu->SetFocusItem(this);
+        }
+
+        if (!gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED && !gGameMenuMgr.MouseOutsideBounds(&gGameMenuMgr.m_mousedownpos, x<<16, y<<16, m_nWidth<<16, height<<16))
+        {
+            pMenu->SetFocusItem(this);
+
+            CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+            gGameMenuMgr.m_mousecaught = 1;
+
+            if (Event(event))
+                gGameMenuMgr.PostPop();
+        }
+    }
+}
+
+bool CGameMenuItemZCycle::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventEnter:
+        if (m_bMenu)
+        {
+            if (m_pMenuSelect)
+            {
+                delete m_pMenuSelect;
+                m_pMenuSelect = NULL;
+            }
+            if (m_pItemSelectTitle)
+            {
+                delete m_pItemSelectTitle;
+                m_pItemSelectTitle = NULL;
+            }
+            if (m_pItemSelect)
+            {
+                delete m_pItemSelect;
+                m_pItemSelect = NULL;
+            }
+            m_pMenuSelect = new CGameMenu();
+            dassert(m_pMenuSelect != NULL);
+            strncpy(m_zTitle, m_pzText, kMaxTitleLength);
+            int l = strlen(m_zTitle);
+            if (l > 0 && m_zTitle[l-1] == ':')
+                l--;
+            m_zTitle[l] = 0;
+            m_pItemSelectTitle = new CGameMenuItemTitle(m_zTitle, 1, 160, 20, 2038);
+            dassert(m_pItemSelectTitle != NULL);
+            m_pItemSelect = new CGameMenuItemZCycleSelect("", 3, 100, 40, 100, 16, m_nItems, m_pzStrings, &m_nMenuSelectReturn, m_pCallbackSelect);
+            dassert(m_pItemSelect != NULL);
+            m_pMenuSelect->Add(m_pItemSelectTitle, false);
+            m_pMenuSelect->Add(m_pItemSelect, true);
+            m_pMenuSelect->Add(&itemBloodQAV, false);
+            gGameMenuMgr.Push(m_pMenuSelect, -1);
+            return false;
+        }
+        fallthrough__;
+    case kMenuEventRight:
+    case kMenuEventSpace:
+        Next();
+        if (m_pCallback)
+            m_pCallback(this);
+        return false;
+    case kMenuEventLeft:
+        Prev();
+        if (m_pCallback)
+            m_pCallback(this);
+        return false;
+    case kMenuEventDeInit:
+        if (m_pMenuSelect)
+        {
+            delete m_pMenuSelect;
+            m_pMenuSelect = NULL;
+        }
+        if (m_pItemSelectTitle)
+        {
+            delete m_pItemSelectTitle;
+            m_pItemSelectTitle = NULL;
+        }
+        if (m_pItemSelect)
+        {
+            delete m_pItemSelect;
+            m_pItemSelect = NULL;
+        }
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+void CGameMenuItemZCycle::Add(const char *pItem, bool active)
+{
+    dassert(pItem != NULL);
+    dassert(m_nItems < kMaxGameCycleItems);
+    m_pzStrings[m_nItems] = pItem;
+    if (active)
+        m_nFocus = m_nItems;
+    m_nItems++;
+}
+
+void CGameMenuItemZCycle::Next(void)
+{
+    if (m_nItems > 0)
+    {
+        m_nFocus++;
+        if (m_nFocus >= m_nItems)
+            m_nFocus = 0;
+    }
+}
+
+void CGameMenuItemZCycle::Prev(void)
+{
+    if (m_nItems > 0)
+    {
+        m_nFocus--;
+        if (m_nFocus < 0)
+            m_nFocus += m_nItems;
+    }
+}
+
+void CGameMenuItemZCycle::Clear(void)
+{
+    m_nItems = m_nFocus = 0;
+    memset(m_pzStrings, 0, sizeof(m_pzStrings));
+}
+
+void CGameMenuItemZCycle::SetTextArray(const char **pTextArray, int nTextPtrCount, int nIndex)
+{
+    Clear();
+    dassert(nTextPtrCount <= kMaxGameCycleItems);
+    for (int i = 0; i < nTextPtrCount; i++)
+        Add(pTextArray[i], false);
+    SetTextIndex(nIndex);
+}
+
+void CGameMenuItemZCycle::SetTextIndex(int nIndex)
+{
+    m_nFocus = ClipRange(nIndex, 0, m_nItems);
+}
+
+CGameMenuItemYesNoQuit::CGameMenuItemYesNoQuit()
+{
+    m_pzText = NULL;
+    m_nRestart = 0;
+}
+
+CGameMenuItemYesNoQuit::CGameMenuItemYesNoQuit(const char *a1, int a2, int a3, int a4, int a5, int a6, int a7)
+{
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+    m_nWidth = a5;
+    at20 = a6;
+    m_nRestart = a7;
+}
+
+void CGameMenuItemYesNoQuit::Draw(void)
+{
+    if (!m_pzText) return;
+    int shade = 32;
+    if (pMenu->IsFocusItem(this))
+        shade = 32-(totalclock&63);
+    int width;
+    int x = m_nX;
+    switch (at20)
+    {
+    case 1:
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+        x = m_nX+m_nWidth/2-width/2;
+        break;
+    case 2:
+        gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+        x = m_nX+m_nWidth-1-width;
+        break;
+    case 0:
+    default:
+        break;
+    }
+    gMenuTextMgr.DrawText(m_pzText, m_nFont, x, m_nY, shade, 0, true);
+
+    if (bEnable && !gGameMenuMgr.m_mousecaught && g_mouseClickState == MOUSE_RELEASED)
+    {
+        pMenu->SetFocusItem(this);
+
+        CGameMenuEvent event = { kMenuEventEnter, 0 };
+
+        gGameMenuMgr.m_mousecaught = 1;
+
+        if (Event(event))
+            gGameMenuMgr.PostPop();
+    }
+}
+
+extern void Restart(CGameMenuItemChain *pItem);
+extern void Quit(CGameMenuItemChain *pItem);
+
+bool CGameMenuItemYesNoQuit::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventKey:
+        if (event.at2 == sc_Y)
+        {
+            if (m_nRestart)
+                Restart(NULL);
+            else
+                Quit(NULL);
+        }
+        else if (event.at2 == sc_N)
+            gGameMenuMgr.Pop();
+        return false;
+    case kMenuEventEnter:
+        if (m_nRestart)
+            Restart(NULL);
+        else
+            Quit(NULL);
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+CGameMenuItemPicCycle::CGameMenuItemPicCycle()
+{
+    m_pzText = NULL;
+    at24 = 0;
+    m_nItems = 0;
+    atb0 = 0;
+    at2c = 0;
+    atb4 = 0;
+}
+
+CGameMenuItemPicCycle::CGameMenuItemPicCycle(int a1, int a2, void(*a3)(CGameMenuItemPicCycle *), int *a4, int a5, int a6)
+{
+    m_nWidth = 0;
+    at24 = 0;
+    m_nItems = 0;
+    m_nX = a1;
+    m_nY = a2;
+    atb0 = a3;
+    atb4 = 0;
+    SetPicArray(a4, a5, a6);
+}
+
+void CGameMenuItemPicCycle::Draw(void)
+{
+    videoSetViewableArea(0, 0, xdim - 1, ydim - 1);
+    if (atb4)
+        rotatesprite(0, 0, 65536, 0, atb4, 0, 0, 82, 0, 0, xdim - 1, ydim - 1);
+    if (at30[at24])
+        rotatesprite(0, 0, 65536, 0, at30[at24], 0, 0, 82, 0, 0, xdim - 1, ydim - 1);
+}
+
+bool CGameMenuItemPicCycle::Event(CGameMenuEvent &event)
+{
+    switch (event.at0)
+    {
+    case kMenuEventRight:
+    case kMenuEventEnter:
+    case kMenuEventSpace:
+        Next();
+        if (atb0)
+            atb0(this);
+        return false;
+    case kMenuEventLeft:
+        Prev();
+        if (atb0)
+            atb0(this);
+        return false;
+    }
+    return CGameMenuItem::Event(event);
+}
+
+void CGameMenuItemPicCycle::Add(int nItem, bool active)
+{
+    dassert(m_nItems < kMaxPicCycleItems);
+    at30[m_nItems] = nItem;
+    if (active)
+        at24 = m_nItems;
+    m_nItems++;
+}
+
+void CGameMenuItemPicCycle::Next(void)
+{
+    if (m_nItems > 0)
+    {
+        at24++;
+        if (at24 >= m_nItems)
+            at24 = 0;
+    }
+}
+
+void CGameMenuItemPicCycle::Prev(void)
+{
+    if (m_nItems > 0)
+    {
+        at24--;
+        if (at24 < 0)
+            at24 += m_nItems;
+    }
+}
+
+void CGameMenuItemPicCycle::Clear(void)
+{
+    m_nItems = at24 = 0;
+    memset(at30, 0, sizeof(at30));
+    at2c = 0;
+}
+
+void CGameMenuItemPicCycle::SetPicArray(int *pArray, int nTileCount, int nIndex)
+{
+    Clear();
+    at2c = 0;
+    dassert(nTileCount <= kMaxPicCycleItems);
+    for (int i = 0; i < nTileCount; i++)
+        Add(pArray[i], false);
+    SetPicIndex(nIndex);
+}
+
+void CGameMenuItemPicCycle::SetPicIndex(int nIndex)
+{
+    at24 = ClipRange(nIndex, 0, m_nItems);
+}
+
+CGameMenuItemPassword::CGameMenuItemPassword()
+{
+    at37 = 0;
+    m_pzText = NULL;
+    at36 = 0;
+    at32 = 0;
+    at5b = 0;
+}
+
+CGameMenuItemPassword::CGameMenuItemPassword(const char *a1, int a2, int a3, int a4)
+{
+    at37 = 0;
+    m_nWidth = 0;
+    at36 = 0;
+    at32 = 0;
+    at5b = 0;
+    m_pzText = a1;
+    m_nFont = a2;
+    m_nX = a3;
+    m_nY = a4;
+}
+
+const char *kCheckPasswordMsg = "ENTER PASSWORD: ";
+const char *kOldPasswordMsg = "ENTER OLD PASSWORD: ";
+const char *kNewPasswordMsg = "ENTER NEW PASSWORD: ";
+const char *kInvalidPasswordMsg = "INVALID PASSWORD.";
+
+void CGameMenuItemPassword::Draw(void)
+{
+    bool focus = pMenu->IsFocusItem(this);
+    int shade = 32;
+    int shadef = 32-(totalclock&63);
+    int width;
+    switch (at37)
+    {
+    case 1:
+    case 2:
+    case 3:
+        switch (at37)
+        {
+        case 1:
+            strcpy(at3b, kCheckPasswordMsg);
+            break;
+        case 2:
+            strcpy(at3b, kOldPasswordMsg);
+            break;
+        case 3:
+            strcpy(at3b, kNewPasswordMsg);
+            break;
+        }
+        for (int i = 0; i < at32; i++)
+            strcat(at3b, "*");
+        strcat(at3b, "_");
+        gMenuTextMgr.GetFontInfo(m_nFont, at3b, &width, NULL);
+        gMenuTextMgr.DrawText(at3b, m_nFont, m_nX-width/2, m_nY+20, shadef, 0, false);
+        shadef = 32;
+        break;
+    case 4:
+        if ((totalclock - at5b) & 32)
+        {
+            gMenuTextMgr.GetFontInfo(m_nFont, kInvalidPasswordMsg, &width, NULL);
+            gMenuTextMgr.DrawText(kInvalidPasswordMsg, m_nFont, m_nX - width / 2, m_nY + 20, shade, 0, false);
+        }
+        if (at5b && totalclock-at5b > 256)
+        {
+            at5b = 0;
+            at37 = 0;
+        }
+        break;
+    }
+    gMenuTextMgr.GetFontInfo(m_nFont, m_pzText, &width, NULL);
+    gMenuTextMgr.DrawText(m_pzText, m_nFont, m_nX-width/2, m_nY, focus ? shadef : shade, 0, false);
+}
+
+bool CGameMenuItemPassword::Event(CGameMenuEvent &event)
+{
+    switch (at37)
+    {
+    case 0:
+    case 4:
+        if (event.at0 == kMenuEventEnter)
+        {
+            at29[0] = 0;
+            if (strcmp(at20, ""))
+                at37 = 2;
+            else
+                at37 = 3;
+            return false;
+        }
+        return CGameMenuItem::Event(event);
+    case 1:
+    case 2:
+    case 3:
+        switch (event.at0)
+        {
+        case kMenuEventEnter:
+            switch (at37)
+            {
+            case 1:
+                at36 = strcmp(at20,at29) == 0;
+                if (at36)
+                    at37 = 0;
+                else
+                    at37 = 4;
+                if (!at36)
+                {
+                    at5b = totalclock;
+                    pMenu->FocusPrevItem();
+                }
+                else
+                {
+                    at5f->at20 = 0;
+                    at5f->Draw();
+                    gbAdultContent = false;
+                    // NUKE-TODO:
+                    //CONFIG_WriteAdultMode();
+                    pMenu->FocusPrevItem();
+                }
+                return false;
+            case 2:
+                at36 = strcmp(at20,at29) == 0;
+                if (at36)
+                    at37 = 0;
+                else
+                    at37 = 4;
+                if (at36)
+                {
+                    strcpy(at20, "");
+                    strcpy(gzAdultPassword, "");
+                    // NUKE-TODO:
+                    //CONFIG_WriteAdultMode();
+                    at37 = 0;
+                }
+                else
+                    at5b = totalclock;
+                return false;
+            case 3:
+                strcpy(at20, at29);
+                strcpy(at20, gzAdultPassword);
+                strcpy(gzAdultPassword, "");
+                // NUKE-TODO:
+                //CONFIG_WriteAdultMode();
+                at37 = 0;
+                return false;
+            }
+            break;
+        case kMenuEventEscape:
+            at37 = 0;
+            Draw();
+            return false;
+        case kMenuEventKey:
+            if (at32 < 8)
+            {
+                char key = Btoupper(g_keyAsciiTable[event.at2]);
+                if (isalnum(key) || ispunct(key) || isspace(key))
+                {
+                    at29[at32++] = key;
+                    at29[at32] = 0;
+                }
+            }
+            return false;
+        case kMenuEventBackSpace:
+            if (at32 > 0)
+                at29[--at32] = 0;
+            return false;
+        case kMenuEventLeft:
+        case kMenuEventRight:
+        case kMenuEventSpace:
+            return false;
+        }
+    }
+    return CGameMenuItem::Event(event);
+}
\ No newline at end of file
diff --git a/source/blood/src/gamemenu.h b/source/blood/src/gamemenu.h
new file mode 100644
index 000000000..f9163df24
--- /dev/null
+++ b/source/blood/src/gamemenu.h
@@ -0,0 +1,490 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "compat.h"
+#include "common_game.h"
+#include "blood.h"
+#include "inifile.h"
+#include "resource.h"
+#include "qav.h"
+
+#define M_MOUSETIMEOUT 210
+
+#define kMaxGameMenuItems 128
+#define kMaxGameCycleItems 128
+#define kMaxPicCycleItems 128
+#define kMaxTitleLength 32
+
+// alpha increments of 3 --> 255 / 3 = 85 --> round up to power of 2 --> 128 --> divide by 2 --> 64 alphatabs required
+// use 16 anyway :P
+#define MOUSEUSEALPHA (videoGetRenderMode() != REND_CLASSIC || numalphatabs >= 15)
+#define MOUSEALPHA (MOUSEUSEALPHA ? clamp((totalclock - gGameMenuMgr.m_mouselastactivity - 90)*3, 0, 255) : 0)
+#define CURSORALPHA (MOUSEUSEALPHA ? clamp((totalclock - gGameMenuMgr.m_mouselastactivity - 90)*2 + (255/3), (255/3), 255) : 255/3)
+#define MOUSEACTIVECONDITION (totalclock - gGameMenuMgr.m_mouselastactivity < M_MOUSETIMEOUT)
+#define MOUSEACTIVECONDITIONAL(condition) (MOUSEACTIVECONDITION && (condition))
+#define MOUSEINACTIVECONDITIONAL(condition) (!MOUSEACTIVECONDITION && (condition))
+#define MOUSEWATCHPOINTCONDITIONAL(condition) ((condition) || gGameMenuMgr.m_mousewake_watchpoint || gGameMenuMgr.m_menuchange_watchpoint == 3)
+
+enum {
+    kMenuEventNone = 0,
+    kMenuEventKey = 1,
+    kMenuEventUp = 2,
+    kMenuEventDown = 3,
+    kMenuEventLeft = 4,
+    kMenuEventRight = 5,
+    kMenuEventEnter = 6,
+    kMenuEventEscape = 7,
+    kMenuEventSpace = 8,
+    kMenuEventBackSpace = 9,
+    kMenuEventDelete = 10,
+    kMenuEventScrollUp = 11,
+    kMenuEventScrollDown = 12,
+
+
+    kMenuEventInit = 0x8000,
+    kMenuEventDeInit = 0x8001
+};
+
+enum {
+    kMenuSliderNone = 0,
+    kMenuSliderValue,
+    kMenuSliderPercent,
+    kMenuSliderQ16
+};
+
+struct CGameMenuEvent {
+    unsigned short at0;
+    char at2;
+};
+
+// NUKE-TODO:
+#ifdef DrawText
+#undef DrawText
+#endif
+
+class CMenuTextMgr
+{
+public:
+    int at0;
+    CMenuTextMgr();
+    void DrawText(const char *pString, int nFont, int x, int y, int nShade, int nPalette, bool shadow );
+    void GetFontInfo(int nFont, const char *pString, int *pXSize, int *pYSize);
+};
+
+class CGameMenu;
+
+class CGameMenuItem {
+public:
+    CGameMenu *pMenu;
+    const char* m_pzText;
+    int m_nFont;
+    int m_nX;
+    int m_nY;
+    int m_nWidth;
+    void (*pPreDrawCallback)(CGameMenuItem *pItem);
+    //int nFlags;
+    unsigned int bCanSelect : 1;
+    unsigned int bEnable : 1;
+    unsigned int bNoDraw : 1;
+    CGameMenuItem();
+    virtual ~CGameMenuItem();
+    virtual void Draw(void) = 0;
+    virtual bool Event(CGameMenuEvent &);
+    virtual bool MouseEvent(CGameMenuEvent &);
+};
+
+class CGameMenuItemText : public CGameMenuItem
+{
+public:
+    int at20;
+    CGameMenuItemText();
+    CGameMenuItemText(const char *, int, int, int, int);
+    virtual void Draw(void);
+    //virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemTitle : public CGameMenuItem
+{
+public:
+    int at20;
+    CGameMenuItemTitle();
+    CGameMenuItemTitle(const char *, int, int, int, int);
+    virtual void Draw(void);
+    //virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemZBool : public CGameMenuItem
+{
+public:
+    bool at20;
+    const char *at21;
+    const char *at25;
+    void (*at29)(CGameMenuItemZBool *);
+    CGameMenuItemZBool();
+    CGameMenuItemZBool(const char *,int,int,int,int,bool,void (*)(CGameMenuItemZBool *),const char *,const char *);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemChain : public CGameMenuItem
+{
+public:
+    int at20;
+    CGameMenu *at24;
+    int at28;
+    void(*at2c)(CGameMenuItemChain *);
+    int at30;
+    CGameMenuItemChain();
+    CGameMenuItemChain(const char *, int, int, int, int, int, CGameMenu *, int, void(*)(CGameMenuItemChain *), int);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItem7EA1C : public CGameMenuItem
+{
+public:
+    int at20; // text align
+    CGameMenu *at24;
+    int at28;
+    void(*at2c)(CGameMenuItem7EA1C *);
+    int at30;
+    IniFile *at34;
+    char at38[16];
+    char at48[16];
+    CGameMenuItem7EA1C();
+    CGameMenuItem7EA1C(const char *a1, int a2, int a3, int a4, int a5, const char *a6, const char *a7, int a8, int a9, void(*a10)(CGameMenuItem7EA1C *), int a11);
+    void Setup(void);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItem7EE34 : public CGameMenuItem
+{
+public:
+    int at20;
+    int at24;
+    CGameMenu *at28;
+    CGameMenu *at2c;
+    CGameMenuItem7EE34();
+    CGameMenuItem7EE34(const char *a1, int a2, int a3, int a4, int a5, int a6);
+    void Setup(void);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemChain7F2F0 : public CGameMenuItemChain
+{
+public:
+    int at34;
+    CGameMenuItemChain7F2F0();
+    CGameMenuItemChain7F2F0(char *a1, int a2, int a3, int a4, int a5, int a6, CGameMenu *a7, int a8, void(*a9)(CGameMenuItemChain *), int a10);
+    //virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemBitmap : public CGameMenuItem
+{
+public:
+    int at20;
+    CGameMenuItemBitmap();
+    CGameMenuItemBitmap(const char *, int, int, int, int);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemBitmapLS : public CGameMenuItemBitmap
+{
+public:
+    int at24;
+    int at28;
+    CGameMenuItemBitmapLS();
+    CGameMenuItemBitmapLS(const char *, int, int, int, int);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemKeyList : public CGameMenuItem
+{
+public:
+    void(*pCallback)(CGameMenuItemKeyList *);
+    int at24;
+    int nRows;
+    int nTopDelta;
+    int nFocus;
+    int nGameFuncs;
+    bool bScan;
+    CGameMenuItemKeyList();
+    CGameMenuItemKeyList(const char * a1, int a2, int a3, int a4, int a5, int a6, int a7, void(*a8)(CGameMenuItemKeyList *));
+    void Scan(void);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+    virtual bool MouseEvent(CGameMenuEvent &);
+};
+
+class CGameMenuItemSlider : public CGameMenuItem
+{
+public:
+    int *pValue;
+    int nValue;
+    int nRangeLow;
+    int nRangeHigh;
+    int nStep;
+    void(*pCallback)(CGameMenuItemSlider *);
+    int nSliderTile;
+    int nCursorTile;
+    int nShowValue;
+    CGameMenuItemSlider();
+    CGameMenuItemSlider(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, int _nValue, int _nRangeLow, int _nRangeHigh, int _nStep, void(*_pCallback)(CGameMenuItemSlider *), int _nSliderTile, int _nCursorTile, int _nShowValue = kMenuSliderNone);
+    CGameMenuItemSlider(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, int *pnValue, int _nRangeLow, int _nRangeHigh, int _nStep, void(*_pCallback)(CGameMenuItemSlider *), int _nSliderTile, int _nCursorTile, int _nShowValue = kMenuSliderNone);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+    virtual bool MouseEvent(CGameMenuEvent &);
+};
+
+class CGameMenuItemSliderFloat : public CGameMenuItem
+{
+public:
+    float *pValue;
+    float fValue;
+    float fRangeLow;
+    float fRangeHigh;
+    float fStep;
+    void(*pCallback)(CGameMenuItemSliderFloat *);
+    int nSliderTile;
+    int nCursorTile;
+    int nShowValue;
+    CGameMenuItemSliderFloat();
+    CGameMenuItemSliderFloat(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, float _fValue, float _fRangeLow, float _fRangeHigh, float _fStep, void(*_pCallback)(CGameMenuItemSliderFloat *), int _nSliderTile, int _nCursorTile, int _nShowValue = kMenuSliderNone);
+    CGameMenuItemSliderFloat(const char *_pzText, int _nFont, int _nX, int _nY, int _nWidth, float *pnValue, float _fRangeLow, float _fRangeHigh, float _fStep, void(*_pCallback)(CGameMenuItemSliderFloat *), int _nSliderTile, int _nCursorTile, int _nShowValue = kMenuSliderNone);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemZEdit : public CGameMenuItem
+{
+public:
+    char *at20;
+    int at24;
+    int at28;
+    void(*at2c)(CGameMenuItemZEdit *, CGameMenuEvent *);
+    char at30;
+    char at31;
+    char at32;
+    CGameMenuItemZEdit();
+    CGameMenuItemZEdit(const char *, int, int, int, int, char *, int, char, void(*)(CGameMenuItemZEdit *, CGameMenuEvent *), int);
+    void AddChar(char);
+    void BackChar(void);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemZEditBitmap : public CGameMenuItem
+{
+public:
+    char *at20;
+    int at24;
+    int at28;
+    CGameMenuItemBitmapLS *at2c;
+    void(*at30)(CGameMenuItemZEditBitmap *, CGameMenuEvent *);
+    char at34;
+    char at35;
+    char at36;
+    char at37;
+    CGameMenuItemZEditBitmap();
+    CGameMenuItemZEditBitmap(char *, int, int, int, int, char *, int, char, void(*)(CGameMenuItemZEditBitmap *, CGameMenuEvent *), int);
+    void AddChar(char);
+    void BackChar(void);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemQAV : public CGameMenuItem
+{
+public:
+    const char *at20;
+    DICTNODE *at24;
+    QAV *at28;
+    int at2c;
+    int at30;
+    bool bWideScreen;
+    bool bClearBackground;
+    CGameMenuItemQAV();
+    CGameMenuItemQAV(const char *, int, int, int, const char *, bool widescreen = false, bool clearbackground = false);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+    void Reset(void);
+};
+
+class CGameMenuItemZCycleSelect : public CGameMenuItem
+{
+public:
+    void(*m_pCallback)(CGameMenuItemZCycleSelect *);
+    int m_nRows;
+    int m_nTopDelta;
+    int m_nFocus;
+    int m_nItems;
+    int *m_pReturn;
+    const char **m_pzStrings;
+    CGameMenuItemZCycleSelect();
+    CGameMenuItemZCycleSelect(const char *pzText, int nFont, int nX, int nY, int nWidth, int nRows, int nItems, const char **pzStrings, int *pReturn, void(*pCallback)(CGameMenuItemZCycleSelect *));
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+    virtual bool MouseEvent(CGameMenuEvent &);
+};
+
+
+class CGameMenuItemZCycle : public CGameMenuItem
+{
+public:
+    int m_nItems;
+    int m_nFocus;
+    int m_nAlign;
+    const char *m_pzStrings[kMaxGameCycleItems];
+    char m_zTitle[kMaxTitleLength];
+    void(*m_pCallback)(CGameMenuItemZCycle *);
+    void(*m_pCallbackSelect)(CGameMenuItemZCycleSelect *);
+    bool m_bMenu;
+    int m_nMenuSelectReturn;
+    CGameMenu *m_pMenuSelect;
+    CGameMenuItemTitle *m_pItemSelectTitle;
+    CGameMenuItemZCycleSelect *m_pItemSelect;
+    CGameMenuItemZCycle();
+    CGameMenuItemZCycle(const char *, int, int, int, int, int, void(*)(CGameMenuItemZCycle *), const char **, int, int, bool = false, void(*)(CGameMenuItemZCycleSelect*) = NULL);
+    ~CGameMenuItemZCycle();
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+    void Add(const char *, bool);
+    void Next(void);
+    void Prev(void);
+    void Clear(void);
+    void SetTextArray(const char **, int, int);
+    void SetTextIndex(int);
+};
+
+class CGameMenuItemYesNoQuit : public CGameMenuItem
+{
+public:
+    int at20;
+    int m_nRestart;
+    CGameMenuItemYesNoQuit();
+    CGameMenuItemYesNoQuit(const char *, int, int, int, int, int, int);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+class CGameMenuItemPicCycle : public CGameMenuItem
+{
+public:
+    int m_nItems;
+    int at24;
+    int at28;
+    int at2c;
+    int at30[kMaxPicCycleItems];
+    void(*atb0)(CGameMenuItemPicCycle *);
+    int atb4;
+    CGameMenuItemPicCycle();
+    CGameMenuItemPicCycle(int, int, void(*)(CGameMenuItemPicCycle *), int *, int, int);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+    void Add(int, bool);
+    void Next(void);
+    void Prev(void);
+    void Clear(void);
+    void SetPicArray(int *, int, int);
+    void SetPicIndex(int);
+};
+
+class CGameMenuItemPassword : public CGameMenuItem
+{
+public:
+    char at20[9];
+    char at29[9];
+    int at32;
+    char at36;
+    int at37;
+    char at3b[32];
+    int at5b;
+    CGameMenuItemZBool *at5f;
+    CGameMenuItemPassword();
+    CGameMenuItemPassword(const char *, int, int, int);
+    virtual void Draw(void);
+    virtual bool Event(CGameMenuEvent &);
+};
+
+
+class CGameMenu
+{
+public:
+    int m_nItems;
+    int m_nFocus;
+    int at8;
+    char atc;
+    CGameMenuItem *pItemList[kMaxGameMenuItems]; // atd
+    CGameMenu();
+    CGameMenu(int);
+    ~CGameMenu();
+    void InitializeItems(CGameMenuEvent &event);
+    void Draw(void);
+    bool Event(CGameMenuEvent &event);
+    void Add(CGameMenuItem *pItem, bool active);
+    void SetFocusItem(int nItem);
+    void SetFocusItem(CGameMenuItem *Item);
+    bool CanSelectItem(int nItem);
+    void FocusPrevItem(void);
+    void FocusNextItem(void);
+    bool IsFocusItem(CGameMenuItem *pItem);
+    bool MouseEvent(CGameMenuEvent &event);
+};
+
+class CGameMenuMgr
+{
+public:
+    static bool m_bInitialized;
+    static bool m_bActive;
+    static bool m_bFirstPush;
+    CGameMenu *pTempMenu;
+    CGameMenu *pActiveMenu;
+    CGameMenu *pMenuStack[8];
+    int nMenuPointer;
+    int32_t m_mouselastactivity;
+    int32_t m_mousewake_watchpoint, m_menuchange_watchpoint;
+    int32_t m_mousecaught;
+    vec2_t m_prevmousepos, m_mousepos, m_mousedownpos;
+    bool m_postPop;
+    CGameMenuMgr();
+    ~CGameMenuMgr();
+    void InitializeMenu(void);
+    void DeInitializeMenu(void);
+    bool Push(CGameMenu *pMenu, int data);
+    void Pop(void);
+    void PostPop(void);
+    void Draw(void);
+    void Clear(void);
+    void Process(void);
+    void Deactivate(void);
+    bool MouseOutsideBounds(vec2_t const * const pos, const int32_t x, const int32_t y, const int32_t width, const int32_t height);
+};
+
+extern CMenuTextMgr gMenuTextMgr;
+extern CGameMenuMgr gGameMenuMgr;
diff --git a/source/blood/src/gameutil.cpp b/source/blood/src/gameutil.cpp
new file mode 100644
index 000000000..2c9a50067
--- /dev/null
+++ b/source/blood/src/gameutil.cpp
@@ -0,0 +1,904 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "build.h"
+#include "common_game.h"
+
+#include "actor.h"
+#include "db.h"
+#include "gameutil.h"
+#include "globals.h"
+#include "tile.h"
+#include "trig.h"
+
+POINT2D baseWall[kMaxWalls];
+POINT3D baseSprite[kMaxSprites];
+int baseFloor[kMaxSectors];
+int baseCeil[kMaxSectors];
+int velFloor[kMaxSectors];
+int velCeil[kMaxSectors];
+short gUpperLink[kMaxSectors];
+short gLowerLink[kMaxSectors];
+HITINFO gHitInfo;
+
+bool AreSectorsNeighbors(int sect1, int sect2)
+{
+    dassert(sect1 >= 0 && sect1 < kMaxSectors);
+    dassert(sect2 >= 0 && sect2 < kMaxSectors);
+    if (sector[sect1].wallnum < sector[sect2].wallnum)
+    {
+        for (int i = 0; i < sector[sect1].wallnum; i++)
+        {
+            if (wall[sector[sect1].wallptr+i].nextsector == sect2)
+            {
+                return 1;
+            }
+        }
+    }
+    else
+    {
+        for (int i = 0; i < sector[sect2].wallnum; i++)
+        {
+            if (wall[sector[sect2].wallptr+i].nextsector == sect1)
+            {
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+bool FindSector(int nX, int nY, int nZ, int *nSector)
+{
+    int32_t nZFloor, nZCeil;
+    dassert(*nSector >= 0 && *nSector < kMaxSectors);
+    if (inside(nX, nY, *nSector))
+    {
+        getzsofslope(*nSector, nX, nY, &nZCeil, &nZFloor);
+        if (nZ >= nZCeil && nZ <= nZFloor)
+        {
+            return 1;
+        }
+    }
+    walltype *pWall = &wall[sector[*nSector].wallptr];
+    for (int i = sector[*nSector].wallnum; i > 0; i--, pWall++)
+    {
+        int nOSector = pWall->nextsector;
+        if (nOSector >= 0 && inside(nX, nY, nOSector))
+        {
+            getzsofslope(nOSector, nX, nY, &nZCeil, &nZFloor);
+            if (nZ >= nZCeil && nZ <= nZFloor)
+            {
+                *nSector = nOSector;
+                return 1;
+            }
+        }
+    }
+    for (int i = 0; i < numsectors; i++)
+    {
+        if (inside(nX, nY, i))
+        {
+            getzsofslope(i, nX, nY, &nZCeil, &nZFloor);
+            if (nZ >= nZCeil && nZ <= nZFloor)
+            {
+                *nSector = i;
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
+
+bool FindSector(int nX, int nY, int *nSector)
+{
+    dassert(*nSector >= 0 && *nSector < kMaxSectors);
+    if (inside(nX, nY, *nSector))
+    {
+        return 1;
+    }
+    walltype *pWall = &wall[sector[*nSector].wallptr];
+    for (int i = sector[*nSector].wallnum; i > 0; i--, pWall++)
+    {
+        int nOSector = pWall->nextsector;
+        if (nOSector >= 0 && inside(nX, nY, nOSector))
+        {
+            *nSector = nOSector;
+            return 1;
+        }
+    }
+    for (int i = 0; i < numsectors; i++)
+    {
+        if (inside(nX, nY, i))
+        {
+            *nSector = i;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+void CalcFrameRate(void)
+{
+    static int ticks[64];
+    static int index;
+    if (ticks[index] != gFrameClock)
+    {
+        gFrameRate = (120*64)/(gFrameClock-ticks[index]);
+        ticks[index] = gFrameClock;
+    }
+    index = (index+1) & 63;
+}
+
+bool CheckProximity(spritetype *pSprite, int nX, int nY, int nZ, int nSector, int nDist)
+{
+    dassert(pSprite != NULL);
+    int oX = klabs(nX-pSprite->x)>>4;
+    if (oX >= nDist) return 0;
+
+    int oY = klabs(nY-pSprite->y)>>4;
+    if (oY >= nDist) return 0;
+
+    int oZ = klabs(nZ-pSprite->z)>>8;
+    if (oZ >= nDist) return 0;
+
+    if (approxDist(oX, oY) >= nDist) return 0;
+
+    int bottom, top;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    if (cansee(pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, nX, nY, nZ, nSector))
+        return 1;
+    if (cansee(pSprite->x, pSprite->y, bottom, pSprite->sectnum, nX, nY, nZ, nSector))
+        return 1;
+    if (cansee(pSprite->x, pSprite->y, top, pSprite->sectnum, nX, nY, nZ, nSector))
+        return 1;
+    return 0;
+}
+
+bool CheckProximityPoint(int nX1, int nY1, int nZ1, int nX2, int nY2, int nZ2, int nDist)
+{
+    int oX = klabs(nX2-nX1)>>4;
+    if (oX >= nDist)
+        return 0;
+    int oY = klabs(nY2-nY1)>>4;
+    if (oY >= nDist)
+        return 0;
+    int oZ = klabs(nZ2-nZ1)>>4;
+    if (oZ >= nDist)
+        return 0;
+    if (approxDist(oX, oY) >= nDist) return 0;
+    return 1;
+}
+
+bool CheckProximityWall(int nWall, int x, int y, int nDist)
+{
+    int x1 = wall[nWall].x;
+    int y1 = wall[nWall].y;
+    int x2 = wall[wall[nWall].point2].x;
+    int y2 = wall[wall[nWall].point2].y;
+    nDist <<= 4;
+    if (x1 < x2)
+    {
+        if (x <= x1 - nDist || x >= x2 + nDist)
+        {
+            return 0;
+        }
+    }
+    else
+    {
+        if (x <= x2 - nDist || x >= x1 + nDist)
+        {
+            return 0;
+        }
+        if (x1 == x2)
+        {
+            int px1 = x - x1;
+            int py1 = y - y1;
+            int px2 = x - x2;
+            int py2 = y - y2;
+            int dist1 = px1 * px1 + py1 * py1;
+            int dist2 = px2 * px2 + py2 * py2;
+            if (y1 < y2)
+            {
+                if (y <= y1 - nDist || y >= y2 + nDist)
+                {
+                    return 0;
+                }
+                if (y < y1)
+                {
+                    return dist1 < nDist * nDist;
+                }
+                if (y > y2)
+                {
+                    return dist2 < nDist * nDist;
+                }
+            }
+            else
+            {
+                if (y <= y2 - nDist || y >= y1 + nDist)
+                {
+                    return 0;
+                }
+                if (y < y2)
+                {
+                    return dist2 < nDist * nDist;
+                }
+                if (y > y1)
+                {
+                    return dist1 < nDist * nDist;
+                }
+            }
+            return 1;
+        }
+    }
+    if (y1 < y2)
+    {
+        if (y <= y1 - nDist || y >= y2 + nDist)
+        {
+            return 0;
+        }
+    }
+    else
+    {
+        if (y <= y2 - nDist || y >= y1 + nDist)
+        {
+            return 0;
+        }
+        if (y1 == y2)
+        {
+            int px1 = x - x1;
+            int py1 = y - y1;
+            int px2 = x - x2;
+            int py2 = y - y2;
+            int check1 = px1 * px1 + py1 * py1;
+            int check2 = px2 * px2 + py2 * py2;
+            if (x1 < x2)
+            {
+                if (x <= x1 - nDist || x >= x2 + nDist)
+                {
+                    return 0;
+                }
+                if (x < x1)
+                {
+                    return check1 < nDist * nDist;
+                }
+                if (x > x2)
+                {
+                    return check2 < nDist * nDist;
+                }
+            }
+            else
+            {
+                if (x <= x2 - nDist || x >= x1 + nDist)
+                {
+                    return 0;
+                }
+                if (x < x2)
+                {
+                    return check2 < nDist * nDist;
+                }
+                if (x > x1)
+                {
+                    return check1 < nDist * nDist;
+                }
+            }
+        }
+    }
+
+    int dx = x2 - x1;
+    int dy = y2 - y1;
+    int px = x - x2;
+    int py = y - y2;
+    int side = px * dx + dy * py;
+    if (side >= 0)
+    {
+        return px * px + py * py < nDist * nDist;
+    }
+    px = x - x1;
+    py = y - y1;
+    side = px * dx + dy * py;
+    if (side <= 0)
+    {
+        return px * px + py * py < nDist * nDist;
+    }
+    int check1 = px * dy - dx * py;
+    int check2 = dy * dy + dx * dx;
+    return check1 * check1 < check2 * nDist * nDist;
+}
+
+int GetWallAngle(int nWall)
+{
+    int nWall2 = wall[nWall].point2;
+    return getangle(wall[nWall2].x - wall[nWall].x, wall[nWall2].y - wall[nWall].y);
+}
+
+void GetWallNormal(int nWall, int *pX, int *pY)
+{
+    dassert(nWall >= 0 && nWall < kMaxWalls);
+    int nWall2 = wall[nWall].point2;
+    int dX = -(wall[nWall2].y - wall[nWall].y);
+    dX >>= 4;
+    int dY = wall[nWall2].x - wall[nWall].x;
+    dY >>= 4;
+    int nLength = ksqrt(dX*dX+dY*dY);
+    if (nLength <= 0)
+        nLength = 1;
+    *pX = divscale16(dX, nLength);
+    *pY = divscale16(dY, nLength);
+}
+
+bool IntersectRay(int wx, int wy, int wdx, int wdy, int x1, int y1, int z1, int x2, int y2, int z2, int *ix, int *iy, int *iz)
+{
+    int dX = x1 - x2;
+    int dY = y1 - y2;
+    int dZ = z1 - z2;
+    int side = wdx * dY - wdy * dX;
+    int dX2 = x1 - wx;
+    int dY2 = y1 - wy;
+    int check1 = dX2 * dY - dY2 * dX;
+    int check2 = wdx * dY2 - wdy * dX2;
+    if (side >= 0)
+    {
+        if (!side)
+            return 0;
+        if (check1 < 0)
+            return 0;
+        if (check2 < 0 || check2 >= side)
+            return 0;
+    }
+    else
+    {
+        if (check1 > 0)
+            return 0;
+        if (check2 > 0 || check2 <= side)
+            return 0;
+    }
+    int nScale = divscale16(check2, side);
+    *ix = x1 + mulscale16(dX, nScale);
+    *iy = y1 + mulscale16(dY, nScale);
+    *iz = z1 + mulscale16(dZ, nScale);
+    return 1;
+}
+
+int HitScan(spritetype *pSprite, int z, int dx, int dy, int dz, unsigned int nMask, int a8)
+{
+    dassert(pSprite != NULL);
+    dassert(dx != 0 || dy != 0);
+    gHitInfo.hitsect = -1;
+    gHitInfo.hitwall = -1;
+    gHitInfo.hitsprite = -1;
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int nSector = pSprite->sectnum;
+    int bakCstat = pSprite->cstat;
+    pSprite->cstat &= ~256;
+    if (a8)
+    {
+        hitscangoal.x = x + mulscale30(a8 << 4, Cos(pSprite->ang));
+        hitscangoal.y = y + mulscale30(a8 << 4, Sin(pSprite->ang));
+    }
+    else
+    {
+        hitscangoal.x = hitscangoal.y = 0x1ffffff;
+    }
+    vec3_t pos = { x, y, z };
+    hitdata_t hitData;
+    hitData.pos.z = gHitInfo.hitz;
+    hitscan(&pos, nSector, dx, dy, dz << 4, &hitData, nMask);
+    gHitInfo.hitsect = hitData.sect;
+    gHitInfo.hitwall = hitData.wall;
+    gHitInfo.hitsprite = hitData.sprite;
+    gHitInfo.hitx = hitData.pos.x;
+    gHitInfo.hity = hitData.pos.y;
+    gHitInfo.hitz = hitData.pos.z;
+    hitscangoal.x = hitscangoal.y = 0x1ffffff;
+    pSprite->cstat = bakCstat;
+    if (gHitInfo.hitsprite >= kMaxSprites || gHitInfo.hitwall >= kMaxWalls || gHitInfo.hitsect >= kMaxSectors)
+        return -1;
+    if (gHitInfo.hitsprite >= 0)
+        return 3;
+    if (gHitInfo.hitwall >= 0)
+    {
+        if (wall[gHitInfo.hitwall].nextsector == -1)
+            return 0;
+        int nZCeil, nZFloor;
+        getzsofslope(wall[gHitInfo.hitwall].nextsector, gHitInfo.hitx, gHitInfo.hity, &nZCeil, &nZFloor);
+        if (gHitInfo.hitz <= nZCeil || gHitInfo.hitz >= nZFloor)
+            return 0;
+        return 4;
+    }
+    if (gHitInfo.hitsect >= 0)
+        return 1 + (z < gHitInfo.hitz);
+    return -1;
+}
+
+int VectorScan(spritetype *pSprite, int nOffset, int nZOffset, int dx, int dy, int dz, int nRange, int ac)
+{
+    int nNum = 256;
+    dassert(pSprite != NULL);
+    gHitInfo.hitsect = -1;
+    gHitInfo.hitwall = -1;
+    gHitInfo.hitsprite = -1;
+    int x1 = pSprite->x+mulscale30(nOffset, Cos(pSprite->ang+512));
+    int y1 = pSprite->y+mulscale30(nOffset, Sin(pSprite->ang+512));
+    int z1 = pSprite->z+nZOffset;
+    int bakCstat = pSprite->cstat;
+    pSprite->cstat &= ~256;
+    int nSector = pSprite->sectnum;
+    if (nRange)
+    {
+        hitscangoal.x = x1+mulscale30(nRange<<4, Cos(pSprite->ang));
+        hitscangoal.y = y1+mulscale30(nRange<<4, Sin(pSprite->ang));
+    }
+    else
+    {
+        hitscangoal.x = hitscangoal.y = 0x1fffffff;
+    }
+    vec3_t pos = { x1, y1, z1 };
+    hitdata_t hitData;
+    hitData.pos.z = gHitInfo.hitz;
+    hitscan(&pos, nSector, dx, dy, dz << 4, &hitData, CLIPMASK1);
+    gHitInfo.hitsect = hitData.sect;
+    gHitInfo.hitwall = hitData.wall;
+    gHitInfo.hitsprite = hitData.sprite;
+    gHitInfo.hitx = hitData.pos.x;
+    gHitInfo.hity = hitData.pos.y;
+    gHitInfo.hitz = hitData.pos.z;
+    hitscangoal.x = hitscangoal.y = 0x1ffffff;
+    pSprite->cstat = bakCstat;
+    while (nNum--)
+    {
+        if (gHitInfo.hitsprite >= kMaxSprites || gHitInfo.hitwall >= kMaxWalls || gHitInfo.hitsect >= kMaxSectors)
+            return -1;
+        if (nRange && approxDist(gHitInfo.hitx - pSprite->x, gHitInfo.hity - pSprite->y) > nRange)
+            return -1;
+        if (gHitInfo.hitsprite >= 0)
+        {
+            spritetype *pOther = &sprite[gHitInfo.hitsprite];
+            if ((pOther->hitag & 8) && !(ac & 1))
+                return 3;
+            if ((pOther->cstat & 0x30) != 0)
+                return 3;
+            int nPicnum = pOther->picnum;
+            if (tilesiz[nPicnum].x == 0 || tilesiz[nPicnum].y == 0)
+                return 3;
+            int height = (tilesiz[nPicnum].y*pOther->yrepeat)<<2;
+            int otherZ = pOther->z;
+            if (pOther->cstat & 0x80)
+                otherZ += height / 2;
+            int nOffset = picanm[nPicnum].yofs;
+            if (nOffset)
+                otherZ -= (nOffset*pOther->yrepeat)<<2;
+            dassert(height > 0);
+            int height2 = scale(otherZ-gHitInfo.hitz, tilesiz[nPicnum].y, height);
+            if (!(pOther->cstat & 8))
+                height2 = tilesiz[nPicnum].y-height2;
+            if (height2 >= 0 && height2 < tilesiz[nPicnum].y)
+            {
+                int width = (tilesiz[nPicnum].x*pOther->xrepeat)>>2;
+                width = (width*3)/4;
+                int check1 = ((y1 - pOther->y)*dx - (x1 - pOther->x)*dy) / ksqrt(dx*dx+dy*dy);
+                dassert(width > 0);
+                int width2 = scale(check1, tilesiz[nPicnum].x, width);
+                int nOffset = picanm[nPicnum].xofs;
+                width2 += nOffset + tilesiz[nPicnum].x / 2;
+                if (width2 >= 0 && width2 < tilesiz[nPicnum].x)
+                {
+                    char *pData = tileLoadTile(nPicnum);
+                    if (pData[width2*tilesiz[nPicnum].y+height2] != (char)255)
+                        return 3;
+                }
+            }
+            int bakCstat = pOther->cstat;
+            pOther->cstat &= ~256;
+            gHitInfo.hitsect = -1;
+            gHitInfo.hitwall = -1;
+            gHitInfo.hitsprite = -1;
+            x1 = gHitInfo.hitx;
+            y1 = gHitInfo.hity;
+            z1 = gHitInfo.hitz;
+            pos = { x1, y1, z1 };
+            hitData.pos.z = gHitInfo.hitz;
+            hitscan(&pos, pOther->sectnum,
+                dx, dy, dz << 4, &hitData, CLIPMASK1);
+            gHitInfo.hitsect = hitData.sect;
+            gHitInfo.hitwall = hitData.wall;
+            gHitInfo.hitsprite = hitData.sprite;
+            gHitInfo.hitx = hitData.pos.x;
+            gHitInfo.hity = hitData.pos.y;
+            gHitInfo.hitz = hitData.pos.z;
+            pOther->cstat = bakCstat;
+            continue;
+        }
+        if (gHitInfo.hitwall >= 0)
+        {
+            walltype *pWall = &wall[gHitInfo.hitwall];
+            if (pWall->nextsector == -1)
+                return 0;
+            sectortype *pSector = &sector[gHitInfo.hitsect];
+            sectortype *pSectorNext = &sector[pWall->nextsector];
+            int nZCeil, nZFloor;
+            getzsofslope(pWall->nextsector, gHitInfo.hitx, gHitInfo.hity, &nZCeil, &nZFloor);
+            if (gHitInfo.hitz <= nZCeil)
+                return 0;
+            if (gHitInfo.hitz >= nZFloor)
+            {
+                if (!(pSector->floorstat&1) || !(pSectorNext->floorstat&1))
+                    return 0;
+                return 2;
+            }
+            if (!(pWall->cstat & 0x30))
+                return 0;
+            int nOffset;
+            if (pWall->cstat & 4)
+                nOffset = ClipHigh(pSector->floorz, pSectorNext->floorz);
+            else
+                nOffset = ClipLow(pSector->ceilingz, pSectorNext->ceilingz);
+            nOffset = (gHitInfo.hitz - nOffset) >> 8;
+            if (pWall->cstat & 256)
+                nOffset = -nOffset;
+
+            int nPicnum = pWall->overpicnum;
+            int nSizX = tilesiz[nPicnum].x;
+            int nSizY = tilesiz[nPicnum].y;
+            if (!nSizX || !nSizY)
+                return 0;
+
+            int potX = nSizX == (1<<(picsiz[nPicnum]&15));
+            int potY = nSizY == (1<<(picsiz[nPicnum]>>4));
+
+            nOffset = (nOffset*pWall->yrepeat) / 8;
+            nOffset += (nSizY*pWall->ypanning) / 256;
+            int nLength = approxDist(pWall->x - wall[pWall->point2].x, pWall->y - wall[pWall->point2].y);
+            int nHOffset;
+            if (pWall->cstat & 8)
+                nHOffset = approxDist(gHitInfo.hitx - wall[pWall->point2].x, gHitInfo.hity - wall[pWall->point2].y);
+            else
+                nHOffset = approxDist(gHitInfo.hitx - pWall->x, gHitInfo.hity - pWall->y);
+
+            nHOffset = pWall->xpanning + ((nHOffset*pWall->xrepeat) << 3) / nLength;
+            if (potX)
+                nHOffset &= nSizX - 1;
+            else
+                nHOffset %= nSizX;
+            if (potY)
+                nOffset &= nSizY - 1;
+            else
+                nOffset %= nSizY;
+            char *pData = tileLoadTile(nPicnum);
+            int nPixel;
+            if (potY)
+                nPixel = (nHOffset<<(picsiz[nPicnum]>>4)) + nOffset;
+            else
+                nPixel = nHOffset*nSizY + nOffset;
+
+            if (pData[nPixel] == (char)255)
+            {
+                int bakCstat = pWall->cstat;
+                pWall->cstat &= ~64;
+                int bakCstat2 = wall[pWall->nextwall].cstat;
+                wall[pWall->nextwall].cstat &= ~64;
+                gHitInfo.hitsect = -1;
+                gHitInfo.hitwall = -1;
+                gHitInfo.hitsprite = -1;
+                x1 = gHitInfo.hitx;
+                y1 = gHitInfo.hity;
+                z1 = gHitInfo.hitz;
+                pos = { x1, y1, z1 };
+                hitData.pos.z = gHitInfo.hitz;
+                hitscan(&pos, pWall->nextsector,
+                    dx, dy, dz << 4, &hitData, CLIPMASK1);
+                gHitInfo.hitsect = hitData.sect;
+                gHitInfo.hitwall = hitData.wall;
+                gHitInfo.hitsprite = hitData.sprite;
+                gHitInfo.hitx = hitData.pos.x;
+                gHitInfo.hity = hitData.pos.y;
+                gHitInfo.hitz = hitData.pos.z;
+                pWall->cstat = bakCstat;
+                wall[pWall->nextwall].cstat = bakCstat2;
+                continue;
+            }
+            return 4;
+        }
+        if (gHitInfo.hitsect >= 0)
+        {
+            if (dz > 0)
+            {
+                if (gUpperLink[gHitInfo.hitsect] < 0)
+                    return 2;
+                int nSprite = gUpperLink[gHitInfo.hitsect];
+                int nLink = sprite[nSprite].owner & 0xfff;
+                gHitInfo.hitsect = -1;
+                gHitInfo.hitwall = -1;
+                gHitInfo.hitsprite = -1;
+                x1 = gHitInfo.hitx + sprite[nLink].x - sprite[nSprite].x;
+                y1 = gHitInfo.hity + sprite[nLink].y - sprite[nSprite].y;
+                z1 = gHitInfo.hitz + sprite[nLink].z - sprite[nSprite].z;
+                pos = { x1, y1, z1 };
+                hitData.pos.z = gHitInfo.hitz;
+                hitscan(&pos, sprite[nLink].sectnum, dx, dy, dz<<4, &hitData, CLIPMASK1);
+                gHitInfo.hitsect = hitData.sect;
+                gHitInfo.hitwall = hitData.wall;
+                gHitInfo.hitsprite = hitData.sprite;
+                gHitInfo.hitx = hitData.pos.x;
+                gHitInfo.hity = hitData.pos.y;
+                gHitInfo.hitz = hitData.pos.z;
+                continue;
+            }
+            else
+            {
+                if (gLowerLink[gHitInfo.hitsect] < 0)
+                    return 1;
+                int nSprite = gLowerLink[gHitInfo.hitsect];
+                int nLink = sprite[nSprite].owner & 0xfff;
+                gHitInfo.hitsect = -1;
+                gHitInfo.hitwall = -1;
+                gHitInfo.hitsprite = -1;
+                x1 = gHitInfo.hitx + sprite[nLink].x - sprite[nSprite].x;
+                y1 = gHitInfo.hity + sprite[nLink].y - sprite[nSprite].y;
+                z1 = gHitInfo.hitz + sprite[nLink].z - sprite[nSprite].z;
+                pos = { x1, y1, z1 };
+                hitData.pos.z = gHitInfo.hitz;
+                hitscan(&pos, sprite[nLink].sectnum, dx, dy, dz<<4, &hitData, CLIPMASK1);
+                gHitInfo.hitsect = hitData.sect;
+                gHitInfo.hitwall = hitData.wall;
+                gHitInfo.hitsprite = hitData.sprite;
+                gHitInfo.hitx = hitData.pos.x;
+                gHitInfo.hity = hitData.pos.y;
+                gHitInfo.hitz = hitData.pos.z;
+                continue;
+            }
+        }
+        return -1;
+    }
+    return -1;
+}
+
+void GetZRange(spritetype *pSprite, int *ceilZ, int *ceilHit, int *floorZ, int *floorHit, int nDist, unsigned int nMask)
+{
+    dassert(pSprite != NULL);
+    int bakCstat = pSprite->cstat;
+    int32_t nTemp1, nTemp2;
+    pSprite->cstat &= ~257;
+    getzrange_old(pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum, (int32_t*)ceilZ, (int32_t*)ceilHit, (int32_t*)floorZ, (int32_t*)floorHit, nDist, nMask);
+    if (((*floorHit) & 0xe000) == 0x4000)
+    {
+        int nSector = (*floorHit) & 0x1fff;
+        if ((nMask & 0x2000) == 0 && (sector[nSector].floorstat & 1))
+            *floorZ = 0x7fffffff;
+        if (sector[nSector].extra > 0)
+        {
+            XSECTOR *pXSector = &xsector[sector[nSector].extra];
+            *floorZ += pXSector->Depth << 10;
+        }
+        if (gUpperLink[nSector] >= 0)
+        {
+            int nSprite = gUpperLink[nSector];
+            int nLink = sprite[nSprite].owner & 0xfff;
+            getzrange_old(pSprite->x+sprite[nLink].x-sprite[nSprite].x, pSprite->y+sprite[nLink].y-sprite[nSprite].y,
+                pSprite->z+sprite[nLink].z-sprite[nSprite].z, sprite[nLink].sectnum, &nTemp1, &nTemp2, (int32_t*)floorZ, (int32_t*)floorHit,
+                nDist, nMask);
+            *floorZ -= sprite[nLink].z - sprite[nSprite].z;
+        }
+    }
+    if (((*ceilHit) & 0xe000) == 0x4000)
+    {
+        int nSector = (*ceilHit) & 0x1fff;
+        if ((nMask & 0x1000) == 0 && (sector[nSector].ceilingstat & 1))
+            *ceilZ = 0x80000000;
+        if (gLowerLink[nSector] >= 0)
+        {
+            int nSprite = gLowerLink[nSector];
+            int nLink = sprite[nSprite].owner & 0xfff;
+            getzrange_old(pSprite->x+sprite[nLink].x-sprite[nSprite].x, pSprite->y+sprite[nLink].y-sprite[nSprite].y,
+                pSprite->z+sprite[nLink].z-sprite[nSprite].z, sprite[nLink].sectnum, (int32_t*)ceilZ, (int32_t*)ceilHit, &nTemp1, &nTemp2,
+                nDist, nMask);
+            *ceilZ -= sprite[nLink].z - sprite[nSprite].z;
+        }
+    }
+    pSprite->cstat = bakCstat;
+}
+
+void GetZRangeAtXYZ(int x, int y, int z, int nSector, int *ceilZ, int *ceilHit, int *floorZ, int *floorHit, int nDist, unsigned int nMask)
+{
+    int32_t nTemp1, nTemp2;
+    getzrange_old(x, y, z, nSector, (int32_t*)ceilZ, (int32_t*)ceilHit, (int32_t*)floorZ, (int32_t*)floorHit, nDist, nMask);
+    if (((*floorHit) & 0xe000) == 0x4000)
+    {
+        int nSector = (*floorHit) & 0x1fff;
+        if ((nMask & 0x2000) == 0 && (sector[nSector].floorstat & 1))
+            *floorZ = 0x7fffffff;
+        if (sector[nSector].extra > 0)
+        {
+            XSECTOR *pXSector = &xsector[sector[nSector].extra];
+            *floorZ += pXSector->Depth << 10;
+        }
+        if (gUpperLink[nSector] >= 0)
+        {
+            int nSprite = gUpperLink[nSector];
+            int nLink = sprite[nSprite].owner & 0xfff;
+            getzrange_old(x+sprite[nLink].x-sprite[nSprite].x, y+sprite[nLink].y-sprite[nSprite].y,
+                z+sprite[nLink].z-sprite[nSprite].z, sprite[nLink].sectnum, &nTemp1, &nTemp2, (int32_t*)floorZ, (int32_t*)floorHit,
+                nDist, nMask);
+            *floorZ -= sprite[nLink].z - sprite[nSprite].z;
+        }
+    }
+    if (((*ceilHit) & 0xe000) == 0x4000)
+    {
+        int nSector = (*ceilHit) & 0x1fff;
+        if ((nMask & 0x1000) == 0 && (sector[nSector].ceilingstat & 1))
+            *ceilZ = 0x80000000;
+        if (gLowerLink[nSector] >= 0)
+        {
+            int nSprite = gLowerLink[nSector];
+            int nLink = sprite[nSprite].owner & 0xfff;
+            getzrange_old(x+sprite[nLink].x-sprite[nSprite].x, y+sprite[nLink].y-sprite[nSprite].y,
+                z+sprite[nLink].z-sprite[nSprite].z, sprite[nLink].sectnum, (int32_t*)ceilZ, (int32_t*)ceilHit, &nTemp1, &nTemp2,
+                nDist, nMask);
+            *ceilZ -= sprite[nLink].z - sprite[nSprite].z;
+        }
+    }
+}
+
+int GetDistToLine(int x1, int y1, int x2, int y2, int x3, int y3)
+{
+    int check = (y1-y3)*(x3-x2);
+    int check2 = (x1-x2)*(y3-y2);
+    if (check2 > check)
+        return -1;
+    int v8 = dmulscale(x1-x2,x3-x2,y1-y3,y3-y2,4);
+    int vv = dmulscale(x3-x2,x3-x2,y3-y2,y3-y2,4);
+    int t1, t2;
+    if (v8 <= 0)
+    {
+        t1 = x2;
+        t2 = y2;
+    }
+    else if (vv > v8)
+    {
+        t1 = x2+scale(x3-x2,v8,vv);
+        t2 = y2+scale(y3-y2,v8,vv);
+    }
+    else
+    {
+        t1 = x3;
+        t2 = y3;
+    }
+    return approxDist(t1-x1, t2-y1);
+}
+
+unsigned int ClipMove(int *x, int *y, int *z, int *nSector, int xv, int yv, int wd, int cd, int fd, unsigned int nMask)
+{
+    int bakX = *x;
+    int bakY = *y;
+    int bakZ = *z;
+    short bakSect = *nSector;
+    unsigned int nRes = clipmove_old((int32_t*)x, (int32_t*)y, (int32_t*)z, &bakSect, xv<<14, yv<<14, wd, cd, fd, nMask);
+    if (bakSect == -1)
+    {
+        *x = bakX; *y = bakY; *z = bakZ;
+    }
+    else
+    {
+        *nSector = bakSect;
+    }
+    return nRes;
+}
+
+int GetClosestSectors(int nSector, int x, int y, int nDist, short *pSectors, char *pSectBit)
+{
+    char sectbits[(kMaxSectors+7)>>3];
+    dassert(pSectors != NULL);
+    memset(sectbits, 0, sizeof(sectbits));
+    pSectors[0] = nSector;
+    SetBitString(sectbits, nSector);
+    int n = 1;
+    int i = 0;
+    if (pSectBit)
+    {
+        memset(pSectBit, 0, (kMaxSectors+7)>>3);
+        SetBitString(pSectBit, nSector);
+    }
+    while (i < n)
+    {
+        int nCurSector = pSectors[i];
+        int nStartWall = sector[nCurSector].wallptr;
+        int nEndWall = nStartWall + sector[nCurSector].wallnum;
+        walltype *pWall = &wall[nStartWall];
+        for (int j = nStartWall; j < nEndWall; j++, pWall++)
+        {
+            int nNextSector = pWall->nextsector;
+            if (nNextSector < 0)
+                continue;
+            if (TestBitString(sectbits, nNextSector))
+                continue;
+            SetBitString(sectbits, nNextSector);
+            int dx = klabs(wall[pWall->point2].x - x)>>4;
+            int dy = klabs(wall[pWall->point2].y - y)>>4;
+            if (dx < nDist && dy < nDist)
+            {
+                if (approxDist(dx, dy) < nDist)
+                {
+                    if (pSectBit)
+                        SetBitString(pSectBit, nNextSector);
+                    pSectors[n++] = nNextSector;
+                }
+            }
+        }
+        i++;
+    }
+    pSectors[n] = -1;
+    return n;
+}
+
+int GetClosestSpriteSectors(int nSector, int x, int y, int nDist, short *pSectors, char *pSectBit, short *a8)
+{
+    char sectbits[(kMaxSectors+7)>>3];
+    dassert(pSectors != NULL);
+    memset(sectbits, 0, sizeof(sectbits));
+    pSectors[0] = nSector;
+    SetBitString(sectbits, nSector);
+    int n = 1, m = 0;
+    int i = 0;
+    if (pSectBit)
+    {
+        memset(pSectBit, 0, (kMaxSectors+7)>>3);
+        SetBitString(pSectBit, nSector);
+    }
+    while (i < n)
+    {
+        int nCurSector = pSectors[i];
+        int nStartWall = sector[nCurSector].wallptr;
+        int nEndWall = nStartWall + sector[nCurSector].wallnum;
+        walltype *pWall = &wall[nStartWall];
+        for (int j = nStartWall; j < nEndWall; j++, pWall++)
+        {
+            int nNextSector = pWall->nextsector;
+            if (nNextSector < 0)
+                continue;
+            if (TestBitString(sectbits, nNextSector))
+                continue;
+            SetBitString(sectbits, nNextSector);
+            if (CheckProximityWall(wall[j].point2, x, y, nDist))
+            {
+                if (pSectBit)
+                    SetBitString(pSectBit, nNextSector);
+                pSectors[n++] = nNextSector;
+                if (a8 && pWall->extra > 0)
+                {
+                    XWALL *pXWall = &xwall[pWall->extra];
+                    if (pXWall->triggerVector && !pXWall->isTriggered && !pXWall->state)
+                        a8[m++] = j;
+                }
+            }
+        }
+        i++;
+    }
+    pSectors[n] = -1;
+    if (a8)
+    {
+        a8[m] = -1;
+    }
+    return n;
+}
diff --git a/source/blood/src/gameutil.h b/source/blood/src/gameutil.h
new file mode 100644
index 000000000..2b104582d
--- /dev/null
+++ b/source/blood/src/gameutil.h
@@ -0,0 +1,63 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common_game.h"
+
+struct HITINFO {
+    short hitsect;
+    short hitwall;
+    short hitsprite;
+    int hitx;
+    int hity;
+    int hitz;
+};
+
+extern POINT2D baseWall[kMaxWalls];
+extern POINT3D baseSprite[kMaxSprites];
+extern int baseFloor[kMaxSectors];
+extern int baseCeil[kMaxSectors];
+extern int velFloor[kMaxSectors];
+extern int velCeil[kMaxSectors];
+extern short gUpperLink[kMaxSectors];
+extern short gLowerLink[kMaxSectors];
+extern HITINFO gHitInfo;
+
+bool AreSectorsNeighbors(int sect1, int sect2);
+bool FindSector(int nX, int nY, int nZ, int *nSector);
+bool FindSector(int nX, int nY, int *nSector);
+void CalcFrameRate(void);
+bool CheckProximity(spritetype *pSprite, int nX, int nY, int nZ, int nSector, int nDist);
+bool CheckProximityPoint(int nX1, int nY1, int nZ1, int nX2, int nY2, int nZ2, int nDist);
+bool CheckProximityWall(int nWall, int x, int y, int nDist);
+int GetWallAngle(int nWall);
+void GetWallNormal(int nWall, int *pX, int *pY);
+bool IntersectRay(int wx, int wy, int wdx, int wdy, int x1, int y1, int z1, int x2, int y2, int z2, int *ix, int *iy, int *iz);
+int HitScan(spritetype *pSprite, int z, int dx, int dy, int dz, unsigned int nMask, int a8);
+int VectorScan(spritetype *pSprite, int nOffset, int nZOffset, int dx, int dy, int dz, int nRange, int ac);
+void GetZRange(spritetype *pSprite, int *ceilZ, int *ceilHit, int *floorZ, int *floorHit, int nDist, unsigned int nMask);
+void GetZRangeAtXYZ(int x, int y, int z, int nSector, int *ceilZ, int *ceilHit, int *floorZ, int *floorHit, int nDist, unsigned int nMask);
+int GetDistToLine(int x1, int y1, int x2, int y2, int x3, int y3);
+unsigned int ClipMove(int *x, int *y, int *z, int *nSector, int xv, int yv, int wd, int cd, int fd, unsigned int nMask);
+int GetClosestSectors(int nSector, int x, int y, int nDist, short *pSectors, char *pSectBit);
+int GetClosestSpriteSectors(int nSector, int x, int y, int nDist, short *pSectors, char *pSectBit, short *a8);
diff --git a/source/blood/src/getopt.cpp b/source/blood/src/getopt.cpp
new file mode 100644
index 000000000..aed9da806
--- /dev/null
+++ b/source/blood/src/getopt.cpp
@@ -0,0 +1,92 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdlib.h>
+#include <string.h>
+#include "compat.h"
+#include "getopt.h"
+
+int margc;
+char const * const *margv;
+
+const char *OptArgv[16];
+int OptArgc;
+const char *OptFull;
+const char *SwitchChars = "-/";
+
+int GetOptions(SWITCH *switches)
+{
+    static const char *pChar = NULL;
+    static int OptIndex = 1;
+    if (!pChar)
+    {
+        if (OptIndex >= margc)
+            return -1;
+        pChar = margv[OptIndex++];
+        if (!pChar)
+            return -1;
+    }
+    OptFull = pChar;
+    if (!strchr(SwitchChars, *pChar))
+    {
+        pChar = NULL;
+        return -2;
+    }
+    pChar++;
+    int i;
+    int vd;
+    for (i = 0; true; i++)
+    {
+        if (!switches[i].name)
+            return -3;
+        int nLength = strlen(switches[i].name);
+        if (!Bstrncasecmp(pChar, switches[i].name, nLength) && (pChar[nLength]=='=' || pChar[nLength]==0))
+        {
+            pChar += nLength;
+            if (*pChar=='=')
+            {
+                pChar++;
+            }
+            else
+            {
+                pChar = NULL;
+            }
+            break;
+        }
+    }
+    vd = switches[i].at4;
+    OptArgc = 0;
+    while (OptArgc < switches[i].at8)
+    {
+        if (!pChar)
+        {
+            if (OptIndex >= margc)
+                break;
+            pChar = margv[OptIndex++];
+            if (strchr(SwitchChars, *pChar) != 0)
+                break;
+        }
+        OptArgv[OptArgc++] = pChar;
+        pChar = NULL;
+    }
+    return vd;
+}
diff --git a/source/blood/src/getopt.h b/source/blood/src/getopt.h
new file mode 100644
index 000000000..d0bb6e7e1
--- /dev/null
+++ b/source/blood/src/getopt.h
@@ -0,0 +1,37 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+
+extern int margc;
+extern char const * const *margv;
+
+extern const char *OptArgv[16];
+extern int OptArgc;
+extern const char *OptFull;
+
+struct SWITCH {
+    const char *name;
+    int at4, at8;
+};
+int GetOptions(SWITCH *switches);
\ No newline at end of file
diff --git a/source/blood/src/gib.cpp b/source/blood/src/gib.cpp
new file mode 100644
index 000000000..58cfa0a4d
--- /dev/null
+++ b/source/blood/src/gib.cpp
@@ -0,0 +1,509 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "common_game.h"
+#include "actor.h"
+#include "blood.h"
+#include "db.h"
+#include "callback.h"
+#include "config.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gib.h"
+#include "levels.h"
+#include "sfx.h"
+#include "trig.h"
+
+struct GIBFX
+{
+    FX_ID at0;
+    int at1;
+    int chance;
+    int at9;
+    int atd;
+    int at11;
+};
+
+
+struct GIBTHING
+{
+    int at0;
+    int at4;
+    int chance;
+    int atc;
+    int at10;
+};
+
+struct GIBLIST
+{
+    GIBFX *at0;
+    int at4;
+    GIBTHING *at8;
+    int atc;
+    int at10;
+};
+
+GIBFX gibFxGlassT[] = {
+    { FX_18, 0, 65536, 3, 200, 400 },
+    { FX_31, 0, 32768, 5, 200, 400 }
+};
+
+GIBFX gibFxGlassS[] = {
+    { FX_18, 0, 65536, 8, 200, 400 }
+};
+
+GIBFX gibFxBurnShard[] = {
+    { FX_16, 0, 65536, 12, 500, 1000 }
+};
+
+GIBFX gibFxWoodShard[] = {
+    { FX_17, 0, 65536, 12, 500, 1000 }
+};
+
+GIBFX gibFxMetalShard[] = {
+    { FX_30, 0, 65536, 12, 500, 1000 }
+};
+
+GIBFX gibFxFireSpark[] = {
+    { FX_14, 0, 65536, 8, 500, 1000 }
+};
+
+GIBFX gibFxShockSpark[] = {
+    { FX_15, 0, 65536, 8, 500, 1000 }
+};
+
+GIBFX gibFxBloodChunks[] = {
+    { FX_13, 0, 65536, 8, 90, 600 }
+};
+
+GIBFX gibFxBubblesS[] = {
+    { FX_25, 0, 65536, 8, 200, 400 }
+};
+
+GIBFX gibFxBubblesM[] = {
+    { FX_24, 0, 65536, 8, 200, 400 }
+};
+
+GIBFX gibFxBubblesL[] = {
+    { FX_23, 0, 65536, 8, 200, 400 }
+};
+
+GIBFX gibFxIcicles[] = {
+    { FX_31, 0, 65536, 15, 200, 400 }
+};
+
+GIBFX gibFxGlassCombo1[] = {
+    { FX_18, 0, 65536, 15, 200, 400 },
+    { FX_31, 0, 65536, 10, 200, 400 }
+};
+
+GIBFX gibFxGlassCombo2[] = {
+    { FX_18, 0, 65536, 5, 200, 400 },
+    { FX_20, 0, 53248, 5, 200, 400 },
+    { FX_21, 0, 53248, 5, 200, 400 },
+    { FX_19, 0, 53248, 5, 200, 400 },
+    { FX_22, 0, 53248, 5, 200, 400 }
+};
+
+GIBFX gibFxWoodCombo[] = {
+    { FX_16, 0, 65536, 8, 500, 1000 },
+    { FX_17, 0, 65536, 8, 500, 1000 },
+    { FX_14, 0, 65536, 8, 500, 1000 }
+};
+
+GIBFX gibFxMedicCombo[] = {
+    { FX_18, 0, 32768, 7, 200, 400 },
+    { FX_30, 0, 65536, 7, 500, 1000 },
+    { FX_13, 0, 65536, 10, 90, 600 },
+    { FX_14, 0, 32768, 7, 500, 1000 }
+};
+
+GIBFX gibFxFlareSpark[] = {
+    { FX_28, 0, 32768, 15, 128, -128 }
+};
+
+GIBFX gibFxBloodBits[] = {
+    { FX_13, 0, 45056, 8, 90, 600 }
+};
+
+GIBFX gibFxRockShards[] = {
+    { FX_46, 0, 65536, 10, 300, 800 },
+    { FX_31, 0, 32768, 10, 200, 1000 }
+};
+
+GIBFX gibFxPaperCombo1[] = {
+    { FX_47, 0, 65536, 12, 300, 600 },
+    { FX_14, 0, 65536, 8, 500, 1000 }
+};
+
+GIBFX gibFxPlantCombo1[] = {
+    { FX_44, 0, 45056, 8, 400, 800 },
+    { FX_45, 0, 45056, 8, 300, 800 },
+    { FX_14, 0, 45056, 6, 500, 1000 }
+};
+
+GIBFX gibFx13BBA8[] = {
+    { FX_49, 0, 65536, 4, 80, 300 }
+};
+
+GIBFX gibFx13BBC0[] = {
+    { FX_50, 0, 65536, 4, 80, 0 }
+};
+
+GIBFX gibFx13BBD8[] = {
+    { FX_50, 0, 65536, 20, 800, -40 },
+    { FX_15, 0, 65536, 15, 400, 10 }
+};
+
+GIBFX gibFx13BC04[] = {
+    { FX_32, 0, 65536, 8, 100, 0 }
+};
+
+GIBFX gibFx13BC1C[] = {
+    { FX_56, 0, 65536, 8, 100, 0 }
+};
+
+GIBTHING gibHuman[] = {
+    { 425, 1454, 917504, 300, 900 },
+    { 425, 1454, 917504, 300, 900 },
+    { 425, 1267, 917504, 300, 900 },
+    { 425, 1267, 917504, 300, 900 },
+    { 425, 1268, 917504, 300, 900 },
+    { 425, 1269, 917504, 300, 900 },
+    { 425, 1456, 917504, 300, 900 }
+};
+
+GIBTHING gibMime[] = {
+    { 425, 2405, 917504, 300, 900 },
+    { 425, 2405, 917504, 300, 900 },
+    { 425, 2404, 917504, 300, 900 },
+    { 425, 1268, 32768, 300, 900 },
+    { 425, 1269, 32768, 300, 900 },
+    { 425, 1456, 32768, 300, 900 },
+};
+
+GIBTHING gibHound[] = {
+    { 425, 1326, 917504, 300, 900 },
+    { 425, 1268, 32768, 300, 900 },
+    { 425, 1269, 32768, 300, 900 },
+    { 425, 1456, 32768, 300, 900 }
+};
+
+GIBTHING gibFleshGargoyle[] = {
+    { 425, 1369, 917504, 300, 900 },
+    { 425, 1361, 917504, 300, 900 },
+    { 425, 1268, 32768, 300, 900 },
+    { 425, 1269, 32768, 300, 900 },
+    { 425, 1456, 32768, 300, 900 }
+};
+
+GIBTHING gibAxeZombieHead[] = {
+    { 427, 3405, 917504, 0, 0 }
+};
+
+GIBLIST gibList[] = {
+    { gibFxGlassT, 2, NULL, 0, 300 },
+    { gibFxGlassS, 1, NULL, 0, 300 },
+    { gibFxBurnShard, 1, NULL, 0, 0 },
+    { gibFxWoodShard, 1, NULL, 0, 0 },
+    { gibFxMetalShard, 1, NULL, 0, 0 },
+    { gibFxFireSpark, 1, NULL, 0, 0 },
+    { gibFxShockSpark, 1, NULL, 0, 0 },
+    { gibFxBloodChunks, 1, NULL, 0, 0 },
+    { gibFxBubblesS, 1, NULL, 0, 0 },
+    { gibFxBubblesM, 1, NULL, 0, 0 },
+    { gibFxBubblesL, 1, NULL, 0, 0 },
+    { gibFxIcicles, 1, NULL, 0, 0 },
+    { gibFxGlassCombo1, 2, NULL, 0, 300 },
+    { gibFxGlassCombo2, 5, NULL, 0, 300 },
+    { gibFxWoodCombo, 3, NULL, 0, 0 },
+    { NULL, 0, gibHuman, 7, 0 },
+    { gibFxMedicCombo, 4, NULL, 0, 0 },
+    { gibFxFlareSpark, 1, NULL, 0, 0 },
+    { gibFxBloodBits, 1, NULL, 0, 0 },
+    { gibFxRockShards, 2, NULL, 0, 0 },
+    { gibFxPaperCombo1, 2, NULL, 0, 0 },
+    { gibFxPlantCombo1, 3, NULL, 0, 0 },
+    { gibFx13BBA8, 1, NULL, 0, 0 },
+    { gibFx13BBC0, 1, NULL, 0, 0 },
+    { gibFx13BBD8, 2, NULL, 0, 0 },
+    { gibFx13BC04, 1, NULL, 0, 0 },
+    { gibFx13BC1C, 1, NULL, 0, 0 },
+    { NULL, 0, gibAxeZombieHead, 1, 0 },
+    { NULL, 0, gibMime, 6, 0 },
+    { NULL, 0, gibHound, 4, 0 },
+    { NULL, 0, gibFleshGargoyle, 5, 0 },
+};
+
+void gibCalcWallArea(int a1, int &a2, int &a3, int &a4, int &a5, int &a6, int &a7, int &a8)
+{
+    walltype *pWall = &wall[a1];
+    a2 = (pWall->x+wall[pWall->point2].x)>>1;
+    a3 = (pWall->y+wall[pWall->point2].y)>>1;
+    int nSector = sectorofwall(a1);
+    int32_t ceilZ, floorZ;
+    getzsofslope(nSector, a2, a3, &ceilZ, &floorZ);
+    int32_t ceilZ2, floorZ2;
+    getzsofslope(pWall->nextsector, a2, a3, &ceilZ2, &floorZ2);
+    ceilZ = ClipLow(ceilZ, ceilZ2);
+    floorZ = ClipHigh(floorZ, floorZ2);
+    a7 = floorZ-ceilZ;
+    a5 = wall[pWall->point2].x-pWall->x;
+    a6 = wall[pWall->point2].y-pWall->y;
+    a8 = (a7>>8)*approxDist(a5>>4, a6>>4);
+    a4 = (ceilZ+floorZ)>>1;
+}
+
+int ChanceToCount(int a1, int a2)
+{
+    int vb = a2;
+    if (a1 < 0x10000)
+    {
+        for (int i = 0; i < a2; i++)
+            if (!Chance(a1))
+                vb--;
+    }
+    return vb;
+}
+
+void GibFX(spritetype *pSprite, GIBFX *pGFX, CGibPosition *pPos, CGibVelocity *pVel)
+{
+    int nSector = pSprite->sectnum;
+    if (gbAdultContent && gGameOptions.nGameType == 0 && pGFX->at0 == FX_13)
+        return;
+    CGibPosition gPos(pSprite->x, pSprite->y, pSprite->z);
+    if (pPos)
+        gPos = *pPos;
+    int32_t ceilZ, floorZ;
+    getzsofslope(nSector, gPos.x, gPos.y, &ceilZ, &floorZ);
+    int nCount = ChanceToCount(pGFX->chance, pGFX->at9);
+    int dz1 = floorZ-gPos.z;
+    int dz2 = gPos.z-ceilZ;
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    for (int i = 0; i < nCount; i++)
+    {
+        if (!pPos && (pSprite->cstat&48) == 0)
+        {
+            int nAngle = Random(2048);
+            gPos.x = pSprite->x+mulscale30(pSprite->clipdist<<2, Cos(nAngle));
+            gPos.y = pSprite->y+mulscale30(pSprite->clipdist<<2, Sin(nAngle));
+            gPos.z = bottom-Random(bottom-top);
+        }
+        spritetype *pFX = gFX.fxSpawn(pGFX->at0, nSector, gPos.x, gPos.y, gPos.z, 0);
+        if (pFX)
+        {
+            if (pGFX->at1 < 0)
+                pFX->pal = pSprite->pal;
+            if (pVel)
+            {
+                xvel[pFX->index] = pVel->vx+Random2(pGFX->atd);
+                yvel[pFX->index] = pVel->vy+Random2(pGFX->atd);
+                zvel[pFX->index] = pVel->vz-Random(pGFX->at11);
+            }
+            else
+            {
+                xvel[pFX->index] = Random2((pGFX->atd<<18)/120);
+                yvel[pFX->index] = Random2((pGFX->atd<<18)/120);
+                switch(pSprite->cstat&48)
+                {
+                case 16:
+                    zvel[pFX->index] = Random2((pGFX->at11<<18)/120);
+                    break;
+                default:
+                    if (dz2 < dz1 && dz2 < 0x4000)
+                    {
+                        zvel[pFX->index] = 0;
+                    }
+                    else if (dz2 > dz1 && dz1 < 0x4000)
+                    {
+                        zvel[pFX->index] = -Random((klabs(pGFX->at11)<<18)/120);
+                    }
+                    else
+                    {
+                        if ((pGFX->at11<<18)/120 < 0)
+                            zvel[pFX->index] = -Random((klabs(pGFX->at11)<<18)/120);
+                        else
+                            zvel[pFX->index] = Random2((pGFX->at11<<18)/120);
+                    }
+                    break;
+                }
+            }
+        }
+    }
+}
+
+void GibThing(spritetype *pSprite, GIBTHING *pGThing, CGibPosition *pPos, CGibVelocity *pVel)
+{
+    if (gbAdultContent && gGameOptions.nGameType <= 0)
+        switch (pGThing->at0)
+        {
+        case 425:
+        case 427:
+            return;
+        }
+
+    if (pGThing->chance == 65536 || Chance(pGThing->chance))
+    {
+        int nSector = pSprite->sectnum;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        int x, y, z;
+        if (!pPos)
+        {
+            int nAngle = Random(2048);
+            x = pSprite->x+mulscale30(pSprite->clipdist<<2, Cos(nAngle));
+            y = pSprite->y+mulscale30(pSprite->clipdist<<2, Sin(nAngle));
+            z = bottom-Random(bottom-top);
+        }
+        else
+        {
+            x = pPos->x;
+            y = pPos->y;
+            z = pPos->z;
+        }
+        int32_t ceilZ, floorZ;
+        getzsofslope(nSector, x, y, &ceilZ, &floorZ);
+        int dz1 = floorZ-z;
+        int dz2 = z-ceilZ;
+        spritetype *pGib = actSpawnThing(nSector, x, y, z, pGThing->at0);
+        dassert(pGib != NULL);
+        if (pGThing->at4 > -1)
+            pGib->picnum = pGThing->at4;
+        if (pVel)
+        {
+            xvel[pGib->index] = pVel->vx+Random2(pGThing->atc);
+            yvel[pGib->index] = pVel->vy+Random2(pGThing->atc);
+            zvel[pGib->index] = pVel->vz-Random(pGThing->at10);
+        }
+        else
+        {
+            xvel[pGib->index] = Random2((pGThing->atc<<18)/120);
+            yvel[pGib->index] = Random2((pGThing->atc<<18)/120);
+            switch (pSprite->cstat&48)
+            {
+            case 16:
+                zvel[pGib->index] = Random2((pGThing->at10<<18)/120);
+                break;
+            default:
+                if (dz2 < dz1 && dz2 < 0x4000)
+                {
+                    zvel[pGib->index] = 0;
+                }
+                else if (dz2 > dz1 && dz1 < 0x4000)
+                {
+                    zvel[pGib->index] = -Random((pGThing->at10<<18)/120);
+                }
+                else
+                {
+                    zvel[pGib->index] = Random2((pGThing->at10<<18)/120);
+                }
+                break;
+            }
+        }
+    }
+}
+
+void GibSprite(spritetype *pSprite, GIBTYPE nGibType, CGibPosition *pPos, CGibVelocity *pVel)
+{
+    dassert(pSprite != NULL);
+    dassert(nGibType >= 0 && nGibType < kGibMax);
+    if (pSprite->sectnum < 0 || pSprite->sectnum >= numsectors)
+        return;
+    GIBLIST *pGib = &gibList[nGibType];
+    for (int i = 0; i < pGib->at4; i++)
+    {
+        GIBFX *pGibFX = &pGib->at0[i];
+        dassert(pGibFX->chance > 0);
+        GibFX(pSprite, pGibFX, pPos, pVel);
+    }
+    for (int i = 0; i < pGib->atc; i++)
+    {
+        GIBTHING *pGibThing = &pGib->at8[i];
+        dassert(pGibThing->chance > 0);
+        GibThing(pSprite, pGibThing, pPos, pVel);
+    }
+}
+
+void GibFX(int nWall, GIBFX * pGFX, int a3, int a4, int a5, int a6, CGibVelocity * pVel)
+{
+    dassert(nWall >= 0 && nWall < numwalls);
+    walltype *pWall = &wall[nWall];
+    int nCount = ChanceToCount(pGFX->chance, pGFX->at9);
+    int nSector = sectorofwall(nWall);
+    for (int i = 0; i < nCount; i++)
+    {
+        int r1 = Random(a6);
+        int r2 = Random(a5);
+        int r3 = Random(a4);
+        spritetype *pGib = gFX.fxSpawn(pGFX->at0, nSector, pWall->x+r3, pWall->y+r2, a3+r1, 0);
+        if (pGib)
+        {
+            if (pGFX->at1 < 0)
+                pGib->pal = pWall->pal;
+            if (!pVel)
+            {
+                xvel[pGib->index] = Random2((pGFX->atd<<18)/120);
+                yvel[pGib->index] = Random2((pGFX->atd<<18)/120);
+                zvel[pGib->index] = -Random((pGFX->at11<<18)/120);
+            }
+            else
+            {
+                xvel[pGib->index] = Random2((pVel->vx<<18)/120);
+                yvel[pGib->index] = Random2((pVel->vy<<18)/120);
+                zvel[pGib->index] = -Random((pVel->vz<<18)/120);
+            }
+        }
+    }
+}
+
+void GibWall(int nWall, GIBTYPE nGibType, CGibVelocity *pVel)
+{
+    dassert(nWall >= 0 && nWall < numwalls);
+    dassert(nGibType >= 0 && nGibType < kGibMax);
+    int cx, cy, cz, wx, wy, wz;
+    walltype *pWall = &wall[nWall];
+    cx = (pWall->x+wall[pWall->point2].x)>>1;
+    cy = (pWall->y+wall[pWall->point2].y)>>1;
+    int nSector = sectorofwall(nWall);
+    int32_t ceilZ, floorZ;
+    getzsofslope(nSector, cx, cy, &ceilZ, &floorZ);
+    int32_t ceilZ2, floorZ2;
+    getzsofslope(pWall->nextsector, cx, cy, &ceilZ2, &floorZ2);
+    ceilZ = ClipLow(ceilZ, ceilZ2);
+    floorZ = ClipHigh(floorZ, floorZ2);
+    wz = floorZ-ceilZ;
+    wx = wall[pWall->point2].x-pWall->x;
+    wy = wall[pWall->point2].y-pWall->y;
+    cz = (ceilZ+floorZ)>>1;
+    GIBLIST *pGib = &gibList[nGibType];
+    sfxPlay3DSound(cx, cy, cz, pGib->at10, nSector);
+    for (int i = 0; i < pGib->at4; i++)
+    {
+        GIBFX *pGibFX = &pGib->at0[i];
+        dassert(pGibFX->chance > 0);
+        GibFX(nWall, pGibFX, ceilZ, wx, wy, wz, pVel);
+    }
+}
diff --git a/source/blood/src/gib.h b/source/blood/src/gib.h
new file mode 100644
index 000000000..70dee7135
--- /dev/null
+++ b/source/blood/src/gib.h
@@ -0,0 +1,75 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+
+enum GIBTYPE {
+    GIBTYPE_0 = 0,
+    GIBTYPE_1,
+    GIBTYPE_2,
+    GIBTYPE_3,
+    GIBTYPE_4,
+    GIBTYPE_5,
+    GIBTYPE_6,
+    GIBTYPE_7,
+    GIBTYPE_8,
+    GIBTYPE_9,
+    GIBTYPE_10,
+    GIBTYPE_11,
+    GIBTYPE_12,
+    GIBTYPE_13,
+    GIBTYPE_14,
+    GIBTYPE_15,
+    GIBTYPE_16,
+    GIBTYPE_17,
+    GIBTYPE_18,
+    GIBTYPE_19,
+    GIBTYPE_20,
+    GIBTYPE_21,
+    GIBTYPE_22,
+    GIBTYPE_23,
+    GIBTYPE_24,
+    GIBTYPE_25,
+    GIBTYPE_26,
+    GIBTYPE_27,
+    GIBTYPE_28,
+    GIBTYPE_29,
+    GIBTYPE_30,
+    kGibMax
+};
+
+class CGibPosition {
+public:
+    int x, y, z;
+    CGibPosition(int _x, int _y, int _z) : x(_x), y(_y), z(_z) {}
+};
+
+class CGibVelocity {
+public:
+    int vx, vy, vz;
+    CGibVelocity(int _vx, int _vy, int _vz) : vx(_vx), vy(_vy), vz(_vz) {}
+};
+
+void GibSprite(spritetype *pSprite, GIBTYPE nGibType, CGibPosition *pPos, CGibVelocity *pVel);
+//void GibFX(int nWall, GIBFX * pGFX, int a3, int a4, int a5, int a6, CGibVelocity * pVel);
+void GibWall(int nWall, GIBTYPE nGibType, CGibVelocity *pVel);
diff --git a/source/blood/src/globals.cpp b/source/blood/src/globals.cpp
new file mode 100644
index 000000000..6fe8ad9a8
--- /dev/null
+++ b/source/blood/src/globals.cpp
@@ -0,0 +1,55 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdlib.h>
+
+#include "compat.h"
+#include "blood.h"
+
+int gFrameClock;
+int gFrameTicks;
+int gFrame;
+int volatile gGameClock;
+int gFrameRate;
+int gGamma;
+int gSaveGameNum;
+
+bool gQuitGame;
+int gQuitRequest;
+bool gPaused;
+bool gSaveGameActive;
+int gCacheMiss;
+
+char *gVersionString;
+char gVersionStringBuf[16];
+
+const char *GetVersionString(void)
+{
+    if (!gVersionString)
+    {
+        gVersionString = gVersionStringBuf;
+        if (!gVersionString)
+            return NULL;
+        sprintf(gVersionString, "%d.%02d", EXEVERSION / 100, EXEVERSION % 100);
+    }
+    return gVersionString;
+}
\ No newline at end of file
diff --git a/source/blood/src/globals.h b/source/blood/src/globals.h
new file mode 100644
index 000000000..085a25167
--- /dev/null
+++ b/source/blood/src/globals.h
@@ -0,0 +1,41 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+extern int gFrameClock;
+extern int gFrameTicks;
+extern int gFrame;
+extern int volatile gGameClock;
+extern int gFrameRate;
+extern int gGamma;
+extern int gSaveGameNum;
+
+
+extern bool gPaused;
+extern bool gSaveGameActive;
+extern bool gSavingGame;
+extern bool gQuitGame;
+extern int gQuitRequest;
+extern int gCacheMiss;
+
+const char *GetVersionString(void);
\ No newline at end of file
diff --git a/source/blood/src/gmtimbre.cpp b/source/blood/src/gmtimbre.cpp
new file mode 100644
index 000000000..6c751a82d
--- /dev/null
+++ b/source/blood/src/gmtimbre.cpp
@@ -0,0 +1,297 @@
+/*
+Copyright (C) 1994-1995 Apogee Software, Ltd.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+
+*/
+
+#include <inttypes.h>
+
+namespace OPLMusic {
+
+typedef struct
+   {
+   uint8_t SAVEK[ 2 ];
+   uint8_t Level[ 2 ];
+   uint8_t Env1[ 2 ];
+   uint8_t Env2[ 2 ];
+   uint8_t Wave[ 2 ];
+   uint8_t Feedback;
+   int8_t  Transpose;
+   int8_t  Velocity;
+   } TIMBRE;
+
+TIMBRE ADLIB_TimbreBank[ 256 ] =
+   {
+      { { 33, 33 }, { 143, 6 }, { 242, 242 }, { 69, 118 }, { 0, 0 }, 8, 0, 0 },
+      { { 49, 33 }, { 75, 0 }, { 242, 242 }, { 84, 86 }, { 0, 0 }, 8, 0, 0 },
+      { { 49, 33 }, { 73, 0 }, { 242, 242 }, { 85, 118 }, { 0, 0 }, 8, 0, 0 },
+      { { 177, 97 }, { 14, 0 }, { 242, 243 }, { 59, 11 }, { 0, 0 }, 6, 0, 0 },
+      { { 1, 33 }, { 87, 0 }, { 241, 241 }, { 56, 40 }, { 0, 0 }, 0, 0, 0 },
+      { { 1, 33 }, { 147, 0 }, { 241, 241 }, { 56, 40 }, { 0, 0 }, 0, 0, 0 },
+      { { 33, 54 }, { 128, 14 }, { 162, 241 }, { 1, 213 }, { 0, 0 }, 8, 0, 0 },
+      { { 1, 1 }, { 146, 0 }, { 194, 194 }, { 168, 88 }, { 0, 0 }, 10, 0, 0 },
+      { { 12, 129 }, { 92, 0 }, { 246, 243 }, { 84, 181 }, { 0, 0 }, 0, 0, 0 },
+      { { 7, 17 }, { 151, 128 }, { 246, 245 }, { 50, 17 }, { 0, 0 }, 2, 0, 0 },
+      { { 23, 1 }, { 33, 0 }, { 86, 246 }, { 4, 4 }, { 0, 0 }, 2, 0, 0 },
+      { { 24, 129 }, { 98, 0 }, { 243, 242 }, { 230, 246 }, { 0, 0 }, 0, 0, 0 },
+      { { 24, 33 }, { 35, 0 }, { 247, 229 }, { 85, 216 }, { 0, 0 }, 0, 0, 0 },
+      { { 21, 1 }, { 145, 0 }, { 246, 246 }, { 166, 230 }, { 0, 0 }, 4, 0, 0 },
+      { { 69, 129 }, { 89, 128 }, { 211, 163 }, { 130, 227 }, { 0, 0 }, 12, 0, 0 },
+      { { 3, 129 }, { 73, 128 }, { 116, 179 }, { 85, 5 }, { 1, 0 }, 4, 0, 0 },
+      { { 113, 49 }, { 146, 0 }, { 246, 241 }, { 20, 7 }, { 0, 0 }, 2, 0, 0 },
+      { { 114, 48 }, { 20, 0 }, { 199, 199 }, { 88, 8 }, { 0, 0 }, 2, 0, 0 },
+      { { 112, 177 }, { 68, 0 }, { 170, 138 }, { 24, 8 }, { 0, 0 }, 4, 0, 0 },
+      { { 35, 177 }, { 147, 0 }, { 151, 85 }, { 35, 20 }, { 1, 0 }, 4, 0, 0 },
+      { { 97, 177 }, { 19, 128 }, { 151, 85 }, { 4, 4 }, { 1, 0 }, 0, 0, 0 },
+      { { 36, 177 }, { 72, 0 }, { 152, 70 }, { 42, 26 }, { 1, 0 }, 12, 0, 0 },
+      { { 97, 33 }, { 19, 0 }, { 145, 97 }, { 6, 7 }, { 1, 0 }, 10, 0, 0 },
+      { { 33, 161 }, { 19, 137 }, { 113, 97 }, { 6, 7 }, { 0, 0 }, 6, 0, 0 },
+      { { 2, 65 }, { 156, 128 }, { 243, 243 }, { 148, 200 }, { 1, 0 }, 12, 0, 0 },
+      { { 3, 17 }, { 84, 0 }, { 243, 241 }, { 154, 231 }, { 1, 0 }, 12, 0, 0 },
+      { { 35, 33 }, { 95, 0 }, { 241, 242 }, { 58, 248 }, { 0, 0 }, 0, 0, 0 },
+      { { 3, 33 }, { 135, 128 }, { 246, 243 }, { 34, 243 }, { 1, 0 }, 6, 0, 0 },
+      { { 3, 33 }, { 71, 0 }, { 249, 246 }, { 84, 58 }, { 0, 0 }, 0, 0, 0 },
+      { { 35, 33 }, { 72, 0 }, { 149, 132 }, { 25, 25 }, { 1, 0 }, 8, 0, 0 },
+      { { 35, 33 }, { 74, 0 }, { 149, 148 }, { 25, 25 }, { 1, 0 }, 8, 0, 0 },
+      { { 9, 132 }, { 161, 128 }, { 32, 209 }, { 79, 248 }, { 0, 0 }, 8, 0, 0 },
+      { { 33, 162 }, { 30, 0 }, { 148, 195 }, { 6, 166 }, { 0, 0 }, 2, 0, 0 },
+      { { 49, 49 }, { 18, 0 }, { 241, 241 }, { 40, 24 }, { 0, 0 }, 10, 0, 0 },
+      { { 49, 49 }, { 141, 0 }, { 241, 241 }, { 232, 120 }, { 0, 0 }, 10, 0, 0 },
+      { { 49, 50 }, { 91, 0 }, { 81, 113 }, { 40, 72 }, { 0, 0 }, 12, 0, 0 },
+      { { 1, 33 }, { 139, 64 }, { 161, 242 }, { 154, 223 }, { 0, 0 }, 8, 0, 0 },
+      { { 1, 33 }, { 137, 64 }, { 161, 242 }, { 154, 223 }, { 0, 0 }, 8, 0, 0 },
+      { { 49, 49 }, { 139, 0 }, { 244, 241 }, { 232, 120 }, { 0, 0 }, 10, 0, 0 },
+      { { 49, 49 }, { 18, 0 }, { 241, 241 }, { 40, 24 }, { 0, 0 }, 10, 0, 0 },
+      { { 49, 33 }, { 21, 0 }, { 221, 86 }, { 19, 38 }, { 1, 0 }, 8, 0, 0 },
+      { { 49, 33 }, { 22, 0 }, { 221, 102 }, { 19, 6 }, { 1, 0 }, 8, 0, 0 },
+      { { 113, 49 }, { 73, 0 }, { 209, 97 }, { 28, 12 }, { 1, 0 }, 8, 0, 0 },
+      { { 33, 35 }, { 77, 128 }, { 113, 114 }, { 18, 6 }, { 1, 0 }, 2, 0, 0 },
+      { { 241, 225 }, { 64, 0 }, { 241, 111 }, { 33, 22 }, { 1, 0 }, 2, 0, 0 },
+      { { 2, 1 }, { 26, 128 }, { 245, 133 }, { 117, 53 }, { 1, 0 }, 0, 0, 0 },
+      { { 2, 1 }, { 29, 128 }, { 245, 243 }, { 117, 244 }, { 1, 0 }, 0, 0, 0 },
+      { { 16, 17 }, { 65, 0 }, { 245, 242 }, { 5, 195 }, { 1, 0 }, 2, 0, 0 },
+      { { 33, 162 }, { 155, 1 }, { 177, 114 }, { 37, 8 }, { 1, 0 }, 14, 0, 0 },
+      { { 161, 33 }, { 152, 0 }, { 127, 63 }, { 3, 7 }, { 1, 1 }, 0, 0, 0 },
+      { { 161, 97 }, { 147, 0 }, { 193, 79 }, { 18, 5 }, { 0, 0 }, 10, 0, 0 },
+      { { 33, 97 }, { 24, 0 }, { 193, 79 }, { 34, 5 }, { 0, 0 }, 12, 0, 0 },
+      { { 49, 114 }, { 91, 131 }, { 244, 138 }, { 21, 5 }, { 0, 0 }, 0, 0, 0 },
+      { { 161, 97 }, { 144, 0 }, { 116, 113 }, { 57, 103 }, { 0, 0 }, 0, 0, 0 },
+      { { 113, 114 }, { 87, 0 }, { 84, 122 }, { 5, 5 }, { 0, 0 }, 12, 0, 0 },
+      { { 144, 65 }, { 0, 0 }, { 84, 165 }, { 99, 69 }, { 0, 0 }, 8, 0, 0 },
+      { { 33, 33 }, { 146, 1 }, { 133, 143 }, { 23, 9 }, { 0, 0 }, 12, 0, 0 },
+      { { 33, 33 }, { 148, 5 }, { 117, 143 }, { 23, 9 }, { 0, 0 }, 12, 0, 0 },
+      { { 33, 97 }, { 148, 0 }, { 118, 130 }, { 21, 55 }, { 0, 0 }, 12, 0, 0 },
+      { { 49, 33 }, { 67, 0 }, { 158, 98 }, { 23, 44 }, { 1, 1 }, 2, 0, 0 },
+      { { 33, 33 }, { 155, 0 }, { 97, 127 }, { 106, 10 }, { 0, 0 }, 2, 0, 0 },
+      { { 97, 34 }, { 138, 6 }, { 117, 116 }, { 31, 15 }, { 0, 0 }, 8, 0, 0 },
+      { { 161, 33 }, { 134, 13 }, { 114, 113 }, { 85, 24 }, { 1, 0 }, 0, 0, 0 },
+      { { 33, 33 }, { 77, 0 }, { 84, 166 }, { 60, 28 }, { 0, 0 }, 8, 0, 0 },
+      { { 49, 97 }, { 143, 0 }, { 147, 114 }, { 2, 11 }, { 1, 0 }, 8, 0, 0 },
+      { { 49, 97 }, { 142, 0 }, { 147, 114 }, { 3, 9 }, { 1, 0 }, 8, 0, 0 },
+      { { 49, 97 }, { 145, 0 }, { 147, 130 }, { 3, 9 }, { 1, 0 }, 10, 0, 0 },
+      { { 49, 97 }, { 142, 0 }, { 147, 114 }, { 15, 15 }, { 1, 0 }, 10, 0, 0 },
+      { { 33, 33 }, { 75, 0 }, { 170, 143 }, { 22, 10 }, { 1, 0 }, 8, 0, 0 },
+      { { 49, 33 }, { 144, 0 }, { 126, 139 }, { 23, 12 }, { 1, 1 }, 6, 0, 0 },
+      { { 49, 50 }, { 129, 0 }, { 117, 97 }, { 25, 25 }, { 1, 0 }, 0, 0, 0 },
+      { { 50, 33 }, { 144, 0 }, { 155, 114 }, { 33, 23 }, { 0, 0 }, 4, 0, 0 },
+      { { 225, 225 }, { 31, 0 }, { 133, 101 }, { 95, 26 }, { 0, 0 }, 0, 0, 0 },
+      { { 225, 225 }, { 70, 0 }, { 136, 101 }, { 95, 26 }, { 0, 0 }, 0, 0, 0 },
+      { { 161, 33 }, { 156, 0 }, { 117, 117 }, { 31, 10 }, { 0, 0 }, 2, 0, 0 },
+      { { 49, 33 }, { 139, 0 }, { 132, 101 }, { 88, 26 }, { 0, 0 }, 0, 0, 0 },
+      { { 225, 161 }, { 76, 0 }, { 102, 101 }, { 86, 38 }, { 0, 0 }, 0, 0, 0 },
+      { { 98, 161 }, { 203, 0 }, { 118, 85 }, { 70, 54 }, { 0, 0 }, 0, 0, 0 },
+      { { 98, 161 }, { 153, 0 }, { 87, 86 }, { 7, 7 }, { 0, 0 }, 11, 0, 0 },
+      { { 98, 161 }, { 147, 0 }, { 119, 118 }, { 7, 7 }, { 0, 0 }, 11, 0, 0 },
+      { { 34, 33 }, { 89, 0 }, { 255, 255 }, { 3, 15 }, { 2, 0 }, 0, 0, 0 },
+      { { 33, 33 }, { 14, 0 }, { 255, 255 }, { 15, 15 }, { 1, 1 }, 0, 0, 0 },
+      { { 34, 33 }, { 70, 128 }, { 134, 100 }, { 85, 24 }, { 0, 0 }, 0, 0, 0 },
+      { { 33, 161 }, { 69, 0 }, { 102, 150 }, { 18, 10 }, { 0, 0 }, 0, 0, 0 },
+      { { 33, 34 }, { 139, 0 }, { 146, 145 }, { 42, 42 }, { 1, 0 }, 0, 0, 0 },
+      { { 162, 97 }, { 158, 64 }, { 223, 111 }, { 5, 7 }, { 0, 0 }, 2, 0, 0 },
+      { { 32, 96 }, { 26, 0 }, { 239, 143 }, { 1, 6 }, { 0, 2 }, 0, 0, 0 },
+      { { 33, 33 }, { 143, 128 }, { 241, 244 }, { 41, 9 }, { 0, 0 }, 10, 0, 0 },
+      { { 119, 161 }, { 165, 0 }, { 83, 160 }, { 148, 5 }, { 0, 0 }, 2, 0, 0 },
+      { { 97, 177 }, { 31, 128 }, { 168, 37 }, { 17, 3 }, { 0, 0 }, 10, 0, 0 },
+      { { 97, 97 }, { 23, 0 }, { 145, 85 }, { 52, 22 }, { 0, 0 }, 12, 0, 0 },
+      { { 113, 114 }, { 93, 0 }, { 84, 106 }, { 1, 3 }, { 0, 0 }, 0, 0, 0 },
+      { { 33, 162 }, { 151, 0 }, { 33, 66 }, { 67, 53 }, { 0, 0 }, 8, 0, 0 },
+      { { 161, 33 }, { 28, 0 }, { 161, 49 }, { 119, 71 }, { 1, 1 }, 0, 0, 0 },
+      { { 33, 97 }, { 137, 3 }, { 17, 66 }, { 51, 37 }, { 0, 0 }, 10, 0, 0 },
+      { { 161, 33 }, { 21, 0 }, { 17, 207 }, { 71, 7 }, { 1, 0 }, 0, 0, 0 },
+      { { 58, 81 }, { 206, 0 }, { 248, 134 }, { 246, 2 }, { 0, 0 }, 2, 0, 0 },
+      { { 33, 33 }, { 21, 0 }, { 33, 65 }, { 35, 19 }, { 1, 0 }, 0, 0, 0 },
+      { { 6, 1 }, { 91, 0 }, { 116, 165 }, { 149, 114 }, { 0, 0 }, 0, 0, 0 },
+      { { 34, 97 }, { 146, 131 }, { 177, 242 }, { 129, 38 }, { 0, 0 }, 12, 0, 0 },
+      { { 65, 66 }, { 77, 0 }, { 241, 242 }, { 81, 245 }, { 1, 0 }, 0, 0, 0 },
+      { { 97, 163 }, { 148, 128 }, { 17, 17 }, { 81, 19 }, { 1, 0 }, 6, 0, 0 },
+      { { 97, 161 }, { 140, 128 }, { 17, 29 }, { 49, 3 }, { 0, 0 }, 6, 0, 0 },
+      { { 164, 97 }, { 76, 0 }, { 243, 129 }, { 115, 35 }, { 1, 0 }, 4, 0, 0 },
+      { { 2, 7 }, { 133, 3 }, { 210, 242 }, { 83, 246 }, { 0, 1 }, 0, 0, 0 },
+      { { 17, 19 }, { 12, 128 }, { 163, 162 }, { 17, 229 }, { 1, 0 }, 0, 0, 0 },
+      { { 17, 17 }, { 6, 0 }, { 246, 242 }, { 65, 230 }, { 1, 2 }, 4, 0, 0 },
+      { { 147, 145 }, { 145, 0 }, { 212, 235 }, { 50, 17 }, { 0, 1 }, 8, 0, 0 },
+      { { 4, 1 }, { 79, 0 }, { 250, 194 }, { 86, 5 }, { 0, 0 }, 12, 0, 0 },
+      { { 33, 34 }, { 73, 0 }, { 124, 111 }, { 32, 12 }, { 0, 1 }, 6, 0, 0 },
+      { { 49, 33 }, { 133, 0 }, { 221, 86 }, { 51, 22 }, { 1, 0 }, 10, 0, 0 },
+      { { 32, 33 }, { 4, 129 }, { 218, 143 }, { 5, 11 }, { 2, 0 }, 6, 0, 0 },
+      { { 5, 3 }, { 106, 128 }, { 241, 195 }, { 229, 229 }, { 0, 0 }, 6, 0, 0 },
+      { { 7, 2 }, { 21, 0 }, { 236, 248 }, { 38, 22 }, { 0, 0 }, 10, 0, 0 },
+      { { 5, 1 }, { 157, 0 }, { 103, 223 }, { 53, 5 }, { 0, 0 }, 8, 0, 0 },
+      { { 24, 18 }, { 150, 0 }, { 250, 248 }, { 40, 229 }, { 0, 0 }, 10, 0, 0 },
+      { { 16, 0 }, { 134, 3 }, { 168, 250 }, { 7, 3 }, { 0, 0 }, 6, 0, 0 },
+      { { 17, 16 }, { 65, 3 }, { 248, 243 }, { 71, 3 }, { 2, 0 }, 4, 0, 0 },
+      { { 1, 16 }, { 142, 0 }, { 241, 243 }, { 6, 2 }, { 2, 0 }, 14, 0, 0 },
+      { { 14, 192 }, { 0, 0 }, { 31, 31 }, { 0, 255 }, { 0, 3 }, 14, 0, 0 },
+      { { 6, 3 }, { 128, 136 }, { 248, 86 }, { 36, 132 }, { 0, 2 }, 14, 0, 0 },
+      { { 14, 208 }, { 0, 5 }, { 248, 52 }, { 0, 4 }, { 0, 3 }, 14, 0, 0 },
+      { { 14, 192 }, { 0, 0 }, { 246, 31 }, { 0, 2 }, { 0, 3 }, 14, 0, 0 },
+      { { 213, 218 }, { 149, 64 }, { 55, 86 }, { 163, 55 }, { 0, 0 }, 0, 0, 0 },
+      { { 53, 20 }, { 92, 8 }, { 178, 244 }, { 97, 21 }, { 2, 0 }, 10, 0, 0 },
+      { { 14, 208 }, { 0, 0 }, { 246, 79 }, { 0, 245 }, { 0, 3 }, 14, 0, 0 },
+      { { 38, 228 }, { 0, 0 }, { 255, 18 }, { 1, 22 }, { 0, 1 }, 14, 0, 0 },
+      { { 0, 0 }, { 0, 0 }, { 243, 246 }, { 240, 201 }, { 0, 2 }, 14, 0, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 0, 0 }, { 0, 0 }, { 252, 250 }, { 5, 23 }, { 2, 0 }, 14, 52, 0 },
+      { { 0, 1 }, { 2, 0 }, { 255, 255 }, { 7, 8 }, { 0, 0 }, 0, 48, 0 },
+      { { 0, 0 }, { 0, 0 }, { 252, 250 }, { 5, 23 }, { 2, 0 }, 14, 58, 0 },
+      { { 0, 0 }, { 0, 0 }, { 246, 246 }, { 12, 6 }, { 0, 0 }, 4, 60, 0 },
+      { { 12, 18 }, { 0, 0 }, { 246, 251 }, { 8, 71 }, { 0, 2 }, 10, 47, 0 },
+      { { 0, 0 }, { 3, 0 }, { 248, 246 }, { 42, 69 }, { 0, 1 }, 4, 43, 0 },
+      { { 12, 18 }, { 0, 0 }, { 246, 251 }, { 8, 71 }, { 0, 2 }, 10, 49, 0 },
+      { { 0, 0 }, { 3, 0 }, { 248, 246 }, { 42, 69 }, { 0, 1 }, 4, 43, 0 },
+      { { 12, 18 }, { 0, 0 }, { 246, 251 }, { 8, 71 }, { 0, 2 }, 10, 51, 0 },
+      { { 0, 0 }, { 3, 0 }, { 248, 246 }, { 42, 69 }, { 0, 1 }, 4, 43, 0 },
+      { { 12, 18 }, { 0, 0 }, { 246, 251 }, { 8, 71 }, { 0, 2 }, 10, 54, 0 },
+      { { 12, 18 }, { 0, 0 }, { 246, 251 }, { 8, 71 }, { 0, 2 }, 10, 57, 0 },
+      { { 0, 0 }, { 3, 0 }, { 248, 246 }, { 42, 69 }, { 0, 1 }, 4, 72, 0 },
+      { { 12, 18 }, { 0, 0 }, { 246, 251 }, { 8, 71 }, { 0, 2 }, 10, 60, 0 },
+      { { 14, 208 }, { 0, 10 }, { 245, 159 }, { 48, 2 }, { 0, 0 }, 14, 76, 0 },
+      { { 14, 7 }, { 10, 93 }, { 228, 245 }, { 228, 229 }, { 3, 1 }, 6, 84, 0 },
+      { { 2, 5 }, { 3, 10 }, { 180, 151 }, { 4, 247 }, { 0, 0 }, 14, 36, 0 },
+      { { 78, 158 }, { 0, 0 }, { 246, 159 }, { 0, 2 }, { 0, 3 }, 14, 76, 0 },
+      { { 17, 16 }, { 69, 8 }, { 248, 243 }, { 55, 5 }, { 2, 0 }, 8, 84, 0 },
+      { { 14, 208 }, { 0, 0 }, { 246, 159 }, { 0, 2 }, { 0, 3 }, 14, 83, 0 },
+      { { 128, 16 }, { 0, 13 }, { 255, 255 }, { 3, 20 }, { 3, 0 }, 12, 84, 0 },
+      { { 14, 7 }, { 8, 81 }, { 248, 244 }, { 66, 228 }, { 0, 3 }, 14, 24, 0 },
+      { { 14, 208 }, { 0, 10 }, { 245, 159 }, { 48, 2 }, { 0, 0 }, 14, 77, 0 },
+      { { 1, 2 }, { 0, 0 }, { 250, 200 }, { 191, 151 }, { 0, 0 }, 7, 60, 0 },
+      { { 1, 1 }, { 81, 0 }, { 250, 250 }, { 135, 183 }, { 0, 0 }, 6, 65, 0 },
+      { { 1, 2 }, { 84, 0 }, { 250, 248 }, { 141, 184 }, { 0, 0 }, 6, 59, 0 },
+      { { 1, 2 }, { 89, 0 }, { 250, 248 }, { 136, 182 }, { 0, 0 }, 6, 51, 0 },
+      { { 1, 0 }, { 0, 0 }, { 249, 250 }, { 10, 6 }, { 3, 0 }, 14, 45, 0 },
+      { { 0, 0 }, { 128, 0 }, { 249, 246 }, { 137, 108 }, { 3, 0 }, 14, 71, 0 },
+      { { 3, 12 }, { 128, 8 }, { 248, 246 }, { 136, 182 }, { 3, 0 }, 15, 60, 0 },
+      { { 3, 12 }, { 133, 0 }, { 248, 246 }, { 136, 182 }, { 3, 0 }, 15, 58, 0 },
+      { { 14, 0 }, { 64, 8 }, { 118, 119 }, { 79, 24 }, { 0, 2 }, 14, 53, 0 },
+      { { 14, 3 }, { 64, 0 }, { 200, 155 }, { 73, 105 }, { 0, 2 }, 14, 64, 0 },
+      { { 215, 199 }, { 220, 0 }, { 173, 141 }, { 5, 5 }, { 3, 0 }, 14, 71, 0 },
+      { { 215, 199 }, { 220, 0 }, { 168, 136 }, { 4, 4 }, { 3, 0 }, 14, 61, 0 },
+      { { 128, 17 }, { 0, 0 }, { 246, 103 }, { 6, 23 }, { 3, 3 }, 14, 61, 0 },
+      { { 128, 17 }, { 0, 9 }, { 245, 70 }, { 5, 22 }, { 2, 3 }, 14, 48, 0 },
+      { { 6, 21 }, { 63, 0 }, { 0, 247 }, { 244, 245 }, { 0, 0 }, 1, 48, 0 },
+      { { 6, 18 }, { 63, 0 }, { 0, 247 }, { 244, 245 }, { 3, 0 }, 0, 69, 0 },
+      { { 6, 18 }, { 63, 0 }, { 0, 247 }, { 244, 245 }, { 0, 0 }, 1, 68, 0 },
+      { { 1, 2 }, { 88, 0 }, { 103, 117 }, { 231, 7 }, { 0, 0 }, 0, 63, 0 },
+      { { 65, 66 }, { 69, 8 }, { 248, 117 }, { 72, 5 }, { 0, 0 }, 0, 74, 0 },
+      { { 10, 30 }, { 64, 78 }, { 224, 255 }, { 240, 5 }, { 3, 0 }, 8, 60, 0 },
+      { { 10, 30 }, { 124, 82 }, { 224, 255 }, { 240, 2 }, { 3, 0 }, 8, 80, 0 },
+      { { 14, 0 }, { 64, 8 }, { 122, 123 }, { 74, 27 }, { 0, 2 }, 14, 64, 0 },
+      { { 14, 7 }, { 10, 64 }, { 228, 85 }, { 228, 57 }, { 3, 1 }, 6, 69, 0 },
+      { { 5, 4 }, { 5, 64 }, { 249, 214 }, { 50, 165 }, { 3, 0 }, 14, 73, 0 },
+      { { 2, 21 }, { 63, 0 }, { 0, 247 }, { 243, 245 }, { 3, 0 }, 8, 75, 0 },
+      { { 1, 2 }, { 79, 0 }, { 250, 248 }, { 141, 181 }, { 0, 0 }, 7, 68, 0 },
+      { { 0, 0 }, { 0, 0 }, { 246, 246 }, { 12, 6 }, { 0, 0 }, 4, 48, 0 },
+      { { 33, 17 }, { 17, 0 }, { 163, 196 }, { 67, 34 }, { 2, 0 }, 13, 53, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 },
+      { { 16, 17 }, { 68, 0 }, { 248, 243 }, { 119, 6 }, { 2, 0 }, 8, 35, 0 }
+   };
+
+}
diff --git a/source/blood/src/inifile.cpp b/source/blood/src/inifile.cpp
new file mode 100644
index 000000000..a3ee29364
--- /dev/null
+++ b/source/blood/src/inifile.cpp
@@ -0,0 +1,465 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 sirlemonhead, Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+// Note: This module is based on the sirlemonhead's work
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+#include "common_game.h"
+
+#include "inifile.h"
+#include "misc.h"
+
+
+IniFile::IniFile(const char *fileName)
+{
+    head.next = &head;
+
+    // debug stuff
+    // curNode = NULL;
+    // anotherNode = NULL;
+    //_13 = NULL;
+
+    strcpy(this->fileName, fileName);
+    Load();
+}
+
+IniFile::IniFile(void *res)
+{
+    head.next = &head;
+    strcpy(fileName, "menus.mat");
+    LoadRes(res);
+}
+
+void IniFile::LoadRes(void *res)
+{
+    char buffer[256];
+
+    curNode = &head;
+
+    while (ResReadLine(buffer, sizeof(buffer), &res) != 0)
+    {
+        char *ch = strchr(buffer, '\n');
+        if (ch != NULL) {
+            ch[0] = '\0';
+        }
+
+        // do the same for carriage return?
+        ch = strchr(buffer, '\r');
+        if (ch != NULL) {
+            ch[0] = '\0';
+        }
+
+        char *pBuffer = buffer;
+
+        // remove whitespace from buffer
+        while (isspace(*pBuffer)) {
+            pBuffer++;
+        }
+
+        curNode->next = (FNODE*)malloc(strlen(pBuffer) + sizeof(FNODE));
+        dassert(curNode->next != NULL);
+
+        anotherNode = curNode;
+        curNode = curNode->next;
+
+
+        strcpy(curNode->name, pBuffer);
+
+        /*
+            check for:
+            ; - comment line. continue and grab a new line  (59)
+            [ - start of section marker                     (91)
+            ] - end of section marker                       (93)
+            = - key and value seperator                     (61)
+        */
+
+        switch (*pBuffer)
+        {
+        case 0:
+        case ';': // comment line
+            break;
+        case '[':
+            if (!strchr(pBuffer, ']'))
+            {
+                free(curNode);
+                curNode = anotherNode;
+            }
+            break;
+        default:
+
+            if (strchr(pBuffer, '=') <= pBuffer) {
+                free(curNode);
+                curNode = anotherNode;
+            }
+            break;
+        }
+    }
+
+    curNode->next = &head;
+}
+
+void IniFile::Load()
+{
+    // char buffer[256];
+
+    curNode = &head;
+
+    int fp = kopen4loadfrommod(fileName, 0);
+    if (fp >= 0)
+    {
+        int nSize = kfilelength(fp);
+        void *pBuffer = Xcalloc(1, nSize + 1);
+        kread(fp, pBuffer, nSize);
+        LoadRes(pBuffer);
+        Bfree(pBuffer);
+    }
+    else
+        curNode->next = &head;
+#if 0
+    if (fp)
+    {
+        while (fgets(buffer, sizeof(buffer), fp) != NULL)
+        {
+            char *ch = strchr(buffer, '\n');
+            if (ch != NULL) {
+                ch[0] = '\0';
+            }
+
+            // do the same for carriage return?
+            ch = strchr(buffer, '\r');
+            if (ch != NULL) {
+                ch[0] = '\0';
+            }
+
+            char *pBuffer = buffer;
+
+            // remove whitespace from buffer
+            while (isspace(*pBuffer)) {
+                pBuffer++;
+            }
+
+            curNode->next = (FNODE*)malloc(strlen(pBuffer) + sizeof(FNODE));
+            dassert(curNode->next != NULL);
+
+            anotherNode = curNode;
+            curNode = curNode->next;
+
+
+            strcpy(curNode->name, pBuffer);
+
+            /*
+                check for:
+                ; - comment line. continue and grab a new line  (59)
+                [ - start of section marker                     (91)
+                ] - end of section marker                       (93)
+                = - key and value seperator                     (61)
+            */
+
+            switch (*pBuffer)
+            {
+            case 0:
+            case ';': // comment line
+                break;
+            case '[':
+                if (!strchr(pBuffer, ']'))
+                {
+                    free(curNode);
+                    curNode = anotherNode;
+                }
+                break;
+            default:
+
+                if (strchr(pBuffer, '=') <= pBuffer) {
+                    free(curNode);
+                    curNode = anotherNode;
+                }
+                break;
+            }
+        }
+        fclose(fp);
+    }
+
+    curNode->next = &head;
+#endif
+}
+
+void IniFile::Save(void)
+{
+    char buffer[256];
+    FILE *hFile = fopen(fileName, "w");
+    dassert(hFile != NULL);
+    curNode = head.next;
+    while (curNode != &head)
+    {
+        sprintf(buffer, "%s\n", curNode->name);
+        fwrite(buffer, 1, strlen(buffer), hFile);
+        curNode = curNode->next;
+    }
+    fclose(hFile);
+}
+
+bool IniFile::FindSection(const char *section)
+{
+    char buffer[256];
+    curNode = anotherNode = &head;
+    if (section)
+    {
+        sprintf(buffer, "[%s]", section);
+        do
+        {
+            anotherNode = curNode;
+            curNode = curNode->next;
+            if (curNode == &head)
+                return false;
+        } while(Bstrcasecmp(curNode->name, buffer) != 0);
+    }
+    return true;
+}
+
+bool IniFile::SectionExists(const char *section)
+{
+    return FindSection(section);
+}
+
+bool IniFile::FindKey(const char *key)
+{
+    anotherNode = curNode;
+    curNode = curNode->next;
+    while (curNode != &head)
+    {
+        char c = curNode->name[0];
+
+        if (c == ';' || c == '\0') {
+            anotherNode = curNode;
+            curNode = curNode->next;
+            continue;
+        }
+
+        if (c == '[') {
+            return 0;
+        }
+
+        char *pEqual = strchr(curNode->name, '=');
+        char *pEqualStart = pEqual;
+        dassert(pEqual != NULL);
+
+        // remove whitespace
+        while (isspace(*(pEqual - 1))) {
+            pEqual--;
+        }
+
+        c = *pEqual;
+        *pEqual = '\0';
+
+        if (strcmp(key, curNode->name) == 0)
+        {
+            // strings match
+            *pEqual = c;
+            _13 = ++pEqualStart;
+            while (isspace(*_13)) {
+                _13++;
+            }
+
+            return true;
+        }
+        *pEqual = c;
+        anotherNode = curNode;
+        curNode = curNode->next;
+    }
+    
+    return false;
+}
+
+void IniFile::AddSection(const char *section)
+{
+    char buffer[256];
+
+    if (anotherNode != &head)
+    {
+        FNODE *newNode = (FNODE*)malloc(sizeof(FNODE));
+        dassert(newNode != NULL);
+
+        newNode->name[0] = 0;
+        newNode->next = anotherNode->next;
+        anotherNode->next = newNode;
+        anotherNode = newNode;
+    }
+
+    sprintf(buffer, "[%s]", section);
+    FNODE *newNode = (FNODE*)malloc(strlen(buffer) + sizeof(FNODE));
+    dassert(newNode != NULL);
+
+    strcpy(newNode->name, buffer);
+
+    newNode->next = anotherNode->next;
+    anotherNode->next = newNode;
+    anotherNode = newNode;
+}
+
+void IniFile::AddKeyString(const char *key, const char *value)
+{
+    char buffer[256];
+
+    sprintf(buffer, "%s=%s", key, value);
+
+    FNODE *newNode = (FNODE*)malloc(strlen(buffer) + sizeof(FNODE));
+    dassert(newNode != NULL);
+
+    strcpy(newNode->name, buffer);
+
+    newNode->next = anotherNode->next;
+    anotherNode->next = newNode;
+    curNode = newNode;
+}
+
+void IniFile::ChangeKeyString(const char *key, const char *value)
+{
+    char buffer[256];
+
+    sprintf(buffer, "%s=%s", key, value);
+
+    FNODE *newNode = (FNODE*)realloc(curNode, strlen(buffer) + sizeof(FNODE));
+    dassert(newNode != NULL);
+
+    strcpy(newNode->name, buffer);
+
+    anotherNode->next = newNode;
+}
+
+bool IniFile::KeyExists(const char *section, const char *key)
+{
+    if (FindSection(section) && FindKey(key))
+        return true;
+
+    return false;
+}
+
+void IniFile::PutKeyString(const char *section, const char *key, const char *value)
+{
+    if (FindSection(section))
+    {
+        if (FindKey(key))
+        {
+            ChangeKeyString(key, value);
+            return;
+        }
+    }
+    else
+    {
+        AddSection(section);
+    }
+
+    AddKeyString(key, value);
+}
+
+const char* IniFile::GetKeyString(const char *section, const char *key, const char *defaultValue)
+{
+    if (FindSection(section) && FindKey(key))
+        return _13;
+    return defaultValue;
+}
+
+void IniFile::PutKeyInt(const char *section, const char *key, int value)
+{
+    char buffer[256];
+
+    // convert int to string
+    sprintf(buffer,"%d",value);
+
+    PutKeyString(section, key, buffer);
+}
+
+int IniFile::GetKeyInt(const char *section, const char *key, int defaultValue)
+{
+    if (FindSection(section) && FindKey(key))
+    {
+        // convert string to int int
+        return strtol(_13, NULL, 0);
+    }
+    return defaultValue;
+}
+
+void IniFile::PutKeyHex(const char *section, const char *key, int value)
+{
+    char buffer[256] = "0x";
+
+    // convert int to string
+    sprintf(buffer,"%x",value);
+
+    PutKeyString(section, key, buffer);
+}
+
+int IniFile::GetKeyHex(const char *section, const char *key, int defaultValue)
+{
+    return GetKeyInt(section, key, defaultValue);
+}
+
+bool IniFile::GetKeyBool(const char *section, const char *key, int defaultValue)
+{
+    return (bool)GetKeyInt(section, key, defaultValue);
+}
+
+void IniFile::RemoveKey(const char *section, const char *key)
+{
+    if (FindSection(section) && FindKey(key))
+    {
+        anotherNode->next = curNode->next;
+        free(curNode);
+        curNode = anotherNode->next;
+    }
+}
+
+void IniFile::RemoveSection(const char *section)
+{
+    if (FindSection(section))
+    {
+        anotherNode = curNode;
+
+        curNode = curNode->next;
+
+        while (curNode != &head)
+        {
+            if (curNode->name[0] == '[') {
+                return;
+            }
+
+            anotherNode->next = curNode->next;
+            free(curNode);
+            curNode = anotherNode->next;
+        }
+    }
+}
+
+IniFile::~IniFile()
+{
+    curNode = head.next;
+
+    while (curNode != &head)
+    {
+        anotherNode = curNode;
+        curNode = curNode->next;
+        free(anotherNode);
+    }
+}
diff --git a/source/blood/src/inifile.h b/source/blood/src/inifile.h
new file mode 100644
index 000000000..2fe0490eb
--- /dev/null
+++ b/source/blood/src/inifile.h
@@ -0,0 +1,68 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 sirlemonhead, Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+// Note: This module is based on the sirlemonhead's work
+#pragma once
+struct FNODE
+{
+    FNODE *next;
+    char name[1];
+};
+
+// 161 bytes
+class IniFile
+{
+public:
+    IniFile(const char *fileName);
+    IniFile(void *res);
+    ~IniFile();
+
+    void Save(void);
+    bool FindSection(const char *section);
+    bool SectionExists(const char *section);
+    bool FindKey(const char *key);
+    void AddSection(const char *section);
+    void AddKeyString(const char *key, const char *value);
+    void ChangeKeyString(const char *key, const char *value);
+    bool KeyExists(const char *section, const char *key);
+    void PutKeyString(const char *section, const char *key, const char *value);
+    const char* GetKeyString(const char *section, const char *key, const char *defaultValue);
+    void PutKeyInt(const char *section, const char *key, const int value);
+    int GetKeyInt(const char *section, const char *key, int defaultValue);
+    void PutKeyHex(const char *section, const char *key, int value);
+    int GetKeyHex(const char *section, const char *key, int defaultValue);
+    bool GetKeyBool(const char *section, const char *key, int defaultValue);
+    void RemoveKey(const char *section, const char *key);
+    void RemoveSection(const char *section);
+
+private:
+    FNODE head;
+    FNODE *curNode;
+    FNODE *anotherNode;
+
+    char *_13;
+
+    char fileName[BMAX_PATH]; // watcom maxpath
+    
+    void LoadRes(void *);
+    void Load();
+};
\ No newline at end of file
diff --git a/source/blood/src/iob.cpp b/source/blood/src/iob.cpp
new file mode 100644
index 000000000..e0c0accfa
--- /dev/null
+++ b/source/blood/src/iob.cpp
@@ -0,0 +1,74 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdlib.h>
+#include <string.h>
+#include "compat.h"
+#include "common_game.h"
+#include "iob.h"
+
+IOBuffer::IOBuffer(int _nRemain, char *_pBuffer)
+{
+    nRemain = _nRemain;
+    pBuffer =_pBuffer;
+}
+
+void IOBuffer::Read(void *pData, int nSize)
+{
+    if (nSize <= nRemain)
+    {
+        memcpy(pData, pBuffer, nSize);
+        nRemain -= nSize;
+        pBuffer += nSize;
+    }
+    else
+    {
+        ThrowError("Read buffer overflow");
+    }
+}
+
+void IOBuffer::Write(void *pData, int nSize)
+{
+    if (nSize <= nRemain)
+    {
+        memcpy(pBuffer, pData, nSize);
+        nRemain -= nSize;
+        pBuffer += nSize;
+    }
+    else
+    {
+        ThrowError("Write buffer overflow");
+    }
+}
+
+void IOBuffer::Skip(int nSize)
+{
+    if (nSize <= nRemain)
+    {
+        nRemain -= nSize;
+        pBuffer += nSize;
+    }
+    else
+    {
+        ThrowError("Skip overflow");
+    }
+}
diff --git a/source/blood/src/iob.h b/source/blood/src/iob.h
new file mode 100644
index 000000000..b9379866d
--- /dev/null
+++ b/source/blood/src/iob.h
@@ -0,0 +1,34 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+class IOBuffer
+{
+public:
+    IOBuffer(int _nRemain, char *pBuffer);
+    int nRemain;
+    char *pBuffer;
+    void Read(void *, int);
+    void Write(void *, int);
+    void Skip(int);
+};
\ No newline at end of file
diff --git a/source/blood/src/levels.cpp b/source/blood/src/levels.cpp
new file mode 100644
index 000000000..57b67f0ca
--- /dev/null
+++ b/source/blood/src/levels.cpp
@@ -0,0 +1,443 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdlib.h>
+#include <string.h>
+#include "compat.h"
+#include "common_game.h"
+
+#include "asound.h"
+#include "blood.h"
+#include "config.h"
+#include "credits.h"
+#include "endgame.h"
+#include "inifile.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "messages.h"
+#include "network.h"
+#include "screen.h"
+#include "seq.h"
+#include "sound.h"
+#include "sfx.h"
+#include "view.h"
+
+GAMEOPTIONS gGameOptions;
+
+GAMEOPTIONS gSingleGameOptions = {
+    0, 2, 0, 0, "", "", 2, "", "", 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 3600, 1800, 1800, 7200
+};
+
+EPISODEINFO gEpisodeInfo[kMaxEpisodes+1];
+
+int gSkill = 2;
+int gEpisodeCount;
+int gNextLevel;
+bool gGameStarted;
+
+int gLevelTime;
+
+char BloodIniFile[BMAX_PATH] = "BLOOD.INI";
+char BloodIniPre[BMAX_PATH];
+bool bINIOverride = false;
+IniFile *BloodINI;
+
+
+void levelInitINI(const char *pzIni)
+{
+    int fp = kopen4loadfrommod(pzIni, 0);
+    if (fp < 0)
+        ThrowError("Initialization: %s does not exist", pzIni);
+    kclose(fp);
+    BloodINI = new IniFile(pzIni);
+    Bstrncpy(BloodIniFile, pzIni, BMAX_PATH);
+    Bstrncpy(BloodIniPre, pzIni, BMAX_PATH);
+    ChangeExtension(BloodIniPre, "");
+}
+
+
+void levelOverrideINI(const char *pzIni)
+{
+    bINIOverride = true;
+    strcpy(BloodIniFile, pzIni);
+}
+
+void levelPlayIntroScene(int nEpisode)
+{
+    gGameOptions.uGameFlags &= ~4;
+    sndStopSong();
+    sndKillAllSounds();
+    sfxKillAllSounds();
+    ambKillAll();
+    seqKillAll();
+    EPISODEINFO *pEpisode = &gEpisodeInfo[nEpisode];
+    credPlaySmk(pEpisode->at8f08, pEpisode->at9030, pEpisode->at9028);
+    scrSetDac();
+    viewResizeView(gViewSize);
+    credReset();
+    scrSetDac();
+}
+
+void levelPlayEndScene(int nEpisode)
+{
+    gGameOptions.uGameFlags &= ~8;
+    sndStopSong();
+    sndKillAllSounds();
+    sfxKillAllSounds();
+    ambKillAll();
+    seqKillAll();
+    EPISODEINFO *pEpisode = &gEpisodeInfo[nEpisode];
+    credPlaySmk(pEpisode->at8f98, pEpisode->at90c0, pEpisode->at902c);
+    scrSetDac();
+    viewResizeView(gViewSize);
+    credReset();
+    scrSetDac();
+}
+
+void levelClearSecrets(void)
+{
+    gSecretMgr.Clear();
+}
+
+void levelSetupSecret(int nCount)
+{
+    gSecretMgr.SetCount(nCount);
+}
+
+void levelTriggerSecret(int nSecret)
+{
+    gSecretMgr.Found(nSecret);
+}
+
+void CheckSectionAbend(const char *pzSection)
+{
+    if (!pzSection || !BloodINI->SectionExists(pzSection))
+        ThrowError("Section [%s] expected in BLOOD.INI", pzSection);
+}
+
+void CheckKeyAbend(const char *pzSection, const char *pzKey)
+{
+    dassert(pzSection != NULL);
+
+    if (!pzKey || !BloodINI->KeyExists(pzSection, pzKey))
+        ThrowError("Key %s expected in section [%s] of BLOOD.INI", pzKey, pzSection);
+}
+
+LEVELINFO * levelGetInfoPtr(int nEpisode, int nLevel)
+{
+    dassert(nEpisode >= 0 && nEpisode < gEpisodeCount);
+    EPISODEINFO *pEpisodeInfo = &gEpisodeInfo[nEpisode];
+    dassert(nLevel >= 0 && nLevel < pEpisodeInfo->nLevels);
+    return &pEpisodeInfo->at28[nLevel];
+}
+
+char * levelGetFilename(int nEpisode, int nLevel)
+{
+    dassert(nEpisode >= 0 && nEpisode < gEpisodeCount);
+    EPISODEINFO *pEpisodeInfo = &gEpisodeInfo[nEpisode];
+    dassert(nLevel >= 0 && nLevel < pEpisodeInfo->nLevels);
+    return pEpisodeInfo->at28[nLevel].at0;
+}
+
+char * levelGetMessage(int nMessage)
+{
+    int nEpisode = gGameOptions.nEpisode;
+    int nLevel = gGameOptions.nLevel;
+    dassert(nMessage < kMaxMessages);
+    char *pMessage = gEpisodeInfo[nEpisode].at28[nLevel].atec[nMessage];
+    if (*pMessage == 0)
+        return NULL;
+    return pMessage;
+}
+
+char * levelGetTitle(void)
+{
+    int nEpisode = gGameOptions.nEpisode;
+    int nLevel = gGameOptions.nLevel;
+    char *pTitle = gEpisodeInfo[nEpisode].at28[nLevel].at90;
+    if (*pTitle == 0)
+        return NULL;
+    return pTitle;
+}
+
+char * levelGetAuthor(void)
+{
+    int nEpisode = gGameOptions.nEpisode;
+    int nLevel = gGameOptions.nLevel;
+    char *pAuthor = gEpisodeInfo[nEpisode].at28[nLevel].atb0;
+    if (*pAuthor == 0)
+        return NULL;
+    return pAuthor;
+}
+
+void levelSetupOptions(int nEpisode, int nLevel)
+{
+    gGameOptions.nEpisode = nEpisode;
+    gGameOptions.nLevel = nLevel;
+    strcpy(gGameOptions.zLevelName, gEpisodeInfo[nEpisode].at28[nLevel].at0);
+    gGameOptions.uMapCRC = dbReadMapCRC(gGameOptions.zLevelName);
+    // strcpy(gGameOptions.zLevelSong, gEpisodeInfo[nEpisode].at28[nLevel].atd0);
+    gGameOptions.nTrackNumber = gEpisodeInfo[nEpisode].at28[nLevel].ate0;
+}
+
+void levelLoadMapInfo(IniFile *pIni, LEVELINFO *pLevelInfo, const char *pzSection)
+{
+    char buffer[16];
+    strncpy(pLevelInfo->at90, pIni->GetKeyString(pzSection, "Title", pLevelInfo->at0), 31);
+    strncpy(pLevelInfo->atb0, pIni->GetKeyString(pzSection, "Author", ""), 31);
+    strncpy(pLevelInfo->atd0, pIni->GetKeyString(pzSection, "Song", ""), BMAX_PATH);
+    pLevelInfo->ate0 = pIni->GetKeyInt(pzSection, "Track", -1);
+    pLevelInfo->ate4 = pIni->GetKeyInt(pzSection, "EndingA", -1);
+    pLevelInfo->ate8 = pIni->GetKeyInt(pzSection, "EndingB", -1);
+    pLevelInfo->at8ec = pIni->GetKeyInt(pzSection, "Fog", -0);
+    pLevelInfo->at8ed = pIni->GetKeyInt(pzSection, "Weather", -0);
+    for (int i = 0; i < kMaxMessages; i++)
+    {
+        sprintf(buffer, "Message%d", i+1);
+        strncpy(pLevelInfo->atec[i], pIni->GetKeyString(pzSection, buffer, ""), 63);
+    }
+}
+
+extern void MenuSetupEpisodeInfo(void);
+
+void levelLoadDefaults(void)
+{
+    char buffer[64];
+    char buffer2[16];
+    levelInitINI(pINISelected->zName);
+    memset(gEpisodeInfo, 0, sizeof(gEpisodeInfo));
+    strncpy(gEpisodeInfo[MUS_INTRO/kMaxLevels].at28[MUS_INTRO%kMaxLevels].atd0, "PESTIS", BMAX_PATH);
+    int i;
+    for (i = 0; i < kMaxEpisodes; i++)
+    {
+        sprintf(buffer, "Episode%d", i+1);
+        if (!BloodINI->SectionExists(buffer))
+            break;
+        EPISODEINFO *pEpisodeInfo = &gEpisodeInfo[i];
+        strncpy(pEpisodeInfo->at0, BloodINI->GetKeyString(buffer, "Title", buffer), 31);
+        strncpy(pEpisodeInfo->at8f08, BloodINI->GetKeyString(buffer, "CutSceneA", ""), BMAX_PATH);
+        pEpisodeInfo->at9028 = BloodINI->GetKeyInt(buffer, "CutWavA", -1);
+        if (pEpisodeInfo->at9028 == 0)
+            strncpy(pEpisodeInfo->at9030, BloodINI->GetKeyString(buffer, "CutWavA", ""), BMAX_PATH);
+        else
+            pEpisodeInfo->at9030[0] = 0;
+        strncpy(pEpisodeInfo->at8f98, BloodINI->GetKeyString(buffer, "CutSceneB", ""), BMAX_PATH);
+        pEpisodeInfo->at902c = BloodINI->GetKeyInt(buffer, "CutWavB", -1);
+        if (pEpisodeInfo->at902c == 0)
+            strncpy(pEpisodeInfo->at90c0, BloodINI->GetKeyString(buffer, "CutWavB", ""), BMAX_PATH);
+        else
+            pEpisodeInfo->at90c0[0] = 0;
+
+        pEpisodeInfo->bloodbath = BloodINI->GetKeyInt(buffer, "BloodBathOnly", 0);
+        pEpisodeInfo->cutALevel = BloodINI->GetKeyInt(buffer, "CutSceneALevel", 0);
+        if (pEpisodeInfo->cutALevel > 0)
+            pEpisodeInfo->cutALevel--;
+        int j;
+        for (j = 0; j < kMaxLevels; j++)
+        {
+            LEVELINFO *pLevelInfo = &pEpisodeInfo->at28[j];
+            sprintf(buffer2, "Map%d", j+1);
+            if (!BloodINI->KeyExists(buffer, buffer2))
+                break;
+            const char *pMap = BloodINI->GetKeyString(buffer, buffer2, NULL);
+            CheckSectionAbend(pMap);
+            strncpy(pLevelInfo->at0, pMap, BMAX_PATH);
+            levelLoadMapInfo(BloodINI, pLevelInfo, pMap);
+        }
+        pEpisodeInfo->nLevels = j;
+    }
+    gEpisodeCount = i;
+    MenuSetupEpisodeInfo();
+}
+
+void levelAddUserMap(const char *pzMap)
+{
+    char buffer[BMAX_PATH];
+    //strcpy(buffer, g_modDir);
+    strncpy(buffer, pzMap, BMAX_PATH);
+    ChangeExtension(buffer, ".DEF");
+
+    IniFile UserINI(buffer);
+    int nEpisode = ClipRange(UserINI.GetKeyInt(NULL, "Episode", 0), 0, 5);
+    EPISODEINFO *pEpisodeInfo = &gEpisodeInfo[nEpisode];
+    int nLevel = ClipRange(UserINI.GetKeyInt(NULL, "Level", pEpisodeInfo->nLevels), 0, 15);
+    if (nLevel >= pEpisodeInfo->nLevels)
+    {
+        if (pEpisodeInfo->nLevels == 0)
+        {
+            gEpisodeCount++;
+            sprintf(pEpisodeInfo->at0, "Episode %d", nEpisode);
+        }
+        nLevel = pEpisodeInfo->nLevels++;
+    }
+    LEVELINFO *pLevelInfo = &pEpisodeInfo->at28[nLevel];
+    ChangeExtension(buffer, "");
+    strncpy(pLevelInfo->at0, buffer, BMAX_PATH);
+    levelLoadMapInfo(&UserINI, pLevelInfo, NULL);
+    gGameOptions.nEpisode = nEpisode;
+    gGameOptions.nLevel = nLevel;
+    gGameOptions.uMapCRC = dbReadMapCRC(pLevelInfo->at0);
+    strcpy(gGameOptions.zLevelName, pLevelInfo->at0);
+    MenuSetupEpisodeInfo();
+}
+
+void levelGetNextLevels(int nEpisode, int nLevel, int *pnEndingA, int *pnEndingB)
+{
+    dassert(pnEndingA != NULL && pnEndingB != NULL);
+    LEVELINFO *pLevelInfo = &gEpisodeInfo[nEpisode].at28[nLevel];
+    int nEndingA = pLevelInfo->ate4;
+    if (nEndingA >= 0)
+        nEndingA--;
+    int nEndingB = pLevelInfo->ate8;
+    if (nEndingB >= 0)
+        nEndingB--;
+    *pnEndingA = nEndingA;
+    *pnEndingB = nEndingB;
+}
+
+void levelEndLevel(int arg)
+{
+    int nEndingA, nEndingB;
+    EPISODEINFO *pEpisodeInfo = &gEpisodeInfo[gGameOptions.nEpisode];
+    gGameOptions.uGameFlags |= 1;
+    levelGetNextLevels(gGameOptions.nEpisode, gGameOptions.nLevel, &nEndingA, &nEndingB);
+    switch (arg)
+    {
+    case 0:
+        if (nEndingA == -1)
+        {
+            if (pEpisodeInfo->at8f98[0])
+                gGameOptions.uGameFlags |= 8;
+            gGameOptions.nLevel = 0;
+            gGameOptions.uGameFlags |= 2;
+        }
+        else
+            gNextLevel = nEndingA;
+        break;
+    case 1:
+        if (nEndingB == -1)
+        {
+            if (gGameOptions.nEpisode + 1 < gEpisodeCount)
+            {
+                if (pEpisodeInfo->at8f98[0])
+                    gGameOptions.uGameFlags |= 8;
+                gGameOptions.nLevel = 0;
+                gGameOptions.uGameFlags |= 2;
+            }
+            else
+            {
+                gGameOptions.nLevel = 0;
+                gGameOptions.uGameFlags |= 1;
+            }
+        }
+        else
+            gNextLevel = nEndingB;
+        break;
+    }
+}
+
+// By NoOne: this function can be called via sending numbered command to TX kGDXChannelEndLevel
+// This allows to set custom next level instead of taking it from INI file.
+void levelEndLevelCustom(int nLevel) {
+
+    gGameOptions.uGameFlags |= 1;
+
+    if (nLevel >= 16  || nLevel < 0)
+    {
+
+        gGameOptions.uGameFlags |= 2;
+        gGameOptions.nLevel = 0;
+        return;
+    }
+
+    gNextLevel = nLevel;
+}
+
+void levelRestart(void)
+{
+    levelSetupOptions(gGameOptions.nEpisode, gGameOptions.nLevel);
+    gStartNewGame = true;
+}
+
+int levelGetMusicIdx(const char *str)
+{
+    int32_t lev, ep;
+    signed char b1, b2;
+
+    int numMatches = sscanf(str, "%c%d%c%d", &b1, &ep, &b2, &lev);
+
+    if (numMatches != 4 || Btoupper(b1) != 'E' || Btoupper(b2) != 'L')
+        return -1;
+
+    if ((unsigned)--lev >= kMaxLevels || (unsigned)--ep >= kMaxEpisodes)
+        return -2;
+
+    return (ep * kMaxLevels) + lev;
+}
+
+bool levelTryPlayMusic(int nEpisode, int nLevel, bool bSetLevelSong)
+{
+    char buffer[BMAX_PATH];
+    if (CDAudioToggle && gEpisodeInfo[nEpisode].at28[nLevel].ate0 > 0)
+        snprintf(buffer, BMAX_PATH, "blood%02i.ogg", gEpisodeInfo[nEpisode].at28[nLevel].ate0);
+    else
+        strncpy(buffer, gEpisodeInfo[nEpisode].at28[nLevel].atd0, BMAX_PATH);
+    bool bReturn = !!sndPlaySong(buffer, true);
+    if (!bReturn || bSetLevelSong)
+        strncpy(gGameOptions.zLevelSong, buffer, BMAX_PATH);
+    return bReturn;
+}
+
+void levelTryPlayMusicOrNothing(int nEpisode, int nLevel)
+{
+    if (levelTryPlayMusic(nEpisode, nLevel, true))
+        sndStopSong();
+}
+
+class LevelsLoadSave : public LoadSave
+{
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+
+static LevelsLoadSave *myLoadSave;
+
+void LevelsLoadSave::Load(void)
+{
+    Read(&gNextLevel, sizeof(gNextLevel));
+    Read(&gGameOptions, sizeof(gGameOptions));
+    Read(&gGameStarted, sizeof(gGameStarted));
+}
+
+void LevelsLoadSave::Save(void)
+{
+    Write(&gNextLevel, sizeof(gNextLevel));
+    Write(&gGameOptions, sizeof(gGameOptions));
+    Write(&gGameStarted, sizeof(gGameStarted));
+}
+
+void LevelsLoadSaveConstruct(void)
+{
+    myLoadSave = new LevelsLoadSave();
+}
+
diff --git a/source/blood/src/levels.h b/source/blood/src/levels.h
new file mode 100644
index 000000000..306ffff9a
--- /dev/null
+++ b/source/blood/src/levels.h
@@ -0,0 +1,138 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "common_game.h"
+#include "inifile.h"
+
+#define kMaxMessages 32
+#define kMaxEpisodes 7
+#define kMaxLevels 16
+
+#pragma pack(push, 1)
+
+struct GAMEOPTIONS {
+    char nGameType;
+    char nDifficulty;
+    int nEpisode;
+    int nLevel;
+    char zLevelName[BMAX_PATH];
+    char zLevelSong[BMAX_PATH];
+    int nTrackNumber; //at12a;
+    char szSaveGameName[BMAX_PATH];
+    char szUserGameName[BMAX_PATH];
+    short nSaveGameSlot;
+    int picEntry;
+    unsigned int uMapCRC;
+    char nMonsterSettings;
+    int uGameFlags;
+    int uNetGameFlags;
+    char nWeaponSettings;
+    char nItemSettings;
+    char nRespawnSettings;
+    char nTeamSettings;
+    int nMonsterRespawnTime;
+    int nWeaponRespawnTime;
+    int nItemRespawnTime;
+    int nSpecialRespawnTime;
+};
+
+#pragma pack(pop)
+
+enum {
+    MUS_FIRST_SPECIAL = kMaxEpisodes*kMaxLevels,
+
+    MUS_INTRO = MUS_FIRST_SPECIAL,
+    MUS_LOADING = MUS_FIRST_SPECIAL + 1,
+};
+
+struct LEVELINFO
+{
+    char at0[BMAX_PATH]; // Filename
+    char at90[32]; // Title
+    char atb0[32]; // Author
+    char atd0[BMAX_PATH]; // Song;
+    int ate0; // SongId
+    int ate4; // EndingA
+    int ate8; // EndingB
+    char atec[kMaxMessages][64]; // Messages
+    char at8ec; // Fog
+    char at8ed; // Weather
+}; // 0x8ee bytes
+
+struct EPISODEINFO
+{
+    char at0[32];
+    int nLevels;
+    unsigned int bloodbath : 1;
+    unsigned int cutALevel : 4;
+    LEVELINFO at28[kMaxLevels];
+    char at8f08[BMAX_PATH];
+    char at8f98[BMAX_PATH];
+    int at9028;
+    int at902c;
+    char at9030[BMAX_PATH];
+    char at90c0[BMAX_PATH];
+};
+
+extern EPISODEINFO gEpisodeInfo[];
+extern GAMEOPTIONS gSingleGameOptions;
+extern GAMEOPTIONS gGameOptions;
+extern int gSkill;
+extern char BloodIniFile[];
+extern char BloodIniPre[];
+extern bool bINIOverride;
+extern int gEpisodeCount;
+extern int gNextLevel;
+extern bool gGameStarted;
+extern int gLevelTime;
+
+void levelInitINI(const char *pzIni);
+void levelOverrideINI(const char *pzIni);
+void levelPlayIntroScene(int nEpisode);
+void levelPlayEndScene(int nEpisode);
+void levelClearSecrets(void);
+void levelSetupSecret(int nCount);
+void levelTriggerSecret(int nSecret);
+void CheckSectionAbend(const char *pzSection);
+void CheckKeyAbend(const char *pzSection, const char *pzKey);
+LEVELINFO * levelGetInfoPtr(int nEpisode, int nLevel);
+char * levelGetFilename(int nEpisode, int nLevel);
+char * levelGetMessage(int nMessage);
+char * levelGetTitle(void);
+char * levelGetAuthor(void);
+void levelSetupOptions(int nEpisode, int nLevel);
+void levelLoadMapInfo(IniFile *pIni, LEVELINFO *pLevelInfo, const char *pzSection);
+void levelLoadDefaults(void);
+void levelAddUserMap(const char *pzMap);
+// EndingA is normal ending, EndingB is secret level
+void levelGetNextLevels(int nEpisode, int nLevel, int *pnEndingA, int *pnEndingB);
+// arg: 0 is normal exit, 1 is secret level
+void levelEndLevel(int arg);
+
+// By NoOne: custom level selection via numbered command which sent to TX ID 6.
+void levelEndLevelCustom(int nLevel);
+
+void levelRestart(void);
+int levelGetMusicIdx(const char *str);
+bool levelTryPlayMusic(int nEpisode, int nlevel, bool bSetLevelSong = false);
+void levelTryPlayMusicOrNothing(int nEpisode, int nLevel);
diff --git a/source/blood/src/loadsave.cpp b/source/blood/src/loadsave.cpp
new file mode 100644
index 000000000..ad08434b2
--- /dev/null
+++ b/source/blood/src/loadsave.cpp
@@ -0,0 +1,485 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include "build.h"
+#include "compat.h"
+#include "mmulti.h"
+#include "common_game.h"
+#include "config.h"
+#include "ai.h"
+#include "asound.h"
+#include "blood.h"
+#include "demo.h"
+#include "globals.h"
+#include "db.h"
+#include "messages.h"
+#include "menu.h"
+#include "network.h"
+#include "loadsave.h"
+#include "resource.h"
+#include "screen.h"
+#include "sectorfx.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "view.h"
+
+GAMEOPTIONS gSaveGameOptions[10];
+char *gSaveGamePic[10];
+unsigned int gSavedOffset = 0;
+
+unsigned int dword_27AA38 = 0;
+unsigned int dword_27AA3C = 0;
+unsigned int dword_27AA40 = 0;
+void *dword_27AA44 = NULL;
+
+LoadSave LoadSave::head(123);
+FILE *LoadSave::hSFile = NULL;
+int LoadSave::hLFile = -1;
+
+short word_27AA54 = 0;
+
+void sub_76FD4(void)
+{
+    if (!dword_27AA44)
+        dword_27AA44 = Resource::Alloc(0x186a0);
+}
+
+void LoadSave::Save(void)
+{
+    ThrowError("Pure virtual function called");
+}
+
+void LoadSave::Load(void)
+{
+    ThrowError("Pure virtual function called");
+}
+
+void LoadSave::Read(void *pData, int nSize)
+{
+    dword_27AA38 += nSize;
+    dassert(hLFile != -1);
+    if (kread(hLFile, pData, nSize) != nSize)
+        ThrowError("Error reading save file.");
+}
+
+void LoadSave::Write(void *pData, int nSize)
+{
+    dword_27AA38 += nSize;
+    dword_27AA3C += nSize;
+    dassert(hSFile != NULL);
+    if (fwrite(pData, 1, nSize, hSFile) != (size_t)nSize)
+        ThrowError("File error #%d writing save file.", errno);
+}
+
+void LoadSave::LoadGame(char *pzFile)
+{
+    bool demoWasPlayed = gDemo.at1;
+    if (gDemo.at1)
+        gDemo.Close();
+
+    sndKillAllSounds();
+    sfxKillAllSounds();
+    ambKillAll();
+    seqKillAll();
+    if (!gGameStarted)
+    {
+        memset(xsprite, 0, sizeof(xsprite));
+        memset(sprite, 0, sizeof(spritetype)*kMaxSprites);
+        automapping = 1;
+    }
+    hLFile = kopen4load(pzFile, 0);
+    if (hLFile == -1)
+        ThrowError("Error loading save file.");
+    LoadSave *rover = head.next;
+    while (rover != &head)
+    {
+        rover->Load();
+        rover = rover->next;
+    }
+    kclose(hLFile);
+    hLFile = -1;
+    if (!gGameStarted)
+        scrLoadPLUs();
+    InitSectorFX();
+    viewInitializePrediction();
+    PreloadCache();
+    if (!bVanilla && !gMe->packInfo[1].at0) // if diving suit is not active, turn off reverb sound effect
+        sfxSetReverb(0);
+    ambInit();
+    memset(myMinLag, 0, sizeof(myMinLag));
+    otherMinLag = 0;
+    myMaxLag = 0;
+    gNetFifoClock = 0;
+    gNetFifoTail = 0;
+    memset(gNetFifoHead, 0, sizeof(gNetFifoHead));
+    gPredictTail = 0;
+    gNetFifoMasterTail = 0;
+    memset(gFifoInput, 0, sizeof(gFifoInput));
+    memset(gChecksum, 0, sizeof(gChecksum));
+    memset(gCheckFifo, 0, sizeof(gCheckFifo));
+    memset(gCheckHead, 0, sizeof(gCheckHead));
+    gSendCheckTail = 0;
+    gCheckTail = 0;
+    gBufferJitter = 0;
+    bOutOfSync = 0;
+    for (int i = 0; i < gNetPlayers; i++)
+        playerSetRace(&gPlayer[i], gPlayer[i].at5f);
+    viewSetMessage("");
+    viewSetErrorMessage("");
+    if (!gGameStarted)
+    {
+        netWaitForEveryone(0);
+        memset(gPlayerReady, 0, sizeof(gPlayerReady));
+    }
+    gFrameTicks = 0;
+    gFrame = 0;
+    gCacheMiss = 0;
+    gFrameRate = 0;
+    gGameClock = 0;
+    gPaused = 0;
+    gGameStarted = 1;
+    bVanilla = false;
+
+    if (MusicRestartsOnLoadToggle
+        || demoWasPlayed
+        || (gMusicPrevLoadedEpisode != gGameOptions.nEpisode || gMusicPrevLoadedLevel != gGameOptions.nLevel))
+    {
+        levelTryPlayMusic(gGameOptions.nEpisode, gGameOptions.nLevel);
+    }
+    gMusicPrevLoadedEpisode = gGameOptions.nEpisode;
+    gMusicPrevLoadedLevel = gGameOptions.nLevel;
+
+    netBroadcastPlayerInfo(myconnectindex);
+    //sndPlaySong(gGameOptions.zLevelSong, 1);
+}
+
+void LoadSave::SaveGame(char *pzFile)
+{
+    hSFile = fopen(pzFile, "wb");
+    if (hSFile == NULL)
+        ThrowError("File error #%d creating save file.", errno);
+    dword_27AA38 = 0;
+    dword_27AA40 = 0;
+    LoadSave *rover = head.next;
+    while (rover != &head)
+    {
+        rover->Save();
+        if (dword_27AA38 > dword_27AA40)
+            dword_27AA40 = dword_27AA38;
+        dword_27AA38 = 0;
+        rover = rover->next;
+    }
+    fclose(hSFile);
+    hSFile = NULL;
+}
+
+class MyLoadSave : public LoadSave
+{
+public:
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+void MyLoadSave::Load(void)
+{
+    psky_t *pSky = tileSetupSky(0);
+    int id;
+    Read(&id, sizeof(id));
+    if (id != 0x5653424e/*'VSBN'*/)
+        ThrowError("Old saved game found");
+    short version;
+    Read(&version, sizeof(version));
+    if (version != BYTEVERSION)
+        ThrowError("Incompatible version of saved game found!");
+    Read(&gGameOptions, sizeof(gGameOptions));
+    Read(&numsectors, sizeof(numsectors));
+    Read(&numwalls, sizeof(numwalls));
+    Read(&numsectors, sizeof(numsectors));
+    int nNumSprites;
+    Read(&nNumSprites, sizeof(nNumSprites));
+    memset(sector, 0, sizeof(sector[0])*kMaxSectors);
+    memset(wall, 0, sizeof(wall[0])*kMaxWalls);
+    memset(sprite, 0, sizeof(sprite[0])*kMaxSprites);
+    Read(sector, sizeof(sector[0])*numsectors);
+    Read(wall, sizeof(wall[0])*numwalls);
+    Read(sprite, sizeof(sprite[0])*kMaxSprites);
+    Read(qsector_filler, sizeof(qsector_filler[0])*numsectors);
+    Read(qsprite_filler, sizeof(qsprite_filler[0])*kMaxSprites);
+    Read(&randomseed, sizeof(randomseed));
+    Read(&parallaxtype, sizeof(parallaxtype));
+    Read(&showinvisibility, sizeof(showinvisibility));
+    Read(&pSky->horizfrac, sizeof(pSky->horizfrac));
+    Read(&pSky->yoffs, sizeof(pSky->yoffs));
+    Read(&pSky->yscale, sizeof(pSky->yscale));
+    Read(&gVisibility, sizeof(gVisibility));
+    Read(&g_visibility, sizeof(g_visibility));
+    Read(&parallaxvisibility, sizeof(parallaxvisibility));
+    Read(pSky->tileofs, sizeof(pSky->tileofs));
+    Read(&pSky->lognumtiles, sizeof(pSky->lognumtiles));
+    Read(headspritesect, sizeof(headspritesect));
+    Read(headspritestat, sizeof(headspritestat));
+    Read(prevspritesect, sizeof(prevspritesect));
+    Read(prevspritestat, sizeof(prevspritestat));
+    Read(nextspritesect, sizeof(nextspritesect));
+    Read(nextspritestat, sizeof(nextspritestat));
+    Read(show2dsector, sizeof(show2dsector));
+    Read(show2dwall, sizeof(show2dwall));
+    Read(show2dsprite, sizeof(show2dsprite));
+    Read(&automapping, sizeof(automapping));
+    Read(gotpic, sizeof(gotpic));
+    Read(gotsector, sizeof(gotsector));
+    Read(&gFrameClock, sizeof(gFrameClock));
+    Read(&gFrameTicks, sizeof(gFrameTicks));
+    Read(&gFrame, sizeof(gFrame));
+    int nGameClock;
+    Read(&nGameClock, sizeof(nGameClock));
+    gGameClock = nGameClock;
+    Read(&gLevelTime, sizeof(gLevelTime));
+    Read(&gPaused, sizeof(gPaused));
+    Read(&gbAdultContent, sizeof(gbAdultContent));
+    Read(baseWall, sizeof(baseWall[0])*numwalls);
+    Read(baseSprite, sizeof(baseSprite[0])*nNumSprites);
+    Read(baseFloor, sizeof(baseFloor[0])*numsectors);
+    Read(baseCeil, sizeof(baseCeil[0])*numsectors);
+    Read(velFloor, sizeof(velFloor[0])*numsectors);
+    Read(velCeil, sizeof(velCeil[0])*numsectors);
+    Read(&gHitInfo, sizeof(gHitInfo));
+    Read(&byte_1A76C6, sizeof(byte_1A76C6));
+    Read(&byte_1A76C8, sizeof(byte_1A76C8));
+    Read(&byte_1A76C7, sizeof(byte_1A76C7));
+    Read(&byte_19AE44, sizeof(byte_19AE44));
+    Read(gStatCount, sizeof(gStatCount));
+    Read(nextXSprite, sizeof(nextXSprite));
+    Read(nextXWall, sizeof(nextXWall));
+    Read(nextXSector, sizeof(nextXSector));
+    memset(xsprite, 0, sizeof(xsprite));
+    for (int nSprite = 0; nSprite < kMaxSprites; nSprite++)
+    {
+        if (sprite[nSprite].statnum < kMaxStatus)
+        {
+            int nXSprite = sprite[nSprite].extra;
+            if (nXSprite > 0)
+                Read(&xsprite[nXSprite], sizeof(XSPRITE));
+        }
+    }
+    memset(xwall, 0, sizeof(xwall));
+    for (int nWall = 0; nWall < numwalls; nWall++)
+    {
+        int nXWall = wall[nWall].extra;
+        if (nXWall > 0)
+            Read(&xwall[nXWall], sizeof(XWALL));
+    }
+    memset(xsector, 0, sizeof(xsector));
+    for (int nSector = 0; nSector < numsectors; nSector++)
+    {
+        int nXSector = sector[nSector].extra;
+        if (nXSector > 0)
+            Read(&xsector[nXSector], sizeof(XSECTOR));
+    }
+    Read(xvel, nNumSprites*sizeof(xvel[0]));
+    Read(yvel, nNumSprites*sizeof(yvel[0]));
+    Read(zvel, nNumSprites*sizeof(zvel[0]));
+    Read(&gMapRev, sizeof(gMapRev));
+    Read(&gSongId, sizeof(gSkyCount));
+    Read(&gFogMode, sizeof(gFogMode));
+    gCheatMgr.sub_5BCF4();
+}
+
+void MyLoadSave::Save(void)
+{
+    psky_t *pSky = tileSetupSky(0);
+    int nNumSprites = 0;
+    int id = 0x5653424e/*'VSBN'*/;
+    Write(&id, sizeof(id));
+    short version = BYTEVERSION;
+    Write(&version, sizeof(version));
+    for (int nSprite = 0; nSprite < kMaxSprites; nSprite++)
+    {
+        if (sprite[nSprite].statnum < kMaxStatus && nSprite > nNumSprites)
+            nNumSprites = nSprite;
+    }
+    //nNumSprites += 2;
+    nNumSprites++;
+    Write(&gGameOptions, sizeof(gGameOptions));
+    Write(&numsectors, sizeof(numsectors));
+    Write(&numwalls, sizeof(numwalls));
+    Write(&numsectors, sizeof(numsectors));
+    Write(&nNumSprites, sizeof(nNumSprites));
+    Write(sector, sizeof(sector[0])*numsectors);
+    Write(wall, sizeof(wall[0])*numwalls);
+    Write(sprite, sizeof(sprite[0])*kMaxSprites);
+    Write(qsector_filler, sizeof(qsector_filler[0])*numsectors);
+    Write(qsprite_filler, sizeof(qsprite_filler[0])*kMaxSprites);
+    Write(&randomseed, sizeof(randomseed));
+    Write(&parallaxtype, sizeof(parallaxtype));
+    Write(&showinvisibility, sizeof(showinvisibility));
+    Write(&pSky->horizfrac, sizeof(pSky->horizfrac));
+    Write(&pSky->yoffs, sizeof(pSky->yoffs));
+    Write(&pSky->yscale, sizeof(pSky->yscale));
+    Write(&gVisibility, sizeof(gVisibility));
+    Write(&g_visibility, sizeof(g_visibility));
+    Write(&parallaxvisibility, sizeof(parallaxvisibility));
+    Write(pSky->tileofs, sizeof(pSky->tileofs));
+    Write(&pSky->lognumtiles, sizeof(pSky->lognumtiles));
+    Write(headspritesect, sizeof(headspritesect));
+    Write(headspritestat, sizeof(headspritestat));
+    Write(prevspritesect, sizeof(prevspritesect));
+    Write(prevspritestat, sizeof(prevspritestat));
+    Write(nextspritesect, sizeof(nextspritesect));
+    Write(nextspritestat, sizeof(nextspritestat));
+    Write(show2dsector, sizeof(show2dsector));
+    Write(show2dwall, sizeof(show2dwall));
+    Write(show2dsprite, sizeof(show2dsprite));
+    Write(&automapping, sizeof(automapping));
+    Write(gotpic, sizeof(gotpic));
+    Write(gotsector, sizeof(gotsector));
+    Write(&gFrameClock, sizeof(gFrameClock));
+    Write(&gFrameTicks, sizeof(gFrameTicks));
+    Write(&gFrame, sizeof(gFrame));
+    int nGameClock = gGameClock;
+    Write(&nGameClock, sizeof(nGameClock));
+    Write(&gLevelTime, sizeof(gLevelTime));
+    Write(&gPaused, sizeof(gPaused));
+    Write(&gbAdultContent, sizeof(gbAdultContent));
+    Write(baseWall, sizeof(baseWall[0])*numwalls);
+    Write(baseSprite, sizeof(baseSprite[0])*nNumSprites);
+    Write(baseFloor, sizeof(baseFloor[0])*numsectors);
+    Write(baseCeil, sizeof(baseCeil[0])*numsectors);
+    Write(velFloor, sizeof(velFloor[0])*numsectors);
+    Write(velCeil, sizeof(velCeil[0])*numsectors);
+    Write(&gHitInfo, sizeof(gHitInfo));
+    Write(&byte_1A76C6, sizeof(byte_1A76C6));
+    Write(&byte_1A76C8, sizeof(byte_1A76C8));
+    Write(&byte_1A76C7, sizeof(byte_1A76C7));
+    Write(&byte_19AE44, sizeof(byte_19AE44));
+    Write(gStatCount, sizeof(gStatCount));
+    Write(nextXSprite, sizeof(nextXSprite));
+    Write(nextXWall, sizeof(nextXWall));
+    Write(nextXSector, sizeof(nextXSector));
+    for (int nSprite = 0; nSprite < kMaxSprites; nSprite++)
+    {
+        if (sprite[nSprite].statnum < kMaxStatus)
+        {
+            int nXSprite = sprite[nSprite].extra;
+            if (nXSprite > 0)
+                Write(&xsprite[nXSprite], sizeof(XSPRITE));
+        }
+    }
+    for (int nWall = 0; nWall < numwalls; nWall++)
+    {
+        int nXWall = wall[nWall].extra;
+        if (nXWall > 0)
+            Write(&xwall[nXWall], sizeof(XWALL));
+    }
+    for (int nSector = 0; nSector < numsectors; nSector++)
+    {
+        int nXSector = sector[nSector].extra;
+        if (nXSector > 0)
+            Write(&xsector[nXSector], sizeof(XSECTOR));
+    }
+    Write(xvel, nNumSprites*sizeof(xvel[0]));
+    Write(yvel, nNumSprites*sizeof(yvel[0]));
+    Write(zvel, nNumSprites*sizeof(zvel[0]));
+    Write(&gMapRev, sizeof(gMapRev));
+    Write(&gSongId, sizeof(gSkyCount));
+    Write(&gFogMode, sizeof(gFogMode));
+}
+
+void LoadSavedInfo(void)
+{
+    auto pList = klistpath("./", "game*.sav", CACHE1D_FIND_FILE);
+    int nCount = 0;
+    for (auto pIterator = pList; pIterator != NULL && nCount < 10; pIterator = pIterator->next, nCount++)
+    {
+        int hFile = kopen4loadfrommod(pIterator->name, 0);
+        if (hFile == -1)
+            ThrowError("Error loading save file header.");
+        int vc;
+        short v4;
+        vc = 0;
+        v4 = word_27AA54;
+        if ((uint32_t)kread(hFile, &vc, sizeof(vc)) != sizeof(vc))
+        {
+            kclose(hFile);
+            continue;
+        }
+        if (vc != 0x5653424e/*'VSBN'*/)
+        {
+            kclose(hFile);
+            continue;
+        }
+        kread(hFile, &v4, sizeof(v4));
+        if (v4 != BYTEVERSION)
+        {
+            kclose(hFile);
+            continue;
+        }
+        if ((uint32_t)kread(hFile, &gSaveGameOptions[nCount], sizeof(gSaveGameOptions[0])) != sizeof(gSaveGameOptions[0]))
+            ThrowError("Error reading save file.");
+        strcpy(strRestoreGameStrings[gSaveGameOptions[nCount].nSaveGameSlot], gSaveGameOptions[nCount].szUserGameName);
+        kclose(hFile);
+    }
+    klistfree(pList);
+}
+
+void UpdateSavedInfo(int nSlot)
+{
+    strcpy(strRestoreGameStrings[gSaveGameOptions[nSlot].nSaveGameSlot], gSaveGameOptions[nSlot].szUserGameName);
+}
+
+static MyLoadSave *myLoadSave;
+
+
+void LoadSaveSetup(void)
+{
+    void ActorLoadSaveConstruct(void);
+    void AILoadSaveConstruct(void);
+    void EndGameLoadSaveConstruct(void);
+    void EventQLoadSaveConstruct(void);
+    void LevelsLoadSaveConstruct(void);
+    void MessagesLoadSaveConstruct(void);
+    void MirrorLoadSaveConstruct(void);
+    void PlayerLoadSaveConstruct(void);
+    void SeqLoadSaveConstruct(void);
+    void TriggersLoadSaveConstruct(void);
+    void ViewLoadSaveConstruct(void);
+    void WarpLoadSaveConstruct(void);
+    void WeaponLoadSaveConstruct(void);
+
+    myLoadSave = new MyLoadSave();
+
+    ActorLoadSaveConstruct();
+    AILoadSaveConstruct();
+    EndGameLoadSaveConstruct();
+    EventQLoadSaveConstruct();
+    LevelsLoadSaveConstruct();
+    MessagesLoadSaveConstruct();
+    MirrorLoadSaveConstruct();
+    PlayerLoadSaveConstruct();
+    SeqLoadSaveConstruct();
+    TriggersLoadSaveConstruct();
+    ViewLoadSaveConstruct();
+    WarpLoadSaveConstruct();
+    WeaponLoadSaveConstruct();
+}
\ No newline at end of file
diff --git a/source/blood/src/loadsave.h b/source/blood/src/loadsave.h
new file mode 100644
index 000000000..0f8c79c57
--- /dev/null
+++ b/source/blood/src/loadsave.h
@@ -0,0 +1,60 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include <stdio.h>
+#include "levels.h"
+
+class LoadSave {
+public:
+    static LoadSave head;
+    static FILE *hSFile;
+    static int hLFile;
+    LoadSave *prev;
+    LoadSave *next;
+    LoadSave() {
+        prev = head.prev;
+        prev->next = this;
+        next = &head;
+        next->prev = this;
+    }
+    LoadSave(int dummy)
+    {
+        UNREFERENCED_PARAMETER(dummy);
+        next = prev = this;
+    }
+    //~LoadSave() { }
+    virtual void Save(void);
+    virtual void Load(void);
+    void Read(void *, int);
+    void Write(void *, int);
+    static void LoadGame(char *);
+    static void SaveGame(char *);
+};
+
+extern unsigned int gSavedOffset;
+extern GAMEOPTIONS gSaveGameOptions[];
+extern char *gSaveGamePic[10];
+
+void UpdateSavedInfo(int nSlot);
+void LoadSavedInfo(void);
+void LoadSaveSetup(void);
diff --git a/source/blood/src/map2d.cpp b/source/blood/src/map2d.cpp
new file mode 100644
index 000000000..7e6d89fe7
--- /dev/null
+++ b/source/blood/src/map2d.cpp
@@ -0,0 +1,255 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "mmulti.h"
+#include "common_game.h"
+#include "levels.h"
+#include "map2d.h"
+#include "trig.h"
+#include "view.h"
+
+char byte_27AACB;
+
+void sub_2541C(int x, int y, int z, short a)
+{
+    int tmpydim = (xdim * 5) / 8;
+    renderSetAspect(65536, divscale16(tmpydim * 320, xdim * 200));
+    int nCos = z*sintable[(0-a)&2047];
+    int nSin = z*sintable[(1536-a)&2047];
+    int nCos2 = mulscale16(nCos, yxaspect);
+    int nSin2 = mulscale16(nSin, yxaspect);
+    for (int i = 0; i < numsectors; i++)
+    {
+        if (byte_27AACB || (show2dsector[i>>3]&(1<<(i&7))))
+        {
+            int nStartWall = sector[i].wallptr;
+            int nEndWall = nStartWall+sector[i].wallnum;
+            int nZCeil = sector[i].ceilingz;
+            int nZFloor = sector[i].floorz;
+            walltype *pWall = &wall[nStartWall];
+            for (int j = nStartWall; j < nEndWall; j++, pWall++)
+            {
+                int nNextWall = pWall->nextwall;
+                if (nNextWall < 0)
+                    continue;
+                if (sector[pWall->nextsector].ceilingz == nZCeil && sector[pWall->nextsector].floorz == nZFloor
+                    && ((wall[nNextWall].cstat | pWall->cstat) & 0x30) == 0)
+                    continue;
+                if (byte_27AACB)
+                    continue;
+                if (show2dsector[pWall->nextsector>>3]&(1<<(pWall->nextsector&7)))
+                    continue;
+                int wx = pWall->x-x;
+                int wy = pWall->y-y;
+                int cx = xdim<<11;
+                int x1 = cx+dmulscale16(wx, nCos, -wy, nSin);
+                int cy = ydim<<11;
+                int y1 = cy+dmulscale16(wy, nCos2, wx, nSin2);
+                walltype *pWall2 = &wall[pWall->point2];
+                wx = pWall2->x-x;
+                wy = pWall2->y-y;
+                int x2 = cx+dmulscale16(wx, nCos, -wy, nSin);
+                int y2 = cy+dmulscale16(wy, nCos2, wx, nSin2);
+                renderDrawLine(x1,y1,x2,y2,24);
+            }
+        }
+    }
+    int nPSprite = gView->pSprite->index;
+    for (int i = 0; i < numsectors; i++)
+    {
+        if (byte_27AACB || (show2dsector[i>>3]&(1<<(i&7))))
+        {
+            for (int nSprite = headspritesect[i]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+            {
+                spritetype *pSprite = &sprite[nSprite];
+                if (nSprite == nPSprite)
+                    continue;
+                if (pSprite->cstat&32768)
+                    continue;
+            }
+        }
+    }
+    for (int i = 0; i < numsectors; i++)
+    {
+        if (byte_27AACB || (show2dsector[i>>3]&(1<<(i&7))))
+        {
+            int nStartWall = sector[i].wallptr;
+            int nEndWall = nStartWall+sector[i].wallnum;
+            walltype *pWall = &wall[nStartWall];
+            int nNWall = -1;
+            int x1, y1, x2 = 0, y2 = 0;
+            for (int j = nStartWall; j < nEndWall; j++, pWall++)
+            {
+                int nNextWall = pWall->nextwall;
+                if (nNextWall >= 0)
+                    continue;
+                if (!tilesiz[pWall->picnum].x || !tilesiz[pWall->picnum].y)
+                    continue;
+                if (nNWall == j)
+                {
+                    x1 = x2;
+                    y1 = y2;
+                }
+                else
+                {
+                    int wx = pWall->x-x;
+                    int wy = pWall->y-y;
+                    x1 = (xdim<<11)+dmulscale16(wx, nCos, -wy, nSin);
+                    y1 = (ydim<<11)+dmulscale16(wy, nCos2, wx, nSin2);
+                }
+                nNWall = pWall->point2;
+                walltype *pWall2 = &wall[nNWall];
+                int wx = pWall2->x-x;
+                int wy = pWall2->y-y;
+                x2 = (xdim<<11)+dmulscale16(wx, nCos, -wy, nSin);
+                y2 = (ydim<<11)+dmulscale16(wy, nCos2, wx, nSin2);
+                renderDrawLine(x1,y1,x2,y2,24);
+            }
+        }
+    }
+    videoSetCorrectedAspect();
+
+    for (int i = connecthead; i >= 0; i = connectpoint2[i])
+    {
+        if (gViewMap.bFollowMode || gView->at57 != i)
+        {
+            PLAYER *pPlayer = &gPlayer[i];
+            spritetype *pSprite = pPlayer->pSprite;
+            int px = pSprite->x-x;
+            int py = pSprite->y-y;
+            int pa = (pSprite->ang-a)&2047;
+            if (i == gView->at57)
+            {
+                px = 0;
+                py = 0;
+                pa = 0;
+            }
+            int x1 = dmulscale16(px, nCos, -py, nSin);
+            int y1 = dmulscale16(py, nCos2, px, nSin2);
+            if (i == gView->at57 || gGameOptions.nGameType == 1)
+            {
+                int nTile = pSprite->picnum;
+                int ceilZ, ceilHit, floorZ, floorHit;
+                GetZRange(pSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, (pSprite->clipdist<<2)+16, 0x13001);
+                int nTop, nBottom;
+                GetSpriteExtents(pSprite, &nTop, &nBottom);
+                int nScale = mulscale((pSprite->yrepeat+((floorZ-nBottom)>>8))*z, yxaspect, 16);
+                nScale = ClipRange(nScale, 8000, 65536<<1);
+                rotatesprite((xdim<<15)+(x1<<4), (ydim<<15)+(y1<<4), nScale, pa, nTile, pSprite->shade, pSprite->pal, (pSprite->cstat&2)>>1,
+                    windowxy1.x, windowxy1.y, windowxy2.x, windowxy2.y);
+            }
+        }
+    }
+}
+
+CViewMap::CViewMap()
+{
+    bActive = 0;
+}
+
+void CViewMap::sub_25C38(int _x, int _y, int _angle, short zoom, char unk1)
+{
+    bActive = 1;
+    x = _x;
+    y = _y;
+    angle = _angle;
+    nZoom = zoom;
+    FollowMode(unk1);
+    forward = 0;
+    turn = 0;
+    strafe = 0;
+}
+
+void CViewMap::sub_25C74(void)
+{
+    char pBuffer[128];
+    if (!bActive)
+        return;
+    char tm = 0;
+    int viewSize = gViewSize;
+    if (gViewSize > 3)
+    {
+        viewResizeView(3);
+        tm = 1;
+    }
+    videoClearScreen(0);
+    renderDrawMapView(x,y,nZoom>>2,angle);
+    sub_2541C(x,y,nZoom>>2,angle);
+    char *pTitle = levelGetTitle();
+    char *pFilename = levelGetFilename(gGameOptions.nEpisode, gGameOptions.nLevel);
+    if (pTitle)
+        sprintf(pBuffer, "%s: %s", pFilename, pTitle);
+    else
+        sprintf(pBuffer, "%s", pFilename);
+    int nViewY;
+    if (gViewSize > 3)
+        nViewY = gViewY1S-16;
+    else
+        nViewY = gViewY0S+1;
+    viewDrawText(3, pBuffer, gViewX1S, nViewY, -128, 0, 2, 0, 256);
+
+    if (gViewMap.bFollowMode)
+        viewDrawText(3, "MAP FOLLOW MODE", gViewX1S, nViewY+8, -128, 0, 2, 0, 256);
+    else
+        viewDrawText(3, "MAP SCROLL MODE", gViewX1S, nViewY+8, -128, 0, 2, 0, 256);
+    if (tm)
+        viewResizeView(viewSize);
+}
+
+void CViewMap::sub_25DB0(spritetype *pSprite)
+{
+    nZoom = gZoom;
+    if (bFollowMode)
+    {
+        x = pSprite->x;
+        y = pSprite->y;
+        angle = pSprite->ang;
+    }
+    else
+    {
+        angle += turn>>3;
+        x += mulscale24(forward, Cos(angle));
+        y += mulscale24(forward, Sin(angle));
+        x -= mulscale24(strafe, Cos(angle+512));
+        y -= mulscale24(strafe, Sin(angle+512));
+        forward = 0;
+        strafe = 0;
+        turn = 0;
+    }
+    sub_25C74();
+}
+
+void CViewMap::sub_25E84(int *_x, int *_y)
+{
+    if (_x)
+        *_x = x;
+    if (_y)
+        *_y = y;
+}
+
+void CViewMap::FollowMode(char mode)
+{
+    bFollowMode = mode;
+}
+
+CViewMap gViewMap;
diff --git a/source/blood/src/map2d.h b/source/blood/src/map2d.h
new file mode 100644
index 000000000..b74c819c8
--- /dev/null
+++ b/source/blood/src/map2d.h
@@ -0,0 +1,41 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "common_game.h"
+#include "db.h"
+class CViewMap {
+public:
+    char bActive;
+    int x, y, nZoom;
+    short angle;
+    char bFollowMode;
+    int forward, turn, strafe;
+    CViewMap();
+    void sub_25C38(int, int, int, short, char);
+    void sub_25C74(void);
+    void sub_25DB0(spritetype *pSprite);
+    void sub_25E84(int *, int*);
+    void FollowMode(char);
+};
+
+extern CViewMap gViewMap;
diff --git a/source/blood/src/menu.cpp b/source/blood/src/menu.cpp
new file mode 100644
index 000000000..76d7a9e05
--- /dev/null
+++ b/source/blood/src/menu.cpp
@@ -0,0 +1,2249 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "mmulti.h"
+#include "common_game.h"
+#include "fx_man.h"
+#include "music.h"
+#include "blood.h"
+#include "demo.h"
+#include "config.h"
+#include "gamemenu.h"
+#include "globals.h"
+#include "loadsave.h"
+#include "menu.h"
+#include "messages.h"
+#include "network.h"
+#include "osdcmds.h"
+#include "sfx.h"
+#include "screen.h"
+#include "sound.h"
+#include "view.h"
+
+void SaveGame(CGameMenuItemZEditBitmap *, CGameMenuEvent *);
+
+void SaveGameProcess(CGameMenuItemChain *);
+void SetDifficultyAndStart(CGameMenuItemChain *);
+void SetDetail(CGameMenuItemSlider *);
+void SetGamma(CGameMenuItemSlider *);
+void SetMusicVol(CGameMenuItemSlider *);
+void SetSoundVol(CGameMenuItemSlider *);
+void SetCDVol(CGameMenuItemSlider *);
+void SetDoppler(CGameMenuItemZBool *);
+void SetCrosshair(CGameMenuItemZBool *);
+void SetCenterHoriz(CGameMenuItemZBool *);
+void SetShowWeapons(CGameMenuItemZBool *);
+void SetSlopeTilting(CGameMenuItemZBool *);
+void SetViewBobbing(CGameMenuItemZBool *);
+void SetViewSwaying(CGameMenuItemZBool *);
+void SetMouseSensitivity(CGameMenuItemSliderFloat *);
+void SetMouseAimFlipped(CGameMenuItemZBool *);
+void SetTurnSpeed(CGameMenuItemSlider *);
+void ResetKeys(CGameMenuItemChain *);
+void ResetKeysClassic(CGameMenuItemChain *);
+void SetMessages(CGameMenuItemZBool *);
+void LoadGame(CGameMenuItemZEditBitmap *, CGameMenuEvent *);
+void SetupNetLevels(CGameMenuItemZCycle *);
+void StartNetGame(CGameMenuItemChain *);
+void SetParentalLock(CGameMenuItemZBool *);
+void TenProcess(CGameMenuItem7EA1C *);
+void SetupLevelMenuItem(int);
+void SetupVideoModeMenu(CGameMenuItemChain *);
+void SetVideoMode(CGameMenuItemChain *);
+void SetWidescreen(CGameMenuItemZBool *);
+void SetFOV(CGameMenuItemSlider *);
+void UpdateVideoModeMenuFrameLimit(CGameMenuItemZCycle *pItem);
+void UpdateVideoModeMenuFPSOffset(CGameMenuItemSlider *pItem);
+void UpdateVideoColorMenu(CGameMenuItemSliderFloat *);
+void ResetVideoColor(CGameMenuItemChain *);
+#ifdef USE_OPENGL
+void SetupVideoPolymostMenu(CGameMenuItemChain *);
+#endif
+
+char strRestoreGameStrings[][16] = 
+{
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+    "<Empty>",
+};
+
+const char *zNetGameTypes[] =
+{
+    "Cooperative",
+    "Bloodbath",
+    "Teams",
+};
+
+const char *zMonsterStrings[] =
+{
+    "None",
+    "Bring 'em on",
+    "Respawn",
+};
+
+const char *zWeaponStrings[] =
+{
+    "Do not Respawn",
+    "Are Permanent",
+    "Respawn",
+    "Respawn with Markers",
+};
+
+const char *zItemStrings[] =
+{
+    "Do not Respawn",
+    "Respawn",
+    "Respawn with Markers",
+};
+
+const char *zRespawnStrings[] =
+{
+    "At Random Locations",
+    "Close to Weapons",
+    "Away from Enemies",
+};
+
+const char *zDiffStrings[] =
+{
+    "STILL KICKING",
+    "PINK ON THE INSIDE",
+    "LIGHTLY BROILED",
+    "WELL DONE",
+    "EXTRA CRISPY",
+};
+
+char zUserMapName[16];
+const char *zEpisodeNames[6];
+const char *zLevelNames[6][16];
+
+static char MenuGameFuncs[NUMGAMEFUNCTIONS][MAXGAMEFUNCLEN];
+static char const *MenuGameFuncNone = "  -None-";
+static char const *pzGamefuncsStrings[NUMGAMEFUNCTIONS + 1];
+static int nGamefuncsValues[NUMGAMEFUNCTIONS + 1];
+static int nGamefuncsNum;
+
+CGameMenu menuMain;
+CGameMenu menuMainWithSave;
+CGameMenu menuNetMain;
+CGameMenu menuNetStart;
+CGameMenu menuEpisode;
+CGameMenu menuDifficulty;
+CGameMenu menuOptionsOld;
+CGameMenu menuControls;
+CGameMenu menuMessages;
+CGameMenu menuKeys;
+CGameMenu menuSaveGame;
+CGameMenu menuLoadGame;
+CGameMenu menuLoading;
+CGameMenu menuSounds;
+CGameMenu menuQuit;
+CGameMenu menuRestart;
+CGameMenu menuCredits;
+CGameMenu menuOrder;
+CGameMenu menuPlayOnline;
+CGameMenu menuParentalLock;
+CGameMenu menuSorry;
+CGameMenu menuSorry2;
+CGameMenu menuNetwork;
+CGameMenu menuNetworkHost;
+CGameMenu menuNetworkJoin;
+
+CGameMenuItemQAV itemBloodQAV("", 3, 160, 100, "BDRIP", true);
+CGameMenuItemQAV itemCreditsQAV("", 3, 160, 100, "CREDITS", false, true);
+CGameMenuItemQAV itemHelp3QAV("", 3, 160, 100, "HELP3", false, false);
+CGameMenuItemQAV itemHelp3BQAV("", 3, 160, 100, "HELP3B", false, false);
+CGameMenuItemQAV itemHelp4QAV("", 3, 160, 100, "HELP4", false, true);
+CGameMenuItemQAV itemHelp5QAV("", 3, 160, 100, "HELP5", false, true);
+
+CGameMenuItemTitle itemMainTitle("BLOOD", 1, 160, 20, 2038);
+CGameMenuItemChain itemMain1("NEW GAME", 1, 0, 45, 320, 1, &menuEpisode, -1, NULL, 0);
+//CGameMenuItemChain itemMain2("PLAY ONLINE", 1, 0, 65, 320, 1, &menuPlayOnline, -1, NULL, 0);
+CGameMenuItemChain itemMain2("MULTIPLAYER", 1, 0, 65, 320, 1, &menuNetwork, -1, NULL, 0);
+CGameMenuItemChain itemMain3("OPTIONS", 1, 0, 85, 320, 1, &menuOptions, -1, NULL, 0);
+CGameMenuItemChain itemMain4("LOAD GAME", 1, 0, 105, 320, 1, &menuLoadGame, -1, NULL, 0);
+CGameMenuItemChain itemMain5("HELP", 1, 0, 125, 320, 1, &menuOrder, -1, NULL, 0);
+CGameMenuItemChain itemMain6("CREDITS", 1, 0, 145, 320, 1, &menuCredits, -1, NULL, 0);
+CGameMenuItemChain itemMain7("QUIT", 1, 0, 165, 320, 1, &menuQuit, -1, NULL, 0);
+
+CGameMenuItemTitle itemMainSaveTitle("BLOOD", 1, 160, 20, 2038);
+CGameMenuItemChain itemMainSave1("NEW GAME", 1, 0, 45, 320, 1, &menuEpisode, -1, NULL, 0);
+//CGameMenuItemChain itemMainSave2("PLAY ONLINE", 1, 0, 60, 320, 1, &menuPlayOnline, -1, NULL, 0);
+CGameMenuItemChain itemMainSave2("OPTIONS", 1, 0, 60, 320, 1, &menuOptions, -1, NULL, 0);
+CGameMenuItemChain itemMainSave3("SAVE GAME", 1, 0, 75, 320, 1, &menuSaveGame, -1, SaveGameProcess, 0);
+CGameMenuItemChain itemMainSave4("LOAD GAME", 1, 0, 90, 320, 1, &menuLoadGame, -1, NULL, 0);
+CGameMenuItemChain itemMainSave5("HELP", 1, 0, 105, 320, 1, &menuOrder, -1, NULL, 0);
+CGameMenuItemChain itemMainSave6("CREDITS", 1, 0, 120, 320, 1, &menuCredits, -1, NULL, 0);
+CGameMenuItemChain itemMainSave7("END GAME", 1, 0, 135, 320, 1, &menuRestart, -1, NULL, 0);
+CGameMenuItemChain itemMainSave8("QUIT", 1, 0, 150, 320, 1, &menuQuit, -1, NULL, 0);
+
+CGameMenuItemTitle itemEpisodesTitle("EPISODES", 1, 160, 20, 2038);
+CGameMenuItemChain7F2F0 itemEpisodes[6];
+
+CGameMenuItemTitle itemDifficultyTitle("DIFFICULTY", 1, 160, 20, 2038);
+CGameMenuItemChain itemDifficulty1("STILL KICKING", 1, 0, 60, 320, 1, NULL, -1, SetDifficultyAndStart, 0);
+CGameMenuItemChain itemDifficulty2("PINK ON THE INSIDE", 1, 0, 80, 320, 1, NULL, -1, SetDifficultyAndStart, 1);
+CGameMenuItemChain itemDifficulty3("LIGHTLY BROILED", 1, 0, 100, 320, 1, NULL, -1, SetDifficultyAndStart, 2);
+CGameMenuItemChain itemDifficulty4("WELL DONE", 1, 0, 120, 320, 1, NULL, -1, SetDifficultyAndStart, 3);
+CGameMenuItemChain itemDifficulty5("EXTRA CRISPY", 1, 0, 140, 320, 1, 0, -1, SetDifficultyAndStart, 4);
+
+CGameMenuItemTitle itemOptionsOldTitle("OPTIONS", 1, 160, 20, 2038);
+CGameMenuItemChain itemOption1("CONTROLS...", 3, 0, 40, 320, 1, &menuControls, -1, NULL, 0);
+CGameMenuItemSlider sliderDetail("DETAIL:", 3, 66, 50, 180, gDetail, 0, 4, 1, SetDetail, -1, -1);
+CGameMenuItemSlider sliderGamma("GAMMA:", 3, 66, 60, 180, gGamma, 0, 15, 2, SetGamma, -1, -1);
+CGameMenuItemSlider sliderMusic("MUSIC:", 3, 66, 70, 180, MusicVolume, 0, 256, 48, SetMusicVol, -1, -1);
+CGameMenuItemSlider sliderSound("SOUND:", 3, 66, 80, 180, FXVolume, 0, 256, 48, SetSoundVol, -1, -1);
+CGameMenuItemSlider sliderCDAudio("CD AUDIO:", 3, 66, 90, 180, CDVolume, 0, 256, 48, SetCDVol, -1, -1);
+CGameMenuItemZBool bool3DAudio("3D AUDIO:", 3, 66, 100, 180, gDoppler, SetDoppler, NULL, NULL);
+CGameMenuItemZBool boolCrosshair("CROSSHAIR:", 3, 66, 110, 180, gAimReticle, SetCrosshair, NULL, NULL);
+CGameMenuItemZBool boolShowWeapons("SHOW WEAPONS:", 3, 66, 120, 180, gShowWeapon, SetShowWeapons, NULL, NULL);
+CGameMenuItemZBool boolSlopeTilting("SLOPE TILTING:", 3, 66, 130, 180, gSlopeTilting, SetSlopeTilting, NULL, NULL);
+CGameMenuItemZBool boolViewBobbing("VIEW BOBBING:", 3, 66, 140, 180, gViewVBobbing, SetViewBobbing, NULL, NULL);
+CGameMenuItemZBool boolViewSwaying("VIEW SWAYING:", 3, 66, 150, 180, gViewHBobbing, SetViewSwaying, NULL, NULL);
+CGameMenuItem7EE34 itemOption2("VIDEO MODE...", 3, 0, 160, 320, 1);
+CGameMenuItemChain itemChainParentalLock("PARENTAL LOCK", 3, 0, 170, 320, 1, &menuParentalLock, -1, NULL, 0);
+
+CGameMenuItemTitle itemControlsTitle("CONTROLS", 1, 160, 20, 2038);
+CGameMenuItemSliderFloat sliderMouseSpeed("Mouse Sensitivity:", 1, 10, 70, 300, CONTROL_MouseSensitivity, 0.5f, 16.f, 0.5f, SetMouseSensitivity, -1,-1);
+CGameMenuItemZBool boolMouseFlipped("Invert Mouse Aim:", 1, 10, 90, 300, gMouseAimingFlipped, SetMouseAimFlipped, NULL, NULL);
+CGameMenuItemSlider sliderTurnSpeed("Key Turn Speed:", 1, 10, 110, 300, gTurnSpeed, 64, 128, 4, SetTurnSpeed, -1, -1);
+CGameMenuItemChain itemChainKeyList("Configure Keys...", 1, 0, 130, 320, 1, &menuKeys, -1, NULL, 0);
+CGameMenuItemChain itemChainKeyReset("Reset Keys (default)...", 1, 0, 150, 320, 1, &menuKeys, -1, ResetKeys, 0);
+CGameMenuItemChain itemChainKeyResetClassic("Reset Keys (classic)...", 1, 0, 170, 320, 1, &menuKeys, -1, ResetKeysClassic, 0);
+
+CGameMenuItemTitle itemMessagesTitle("MESSAGES", 1, 160, 20, 2038);
+CGameMenuItemZBool boolMessages("MESSAGES:", 3, 66, 70, 180, 0, SetMessages, NULL, NULL);
+CGameMenuItemSlider sliderMsgCount("MESSAGE COUNT:", 3, 66, 80, 180, gMessageCount, 1, 16, 1, NULL, -1, -1);
+CGameMenuItemSlider sliderMsgTime("MESSAGE TIME:", 3, 66, 90, 180, gMessageTime, 1, 8, 1, NULL, -1, -1);
+CGameMenuItemZBool boolMsgFont("LARGE FONT:", 3, 66, 100, 180, 0, 0, NULL, NULL);
+CGameMenuItemZBool boolMsgIncoming("INCOMING:", 3, 66, 110, 180, 0, 0, NULL, NULL);
+CGameMenuItemZBool boolMsgSelf("SELF PICKUP:", 3, 66, 120, 180, 0, 0, NULL, NULL);
+CGameMenuItemZBool boolMsgOther("OTHER PICKUP:", 3, 66, 130, 180, 0, 0, NULL, NULL);
+CGameMenuItemZBool boolMsgRespawn("RESPAWN:", 3, 66, 140, 180, 0, 0, NULL, NULL);
+
+CGameMenuItemTitle itemKeysTitle("KEY SETUP", 1, 160, 20, 2038);
+CGameMenuItemKeyList itemKeyList("", 3, 56, 40, 200, 16, NUMGAMEFUNCTIONS, 0);
+
+CGameMenuItemTitle itemSaveTitle("Save Game", 1, 160, 20, 2038);
+CGameMenuItemZEditBitmap itemSaveGame1(NULL, 3, 20, 60, 320, strRestoreGameStrings[0], 16, 1, SaveGame, 0);
+CGameMenuItemZEditBitmap itemSaveGame2(NULL, 3, 20, 70, 320, strRestoreGameStrings[1], 16, 1, SaveGame, 1);
+CGameMenuItemZEditBitmap itemSaveGame3(NULL, 3, 20, 80, 320, strRestoreGameStrings[2], 16, 1, SaveGame, 2);
+CGameMenuItemZEditBitmap itemSaveGame4(NULL, 3, 20, 90, 320, strRestoreGameStrings[3], 16, 1, SaveGame, 3);
+CGameMenuItemZEditBitmap itemSaveGame5(NULL, 3, 20, 100, 320, strRestoreGameStrings[4], 16, 1, SaveGame, 4);
+CGameMenuItemZEditBitmap itemSaveGame6(NULL, 3, 20, 110, 320, strRestoreGameStrings[5], 16, 1, SaveGame, 5);
+CGameMenuItemZEditBitmap itemSaveGame7(NULL, 3, 20, 120, 320, strRestoreGameStrings[6], 16, 1, SaveGame, 6);
+CGameMenuItemZEditBitmap itemSaveGame8(NULL, 3, 20, 130, 320, strRestoreGameStrings[7], 16, 1, SaveGame, 7);
+CGameMenuItemZEditBitmap itemSaveGame9(NULL, 3, 20, 140, 320, strRestoreGameStrings[8], 16, 1, SaveGame, 8);
+CGameMenuItemZEditBitmap itemSaveGame10(NULL, 3, 20, 150, 320, strRestoreGameStrings[9], 16, 1, SaveGame, 9);
+CGameMenuItemBitmapLS itemSaveGamePic(NULL, 3, 0, 0, 2050);
+
+CGameMenuItemTitle itemLoadTitle("Load Game", 1, 160, 20, 2038);
+CGameMenuItemZEditBitmap itemLoadGame1(NULL, 3, 20, 60, 320, strRestoreGameStrings[0], 16, 1, LoadGame, 0);
+CGameMenuItemZEditBitmap itemLoadGame2(NULL, 3, 20, 70, 320, strRestoreGameStrings[1], 16, 1, LoadGame, 1);
+CGameMenuItemZEditBitmap itemLoadGame3(NULL, 3, 20, 80, 320, strRestoreGameStrings[2], 16, 1, LoadGame, 2);
+CGameMenuItemZEditBitmap itemLoadGame4(NULL, 3, 20, 90, 320, strRestoreGameStrings[3], 16, 1, LoadGame, 3);
+CGameMenuItemZEditBitmap itemLoadGame5(NULL, 3, 20, 100, 320, strRestoreGameStrings[4], 16, 1, LoadGame, 4);
+CGameMenuItemZEditBitmap itemLoadGame6(NULL, 3, 20, 110, 320, strRestoreGameStrings[5], 16, 1, LoadGame, 5);
+CGameMenuItemZEditBitmap itemLoadGame7(NULL, 3, 20, 120, 320, strRestoreGameStrings[6], 16, 1, LoadGame, 6);
+CGameMenuItemZEditBitmap itemLoadGame8(NULL, 3, 20, 130, 320, strRestoreGameStrings[7], 16, 1, LoadGame, 7);
+CGameMenuItemZEditBitmap itemLoadGame9(NULL, 3, 20, 140, 320, strRestoreGameStrings[8], 16, 1, LoadGame, 8);
+CGameMenuItemZEditBitmap itemLoadGame10(NULL, 3, 20, 150, 320, strRestoreGameStrings[9], 16, 1, LoadGame, 9);
+CGameMenuItemBitmapLS itemLoadGamePic(NULL, 3, 0, 0, 2518);
+
+CGameMenuItemTitle itemNetStartTitle("MULTIPLAYER", 1, 160, 20, 2038);
+CGameMenuItemZCycle itemNetStart1("GAME", 1, 20, 35, 280, 0, 0, zNetGameTypes, 3, 0);
+CGameMenuItemZCycle itemNetStart2("EPISODE", 1, 20, 50, 280, 0, SetupNetLevels, NULL, 0, 0);
+CGameMenuItemZCycle itemNetStart3("LEVEL", 1, 20, 65, 280, 0, NULL, NULL, 0, 0);
+CGameMenuItemZCycle itemNetStart4("DIFFICULTY", 1, 20, 80, 280, 0, 0, zDiffStrings, 5, 0);
+CGameMenuItemZCycle itemNetStart5("MONSTERS", 1, 20, 95, 280, 0, 0, zMonsterStrings, 3, 0);
+CGameMenuItemZCycle itemNetStart6("WEAPONS", 1, 20, 110, 280, 0, 0, zWeaponStrings, 4, 0);
+CGameMenuItemZCycle itemNetStart7("ITEMS", 1, 20, 125, 280, 0, 0, zItemStrings, 3, 0);
+CGameMenuItemZEdit itemNetStart9("USER MAP:", 1, 20, 155, 280, zUserMapName, 13, 0, NULL, 0);
+CGameMenuItemChain itemNetStart10("START GAME", 1, 20, 170, 280, 0, 0, -1, StartNetGame, 0);
+
+CGameMenuItemText itemLoadingText("LOADING...", 1, 160, 100, 1);
+
+CGameMenuItemTitle itemSoundsTitle("SOUNDS", 1, 160, 20, 2038);
+CGameMenuItemSlider itemSoundsMusic("MUSIC:", 3, 40, 60, 180, MusicVolume, 0, 256, 48, SetMusicVol, -1, -1);
+CGameMenuItemSlider itemSoundsSound("SOUND:", 3, 40, 70, 180, FXVolume, 0, 256, 48, SetSoundVol, -1, -1);
+CGameMenuItemSlider itemSoundsCDAudio("CD AUDIO:", 3, 40, 80, 180, CDVolume, 0, 256, 48, SetCDVol, -1, -1);
+CGameMenuItemZBool itemSounds3DAudio("3D SOUND:", 3, 40, 90, 180, gDoppler, SetDoppler, NULL, NULL);
+
+CGameMenuItemTitle itemQuitTitle("QUIT", 1, 160, 20, 2038);
+CGameMenuItemText itemQuitText1("Do you really want to quit?", 0, 160, 100, 1);
+CGameMenuItemYesNoQuit itemQuitYesNo("[Y/N]", 0, 20, 110, 280, 1, 0);
+
+CGameMenuItemTitle itemRestartTitle("RESTART GAME", 1, 160, 20, 2038);
+CGameMenuItemText itemRestartText1("Do you really want to restart game?", 0, 160, 100, 1);
+CGameMenuItemYesNoQuit itemRestartYesNo("[Y/N]", 0, 20, 110, 280, 1, 1);
+
+CGameMenuItemPicCycle itemCreditsPicCycle(0, 0, NULL, NULL, 0, 0);
+CGameMenuItemPicCycle itemOrderPicCycle(0, 0, NULL, NULL, 0, 0);
+
+CGameMenuItemTitle itemParentalLockTitle("PARENTAL LOCK", 1, 160, 20, 2038);
+CGameMenuItemZBool itemParentalLockToggle("LOCK:", 3, 66, 70, 180, 0, SetParentalLock, NULL, NULL);
+CGameMenuItemPassword itemParentalLockPassword("SET PASSWORD:", 3, 160, 80);
+
+CGameMenuItemPicCycle itemSorryPicCycle(0, 0, NULL, NULL, 0, 0);
+CGameMenuItemText itemSorryText1("Loading and saving games", 0, 160, 90, 1);
+CGameMenuItemText itemSorryText2("not supported", 0, 160, 100, 1);
+CGameMenuItemText itemSorryText3("in this demo version of Blood.", 0, 160, 110, 1);
+
+CGameMenuItemText itemSorry2Text1("Buy the complete version of", 0, 160, 90, 1);
+CGameMenuItemText itemSorry2Text2("Blood for three new episodes", 0, 160, 100, 1);
+CGameMenuItemText itemSorry2Text3("plus eight BloodBath-only levels!", 0, 160, 110, 1);
+
+CGameMenuItemTitle unk_26E06C(" ONLINE ", 1, 160, 20, 2038);
+CGameMenuItem7EA1C unk_26E090("DWANGO", 1, 0, 45, 320, "matt", "DWANGO", 1, -1, NULL, 0);
+CGameMenuItem7EA1C unk_26E0E8("RTIME", 1, 0, 65, 320, "matt", "RTIME", 1, -1, NULL, 0);
+CGameMenuItem7EA1C unk_26E140("HEAT", 1, 0, 85, 320, "matt", "HEAT", 1, -1, NULL, 0);
+CGameMenuItem7EA1C unk_26E198("KALI", 1, 0, 105, 320, "matt", "KALI", 1, -1, NULL, 0);
+CGameMenuItem7EA1C unk_26E1F0("MPATH", 1, 0, 125, 320, "matt", "MPATH", 1, -1, NULL, 0);
+CGameMenuItem7EA1C unk_26E248("TEN", 1, 0, 145, 320, "matt", "TEN", 1, -1, TenProcess, 0);
+
+
+// static int32_t newresolution, newrendermode, newfullscreen, newvsync;
+
+enum resflags_t {
+    RES_FS = 0x1,
+    RES_WIN = 0x2,
+};
+
+#define MAXRESOLUTIONSTRINGLENGTH 19
+
+struct resolution_t {
+    int32_t xdim, ydim;
+    int32_t flags;
+    int32_t bppmax;
+    char name[MAXRESOLUTIONSTRINGLENGTH];
+};
+
+resolution_t gResolution[MAXVALIDMODES];
+int gResolutionNum;
+const char *gResolutionName[MAXVALIDMODES];
+
+CGameMenu menuOptions;
+CGameMenu menuOptionsGame;
+CGameMenu menuOptionsDisplay;
+CGameMenu menuOptionsDisplayColor;
+CGameMenu menuOptionsDisplayMode;
+#ifdef USE_OPENGL
+CGameMenu menuOptionsDisplayPolymost;
+#endif
+CGameMenu menuOptionsSound;
+CGameMenu menuOptionsPlayer;
+CGameMenu menuOptionsControl;
+
+void SetupOptionsSound(CGameMenuItemChain *pItem);
+
+CGameMenuItemTitle itemOptionsTitle("OPTIONS", 1, 160, 20, 2038);
+CGameMenuItemChain itemOptionsChainGame("GAME SETUP", 1, 0, 50, 320, 1, &menuOptionsGame, -1, NULL, 0);
+CGameMenuItemChain itemOptionsChainDisplay("DISPLAY SETUP", 1, 0, 70, 320, 1, &menuOptionsDisplay, -1, NULL, 0);
+CGameMenuItemChain itemOptionsChainSound("SOUND SETUP", 1, 0, 90, 320, 1, &menuOptionsSound, -1, SetupOptionsSound, 0);
+CGameMenuItemChain itemOptionsChainPlayer("PLAYER SETUP", 1, 0, 110, 320, 1, &menuOptionsPlayer, -1, NULL, 0);
+CGameMenuItemChain itemOptionsChainControl("CONTROL SETUP", 1, 0, 130, 320, 1, &menuOptionsControl, -1, NULL, 0);
+CGameMenuItemChain itemOptionsChainOld("OLD MENU", 1, 0, 170, 320, 1, &menuOptionsOld, -1, NULL, 0);
+
+const char *pzAutoAimStrings[] = {
+    "NEVER",
+    "ALWAYS",
+    "HITSCAN ONLY"
+};
+
+const char *pzWeaponSwitchStrings[] = {
+    "NEVER",
+    "IF NEW",
+    "BY RATING"
+};
+
+void SetAutoAim(CGameMenuItemZCycle *);
+void SetLevelStats(CGameMenuItemZBool *);
+void SetPowerupDuration(CGameMenuItemZBool *);
+void SetShowMapTitle(CGameMenuItemZBool*);
+void SetWeaponSwitch(CGameMenuItemZCycle *pItem);
+
+CGameMenuItemTitle itemOptionsGameTitle("GAME SETUP", 1, 160, 20, 2038);
+CGameMenuItemZBool itemOptionsGameBoolShowWeapons("SHOW WEAPONS:", 3, 66, 70, 180, gShowWeapon, SetShowWeapons, NULL, NULL);
+CGameMenuItemZBool itemOptionsGameBoolSlopeTilting("SLOPE TILTING:", 3, 66, 80, 180, gSlopeTilting, SetSlopeTilting, NULL, NULL);
+CGameMenuItemZBool itemOptionsGameBoolViewBobbing("VIEW BOBBING:", 3, 66, 90, 180, gViewVBobbing, SetViewBobbing, NULL, NULL);
+CGameMenuItemZBool itemOptionsGameBoolViewSwaying("VIEW SWAYING:", 3, 66, 100, 180, gViewHBobbing, SetViewSwaying, NULL, NULL);
+CGameMenuItemZCycle itemOptionsGameBoolAutoAim("AUTO AIM:", 3, 66, 110, 180, 0, SetAutoAim, pzAutoAimStrings, ARRAY_SSIZE(pzAutoAimStrings), 0);
+CGameMenuItemZCycle itemOptionsGameWeaponSwitch("EQUIP PICKUPS:", 3, 66, 120, 180, 0, SetWeaponSwitch, pzWeaponSwitchStrings, ARRAY_SSIZE(pzWeaponSwitchStrings), 0);
+CGameMenuItemChain itemOptionsGameChainParentalLock("PARENTAL LOCK", 3, 0, 120, 320, 1, &menuParentalLock, -1, NULL, 0);
+
+CGameMenuItemTitle itemOptionsDisplayTitle("DISPLAY SETUP", 1, 160, 20, 2038);
+CGameMenuItemChain itemOptionsDisplayColor("COLOR CORRECTION", 3, 66, 60, 180, 0, &menuOptionsDisplayColor, -1, NULL, 0);
+CGameMenuItemChain itemOptionsDisplayMode("VIDEO MODE", 3, 66, 70, 180, 0, &menuOptionsDisplayMode, -1, SetupVideoModeMenu, 0);
+CGameMenuItemZBool itemOptionsDisplayBoolCrosshair("CROSSHAIR:", 3, 66, 80, 180, gAimReticle, SetCrosshair, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayBoolCenterHoriz("CENTER HORIZON LINE:", 3, 66, 90, 180, gCenterHoriz, SetCenterHoriz, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayBoolLevelStats("LEVEL STATS:", 3, 66, 100, 180, gLevelStats, SetLevelStats, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayBoolPowerupDuration("POWERUP DURATION:", 3, 66, 110, 180, gPowerupDuration, SetPowerupDuration, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayBoolShowMapTitle("MAP TITLE:", 3, 66, 120, 180, gShowMapTitle, SetShowMapTitle, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayBoolMessages("MESSAGES:", 3, 66, 130, 180, gMessageState, SetMessages, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayBoolWidescreen("WIDESCREEN:", 3, 66, 140, 180, r_usenewaspect, SetWidescreen, NULL, NULL);
+CGameMenuItemSlider itemOptionsDisplayFOV("FOV:", 3, 66, 150, 180, &gFov, 75, 140, 5, SetFOV, -1, -1, kMenuSliderValue);
+#ifdef USE_OPENGL
+CGameMenuItemChain itemOptionsDisplayPolymost("POLYMOST SETUP", 3, 66, 160, 180, 0, &menuOptionsDisplayPolymost, -1, SetupVideoPolymostMenu, 0);
+#endif
+
+const char *pzRendererStrings[] = {
+    "CLASSIC",
+    "POLYMOST"
+};
+
+const int nRendererValues[] = {
+    REND_CLASSIC,
+    REND_POLYMOST
+};
+
+const char *pzVSyncStrings[] = {
+    "ADAPTIVE",
+    "OFF",
+    "ON"
+};
+
+const int nVSyncValues[] = {
+    -1,
+    0,
+    1
+};
+
+const char *pzFrameLimitStrings[] = {
+    "30 FPS",
+    "60 FPS",
+    "75 FPS",
+    "100 FPS",
+    "120 FPS",
+    "144 FPS",
+    "165 FPS",
+    "240 FPS"
+};
+
+const int nFrameLimitValues[] = {
+    30,
+    60,
+    75,
+    100,
+    120,
+    144,
+    165,
+    240
+};
+
+
+void PreDrawVideoModeMenu(CGameMenuItem *);
+
+CGameMenuItemTitle itemOptionsDisplayModeTitle("VIDEO MODE", 1, 160, 20, 2038);
+CGameMenuItemZCycle itemOptionsDisplayModeResolution("RESOLUTION:", 3, 66, 60, 180, 0, NULL, NULL, 0, 0, true);
+CGameMenuItemZCycle itemOptionsDisplayModeRenderer("RENDERER:", 3, 66, 70, 180, 0, NULL, pzRendererStrings, 2, 0);
+CGameMenuItemZBool itemOptionsDisplayModeFullscreen("FULLSCREEN:", 3, 66, 80, 180, 0, NULL, NULL, NULL);
+CGameMenuItemZCycle itemOptionsDisplayModeVSync("VSYNC:", 3, 66, 90, 180, 0, NULL, pzVSyncStrings, 3, 0);
+CGameMenuItemZCycle itemOptionsDisplayModeFrameLimit("FRAMERATE LIMIT:", 3, 66, 100, 180, 0, UpdateVideoModeMenuFrameLimit, pzFrameLimitStrings, 8, 0);
+CGameMenuItemSlider itemOptionsDisplayModeFPSOffset("FPS OFFSET:", 3, 66, 110, 180, 0, -10, 10, 1, UpdateVideoModeMenuFPSOffset, -1, -1, kMenuSliderValue);
+CGameMenuItemChain itemOptionsDisplayModeApply("APPLY CHANGES", 3, 66, 125, 180, 0, NULL, 0, SetVideoMode, 0);
+
+void PreDrawDisplayColor(CGameMenuItem *);
+
+CGameMenuItemTitle itemOptionsDisplayColorTitle("COLOR CORRECTION", 1, 160, 20, -1);
+CGameMenuItemSliderFloat itemOptionsDisplayColorGamma("GAMMA:", 3, 66, 140, 180, &g_videoGamma, 0.3f, 4.f, 0.1f, UpdateVideoColorMenu, -1, -1, kMenuSliderValue);
+CGameMenuItemSliderFloat itemOptionsDisplayColorContrast("CONTRAST:", 3, 66, 150, 180, &g_videoContrast, 0.1f, 2.7f, 0.05f, UpdateVideoColorMenu, -1, -1, kMenuSliderValue);
+CGameMenuItemSliderFloat itemOptionsDisplayColorBrightness("BRIGHTNESS:", 3, 66, 160, 180, &g_videoBrightness, -0.8f, 0.8f, 0.05f, UpdateVideoColorMenu, -1, -1, kMenuSliderValue);
+CGameMenuItemSliderFloat itemOptionsDisplayColorVisibility("VISIBILITY:", 3, 66, 170, 180, &r_ambientlight, 0.125f, 4.f, 0.125f, UpdateVideoColorMenu, -1, -1, kMenuSliderValue);
+CGameMenuItemChain itemOptionsDisplayColorReset("RESET TO DEFAULTS", 3, 66, 180, 180, 0, NULL, 0, ResetVideoColor, 0);
+
+const char *pzTextureModeStrings[] = {
+    "CLASSIC",
+    "FILTERED"
+};
+
+#ifdef USE_OPENGL
+int nTextureModeValues[] = {
+    TEXFILTER_OFF,
+    TEXFILTER_ON
+};
+#endif
+
+const char *pzAnisotropyStrings[] = {
+    "MAX",
+    "NONE",
+    "2X",
+    "4X",
+    "8X",
+    "16X"
+};
+
+int nAnisotropyValues[] = {
+    0,
+    1,
+    2,
+    4,
+    8,
+    16
+};
+
+const char *pzTexQualityStrings[] = {
+    "FULL",
+    "HALF",
+    "BARF"
+};
+
+
+void UpdateTextureMode(CGameMenuItemZCycle *pItem);
+void UpdateAnisotropy(CGameMenuItemZCycle *pItem);
+void UpdateTrueColorTextures(CGameMenuItemZBool *pItem);
+void UpdateTexQuality(CGameMenuItemZCycle *pItem);
+void UpdatePreloadCache(CGameMenuItemZBool *pItem);
+void UpdateDetailTex(CGameMenuItemZBool *pItem);
+void UpdateGlowTex(CGameMenuItemZBool *pItem);
+void Update3DModels(CGameMenuItemZBool *pItem);
+void UpdateDeliriumBlur(CGameMenuItemZBool *pItem);
+#ifdef USE_OPENGL
+void PreDrawDisplayPolymost(CGameMenuItem *pItem);
+CGameMenuItemTitle itemOptionsDisplayPolymostTitle("POLYMOST SETUP", 1, 160, 20, 2038);
+CGameMenuItemZCycle itemOptionsDisplayPolymostTextureMode("TEXTURE MODE:", 3, 66, 60, 180, 0, UpdateTextureMode, pzTextureModeStrings, 2, 0);
+CGameMenuItemZCycle itemOptionsDisplayPolymostAnisotropy("ANISOTROPY:", 3, 66, 70, 180, 0, UpdateAnisotropy, pzAnisotropyStrings, 6, 0);
+CGameMenuItemZBool itemOptionsDisplayPolymostTrueColorTextures("TRUE COLOR TEXTURES:", 3, 66, 80, 180, 0, UpdateTrueColorTextures, NULL, NULL);
+CGameMenuItemZCycle itemOptionsDisplayPolymostTexQuality("GL TEXTURE QUALITY:", 3, 66, 90, 180, 0, UpdateTexQuality, pzTexQualityStrings, 3, 0);
+CGameMenuItemZBool itemOptionsDisplayPolymostPreloadCache("PRE-LOAD MAP TEXTURES:", 3, 66, 100, 180, 0, UpdatePreloadCache, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayPolymostDetailTex("DETAIL TEXTURES:", 3, 66, 120, 180, 0, UpdateDetailTex, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayPolymostGlowTex("GLOW TEXTURES:", 3, 66, 130, 180, 0, UpdateGlowTex, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayPolymost3DModels("3D MODELS:", 3, 66, 140, 180, 0, Update3DModels, NULL, NULL);
+CGameMenuItemZBool itemOptionsDisplayPolymostDeliriumBlur("DELIRIUM EFFECT BLUR:", 3, 66, 150, 180, 0, UpdateDeliriumBlur, NULL, NULL);
+#endif
+
+void UpdateSoundToggle(CGameMenuItemZBool *pItem);
+void UpdateMusicToggle(CGameMenuItemZBool *pItem);
+void Update3DToggle(CGameMenuItemZBool *pItem);
+void UpdateCDToggle(CGameMenuItemZBool *pItem);
+void UpdateSoundVolume(CGameMenuItemSlider *pItem);
+void UpdateMusicVolume(CGameMenuItemSlider *pItem);
+void UpdateSoundRate(CGameMenuItemZCycle *pItem);
+void UpdateNumVoices(CGameMenuItemSlider *pItem);
+void UpdateMusicDevice(CGameMenuItemZCycle *pItem);
+void SetSound(CGameMenuItemChain *pItem);
+void PreDrawSound(CGameMenuItem *pItem);
+const char *pzSoundRateStrings[] = {
+    "22050HZ",
+    "44100HZ",
+    "48000HZ"
+};
+
+int nSoundRateValues[] = {
+    22050,
+    44100,
+    48000
+};
+
+const char *pzMusicDeviceStrings[] = {
+    "SYSTEM MIDI",
+    "OPL3(SB/ADLIB)"
+};
+
+CGameMenuItemTitle itemOptionsSoundTitle("SOUND SETUP", 1, 160, 20, 2038);
+CGameMenuItemZBool itemOptionsSoundSoundToggle("SOUND:", 3, 66, 60, 180, false, UpdateSoundToggle, NULL, NULL);
+CGameMenuItemZBool itemOptionsSoundMusicToggle("MUSIC:", 3, 66, 70, 180, false, UpdateMusicToggle, NULL, NULL);
+CGameMenuItemZBool itemOptionsSound3DToggle("3D AUDIO:", 3, 66, 80, 180, false, Update3DToggle, NULL, NULL);
+CGameMenuItemSlider itemOptionsSoundSoundVolume("SOUND VOLUME:", 3, 66, 90, 180, &FXVolume, 0, 256, 48, UpdateSoundVolume, -1, -1, kMenuSliderPercent);
+CGameMenuItemSlider itemOptionsSoundMusicVolume("MUSIC VOLUME:", 3, 66, 100, 180, &MusicVolume, 0, 256, 48, UpdateMusicVolume, -1, -1, kMenuSliderPercent);
+CGameMenuItemZCycle itemOptionsSoundSampleRate("SAMPLE RATE:", 3, 66, 110, 180, 0, UpdateSoundRate, pzSoundRateStrings, 3, 0);
+CGameMenuItemSlider itemOptionsSoundNumVoices("VOICES:", 3, 66, 120, 180, NumVoices, 16, 256, 16, UpdateNumVoices, -1, -1, kMenuSliderValue);
+CGameMenuItemZBool itemOptionsSoundCDToggle("REDBOOK AUDIO:", 3, 66, 130, 180, false, UpdateCDToggle, NULL, NULL);
+CGameMenuItemZCycle itemOptionsSoundMusicDevice("MUSIC DEVICE:", 3, 66, 140, 180, 0, UpdateMusicDevice, pzMusicDeviceStrings, 2, 0);
+CGameMenuItemChain itemOptionsSoundApplyChanges("APPLY CHANGES", 3, 66, 150, 180, 0, NULL, 0, SetSound, 0);
+
+
+void UpdatePlayerName(CGameMenuItemZEdit *pItem, CGameMenuEvent *pEvent);
+
+CGameMenuItemTitle itemOptionsPlayerTitle("PLAYER SETUP", 1, 160, 20, 2038);
+CGameMenuItemZEdit itemOptionsPlayerName("PLAYER NAME:", 3, 66, 60, 180, szPlayerName, MAXPLAYERNAME, 0, UpdatePlayerName, 0);
+
+CGameMenu menuOptionsControlKeyboard;
+CGameMenu menuOptionsControlMouse;
+CGameMenu menuOptionsControlMouseButtonAssignment;
+
+void SetupMouseMenu(CGameMenuItemChain *pItem);
+
+CGameMenuItemTitle itemOptionsControlTitle("CONTROL SETUP", 1, 160, 20, 2038);
+CGameMenuItemChain itemOptionsControlKeyboard("KEYBOARD SETUP", 1, 0, 60, 320, 1, &menuOptionsControlKeyboard, -1, NULL, 0);
+CGameMenuItemChain itemOptionsControlMouse("MOUSE SETUP", 1, 0, 80, 320, 1, &menuOptionsControlMouse, -1, SetupMouseMenu, 0);
+
+CGameMenuItemTitle itemOptionsControlKeyboardTitle("KEYBOARD SETUP", 1, 160, 20, 2038);
+CGameMenuItemChain itemOptionsControlKeyboardList("Configure Keys...", 1, 0, 60, 320, 1, &menuKeys, -1, NULL, 0);
+CGameMenuItemChain itemOptionsControlKeyboardReset("Reset Keys (default)...", 1, 0, 80, 320, 1, &menuKeys, -1, ResetKeys, 0);
+CGameMenuItemChain itemOptionsControlKeyboardResetClassic("Reset Keys (classic)...", 1, 0, 100, 320, 1, &menuKeys, -1, ResetKeysClassic, 0);
+
+void SetMouseFilterInput(CGameMenuItemZBool *pItem);
+void SetMouseAimMode(CGameMenuItemZBool *pItem);
+void SetMouseVerticalAim(CGameMenuItemZBool *pItem);
+void SetMouseXScale(CGameMenuItemSlider *pItem);
+void SetMouseYScale(CGameMenuItemSlider *pItem);
+void SetMouseDigitalAxis(CGameMenuItemZCycle *pItem);
+
+void PreDrawControlMouse(CGameMenuItem *pItem);
+
+void SetupMouseButtonMenu(CGameMenuItemChain *pItem);
+
+CGameMenuItemTitle itemOptionsControlMouseTitle("MOUSE SETUP", 1, 160, 20, 2038);
+CGameMenuItemChain itemOptionsControlMouseButton("BUTTON ASSIGNMENT", 3, 66, 60, 180, 0, &menuOptionsControlMouseButtonAssignment, 0, SetupMouseButtonMenu, 0);
+CGameMenuItemSliderFloat itemOptionsControlMouseSensitivity("SENSITIVITY:", 3, 66, 70, 180, &CONTROL_MouseSensitivity, 0.5f, 16.f, 0.5f, SetMouseSensitivity, -1, -1, kMenuSliderValue);
+CGameMenuItemZBool itemOptionsControlMouseAimFlipped("INVERT AIMING:", 3, 66, 80, 180, false, SetMouseAimFlipped, NULL, NULL);
+CGameMenuItemZBool itemOptionsControlMouseFilterInput("FILTER INPUT:", 3, 66, 90, 180, false, SetMouseFilterInput, NULL, NULL);
+CGameMenuItemZBool itemOptionsControlMouseAimMode("AIMING TYPE:", 3, 66, 100, 180, false, SetMouseAimMode, "HOLD", "TOGGLE");
+CGameMenuItemZBool itemOptionsControlMouseVerticalAim("VERTICAL AIMING:", 3, 66, 110, 180, false, SetMouseVerticalAim, NULL, NULL);
+CGameMenuItemSlider itemOptionsControlMouseXScale("X-SCALE:", 3, 66, 120, 180, (int*)&MouseAnalogueScale[0], 0, 65536, 1024, SetMouseXScale, -1, -1, kMenuSliderQ16);
+CGameMenuItemSlider itemOptionsControlMouseYScale("Y-SCALE:", 3, 66, 130, 180, (int*)&MouseAnalogueScale[1], 0, 65536, 1024, SetMouseYScale, -1, -1, kMenuSliderQ16);
+CGameMenuItemZCycle itemOptionsControlMouseDigitalUp("DIGITAL UP", 3, 66, 140, 180, 0, SetMouseDigitalAxis, NULL, 0, 0, true);
+CGameMenuItemZCycle itemOptionsControlMouseDigitalDown("DIGITAL DOWN", 3, 66, 150, 180, 0, SetMouseDigitalAxis, NULL, 0, 0, true);
+CGameMenuItemZCycle itemOptionsControlMouseDigitalLeft("DIGITAL LEFT", 3, 66, 160, 180, 0, SetMouseDigitalAxis, NULL, 0, 0, true);
+CGameMenuItemZCycle itemOptionsControlMouseDigitalRight("DIGITAL RIGHT", 3, 66, 170, 180, 0, SetMouseDigitalAxis, NULL, 0, 0, true);
+
+void SetupNetworkMenu(void);
+void SetupNetworkHostMenu(CGameMenuItemChain *pItem);
+void SetupNetworkJoinMenu(CGameMenuItemChain *pItem);
+void NetworkHostGame(CGameMenuItemChain *pItem);
+void NetworkJoinGame(CGameMenuItemChain *pItem);
+
+char zNetAddressBuffer[16] = "localhost";
+char zNetPortBuffer[6];
+
+CGameMenuItemTitle itemNetworkTitle("MULTIPLAYER", 1, 160, 20, 2038);
+CGameMenuItemChain itemNetworkHost("HOST A GAME", 1, 0, 80, 320, 1, &menuNetworkHost, -1, SetupNetworkHostMenu, 0);
+CGameMenuItemChain itemNetworkJoin("JOIN A GAME", 1, 0, 100, 320, 1, &menuNetworkJoin, -1, SetupNetworkJoinMenu, 0);
+
+CGameMenuItemTitle itemNetworkHostTitle("HOST A GAME", 1, 160, 20, 2038);
+CGameMenuItemSlider itemNetworkHostPlayerNum("PLAYER NUMBER:", 3, 66, 70, 180, 1, 2, kMaxPlayers, 1, NULL, -1, -1, kMenuSliderValue);
+CGameMenuItemZEdit itemNetworkHostPort("NETWORK PORT:", 3, 66, 80, 180, zNetPortBuffer, 6, 0, NULL, 0);
+CGameMenuItemChain itemNetworkHostHost("HOST A GAME", 3, 66, 100, 180, 1, NULL, -1, NetworkHostGame, 0);
+
+CGameMenuItemTitle itemNetworkJoinTitle("JOIN A GAME", 1, 160, 20, 2038);
+CGameMenuItemZEdit itemNetworkJoinAddress("NETWORK ADDRESS:", 3, 66, 70, 180, zNetAddressBuffer, 16, 0, NULL, 0);
+CGameMenuItemZEdit itemNetworkJoinPort("NETWORK PORT:", 3, 66, 80, 180, zNetPortBuffer, 6, 0, NULL, 0);
+CGameMenuItemChain itemNetworkJoinJoin("JOIN A GAME", 3, 66, 100, 180, 1, NULL, -1, NetworkJoinGame, 0);
+
+// There is no better way to do this than manually.
+
+#define MENUMOUSEFUNCTIONS 12
+
+static char const *MenuMouseNames[MENUMOUSEFUNCTIONS] = {
+    "Button 1",
+    "Double Button 1",
+    "Button 2",
+    "Double Button 2",
+    "Button 3",
+    "Double Button 3",
+
+    "Wheel Up",
+    "Wheel Down",
+
+    "Button 4",
+    "Double Button 4",
+    "Button 5",
+    "Double Button 5",
+};
+
+static int32_t MenuMouseDataIndex[MENUMOUSEFUNCTIONS][2] = {
+    { 0, 0, },
+    { 0, 1, },
+    { 1, 0, },
+    { 1, 1, },
+    { 2, 0, },
+    { 2, 1, },
+
+    // note the mouse wheel
+    { 4, 0, },
+    { 5, 0, },
+
+    { 3, 0, },
+    { 3, 1, },
+    { 6, 0, },
+    { 6, 1, },
+};
+
+void SetMouseButton(CGameMenuItemZCycle *pItem);
+
+CGameMenuItemZCycle *pItemOptionsControlMouseButton[MENUMOUSEFUNCTIONS];
+
+void SetupLoadingScreen(void)
+{
+    menuLoading.Add(&itemLoadingText, true);
+}
+
+void SetupKeyListMenu(void)
+{
+    menuKeys.Add(&itemKeysTitle, false);
+    menuKeys.Add(&itemKeyList, true);
+    menuKeys.Add(&itemBloodQAV, false);
+}
+
+void SetupMessagesMenu(void)
+{
+    menuMessages.Add(&itemMessagesTitle, false);
+    menuMessages.Add(&boolMessages, true);
+    menuMessages.Add(&sliderMsgCount, false);
+    menuMessages.Add(&sliderMsgTime, false);
+    menuMessages.Add(&boolMsgFont, false);
+    menuMessages.Add(&boolMsgIncoming, false);
+    menuMessages.Add(&boolMsgSelf, false);
+    menuMessages.Add(&boolMsgOther, false);
+    menuMessages.Add(&boolMsgRespawn, false);
+    menuMessages.Add(&itemBloodQAV, false);
+}
+
+void SetupControlsMenu(void)
+{
+    sliderMouseSpeed.fValue = ClipRangeF(CONTROL_MouseSensitivity, sliderMouseSpeed.fRangeLow, sliderMouseSpeed.fRangeHigh);
+    sliderTurnSpeed.nValue = ClipRange(gTurnSpeed, sliderTurnSpeed.nRangeLow, sliderTurnSpeed.nRangeHigh);
+    boolMouseFlipped.at20 = gMouseAimingFlipped;
+    menuControls.Add(&itemControlsTitle, false);
+    menuControls.Add(&sliderMouseSpeed, true);
+    menuControls.Add(&boolMouseFlipped, false);
+    menuControls.Add(&sliderTurnSpeed, false);
+    menuControls.Add(&itemChainKeyList, false);
+    menuControls.Add(&itemChainKeyReset, false);
+    menuControls.Add(&itemChainKeyResetClassic, false);
+    menuControls.Add(&itemBloodQAV, false);
+}
+
+void SetupOptionsOldMenu(void)
+{
+    sliderDetail.nValue = ClipRange(gDetail, sliderDetail.nRangeLow, sliderDetail.nRangeHigh);
+    sliderGamma.nValue = ClipRange(gGamma, sliderGamma.nRangeLow, sliderGamma.nRangeHigh);
+    sliderMusic.nValue = ClipRange(MusicVolume, sliderMusic.nRangeLow, sliderMusic.nRangeHigh);
+    sliderSound.nValue = ClipRange(FXVolume, sliderSound.nRangeLow, sliderSound.nRangeHigh);
+    bool3DAudio.at20 = gDoppler;
+    boolCrosshair.at20 = gAimReticle;
+    boolShowWeapons.at20 = gShowWeapon;
+    boolSlopeTilting.at20 = gSlopeTilting;
+    boolViewBobbing.at20 = gViewVBobbing;
+    boolViewSwaying.at20 = gViewHBobbing;
+    boolMessages.at20 = gGameMessageMgr.at0;
+    menuOptionsOld.Add(&itemOptionsTitle, false);
+    menuOptionsOld.Add(&itemOption1, true);
+    menuOptionsOld.Add(&sliderDetail, false);
+    menuOptionsOld.Add(&sliderGamma, false);
+    menuOptionsOld.Add(&sliderMusic, false);
+    menuOptionsOld.Add(&sliderSound, false);
+    menuOptionsOld.Add(&sliderCDAudio, false);
+    menuOptionsOld.Add(&bool3DAudio, false);
+    menuOptionsOld.Add(&boolCrosshair, false);
+    menuOptionsOld.Add(&boolShowWeapons, false);
+    menuOptionsOld.Add(&boolSlopeTilting, false);
+    menuOptionsOld.Add(&boolViewBobbing, false);
+    menuOptionsOld.Add(&boolViewSwaying, false);
+    menuOptionsOld.Add(&itemOption2, false);
+    menuOptionsOld.Add(&itemChainParentalLock, false);
+    menuOptionsOld.Add(&itemBloodQAV, false);
+}
+
+void SetupDifficultyMenu(void)
+{
+    menuDifficulty.Add(&itemDifficultyTitle, false);
+    menuDifficulty.Add(&itemDifficulty1, false);
+    menuDifficulty.Add(&itemDifficulty2, false);
+    menuDifficulty.Add(&itemDifficulty3, true);
+    menuDifficulty.Add(&itemDifficulty4, false);
+    menuDifficulty.Add(&itemDifficulty5, false);
+    menuDifficulty.Add(&itemBloodQAV, false);
+}
+
+void SetupEpisodeMenu(void)
+{
+    menuEpisode.Add(&itemEpisodesTitle, false);
+    bool unk = false;
+    int height;
+    gMenuTextMgr.GetFontInfo(1, NULL, NULL, &height);
+    int j = 0;
+    for (int i = 0; i < 6; i++)
+    {
+        EPISODEINFO *pEpisode = &gEpisodeInfo[i];
+        if (!pEpisode->bloodbath || gGameOptions.nGameType != 0)
+        {
+            if (j < gEpisodeCount)
+            {
+                CGameMenuItemChain7F2F0 *pEpisodeItem = &itemEpisodes[j];
+                pEpisodeItem->m_nFont = 1;
+                pEpisodeItem->m_nX = 0;
+                pEpisodeItem->m_nWidth = 320;
+                pEpisodeItem->at20 = 1;
+                pEpisodeItem->m_pzText = pEpisode->at0;
+                pEpisodeItem->m_nY = 55+(height+8)*j;
+                pEpisodeItem->at34 = i;
+                if (!unk || j == 0)
+                {
+                    pEpisodeItem = &itemEpisodes[j];
+                    pEpisodeItem->at24 = &menuDifficulty;
+                    pEpisodeItem->at28 = 3;
+                }
+                else
+                {
+                    pEpisodeItem->at24 = &menuSorry2;
+                    pEpisodeItem->at28 = 1;
+                }
+                pEpisodeItem = &itemEpisodes[j];
+                pEpisodeItem->bCanSelect = 1;
+                pEpisodeItem->bEnable = 1;
+                bool first = j == 0;
+                menuEpisode.Add(&itemEpisodes[j], first);
+                if (first)
+                    SetupLevelMenuItem(j);
+            }
+            j++;
+        }
+    }
+    menuEpisode.Add(&itemBloodQAV, false);
+}
+
+void SetupMainMenu(void)
+{
+    menuMain.Add(&itemMainTitle, false);
+    menuMain.Add(&itemMain1, true);
+    if (gGameOptions.nGameType > 0)
+    {
+        itemMain1.at24 = &menuNetStart;
+        itemMain1.at28 = 2;
+    }
+    else
+    {
+        itemMain1.at24 = &menuEpisode;
+        itemMain1.at28 = -1;
+    }
+    menuMain.Add(&itemMain2, false);
+    menuMain.Add(&itemMain3, false);
+    menuMain.Add(&itemMain4, false);
+    menuMain.Add(&itemMain5, false);
+    menuMain.Add(&itemMain6, false);
+    menuMain.Add(&itemMain7, false);
+    menuMain.Add(&itemBloodQAV, false);
+}
+
+void SetupMainMenuWithSave(void)
+{
+    menuMainWithSave.Add(&itemMainSaveTitle, false);
+    menuMainWithSave.Add(&itemMainSave1, true);
+    if (gGameOptions.nGameType > 0)
+    {
+        itemMainSave1.at24 = &menuNetStart;
+        itemMainSave1.at28 = 2;
+    }
+    else
+    {
+        itemMainSave1.at24 = &menuEpisode;
+        itemMainSave1.at28 = -1;
+    }
+    menuMainWithSave.Add(&itemMainSave2, false);
+    menuMainWithSave.Add(&itemMainSave3, false);
+    menuMainWithSave.Add(&itemMainSave4, false);
+    menuMainWithSave.Add(&itemMainSave5, false);
+    menuMainWithSave.Add(&itemMainSave6, false);
+    menuMainWithSave.Add(&itemMainSave7, false);
+    menuMainWithSave.Add(&itemMainSave8, false);
+    menuMainWithSave.Add(&itemBloodQAV, false);
+}
+
+void SetupNetStartMenu(void)
+{
+    bool oneEpisode = false;
+    menuNetStart.Add(&itemNetStartTitle, false);
+    menuNetStart.Add(&itemNetStart1, false);
+    for (int i = 0; i < (oneEpisode ? 1 : 6); i++)
+    {
+        EPISODEINFO *pEpisode = &gEpisodeInfo[i];
+        if (i < gEpisodeCount)
+            itemNetStart2.Add(pEpisode->at0, i == 0);
+    }
+    menuNetStart.Add(&itemNetStart2, false);
+    menuNetStart.Add(&itemNetStart3, false);
+    menuNetStart.Add(&itemNetStart4, false);
+    menuNetStart.Add(&itemNetStart5, false);
+    menuNetStart.Add(&itemNetStart6, false);
+    menuNetStart.Add(&itemNetStart7, false);
+    menuNetStart.Add(&itemNetStart9, false);
+    menuNetStart.Add(&itemNetStart10, false);
+    itemNetStart1.SetTextIndex(1);
+    itemNetStart4.SetTextIndex(2);
+    itemNetStart5.SetTextIndex(0);
+    itemNetStart6.SetTextIndex(1);
+    itemNetStart7.SetTextIndex(1);
+    menuNetStart.Add(&itemBloodQAV, false);
+}
+
+void SetupSaveGameMenu(void)
+{
+    menuSaveGame.Add(&itemSaveTitle, false);
+    menuSaveGame.Add(&itemSaveGame1, true);
+    menuSaveGame.Add(&itemSaveGame2, false);
+    menuSaveGame.Add(&itemSaveGame3, false);
+    menuSaveGame.Add(&itemSaveGame4, false);
+    menuSaveGame.Add(&itemSaveGame5, false);
+    menuSaveGame.Add(&itemSaveGame6, false);
+    menuSaveGame.Add(&itemSaveGame7, false);
+    menuSaveGame.Add(&itemSaveGame8, false);
+    menuSaveGame.Add(&itemSaveGame9, false);
+    menuSaveGame.Add(&itemSaveGame10, false);
+    menuSaveGame.Add(&itemSaveGamePic, false);
+    menuSaveGame.Add(&itemBloodQAV, false);
+
+    itemSaveGame1.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[0], "<Empty>"))
+        itemSaveGame1.at37 = 1;
+
+    itemSaveGame2.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[1], "<Empty>"))
+        itemSaveGame2.at37 = 1;
+
+    itemSaveGame3.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[2], "<Empty>"))
+        itemSaveGame3.at37 = 1;
+
+    itemSaveGame4.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[3], "<Empty>"))
+        itemSaveGame4.at37 = 1;
+
+    itemSaveGame5.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[4], "<Empty>"))
+        itemSaveGame5.at37 = 1;
+
+    itemSaveGame6.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[5], "<Empty>"))
+        itemSaveGame6.at37 = 1;
+
+    itemSaveGame7.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[6], "<Empty>"))
+        itemSaveGame7.at37 = 1;
+
+    itemSaveGame8.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[7], "<Empty>"))
+        itemSaveGame8.at37 = 1;
+
+    itemSaveGame9.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[8], "<Empty>"))
+        itemSaveGame9.at37 = 1;
+
+    itemSaveGame10.at2c = &itemSaveGamePic;
+    if (!strcmp(strRestoreGameStrings[9], "<Empty>"))
+        itemSaveGame10.at37 = 1;
+}
+
+void SetupLoadGameMenu(void)
+{
+    menuLoadGame.Add(&itemLoadTitle, false);
+    menuLoadGame.Add(&itemLoadGame1, true);
+    menuLoadGame.Add(&itemLoadGame2, false);
+    menuLoadGame.Add(&itemLoadGame3, false);
+    menuLoadGame.Add(&itemLoadGame4, false);
+    menuLoadGame.Add(&itemLoadGame5, false);
+    menuLoadGame.Add(&itemLoadGame6, false);
+    menuLoadGame.Add(&itemLoadGame7, false);
+    menuLoadGame.Add(&itemLoadGame8, false);
+    menuLoadGame.Add(&itemLoadGame9, false);
+    menuLoadGame.Add(&itemLoadGame10, false);
+    menuLoadGame.Add(&itemLoadGamePic, false);
+    itemLoadGame1.at35 = 0;
+    itemLoadGame2.at35 = 0;
+    itemLoadGame3.at35 = 0;
+    itemLoadGame4.at35 = 0;
+    itemLoadGame5.at35 = 0;
+    itemLoadGame6.at35 = 0;
+    itemLoadGame7.at35 = 0;
+    itemLoadGame8.at35 = 0;
+    itemLoadGame9.at35 = 0;
+    itemLoadGame10.at35 = 0;
+    itemLoadGame1.at2c = &itemLoadGamePic;
+    itemLoadGame2.at2c = &itemLoadGamePic;
+    itemLoadGame3.at2c = &itemLoadGamePic;
+    itemLoadGame4.at2c = &itemLoadGamePic;
+    itemLoadGame5.at2c = &itemLoadGamePic;
+    itemLoadGame6.at2c = &itemLoadGamePic;
+    itemLoadGame7.at2c = &itemLoadGamePic;
+    itemLoadGame8.at2c = &itemLoadGamePic;
+    itemLoadGame9.at2c = &itemLoadGamePic;
+    itemLoadGame10.at2c = &itemLoadGamePic;
+    menuLoadGame.Add(&itemBloodQAV, false);
+}
+
+void SetupSoundsMenu(void)
+{
+    itemSoundsMusic.nValue = ClipRange(MusicVolume, itemSoundsMusic.nRangeLow, itemSoundsMusic.nRangeHigh);
+    itemSoundsSound.nValue = ClipRange(FXVolume, itemSoundsSound.nRangeLow, itemSoundsSound.nRangeHigh);
+    menuSounds.Add(&itemSoundsTitle, false);
+    menuSounds.Add(&itemSoundsMusic, true);
+    menuSounds.Add(&itemSoundsSound, false);
+    menuSounds.Add(&itemSoundsCDAudio, false);
+    menuSounds.Add(&itemSounds3DAudio, false);
+    menuSounds.Add(&itemBloodQAV, false);
+}
+
+void SetupQuitMenu(void)
+{
+    menuQuit.Add(&itemQuitTitle, false);
+    menuQuit.Add(&itemQuitText1, false);
+    menuQuit.Add(&itemQuitYesNo, true);
+    menuQuit.Add(&itemBloodQAV, false);
+
+    menuRestart.Add(&itemRestartTitle, false);
+    menuRestart.Add(&itemRestartText1, false);
+    menuRestart.Add(&itemRestartYesNo, true);
+    menuRestart.Add(&itemBloodQAV, false);
+}
+
+void SetupHelpOrderMenu(void)
+{
+    menuOrder.Add(&itemHelp4QAV, true);
+    menuOrder.Add(&itemHelp5QAV, false);
+    menuOrder.Add(&itemHelp3QAV, false);
+    menuOrder.Add(&itemHelp3BQAV, false);
+    itemHelp4QAV.bEnable = 1;
+    itemHelp4QAV.bNoDraw = 1;
+    itemHelp5QAV.bEnable = 1;
+    itemHelp5QAV.bNoDraw = 1;
+    itemHelp3QAV.bEnable = 1;
+    itemHelp3QAV.bNoDraw = 1;
+    itemHelp3BQAV.bEnable = 1;
+    itemHelp3BQAV.bNoDraw = 1;
+}
+
+void SetupCreditsMenu(void)
+{
+    menuCredits.Add(&itemCreditsQAV, true);
+    itemCreditsQAV.bEnable = 1;
+    itemCreditsQAV.bNoDraw = 1;
+}
+
+void SetupParentalLockMenu(void)
+{
+    itemParentalLockToggle.at20 = gbAdultContent;
+    strcpy(itemParentalLockPassword.at20, gzAdultPassword);
+    menuParentalLock.Add(&itemParentalLockTitle, false);
+    menuParentalLock.Add(&itemParentalLockToggle, true);
+    menuParentalLock.Add(&itemParentalLockPassword, false);
+    menuParentalLock.Add(&itemBloodQAV, false);
+}
+
+void SetupSorry3Menu(void)
+{
+    menuPlayOnline.Add(&unk_26E06C, false);
+    menuPlayOnline.Add(&unk_26E090, true);
+    menuPlayOnline.Add(&unk_26E0E8, false);
+    menuPlayOnline.Add(&unk_26E140, false);
+    menuPlayOnline.Add(&unk_26E198, false);
+    menuPlayOnline.Add(&unk_26E1F0, false);
+    menuPlayOnline.Add(&unk_26E248, false);
+    menuPlayOnline.Add(&itemBloodQAV, false);
+}
+
+void SetupSorryMenu(void)
+{
+    menuSorry.Add(&itemSorryPicCycle, true);
+    menuSorry.Add(&itemSorryText1, false);
+    menuSorry.Add(&itemSorryText3, false);
+    menuSorry.Add(&itemBloodQAV, false);
+}
+
+void SetupSorry2Menu(void)
+{
+    menuSorry2.Add(&itemSorryPicCycle, true);
+    menuSorry2.Add(&itemSorry2Text1, false);
+    menuSorry2.Add(&itemSorry2Text2, false);
+    menuSorry2.Add(&itemSorry2Text3, false);
+    menuSorry2.Add(&itemBloodQAV, false);
+}
+
+void SetupOptionsMenu(void)
+{
+    menuOptions.Add(&itemOptionsTitle, false);
+    menuOptions.Add(&itemOptionsChainGame, true);
+    menuOptions.Add(&itemOptionsChainDisplay, false);
+    menuOptions.Add(&itemOptionsChainSound, false);
+    menuOptions.Add(&itemOptionsChainPlayer, false);
+    menuOptions.Add(&itemOptionsChainControl, false);
+    //menuOptions.Add(&itemOptionsChainOld, false);
+    menuOptions.Add(&itemBloodQAV, false);
+
+    menuOptionsGame.Add(&itemOptionsGameTitle, false);
+    menuOptionsGame.Add(&itemOptionsGameBoolShowWeapons, true);
+    menuOptionsGame.Add(&itemOptionsGameBoolSlopeTilting, false);
+    menuOptionsGame.Add(&itemOptionsGameBoolViewBobbing, false);
+    menuOptionsGame.Add(&itemOptionsGameBoolViewSwaying, false);
+    menuOptionsGame.Add(&itemOptionsGameBoolAutoAim, false);
+    menuOptionsGame.Add(&itemOptionsGameWeaponSwitch, false);
+    //menuOptionsGame.Add(&itemOptionsGameChainParentalLock, false);
+    menuOptionsGame.Add(&itemBloodQAV, false);
+    itemOptionsGameBoolShowWeapons.at20 = gShowWeapon;
+    itemOptionsGameBoolSlopeTilting.at20 = gSlopeTilting;
+    itemOptionsGameBoolViewBobbing.at20 = gViewVBobbing;
+    itemOptionsGameBoolViewSwaying.at20 = gViewHBobbing;
+    itemOptionsGameBoolAutoAim.m_nFocus = gAutoAim;
+    itemOptionsGameWeaponSwitch.m_nFocus = (gWeaponSwitch&1) ? ((gWeaponSwitch&2) ? 1 : 2) : 0;
+
+    menuOptionsDisplay.Add(&itemOptionsDisplayTitle, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayColor, true);
+    menuOptionsDisplay.Add(&itemOptionsDisplayMode, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolCrosshair, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolCenterHoriz, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolLevelStats, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolPowerupDuration, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolShowMapTitle, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolMessages, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayBoolWidescreen, false);
+    menuOptionsDisplay.Add(&itemOptionsDisplayFOV, false);
+#ifdef USE_OPENGL
+    menuOptionsDisplay.Add(&itemOptionsDisplayPolymost, false);
+#endif
+    menuOptionsDisplay.Add(&itemBloodQAV, false);
+    itemOptionsDisplayBoolCrosshair.at20 = gAimReticle;
+    itemOptionsDisplayBoolCenterHoriz.at20 = gCenterHoriz;
+    itemOptionsDisplayBoolLevelStats.at20 = gLevelStats;
+    itemOptionsDisplayBoolPowerupDuration.at20 = gPowerupDuration;
+    itemOptionsDisplayBoolShowMapTitle.at20 = gShowMapTitle;
+    itemOptionsDisplayBoolMessages.at20 = gMessageState;
+    itemOptionsDisplayBoolWidescreen.at20 = r_usenewaspect;
+
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeTitle, false);
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeResolution, true);
+    // prepare video setup
+    for (int i = 0; i < validmodecnt; ++i)
+    {
+        int j;
+        for (j = 0; j < gResolutionNum; ++j)
+        {
+            if (validmode[i].xdim == gResolution[j].xdim && validmode[i].ydim == gResolution[j].ydim)
+            {
+                gResolution[j].flags |= validmode[i].fs ? RES_FS : RES_WIN;
+                Bsnprintf(gResolution[j].name, MAXRESOLUTIONSTRINGLENGTH, "%d x %d%s", gResolution[j].xdim, gResolution[j].ydim, (gResolution[j].flags & RES_FS) ? "" : "Win");
+                gResolutionName[j] = gResolution[j].name;
+                if (validmode[i].bpp > gResolution[j].bppmax)
+                    gResolution[j].bppmax = validmode[i].bpp;
+                break;
+            }
+        }
+
+        if (j == gResolutionNum) // no match found
+        {
+            gResolution[j].xdim = validmode[i].xdim;
+            gResolution[j].ydim = validmode[i].ydim;
+            gResolution[j].bppmax = validmode[i].bpp;
+            gResolution[j].flags = validmode[i].fs ? RES_FS : RES_WIN;
+            Bsnprintf(gResolution[j].name, MAXRESOLUTIONSTRINGLENGTH, "%d x %d%s", gResolution[j].xdim, gResolution[j].ydim, (gResolution[j].flags & RES_FS) ? "" : "Win");
+            gResolutionName[j] = gResolution[j].name;
+            ++gResolutionNum;
+        }
+    }
+    itemOptionsDisplayModeResolution.SetTextArray(gResolutionName, gResolutionNum, 0);
+#ifdef USE_OPENGL
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeRenderer, false);
+#endif
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeFullscreen, false);
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeVSync, false);
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeFrameLimit, false);
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeFPSOffset, false);
+    menuOptionsDisplayMode.Add(&itemOptionsDisplayModeApply, false);
+    menuOptionsDisplayMode.Add(&itemBloodQAV, false);
+
+#ifdef USE_OPENGL
+    itemOptionsDisplayModeRenderer.pPreDrawCallback = PreDrawVideoModeMenu;
+#endif
+    itemOptionsDisplayModeFullscreen.pPreDrawCallback = PreDrawVideoModeMenu;
+    itemOptionsDisplayModeFPSOffset.pPreDrawCallback = PreDrawVideoModeMenu;
+
+    menuOptionsDisplayColor.Add(&itemOptionsDisplayColorTitle, false);
+    menuOptionsDisplayColor.Add(&itemOptionsDisplayColorGamma, true);
+    menuOptionsDisplayColor.Add(&itemOptionsDisplayColorContrast, false);
+    menuOptionsDisplayColor.Add(&itemOptionsDisplayColorBrightness, false);
+    menuOptionsDisplayColor.Add(&itemOptionsDisplayColorVisibility, false);
+    menuOptionsDisplayColor.Add(&itemOptionsDisplayColorReset, false);
+    menuOptionsDisplayColor.Add(&itemBloodQAV, false);
+
+    itemOptionsDisplayColorContrast.pPreDrawCallback = PreDrawDisplayColor;
+    itemOptionsDisplayColorBrightness.pPreDrawCallback = PreDrawDisplayColor;
+
+#ifdef USE_OPENGL
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostTitle, false);
+    //menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostTextureMode, true);
+    //menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostAnisotropy, false);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostTrueColorTextures, true);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostTexQuality, false);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostPreloadCache, false);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostDetailTex, false);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostGlowTex, false);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymost3DModels, false);
+    menuOptionsDisplayPolymost.Add(&itemOptionsDisplayPolymostDeliriumBlur, false);
+    menuOptionsDisplayPolymost.Add(&itemBloodQAV, false);
+
+    itemOptionsDisplayPolymostTexQuality.pPreDrawCallback = PreDrawDisplayPolymost;
+    itemOptionsDisplayPolymostPreloadCache.pPreDrawCallback = PreDrawDisplayPolymost;
+    itemOptionsDisplayPolymostDetailTex.pPreDrawCallback = PreDrawDisplayPolymost;
+    itemOptionsDisplayPolymostGlowTex.pPreDrawCallback = PreDrawDisplayPolymost;
+#endif
+
+    menuOptionsSound.Add(&itemOptionsSoundTitle, false);
+    menuOptionsSound.Add(&itemOptionsSoundSoundToggle, true);
+    menuOptionsSound.Add(&itemOptionsSoundMusicToggle, false);
+    menuOptionsSound.Add(&itemOptionsSound3DToggle, false);
+    menuOptionsSound.Add(&itemOptionsSoundSoundVolume, false);
+    menuOptionsSound.Add(&itemOptionsSoundMusicVolume, false);
+    menuOptionsSound.Add(&itemOptionsSoundSampleRate, false);
+    menuOptionsSound.Add(&itemOptionsSoundNumVoices, false);
+    menuOptionsSound.Add(&itemOptionsSoundCDToggle, false);
+    menuOptionsSound.Add(&itemOptionsSoundMusicDevice, false);
+    menuOptionsSound.Add(&itemOptionsSoundApplyChanges, false);
+    menuOptionsSound.Add(&itemBloodQAV, false);
+
+    menuOptionsPlayer.Add(&itemOptionsPlayerTitle, false);
+    menuOptionsPlayer.Add(&itemOptionsPlayerName, true);
+    menuOptionsPlayer.Add(&itemBloodQAV, false);
+
+    menuOptionsControl.Add(&itemOptionsControlTitle, false);
+    menuOptionsControl.Add(&itemOptionsControlKeyboard, true);
+    menuOptionsControl.Add(&itemOptionsControlMouse, false);
+    menuOptionsControl.Add(&itemBloodQAV, false);
+
+    menuOptionsControlKeyboard.Add(&itemOptionsControlKeyboardTitle, false);
+    menuOptionsControlKeyboard.Add(&itemOptionsControlKeyboardList, true);
+    menuOptionsControlKeyboard.Add(&itemOptionsControlKeyboardReset, false);
+    menuOptionsControlKeyboard.Add(&itemOptionsControlKeyboardResetClassic, false);
+    menuOptionsControlKeyboard.Add(&itemBloodQAV, false);
+
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseTitle, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseButton, true);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseSensitivity, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseAimFlipped, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseFilterInput, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseAimMode, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseVerticalAim, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseXScale, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseYScale, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseDigitalUp, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseDigitalDown, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseDigitalLeft, false);
+    menuOptionsControlMouse.Add(&itemOptionsControlMouseDigitalRight, false);
+    menuOptionsControlMouse.Add(&itemBloodQAV, false);
+
+    itemOptionsControlMouseDigitalUp.SetTextArray(pzGamefuncsStrings, NUMGAMEFUNCTIONS+1, 0);
+    itemOptionsControlMouseDigitalDown.SetTextArray(pzGamefuncsStrings, NUMGAMEFUNCTIONS+1, 0);
+    itemOptionsControlMouseDigitalLeft.SetTextArray(pzGamefuncsStrings, NUMGAMEFUNCTIONS+1, 0);
+    itemOptionsControlMouseDigitalRight.SetTextArray(pzGamefuncsStrings, NUMGAMEFUNCTIONS+1, 0);
+
+    itemOptionsControlMouseVerticalAim.pPreDrawCallback = PreDrawControlMouse;
+
+    menuOptionsControlMouseButtonAssignment.Add(&itemOptionsControlMouseTitle, false);
+    int y = 60;
+    for (int i = 0; i < MENUMOUSEFUNCTIONS; i++)
+    {
+        pItemOptionsControlMouseButton[i] = new CGameMenuItemZCycle(MenuMouseNames[i], 3, 66, y, 180, 0, SetMouseButton, pzGamefuncsStrings, NUMGAMEFUNCTIONS+1, 0, true);
+        dassert(pItemOptionsControlMouseButton[i] != NULL);
+        menuOptionsControlMouseButtonAssignment.Add(pItemOptionsControlMouseButton[i], i == 0);
+        y += 10;
+    }
+    menuOptionsControlMouseButtonAssignment.Add(&itemBloodQAV, false);
+}
+
+void SetupMenus(void)
+{
+    // prepare gamefuncs and keys
+    pzGamefuncsStrings[0] = MenuGameFuncNone;
+    nGamefuncsValues[0] = -1;
+    int k = 1;
+    for (int i = 0; i < NUMGAMEFUNCTIONS; ++i)
+    {
+        Bstrcpy(MenuGameFuncs[i], gamefunctions[i]);
+
+        for (int j = 0; j < MAXGAMEFUNCLEN; ++j)
+            if (MenuGameFuncs[i][j] == '_')
+                MenuGameFuncs[i][j] = ' ';
+
+        if (gamefunctions[i][0] != '\0')
+        {
+            pzGamefuncsStrings[k] = MenuGameFuncs[i];
+            nGamefuncsValues[k] = i;
+            ++k;
+        }
+    }
+
+    nGamefuncsNum = k;
+
+    SetupLoadingScreen();
+    SetupKeyListMenu();
+    SetupMessagesMenu();
+    SetupControlsMenu();
+    SetupSaveGameMenu();
+    SetupLoadGameMenu();
+    SetupOptionsOldMenu();
+    SetupCreditsMenu();
+    SetupHelpOrderMenu();
+    SetupSoundsMenu();
+    SetupDifficultyMenu();
+    SetupEpisodeMenu();
+    SetupMainMenu();
+    SetupMainMenuWithSave();
+    SetupNetStartMenu();
+    SetupQuitMenu();
+    SetupParentalLockMenu();
+    SetupSorryMenu();
+    SetupSorry2Menu();
+    SetupSorry3Menu();
+
+    SetupOptionsMenu();
+    SetupNetworkMenu();
+}
+
+void UpdateNetworkMenus(void)
+{
+    if (gGameOptions.nGameType > 0)
+    {
+        itemMain1.at24 = &menuNetStart;
+        itemMain1.at28 = 2;
+    }
+    else
+    {
+        itemMain1.at24 = &menuEpisode;
+        itemMain1.at28 = -1;
+    }
+    if (gGameOptions.nGameType > 0)
+    {
+        itemMainSave1.at24 = &menuNetStart;
+        itemMainSave1.at28 = 2;
+    }
+    else
+    {
+        itemMainSave1.at24 = &menuEpisode;
+        itemMainSave1.at28 = -1;
+    }
+}
+
+void SetDoppler(CGameMenuItemZBool *pItem)
+{
+    gDoppler = pItem->at20;
+}
+
+void SetCrosshair(CGameMenuItemZBool *pItem)
+{
+    gAimReticle = pItem->at20;
+}
+
+void SetCenterHoriz(CGameMenuItemZBool *pItem)
+{
+    gCenterHoriz = pItem->at20;
+}
+
+void ResetKeys(CGameMenuItemChain *)
+{
+    CONFIG_SetDefaultKeys(keydefaults);
+}
+
+void ResetKeysClassic(CGameMenuItemChain *)
+{
+    CONFIG_SetDefaultKeys(oldkeydefaults);
+}
+
+void SetShowWeapons(CGameMenuItemZBool *pItem)
+{
+    gShowWeapon = pItem->at20;
+}
+
+void SetSlopeTilting(CGameMenuItemZBool *pItem)
+{
+    gSlopeTilting = pItem->at20;
+}
+
+void SetViewBobbing(CGameMenuItemZBool *pItem)
+{
+    gViewVBobbing = pItem->at20;
+}
+
+void SetViewSwaying(CGameMenuItemZBool *pItem)
+{
+    gViewHBobbing = pItem->at20;
+}
+
+void SetDetail(CGameMenuItemSlider *pItem)
+{
+    gDetail = pItem->nValue;
+}
+
+void SetGamma(CGameMenuItemSlider *pItem)
+{
+    gGamma = pItem->nValue;
+    scrSetGamma(gGamma);
+}
+
+void SetMusicVol(CGameMenuItemSlider *pItem)
+{
+    sndSetMusicVolume(pItem->nValue);
+}
+
+void SetSoundVol(CGameMenuItemSlider *pItem)
+{
+    sndSetFXVolume(pItem->nValue);
+}
+
+void SetCDVol(CGameMenuItemSlider *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    // NUKE-TODO:
+}
+
+void SetMessages(CGameMenuItemZBool *pItem)
+{
+    gMessageState = pItem->at20;
+    gGameMessageMgr.SetState(gMessageState);
+}
+
+void SetMouseSensitivity(CGameMenuItemSliderFloat *pItem)
+{
+	CONTROL_MouseSensitivity = pItem->fValue;
+}
+
+void SetMouseAimFlipped(CGameMenuItemZBool *pItem)
+{
+    gMouseAimingFlipped = pItem->at20;
+}
+
+void SetTurnSpeed(CGameMenuItemSlider *pItem)
+{
+    gTurnSpeed = pItem->nValue;
+}
+
+void SetAutoAim(CGameMenuItemZCycle *pItem)
+{
+    gAutoAim = pItem->m_nFocus;
+    if (!gDemo.at0 && !gDemo.at1)
+    {
+        gProfile[myconnectindex].nAutoAim = gAutoAim;
+        netBroadcastPlayerInfo(myconnectindex);
+    }
+}
+
+void SetLevelStats(CGameMenuItemZBool *pItem)
+{
+    gLevelStats = pItem->at20;
+}
+
+void SetPowerupDuration(CGameMenuItemZBool* pItem)
+{
+    gPowerupDuration = pItem->at20;
+}
+
+void SetShowMapTitle(CGameMenuItemZBool* pItem)
+{
+    gShowMapTitle = pItem->at20;
+}
+
+void SetWeaponSwitch(CGameMenuItemZCycle *pItem)
+{
+    gWeaponSwitch &= ~(1|2);
+    switch (pItem->m_nFocus)
+    {
+    case 0:
+        break;
+    case 1:
+        gWeaponSwitch |= 2;
+        fallthrough__;
+    case 2:
+    default:
+        gWeaponSwitch |= 1;
+        break;
+    }
+    if (!gDemo.at0 && !gDemo.at1)
+    {
+        gProfile[myconnectindex].nWeaponSwitch = gWeaponSwitch;
+        netBroadcastPlayerInfo(myconnectindex);
+    }
+}
+
+extern bool gStartNewGame;
+
+void SetDifficultyAndStart(CGameMenuItemChain *pItem)
+{
+    gGameOptions.nDifficulty = pItem->at30;
+    gSkill = pItem->at30;
+    gGameOptions.nLevel = 0;
+    if (gDemo.at1)
+        gDemo.StopPlayback();
+    gStartNewGame = true;
+    gCheatMgr.sub_5BCF4();
+    gGameMenuMgr.Deactivate();
+}
+
+void SetVideoModeOld(CGameMenuItemChain *pItem)
+{
+    if (pItem->at30 == validmodecnt)
+    {
+        gSetup.fullscreen = 0;
+        gSetup.xdim = 640;
+        gSetup.ydim = 480;
+    }
+    else
+    {
+        gSetup.fullscreen = 0;
+        gSetup.xdim = validmode[pItem->at30].xdim;
+        gSetup.ydim = validmode[pItem->at30].ydim;
+    }
+    scrSetGameMode(gSetup.fullscreen, gSetup.xdim, gSetup.ydim, gSetup.bpp);
+    scrSetDac();
+    viewResizeView(gViewSize);
+}
+
+void SetVideoMode(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    resolution_t p = { xres, yres, fullscreen, bpp, 0 };
+    int32_t prend = videoGetRenderMode();
+    int32_t pvsync = vsync;
+
+    int32_t nResolution = itemOptionsDisplayModeResolution.m_nFocus;
+    resolution_t n = { gResolution[nResolution].xdim, gResolution[nResolution].ydim,
+                       (gResolution[nResolution].flags & RES_FS) ? itemOptionsDisplayModeFullscreen.at20 : 0,
+                       (nRendererValues[itemOptionsDisplayModeRenderer.m_nFocus] == REND_CLASSIC) ? 8 : gResolution[nResolution].bppmax, 0 };
+    int32_t UNUSED(nrend) = nRendererValues[itemOptionsDisplayModeRenderer.m_nFocus];
+    int32_t nvsync = nVSyncValues[itemOptionsDisplayModeVSync.m_nFocus];
+
+    if (videoSetGameMode(n.flags, n.xdim, n.ydim, n.bppmax, upscalefactor) < 0)
+    {
+        if (videoSetGameMode(p.flags, p.xdim, p.ydim, p.bppmax, upscalefactor) < 0)
+        {
+            videoSetRenderMode(prend);
+            ThrowError("Failed restoring old video mode.");
+        }
+        else
+        {
+            onvideomodechange(p.bppmax > 8);
+            vsync = videoSetVsync(pvsync);
+        }
+    }
+    else onvideomodechange(n.bppmax > 8);
+
+    viewResizeView(gViewSize);
+    vsync = videoSetVsync(nvsync);
+    gSetup.fullscreen = fullscreen;
+    gSetup.xdim = xres;
+    gSetup.ydim = yres;
+    gSetup.bpp = bpp;
+}
+
+void SetWidescreen(CGameMenuItemZBool *pItem)
+{
+    r_usenewaspect = pItem->at20;
+}
+
+void SetFOV(CGameMenuItemSlider *pItem)
+{
+    gFov = pItem->nValue;
+}
+
+void SetupVideoModeMenu(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    for (int i = 0; i < gResolutionNum; i++)
+    {
+        if (gSetup.xdim == gResolution[i].xdim && gSetup.ydim == gResolution[i].ydim)
+        {
+            itemOptionsDisplayModeResolution.m_nFocus = i;
+            break;
+        }
+    }
+    itemOptionsDisplayModeFullscreen.at20 = gSetup.fullscreen;
+#ifdef USE_OPENGL
+    for (int i = 0; i < 2; i++)
+    {
+        if (videoGetRenderMode() == nRendererValues[i])
+        {
+            itemOptionsDisplayModeRenderer.m_nFocus = i;
+            break;
+        }
+    }
+#endif
+    for (int i = 0; i < 3; i++)
+    {
+        if (vsync == nVSyncValues[i])
+        {
+            itemOptionsDisplayModeVSync.m_nFocus = i;
+            break;
+        }
+    }
+    for (int i = 0; i < 8; i++)
+    {
+        if (r_maxfps == nFrameLimitValues[i])
+        {
+            itemOptionsDisplayModeFrameLimit.m_nFocus = i;
+            break;
+        }
+    }
+    itemOptionsDisplayModeFPSOffset.nValue = r_maxfpsoffset;
+}
+
+void PreDrawVideoModeMenu(CGameMenuItem *pItem)
+{
+    if (pItem == &itemOptionsDisplayModeFullscreen)
+        pItem->bEnable = !!(gResolution[itemOptionsDisplayModeResolution.m_nFocus].flags & RES_FS);
+#ifdef USE_OPENGL
+    else if (pItem == &itemOptionsDisplayModeRenderer)
+        pItem->bEnable = gResolution[itemOptionsDisplayModeResolution.m_nFocus].bppmax > 8;
+#endif
+}
+
+void UpdateVideoModeMenuFrameLimit(CGameMenuItemZCycle *pItem)
+{
+    r_maxfps = nFrameLimitValues[pItem->m_nFocus];
+    g_frameDelay = calcFrameDelay(r_maxfps + r_maxfpsoffset);
+}
+
+void UpdateVideoModeMenuFPSOffset(CGameMenuItemSlider *pItem)
+{
+    r_maxfpsoffset = pItem->nValue;
+    g_frameDelay = calcFrameDelay(r_maxfps + r_maxfpsoffset);
+}
+
+void UpdateVideoColorMenu(CGameMenuItemSliderFloat *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    g_videoGamma = itemOptionsDisplayColorGamma.fValue;
+    g_videoContrast = itemOptionsDisplayColorContrast.fValue;
+    g_videoBrightness = itemOptionsDisplayColorBrightness.fValue;
+    r_ambientlight = itemOptionsDisplayColorVisibility.fValue;
+    r_ambientlightrecip = 1.f/r_ambientlight;
+    gBrightness = GAMMA_CALC<<2;
+    videoSetPalette(gBrightness>>2, gLastPal, 0);
+}
+
+void PreDrawDisplayColor(CGameMenuItem *pItem)
+{
+    if (pItem == &itemOptionsDisplayColorContrast)
+        pItem->bEnable = gammabrightness;
+    else if (pItem == &itemOptionsDisplayColorBrightness)
+        pItem->bEnable = gammabrightness;
+}
+
+void ResetVideoColor(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    g_videoGamma = DEFAULT_GAMMA;
+    g_videoContrast = DEFAULT_CONTRAST;
+    g_videoBrightness = DEFAULT_BRIGHTNESS;
+    gBrightness = 0;
+    r_ambientlight = r_ambientlightrecip = 1.f;
+    videoSetPalette(gBrightness>>2, gLastPal, 0);
+}
+
+#ifdef USE_OPENGL
+void SetupVideoPolymostMenu(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    itemOptionsDisplayPolymostTextureMode.m_nFocus = 0;
+    for (int i = 0; i < 2; i++)
+    {
+        if (nTextureModeValues[i] == gltexfiltermode)
+        {
+            itemOptionsDisplayPolymostTextureMode.m_nFocus = i;
+            break;
+        }
+    }
+    itemOptionsDisplayPolymostAnisotropy.m_nFocus = 0;
+    for (int i = 0; i < 6; i++)
+    {
+        if (nAnisotropyValues[i] == glanisotropy)
+        {
+            itemOptionsDisplayPolymostAnisotropy.m_nFocus = i;
+            break;
+        }
+    }
+    itemOptionsDisplayPolymostTrueColorTextures.at20 = usehightile;
+    itemOptionsDisplayPolymostTexQuality.m_nFocus = r_downsize;
+    itemOptionsDisplayPolymostPreloadCache.at20 = useprecache;
+    itemOptionsDisplayPolymostDetailTex.at20 = r_detailmapping;
+    itemOptionsDisplayPolymostGlowTex.at20 = r_glowmapping;
+    itemOptionsDisplayPolymost3DModels.at20 = usemodels;
+    itemOptionsDisplayPolymostDeliriumBlur.at20 = gDeliriumBlur;
+}
+
+void UpdateTextureMode(CGameMenuItemZCycle *pItem)
+{
+    gltexfiltermode = nTextureModeValues[pItem->m_nFocus];
+    gltexapplyprops();
+}
+
+void UpdateAnisotropy(CGameMenuItemZCycle *pItem)
+{
+    glanisotropy = nAnisotropyValues[pItem->m_nFocus];
+    gltexapplyprops();
+}
+
+void UpdateTrueColorTextures(CGameMenuItemZBool *pItem)
+{
+    usehightile = pItem->at20;
+}
+#endif
+
+void DoModeChange(void)
+{
+    videoResetMode();
+    if (videoSetGameMode(fullscreen, xres, yres, bpp, upscalefactor))
+        OSD_Printf("restartvid: Reset failed...\n");
+    onvideomodechange(gSetup.bpp > 8);
+}
+
+#ifdef USE_OPENGL
+void UpdateTexQuality(CGameMenuItemZCycle *pItem)
+{
+    r_downsize = pItem->m_nFocus;
+    r_downsizevar = r_downsize;
+    DoModeChange();
+}
+
+void UpdatePreloadCache(CGameMenuItemZBool *pItem)
+{
+    useprecache = pItem->at20;
+}
+
+void UpdateDetailTex(CGameMenuItemZBool *pItem)
+{
+    r_detailmapping = pItem->at20;
+}
+
+void UpdateGlowTex(CGameMenuItemZBool *pItem)
+{
+    r_glowmapping = pItem->at20;
+}
+
+void Update3DModels(CGameMenuItemZBool *pItem)
+{
+    usemodels = pItem->at20;
+}
+
+void UpdateDeliriumBlur(CGameMenuItemZBool *pItem)
+{
+    gDeliriumBlur = pItem->at20;
+}
+
+void PreDrawDisplayPolymost(CGameMenuItem *pItem)
+{
+    if (pItem == &itemOptionsDisplayPolymostTexQuality)
+        pItem->bEnable = usehightile;
+    else if (pItem == &itemOptionsDisplayPolymostPreloadCache)
+        pItem->bEnable = usehightile;
+    else if (pItem == &itemOptionsDisplayPolymostDetailTex)
+        pItem->bEnable = usehightile;
+    else if (pItem == &itemOptionsDisplayPolymostGlowTex)
+        pItem->bEnable = usehightile;
+}
+#endif
+
+void UpdateSoundToggle(CGameMenuItemZBool *pItem)
+{
+    SoundToggle = pItem->at20;
+    if (!SoundToggle)
+        FX_StopAllSounds();
+}
+
+void UpdateMusicToggle(CGameMenuItemZBool *pItem)
+{
+    MusicToggle = pItem->at20;
+    if (!MusicToggle)
+        sndStopSong();
+    else
+    {
+        if (gGameStarted || gDemo.at1)
+            sndPlaySong(gGameOptions.zLevelSong, true);
+    }
+}
+
+void Update3DToggle(CGameMenuItemZBool *pItem)
+{
+    gDoppler = pItem->at20;
+}
+
+void UpdateCDToggle(CGameMenuItemZBool *pItem)
+{
+    CDAudioToggle = pItem->at20;
+    if (gGameStarted || gDemo.at1)
+        levelTryPlayMusicOrNothing(gGameOptions.nEpisode, gGameOptions.nLevel);
+}
+
+void UpdateSoundVolume(CGameMenuItemSlider *pItem)
+{
+    sndSetFXVolume(pItem->nValue);
+}
+
+void UpdateMusicVolume(CGameMenuItemSlider *pItem)
+{
+    sndSetMusicVolume(pItem->nValue);
+}
+
+void UpdateSoundRate(CGameMenuItemZCycle *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void UpdateNumVoices(CGameMenuItemSlider *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void UpdateMusicDevice(CGameMenuItemZCycle *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void SetSound(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    MixRate = nSoundRateValues[itemOptionsSoundSampleRate.m_nFocus];
+    NumVoices = itemOptionsSoundNumVoices.nValue;
+    MusicDevice = itemOptionsSoundMusicDevice.m_nFocus;
+    sfxTerm();
+    sndTerm();
+
+    sndInit();
+    sfxInit();
+
+    if (MusicToggle && (gGameStarted || gDemo.at1))
+        sndPlaySong(gGameOptions.zLevelSong, true);
+}
+
+void PreDrawSound(CGameMenuItem *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void SetupOptionsSound(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    itemOptionsSoundSoundToggle.at20 = SoundToggle;
+    itemOptionsSoundMusicToggle.at20 = MusicToggle;
+    itemOptionsSound3DToggle.at20 = gDoppler;
+    itemOptionsSoundCDToggle.at20 = CDAudioToggle;
+    itemOptionsSoundSampleRate.m_nFocus = 0;
+    for (int i = 0; i < 3; i++)
+    {
+        if (nSoundRateValues[i] == MixRate)
+        {
+            itemOptionsSoundSampleRate.m_nFocus = i;
+            break;
+        }
+    }
+    itemOptionsSoundNumVoices.nValue = NumVoices;
+    itemOptionsSoundMusicDevice.m_nFocus = MusicDevice;
+}
+
+void UpdatePlayerName(CGameMenuItemZEdit *pItem, CGameMenuEvent *pEvent)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    if (pEvent->at0 == kMenuEventEnter)
+        netBroadcastPlayerInfo(myconnectindex);
+}
+
+void SetMouseFilterInput(CGameMenuItemZBool *pItem)
+{
+    CONTROL_SmoothMouse = pItem->at20;
+    SmoothInput = pItem->at20;
+}
+
+void SetMouseAimMode(CGameMenuItemZBool *pItem)
+{
+    gMouseAiming = pItem->at20;
+}
+
+void SetMouseVerticalAim(CGameMenuItemZBool *pItem)
+{
+    gMouseAim = pItem->at20;
+}
+
+void SetMouseXScale(CGameMenuItemSlider *pItem)
+{
+    MouseAnalogueScale[0] = pItem->nValue;
+    CONTROL_SetAnalogAxisScale(0, pItem->nValue, controldevice_mouse);
+}
+
+void SetMouseYScale(CGameMenuItemSlider *pItem)
+{
+    MouseAnalogueScale[1] = pItem->nValue;
+    CONTROL_SetAnalogAxisScale(1, pItem->nValue, controldevice_mouse);
+}
+
+void SetMouseDigitalAxis(CGameMenuItemZCycle *pItem)
+{
+    if (pItem == &itemOptionsControlMouseDigitalUp)
+    {
+        MouseDigitalFunctions[1][0] = nGamefuncsValues[pItem->m_nFocus];
+        CONTROL_MapDigitalAxis(1, MouseDigitalFunctions[1][0], 0, controldevice_mouse);
+    }
+    else if (pItem == &itemOptionsControlMouseDigitalDown)
+    {
+        MouseDigitalFunctions[1][1] = nGamefuncsValues[pItem->m_nFocus];
+        CONTROL_MapDigitalAxis(1, MouseDigitalFunctions[1][1], 1, controldevice_mouse);
+    }
+    else if (pItem == &itemOptionsControlMouseDigitalLeft)
+    {
+        MouseDigitalFunctions[0][0] = nGamefuncsValues[pItem->m_nFocus];
+        CONTROL_MapDigitalAxis(0, MouseDigitalFunctions[0][0], 0, controldevice_mouse);
+    }
+    else if (pItem == &itemOptionsControlMouseDigitalRight)
+    {
+        MouseDigitalFunctions[0][1] = nGamefuncsValues[pItem->m_nFocus];
+        CONTROL_MapDigitalAxis(0, MouseDigitalFunctions[0][1], 1, controldevice_mouse);
+    }
+}
+
+void SetupMouseMenu(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    static CGameMenuItemZCycle *pMouseDigitalAxis[4] = {
+        &itemOptionsControlMouseDigitalLeft,
+        &itemOptionsControlMouseDigitalRight,
+        &itemOptionsControlMouseDigitalUp,
+        &itemOptionsControlMouseDigitalDown
+    };
+    for (int i = 0; i < ARRAY_SSIZE(pMouseDigitalAxis); i++)
+    {
+        CGameMenuItemZCycle *pItem = pMouseDigitalAxis[i];
+        pItem->m_nFocus = 0;
+        for (int j = 0; j < NUMGAMEFUNCTIONS+1; j++)
+        {
+            if (nGamefuncsValues[j] == MouseDigitalFunctions[i>>1][i&1])
+            {
+                pItem->m_nFocus = j;
+                break;
+            }
+        }
+    }
+    itemOptionsControlMouseAimFlipped.at20 = gMouseAimingFlipped;
+    itemOptionsControlMouseFilterInput.at20 = SmoothInput;
+    itemOptionsControlMouseAimMode.at20 = gMouseAiming;
+    itemOptionsControlMouseVerticalAim.at20 = gMouseAim;
+}
+
+void PreDrawControlMouse(CGameMenuItem *pItem)
+{
+    if (pItem == &itemOptionsControlMouseVerticalAim)
+        pItem->bEnable = !gMouseAiming;
+}
+
+void SetMouseButton(CGameMenuItemZCycle *pItem)
+{
+    for (int i = 0; i < MENUMOUSEFUNCTIONS; i++)
+    {
+        if (pItem == pItemOptionsControlMouseButton[i])
+        {
+            int nFunc = nGamefuncsValues[pItem->m_nFocus];
+            MouseFunctions[MenuMouseDataIndex[i][0]][MenuMouseDataIndex[i][1]] = nFunc;
+            CONTROL_MapButton(nFunc, MenuMouseDataIndex[i][0], MenuMouseDataIndex[i][1], controldevice_mouse);
+            CONTROL_FreeMouseBind(MenuMouseDataIndex[i][0]);
+            break;
+        }
+    }
+}
+
+void SetupMouseButtonMenu(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    for (int i = 0; i < MENUMOUSEFUNCTIONS; i++)
+    {
+        auto pItem = pItemOptionsControlMouseButton[i];
+        pItem->m_nFocus = 0;
+        for (int j = 0; j < NUMGAMEFUNCTIONS+1; j++)
+        {
+            if (MouseFunctions[MenuMouseDataIndex[i][0]][MenuMouseDataIndex[i][1]] == nGamefuncsValues[j])
+            {
+                pItem->m_nFocus = j;
+                break;
+            }
+        }
+    }
+}
+
+void SetupNetworkMenu(void)
+{
+    sprintf(zNetPortBuffer, "%d", gNetPort);
+    if (strlen(gNetAddress) > 0)
+        strncpy(zNetAddressBuffer, gNetAddress, sizeof(zNetAddressBuffer)-1);
+
+    menuNetwork.Add(&itemNetworkTitle, false);
+    menuNetwork.Add(&itemNetworkHost, true);
+    menuNetwork.Add(&itemNetworkJoin, false);
+    menuNetwork.Add(&itemBloodQAV, false);
+
+    menuNetworkHost.Add(&itemNetworkHostTitle, false);
+    menuNetworkHost.Add(&itemNetworkHostPlayerNum, true);
+    menuNetworkHost.Add(&itemNetworkHostPort, false);
+    menuNetworkHost.Add(&itemNetworkHostHost, false);
+    menuNetworkHost.Add(&itemBloodQAV, false);
+
+    menuNetworkJoin.Add(&itemNetworkJoinTitle, false);
+    menuNetworkJoin.Add(&itemNetworkJoinAddress, true);
+    menuNetworkJoin.Add(&itemNetworkJoinPort, false);
+    menuNetworkJoin.Add(&itemNetworkJoinJoin, false);
+    menuNetworkJoin.Add(&itemBloodQAV, false);
+}
+
+void SetupNetworkHostMenu(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void SetupNetworkJoinMenu(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void NetworkHostGame(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    sndStopSong();
+    FX_StopAllSounds();
+    UpdateDacs(0, true);
+    gNetPlayers = itemNetworkHostPlayerNum.nValue;
+    gNetPort = strtoul(zNetPortBuffer, NULL, 10);
+    if (!gNetPort)
+        gNetPort = kNetDefaultPort;
+    gNetMode = NETWORK_SERVER;
+    netInitialize(false);
+    gGameMenuMgr.Deactivate();
+    gQuitGame = gRestartGame = true;
+}
+
+void NetworkJoinGame(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    sndStopSong();
+    FX_StopAllSounds();
+    UpdateDacs(0, true);
+    strcpy(gNetAddress, zNetAddressBuffer);
+    gNetPort = strtoul(zNetPortBuffer, NULL, 10);
+    if (!gNetPort)
+        gNetPort = kNetDefaultPort;
+    gNetMode = NETWORK_CLIENT;
+    netInitialize(false);
+    gGameMenuMgr.Deactivate();
+    gQuitGame = gRestartGame = true;
+}
+
+void SaveGameProcess(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+void TenProcess(CGameMenuItem7EA1C *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+}
+
+short gQuickLoadSlot = -1;
+short gQuickSaveSlot = -1;
+
+void SaveGame(CGameMenuItemZEditBitmap *pItem, CGameMenuEvent *event)
+{
+    char strSaveGameName[BMAX_PATH];
+    int nSlot = pItem->at28;
+    if (gGameOptions.nGameType > 0 || !gGameStarted)
+        return;
+    if (event->at0 != 6/* || strSaveGameName[0]*/)
+    {
+        gGameMenuMgr.Deactivate();
+        return;
+    }
+    G_ModDirSnprintf(strSaveGameName, BMAX_PATH, "game00%02d.sav", nSlot);
+    strcpy(gGameOptions.szUserGameName, strRestoreGameStrings[nSlot]);
+    sprintf(gGameOptions.szSaveGameName, "%s", strSaveGameName);
+    gGameOptions.nSaveGameSlot = nSlot;
+    viewLoadingScreen(2518, "Saving", "Saving Your Game", strRestoreGameStrings[nSlot]);
+    gSaveGameNum = nSlot;
+    LoadSave::SaveGame(strSaveGameName);
+    gQuickSaveSlot = nSlot;
+    gGameMenuMgr.Deactivate();
+}
+
+void QuickSaveGame(void)
+{
+    char strSaveGameName[BMAX_PATH];
+    if (gGameOptions.nGameType > 0 || !gGameStarted)
+        return;
+    /*if (strSaveGameName[0])
+    {
+        gGameMenuMgr.Deactivate();
+        return;
+    }*/
+    G_ModDirSnprintf(strSaveGameName, BMAX_PATH, "game00%02d.sav", gQuickSaveSlot);
+    strcpy(gGameOptions.szUserGameName, strRestoreGameStrings[gQuickSaveSlot]);
+    sprintf(gGameOptions.szSaveGameName, "%s", strSaveGameName);
+    gGameOptions.nSaveGameSlot = gQuickSaveSlot;
+    viewLoadingScreen(2518, "Saving", "Saving Your Game", strRestoreGameStrings[gQuickSaveSlot]);
+    LoadSave::SaveGame(strSaveGameName);
+    gGameOptions.picEntry = gSavedOffset;
+    gSaveGameOptions[gQuickSaveSlot] = gGameOptions;
+    UpdateSavedInfo(gQuickSaveSlot);
+    gGameMenuMgr.Deactivate();
+}
+
+void LoadGame(CGameMenuItemZEditBitmap *pItem, CGameMenuEvent *event)
+{
+    UNREFERENCED_PARAMETER(event);
+    char strLoadGameName[BMAX_PATH];
+    int nSlot = pItem->at28;
+    if (gGameOptions.nGameType > 0)
+        return;
+    G_ModDirSnprintf(strLoadGameName, BMAX_PATH, "game00%02d.sav", nSlot);
+    if (!testkopen(strLoadGameName, 0))
+        return;
+    viewLoadingScreen(2518, "Loading", "Loading Saved Game", strRestoreGameStrings[nSlot]);
+    LoadSave::LoadGame(strLoadGameName);
+    gGameMenuMgr.Deactivate();
+    gQuickLoadSlot = nSlot;
+}
+
+void QuickLoadGame(void)
+{
+    char strLoadGameName[BMAX_PATH];
+    if (gGameOptions.nGameType > 0)
+        return;
+    G_ModDirSnprintf(strLoadGameName, BMAX_PATH, "game00%02d.sav", gQuickLoadSlot);
+    if (!testkopen(strLoadGameName, 0))
+        return;
+    viewLoadingScreen(2518, "Loading", "Loading Saved Game", strRestoreGameStrings[gQuickLoadSlot]);
+    LoadSave::LoadGame(strLoadGameName);
+    gGameMenuMgr.Deactivate();
+}
+
+void SetupLevelMenuItem(int nEpisode)
+{
+    dassert(nEpisode >= 0 && nEpisode < gEpisodeCount);
+    itemNetStart3.SetTextArray(zLevelNames[nEpisode], gEpisodeInfo[nEpisode].nLevels, 0);
+}
+
+void SetupNetLevels(CGameMenuItemZCycle *pItem)
+{
+    SetupLevelMenuItem(pItem->m_nFocus);
+}
+
+void StartNetGame(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    gPacketStartGame.gameType = itemNetStart1.m_nFocus+1;
+    if (gPacketStartGame.gameType == 0)
+        gPacketStartGame.gameType = 2;
+    gPacketStartGame.episodeId = itemNetStart2.m_nFocus;
+    gPacketStartGame.levelId = itemNetStart3.m_nFocus;
+    gPacketStartGame.difficulty = itemNetStart4.m_nFocus;
+    gPacketStartGame.monsterSettings = itemNetStart5.m_nFocus;
+    gPacketStartGame.weaponSettings = itemNetStart6.m_nFocus;
+    gPacketStartGame.itemSettings = itemNetStart7.m_nFocus;
+    gPacketStartGame.respawnSettings = 0;
+    gPacketStartGame.unk = 0;
+    gPacketStartGame.userMapName[0] = 0;
+    strncpy(gPacketStartGame.userMapName, itemNetStart9.at20, 13);
+    gPacketStartGame.userMapName[12] = 0;
+    gPacketStartGame.userMap = gPacketStartGame.userMapName[0] != 0;
+    netBroadcastNewGame();
+    gStartNewGame = 1;
+    gGameMenuMgr.Deactivate();
+}
+
+void Restart(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    if (gGameOptions.nGameType == 0 || numplayers == 1)
+    {
+        gQuitGame = true;
+        gRestartGame = true;
+    }
+    else
+        gQuitRequest = 2;
+    gGameMenuMgr.Deactivate();
+}
+
+void Quit(CGameMenuItemChain *pItem)
+{
+    UNREFERENCED_PARAMETER(pItem);
+    if (gGameOptions.nGameType == 0 || numplayers == 1)
+        gQuitGame = true;
+    else
+        gQuitRequest = 1;
+    gGameMenuMgr.Deactivate();
+}
+
+void SetParentalLock(CGameMenuItemZBool *pItem)
+{
+    if (!pItem->at20)
+    {
+        pItem->at20 = true;
+        pItem->Draw();
+        if (strcmp(itemParentalLockPassword.at20, ""))
+        {
+            itemParentalLockPassword.pMenu->FocusNextItem();
+            itemParentalLockPassword.at32 = 0;
+            itemParentalLockPassword.at37 = 1;
+            itemParentalLockPassword.at5f = pItem;
+            itemParentalLockPassword.at29[0] = 0;
+            return;
+        }
+        else
+        {
+            itemParentalLockPassword.at20[0] = 0;
+            pItem->Draw();
+            gbAdultContent = false;
+        }
+    }
+    else
+        gbAdultContent = true;
+    // NUKE-TODO: CONFIG_WriteAdultMode();
+}
+
+void MenuSetupEpisodeInfo(void)
+{
+    memset(zEpisodeNames, 0, sizeof(zEpisodeNames));
+    memset(zLevelNames, 0, sizeof(zLevelNames));
+    for (int i = 0; i < 6; i++)
+    {
+        if (i < gEpisodeCount)
+        {
+            EPISODEINFO *pEpisode = &gEpisodeInfo[i];
+            zEpisodeNames[i] = pEpisode->at0;
+            for (int j = 0; j < 16; j++)
+            {
+                if (j < pEpisode->nLevels)
+                {
+                    zLevelNames[i][j] = pEpisode->at28[j].at90;
+                }
+            }
+        }
+    }
+}
+
+void drawLoadingScreen(void)
+{
+    char buffer[80];
+    if (gGameOptions.nGameType == 0)
+    {
+        if (gDemo.at1)
+            sprintf(buffer, "Loading Demo");
+        else
+            sprintf(buffer, "Loading Level");
+    }
+    else
+        sprintf(buffer, "%s", zNetGameTypes[gGameOptions.nGameType-1]);
+    viewLoadingScreen(2049, buffer, levelGetTitle(), NULL);
+}
diff --git a/source/blood/src/menu.h b/source/blood/src/menu.h
new file mode 100644
index 000000000..bfa447e88
--- /dev/null
+++ b/source/blood/src/menu.h
@@ -0,0 +1,58 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "gamemenu.h"
+
+extern CGameMenu menuMain;
+extern CGameMenu menuMainWithSave;
+extern CGameMenu menuNetMain;
+extern CGameMenu menuNetStart;
+extern CGameMenu menuEpisode;
+extern CGameMenu menuDifficulty;
+extern CGameMenu menuOptionsOld;
+extern CGameMenu menuControls;
+extern CGameMenu menuMessages;
+extern CGameMenu menuKeys;
+extern CGameMenu menuSaveGame;
+extern CGameMenu menuLoadGame;
+extern CGameMenu menuLoading;
+extern CGameMenu menuSounds;
+extern CGameMenu menuQuit;
+extern CGameMenu menuRestart;
+extern CGameMenu menuCredits;
+extern CGameMenu menuOrder;
+extern CGameMenu menuPlayOnline;
+extern CGameMenu menuParentalLock;
+extern CGameMenu menuSorry;
+extern CGameMenu menuSorry2;
+
+extern CGameMenu menuOptions;
+extern CGameMenu menuOptionsSound;
+extern short gQuickLoadSlot;
+extern short gQuickSaveSlot;
+extern char strRestoreGameStrings[][16];
+void drawLoadingScreen(void);
+void SetupMenus(void);
+void UpdateNetworkMenus(void);
+void QuickSaveGame(void);
+void QuickLoadGame(void);
\ No newline at end of file
diff --git a/source/blood/src/messages.cpp b/source/blood/src/messages.cpp
new file mode 100644
index 000000000..91d8e001c
--- /dev/null
+++ b/source/blood/src/messages.cpp
@@ -0,0 +1,821 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "mmulti.h"
+#include "compat.h"
+#include "keyboard.h"
+#include "control.h"
+#include "function.h"
+#include "common_game.h"
+#include "blood.h"
+#include "config.h"
+#include "demo.h"
+#include "eventq.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "menu.h"
+#include "messages.h"
+#include "network.h"
+#include "player.h"
+#include "view.h"
+
+CPlayerMsg gPlayerMsg;
+CCheatMgr gCheatMgr;
+
+void sub_5A928(void)
+{
+    for (int i = 0; i < NUMGAMEFUNCTIONS-1; i++)
+        CONTROL_ClearButton(i);
+}
+
+extern uint8_t KeyboardKeys[NUMGAMEFUNCTIONS][2];
+void sub_5A944(char key)
+{
+    for (int i = 0; i < NUMGAMEFUNCTIONS-1; i++)
+    {
+        char key1, key2;
+        key1 = KeyboardKeys[i][0];
+        key2 = KeyboardKeys[i][1];
+        if (key1 == key || key2 == key)
+            CONTROL_ClearButton(i);
+    }
+}
+
+void SetGodMode(bool god)
+{
+    playerSetGodMode(gMe, god);
+    if (gMe->at31a)
+        viewSetMessage("You are immortal.");
+    else
+        viewSetMessage("You are mortal.");
+}
+
+void SetClipMode(bool noclip)
+{
+    gNoClip = noclip;
+    if (gNoClip)
+        viewSetMessage("Unclipped movement.");
+    else
+        viewSetMessage("Normal movement.");
+}
+
+void packStuff(PLAYER *pPlayer)
+{
+    for (int i = 0; i < 5; i++)
+        packAddItem(pPlayer, i);
+}
+
+void packClear(PLAYER *pPlayer)
+{
+    pPlayer->at321 = 0;
+    for (int i = 0; i < 5; i++)
+    {
+        pPlayer->packInfo[i].at0 = 0;
+        pPlayer->packInfo[i].at1 = 0;
+    }
+}
+
+void SetAmmo(bool stat)
+{
+    if (stat)
+    {
+        for (int i = 0; i < 12; i++)
+            gMe->at181[i] = gAmmoInfo[i].at0;
+        viewSetMessage("You have full ammo.");
+    }
+    else
+    {
+        for (int i = 0; i < 12; i++)
+            gMe->at181[i] = 0;
+        viewSetMessage("You have no ammo.");
+    }
+}
+
+void SetWeapons(bool stat)
+{
+    for (int i = 0; i < 14; i++)
+    {
+        gMe->atcb[i] = stat;
+    }
+    SetAmmo(stat);
+    if (stat)
+        viewSetMessage("You have all weapons.");
+    else
+    {
+        if (!VanillaMode())
+        {
+            // Keep the pitchfork to avoid freeze
+            gMe->atcb[1] = 1;
+            gMe->atbd = 0;
+            gMe->atbe = 1;
+        }
+        viewSetMessage("You have no weapons.");
+    }
+}
+
+void SetToys(bool stat)
+{
+    if (stat)
+    {
+        packStuff(gMe);
+        viewSetMessage("Your inventory is full.");
+    }
+    else
+    {
+        packClear(gMe);
+        viewSetMessage("Your inventory is empty.");
+    }
+}
+
+void SetArmor(bool stat)
+{
+    int nAmount;
+    if (stat)
+    {
+        viewSetMessage("You have full armor.");
+        nAmount = 3200;
+    }
+    else
+    {
+        viewSetMessage("You have no armor.");
+        nAmount = 0;
+    }
+    for (int i = 0; i < 3; i++)
+        gMe->at33e[i] = nAmount;
+}
+
+void SetKeys(bool stat)
+{
+    for (int i = 1; i <= 6; i++)
+        gMe->at88[i] = stat;
+    if (stat)
+        viewSetMessage("You have all keys.");
+    else
+        viewSetMessage("You have no keys.");
+}
+
+void SetInfiniteAmmo(bool stat)
+{
+    gInfiniteAmmo = stat;
+    if (gInfiniteAmmo)
+        viewSetMessage("You have infinite ammo.");
+    else
+        viewSetMessage("You have limited ammo.");
+}
+
+void SetMap(bool stat)
+{
+    gFullMap = stat;
+    if (gFullMap)
+        viewSetMessage("You have the map.");
+    else
+        viewSetMessage("You have no map.");
+}
+
+void SetWooMode(bool stat)
+{
+    if (stat)
+    {
+        if (!powerupCheck(gMe, 17))
+            powerupActivate(gMe, 17);
+    }
+    else
+    {
+        if (powerupCheck(gMe, 17))
+        {
+            if (!VanillaMode())
+                gMe->at202[17] = 0;
+            powerupDeactivate(gMe, 17);
+        }
+    }
+}
+
+void ToggleWooMode(void)
+{
+    SetWooMode(!(powerupCheck(gMe, 17) != 0));
+}
+
+void ToggleBoots(void)
+{
+    if (powerupCheck(gMe, 15))
+    {
+        viewSetMessage("You have no Jumping Boots.");
+        if (!VanillaMode())
+        {
+            gMe->at202[15] = 0;
+            gMe->packInfo[4].at1 = 0;
+        }
+        powerupDeactivate(gMe, 15);
+    }
+    else
+    {
+        viewSetMessage("You have the Jumping Boots.");
+        if (!VanillaMode())
+            gMe->at202[15] = gPowerUpInfo[15].at3;
+        powerupActivate(gMe, 15);
+    }
+}
+
+void ToggleInvisibility(void)
+{
+    if (powerupCheck(gMe, 13))
+    {
+        viewSetMessage("You are visible.");
+        if (!VanillaMode())
+            gMe->at202[13] = 0;
+        powerupDeactivate(gMe, 13);
+    }
+    else
+    {
+        viewSetMessage("You are invisible.");
+        powerupActivate(gMe, 13);
+    }
+}
+
+void ToggleInvulnerability(void)
+{
+    if (powerupCheck(gMe, 14))
+    {
+        viewSetMessage("You are vulnerable.");
+        if (!VanillaMode())
+            gMe->at202[14] = 0;
+        powerupDeactivate(gMe, 14);
+    }
+    else
+    {
+        viewSetMessage("You are invulnerable.");
+        powerupActivate(gMe, 14);
+    }
+}
+
+void ToggleDelirium(void)
+{
+    if (powerupCheck(gMe, 28))
+    {
+        viewSetMessage("You are not delirious.");
+        if (!VanillaMode())
+            gMe->at202[28] = 0;
+        powerupDeactivate(gMe, 28);
+    }
+    else
+    {
+        viewSetMessage("You are delirious.");
+        powerupActivate(gMe, 28);
+    }
+}
+
+void LevelWarp(int nEpisode, int nLevel)
+{
+    levelSetupOptions(nEpisode, nLevel);
+    StartLevel(&gGameOptions);
+    viewResizeView(gViewSize);
+}
+
+void LevelWarpAndRecord(int nEpisode, int nLevel)
+{
+    char buffer[BMAX_PATH];
+    levelSetupOptions(nEpisode, nLevel);
+    gGameStarted = false;
+    strcpy(buffer, levelGetFilename(nEpisode, nLevel));
+    ChangeExtension(buffer, ".DEM");
+    gDemo.Create(buffer);
+    StartLevel(&gGameOptions);
+    viewResizeView(gViewSize);
+}
+
+CGameMessageMgr::CGameMessageMgr()
+{
+    at1 = 1;
+    at5 = 0;
+    at9 = 0;
+    atd = 0;
+    at11 = 0;
+    at15 = 8;
+    at19 = 4;
+    at1d = 5;
+    at21 = 15;
+    at22 = 0;
+    at2a = at26 = 0;
+}
+
+void CGameMessageMgr::SetState(char state)
+{
+    if (at0 && !state)
+    {
+        at0 = 0;
+        Clear();
+    }
+    else if (!at0 && state)
+        at0 = 1;
+}
+
+void CGameMessageMgr::Add(const char *a1, char a2)
+{
+    if (a2 && at21)
+    {
+        messageStruct *pMessage = &at2e[at2a];
+        strncpy(pMessage->at4, a1, 80);
+        pMessage->at4[80] = 0;
+        pMessage->at0 = gFrameClock + at1d*120;
+        at2a = (at2a+1)%16;
+        at22++;
+        if (at22 > at19)
+        {
+            at26 = (at26+1)%16;
+            atd = 0;
+            at22 = at19;
+            at9 = at15;
+        }
+    }
+}
+
+void CGameMessageMgr::Display(void)
+{
+    if (at22 && at0 && gInputMode != INPUT_MODE_2)
+    {
+        int v10 = at22;
+        int v18 = at26;
+        int vc = ClipHigh(v10*8, 48);
+        int v14 = gViewMode == 3 ? gViewX0S : 0;
+        int v8 = (gViewMode == 3 ? at5 : 0) + at9;
+        for (int i = 0; i < v10; i++)
+        {
+            messageStruct *pMessage = &at2e[(v18+i)%16];
+            if (pMessage->at0 < gFrameClock)
+            {
+                at26 = (at26+1)%16;
+                at22--;
+                continue;
+            }
+            viewDrawText(at11, pMessage->at4, v14, v8, vc, 0, 0, false, 256);
+            if (gViewMode == 3)
+            {
+                int height;
+                gMenuTextMgr.GetFontInfo(at11, pMessage->at4, &height, NULL);
+                if (v14+height > gViewX1S)
+                    viewUpdatePages();
+            }
+            v8 += at15;
+            vc = ClipLow(vc-64/v10, -128);
+        }
+        if (at9)
+        {
+            at9 = at15*at9/120;
+            atd += gFrameTicks;
+        }
+    }
+}
+
+void CGameMessageMgr::Clear(void)
+{
+    at26 = at2a = at22 = 0;
+}
+
+void CGameMessageMgr::SetMaxMessages(int nMessages)
+{
+    at19 = ClipRange(nMessages, 1, 16);
+}
+
+void CGameMessageMgr::SetFont(int nFont)
+{
+    at11 = nFont;
+    at15 = gFont[nFont].ySize;
+}
+
+void CGameMessageMgr::SetCoordinates(int x, int y)
+{
+    at1 = ClipRange(x, 0, gViewX1S);
+    at5 = ClipRange(y, 0, gViewY1S);
+}
+
+void CGameMessageMgr::SetMessageTime(int nTime)
+{
+    at1d = ClipRange(nTime, 1, 8);
+}
+
+void CGameMessageMgr::SetMessageFlags(unsigned int nFlags)
+{
+    at21 = nFlags&0xf;
+}
+
+void CPlayerMsg::Clear(void)
+{
+    at4[0] = 0;
+    at0 = 0;
+}
+
+void CPlayerMsg::Term(void)
+{
+    Clear();
+    gInputMode = INPUT_MODE_0;
+}
+
+void CPlayerMsg::Draw(void)
+{
+    char buffer[44];
+    strcpy(buffer, at4);
+    if (gGameClock & 16)
+        strcat(buffer, "_");
+    int x = gViewMode == 3 ? gViewX0S : 0;
+    int y = gViewMode == 3 ? gViewY0S : 0;
+    if (gViewSize >= 1)
+        y += tilesiz[2229].y*((gNetPlayers+3)/4);
+    viewDrawText(0, buffer, x+1,y+1, -128, 0, 0, false, 256);
+    viewUpdatePages();
+}
+
+bool CPlayerMsg::AddChar(char ch)
+{
+    if (at0 < 40)
+    {
+        at4[at0++] = ch;
+        at4[at0] = 0;
+        return true;
+    }
+    return false;
+}
+
+void CPlayerMsg::DelChar(void)
+{
+    if (at0 > 0)
+        at4[--at0] = 0;
+}
+
+void CPlayerMsg::Set(const char * pzString)
+{
+    strncpy(at4, pzString, 40);
+    at0 = ClipHigh(strlen(pzString), 40);
+    at4[at0] = 0;
+}
+
+void CPlayerMsg::Send(void)
+{
+    netBroadcastMessage(myconnectindex, at4);
+    viewSetMessage(at4);
+    Term();
+    keyFlushScans();
+}
+
+void CPlayerMsg::ProcessKeys(void)
+{
+    int key = keyGetScan();
+    char ch;
+    if (key != 0)
+    {
+        bool UNUSED(alt) = keystatus[sc_LeftAlt] || keystatus[sc_RightAlt];
+        bool ctrl = keystatus[sc_LeftControl] || keystatus[sc_RightControl];
+        bool shift = keystatus[sc_LeftShift] || keystatus[sc_RightShift];
+        switch (key)
+        {
+        case sc_Escape:
+            Term();
+            break;
+        case sc_F1:
+        case sc_F2:
+        case sc_F3:
+        case sc_F4:
+        case sc_F5:
+        case sc_F6:
+        case sc_F7:
+        case sc_F8:
+        case sc_F9:
+        case sc_F10:
+            CONTROL_ClearButton(gamefunc_See_Chase_View);
+            Set(CommbatMacro[key-sc_F1]);
+            Send();
+            keystatus[key] = 0;
+            break;
+        case sc_BackSpace:
+            if (ctrl)
+                Clear();
+            else
+                DelChar();
+            break;
+        case sc_Enter:
+        case sc_kpad_Enter:
+            if (gCheatMgr.Check(at4))
+                Term();
+            else
+                Send();
+            break;
+        default:
+            if (key < 128)
+            {
+                ch =  shift ? g_keyAsciiTableShift[key] : g_keyAsciiTable[key];
+                if (ch)
+                    AddChar(ch);
+            }
+            break;
+        }
+        sub_5A944(key);
+    }
+}
+
+CCheatMgr::CHEATINFO CCheatMgr::s_CheatInfo[] = {
+    {"NQLGB", kCheatMpkfa, 0 }, // MPKFA (Invincibility)
+    {"DBQJONZBTT", kCheatCapInMyAss, 0 }, // CAPINMYASS (Disable invincibility )
+    {"OPDBQJONZBTT", kCheatNoCapInMyAss, 0 }, // NOCAPINMYASS (Invincibility)
+    {"J!XBOOB!CF!MJLF!LFWJO", kCheatNoCapInMyAss, 0 }, // I WANNA BE LIKE KEVIN (Invincibility)
+    {"JEBIP", kCheatIdaho, 0 }, // IDAHO (All weapons and full ammo)
+    {"NPOUBOB", kCheatMontana, 0 }, // MONTANA (All weapons, full ammo and all items)
+    {"HSJTXPME", kCheatGriswold, 0 }, // GRISWOLD (Full armor (same effect as getting super armor))
+    {"FENBSL", kCheatEdmark, 0 }, // EDMARK (Does a lot of fire damage to you (if you have 200HP and 200 fire armor then you can survive). Displays the message "THOSE WERE THE DAYS".)
+    {"UFRVJMB", kCheatTequila, 0 }, // TEQUILA (Guns akimbo power-up)
+    {"CVO[", kCheatBunz, 0 }, // BUNZ (All weapons, full ammo, and guns akimbo power-up)
+    {"GVOLZ!TIPFT", kCheatFunkyShoes, 0 }, // FUNKY SHOES (Gives jump boots item and activates it)
+    {"HBUFLFFQFS", kCheatGateKeeper, 0 }, // GATEKEEPER (Sets the you cheated flag to true, at the end of the level you will see that you have cheated)
+    {"LFZNBTUFS", kCheatKeyMaster, 0 }, // KEYMASTER (All keys)
+    {"KPKP", kCheatJoJo, 0 }, // JOJO (Drunk mode (same effect as getting bitten by red spider))
+    {"TBUDIFM", kCheatSatchel, 0 }, // SATCHEL (Full inventory)
+    {"TQPSL", kCheatSpork, 0 }, // SPORK (200% health (same effect as getting life seed))
+    {"POFSJOH", kCheatOneRing, 0 }, // ONERING (Cloak of invisibility power-up)
+    {"NBSJP", kCheatMario, 1 }, // MARIO (Warp to level E M, e.g.: MARIO 1 3 will take you to Phantom Express)
+    {"DBMHPO", kCheatCalgon, 1 }, // CALGON (Jumps to next level or can be used like MARIO with parameters)
+    {"LFWPSLJBO", kCheatKevorkian, 0 }, // KEVORKIAN (Does a lot of physical damage to you (if you have 200HP and 200 fire armor then you can survive). Displays the message "KEVORKIAN APPROVES".)
+    {"NDHFF", kCheatMcGee, 0 }, // MCGEE (Sets you on fire. Displays the message "YOU'RE FIRED".)
+    {"LSVFHFS", kCheatKrueger, 0 }, // KRUEGER (200% health, but sets you on fire. Displays the message "FLAME RETARDANT".)
+    {"DIFFTFIFBE", kCheatCheeseHead, 0 }, // CHEESEHEAD (100% diving suit)
+    {"DPVTUFBV", kCheatCousteau, 0 }, // COUSTEAU (200% health and diving suit)
+    {"WPPSIFFT", kCheatVoorhees, 0 }, // VOORHEES (Death mask power-up)
+    {"MBSB!DSPGU", kCheatLaraCroft, 0 }, // LARA CROFT (All weapons and infinite ammo. Displays the message "LARA RULES". Typing it the second time will lose all weapons and ammo.)
+    {"IPOHLPOH", kCheatHongKong, 0 }, // HONGKONG (All weapons and infinite ammo)
+    {"GSBOLFOTUFJO", kCheatFrankenstein, 0 }, // FRANKENSTEIN (100% med-kit)
+    {"TUFSOP", kCheatSterno, 0 }, // STERNO (Temporary blindness (same effect as getting bitten by green spider))
+    {"DMBSJDF", kCheatClarice, 0 }, // CLARICE (Gives 100% body armor, 100% fire armor, 100% spirit armor)
+    {"GPSL!ZPV", kCheatForkYou, 0 }, // FORK YOU (Drunk mode, 1HP, no armor, no weapons, no ammo, no items, no keys, no map, guns akimbo power-up)
+    {"MJFCFSNBO", kCheatLieberMan, 0 }, // LIEBERMAN (Sets the you cheated flag to true, at the end of the level you will see that you have cheated)
+    {"FWB!HBMMJ", kCheatEvaGalli, 0 }, // EVA GALLI (Disable/enable clipping (grant the ability to walk through walls))
+    {"SBUF", kCheatRate, 0 }, // RATE (Display frame rate (doesn't count as a cheat))
+    {"HPPOJFT", kCheatGoonies, 0 }, // GOONIES (Enable full map. Displays the message "YOU HAVE THE MAP".)
+    {"TQJFMCFSH", kCheatSpielberg, 1 }, // SPIELBERG (Disables all cheats. If number values corresponding to a level and episode number are entered after the cheat word (i.e. "spielberg 1 3" for Phantom Express), you will be spawned to said level and the game will begin recording a demo from your actions.)
+};
+
+bool CCheatMgr::m_bPlayerCheated = false;
+
+bool CCheatMgr::Check(char *pzString)
+{
+    char buffer[80];
+    strcpy(buffer, pzString);
+    Bstrupr(buffer);
+    for (size_t i = 0; i < strlen(pzString); i++)
+        buffer[i]++;
+    for (int i = 0; i < 36; i++)
+    {
+        int nCheatLen = strlen(s_CheatInfo[i].pzString);
+        if (s_CheatInfo[i].flags & 1)
+        {
+            if (!strncmp(buffer, s_CheatInfo[i].pzString, nCheatLen))
+            {
+                Process(s_CheatInfo[i].id, buffer+nCheatLen);
+                return true;
+            }
+        }
+        if (!strcmp(buffer, s_CheatInfo[i].pzString))
+        {
+            Process(s_CheatInfo[i].id, NULL);
+            return true;
+        }
+    }
+    return false;
+}
+
+int parseArgs(char *pzArgs, int *nArg1, int *nArg2)
+{
+    if (!nArg1 || !nArg2)
+        return -1;
+    int nLength = strlen(pzArgs);
+    for (int i = 0; i < nLength; i++)
+        pzArgs[i]--;
+    int stat = sscanf(pzArgs, " %d %d", nArg1, nArg2);
+    if (stat == 2 && (*nArg1 == 0 || *nArg2 == 0))
+        return -1;
+    *nArg1 = ClipRange(*nArg1-1, 0, gEpisodeCount-1);
+    *nArg2 = ClipRange(*nArg2-1, 0, gEpisodeInfo[*nArg1].nLevels-1);
+    return stat;
+}
+
+void CCheatMgr::Process(CCheatMgr::CHEATCODE nCheatCode, char* pzArgs)
+{
+    dassert(nCheatCode > kCheatNone && nCheatCode < kCheatMax);
+
+    if (gDemo.at0) return;
+    if (nCheatCode == kCheatRate)
+    {
+        gShowFps = !gShowFps;
+        return;
+    }
+    if (gGameOptions.nGameType != 0)
+        return;
+    int nEpisode, nLevel;
+    switch (nCheatCode)
+    {
+    case kCheatSpielberg:
+        if (parseArgs(pzArgs, &nEpisode, &nLevel) == 2)
+            LevelWarpAndRecord(nEpisode, nLevel);
+        break;
+    case kCheat1:
+        SetAmmo(true);
+        break;
+    case kCheatGriswold:
+        SetArmor(true);
+        break;
+    case kCheatSatchel:
+        SetToys(true);
+        break;
+    case kCheatEvaGalli:
+        SetClipMode(!gNoClip);
+        break;
+    case kCheatMpkfa:
+        SetGodMode(!gMe->at31a);
+        break;
+    case kCheatCapInMyAss:
+        SetGodMode(false);
+        break;
+    case kCheatNoCapInMyAss:
+        SetGodMode(true);
+        break;
+    case kCheatIdaho:
+        SetWeapons(true);
+        break;
+    case kCheatKevorkian:
+        actDamageSprite(gMe->at5b, gMe->pSprite, DAMAGE_TYPE_2, 8000);
+        viewSetMessage("Kevorkian approves.");
+        break;
+    case kCheatMcGee:
+    {
+        if (!gMe->pXSprite->burnTime)
+            evPost(gMe->at5b, 3, 0, CALLBACK_ID_0);
+        actBurnSprite(actSpriteIdToOwnerId(gMe->at5b), gMe->pXSprite, 2400);
+        viewSetMessage("You're fired!");
+        break;
+    }
+    case kCheatEdmark:
+        actDamageSprite(gMe->at5b, gMe->pSprite, DAMAGE_TYPE_3, 8000);
+        viewSetMessage("Ahhh...those were the days.");
+        break;
+    case kCheatKrueger:
+    {
+        actHealDude(gMe->pXSprite, 200, 200);
+        gMe->at33e[1] = VanillaMode() ? 200 : 3200;
+        if (!gMe->pXSprite->burnTime)
+            evPost(gMe->at5b, 3, 0, CALLBACK_ID_0);
+        actBurnSprite(actSpriteIdToOwnerId(gMe->at5b), gMe->pXSprite, 2400);
+        viewSetMessage("Flame retardant!");
+        break;
+    }
+    case kCheatSterno:
+        gMe->at36a = 250;
+        break;
+    case kCheat14: // quake (causing a little flicker), not used by any cheat code (dead code)
+        gMe->at35a = 360;
+        break;
+    case kCheatSpork:
+        actHealDude(gMe->pXSprite, 200, 200);
+        break;
+    case kCheatGoonies:
+        SetMap(!gFullMap);
+        break;
+    case kCheatClarice:
+        if (!VanillaMode())
+        {
+            viewSetMessage("You have half armor.");
+            for (int i = 0; i < 3; i++)
+                gMe->at33e[i] = 1600;
+        }
+        break;
+    case kCheatFrankenstein:
+        gMe->packInfo[0].at1 = 100;
+        break;
+    case kCheatCheeseHead:
+        gMe->packInfo[1].at1 = 100;
+        if (!VanillaMode())
+            gMe->at202[18] = gPowerUpInfo[18].at3;
+        break;
+    case kCheatTequila:
+        ToggleWooMode();
+        break;
+    case kCheatFunkyShoes:
+        ToggleBoots();
+        break;
+    case kCheatKeyMaster:
+        SetKeys(true);
+        break;
+    case kCheatOneRing:
+        ToggleInvisibility();
+        break;
+    case kCheatVoorhees:
+        ToggleInvulnerability();
+        break;
+    case kCheatJoJo:
+        ToggleDelirium();
+        break;
+    case kCheatRate: // show FPS, handled before (dead code), leave here for safety
+        return;
+    case kCheatMario:
+        if (parseArgs(pzArgs, &nEpisode, &nLevel) == 2)
+            LevelWarp(nEpisode, nLevel);
+        break;
+    case kCheatCalgon:
+        if (parseArgs(pzArgs, &nEpisode, &nLevel) == 2)
+            LevelWarp(nEpisode, nLevel);
+        else
+            if (!VanillaMode())
+                levelEndLevel(0);
+        break;
+    case kCheatLaraCroft:
+        SetInfiniteAmmo(!gInfiniteAmmo);
+        SetWeapons(gInfiniteAmmo);
+        break;
+    case kCheatHongKong:
+        SetWeapons(true);
+        SetInfiniteAmmo(true);
+        break;
+    case kCheatMontana:
+        SetWeapons(true);
+        SetToys(true);
+        break;
+    case kCheatBunz:
+        SetWeapons(true);
+        SetWooMode(true);
+        break;
+    case kCheatCousteau:
+        actHealDude(gMe->pXSprite,200,200);
+        gMe->packInfo[1].at1 = 100;
+        if (!VanillaMode())
+            gMe->at202[18] = gPowerUpInfo[18].at3;
+        break;
+    case kCheatForkYou:
+        SetInfiniteAmmo(false);
+        SetMap(false);
+        SetWeapons(false);
+        SetAmmo(false);
+        SetArmor(false);
+        SetToys(false);
+        SetKeys(false);
+        SetWooMode(true);
+        powerupActivate(gMe, 28);
+        gMe->pXSprite->health = 16;
+        gMe->atcb[1] = 1;
+        gMe->atbd = 0;
+        gMe->atbe = 1;
+        break;
+    default:
+        break;
+    }
+    m_bPlayerCheated = true;
+}
+
+void CCheatMgr::sub_5BCF4(void)
+{
+    m_bPlayerCheated = 0;
+    playerSetGodMode(gMe, 0);
+    gNoClip = 0;
+    packClear(gMe);
+    gInfiniteAmmo = 0;
+    gFullMap = 0;
+}
+
+class MessagesLoadSave : public LoadSave
+{
+public:
+    virtual void Load();
+    virtual void Save();
+};
+
+void MessagesLoadSave::Load()
+{
+    Read(&CCheatMgr::m_bPlayerCheated, sizeof(CCheatMgr::m_bPlayerCheated));
+}
+
+void MessagesLoadSave::Save()
+{
+    Write(&CCheatMgr::m_bPlayerCheated, sizeof(CCheatMgr::m_bPlayerCheated));
+}
+
+static MessagesLoadSave *myLoadSave;
+
+void MessagesLoadSaveConstruct(void)
+{
+    myLoadSave = new MessagesLoadSave();
+}
diff --git a/source/blood/src/messages.h b/source/blood/src/messages.h
new file mode 100644
index 000000000..225c1d2ac
--- /dev/null
+++ b/source/blood/src/messages.h
@@ -0,0 +1,146 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "player.h"
+
+class CGameMessageMgr
+{
+public:
+    struct messageStruct
+    {
+        int at0;
+        char at4[81];
+    };
+    char at0;
+    int at1;
+    int at5;
+    int at9;
+    int atd;
+    int at11;
+    int at15;
+    int at19;
+    int at1d;
+    char at21;
+    int at22;
+    int at26;
+    int at2a;
+    messageStruct at2e[16];
+    CGameMessageMgr();
+    void SetState(char state);
+    void Add(const char *, char);
+    void Display(void);
+    void Clear();
+    void SetMaxMessages(int nMessages);
+    void SetFont(int nFont);
+    void SetCoordinates(int x, int y);
+    void SetMessageTime(int nTime);
+    void SetMessageFlags(unsigned int nFlags);
+};
+
+
+class CPlayerMsg
+{
+public:
+    int at0;
+    char at4[41];
+    CPlayerMsg() { at0 = 0; at4[0] = 0; }
+    void Clear(void);
+    void Term(void);
+    void Draw(void);
+    bool AddChar(char);
+    void DelChar(void);
+    void Set(const char *pzString);
+    void Send(void);
+    void ProcessKeys(void);
+};
+
+class CCheatMgr
+{
+public:
+    static bool m_bPlayerCheated;
+    enum CHEATCODE
+    {
+        kCheatNone = 0,
+        kCheat1, // refills ammo, no cheat code for it
+        kCheatGriswold,
+        kCheatSatchel,
+        kCheatEvaGalli,
+        kCheatMpkfa,
+        kCheatCapInMyAss,
+        kCheatNoCapInMyAss,
+        kCheatIdaho,
+        kCheatKevorkian,
+        kCheatMcGee,
+        kCheatEdmark,
+        kCheatKrueger,
+        kCheatSterno,
+        kCheat14, // quake effect, not used
+        kCheatSpork,
+        kCheatGoonies,
+        kCheatClarice,
+        kCheatFrankenstein,
+        kCheatCheeseHead,
+        kCheatTequila,
+        kCheatFunkyShoes,
+        kCheatKeyMaster,
+        kCheatOneRing,
+        kCheatVoorhees,
+        kCheatJoJo,
+        kCheatGateKeeper,
+        kCheatRate,
+        kCheatMario,
+        kCheatLaraCroft,
+        kCheatHongKong,
+        kCheatMontana,
+        kCheatBunz,
+        kCheatCousteau,
+        kCheatForkYou,
+        kCheatLieberMan,
+        kCheatSpielberg,
+        kCheatCalgon,
+        kCheatMax
+    };
+    struct CHEATINFO
+    {
+        const char* pzString;
+        CHEATCODE id;
+        int flags;
+    };
+    static CHEATINFO s_CheatInfo[];
+    CCheatMgr() {}
+    bool Check(char *pzString);
+    void Process(CHEATCODE nCheatCode, char* pzArgs);
+    void sub_5BCF4(void);
+};
+
+extern CPlayerMsg gPlayerMsg;
+extern CCheatMgr gCheatMgr;
+
+void SetAmmo(bool stat);
+void SetWeapons(bool stat);
+void SetToys(bool stat);
+void SetArmor(bool stat);
+void SetKeys(bool stat);
+void SetGodMode(bool god);
+void SetClipMode(bool noclip);
diff --git a/source/blood/src/midi.cpp b/source/blood/src/midi.cpp
new file mode 100644
index 000000000..a4531928f
--- /dev/null
+++ b/source/blood/src/midi.cpp
@@ -0,0 +1,958 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+/**********************************************************************
+   module: MIDI.C
+
+   author: James R. Dose
+   date:   May 25, 1994
+
+   Midi song file playback routines.
+
+   (c) Copyright 1994 James R. Dose.  All Rights Reserved.
+**********************************************************************/
+
+// This object is shared by all Build games with MIDI playback!
+
+#include "compat.h"
+#include "music.h"
+#include "_midi.h"
+#include "midi.h"
+#include "mpu401.h"
+#include "compat.h"
+#include "pragmas.h"
+
+#include "multivoc.h"
+
+#include "windows_inc.h"
+
+extern int32_t MUSIC_SoundDevice;
+
+static const int32_t _MIDI_CommandLengths[ NUM_MIDI_CHANNELS ] =
+{
+    0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 2, 0
+};
+
+static track *_MIDI_TrackPtr = NULL;
+static int32_t    _MIDI_TrackMemSize;
+static int32_t    _MIDI_NumTracks;
+
+static int32_t _MIDI_SongActive = FALSE;
+static int32_t _MIDI_SongLoaded = FALSE;
+static int32_t _MIDI_Loop = FALSE;
+
+static int32_t  _MIDI_Division;
+static int32_t  _MIDI_Tick    = 0;
+static int32_t  _MIDI_Beat    = 1;
+static int32_t  _MIDI_Measure = 1;
+static uint32_t _MIDI_Time;
+static int32_t  _MIDI_BeatsPerMeasure;
+static int32_t  _MIDI_TicksPerBeat;
+static int32_t  _MIDI_TimeBase;
+static int32_t _MIDI_FPSecondsPerTick;
+static uint32_t _MIDI_TotalTime;
+static int32_t  _MIDI_TotalTicks;
+static int32_t  _MIDI_TotalBeats;
+static int32_t  _MIDI_TotalMeasures;
+
+uint32_t _MIDI_PositionInTicks;
+uint32_t _MIDI_GlobalPositionInTicks;
+
+static int32_t  _MIDI_Context;
+
+static int32_t _MIDI_ActiveTracks;
+static int32_t _MIDI_TotalVolume = MIDI_MaxVolume;
+
+static int32_t _MIDI_ChannelVolume[ NUM_MIDI_CHANNELS ];
+
+static midifuncs *_MIDI_Funcs = NULL;
+
+static int32_t Reset = FALSE;
+
+int32_t MIDI_Tempo = 120;
+
+static int32_t _MIDI_PlayRoutine = -1;
+static int32_t _MIDI_MixRate = 44100;
+static int32_t _MIDI_MixTimer = 0;
+
+static int32_t _MIDI_ReadNumber(void *from, size_t size)
+{
+    if (size > 4)
+        size = 4;
+
+    char *FromPtr = (char *)from;
+    int32_t value = 0;
+
+    while (size--)
+    {
+        value <<= 8;
+        value += *FromPtr++;
+    }
+
+    return value;
+}
+
+static int32_t _MIDI_ReadDelta(track *ptr)
+{
+    int32_t value;
+
+    GET_NEXT_EVENT(ptr, value);
+
+    if (value & 0x80)
+    {
+        value &= 0x7f;
+        char c;
+
+        do
+        {
+            GET_NEXT_EVENT(ptr, c);
+            value = (value << 7) + (c & 0x7f);
+        }
+        while (c & 0x80);
+    }
+
+    return value;
+}
+
+static void _MIDI_ResetTracks(void)
+{
+    _MIDI_Tick = 0;
+    _MIDI_Beat = 1;
+    _MIDI_Measure = 1;
+    _MIDI_Time = 0;
+    _MIDI_BeatsPerMeasure = 4;
+    _MIDI_TicksPerBeat = _MIDI_Division;
+    _MIDI_TimeBase = 4;
+    _MIDI_PositionInTicks = 0;
+    _MIDI_ActiveTracks    = 0;
+    _MIDI_Context         = 0;
+
+    track *ptr = _MIDI_TrackPtr;
+    for (bssize_t i = 0; i < _MIDI_NumTracks; ++i)
+    {
+        ptr->pos                    = ptr->start;
+        ptr->delay                  = _MIDI_ReadDelta(ptr);
+        ptr->active                 = ptr->EMIDI_IncludeTrack;
+        ptr->RunningStatus          = 0;
+        ptr->currentcontext         = 0;
+        ptr->context[ 0 ].loopstart = ptr->start;
+        ptr->context[ 0 ].loopcount = 0;
+
+        if (ptr->active)
+            _MIDI_ActiveTracks++;
+
+        ptr++;
+    }
+}
+
+static void _MIDI_AdvanceTick(void)
+{
+    _MIDI_PositionInTicks++;
+    _MIDI_Time += _MIDI_FPSecondsPerTick;
+
+    _MIDI_Tick++;
+    while (_MIDI_Tick > _MIDI_TicksPerBeat)
+    {
+        _MIDI_Tick -= _MIDI_TicksPerBeat;
+        _MIDI_Beat++;
+    }
+    while (_MIDI_Beat > _MIDI_BeatsPerMeasure)
+    {
+        _MIDI_Beat -= _MIDI_BeatsPerMeasure;
+        _MIDI_Measure++;
+    }
+}
+
+static void _MIDI_SysEx(track *Track)
+{
+    int32_t length = _MIDI_ReadDelta(Track);
+    Track->pos += length;
+}
+
+
+static void _MIDI_MetaEvent(track *Track)
+{
+    int32_t   command;
+    int32_t   length;
+
+    GET_NEXT_EVENT(Track, command);
+    GET_NEXT_EVENT(Track, length);
+
+    switch (command)
+    {
+        case MIDI_END_OF_TRACK:
+            Track->active = FALSE;
+
+            _MIDI_ActiveTracks--;
+            break;
+
+        case MIDI_TEMPO_CHANGE:
+        {
+            int32_t tempo = tabledivide32_noinline(60000000L, _MIDI_ReadNumber(Track->pos, 3));
+            MIDI_SetTempo(tempo);
+            break;
+        }
+
+        case MIDI_TIME_SIGNATURE:
+        {
+            if ((_MIDI_Tick > 0) || (_MIDI_Beat > 1))
+                _MIDI_Measure++;
+
+            _MIDI_Tick = 0;
+            _MIDI_Beat = 1;
+            _MIDI_TimeBase = 1;
+            _MIDI_BeatsPerMeasure = (int32_t)*Track->pos;
+            int32_t denominator = (int32_t) * (Track->pos + 1);
+
+            while (denominator > 0)
+            {
+                _MIDI_TimeBase += _MIDI_TimeBase;
+                denominator--;
+            }
+
+            _MIDI_TicksPerBeat = tabledivide32_noinline(_MIDI_Division * 4, _MIDI_TimeBase);
+            break;
+        }
+    }
+
+    Track->pos += length;
+}
+
+static int32_t _MIDI_InterpretControllerInfo(track *Track, int32_t TimeSet, int32_t channel, int32_t c1, int32_t c2)
+{
+    track *trackptr;
+    int32_t tracknum;
+    int32_t loopcount;
+
+    switch (c1)
+    {
+        case MIDI_MONO_MODE_ON :
+            Track->pos++;
+            break;
+
+        case MIDI_VOLUME :
+            if (!Track->EMIDI_VolumeChange)
+                _MIDI_SetChannelVolume(channel, c2);
+            break;
+
+        case EMIDI_INCLUDE_TRACK :
+        case EMIDI_EXCLUDE_TRACK :
+            break;
+
+        case EMIDI_PROGRAM_CHANGE :
+            if (Track->EMIDI_ProgramChange)
+                _MIDI_Funcs->ProgramChange(channel, c2 & 0x7f);
+            break;
+
+        case EMIDI_VOLUME_CHANGE :
+            if (Track->EMIDI_VolumeChange)
+                _MIDI_SetChannelVolume(channel, c2);
+            break;
+
+        case EMIDI_CONTEXT_START :
+            break;
+
+        case EMIDI_CONTEXT_END :
+            if ((Track->currentcontext == _MIDI_Context) || (_MIDI_Context < 0) ||
+                (Track->context[_MIDI_Context].pos == NULL))
+                break;
+
+            Track->currentcontext = _MIDI_Context;
+            Track->context[ 0 ].loopstart = Track->context[ _MIDI_Context ].loopstart;
+            Track->context[ 0 ].loopcount = Track->context[ _MIDI_Context ].loopcount;
+            Track->pos           = Track->context[ _MIDI_Context ].pos;
+            Track->RunningStatus = Track->context[ _MIDI_Context ].RunningStatus;
+
+            if (TimeSet)
+            {
+                break;
+            }
+
+            _MIDI_Time             = Track->context[ _MIDI_Context ].time;
+            _MIDI_FPSecondsPerTick = Track->context[ _MIDI_Context ].FPSecondsPerTick;
+            _MIDI_Tick             = Track->context[ _MIDI_Context ].tick;
+            _MIDI_Beat             = Track->context[ _MIDI_Context ].beat;
+            _MIDI_Measure          = Track->context[ _MIDI_Context ].measure;
+            _MIDI_BeatsPerMeasure  = Track->context[ _MIDI_Context ].BeatsPerMeasure;
+            _MIDI_TicksPerBeat     = Track->context[ _MIDI_Context ].TicksPerBeat;
+            _MIDI_TimeBase         = Track->context[ _MIDI_Context ].TimeBase;
+            TimeSet = TRUE;
+            break;
+
+        case EMIDI_LOOP_START :
+        case EMIDI_SONG_LOOP_START :
+            loopcount = (c2 == 0) ? EMIDI_INFINITE : c2;
+
+            if (c1 == EMIDI_SONG_LOOP_START)
+            {
+                trackptr = _MIDI_TrackPtr;
+                tracknum = _MIDI_NumTracks;
+            }
+            else
+            {
+                trackptr = Track;
+                tracknum = 1;
+            }
+
+            while (tracknum > 0)
+            {
+                trackptr->context[ 0 ].loopcount        = loopcount;
+                trackptr->context[ 0 ].pos              = trackptr->pos;
+                trackptr->context[ 0 ].loopstart        = trackptr->pos;
+                trackptr->context[ 0 ].RunningStatus    = trackptr->RunningStatus;
+                trackptr->context[ 0 ].active           = trackptr->active;
+                trackptr->context[ 0 ].delay            = trackptr->delay;
+                trackptr->context[ 0 ].time             = _MIDI_Time;
+                trackptr->context[ 0 ].FPSecondsPerTick = _MIDI_FPSecondsPerTick;
+                trackptr->context[ 0 ].tick             = _MIDI_Tick;
+                trackptr->context[ 0 ].beat             = _MIDI_Beat;
+                trackptr->context[ 0 ].measure          = _MIDI_Measure;
+                trackptr->context[ 0 ].BeatsPerMeasure  = _MIDI_BeatsPerMeasure;
+                trackptr->context[ 0 ].TicksPerBeat     = _MIDI_TicksPerBeat;
+                trackptr->context[ 0 ].TimeBase         = _MIDI_TimeBase;
+                trackptr++;
+                tracknum--;
+            }
+            break;
+
+        case EMIDI_LOOP_END :
+        case EMIDI_SONG_LOOP_END :
+            if ((c2 != EMIDI_END_LOOP_VALUE) || (Track->context[0].loopstart == NULL) || (Track->context[0].loopcount == 0))
+                break;
+
+            if (c1 == EMIDI_SONG_LOOP_END)
+            {
+                trackptr = _MIDI_TrackPtr;
+                tracknum = _MIDI_NumTracks;
+                _MIDI_ActiveTracks = 0;
+            }
+            else
+            {
+                trackptr = Track;
+                tracknum = 1;
+                _MIDI_ActiveTracks--;
+            }
+
+            while (tracknum > 0)
+            {
+                if (trackptr->context[ 0 ].loopcount != EMIDI_INFINITE)
+                {
+                    trackptr->context[ 0 ].loopcount--;
+                }
+
+                trackptr->pos           = trackptr->context[ 0 ].loopstart;
+                trackptr->RunningStatus = trackptr->context[ 0 ].RunningStatus;
+                trackptr->delay         = trackptr->context[ 0 ].delay;
+                trackptr->active        = trackptr->context[ 0 ].active;
+                if (trackptr->active)
+                {
+                    _MIDI_ActiveTracks++;
+                }
+
+                if (!TimeSet)
+                {
+                    _MIDI_Time             = trackptr->context[ 0 ].time;
+                    _MIDI_FPSecondsPerTick = trackptr->context[ 0 ].FPSecondsPerTick;
+                    _MIDI_Tick             = trackptr->context[ 0 ].tick;
+                    _MIDI_Beat             = trackptr->context[ 0 ].beat;
+                    _MIDI_Measure          = trackptr->context[ 0 ].measure;
+                    _MIDI_BeatsPerMeasure  = trackptr->context[ 0 ].BeatsPerMeasure;
+                    _MIDI_TicksPerBeat     = trackptr->context[ 0 ].TicksPerBeat;
+                    _MIDI_TimeBase         = trackptr->context[ 0 ].TimeBase;
+                    TimeSet = TRUE;
+                }
+
+                trackptr++;
+                tracknum--;
+            }
+            break;
+
+        default :
+            if (_MIDI_Funcs->ControlChange)
+                _MIDI_Funcs->ControlChange(channel, c1, c2);
+    }
+
+    return TimeSet;
+}
+
+static void _MIDI_ServiceRoutine(void)
+{
+    if (!_MIDI_SongActive)
+        return;
+
+    track *Track = _MIDI_TrackPtr;
+    int32_t tracknum = 0;
+    int32_t TimeSet = FALSE;
+    int32_t c1 = 0;
+    int32_t c2 = 0;
+
+    while (tracknum < _MIDI_NumTracks)
+    {
+        while ((Track->active) && (Track->delay == 0))
+        {
+            int32_t event;
+            GET_NEXT_EVENT(Track, event);
+
+            if (GET_MIDI_COMMAND(event) == MIDI_SPECIAL)
+            {
+                switch (event)
+                {
+                    case MIDI_SYSEX:
+                    case MIDI_SYSEX_CONTINUE: _MIDI_SysEx(Track); break;
+                    case MIDI_META_EVENT: _MIDI_MetaEvent(Track); break;
+                }
+
+                if (Track->active)
+                    Track->delay = _MIDI_ReadDelta(Track);
+                continue;
+            }
+
+            if (event & MIDI_RUNNING_STATUS)
+                Track->RunningStatus = event;
+            else
+            {
+                event = Track->RunningStatus;
+                Track->pos--;
+            }
+
+            int const channel = GET_MIDI_CHANNEL(event);
+            int const command = GET_MIDI_COMMAND(event);
+
+            if (_MIDI_CommandLengths[ command ] > 0)
+            {
+                GET_NEXT_EVENT(Track, c1);
+                if (_MIDI_CommandLengths[ command ] > 1)
+                    GET_NEXT_EVENT(Track, c2);
+            }
+
+            switch (command)
+            {
+                case MIDI_NOTE_OFF:
+                    if (_MIDI_Funcs->NoteOff)
+                        _MIDI_Funcs->NoteOff(channel, c1, c2);
+                    break;
+
+                case MIDI_NOTE_ON:
+                    if (_MIDI_Funcs->NoteOn)
+                        _MIDI_Funcs->NoteOn(channel, c1, c2);
+                    break;
+
+                case MIDI_POLY_AFTER_TCH:
+                    if (_MIDI_Funcs->PolyAftertouch)
+                        _MIDI_Funcs->PolyAftertouch(channel, c1, c2);
+                    break;
+
+                case MIDI_CONTROL_CHANGE:
+                    TimeSet = _MIDI_InterpretControllerInfo(Track, TimeSet, channel, c1, c2);
+                    break;
+
+                case MIDI_PROGRAM_CHANGE:
+                    if ((_MIDI_Funcs->ProgramChange) && (!Track->EMIDI_ProgramChange))
+                        _MIDI_Funcs->ProgramChange(channel, c1 & 0x7f);
+                    break;
+
+                case MIDI_AFTER_TOUCH:
+                    if (_MIDI_Funcs->ChannelAftertouch)
+                        _MIDI_Funcs->ChannelAftertouch(channel, c1);
+                    break;
+
+                case MIDI_PITCH_BEND:
+                    if (_MIDI_Funcs->PitchBend)
+                        _MIDI_Funcs->PitchBend(channel, c1, c2);
+                    break;
+
+                default: break;
+            }
+
+            Track->delay = _MIDI_ReadDelta(Track);
+        }
+
+        Track->delay--;
+        Track++;
+        tracknum++;
+
+        if (_MIDI_ActiveTracks == 0)
+        {
+            _MIDI_ResetTracks();
+            if (_MIDI_Loop)
+            {
+                tracknum = 0;
+                Track = _MIDI_TrackPtr;
+            }
+            else
+            {
+                _MIDI_SongActive = FALSE;
+                break;
+            }
+        }
+    }
+
+    _MIDI_AdvanceTick();
+    _MIDI_GlobalPositionInTicks++;
+}
+
+static int32_t _MIDI_SendControlChange(int32_t channel, int32_t c1, int32_t c2)
+{
+    if (_MIDI_Funcs == NULL || _MIDI_Funcs->ControlChange == NULL)
+        return MIDI_Error;
+
+    _MIDI_Funcs->ControlChange(channel, c1, c2);
+
+    return MIDI_Ok;
+}
+
+int32_t MIDI_AllNotesOff(void)
+{
+    for (bssize_t channel = 0; channel < NUM_MIDI_CHANNELS; channel++)
+    {
+        _MIDI_SendControlChange(channel, 0x40, 0);
+        _MIDI_SendControlChange(channel, MIDI_ALL_NOTES_OFF, 0);
+        _MIDI_SendControlChange(channel, 0x78, 0);
+    }
+
+    return MIDI_Ok;
+}
+
+static void _MIDI_SetChannelVolume(int32_t channel, int32_t volume)
+{
+    _MIDI_ChannelVolume[ channel ] = volume;
+
+    if (_MIDI_Funcs == NULL || _MIDI_Funcs->ControlChange == NULL)
+        return;
+
+    volume *= _MIDI_TotalVolume;
+    volume = tabledivide32_noinline(volume, MIDI_MaxVolume);
+
+    _MIDI_Funcs->ControlChange(channel, MIDI_VOLUME, volume);
+}
+
+static void _MIDI_SendChannelVolumes(void)
+{
+    for (bssize_t channel = 0; channel < NUM_MIDI_CHANNELS; channel++)
+        _MIDI_SetChannelVolume(channel, _MIDI_ChannelVolume[channel]);
+}
+
+int32_t MIDI_Reset(void)
+{
+    MIDI_AllNotesOff();
+
+    for (bssize_t channel = 0; channel < NUM_MIDI_CHANNELS; channel++)
+    {
+        _MIDI_SendControlChange(channel, MIDI_RESET_ALL_CONTROLLERS, 0);
+        _MIDI_SendControlChange(channel, MIDI_RPN_MSB, MIDI_PITCHBEND_MSB);
+        _MIDI_SendControlChange(channel, MIDI_RPN_LSB, MIDI_PITCHBEND_LSB);
+        _MIDI_SendControlChange(channel, MIDI_DATAENTRY_MSB, 2); /* Pitch Bend Sensitivity MSB */
+        _MIDI_SendControlChange(channel, MIDI_DATAENTRY_LSB, 0); /* Pitch Bend Sensitivity LSB */
+        _MIDI_ChannelVolume[ channel ] = GENMIDI_DefaultVolume;
+    }
+
+    _MIDI_SendChannelVolumes();
+
+    Reset = TRUE;
+
+    return MIDI_Ok;
+}
+
+int32_t MIDI_SetVolume(int32_t volume)
+{
+    if (_MIDI_Funcs == NULL)
+        return MIDI_NullMidiModule;
+
+    _MIDI_TotalVolume = max(0, min(MIDI_MaxVolume, volume));
+    _MIDI_SendChannelVolumes();
+
+    return MIDI_Ok;
+}
+
+int32_t MIDI_GetVolume(void) { return (_MIDI_Funcs == NULL) ? MIDI_NullMidiModule : _MIDI_TotalVolume; }
+
+void MIDI_SetLoopFlag(int32_t loopflag) { _MIDI_Loop = loopflag; }
+
+void MIDI_ContinueSong(void)
+{
+    if (!_MIDI_SongLoaded)
+        return;
+
+    _MIDI_SongActive = TRUE;
+    MPU_Unpause();
+}
+
+void MIDI_PauseSong(void)
+{
+    if (!_MIDI_SongLoaded)
+        return;
+
+    _MIDI_SongActive = FALSE;
+    MIDI_AllNotesOff();
+    MPU_Pause();
+}
+
+void MIDI_SetMidiFuncs(midifuncs *funcs) { _MIDI_Funcs = funcs; }
+
+void MIDI_StopSong(void)
+{
+    if (!_MIDI_SongLoaded)
+        return;
+
+    _MIDI_SongActive = FALSE;
+    _MIDI_SongLoaded = FALSE;
+
+    MPU_Reset();
+    MPU_Init(MUSIC_SoundDevice);
+
+    MIDI_Reset();
+    _MIDI_ResetTracks();
+
+    DO_FREE_AND_NULL(_MIDI_TrackPtr);
+
+    _MIDI_NumTracks    = 0;
+    _MIDI_TrackMemSize = 0;
+
+    _MIDI_TotalTime     = 0;
+    _MIDI_TotalTicks    = 0;
+    _MIDI_TotalBeats    = 0;
+    _MIDI_TotalMeasures = 0;
+
+}
+
+int32_t MIDI_PlaySong(char *song, int32_t loopflag)
+{
+    extern int32_t MV_MixRate;
+    int32_t    numtracks;
+    int32_t    format;
+    int32_t   headersize;
+    int32_t   tracklength;
+    track *CurrentTrack;
+    char *ptr;
+
+    if (_MIDI_Funcs == NULL)
+        return MIDI_NullMidiModule;
+
+    if (B_UNBUF32(song) != MIDI_HEADER_SIGNATURE)
+        return MIDI_InvalidMidiFile;
+
+    song += 4;
+    headersize      = _MIDI_ReadNumber(song, 4);
+    song += 4;
+    format          = _MIDI_ReadNumber(song, 2);
+    int32_t My_MIDI_NumTracks = _MIDI_ReadNumber(song + 2, 2);
+    int32_t My_MIDI_Division  = _MIDI_ReadNumber(song + 4, 2);
+    if (My_MIDI_Division < 0)
+    {
+        // If a SMPTE time division is given, just set to 96 so no errors occur
+        My_MIDI_Division = 96;
+    }
+
+    if (format > MAX_FORMAT)
+        return MIDI_UnknownMidiFormat;
+
+    ptr = song + headersize;
+
+    if (My_MIDI_NumTracks == 0)
+        return MIDI_NoTracks;
+
+    int32_t My_MIDI_TrackMemSize = My_MIDI_NumTracks  * sizeof(track);
+    track * My_MIDI_TrackPtr = (track *)Xmalloc(My_MIDI_TrackMemSize);
+
+    CurrentTrack = My_MIDI_TrackPtr;
+    numtracks    = My_MIDI_NumTracks;
+
+    while (numtracks--)
+    {
+        if (B_UNBUF32(ptr) != MIDI_TRACK_SIGNATURE)
+        {
+            DO_FREE_AND_NULL(My_MIDI_TrackPtr);
+
+            My_MIDI_TrackMemSize = 0;
+
+            return MIDI_InvalidTrack;
+        }
+
+        tracklength = _MIDI_ReadNumber(ptr + 4, 4);
+        ptr += 8;
+        CurrentTrack->start = ptr;
+        ptr += tracklength;
+        CurrentTrack++;
+    }
+
+    // at this point we know song load is successful
+
+    if (_MIDI_SongLoaded)
+        MIDI_StopSong();
+
+	MPU_Init(0/*MUSIC_SoundDevice*/);
+
+    _MIDI_Loop = loopflag;
+    _MIDI_NumTracks = My_MIDI_NumTracks;
+    _MIDI_Division = My_MIDI_Division;
+    _MIDI_TrackMemSize = My_MIDI_TrackMemSize;
+    _MIDI_TrackPtr = My_MIDI_TrackPtr;
+
+    _MIDI_InitEMIDI();
+    _MIDI_ResetTracks();
+
+    if (!Reset)
+        MIDI_Reset();
+
+    Reset = FALSE;
+
+    MIDI_SetDivision(_MIDI_Division);
+
+    _MIDI_SongLoaded = TRUE;
+    _MIDI_SongActive = TRUE;
+
+    while (_MPU_BuffersWaiting < 4) _MIDI_ServiceRoutine();
+    MPU_BeginPlayback();
+
+    return MIDI_Ok;
+}
+
+void MIDI_SetTempo(int32_t tempo)
+{
+    int32_t tickspersecond;
+
+    MIDI_Tempo = tempo;
+    tickspersecond = ((tempo) * _MIDI_Division)/60;
+    _MIDI_FPSecondsPerTick = tabledivide32_noinline(1 << TIME_PRECISION, tickspersecond);
+    MPU_SetTempo(tempo);
+}
+
+void MIDI_SetDivision(int32_t division)
+{
+    MPU_SetDivision(division);
+}
+
+int32_t MIDI_GetTempo(void) { return MIDI_Tempo; }
+
+static void _MIDI_InitEMIDI(void)
+{
+    int32_t type = EMIDI_GeneralMIDI;
+
+    _MIDI_ResetTracks();
+
+    _MIDI_TotalTime     = 0;
+    _MIDI_TotalTicks    = 0;
+    _MIDI_TotalBeats    = 0;
+    _MIDI_TotalMeasures = 0;
+
+    track *Track = _MIDI_TrackPtr;
+    int32_t tracknum = 0;
+
+    while ((tracknum < _MIDI_NumTracks) && (Track != NULL))
+    {
+        _MIDI_Tick = 0;
+        _MIDI_Beat = 1;
+        _MIDI_Measure = 1;
+        _MIDI_Time = 0;
+        _MIDI_BeatsPerMeasure = 4;
+        _MIDI_TicksPerBeat = _MIDI_Division;
+        _MIDI_TimeBase = 4;
+
+        _MIDI_PositionInTicks = 0;
+        _MIDI_ActiveTracks    = 0;
+        _MIDI_Context         = -1;
+
+        Track->RunningStatus = 0;
+        Track->active        = TRUE;
+
+        Track->EMIDI_ProgramChange = FALSE;
+        Track->EMIDI_VolumeChange  = FALSE;
+        Track->EMIDI_IncludeTrack  = TRUE;
+
+        memset(Track->context, 0, sizeof(Track->context));
+
+        while (Track->delay > 0)
+        {
+            _MIDI_AdvanceTick();
+            Track->delay--;
+        }
+
+        int32_t IncludeFound = FALSE;
+
+        while (Track->active)
+        {
+            int32_t event;
+
+            GET_NEXT_EVENT(Track, event);
+
+            if (GET_MIDI_COMMAND(event) == MIDI_SPECIAL)
+            {
+                switch (event)
+                {
+                    case MIDI_SYSEX:
+                    case MIDI_SYSEX_CONTINUE: _MIDI_SysEx(Track); break;
+                    case MIDI_META_EVENT: _MIDI_MetaEvent(Track); break;
+                }
+
+                if (Track->active)
+                {
+                    Track->delay = _MIDI_ReadDelta(Track);
+                    while (Track->delay > 0)
+                    {
+                        _MIDI_AdvanceTick();
+                        Track->delay--;
+                    }
+                }
+
+                continue;
+            }
+
+            if (event & MIDI_RUNNING_STATUS)
+                Track->RunningStatus = event;
+            else
+            {
+                event = Track->RunningStatus;
+                Track->pos--;
+            }
+
+//            channel = GET_MIDI_CHANNEL(event);
+            int const command = GET_MIDI_COMMAND(event);
+            int length = _MIDI_CommandLengths[ command ];
+
+            if (command == MIDI_CONTROL_CHANGE)
+            {
+                if (*Track->pos == MIDI_MONO_MODE_ON)
+                    length++;
+
+                int32_t c1, c2;
+                GET_NEXT_EVENT(Track, c1);
+                GET_NEXT_EVENT(Track, c2);
+                length -= 2;
+
+                switch (c1)
+                {
+                case EMIDI_LOOP_START :
+                case EMIDI_SONG_LOOP_START :
+                    Track->context[ 0 ].loopcount        = (c2 == 0) ? EMIDI_INFINITE : c2;
+                    Track->context[ 0 ].pos              = Track->pos;
+                    Track->context[ 0 ].loopstart        = Track->pos;
+                    Track->context[ 0 ].RunningStatus    = Track->RunningStatus;
+                    Track->context[ 0 ].time             = _MIDI_Time;
+                    Track->context[ 0 ].FPSecondsPerTick = _MIDI_FPSecondsPerTick;
+                    Track->context[ 0 ].tick             = _MIDI_Tick;
+                    Track->context[ 0 ].beat             = _MIDI_Beat;
+                    Track->context[ 0 ].measure          = _MIDI_Measure;
+                    Track->context[ 0 ].BeatsPerMeasure  = _MIDI_BeatsPerMeasure;
+                    Track->context[ 0 ].TicksPerBeat     = _MIDI_TicksPerBeat;
+                    Track->context[ 0 ].TimeBase         = _MIDI_TimeBase;
+                    break;
+
+                case EMIDI_LOOP_END :
+                case EMIDI_SONG_LOOP_END :
+                    if (c2 == EMIDI_END_LOOP_VALUE)
+                    {
+                        Track->context[ 0 ].loopstart = NULL;
+                        Track->context[ 0 ].loopcount = 0;
+                    }
+                    break;
+
+                case EMIDI_INCLUDE_TRACK :
+                    if (EMIDI_AffectsCurrentCard(c2, type))
+                    {
+                        //printf( "Include track %d on card %d\n", tracknum, c2 );
+                        IncludeFound = TRUE;
+                        Track->EMIDI_IncludeTrack = TRUE;
+                    }
+                    else if (!IncludeFound)
+                    {
+                        //printf( "Track excluded %d on card %d\n", tracknum, c2 );
+                        IncludeFound = TRUE;
+                        Track->EMIDI_IncludeTrack = FALSE;
+                    }
+                    break;
+
+                case EMIDI_EXCLUDE_TRACK :
+                    if (EMIDI_AffectsCurrentCard(c2, type))
+                    {
+                        //printf( "Exclude track %d on card %d\n", tracknum, c2 );
+                        Track->EMIDI_IncludeTrack = FALSE;
+                    }
+                    break;
+
+                case EMIDI_PROGRAM_CHANGE :
+                    if (!Track->EMIDI_ProgramChange)
+                        //printf( "Program change on track %d\n", tracknum );
+                        Track->EMIDI_ProgramChange = TRUE;
+                    break;
+
+                case EMIDI_VOLUME_CHANGE :
+                    if (!Track->EMIDI_VolumeChange)
+                        //printf( "Volume change on track %d\n", tracknum );
+                        Track->EMIDI_VolumeChange = TRUE;
+                    break;
+
+                case EMIDI_CONTEXT_START :
+                    if ((c2 > 0) && (c2 < EMIDI_NUM_CONTEXTS))
+                    {
+                        Track->context[ c2 ].pos              = Track->pos;
+                        Track->context[ c2 ].loopstart        = Track->context[ 0 ].loopstart;
+                        Track->context[ c2 ].loopcount        = Track->context[ 0 ].loopcount;
+                        Track->context[ c2 ].RunningStatus    = Track->RunningStatus;
+                        Track->context[ c2 ].time             = _MIDI_Time;
+                        Track->context[ c2 ].FPSecondsPerTick = _MIDI_FPSecondsPerTick;
+                        Track->context[ c2 ].tick             = _MIDI_Tick;
+                        Track->context[ c2 ].beat             = _MIDI_Beat;
+                        Track->context[ c2 ].measure          = _MIDI_Measure;
+                        Track->context[ c2 ].BeatsPerMeasure  = _MIDI_BeatsPerMeasure;
+                        Track->context[ c2 ].TicksPerBeat     = _MIDI_TicksPerBeat;
+                        Track->context[ c2 ].TimeBase         = _MIDI_TimeBase;
+                    }
+                    break;
+
+                case EMIDI_CONTEXT_END :
+                    break;
+                }
+            }
+
+            Track->pos += length;
+            Track->delay = _MIDI_ReadDelta(Track);
+
+            while (Track->delay > 0)
+            {
+                _MIDI_AdvanceTick();
+                Track->delay--;
+            }
+        }
+
+        _MIDI_TotalTime = max(_MIDI_TotalTime, _MIDI_Time);
+        if (RELATIVE_BEAT(_MIDI_Measure, _MIDI_Beat, _MIDI_Tick) >
+            RELATIVE_BEAT(_MIDI_TotalMeasures, _MIDI_TotalBeats, _MIDI_TotalTicks))
+        {
+            _MIDI_TotalTicks = _MIDI_Tick;
+            _MIDI_TotalBeats = _MIDI_Beat;
+            _MIDI_TotalMeasures = _MIDI_Measure;
+        }
+
+        Track++;
+        tracknum++;
+    }
+
+    _MIDI_ResetTracks();
+}
+
+void MIDI_UpdateMusic(void)
+{
+    if (!_MIDI_SongLoaded || !_MIDI_SongActive) return;
+    while (_MPU_BuffersWaiting < 4) _MIDI_ServiceRoutine();
+}
+
diff --git a/source/blood/src/midi.h b/source/blood/src/midi.h
new file mode 100644
index 000000000..86f1ecbdd
--- /dev/null
+++ b/source/blood/src/midi.h
@@ -0,0 +1,89 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#ifndef __MIDI_H
+#define __MIDI_H
+
+enum MIDI_Errors
+   {
+   MIDI_Warning = -2,
+   MIDI_Error   = -1,
+   MIDI_Ok      = 0,
+   MIDI_NullMidiModule,
+   MIDI_InvalidMidiFile,
+   MIDI_UnknownMidiFormat,
+   MIDI_NoTracks,
+   MIDI_InvalidTrack,
+   MIDI_NoMemory,
+   MIDI_DPMI_Error
+   };
+
+
+#define MIDI_PASS_THROUGH 1
+#define MIDI_DONT_PLAY    0
+
+#define MIDI_MaxVolume 255
+
+extern char MIDI_PatchMap[ 128 ];
+
+typedef struct
+{
+    void (*NoteOff)(int32_t channel, int32_t key, int32_t velocity);
+    void (*NoteOn)(int32_t channel, int32_t key, int32_t velocity);
+    void (*PolyAftertouch)(int32_t channel, int32_t key, int32_t pressure);
+    void (*ControlChange)(int32_t channel, int32_t number, int32_t value);
+    void (*ProgramChange)(int32_t channel, int32_t program);
+    void (*ChannelAftertouch)(int32_t channel, int32_t pressure);
+    void (*PitchBend)(int32_t channel, int32_t lsb, int32_t msb);
+    void (*FinishBuffer)(void);
+} midifuncs;
+
+void MIDI_RerouteMidiChannel( int32_t channel, int32_t ( *function )( int32_t event, int32_t c1, int32_t c2 ) );
+int32_t  MIDI_AllNotesOff( void );
+void MIDI_SetUserChannelVolume( int32_t channel, int32_t volume );
+void MIDI_ResetUserChannelVolume( void );
+int32_t  MIDI_Reset( void );
+int32_t  MIDI_SetVolume( int32_t volume );
+int32_t  MIDI_GetVolume( void );
+void MIDI_SetMidiFuncs( midifuncs *funcs );
+void MIDI_SetContext( int32_t context );
+int32_t  MIDI_GetContext( void );
+void MIDI_SetLoopFlag( int32_t loopflag );
+void MIDI_ContinueSong( void );
+void MIDI_PauseSong( void );
+int32_t  MIDI_SongPlaying( void );
+void MIDI_StopSong( void );
+int32_t  MIDI_PlaySong( char *song, int32_t loopflag );
+void MIDI_SetTempo( int32_t tempo );
+int32_t  MIDI_GetTempo( void );
+void MIDI_SetSongTick( uint32_t PositionInTicks );
+void MIDI_SetSongTime( uint32_t milliseconds );
+void MIDI_SetSongPosition( int32_t measure, int32_t beat, int32_t tick );
+void MIDI_GetSongPosition( songposition *pos );
+void MIDI_GetSongLength( songposition *pos );
+void MIDI_LoadTimbres( void );
+void MIDI_MusicMix( char *buffer, int length );
+void MIDI_UpdateMusic(void);
+void MIDI_SetDivision( int32_t division );
+
+#endif
diff --git a/source/blood/src/mirrors.cpp b/source/blood/src/mirrors.cpp
new file mode 100644
index 000000000..8d178a8ec
--- /dev/null
+++ b/source/blood/src/mirrors.cpp
@@ -0,0 +1,488 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#ifdef POLYMER
+#include "polymer.h"
+#endif
+#include "common_game.h"
+#include "blood.h"
+#include "db.h"
+#include "gameutil.h"
+#include "loadsave.h"
+#include "player.h"
+#include "trig.h"
+#include "view.h"
+#include "warp.h"
+
+int mirrorcnt, mirrorsector, mirrorwall[4];
+
+typedef struct
+{
+    int at0;
+    int at4;
+    int at8;
+    int atc;
+    int at10;
+    int at14;
+} MIRROR;
+
+MIRROR mirror[16];
+
+#ifdef POLYMER
+void PolymerRORCallback(int16_t sectnum, int16_t wallnum, int8_t rorstat, int16_t* msectnum, int32_t* gx, int32_t* gy, int32_t* gz)
+{
+    UNREFERENCED_PARAMETER(wallnum);
+    int nMirror;
+    switch (rorstat)
+    {
+    case 1:
+        nMirror = sector[sectnum].ceilingpicnum-4080;
+        *msectnum = mirror[nMirror].at4;
+        *gx += mirror[nMirror].at8;
+        *gy += mirror[nMirror].atc;
+        *gz += mirror[nMirror].at10;
+        break;
+    case 2:
+        nMirror = sector[sectnum].floorpicnum-4080;
+        *msectnum = mirror[nMirror].at4;
+        *gx += mirror[nMirror].at8;
+        *gy += mirror[nMirror].atc;
+        *gz += mirror[nMirror].at10;
+        break;
+    }
+}
+#endif
+
+void InitMirrors(void)
+{
+
+#ifdef USE_OPENGL
+    r_rortexture = 4080;
+    r_rortexturerange = 16;
+#ifdef  POLYMER
+    polymer_setrorcallback(PolymerRORCallback);
+#endif //  POLYMER
+
+#endif
+    mirrorcnt = 0;
+    tilesiz[504].x = 0;
+    tilesiz[504].y = 0;
+    tileDelete(504);
+    
+    for(int i = 0; i < 16; i++)
+        tilesiz[4080+i].x = 0, tilesiz[4080+i].y = 0;
+    for (int i = numwalls - 1; i >= 0; i--)
+    {
+        if (mirrorcnt == 16)
+            break;
+        int nTile = 4080+mirrorcnt;
+        if (wall[i].overpicnum == 504)
+        {
+            if (wall[i].extra > 0 && wall[i].lotag == 501)
+            {
+                wall[i].overpicnum = nTile;
+                mirror[mirrorcnt].at14 = i;
+                mirror[mirrorcnt].at0 = 0;
+                wall[i].cstat |= 32;
+                int tmp = xwall[wall[i].extra].data;
+                int j;
+                for (j = numwalls - 1; j >= 0; j--)
+                {
+                    if (j == i)
+                        continue;
+                    if (wall[j].extra > 0 && wall[j].lotag == 501)
+                    {
+                        if (tmp != xwall[wall[j].extra].data)
+                            continue;
+                        wall[i].hitag = j;
+                        wall[j].hitag = i;
+                        mirror[mirrorcnt].at4 = j;
+                        break;
+                    }
+                }
+                if (j < 0)
+                    ThrowError("wall[%d] has no matching wall link! (data=%d)\n", i, tmp);
+                mirrorcnt++;
+            }
+            continue;
+        }
+        if (wall[i].picnum == 504)
+        {
+            mirror[mirrorcnt].at4 = i;
+            mirror[mirrorcnt].at14 = i;
+            wall[i].picnum = nTile;
+            mirror[mirrorcnt].at0 = 0;
+            wall[i].cstat |= 32;
+            mirrorcnt++;
+            continue;
+        }
+    }
+    for (int i = numsectors - 1; i >= 0; i--)
+    {
+        if (mirrorcnt >= 15)
+            break;
+
+        if (sector[i].floorpicnum == 504)
+        {
+            int nLink = gUpperLink[i];
+            if (nLink < 0)
+                continue;
+            int nLink2 = sprite[nLink].owner & 0xfff;
+            int j = sprite[nLink2].sectnum;
+            if (sector[j].ceilingpicnum != 504)
+                ThrowError("Lower link sector %d doesn't have mirror picnum\n", j);
+            mirror[mirrorcnt].at0 = 2;
+            mirror[mirrorcnt].at8 = sprite[nLink2].x-sprite[nLink].x;
+            mirror[mirrorcnt].atc = sprite[nLink2].y-sprite[nLink].y;
+            mirror[mirrorcnt].at10 = sprite[nLink2].z-sprite[nLink].z;
+            mirror[mirrorcnt].at14 = i;
+            mirror[mirrorcnt].at4 = j;
+            sector[i].floorpicnum = 4080+mirrorcnt;
+            mirrorcnt++;
+            mirror[mirrorcnt].at0 = 1;
+            mirror[mirrorcnt].at8 = sprite[nLink].x-sprite[nLink2].x;
+            mirror[mirrorcnt].atc = sprite[nLink].y-sprite[nLink2].y;
+            mirror[mirrorcnt].at10 = sprite[nLink].z-sprite[nLink2].z;
+            mirror[mirrorcnt].at14 = j;
+            mirror[mirrorcnt].at4 = i;
+            sector[j].ceilingpicnum = 4080+mirrorcnt;
+            mirrorcnt++;
+        }
+    }
+    mirrorsector = numsectors;
+    for (int i = 0; i < 4; i++)
+    {
+        mirrorwall[i] = numwalls+i;
+        wall[mirrorwall[i]].picnum = 504;
+        wall[mirrorwall[i]].overpicnum = 504;
+        wall[mirrorwall[i]].cstat = 0;
+        wall[mirrorwall[i]].nextsector = -1;
+        wall[mirrorwall[i]].nextwall = -1;
+        wall[mirrorwall[i]].point2 = numwalls;
+    }
+    wall[mirrorwall[3]].point2 = mirrorwall[0];
+    sector[mirrorsector].ceilingpicnum = 504;
+    sector[mirrorsector].floorpicnum = 504;
+    sector[mirrorsector].wallptr = mirrorwall[0];
+    sector[mirrorsector].wallnum = 4;
+}
+
+void TranslateMirrorColors(int nShade, int nPalette)
+{
+    if (videoGetRenderMode() != REND_CLASSIC)
+        return;
+    videoBeginDrawing();
+    nShade = ClipRange(nShade, 0, 63);
+    char *pMap = palookup[nPalette] + (nShade<<8);
+    extern intptr_t frameplace;
+    char *pFrame = (char*)frameplace;
+    unsigned int nPixels = xdim*ydim;
+    for (unsigned int i = 0; i < nPixels; i++, pFrame++)
+    {
+        *pFrame = pMap[*pFrame];
+    }
+    videoEndDrawing();
+}
+
+void sub_5571C(char mode)
+{
+    for (int i = mirrorcnt-1; i >= 0; i--)
+    {
+        int nTile = 4080+i;
+        if (TestBitString(gotpic, nTile))
+        {
+            switch (mirror[i].at0)
+            {
+                case 1:
+                    if (mode)
+                        sector[mirror[i].at14].ceilingstat |= 1;
+                    else
+                        sector[mirror[i].at14].ceilingstat &= ~1;
+                    break;
+                case 2:
+                    if (mode)
+                        sector[mirror[i].at14].floorstat |= 1;
+                    else
+                        sector[mirror[i].at14].floorstat &= ~1;
+                    break;
+            }
+        }
+    }
+}
+
+void sub_557C4(int x, int y, int interpolation)
+{
+    if (spritesortcnt == 0) return;
+    int nViewSprites = spritesortcnt-1;
+    for (int nTSprite = nViewSprites; nTSprite >= 0; nTSprite--)
+    {
+        uspritetype *pTSprite = &tsprite[nTSprite];
+        pTSprite->xrepeat = pTSprite->yrepeat = 0;
+    }
+    for (int i = mirrorcnt-1; i >= 0; i--)
+    {
+        int nTile = 4080+i;
+        if (TestBitString(gotpic, nTile))
+        {
+            if (mirror[i].at0 == 1 || mirror[i].at0 == 2)
+            {
+                int nSector = mirror[i].at4;
+                int nSector2 = mirror[i].at14;
+                for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+                {
+                    spritetype *pSprite = &sprite[nSprite];
+                    if (pSprite == gView->pSprite)
+                        continue;
+                    int top, bottom;
+                    GetSpriteExtents(pSprite, &top, &bottom);
+                    int zCeil, zFloor;
+                    getzsofslope(nSector, pSprite->x, pSprite->y, &zCeil, &zFloor);
+                    if (pSprite->statnum == 6 && (top < zCeil || bottom > zFloor))
+                    {
+                        int j = i;
+                        if (mirror[i].at0 == 2)
+                            j++;
+                        else
+                            j--;
+                        int dx = mirror[j].at8;
+                        int dy = mirror[j].atc;
+                        int dz = mirror[j].at10;
+                        uspritetype *pTSprite = &tsprite[spritesortcnt];
+                        memset(pTSprite, 0, sizeof(uspritetype));
+                        pTSprite->type = pSprite->type;
+                        pTSprite->index = pSprite->index;
+                        pTSprite->sectnum = nSector2;
+                        pTSprite->x = pSprite->x+dx;
+                        pTSprite->y = pSprite->y+dy;
+                        pTSprite->z = pSprite->z+dz;
+                        pTSprite->ang = pSprite->ang;
+                        pTSprite->picnum = pSprite->picnum;
+                        pTSprite->shade = pSprite->shade;
+                        pTSprite->pal = pSprite->pal;
+                        pTSprite->xrepeat = pSprite->xrepeat;
+                        pTSprite->yrepeat = pSprite->yrepeat;
+                        pTSprite->xoffset = pSprite->xoffset;
+                        pTSprite->yoffset = pSprite->yoffset;
+                        pTSprite->cstat = pSprite->cstat;
+                        pTSprite->statnum = 0;
+                        pTSprite->owner = pSprite->index;
+                        pTSprite->extra = pSprite->extra;
+                        pTSprite->hitag = pSprite->hitag|0x200;
+                        LOCATION *pLocation = &gPrevSpriteLoc[pSprite->index];
+                        pTSprite->x = dx+interpolate(pLocation->x, pSprite->x, interpolation);
+                        pTSprite->y = dy+interpolate(pLocation->y, pSprite->y, interpolation);
+                        pTSprite->z = dz+interpolate(pLocation->z, pSprite->z, interpolation);
+                        pTSprite->ang = pLocation->ang+mulscale16(((pSprite->ang-pLocation->ang+1024)&2047)-1024,interpolation);
+                        spritesortcnt++;
+                    }
+                }
+            }
+        }
+    }
+    for (int nTSprite = spritesortcnt-1; nTSprite >= nViewSprites; nTSprite--)
+    {
+        uspritetype *pTSprite = &tsprite[nTSprite];
+        int nAnim = 0;
+        switch (picanm[pTSprite->picnum].extra&7)
+        {
+        case 1:
+        {
+            int dX = x - pTSprite->x;
+            int dY = y - pTSprite->y;
+            RotateVector(&dX, &dY, 128 - pTSprite->ang);
+            nAnim = GetOctant(dX, dY);
+            if (nAnim <= 4)
+            {
+                pTSprite->cstat &= ~4;
+            }
+            else
+            {
+                nAnim = 8 - nAnim;
+                pTSprite->cstat |= 4;
+            }
+            break;
+        }
+        case 2:
+        {
+            int dX = x - pTSprite->x;
+            int dY = y - pTSprite->y;
+            RotateVector(&dX, &dY, 128 - pTSprite->ang);
+            nAnim = GetOctant(dX, dY);
+            break;
+        }
+        }
+        while (nAnim > 0)
+        {
+            pTSprite->picnum += picanm[pTSprite->picnum].num+1;
+            nAnim--;
+        }
+    }
+}
+
+void DrawMirrors(int x, int y, int z, fix16_t a, fix16_t horiz)
+{
+    if (videoGetRenderMode() == REND_POLYMER)
+        return;
+    for (int i = mirrorcnt - 1; i >= 0; i--)
+    {
+        int nTile = 4080+i;
+        if (TestBitString(gotpic, nTile))
+        {
+            ClearBitString(gotpic, nTile);
+            switch (mirror[i].at0)
+            {
+            case 0:
+            {
+                int nWall = mirror[i].at4;
+                int nSector = sectorofwall(nWall);
+                walltype *pWall = &wall[nWall];
+                int nNextWall = pWall->nextwall;
+                int nNextSector = pWall->nextsector;
+                pWall->nextwall = mirrorwall[0];
+                pWall->nextsector = mirrorsector;
+                wall[mirrorwall[0]].nextwall = nWall;
+                wall[mirrorwall[0]].nextsector = nSector;
+                wall[mirrorwall[0]].x = wall[pWall->point2].x;
+                wall[mirrorwall[0]].y = wall[pWall->point2].y;
+                wall[mirrorwall[1]].x = pWall->x;
+                wall[mirrorwall[1]].y = pWall->y;
+                wall[mirrorwall[2]].x = wall[mirrorwall[1]].x+(wall[mirrorwall[1]].x-wall[mirrorwall[0]].x)*16;
+                wall[mirrorwall[2]].y = wall[mirrorwall[1]].y+(wall[mirrorwall[1]].y-wall[mirrorwall[0]].y)*16;
+                wall[mirrorwall[3]].x = wall[mirrorwall[0]].x+(wall[mirrorwall[0]].x-wall[mirrorwall[1]].x)*16;
+                wall[mirrorwall[3]].y = wall[mirrorwall[0]].y+(wall[mirrorwall[0]].y-wall[mirrorwall[1]].y)*16;
+                sector[mirrorsector].floorz = sector[nSector].floorz;
+                sector[mirrorsector].ceilingz = sector[nSector].ceilingz;
+                int cx, cy, ca;
+                if (pWall->lotag == 501)
+                {
+                     cx = x - (wall[pWall->hitag].x-wall[pWall->point2].x);
+                     cy = y - (wall[pWall->hitag].y-wall[pWall->point2].y);
+                     ca = a;
+                }
+                else
+                {
+                    //renderPrepareMirror(x,y, fix16_from_int(a),nWall,&cx,&cy,&ca);
+                    renderPrepareMirrorOld(x,y,z,a,horiz,nWall,mirrorsector,&cx,&cy,&ca);
+                }
+                renderDrawRoomsQ16(cx, cy, z, ca,horiz,mirrorsector|MAXSECTORS);
+                viewProcessSprites(cx,cy,z);
+                renderDrawMasks();
+                if (pWall->lotag != 501)
+                    renderCompleteMirrorOld();
+                if (wall[nWall].pal != 0 || wall[nWall].shade != 0)
+                    TranslateMirrorColors(wall[nWall].shade, wall[nWall].pal);
+                pWall->nextwall = nNextWall;
+                pWall->nextsector = nNextSector;
+                return;
+            }
+            case 1:
+            {
+#ifdef USE_OPENGL
+                r_rorphase = 1;
+#endif
+                int nSector = mirror[i].at4;
+                renderDrawRoomsQ16(x+mirror[i].at8, y+mirror[i].atc, z+mirror[i].at10, a, horiz, nSector|MAXSECTORS);
+                viewProcessSprites(x+mirror[i].at8, y+mirror[i].atc, z+mirror[i].at10);
+                short fstat = sector[nSector].floorstat;
+                sector[nSector].floorstat |= 1;
+                renderDrawMasks();
+                sector[nSector].floorstat = fstat;
+                for (int i = 0; i < 16; i++)
+                    ClearBitString(gotpic, 4080+i);
+#ifdef USE_OPENGL
+                r_rorphase = 0;
+#endif
+                return;
+            }
+            case 2:
+            {
+#ifdef USE_OPENGL
+                r_rorphase = 1;
+#endif
+                int nSector = mirror[i].at4;
+                renderDrawRoomsQ16(x+mirror[i].at8, y+mirror[i].atc, z+mirror[i].at10, a, horiz, nSector|MAXSECTORS);
+                viewProcessSprites(x+mirror[i].at8, y+mirror[i].atc, z+mirror[i].at10);
+                short cstat = sector[nSector].ceilingstat;
+                sector[nSector].ceilingstat |= 1;
+                renderDrawMasks();
+                sector[nSector].ceilingstat = cstat;
+                for (int i = 0; i < 16; i++)
+                    ClearBitString(gotpic, 4080+i);
+#ifdef USE_OPENGL
+                r_rorphase = 0;
+#endif
+                return;
+            }
+            }
+        }
+    }
+}
+
+class MirrorLoadSave : public LoadSave {
+public:
+    void Load(void);
+    void Save(void);
+};
+
+static MirrorLoadSave *myLoadSave;
+
+void MirrorLoadSave::Load(void)
+{
+    Read(&mirrorcnt,sizeof(mirrorcnt));
+    Read(&mirrorsector,sizeof(mirrorsector));
+    Read(mirror, sizeof(mirror));
+    Read(mirrorwall, sizeof(mirrorwall));
+    tilesiz[504].x = 0;
+    tilesiz[504].y = 0;
+
+    for (int i = 0; i < 16; i++)
+        tilesiz[4080 + i].x = 0, tilesiz[4080 + i].y = 0;
+    for (int i = 0; i < 4; i++)
+    {
+        wall[mirrorwall[i]].picnum = 504;
+        wall[mirrorwall[i]].overpicnum = 504;
+        wall[mirrorwall[i]].point2 = numwalls;
+        wall[mirrorwall[i]].cstat = 0;
+        wall[mirrorwall[i]].nextsector = -1;
+        wall[mirrorwall[i]].nextwall = -1;
+    }
+    wall[mirrorwall[3]].point2 = mirrorwall[0];
+    sector[mirrorsector].ceilingpicnum = 504;
+    sector[mirrorsector].floorpicnum = 504;
+    sector[mirrorsector].wallptr = mirrorwall[0];
+    sector[mirrorsector].wallnum = 4;
+}
+
+void MirrorLoadSave::Save(void)
+{
+    Write(&mirrorcnt,sizeof(mirrorcnt));
+    Write(&mirrorsector,sizeof(mirrorsector));
+    Write(mirror, sizeof(mirror));
+    Write(mirrorwall, sizeof(mirrorwall));
+}
+
+void MirrorLoadSaveConstruct(void)
+{
+    myLoadSave = new MirrorLoadSave();
+}
diff --git a/source/blood/src/mirrors.h b/source/blood/src/mirrors.h
new file mode 100644
index 000000000..4300dfe7c
--- /dev/null
+++ b/source/blood/src/mirrors.h
@@ -0,0 +1,28 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "fix16.h"
+void InitMirrors(void);
+void sub_5571C(char mode);
+void sub_557C4(int x, int y, int interpolation);
+void DrawMirrors(int x, int y, int z, fix16_t a, fix16_t horiz);
diff --git a/source/blood/src/misc.cpp b/source/blood/src/misc.cpp
new file mode 100644
index 000000000..cc3e62503
--- /dev/null
+++ b/source/blood/src/misc.cpp
@@ -0,0 +1,141 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <string.h>
+#include "common_game.h"
+
+#include "misc.h"
+
+void *ResReadLine(char *buffer, unsigned int nBytes, void **pRes)
+{
+    unsigned int i;
+    char ch;
+    if (!pRes || !*pRes || *((char*)*pRes) == 0)
+        return NULL;
+    for (i = 0; i < nBytes; i++)
+    {
+        ch = *((char*)*pRes);
+        if(ch == 0 || ch == '\n')
+            break;
+        buffer[i] = ch;
+        *pRes = ((char*)*pRes)+1;
+    }
+    if (*((char*)*pRes) == '\n' && i < nBytes)
+    {
+        ch = *((char*)*pRes);
+        buffer[i] = ch;
+        *pRes = ((char*)*pRes)+1;
+        i++;
+    }
+    else
+    {
+        while (true)
+        {
+            ch = *((char*)*pRes);
+            if (ch == 0 || ch == '\n')
+                break;
+            *pRes = ((char*)*pRes)+1;
+        }
+        if (*((char*)*pRes) == '\n')
+            *pRes = ((char*)*pRes)+1;
+    }
+    if (i < nBytes)
+        buffer[i] = 0;
+    return *pRes;
+}
+
+bool FileRead(FILE *handle, void *buffer, unsigned int size)
+{
+    return fread(buffer, 1, size, handle) == size;
+}
+
+bool FileWrite(FILE *handle, void *buffer, unsigned int size)
+{
+    return fwrite(buffer, 1, size, handle) == size;
+}
+
+bool FileLoad(const char *name, void *buffer, unsigned int size)
+{
+    dassert(buffer != NULL);
+
+    FILE *handle = fopen(name, "rb");
+    if (!handle)
+        return false;
+
+    unsigned int nread = fread(buffer, 1, size, handle);
+    fclose(handle);
+    return nread == size;
+}
+
+int FileLength(FILE *handle)
+{
+    if (!handle)
+        return 0;
+    int nPos = ftell(handle);
+    fseek(handle, 0, SEEK_END);
+    int nLength = ftell(handle);
+    fseek(handle, nPos, SEEK_SET);
+    return nLength;
+}
+
+unsigned int randSeed = 1;
+
+unsigned int qrand(void)
+{
+    if (randSeed&0x80000000)
+        randSeed = ((randSeed<<1)^0x20000004)|0x1;
+    else
+        randSeed = randSeed<<1;
+    return randSeed&0x7fff;
+}
+
+int wRandSeed = 1;
+
+int wrand(void)
+{
+    wRandSeed = (wRandSeed*1103515245)+12345;
+    return (wRandSeed>>16)&0x7fff;
+}
+
+void wsrand(int seed)
+{
+    wRandSeed = seed;
+}
+
+void ChangeExtension(char *pzFile, const char *pzExt)
+{
+#if 0
+    char drive[BMAX_PATH];
+    char dir[BMAX_PATH];
+    char filename[BMAX_PATH];
+    _splitpath(pzFile, drive, dir, filename, NULL);
+    _makepath(pzFile, drive, dir, filename, pzExt);
+#else
+    char *pDot = strrchr(pzFile, '.');
+    if (!pDot)
+        pDot = pzFile + strlen(pzFile);
+    else
+        *pDot = 0;
+    strcat(pDot, pzExt);
+#endif
+}
diff --git a/source/blood/src/misc.h b/source/blood/src/misc.h
new file mode 100644
index 000000000..ea6cd367b
--- /dev/null
+++ b/source/blood/src/misc.h
@@ -0,0 +1,32 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+void *ResReadLine(char *buffer, unsigned int nBytes, void **pRes);
+bool FileRead(FILE *, void *, unsigned int);
+bool FileWrite(FILE *, void *, unsigned int);
+bool FileLoad(const char *, void *, unsigned int);
+int FileLength(FILE *);
+unsigned int qrand(void);
+int wrand(void);
+void wsrand(int);
+void ChangeExtension(char *pzFile, const char *pzExt);
diff --git a/source/blood/src/mpu401.cpp b/source/blood/src/mpu401.cpp
new file mode 100644
index 000000000..ffa022e77
--- /dev/null
+++ b/source/blood/src/mpu401.cpp
@@ -0,0 +1,497 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+/**********************************************************************
+   module: MPU401.C
+
+   author: James R. Dose
+   date:   January 1, 1994
+
+   Low level routines to support sending of MIDI data to MPU401
+   compatible MIDI interfaces.
+
+   (c) Copyright 1994 James R. Dose.  All Rights Reserved.
+**********************************************************************/
+
+// This object is shared by all Build games with MIDI playback!
+
+#include "mpu401.h"
+#include "compat.h"
+#include "pragmas.h"
+
+#define NEED_MMSYSTEM_H
+#include "windows_inc.h"
+
+static HMIDISTRM hmido = (HMIDISTRM)-1;
+static MIDIOUTCAPS midicaps;
+static DWORD mididevice = -1;
+
+#define PAD(x) ((((x)+3)&(~3)))
+
+#define BUFFERLEN (32*4*4)
+#define NUMBUFFERS 6
+static char eventbuf[NUMBUFFERS][BUFFERLEN];
+static int32_t  eventcnt[NUMBUFFERS];
+static MIDIHDR bufferheaders[NUMBUFFERS];
+int32_t  _MPU_CurrentBuffer = 0;
+int32_t  _MPU_BuffersWaiting = 0;
+
+extern uint32_t _MIDI_GlobalPositionInTicks;
+uint32_t _MPU_LastEvent=0;
+
+#define MIDI_NOTE_OFF         0x80
+#define MIDI_NOTE_ON          0x90
+#define MIDI_POLY_AFTER_TCH   0xA0
+#define MIDI_CONTROL_CHANGE   0xB0
+#define MIDI_PROGRAM_CHANGE   0xC0
+#define MIDI_AFTER_TOUCH      0xD0
+#define MIDI_PITCH_BEND       0xE0
+#define MIDI_META_EVENT       0xFF
+#define MIDI_END_OF_TRACK     0x2F
+#define MIDI_TEMPO_CHANGE     0x51
+#define MIDI_MONO_MODE_ON     0x7E
+#define MIDI_ALL_NOTES_OFF    0x7B
+
+
+/**********************************************************************
+
+   Memory locked functions:
+
+**********************************************************************/
+
+
+void MPU_FinishBuffer(int32_t buffer)
+{
+    if (!eventcnt[buffer]) return;
+    ZeroMemory(&bufferheaders[buffer], sizeof(MIDIHDR));
+    bufferheaders[buffer].lpData = eventbuf[buffer];
+    bufferheaders[buffer].dwBufferLength =
+        bufferheaders[buffer].dwBytesRecorded = eventcnt[buffer];
+    midiOutPrepareHeader((HMIDIOUT)hmido, &bufferheaders[buffer], sizeof(MIDIHDR));
+    midiStreamOut(hmido, &bufferheaders[buffer], sizeof(MIDIHDR));
+//	printf("Sending %d bytes (buffer %d)\n",eventcnt[buffer],buffer);
+    _MPU_BuffersWaiting++;
+}
+
+void MPU_BeginPlayback(void)
+{
+    _MPU_LastEvent = _MIDI_GlobalPositionInTicks;
+    if (hmido != (HMIDISTRM)-1) midiStreamRestart(hmido);
+}
+
+void MPU_Pause(void)
+{
+    if (hmido != (HMIDISTRM)-1) midiStreamPause(hmido);
+}
+
+void MPU_Unpause(void)
+{
+    if (hmido != (HMIDISTRM)-1) midiStreamRestart(hmido);
+}
+
+
+void CALLBACK MPU_MIDICallback(HMIDIOUT handle, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
+{
+    int32_t i;
+
+    UNREFERENCED_PARAMETER(dwInstance);
+    UNREFERENCED_PARAMETER(dwParam2);
+
+    switch (uMsg)
+    {
+    case MOM_DONE:
+        midiOutUnprepareHeader((HMIDIOUT)handle, (MIDIHDR *)dwParam1, sizeof(MIDIHDR));
+        for (i=0; i<NUMBUFFERS; i++)
+        {
+            if ((MIDIHDR *)dwParam1 == &bufferheaders[i])
+            {
+                eventcnt[i] = 0;	// marks the buffer as free
+//					printf("Finished buffer %d\n",i);
+                _MPU_BuffersWaiting--;
+                break;
+            }
+        }
+        return;
+
+    default: return;
+    }
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_SendMidi
+
+   Queues a MIDI message to the music device.
+---------------------------------------------------------------------*/
+
+int32_t MPU_GetNextBuffer(void)
+{
+    int32_t i;
+    for (i=0; i<NUMBUFFERS; i++)
+    {
+        if (eventcnt[i] == 0) return i;
+    }
+    return -1;
+}
+
+void MPU_SendMidi(char *data, int32_t count)
+{
+    char *p;
+    int32_t padded, nextbuffer;
+    static int32_t masks[3] = { 0x000000ffl, 0x0000ffffl, 0x00ffffffl };
+
+    if (count <= 0) return;
+    if (count <= 3)
+    {
+        if (eventcnt[_MPU_CurrentBuffer] + 12 > BUFFERLEN)
+        {
+            // buffer over-full
+            nextbuffer = MPU_GetNextBuffer();
+            if (nextbuffer < 0)
+            {
+//				printf("All buffers full!\n");
+                return;
+            }
+            MPU_FinishBuffer(_MPU_CurrentBuffer);
+            _MPU_CurrentBuffer = nextbuffer;
+        }
+
+        p = eventbuf[_MPU_CurrentBuffer] + eventcnt[_MPU_CurrentBuffer];
+        ((int32_t *)p)[0] = _MIDI_GlobalPositionInTicks - _MPU_LastEvent;
+        ((int32_t *)p)[1] = 0;
+        ((int32_t *)p)[2] = (MEVT_SHORTMSG << 24) | ((*((int32_t *)data)) & masks[count-1]);
+        eventcnt[_MPU_CurrentBuffer] += 12;
+    }
+    else
+    {
+        padded = PAD(count);
+        if (eventcnt[_MPU_CurrentBuffer] + 12 + padded > BUFFERLEN)
+        {
+            // buffer over-full
+            nextbuffer = MPU_GetNextBuffer();
+            if (nextbuffer < 0)
+            {
+//				printf("All buffers full!\n");
+                return;
+            }
+            MPU_FinishBuffer(_MPU_CurrentBuffer);
+            _MPU_CurrentBuffer = nextbuffer;
+        }
+
+        p = eventbuf[_MPU_CurrentBuffer] + eventcnt[_MPU_CurrentBuffer];
+        ((int32_t *)p)[0] = _MIDI_GlobalPositionInTicks - _MPU_LastEvent;
+        ((int32_t *)p)[1] = 0;
+        ((int32_t *)p)[2] = (MEVT_LONGMSG<<24) | (count & 0xffffffl);
+        p+=12; eventcnt[_MPU_CurrentBuffer] += 12;
+        for (; count>0; count--, padded--, eventcnt[_MPU_CurrentBuffer]++)
+            *(p++) = *(data++);
+        for (; padded>0; padded--, eventcnt[_MPU_CurrentBuffer]++)
+            *(p++) = 0;
+    }
+    _MPU_LastEvent = _MIDI_GlobalPositionInTicks;
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_SendMidiImmediate
+
+   Sends a MIDI message immediately to the the music device.
+---------------------------------------------------------------------*/
+void MPU_SendMidiImmediate(char *data, int32_t count)
+{
+    MIDIHDR mhdr;
+    static int32_t masks[3] = { 0x00ffffffl, 0x0000ffffl, 0x000000ffl };
+
+    if (!count) return;
+    if (count<=3) midiOutShortMsg((HMIDIOUT)hmido, (*((int32_t *)data)) & masks[count-1]);
+    else
+    {
+        ZeroMemory(&mhdr, sizeof(mhdr));
+        mhdr.lpData = data;
+        mhdr.dwBufferLength = count;
+        midiOutPrepareHeader((HMIDIOUT)hmido, &mhdr, sizeof(MIDIHDR));
+        midiOutLongMsg((HMIDIOUT)hmido, &mhdr, sizeof(MIDIHDR));
+        while (!(mhdr.dwFlags & MHDR_DONE)) ;
+        midiOutUnprepareHeader((HMIDIOUT)hmido, &mhdr, sizeof(MIDIHDR));
+    }
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_Reset
+
+   Resets the MPU401 card.
+---------------------------------------------------------------------*/
+
+int32_t MPU_Reset
+(
+    void
+)
+
+{
+    midiStreamStop(hmido);
+    midiStreamClose(hmido);
+    hmido = (HMIDISTRM)-1;
+
+    return MPU_Ok;
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_Init
+
+   Detects and initializes the MPU401 card.
+---------------------------------------------------------------------*/
+
+int32_t MPU_Init
+(
+    int32_t addr
+)
+
+{
+    if (hmido != (HMIDISTRM)-1)
+        return MPU_Ok;
+
+    int32_t i;
+
+    for (i=0; i<NUMBUFFERS; i++) eventcnt[i]=0;
+
+    mididevice = addr;
+
+    if (midiOutGetDevCaps(mididevice, &midicaps, sizeof(MIDIOUTCAPS)) != MMSYSERR_NOERROR) return MPU_Error;
+
+    if (midiStreamOpen(&hmido,(LPUINT)&mididevice,1,(DWORD_PTR)MPU_MIDICallback,0L,CALLBACK_FUNCTION) != MMSYSERR_NOERROR) return MPU_Error;
+
+    return MPU_Ok;
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_NoteOff
+
+   Sends a full MIDI note off event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_NoteOff
+(
+    int32_t channel,
+    int32_t key,
+    int32_t velocity
+)
+
+{
+    char msg[3];
+    msg[0] = (MIDI_NOTE_OFF | channel);
+    msg[1] = (key);
+    msg[2] = (velocity);
+    MPU_SendMidi(msg, 3);
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_NoteOn
+
+   Sends a full MIDI note on event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_NoteOn
+(
+    int32_t channel,
+    int32_t key,
+    int32_t velocity
+)
+
+{
+    char msg[3];
+    msg[0] = (MIDI_NOTE_ON | channel);
+    msg[1] = (key);
+    msg[2] = (velocity);
+    MPU_SendMidi(msg, 3);
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_PolyAftertouch
+
+   Sends a full MIDI polyphonic aftertouch event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_PolyAftertouch
+(
+    int32_t channel,
+    int32_t key,
+    int32_t pressure
+)
+
+{
+    char msg[3];
+    msg[0] = (MIDI_POLY_AFTER_TCH | channel);
+    msg[1] = (key);
+    msg[2] = (pressure);
+    MPU_SendMidi(msg, 3);
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_ControlChange
+
+   Sends a full MIDI control change event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_ControlChange
+(
+    int32_t channel,
+    int32_t number,
+    int32_t value
+)
+
+{
+    char msg[3];
+    msg[0] = (MIDI_CONTROL_CHANGE | channel);
+    msg[1] = (number);
+    msg[2] = (value);
+    MPU_SendMidi(msg, 3);
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_ProgramChange
+
+   Sends a full MIDI program change event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_ProgramChange
+(
+    int32_t channel,
+    int32_t program
+)
+
+{
+    char msg[2];
+    msg[0] = (MIDI_PROGRAM_CHANGE | channel);
+    msg[1] = (program);
+    MPU_SendMidi(msg, 2);
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_ChannelAftertouch
+
+   Sends a full MIDI channel aftertouch event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_ChannelAftertouch
+(
+    int32_t channel,
+    int32_t pressure
+)
+
+{
+    char msg[2];
+    msg[0] = (MIDI_AFTER_TOUCH | channel);
+    msg[1] = (pressure);
+    MPU_SendMidi(msg, 2);
+}
+
+
+/*---------------------------------------------------------------------
+   Function: MPU_PitchBend
+
+   Sends a full MIDI pitch bend event out to the music device.
+---------------------------------------------------------------------*/
+
+void MPU_PitchBend
+(
+    int32_t channel,
+    int32_t lsb,
+    int32_t msb
+)
+
+{
+    char msg[3];
+    msg[0] = (MIDI_PITCH_BEND | channel);
+    msg[1] = (lsb);
+    msg[2] = (msb);
+    MPU_SendMidi(msg, 3);
+}
+
+
+
+void MPU_SetTempo(int32_t tempo)
+{
+    MIDIPROPTEMPO prop;
+    prop.cbStruct = sizeof(MIDIPROPTEMPO);
+    prop.dwTempo = tabledivide32_noinline(60000000l, tempo);
+    midiStreamProperty(hmido, (LPBYTE)&prop, MIDIPROP_SET|MIDIPROP_TEMPO);
+}
+
+void MPU_SetDivision(int32_t division)
+{
+    MIDIPROPTIMEDIV prop;
+    prop.cbStruct = sizeof(MIDIPROPTIMEDIV);
+    prop.dwTimeDiv = division;
+    midiStreamProperty(hmido, (LPBYTE)&prop, MIDIPROP_SET|MIDIPROP_TIMEDIV);
+}
+
+void MPU_SetVolume(int32_t volume)
+{
+    /*
+    HMIXER hmixer;
+    int32_t mixerid;
+    MIXERCONTROLDETAILS mxcd;
+    MIXERCONTROLDETAILS_UNSIGNED mxcdu;
+    MMRESULT mme;
+
+    if (mididevice < 0) return;
+
+    mme = mixerOpen(&hmixer, mididevice, 0,0, MIXER_OBJECTF_MIDIOUT);
+    if (mme) {
+    	puts("Failed opening mixer");
+    	return;
+    }
+
+    mixerGetID(hmixer, &mixerid, MIXER_OBJECTF_HMIXER);
+    printf("mixerid=%d\n",mixerid);
+
+    ZeroMemory(&mxcd,sizeof(mxcd));
+    mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS);
+    mxcd.dwControlID = MIXERCONTROL_CONTROLTYPE_VOLUME;
+    mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
+    mxcd.paDetails = (LPVOID)&mxcdu;
+    mxcdu.dwValue = (volume << 8) & 0xffff;
+
+    printf("set %d\n",mixerSetControlDetails((HMIXEROBJ)mididevice, &mxcd,
+    	MIXER_OBJECTF_MIDIOUT|MIXER_SETCONTROLDETAILSF_VALUE));
+
+    mixerClose(hmixer);
+    */
+    UNREFERENCED_PARAMETER(volume);
+}
+
+int32_t MPU_GetVolume(void)
+{
+//    if (mididevice < 0) return 0;
+
+    return 0;
+}
+
diff --git a/source/blood/src/mpu401.h b/source/blood/src/mpu401.h
new file mode 100644
index 000000000..419ce59af
--- /dev/null
+++ b/source/blood/src/mpu401.h
@@ -0,0 +1,68 @@
+/*
+Copyright (C) 1994-1995 Apogee Software, Ltd.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+Modifications for JonoF's port by Jonathon Fowler (jf@jonof.id.au)
+*/
+#ifndef __MPU401_H
+#define __MPU401_H
+
+#include "compat.h"
+
+#define MPU_DefaultAddress 0x330
+
+enum MPU_ERRORS
+   {
+   MPU_Warning = -2,
+   MPU_Error = -1,
+   MPU_Ok = 0
+   };
+
+#define MPU_NotFound       -1
+#define MPU_UARTFailed     -2
+
+#define MPU_ReadyToWrite   0x40
+#define MPU_ReadyToRead    0x80
+#define MPU_CmdEnterUART   0x3f
+#define MPU_CmdReset       0xff
+#define MPU_CmdAcknowledge 0xfe
+
+extern int32_t _MPU_CurrentBuffer;
+extern int32_t _MPU_BuffersWaiting;
+
+void MPU_SendMidi( char *data, int32_t count );
+void MPU_SendMidiImmediate( char *data, int32_t count );
+int32_t  MPU_Reset( void );
+int32_t  MPU_Init( int32_t addr );
+void MPU_NoteOff( int32_t channel, int32_t key, int32_t velocity );
+void MPU_NoteOn( int32_t channel, int32_t key, int32_t velocity );
+void MPU_PolyAftertouch( int32_t channel, int32_t key, int32_t pressure );
+void MPU_ControlChange( int32_t channel, int32_t number, int32_t value );
+void MPU_ProgramChange( int32_t channel, int32_t program );
+void MPU_ChannelAftertouch( int32_t channel, int32_t pressure );
+void MPU_PitchBend( int32_t channel, int32_t lsb, int32_t msb );
+
+void MPU_SetTempo(int32_t tempo);
+void MPU_SetDivision(int32_t division);
+void MPU_SetVolume(int32_t volume);
+int32_t  MPU_GetVolume(void);
+
+void MPU_BeginPlayback( void );
+void MPU_Pause(void);
+void MPU_Unpause(void);
+
+#endif
diff --git a/source/blood/src/music.cpp b/source/blood/src/music.cpp
new file mode 100644
index 000000000..0288a6e52
--- /dev/null
+++ b/source/blood/src/music.cpp
@@ -0,0 +1,126 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+// This object is shared by all Build games with MIDI playback!
+
+#include "compat.h"
+#include "music.h"
+#include "midi.h"
+#include "mpu401.h"
+
+int32_t MUSIC_SoundDevice = -1;
+int32_t MUSIC_ErrorCode = MUSIC_Ok;
+
+static midifuncs MUSIC_MidiFunctions;
+
+int32_t MUSIC_InitMidi(int32_t card, midifuncs *Funcs, int32_t Address);
+
+#define MUSIC_SetErrorCode(status) MUSIC_ErrorCode = (status);
+
+const char *MUSIC_ErrorString(int32_t ErrorNumber)
+{
+    const char *ErrorString;
+
+    switch (ErrorNumber)
+    {
+        case MUSIC_Warning:
+        case MUSIC_Error:       ErrorString = MUSIC_ErrorString(MUSIC_ErrorCode); break;
+        case MUSIC_Ok:          ErrorString = "Music ok."; break;
+        case MUSIC_MidiError:   ErrorString = "Error playing MIDI file."; break;
+        default:                ErrorString = "Unknown Music error code."; break;
+    }
+
+    return ErrorString;
+}
+
+
+int32_t MUSIC_Init(int32_t SoundCard, int32_t Address)
+{
+    MUSIC_SoundDevice = SoundCard;
+
+    return MUSIC_InitMidi(SoundCard, &MUSIC_MidiFunctions, Address);
+}
+
+
+int32_t MUSIC_Shutdown(void)
+{
+    MIDI_StopSong();
+
+    return MUSIC_Ok;
+}
+
+
+void MUSIC_SetVolume(int32_t volume)
+{
+    if (MUSIC_SoundDevice != -1)
+        MIDI_SetVolume(min(max(0, volume), 255));
+}
+
+
+int32_t MUSIC_GetVolume(void) { return MUSIC_SoundDevice == -1 ? 0 : MIDI_GetVolume(); }
+void MUSIC_SetLoopFlag(int32_t loopflag) { MIDI_SetLoopFlag(loopflag); }
+void MUSIC_Continue(void) { MIDI_ContinueSong(); }
+void MUSIC_Pause(void) { MIDI_PauseSong(); }
+
+int32_t MUSIC_StopSong(void)
+{
+    MIDI_StopSong();
+    MUSIC_SetErrorCode(MUSIC_Ok);
+    return MUSIC_Ok;
+}
+
+
+int32_t MUSIC_PlaySong(char *song, int32_t songsize, int32_t loopflag)
+{
+    UNREFERENCED_PARAMETER(songsize);
+
+    MUSIC_SetErrorCode(MUSIC_Ok)
+
+    if (MIDI_PlaySong(song, loopflag) != MIDI_Ok)
+    {
+        MUSIC_SetErrorCode(MUSIC_MidiError);
+        return MUSIC_Warning;
+    }
+
+    return MUSIC_Ok;
+}
+
+
+int32_t MUSIC_InitMidi(int32_t card, midifuncs *Funcs, int32_t Address)
+{
+    UNREFERENCED_PARAMETER(card);
+    UNREFERENCED_PARAMETER(Address);
+    Funcs->NoteOff = MPU_NoteOff;
+    Funcs->NoteOn = MPU_NoteOn;
+    Funcs->PolyAftertouch = MPU_PolyAftertouch;
+    Funcs->ControlChange = MPU_ControlChange;
+    Funcs->ProgramChange = MPU_ProgramChange;
+    Funcs->ChannelAftertouch = MPU_ChannelAftertouch;
+    Funcs->PitchBend = MPU_PitchBend;
+
+    MIDI_SetMidiFuncs(Funcs);
+
+    return MIDI_Ok;
+}
+
+void MUSIC_Update(void) { MIDI_UpdateMusic(); }
diff --git a/source/blood/src/network.cpp b/source/blood/src/network.cpp
new file mode 100644
index 000000000..bc7d924d8
--- /dev/null
+++ b/source/blood/src/network.cpp
@@ -0,0 +1,1428 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "mmulti.h"
+#include "pragmas.h"
+#ifndef NETCODE_DISABLE
+#include "enet/enet.h"
+#endif
+#include "compat.h"
+#include "config.h"
+#include "controls.h"
+#include "globals.h"
+#include "network.h"
+#include "menu.h"
+#include "player.h"
+#include "seq.h"
+#include "sound.h"
+#include "view.h"
+
+char packet[576];
+bool gStartNewGame = 0;
+PACKETMODE gPacketMode = PACKETMODE_1;
+int gNetFifoClock = 0;
+int gNetFifoTail = 0;
+int gNetFifoHead[8];
+int gPredictTail = 0;
+int gNetFifoMasterTail = 0;
+GINPUT gFifoInput[256][8];
+int myMinLag[8];
+int otherMinLag = 0;
+int myMaxLag = 0;
+unsigned int gChecksum[4];
+unsigned int gCheckFifo[256][8][4];
+int gCheckHead[8];
+int gSendCheckTail = 0;
+int gCheckTail = 0;
+int gInitialNetPlayers = 0;
+int gBufferJitter = 1;
+int gPlayerReady[8];
+int gSyncRate = 1;
+bool bNoResend = true;
+bool gRobust = false;
+bool bOutOfSync = false;
+bool ready2send = false;
+
+NETWORKMODE gNetMode = NETWORK_NONE;
+char gNetAddress[32];
+// PORT-TODO: Use different port?
+int gNetPort = kNetDefaultPort;
+
+const short word_1328AC = 0x214;
+
+struct struct28E3B0
+{
+    int at0;
+    int at4;
+    int at8;
+    int atc;
+    int at10;
+    int at14;
+    int at18;
+    char at1c;
+    int at1d;
+};
+
+struct28E3B0 byte_28E3B0;
+
+PKT_STARTGAME gPacketStartGame;
+
+#ifndef NETCODE_DISABLE
+ENetAddress gNetENetAddress;
+ENetHost *gNetENetServer;
+ENetHost *gNetENetClient;
+ENetPeer *gNetENetPeer;
+ENetPeer *gNetPlayerPeer[kMaxPlayers];
+bool gNetENetInit = false;
+
+#define kENetFifoSize 2048
+
+//ENetPacket *gNetServerPacketFifo[kENetFifoSize];
+//int gNetServerPacketHead, gNetServerPacketTail;
+ENetPacket *gNetPacketFifo[kENetFifoSize];
+int gNetPacketHead, gNetPacketTail;
+
+enum BLOOD_ENET_CHANNEL {
+    BLOOD_ENET_SERVICE = 0,
+    BLOOD_ENET_GAME,
+    BLOOD_ENET_CHANNEL_MAX
+};
+
+enum BLOOD_SERVICE {
+    BLOOD_SERVICE_CONNECTINFO = 0,
+    BLOOD_SERVICE_CONNECTID,
+    BLOOD_SERVICE_SENDTOID,
+};
+
+struct PKT_CONNECTINFO {
+    short numplayers;
+    short connecthead;
+    short connectpoint2[kMaxPlayers];
+};
+
+void netServerDisconnect(void)
+{
+    ENetEvent event;
+    if (gNetMode != NETWORK_SERVER)
+        return;
+    for (int p = 0; p < kMaxPlayers; p++)
+        if (gNetPlayerPeer[p])
+        {
+            bool bDisconnectStatus = false;
+            enet_peer_disconnect_later(gNetPlayerPeer[p], 0);
+            if (enet_host_service(gNetENetServer, &event, 3000) > 0)
+            {
+                switch (event.type)
+                {
+                case ENET_EVENT_TYPE_DISCONNECT:
+                    bDisconnectStatus = true;
+                    break;
+                default:
+                    break;
+                }
+            }
+            if (!bDisconnectStatus)
+                enet_peer_reset(gNetPlayerPeer[p]);
+            gNetPlayerPeer[p] = NULL;
+        }
+}
+
+void netClientDisconnect(void)
+{
+    ENetEvent event;
+    if (gNetMode != NETWORK_CLIENT || gNetENetPeer == NULL)
+        return;
+    enet_peer_disconnect_later(gNetENetPeer, 0);
+    bool bDisconnectStatus = false;
+    if (enet_host_service(gNetENetClient, &event, 3000) > 0)
+    {
+        switch (event.type)
+        {
+        case ENET_EVENT_TYPE_DISCONNECT:
+            bDisconnectStatus = true;
+            break;
+        default:
+            break;
+        }
+    }
+    if (!bDisconnectStatus)
+        enet_peer_reset(gNetENetPeer);
+    gNetENetPeer = NULL;
+}
+#endif
+
+void netResetToSinglePlayer(void)
+{
+    myconnectindex = connecthead = 0;
+    gInitialNetPlayers = gNetPlayers = numplayers = 1;
+    connectpoint2[0] = -1;
+    gGameOptions.nGameType = 0;
+    gNetMode = NETWORK_NONE;
+    UpdateNetworkMenus();
+}
+
+void netSendPacket(int nDest, char *pBuffer, int nSize)
+{
+#ifndef NETCODE_DISABLE
+    if (gNetMode == NETWORK_NONE)
+        return;
+    netUpdate();
+
+    if (gNetMode == NETWORK_SERVER)
+    {
+        if (gNetPlayerPeer[nDest] != NULL)
+        {
+            ENetPacket *pNetPacket = enet_packet_create(NULL, nSize + 1, ENET_PACKET_FLAG_RELIABLE);
+            char *pPBuffer = (char*)pNetPacket->data;
+            PutPacketByte(pPBuffer, myconnectindex);
+            PutPacketBuffer(pPBuffer, pBuffer, nSize);
+            enet_peer_send(gNetPlayerPeer[nDest], BLOOD_ENET_GAME, pNetPacket);
+            enet_host_service(gNetENetServer, NULL, 0);
+        }
+    }
+    else
+    {
+        if (nDest == 0)
+        {
+            ENetPacket *pNetPacket = enet_packet_create(NULL, nSize + 1, ENET_PACKET_FLAG_RELIABLE);
+            char *pPBuffer = (char*)pNetPacket->data;
+            PutPacketByte(pPBuffer, myconnectindex);
+            PutPacketBuffer(pPBuffer, pBuffer, nSize);
+            enet_peer_send(gNetENetPeer, BLOOD_ENET_GAME, pNetPacket);
+        }
+        else
+        {
+            ENetPacket *pNetPacket = enet_packet_create(NULL, nSize + 3, ENET_PACKET_FLAG_RELIABLE);
+            char *pPBuffer = (char*)pNetPacket->data;
+            PutPacketByte(pPBuffer, BLOOD_SERVICE_SENDTOID);
+            PutPacketByte(pPBuffer, nDest);
+            PutPacketByte(pPBuffer, myconnectindex);
+            PutPacketBuffer(pPBuffer, pBuffer, nSize);
+            enet_peer_send(gNetENetPeer, BLOOD_ENET_SERVICE, pNetPacket);
+        }
+        enet_host_service(gNetENetClient, NULL, 0);
+    }
+
+    netUpdate();
+#endif
+}
+
+void netSendPacketAll(char *pBuffer, int nSize)
+{
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        if (p != myconnectindex)
+            netSendPacket(p, pBuffer, nSize);
+}
+
+void sub_79760(void)
+{
+    gNetFifoClock = gFrameClock = gGameClock = 0;
+    gNetFifoMasterTail = 0;
+    gPredictTail = 0;
+    gNetFifoTail = 0;
+    memset(gNetFifoHead, 0, sizeof(gNetFifoHead));
+    memset(gCheckFifo, 0, sizeof(gCheckFifo));
+    memset(myMinLag, 0, sizeof(myMinLag));
+    otherMinLag = 0;
+    myMaxLag = 0;
+    memset(gCheckHead, 0, sizeof(gCheckHead));
+    gSendCheckTail = 0;
+    gCheckTail = 0;
+    memset(&byte_28E3B0, 0, sizeof(byte_28E3B0));
+    bOutOfSync = 0;
+    gBufferJitter = 1;
+}
+
+void CalcGameChecksum(void)
+{
+    memset(gChecksum, 0, sizeof(gChecksum));
+    gChecksum[0] = wrand();
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+    {
+        int *pBuffer = &gPlayer[p].at22;
+        int sum = 0;
+        int length = ((char*)&gPlayer[p+1]-(char*)pBuffer)/4;
+        while (length--)
+        {
+            sum += *pBuffer++;
+        }
+        gChecksum[1] ^= sum;
+        pBuffer = (int*)gPlayer[p].pSprite;
+        sum = 0;
+        length = sizeof(spritetype)/4;
+        while (length--)
+        {
+            sum += *pBuffer++;
+        }
+        gChecksum[2] ^= sum;
+        pBuffer = (int*)gPlayer[p].pXSprite;
+        sum = 0;
+        length = sizeof(XSPRITE)/4;
+        while (length--)
+        {
+            sum += *pBuffer++;
+        }
+        gChecksum[3] ^= sum;
+    }
+}
+
+void netCheckSync(void)
+{
+    char buffer[80];
+    if (gGameOptions.nGameType == 0)
+        return;
+    if (numplayers == 1)
+        return;
+    if (bOutOfSync)
+        return;
+    while (1)
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            if (gCheckTail >= gCheckHead[p])
+                return;
+        }
+
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            if (p != myconnectindex)
+            {
+                int status = memcmp(gCheckFifo[gCheckTail&255][p], gCheckFifo[gCheckTail&255][connecthead], 16);
+                if (status)
+                {
+                    sprintf(buffer, "OUT OF SYNC (%d)", p);
+                    char *pBuffer = buffer + strlen(buffer);
+                    for (unsigned int i = 0; i < 4; i++)
+                    {
+                        if (gCheckFifo[gCheckTail&255][p][i] != gCheckFifo[gCheckTail&255][connecthead][i])
+                            pBuffer += sprintf(pBuffer, " %d", i);
+                    }
+                    viewSetErrorMessage(buffer);
+                    bOutOfSync = 1;
+                }
+            }
+        }
+        gCheckTail++;
+    }
+}
+
+short netGetPacket(short *pSource, char *pMessage)
+{
+#ifndef NETCODE_DISABLE
+    if (gNetMode == NETWORK_NONE)
+        return 0;
+    netUpdate();
+    if (gNetPacketTail != gNetPacketHead)
+    {
+        ENetPacket *pEPacket = gNetPacketFifo[gNetPacketTail];
+        gNetPacketTail = (gNetPacketTail+1)%kENetFifoSize;
+        char *pPacket = (char*)pEPacket->data;
+        *pSource = GetPacketByte(pPacket);
+        int nSize = pEPacket->dataLength-1;
+        memcpy(pMessage, pPacket, nSize);
+        enet_packet_destroy(pEPacket);
+        netUpdate();
+        return nSize;
+    }
+    netUpdate();
+#endif
+    return 0;
+}
+
+void netGetPackets(void)
+{
+    short nPlayer;
+    short nSize;
+    char buffer[128];
+    while ((nSize = netGetPacket(&nPlayer, packet)) > 0)
+    {
+        char *pPacket = packet;
+        switch (GetPacketByte(pPacket))
+        {
+        case 0: // From master
+            for (int p = connecthead; p >= 0; p = connectpoint2[p])
+            {
+                if (p != myconnectindex)
+                {
+                    GINPUT *pInput = &gFifoInput[gNetFifoHead[p]&255][p];
+                    memset(pInput, 0, sizeof(GINPUT));
+                    pInput->syncFlags.byte = GetPacketByte(pPacket);
+                    pInput->forward = GetPacketWord(pPacket);
+                    pInput->q16turn = GetPacketDWord(pPacket);
+                    pInput->strafe = GetPacketWord(pPacket);
+                    if (pInput->syncFlags.buttonChange)
+                        pInput->buttonFlags.byte = GetPacketByte(pPacket);
+                    if (pInput->syncFlags.keyChange)
+                        pInput->keyFlags.word = GetPacketWord(pPacket);
+                    if (pInput->syncFlags.useChange)
+                        pInput->useFlags.byte = GetPacketByte(pPacket);
+                    if (pInput->syncFlags.weaponChange)
+                        pInput->newWeapon = GetPacketByte(pPacket);
+                    if (pInput->syncFlags.mlookChange)
+                        pInput->q16mlook = GetPacketDWord(pPacket);
+                    gNetFifoHead[p]++;
+                }
+                else
+                {
+                    SYNCFLAGS syncFlags;
+                    syncFlags.byte = GetPacketByte(pPacket);
+                    pPacket += 2+4+2;
+                    if (syncFlags.buttonChange)
+                        pPacket++;
+                    if (syncFlags.keyChange)
+                        pPacket+=2;
+                    if (syncFlags.useChange)
+                        pPacket++;
+                    if (syncFlags.weaponChange)
+                        pPacket++;
+                    if (syncFlags.mlookChange)
+                        pPacket+=4;
+                }
+            }
+            if (((gNetFifoHead[connecthead]-1)&15)==0)
+            {
+                for (int p = connectpoint2[connecthead]; p >= 0; p = connectpoint2[p])
+                {
+                    int nLag = (signed char)GetPacketByte(pPacket);
+                    if (p == myconnectindex)
+                        otherMinLag = nLag;
+                }
+            }
+            while (pPacket < packet+nSize)
+            {
+                int checkSum[4];
+                GetPacketBuffer(pPacket, checkSum, sizeof(checkSum));
+                for (int p = connecthead; p >= 0; p = connectpoint2[p])
+                {
+                    if (p != myconnectindex)
+                    {
+                        memcpy(gCheckFifo[gCheckHead[p]&255][p], checkSum, sizeof(checkSum));
+                        gCheckHead[p]++;
+                    }
+                }
+            }
+            break;
+        case 1: // From slave
+        {
+            GINPUT *pInput = &gFifoInput[gNetFifoHead[nPlayer]&255][nPlayer];
+            memset(pInput, 0, sizeof(GINPUT));
+            pInput->syncFlags.byte = GetPacketByte(pPacket);
+            pInput->forward = GetPacketWord(pPacket);
+            pInput->q16turn = GetPacketDWord(pPacket);
+            pInput->strafe = GetPacketWord(pPacket);
+            if (pInput->syncFlags.buttonChange)
+                pInput->buttonFlags.byte = GetPacketByte(pPacket);
+            if (pInput->syncFlags.keyChange)
+                pInput->keyFlags.word = GetPacketWord(pPacket);
+            if (pInput->syncFlags.useChange)
+                pInput->useFlags.byte = GetPacketByte(pPacket);
+            if (pInput->syncFlags.weaponChange)
+                pInput->newWeapon = GetPacketByte(pPacket);
+            if (pInput->syncFlags.mlookChange)
+                pInput->q16mlook = GetPacketDWord(pPacket);
+            gNetFifoHead[nPlayer]++;
+            while (pPacket < packet+nSize)
+            {
+                int checkSum[4];
+                GetPacketBuffer(pPacket, checkSum, sizeof(checkSum));
+                memcpy(gCheckFifo[gCheckHead[nPlayer]&255][nPlayer], checkSum, sizeof(checkSum));
+                gCheckHead[nPlayer]++;
+            }
+            break;
+        }
+        case 2:
+        {
+            if (nPlayer == connecthead && (gNetFifoHead[nPlayer]&15) == 0)
+            {
+                for (int p = connectpoint2[connecthead]; p >= 0; p = connectpoint2[p])
+                {
+                    int nLag = (signed char)GetPacketByte(pPacket);
+                    if (p == myconnectindex)
+                        otherMinLag = nLag;
+                }
+            }
+            GINPUT *pInput = &gFifoInput[gNetFifoHead[nPlayer]&255][nPlayer];
+            memset(pInput, 0, sizeof(GINPUT));
+            pInput->syncFlags.byte = GetPacketByte(pPacket);
+            pInput->forward = GetPacketWord(pPacket);
+            pInput->q16turn = GetPacketDWord(pPacket);
+            pInput->strafe = GetPacketWord(pPacket);
+            if (pInput->syncFlags.buttonChange)
+                pInput->buttonFlags.byte = GetPacketByte(pPacket);
+            if (pInput->syncFlags.keyChange)
+                pInput->keyFlags.word = GetPacketWord(pPacket);
+            if (pInput->syncFlags.useChange)
+                pInput->useFlags.byte = GetPacketByte(pPacket);
+            if (pInput->syncFlags.weaponChange)
+                pInput->newWeapon = GetPacketByte(pPacket);
+            if (pInput->syncFlags.mlookChange)
+                pInput->q16mlook = GetPacketDWord(pPacket);
+            gNetFifoHead[nPlayer]++;
+            for (int i = gSyncRate; i > 1; i--)
+            {
+                GINPUT *pInput2 = &gFifoInput[gNetFifoHead[nPlayer]&255][nPlayer];
+                memcpy(pInput2, pInput, sizeof(GINPUT));
+                pInput2->keyFlags.word = 0;
+                pInput2->useFlags.byte = 0;
+                pInput2->newWeapon = 0;
+                pInput2->syncFlags.weaponChange = 0;
+                gNetFifoHead[nPlayer]++;
+            }
+            while (pPacket < packet+nSize)
+            {
+                int checkSum[4];
+                GetPacketBuffer(pPacket, checkSum, sizeof(checkSum));
+                memcpy(gCheckFifo[gCheckHead[nPlayer]&255][nPlayer], checkSum, sizeof(checkSum));
+                gCheckHead[nPlayer]++;
+            }
+            break;
+        }
+        case 3:
+            pPacket += 4;
+            if (*pPacket != '/' || (*pPacket == 0 && *(pPacket+1) == 0) || (*(pPacket+1) >= '1' && *(pPacket+1) <= '8' && *(pPacket+1)-'1' == myconnectindex))
+            {
+                sprintf(buffer, "%s : %s", gProfile[nPlayer].name, pPacket);
+                viewSetMessage(buffer);
+                sndStartSample("DMRADIO", 128, -1);
+            }
+            break;
+        case 4:
+            sndStartSample(4400+GetPacketByte(pPacket), 128, 1, 0);
+            break;
+        case 7:
+            nPlayer = GetPacketDWord(pPacket);
+            dassert(nPlayer != myconnectindex);
+            netWaitForEveryone(0);
+            netPlayerQuit(nPlayer);
+            netWaitForEveryone(0);
+            break;
+        case 249:
+            nPlayer = GetPacketDWord(pPacket);
+            dassert(nPlayer != myconnectindex);
+            netPlayerQuit(nPlayer);
+            netWaitForEveryone(0);
+            break;
+        case 250:
+            gPlayerReady[nPlayer]++;
+            break;
+        case 251:
+            memcpy(&gProfile[nPlayer], pPacket, sizeof(PROFILE));
+            break;
+        case 252:
+            pPacket += 4;
+            memcpy(&gPacketStartGame, pPacket, sizeof(PKT_STARTGAME));
+            if (gPacketStartGame.version != word_1328AC)
+                ThrowError("\nThese versions of Blood cannot play together.\n");
+            gStartNewGame = 1;
+            break;
+        case 255:
+            keystatus[1] = 1;
+            break;
+        }
+    }
+}
+
+void netBroadcastPlayerLogoff(int nPlayer)
+{
+    if (numplayers < 2)
+        return;
+    netWaitForEveryone(0);
+    netPlayerQuit(nPlayer);
+    if (nPlayer != myconnectindex)
+        netWaitForEveryone(0);
+}
+
+void netBroadcastMyLogoff(bool bRestart)
+{
+    if (numplayers < 2)
+        return;
+    char *pPacket = packet;
+    PutPacketByte(pPacket, 7);
+    PutPacketDWord(pPacket, myconnectindex);
+    netSendPacketAll(packet, pPacket - packet);
+    netWaitForEveryone(0);
+    ready2send = 0;
+    gQuitGame = true;
+    if (bRestart)
+        gRestartGame = true;
+    netDeinitialize();
+    netResetToSinglePlayer();
+}
+
+void netBroadcastPlayerInfo(int nPlayer)
+{
+    PROFILE *pProfile = &gProfile[nPlayer];
+    strcpy(pProfile->name, szPlayerName);
+    pProfile->skill = gSkill;
+    pProfile->nAutoAim = gAutoAim;
+    pProfile->nWeaponSwitch = gWeaponSwitch;
+    if (numplayers < 2)
+        return;
+    char *pPacket = packet;
+    PutPacketByte(pPacket, 251);
+    PutPacketBuffer(pPacket, pProfile, sizeof(PROFILE));
+    netSendPacketAll(packet, pPacket-packet);
+}
+
+void netBroadcastNewGame(void)
+{
+    if (numplayers < 2)
+        return;
+    gPacketStartGame.version = word_1328AC;
+    char *pPacket = packet;
+    PutPacketByte(pPacket, 252);
+    PutPacketDWord(pPacket, myconnectindex);
+    PutPacketBuffer(pPacket, &gPacketStartGame, sizeof(PKT_STARTGAME));
+    netSendPacketAll(packet, pPacket-packet);
+}
+
+void netBroadcastTaunt(int nPlayer, int nTaunt)
+{
+    UNREFERENCED_PARAMETER(nPlayer);
+    if (numplayers > 1)
+    {
+        char *pPacket = packet;
+        PutPacketByte(pPacket, 4);
+        PutPacketByte(pPacket, nTaunt);
+        netSendPacketAll(packet, pPacket-packet);
+    }
+    sndStartSample(4400+nTaunt, 128, 1, 0);
+}
+
+void netBroadcastMessage(int nPlayer, const char *pzMessage)
+{
+    if (numplayers > 1)
+    {
+        int nSize = strlen(pzMessage);
+        char *pPacket = packet;
+        PutPacketByte(pPacket, 3);
+        PutPacketDWord(pPacket, nPlayer);
+        PutPacketBuffer(pPacket, pzMessage, nSize+1);
+        netSendPacketAll(packet, pPacket-packet);
+    }
+}
+
+void netWaitForEveryone(char a1)
+{
+    if (numplayers < 2)
+        return;
+    char *pPacket = packet;
+    PutPacketByte(pPacket, 250);
+    netSendPacketAll(packet, pPacket-packet);
+    gPlayerReady[myconnectindex]++;
+    int p;
+    do
+    {
+        if (keystatus[sc_Escape] && a1)
+            exit(0);
+        G_HandleAsync();
+        faketimerhandler();
+        for (p = connecthead; p >= 0; p = connectpoint2[p])
+            if (gPlayerReady[p] < gPlayerReady[myconnectindex])
+                break;
+        if (gRestartGame || gNetPlayers <= 1)
+            break;
+    } while (p >= 0);
+}
+
+void sub_7AC28(const char *pzString)
+{
+    if (numplayers < 2)
+        return;
+    if (pzString)
+    {
+        int nLength = strlen(pzString);
+        if (nLength > 0)
+        {
+            char *pPacket = packet;
+            PutPacketByte(pPacket, 5);
+            PutPacketBuffer(pPacket, pzString, nLength+1);
+            netSendPacketAll(packet, pPacket-packet);
+        }
+    }
+}
+
+void netSendEmptyPackets(void)
+{
+    int nClock = gGameClock;
+    char *pPacket = packet;
+    PutPacketByte(pPacket, 254);
+    for (int i = 0; i < 8; i++)
+    {
+        if (nClock <= gGameClock)
+        {
+            nClock = gGameClock+4;
+            netSendPacketAll(packet, pPacket-packet);
+        }
+    }
+}
+
+void sub_7AD90(GINPUT *pInput)
+{
+    byte_28E3B0.at0 |= pInput->syncFlags.byte;
+    byte_28E3B0.at4 += pInput->forward;
+    byte_28E3B0.at8 += pInput->q16turn;
+    byte_28E3B0.atc += pInput->strafe;
+    byte_28E3B0.at10 |= pInput->buttonFlags.byte;
+    byte_28E3B0.at14 |= pInput->keyFlags.word;
+    byte_28E3B0.at18 |= pInput->useFlags.byte;
+    if (pInput->newWeapon)
+        byte_28E3B0.at1c = pInput->newWeapon;
+    byte_28E3B0.at1d = pInput->q16mlook;
+}
+
+void sub_7AE2C(GINPUT *pInput)
+{
+    pInput->syncFlags.byte = byte_28E3B0.at0;
+    pInput->forward = byte_28E3B0.at4;
+    pInput->q16turn = byte_28E3B0.at8;
+    pInput->strafe = byte_28E3B0.atc;
+    pInput->buttonFlags.byte = byte_28E3B0.at10;
+    pInput->keyFlags.word = byte_28E3B0.at14;
+    pInput->useFlags.byte = byte_28E3B0.at18;
+    pInput->newWeapon = byte_28E3B0.at1c;
+    pInput->q16mlook = byte_28E3B0.at1d;
+    memset(&byte_28E3B0, 0, sizeof(byte_28E3B0));
+}
+
+void netMasterUpdate(void)
+{
+    if (myconnectindex != connecthead)
+        return;
+    char v4 = 0;
+    do
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+            if (gNetFifoMasterTail >= gNetFifoHead[p])
+            {
+                if (v4)
+                    return;
+                char *pPacket = packet;
+                PutPacketByte(pPacket, 254);
+                for (; p >= 0; p = connectpoint2[p])
+                    netSendPacket(p, packet, pPacket-packet);
+                return;
+            }
+        v4 = 1;
+        char *pPacket = packet;
+        PutPacketByte(pPacket, 0);
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            GINPUT *pInput = &gFifoInput[gNetFifoMasterTail&255][p];
+            if (pInput->buttonFlags.byte)
+                pInput->syncFlags.buttonChange = 1;
+            if (pInput->keyFlags.word)
+                pInput->syncFlags.keyChange = 1;
+            if (pInput->useFlags.byte)
+                pInput->syncFlags.useChange = 1;
+            if (pInput->newWeapon)
+                pInput->syncFlags.weaponChange = 1;
+            if (pInput->q16mlook)
+                pInput->syncFlags.mlookChange = 1;
+            PutPacketByte(pPacket, pInput->syncFlags.byte);
+            PutPacketWord(pPacket, pInput->forward);
+            PutPacketDWord(pPacket, pInput->q16turn);
+            PutPacketWord(pPacket, pInput->strafe);
+            if (pInput->syncFlags.buttonChange)
+                PutPacketByte(pPacket, pInput->buttonFlags.byte);
+            if (pInput->syncFlags.keyChange)
+                PutPacketWord(pPacket, pInput->keyFlags.word);
+            if (pInput->syncFlags.useChange)
+                PutPacketByte(pPacket, pInput->useFlags.byte);
+            if (pInput->syncFlags.weaponChange)
+                PutPacketByte(pPacket, pInput->newWeapon);
+            if (pInput->syncFlags.mlookChange)
+                PutPacketDWord(pPacket, pInput->q16mlook);
+        }
+        if ((gNetFifoMasterTail&15) == 0)
+        {
+            for (int p = connectpoint2[connecthead]; p >= 0; p = connectpoint2[p])
+                PutPacketByte(pPacket, ClipRange(myMinLag[p], -128, 127));
+            for (int p = connecthead; p >= 0; p = connectpoint2[p])
+                myMinLag[p] = 0x7fffffff;
+        }
+        while (gSendCheckTail != gCheckHead[myconnectindex])
+        {
+            PutPacketBuffer(pPacket, gCheckFifo[gSendCheckTail&255][myconnectindex], 16);
+            gSendCheckTail++;
+        }
+        for (int p = connectpoint2[connecthead]; p >= 0; p = connectpoint2[p])
+            netSendPacket(p, packet, pPacket-packet);
+        gNetFifoMasterTail++;
+    } while (1);
+}
+
+void netGetInput(void)
+{
+    if (numplayers > 1)
+        netGetPackets();
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        if (gNetFifoHead[myconnectindex]-200 > gNetFifoHead[p])
+            return;
+    ctrlGetInput();
+    sub_7AD90(&gInput);
+    if (gNetFifoHead[myconnectindex]&(gSyncRate-1))
+    {
+        GINPUT *pInput1 = &gFifoInput[gNetFifoHead[myconnectindex]&255][myconnectindex];
+        GINPUT *pInput2 = &gFifoInput[(gNetFifoHead[myconnectindex]-1)&255][myconnectindex];
+        memcpy(pInput1, pInput2, sizeof(GINPUT));
+        pInput1->keyFlags.word = 0;
+        pInput1->useFlags.byte = 0;
+        pInput1->newWeapon = 0;
+        pInput1->syncFlags.weaponChange = 0;
+        gNetFifoHead[myconnectindex]++;
+        return;
+    }
+    GINPUT *pInput = &gFifoInput[gNetFifoHead[myconnectindex]&255][myconnectindex];
+    sub_7AE2C(pInput);
+    memcpy(&gInput, pInput, sizeof(GINPUT));
+    gNetFifoHead[myconnectindex]++;
+    if (gGameOptions.nGameType == 0 || numplayers == 1)
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            if (p != myconnectindex)
+            {
+                GINPUT *pInput1 = &gFifoInput[(gNetFifoHead[p]-1)&255][p];
+                GINPUT *pInput2 = &gFifoInput[gNetFifoHead[p]&255][p];
+                memcpy(pInput2, pInput1, sizeof(GINPUT));
+                gNetFifoHead[p]++;
+            }
+        }
+        return;
+    }
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+    {
+        if (p != myconnectindex)
+        {
+            int nLag = gNetFifoHead[myconnectindex]-1-gNetFifoHead[p];
+            myMinLag[p] = ClipHigh(nLag, myMinLag[p]);
+            myMaxLag = ClipLow(nLag, myMaxLag);
+        }
+    }
+    if (((gNetFifoHead[myconnectindex]-1)&15) == 0)
+    {
+        int t = myMaxLag-gBufferJitter;
+        myMaxLag = 0;
+        if (t > 0)
+            gBufferJitter += (3+t)>>2;
+        else if (t < 0)
+            gBufferJitter -= (1-t)>>2;
+    }
+    if (gPacketMode == PACKETMODE_2)
+    {
+        char *pPacket = packet;
+        PutPacketByte(pPacket, 2);
+        if (((gNetFifoHead[myconnectindex]-1)&15) == 0)
+        {
+            if (myconnectindex == connecthead)
+            {
+                for (int p = connectpoint2[connecthead]; p >= 0; p = connectpoint2[p])
+                    PutPacketByte(pPacket, ClipRange(myMinLag[p], -128, 127));
+            }
+            else
+            {
+                int t = myMinLag[connecthead]-otherMinLag;
+                if (klabs(t) > 2)
+                {
+                    if (klabs(t) > 8)
+                    {
+                        if (t < 0)
+                            t++;
+                        t >>= 1;
+                    }
+                    else
+                        t = ksgn(t);
+                    gGameClock -= t<<2;
+                    otherMinLag += t;
+                    myMinLag[connecthead] -= t;
+                }
+            }
+            for (int p = connecthead; p >= 0; p = connectpoint2[p])
+                myMinLag[p] = 0x7fffffff;
+        }
+        if (gInput.buttonFlags.byte)
+            gInput.syncFlags.buttonChange = 1;
+        if (gInput.keyFlags.word)
+            gInput.syncFlags.keyChange = 1;
+        if (gInput.useFlags.byte)
+            gInput.syncFlags.useChange = 1;
+        if (gInput.newWeapon)
+            gInput.syncFlags.weaponChange = 1;
+        if (gInput.q16mlook)
+            gInput.syncFlags.mlookChange = 1;
+        PutPacketByte(pPacket, gInput.syncFlags.byte);
+        PutPacketWord(pPacket, gInput.forward);
+        PutPacketDWord(pPacket, gInput.q16turn);
+        PutPacketWord(pPacket, gInput.strafe);
+        if (gInput.syncFlags.buttonChange)
+            PutPacketByte(pPacket, gInput.buttonFlags.byte);
+        if (gInput.syncFlags.keyChange)
+            PutPacketWord(pPacket, gInput.keyFlags.word);
+        if (gInput.syncFlags.useChange)
+            PutPacketByte(pPacket, gInput.useFlags.byte);
+        if (gInput.syncFlags.weaponChange)
+            PutPacketByte(pPacket, gInput.newWeapon);
+        if (gInput.syncFlags.mlookChange)
+            PutPacketDWord(pPacket, gInput.q16mlook);
+        while (gSendCheckTail != gCheckHead[myconnectindex])
+        {
+            unsigned int *checkSum = gCheckFifo[gSendCheckTail&255][myconnectindex];
+            PutPacketBuffer(pPacket, checkSum, 16);
+            gSendCheckTail++;
+        }
+        netSendPacketAll(packet, pPacket-packet);
+        return;
+    }
+    if (myconnectindex != connecthead)
+    {
+        char *pPacket = packet;
+        PutPacketByte(pPacket, 1);
+        if (gInput.buttonFlags.byte)
+            gInput.syncFlags.buttonChange = 1;
+        if (gInput.keyFlags.word)
+            gInput.syncFlags.keyChange = 1;
+        if (gInput.useFlags.byte)
+            gInput.syncFlags.useChange = 1;
+        if (gInput.newWeapon)
+            gInput.syncFlags.weaponChange = 1;
+        if (gInput.q16mlook)
+            gInput.syncFlags.mlookChange = 1;
+        PutPacketByte(pPacket, gInput.syncFlags.byte);
+        PutPacketWord(pPacket, gInput.forward);
+        PutPacketDWord(pPacket, gInput.q16turn);
+        PutPacketWord(pPacket, gInput.strafe);
+        if (gInput.syncFlags.buttonChange)
+            PutPacketByte(pPacket, gInput.buttonFlags.byte);
+        if (gInput.syncFlags.keyChange)
+            PutPacketWord(pPacket, gInput.keyFlags.word);
+        if (gInput.syncFlags.useChange)
+            PutPacketByte(pPacket, gInput.useFlags.byte);
+        if (gInput.syncFlags.weaponChange)
+            PutPacketByte(pPacket, gInput.newWeapon);
+        if (gInput.syncFlags.mlookChange)
+            PutPacketDWord(pPacket, gInput.q16mlook);
+        if (((gNetFifoHead[myconnectindex]-1)&15) == 0)
+        {
+            int t = myMinLag[connecthead]-otherMinLag;
+            if (klabs(t) > 2)
+            {
+                if (klabs(t) > 8)
+                {
+                    if (t < 0)
+                        t++;
+                    t >>= 1;
+                }
+                else
+                    t = ksgn(t);
+                gGameClock -= t<<2;
+                otherMinLag += t;
+                myMinLag[connecthead] -= t;
+            }
+            for (int p = connecthead; p >= 0; p = connectpoint2[p])
+                myMinLag[p] = 0x7fffffff;
+        }
+        while (gSendCheckTail != gCheckHead[myconnectindex])
+        {
+            PutPacketBuffer(pPacket, gCheckFifo[gSendCheckTail&255][myconnectindex], 16);
+            gSendCheckTail++;
+        }
+        netSendPacket(connecthead, packet, pPacket-packet);
+        return;
+    }
+    netMasterUpdate();
+}
+
+void netInitialize(bool bConsole)
+{
+    netDeinitialize();
+    memset(gPlayerReady, 0, sizeof(gPlayerReady));
+    sub_79760();
+#ifndef NETCODE_DISABLE
+    char buffer[128];
+    gNetENetServer = gNetENetClient = NULL;
+    //gNetServerPacketHead = gNetServerPacketTail = 0;
+    gNetPacketHead = gNetPacketTail = 0;
+    if (gNetMode == NETWORK_NONE)
+    {
+        netResetToSinglePlayer();
+        return;
+    }
+    if (enet_initialize() != 0)
+    {
+        initprintf("An error occurred while initializing ENet.\n");
+        netResetToSinglePlayer();
+        return;
+    }
+    if (gNetMode == NETWORK_SERVER)
+    {
+        memset(gNetPlayerPeer, 0, sizeof(gNetPlayerPeer));
+        ENetEvent event;
+        gNetENetAddress.host = ENET_HOST_ANY;
+        gNetENetAddress.port = gNetPort;
+        gNetENetServer = enet_host_create(&gNetENetAddress, 8, BLOOD_ENET_CHANNEL_MAX, 0, 0);
+        if (!gNetENetServer)
+        {
+            initprintf("An error occurred while trying to create an ENet server host.\n");
+            netDeinitialize();
+            netResetToSinglePlayer();
+            return;
+        }
+
+        numplayers = 1;
+        // Wait for clients
+        if (!bConsole)
+        {
+            char buffer[128];
+            sprintf(buffer, "Waiting for players (%i\\%i)", numplayers, gNetPlayers);
+            viewLoadingScreen(2518, "Network Game", NULL, buffer);
+        }
+        while (numplayers < gNetPlayers)
+        {
+            handleevents();
+            if (quitevent)
+            {
+                netServerDisconnect();
+                QuitGame();
+            }
+            if (!bConsole && KB_KeyPressed(sc_Escape))
+            {
+                netServerDisconnect();
+                netDeinitialize();
+                netResetToSinglePlayer();
+                return;
+            }
+            enet_host_service(gNetENetServer, NULL, 0);
+            if (enet_host_check_events(gNetENetServer, &event) > 0)
+            {
+                switch (event.type)
+                {
+                case ENET_EVENT_TYPE_CONNECT:
+                {
+                    char ipaddr[32];
+
+                    enet_address_get_host_ip(&event.peer->address, ipaddr, sizeof(ipaddr));
+                    initprintf("Client connected: %s:%u\n", ipaddr, event.peer->address.port);
+                    numplayers++;
+                    for (int i = 1; i < kMaxPlayers; i++)
+                    {
+                        if (gNetPlayerPeer[i] == NULL)
+                        {
+                            gNetPlayerPeer[i] = event.peer;
+                            break;
+                        }
+                    }
+                    if (!bConsole)
+                    {
+                        char buffer[128];
+                        sprintf(buffer, "Waiting for players (%i\\%i)", numplayers, gNetPlayers);
+                        viewLoadingScreen(2518, "Network Game", NULL, buffer);
+                    }
+                    break;
+                }
+                case ENET_EVENT_TYPE_DISCONNECT:
+                {
+                    char ipaddr[32];
+
+                    enet_address_get_host_ip(&event.peer->address, ipaddr, sizeof(ipaddr));
+                    initprintf("Client disconnected: %s:%u\n", ipaddr, event.peer->address.port);
+                    numplayers--;
+                    for (int i = 1; i < kMaxPlayers; i++)
+                    {
+                        if (gNetPlayerPeer[i] == event.peer)
+                        {
+                            gNetPlayerPeer[i] = NULL;
+                            for (int j = kMaxPlayers-1; j > i; j--)
+                            {
+                                if (gNetPlayerPeer[j])
+                                {
+                                    gNetPlayerPeer[i] = gNetPlayerPeer[j];
+                                    gNetPlayerPeer[j] = NULL;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (!bConsole)
+                    {
+                        char buffer[128];
+                        sprintf(buffer, "Waiting for players (%i\\%i)", numplayers, gNetPlayers);
+                        viewLoadingScreen(2518, "Network Game", NULL, buffer);
+                    }
+                    break;
+                }
+                default:
+                    break;
+                }
+            }
+            enet_host_service(gNetENetServer, NULL, 0);
+        }
+        initprintf("All clients connected\n");
+
+        dassert(numplayers >= 1);
+
+        gInitialNetPlayers = gNetPlayers = numplayers;
+        connecthead = 0;
+        for (int i = 0; i < numplayers-1; i++)
+            connectpoint2[i] = i+1;
+        connectpoint2[numplayers-1] = -1;
+
+        enet_host_service(gNetENetServer, NULL, 0);
+
+        // Send connect info
+        char *pPacket = packet;
+        PutPacketByte(pPacket, BLOOD_SERVICE_CONNECTINFO);
+        PKT_CONNECTINFO connectinfo;
+        connectinfo.numplayers = numplayers;
+        connectinfo.connecthead = connecthead;
+        for (int i = 0; i < numplayers; i++)
+            connectinfo.connectpoint2[i] = connectpoint2[i];
+        PutPacketBuffer(pPacket, &connectinfo, sizeof(connectinfo));
+        ENetPacket *pEPacket = enet_packet_create(packet, pPacket-packet, ENET_PACKET_FLAG_RELIABLE);
+        enet_host_broadcast(gNetENetServer, BLOOD_ENET_SERVICE, pEPacket);
+        //enet_packet_destroy(pEPacket);
+        
+        enet_host_service(gNetENetServer, NULL, 0);
+
+        // Send id
+        myconnectindex = 0;
+        for (int i = 1; i < numplayers; i++)
+        {
+            if (!gNetPlayerPeer[i])
+                continue;
+            char *pPacket = packet;
+            PutPacketByte(pPacket, BLOOD_SERVICE_CONNECTID);
+            PutPacketByte(pPacket, i);
+            ENetPacket *pEPacket = enet_packet_create(packet, pPacket-packet, ENET_PACKET_FLAG_RELIABLE);
+            enet_peer_send(gNetPlayerPeer[i], BLOOD_ENET_SERVICE, pEPacket);
+
+            enet_host_service(gNetENetServer, NULL, 0);
+        }
+        
+        enet_host_service(gNetENetServer, NULL, 0);
+    }
+    else if (gNetMode == NETWORK_CLIENT)
+    {
+        ENetEvent event;
+        sprintf(buffer, "Connecting to %s:%u", gNetAddress, gNetPort);
+        initprintf("%s\n", buffer);
+        if (!bConsole)
+        {
+            viewLoadingScreen(2518, "Network Game", NULL, buffer);
+        }
+        gNetENetClient = enet_host_create(NULL, 1, BLOOD_ENET_CHANNEL_MAX, 0, 0);
+        enet_address_set_host(&gNetENetAddress, gNetAddress);
+        gNetENetAddress.port = gNetPort;
+        gNetENetPeer = enet_host_connect(gNetENetClient, &gNetENetAddress, BLOOD_ENET_CHANNEL_MAX, 0);
+        if (!gNetENetPeer)
+        {
+            initprintf("No available peers for initiating an ENet connection.\n");
+            netDeinitialize();
+            netResetToSinglePlayer();
+            return;
+        }
+        if (enet_host_service(gNetENetClient, &event, 5000) > 0 && event.type == ENET_EVENT_TYPE_CONNECT)
+            initprintf("Connected to %s:%i\n", gNetAddress, gNetPort);
+        else
+        {
+            initprintf("Could not connect to %s:%i\n", gNetAddress, gNetPort);
+            netDeinitialize();
+            return;
+        }
+        bool bWaitServer = true;
+        if (!bConsole)
+        {
+            viewLoadingScreen(2518, "Network Game", NULL, "Waiting for server response");
+        }
+        while (bWaitServer)
+        {
+            handleevents();
+            if (quitevent)
+            {
+                netClientDisconnect();
+                QuitGame();
+            }
+            if (!bConsole && KB_KeyPressed(sc_Escape))
+            {
+                netClientDisconnect();
+                netDeinitialize();
+                netResetToSinglePlayer();
+                return;
+            }
+            enet_host_service(gNetENetClient, NULL, 0);
+            if (enet_host_check_events(gNetENetClient, &event) > 0)
+            {
+                switch (event.type)
+                {
+                case ENET_EVENT_TYPE_DISCONNECT:
+                    initprintf("Lost connection to server\n");
+                    netDeinitialize();
+                    netResetToSinglePlayer();
+                    return;
+                case ENET_EVENT_TYPE_RECEIVE:
+                    //initprintf("NETEVENT\n");
+                    if (event.channelID == BLOOD_ENET_SERVICE)
+                    {
+                        char *pPacket = (char*)event.packet->data;
+                        switch (GetPacketByte(pPacket))
+                        {
+                        case BLOOD_SERVICE_CONNECTINFO:
+                        {
+                            PKT_CONNECTINFO *connectinfo = (PKT_CONNECTINFO*)pPacket;
+                            gInitialNetPlayers = gNetPlayers = numplayers = connectinfo->numplayers;
+                            connecthead = connectinfo->connecthead;
+                            for (int i = 0; i < numplayers; i++)
+                                connectpoint2[i] = connectinfo->connectpoint2[i];
+                            //initprintf("BLOOD_SERVICE_CONNECTINFO\n");
+                            break;
+                        }
+                        case BLOOD_SERVICE_CONNECTID:
+                            dassert(numplayers > 1);
+                            myconnectindex = GetPacketByte(pPacket);
+                            bWaitServer = false;
+                            //initprintf("BLOOD_SERVICE_CONNECTID\n");
+                            break;
+                        }
+                    }
+                    enet_packet_destroy(event.packet);
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+        enet_host_service(gNetENetClient, NULL, 0);
+        initprintf("Successfully connected to server\n");
+    }
+    gNetENetInit = true;
+    gGameOptions.nGameType = 2;
+#endif
+}
+
+void netDeinitialize(void)
+{
+#ifndef NETCODE_DISABLE
+    if (!gNetENetInit)
+        return;
+    gNetENetInit = false;
+    if (gNetMode != NETWORK_NONE)
+    {
+        if (gNetENetServer)
+        {
+            netServerDisconnect();
+            enet_host_destroy(gNetENetServer);
+        }
+        else if (gNetENetClient)
+        {
+            netClientDisconnect();
+            enet_host_destroy(gNetENetClient);
+        }
+        enet_deinitialize();
+    }
+    gNetENetServer = gNetENetClient = NULL;
+#endif
+}
+
+#ifndef NETCODE_DISABLE
+void netPostEPacket(ENetPacket *pEPacket)
+{
+    //static int number;
+    //initprintf("netPostEPacket %i\n", number++);
+    gNetPacketFifo[gNetPacketHead] = pEPacket;
+    gNetPacketHead = (gNetPacketHead+1)%kENetFifoSize;
+}
+#endif
+
+void netUpdate(void)
+{
+#ifndef NETCODE_DISABLE
+    ENetEvent event;
+    if (gNetMode == NETWORK_NONE)
+        return;
+
+    if (gNetENetServer)
+    {
+        enet_host_service(gNetENetServer, NULL, 0);
+        while (enet_host_check_events(gNetENetServer, &event) > 0)
+        {
+            switch (event.type)
+            {
+            case ENET_EVENT_TYPE_DISCONNECT:
+            {
+                int nPlayer;
+                for (nPlayer = connectpoint2[connecthead]; nPlayer >= 0; nPlayer = connectpoint2[nPlayer])
+                    if (gNetPlayerPeer[nPlayer] == event.peer)
+                        break;
+
+                for (int p = 0; p < kMaxPlayers; p++)
+                    if (gNetPlayerPeer[p] == event.peer)
+                        gNetPlayerPeer[p] = NULL;
+                if (nPlayer >= 0)
+                {
+                    // TODO: Game most likely will go out of sync here...
+                    char *pPacket = packet;
+                    PutPacketByte(pPacket, 249);
+                    PutPacketDWord(pPacket, nPlayer);
+                    netSendPacketAll(packet, pPacket - packet);
+                    netPlayerQuit(nPlayer);
+                    netWaitForEveryone(0);
+                }
+                if (gNetPlayers <= 1)
+                {
+                    netDeinitialize();
+                    netResetToSinglePlayer();
+                    return;
+                }
+                break;
+            }
+            case ENET_EVENT_TYPE_RECEIVE:
+                switch (event.channelID)
+                {
+                case BLOOD_ENET_SERVICE:
+                {
+                    char *pPacket = (char*)event.packet->data;
+                    if (GetPacketByte(pPacket) != BLOOD_SERVICE_SENDTOID)
+                        ThrowError("Packet error\n");
+                    int nDest = GetPacketByte(pPacket);
+                    int nSource = GetPacketByte(pPacket);
+                    int nSize = event.packet->dataLength-3;
+                    if (gNetPlayerPeer[nDest] != NULL)
+                    {
+                        ENetPacket *pNetPacket = enet_packet_create(NULL, nSize + 1, ENET_PACKET_FLAG_RELIABLE);
+                        char *pPBuffer = (char*)pNetPacket->data;
+                        PutPacketByte(pPBuffer, nSource);
+                        PutPacketBuffer(pPBuffer, pPacket, nSize);
+                        enet_peer_send(gNetPlayerPeer[nDest], BLOOD_ENET_GAME, pNetPacket);
+                        enet_host_service(gNetENetServer, NULL, 0);
+                    }
+                    enet_packet_destroy(event.packet);
+                    break;
+                }
+                case BLOOD_ENET_GAME:
+                    netPostEPacket(event.packet);
+                    break;
+                }
+            default:
+                break;
+            }
+            enet_host_service(gNetENetServer, NULL, 0);
+        }
+        enet_host_service(gNetENetServer, NULL, 0);
+    }
+    else if (gNetENetClient)
+    {
+        enet_host_service(gNetENetClient, NULL, 0);
+        while (enet_host_check_events(gNetENetClient, &event) > 0)
+        {
+            switch (event.type)
+            {
+            case ENET_EVENT_TYPE_DISCONNECT:
+                initprintf("Lost connection to server\n");
+                netDeinitialize();
+                netResetToSinglePlayer();
+                gQuitGame = gRestartGame = true;
+                return;
+            case ENET_EVENT_TYPE_RECEIVE:
+                switch (event.channelID)
+                {
+                case BLOOD_ENET_SERVICE:
+                {
+                    ThrowError("Packet error\n");
+                    break;
+                }
+                case BLOOD_ENET_GAME:
+                    netPostEPacket(event.packet);
+                    break;
+                }
+            default:
+                break;
+            }
+            enet_host_service(gNetENetClient, NULL, 0);
+        }
+        enet_host_service(gNetENetClient, NULL, 0);
+    }
+#endif
+}
+
+void faketimerhandler(void)
+{
+    timerUpdate();
+#ifndef NETCODE_DISABLE
+    if (gNetMode != NETWORK_NONE && gNetENetInit)
+        netUpdate();
+#if 0
+    if (gGameClock >= gNetFifoClock && ready2send)
+    {
+        gNetFifoClock += 4;
+        netGetInput();
+    }
+#endif
+#endif
+    //if (gNetMode != NETWORK_NONE && gNetENetInit)
+    //    enet_host_service(gNetMode == NETWORK_SERVER ? gNetENetServer : gNetENetClient, NULL, 0);
+}
+
+void netPlayerQuit(int nPlayer)
+{
+    char buffer[128];
+    sprintf(buffer, "%s left the game with %d frags.", gProfile[nPlayer].name, gPlayer[nPlayer].at2c6);
+    viewSetMessage(buffer);
+    if (gGameStarted)
+    {
+        seqKill(3, gPlayer[nPlayer].pSprite->extra);
+        actPostSprite(gPlayer[nPlayer].at5b, kStatFree);
+        if (nPlayer == gViewIndex)
+            gViewIndex = myconnectindex;
+        gView = &gPlayer[gViewIndex];
+    }
+    if (nPlayer == connecthead)
+    {
+        connecthead = connectpoint2[connecthead];
+        //if (gPacketMode == PACKETMODE_1)
+        {
+            //byte_27B2CC = 1;
+            gQuitGame = true;
+            gRestartGame = true;
+            gNetPlayers = 1;
+            //gQuitRequest = 1;
+        }
+    }
+    else
+    {
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            if (connectpoint2[p] == nPlayer)
+                connectpoint2[p] = connectpoint2[nPlayer];
+        }
+#ifndef NETCODE_DISABLE
+        gNetPlayerPeer[nPlayer] = NULL;
+#endif
+    }
+    gNetPlayers--;
+    numplayers = ClipLow(numplayers-1, 1);
+    if (gNetPlayers <= 1)
+    {
+        netDeinitialize();
+        netResetToSinglePlayer();
+    }
+}
diff --git a/source/blood/src/network.h b/source/blood/src/network.h
new file mode 100644
index 000000000..a6c8f34b1
--- /dev/null
+++ b/source/blood/src/network.h
@@ -0,0 +1,153 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "controls.h"
+
+enum PACKETMODE {
+    PACKETMODE_0 = 0,
+    PACKETMODE_1,
+    PACKETMODE_2,
+    PACKETMODE_3,
+};
+
+enum NETWORKMODE {
+    NETWORK_NONE = 0,
+    NETWORK_SERVER,
+    NETWORK_CLIENT
+};
+
+#define kNetDefaultPort 23513
+
+extern char packet[576];
+extern bool gStartNewGame;
+extern PACKETMODE gPacketMode;
+extern int gNetFifoClock;
+extern int gNetFifoTail;
+extern int gNetFifoHead[8];
+extern int gPredictTail;
+extern int gNetFifoMasterTail;
+extern GINPUT gFifoInput[256][8];
+extern int myMinLag[8];
+extern int otherMinLag;
+extern int myMaxLag;
+extern unsigned int gChecksum[4];
+extern unsigned int gCheckFifo[256][8][4];
+extern int gCheckHead[8];
+extern int gSendCheckTail;
+extern int gCheckTail;
+extern int gInitialNetPlayers;
+extern int gBufferJitter;
+extern int gPlayerReady[8];
+extern int gSyncRate;
+extern bool bNoResend;
+extern bool gRobust;
+extern bool bOutOfSync;
+extern bool ready2send;
+extern NETWORKMODE gNetMode;
+extern char gNetAddress[32];
+extern int gNetPort;
+
+
+struct PKT_STARTGAME {
+    short version;
+    char gameType, difficulty, monsterSettings, weaponSettings, itemSettings, respawnSettings;
+    char episodeId, levelId;
+    int unk;
+    char userMap, userMapName[13];
+};
+
+extern PKT_STARTGAME gPacketStartGame;
+
+
+inline void PutPacketByte(char *&p, int a2)
+{
+    //dassert(p - packet + 1 < sizeof(packet));
+    *p++ = a2;
+}
+
+inline void PutPacketWord(char *&p, int a2)
+{
+    //dassert(p - packet + 2 < sizeof(packet));
+    *(short*)p = a2;
+    p += 2;
+}
+
+inline void PutPacketDWord(char *&p, int a2)
+{
+    //dassert(p - packet + 4 < sizeof(packet));
+    *(int*)p = a2;
+    p += 4;
+}
+
+inline void PutPacketBuffer(char *&p, const void *pBuffer, int size)
+{
+    //dassert(p + size < packet + sizeof(packet));
+    memcpy(p, pBuffer, size);
+    p += size;
+}
+
+inline char GetPacketByte(char *&p)
+{
+    return *p++;
+}
+
+inline short GetPacketWord(char *&p)
+{
+    short t = *(short*)p;
+    p += 2;
+    return t;
+}
+
+inline int GetPacketDWord(char *&p)
+{
+    int t = *(int*)p;
+    p += 4;
+    return t;
+}
+
+inline void GetPacketBuffer(char *&p, void *pBuffer, int size)
+{
+    //dassert(p + size < packet + sizeof(packet));
+    memcpy(pBuffer, p, size);
+    p += size;
+}
+
+void sub_79760(void);
+void netResetToSinglePlayer(void);
+void netBroadcastMessage(int nPlayer, const char *pzMessage);
+void netWaitForEveryone(char a1);
+void sub_7AC28(const char *pzString);
+void netGetPackets(void);
+void netBroadcastTaunt(int nPlayer, int nTaunt);
+void CalcGameChecksum(void);
+void netBroadcastPlayerLogoff(int nPlayer);
+void netBroadcastMyLogoff(bool bRestart);
+void netInitialize(bool bConsole);
+void netBroadcastPlayerInfo(int nPlayer);
+void netCheckSync(void);
+void netMasterUpdate(void);
+void netGetInput(void);
+void netPlayerQuit(int nPlayer);
+void netUpdate(void);
+void netDeinitialize(void);
+void netBroadcastNewGame(void);
diff --git a/source/blood/src/osdcmd.cpp b/source/blood/src/osdcmd.cpp
new file mode 100644
index 000000000..b98d0fc57
--- /dev/null
+++ b/source/blood/src/osdcmd.cpp
@@ -0,0 +1,1161 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#include "build.h"
+#include "baselayer.h"
+#include "keyboard.h"
+#include "control.h"
+#include "osd.h"
+#include "compat.h"
+#include "mmulti.h"
+#include "common_game.h"
+#include "config.h"
+#include "blood.h"
+#include "demo.h"
+#include "gamemenu.h"
+#include "levels.h"
+#include "messages.h"
+#include "network.h"
+#include "osdcmds.h"
+#include "screen.h"
+#include "sound.h"
+#include "sfx.h"
+#include "view.h"
+
+
+static inline int osdcmd_quit(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+    OSD_ShowDisplay(0);
+    QuitGame();
+    return OSDCMD_OK;
+}
+
+static int osdcmd_changelevel(osdcmdptr_t parm)
+{
+    int32_t volume,level;
+    char *p;
+
+    if (parm->numparms != 2) return OSDCMD_SHOWHELP;
+
+    volume = strtol(parm->parms[0], &p, 10) - 1;
+    if (p[0]) return OSDCMD_SHOWHELP;
+    level = strtol(parm->parms[1], &p, 10) - 1;
+    if (p[0]) return OSDCMD_SHOWHELP;
+
+    if (volume < 0) return OSDCMD_SHOWHELP;
+    if (level < 0) return OSDCMD_SHOWHELP;
+
+    if (volume >= 6)
+    {
+        OSD_Printf("changelevel: invalid volume number (range 1-%d)\n",6);
+        return OSDCMD_OK;
+    }
+
+    if (level >= gEpisodeInfo[volume].nLevels)
+    {
+        OSD_Printf("changelevel: invalid level number\n");
+        return OSDCMD_SHOWHELP;
+    }
+
+    if (gDemo.at1)
+        gDemo.StopPlayback();
+
+    if (numplayers > 1)
+    {
+        gPacketStartGame.episodeId = volume;
+        gPacketStartGame.levelId = level;
+        netBroadcastNewGame();
+        gStartNewGame = 1;
+        gGameMenuMgr.Deactivate();
+        return OSDCMD_OK;
+    }
+    levelSetupOptions(volume, level);
+    StartLevel(&gGameOptions);
+    viewResizeView(gViewSize);
+    gGameMenuMgr.Deactivate();
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_map(osdcmdptr_t parm)
+{
+    char filename[BMAX_PATH];
+
+    const int32_t wildcardp = parm->numparms==1 &&
+        (Bstrchr(parm->parms[0], '*') != NULL);
+
+    if (parm->numparms != 1 || wildcardp)
+    {
+        CACHE1D_FIND_REC *r;
+        fnlist_t fnlist = FNLIST_INITIALIZER;
+        int32_t maxwidth = 0;
+
+        if (wildcardp)
+            maybe_append_ext(filename, sizeof(filename), parm->parms[0], ".map");
+        else
+            Bstrcpy(filename, "*.MAP");
+
+        fnlist_getnames(&fnlist, "/", filename, -1, 0);
+        gSysRes.FNAddFiles(&fnlist, filename);
+
+        for (r=fnlist.findfiles; r; r=r->next)
+            maxwidth = max<int>(maxwidth, Bstrlen(r->name));
+
+        if (maxwidth > 0)
+        {
+            int32_t x = 0;
+            maxwidth += 3;
+            OSD_Printf(OSDTEXT_RED "Map listing:\n");
+            for (r=fnlist.findfiles; r; r=r->next)
+            {
+                OSD_Printf("%-*s",maxwidth,r->name);
+                x += maxwidth;
+                if (x > OSD_GetCols() - maxwidth)
+                {
+                    x = 0;
+                    OSD_Printf("\n");
+                }
+            }
+            if (x) OSD_Printf("\n");
+            OSD_Printf(OSDTEXT_RED "Found %d maps\n", fnlist.numfiles);
+        }
+
+        fnlist_clearnames(&fnlist);
+
+        return OSDCMD_SHOWHELP;
+    }
+
+    strcpy(filename, parm->parms[0]);
+    ChangeExtension(filename, "");
+
+    if (!gSysRes.Lookup(filename, "MAP"))
+    {
+        OSD_Printf(OSD_ERROR "map: file \"%s\" not found.\n", filename);
+        return OSDCMD_OK;
+    }
+
+    if (gDemo.at1)
+        gDemo.StopPlayback();
+
+    levelAddUserMap(filename);
+
+    if (numplayers > 1)
+    {
+        gPacketStartGame.episodeId = gGameOptions.nEpisode;
+        gPacketStartGame.levelId = gGameOptions.nLevel;
+        netBroadcastNewGame();
+        gStartNewGame = 1;
+        gGameMenuMgr.Deactivate();
+        return OSDCMD_OK;
+    }
+    levelSetupOptions(gGameOptions.nEpisode, gGameOptions.nLevel);
+    StartLevel(&gGameOptions);
+    viewResizeView(gViewSize);
+    gGameMenuMgr.Deactivate();
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_demo(osdcmdptr_t parm)
+{
+    if (numplayers > 1)
+    {
+        OSD_Printf("Command not allowed in multiplayer\n");
+        return OSDCMD_OK;
+    }
+
+    //if (g_player[myconnectindex].ps->gm & MODE_GAME)
+    //{
+    //    OSD_Printf("demo: Must not be in a game.\n");
+    //    return OSDCMD_OK;
+    //}
+
+    if (parm->numparms != 1/* && parm->numparms != 2*/)
+        return OSDCMD_SHOWHELP;
+
+    gDemo.SetupPlayback(parm->parms[0]);
+    gGameStarted = 0;
+    gDemo.Playback();
+
+    return OSDCMD_OK;
+}
+
+int osdcmd_restartvid(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+    videoResetMode();
+    if (videoSetGameMode(gSetup.fullscreen,gSetup.xdim,gSetup.ydim,gSetup.bpp,0))
+        ThrowError("restartvid: Reset failed...\n");
+    onvideomodechange(gSetup.bpp>8);
+    viewResizeView(gViewSize);
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_music(osdcmdptr_t parm)
+{
+    char buffer[128];
+    if (parm->numparms == 1)
+    {
+        int32_t sel = levelGetMusicIdx(parm->parms[0]);
+
+        if (sel == -1)
+            return OSDCMD_SHOWHELP;
+
+        if (sel == -2)
+        {
+            OSD_Printf("%s is not a valid episode/level number pair\n", parm->parms[0]);
+            return OSDCMD_OK;
+        }
+
+        int nEpisode = sel/kMaxLevels;
+        int nLevel = sel%kMaxLevels;
+
+        if (!levelTryPlayMusic(nEpisode, nLevel))
+        {
+            if (CDAudioToggle)
+                snprintf(buffer, sizeof(buffer), "Playing %i track", gEpisodeInfo[nEpisode].at28[nLevel].ate0);
+            else
+                snprintf(buffer, sizeof(buffer), "Playing %s", gEpisodeInfo[nEpisode].at28[nLevel].atd0);
+            viewSetMessage(buffer);
+        }
+        else
+        {
+            OSD_Printf("No music defined for %s\n", parm->parms[0]);
+        }
+
+        return OSDCMD_OK;
+    }
+
+    return OSDCMD_SHOWHELP;
+}
+
+static int osdcmd_vidmode(osdcmdptr_t parm)
+{
+    int32_t newbpp = gSetup.bpp, newwidth = gSetup.xdim,
+            newheight = gSetup.ydim, newfs = gSetup.fullscreen;
+    int32_t tmp;
+
+    if (parm->numparms < 1 || parm->numparms > 4) return OSDCMD_SHOWHELP;
+
+    switch (parm->numparms)
+    {
+    case 1: // bpp switch
+        tmp = Batol(parm->parms[0]);
+        if (!(tmp==8 || tmp==16 || tmp==32))
+            return OSDCMD_SHOWHELP;
+        newbpp = tmp;
+        break;
+    case 2: // res switch
+        newwidth = Batol(parm->parms[0]);
+        newheight = Batol(parm->parms[1]);
+        break;
+    case 3: // res & bpp switch
+    case 4:
+        newwidth = Batol(parm->parms[0]);
+        newheight = Batol(parm->parms[1]);
+        tmp = Batol(parm->parms[2]);
+        if (!(tmp==8 || tmp==16 || tmp==32))
+            return OSDCMD_SHOWHELP;
+        newbpp = tmp;
+        if (parm->numparms == 4)
+            newfs = (Batol(parm->parms[3]) != 0);
+        break;
+    }
+
+    if (videoSetGameMode(newfs,newwidth,newheight,newbpp,upscalefactor))
+    {
+        initprintf("vidmode: Mode change failed!\n");
+        if (videoSetGameMode(gSetup.fullscreen, gSetup.xdim, gSetup.ydim, gSetup.bpp, upscalefactor))
+            ThrowError("vidmode: Reset failed!\n");
+    }
+    gSetup.bpp = newbpp;
+    gSetup.xdim = newwidth;
+    gSetup.ydim = newheight;
+    gSetup.fullscreen = newfs;
+    onvideomodechange(gSetup.bpp>8);
+    viewResizeView(gViewSize);
+    return OSDCMD_OK;
+}
+
+static int osdcmd_crosshaircolor(osdcmdptr_t parm)
+{
+    if (parm->numparms != 3)
+    {
+        OSD_Printf("crosshaircolor: r:%d g:%d b:%d\n",CrosshairColors.r,CrosshairColors.g,CrosshairColors.b);
+        return OSDCMD_SHOWHELP;
+    }
+
+    uint8_t const r = Batol(parm->parms[0]);
+    uint8_t const g = Batol(parm->parms[1]);
+    uint8_t const b = Batol(parm->parms[2]);
+
+    g_isAlterDefaultCrosshair = true;
+    viewSetCrosshairColor(r,g,b);
+
+    if (!OSD_ParsingScript())
+        OSD_Printf("%s\n", parm->raw);
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_resetcrosshair(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+    g_isAlterDefaultCrosshair = false;
+    viewResetCrosshairToDefault();
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_give(osdcmdptr_t parm)
+{
+    if (numplayers != 1 || !gGameStarted || gMe->pXSprite->health == 0)
+    {
+        OSD_Printf("give: Cannot give while dead or not in a single-player game.\n");
+        return OSDCMD_OK;
+    }
+
+    if (parm->numparms != 1) return OSDCMD_SHOWHELP;
+
+    if (!Bstrcasecmp(parm->parms[0], "all"))
+    {
+        SetWeapons(true);
+        SetAmmo(true);
+        SetToys(true);
+        SetArmor(true);
+        SetKeys(true);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    else if (!Bstrcasecmp(parm->parms[0], "health"))
+    {
+        actHealDude(gMe->pXSprite, 200, 200);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    else if (!Bstrcasecmp(parm->parms[0], "weapons"))
+    {
+        SetWeapons(true);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    else if (!Bstrcasecmp(parm->parms[0], "ammo"))
+    {
+        SetAmmo(true);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    else if (!Bstrcasecmp(parm->parms[0], "armor"))
+    {
+        SetArmor(true);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    else if (!Bstrcasecmp(parm->parms[0], "keys"))
+    {
+        SetKeys(true);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    else if (!Bstrcasecmp(parm->parms[0], "inventory"))
+    {
+        SetToys(true);
+        gCheatMgr.m_bPlayerCheated = true;
+        return OSDCMD_OK;
+    }
+    return OSDCMD_SHOWHELP;
+}
+
+static int osdcmd_god(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+    if (numplayers == 1 && gGameStarted)
+    {
+        SetGodMode(!gMe->at31a);
+        gCheatMgr.m_bPlayerCheated = true;
+    }
+    else
+        OSD_Printf("god: Not in a single-player game.\n");
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_noclip(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+
+    if (numplayers == 1 && gGameStarted)
+    {
+        SetClipMode(!gNoClip);
+        gCheatMgr.m_bPlayerCheated = true;
+    }
+    else
+    {
+        OSD_Printf("noclip: Not in a single-player game.\n");
+    }
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_restartsound(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+    sfxTerm();
+    sndTerm();
+
+    sndInit();
+    sfxInit();
+
+    if (MusicToggle && (gGameStarted || gDemo.at1))
+        sndPlaySong(gGameOptions.zLevelSong, true);
+
+    return OSDCMD_OK;
+}
+
+void onvideomodechange(int32_t newmode)
+{
+    UNREFERENCED_PARAMETER(newmode);
+#if 0
+    uint8_t palid;
+
+    // XXX?
+    if (!newmode || g_player[screenpeek].ps->palette < BASEPALCOUNT)
+        palid = g_player[screenpeek].ps->palette;
+    else
+        palid = BASEPAL;
+
+#ifdef POLYMER
+    if (videoGetRenderMode() == REND_POLYMER)
+    {
+        int32_t i = 0;
+
+        while (i < MAXSPRITES)
+        {
+            if (actor[i].lightptr)
+            {
+                polymer_deletelight(actor[i].lightId);
+                actor[i].lightptr = NULL;
+                actor[i].lightId = -1;
+            }
+            i++;
+        }
+    }
+#endif
+
+    videoSetPalette(ud.brightness>>2, palid, 0);
+    g_restorePalette = -1;
+#endif
+    if (newmode)
+        scrResetPalette();
+    UpdateDacs(gLastPal, false);
+}
+
+static int osdcmd_button(osdcmdptr_t parm)
+{
+    static char const s_gamefunc_[] = "gamefunc_";
+    int constexpr strlen_gamefunc_  = ARRAY_SIZE(s_gamefunc_) - 1;
+
+    char const *p = parm->name + strlen_gamefunc_;
+
+//    if (g_player[myconnectindex].ps->gm == MODE_GAME) // only trigger these if in game
+    if (gInputMode == INPUT_MODE_0)
+        CONTROL_ButtonFlags[CONFIG_FunctionNameToNum(p)] = 1; // FIXME
+
+    return OSDCMD_OK;
+}
+
+const char *const ConsoleButtons[] =
+{
+    "mouse1", "mouse2", "mouse3", "mouse4", "mwheelup",
+    "mwheeldn", "mouse5", "mouse6", "mouse7", "mouse8"
+};
+
+static int osdcmd_bind(osdcmdptr_t parm)
+{
+    char buffer[256];
+    if (parm->numparms==1 && !Bstrcasecmp(parm->parms[0],"showkeys"))
+    {
+        for (int i=0; sctokeylut[i].key; i++)
+            OSD_Printf("%s\n",sctokeylut[i].key);
+        for (auto ConsoleButton : ConsoleButtons)
+            OSD_Printf("%s\n",ConsoleButton);
+        return OSDCMD_OK;
+    }
+
+    if (parm->numparms==0)
+    {
+        int j=0;
+
+        OSD_Printf("Current key bindings:\n");
+
+        for (int i=0; i<MAXBOUNDKEYS+MAXMOUSEBUTTONS; i++)
+            if (CONTROL_KeyIsBound(i))
+            {
+                j++;
+                OSD_Printf("%-9s %s\"%s\"\n", CONTROL_KeyBinds[i].key, CONTROL_KeyBinds[i].repeat?"":"norepeat ",
+                           CONTROL_KeyBinds[i].cmdstr);
+            }
+
+        if (j == 0)
+            OSD_Printf("No binds found.\n");
+
+        return OSDCMD_OK;
+    }
+
+    int i, j, repeat;
+
+    for (i=0; i < ARRAY_SSIZE(sctokeylut); i++)
+    {
+        if (!Bstrcasecmp(parm->parms[0], sctokeylut[i].key))
+            break;
+    }
+
+    // didn't find the key
+    if (i == ARRAY_SSIZE(sctokeylut))
+    {
+        for (i=0; i<MAXMOUSEBUTTONS; i++)
+            if (!Bstrcasecmp(parm->parms[0],ConsoleButtons[i]))
+                break;
+
+        if (i >= MAXMOUSEBUTTONS)
+            return OSDCMD_SHOWHELP;
+
+        if (parm->numparms < 2)
+        {
+            if (CONTROL_KeyBinds[MAXBOUNDKEYS + i].cmdstr && CONTROL_KeyBinds[MAXBOUNDKEYS + i ].key)
+                OSD_Printf("%-9s %s\"%s\"\n", ConsoleButtons[i], CONTROL_KeyBinds[MAXBOUNDKEYS + i].repeat?"":"norepeat ",
+                CONTROL_KeyBinds[MAXBOUNDKEYS + i].cmdstr);
+            else OSD_Printf("%s is unbound\n", ConsoleButtons[i]);
+            return OSDCMD_OK;
+        }
+
+        j = 1;
+
+        repeat = 1;
+        if (!Bstrcasecmp(parm->parms[j],"norepeat"))
+        {
+            repeat = 0;
+            j++;
+        }
+
+        Bstrcpy(buffer,parm->parms[j++]);
+        for (; j<parm->numparms; j++)
+        {
+            Bstrcat(buffer," ");
+            Bstrcat(buffer,parm->parms[j++]);
+        }
+
+        CONTROL_BindMouse(i, buffer, repeat, ConsoleButtons[i]);
+
+        if (!OSD_ParsingScript())
+            OSD_Printf("%s\n",parm->raw);
+        return OSDCMD_OK;
+    }
+
+    if (parm->numparms < 2)
+    {
+        if (CONTROL_KeyIsBound(sctokeylut[i].sc))
+            OSD_Printf("%-9s %s\"%s\"\n", sctokeylut[i].key, CONTROL_KeyBinds[sctokeylut[i].sc].repeat?"":"norepeat ",
+                       CONTROL_KeyBinds[sctokeylut[i].sc].cmdstr);
+        else OSD_Printf("%s is unbound\n", sctokeylut[i].key);
+
+        return OSDCMD_OK;
+    }
+
+    j = 1;
+
+    repeat = 1;
+    if (!Bstrcasecmp(parm->parms[j],"norepeat"))
+    {
+        repeat = 0;
+        j++;
+    }
+
+    Bstrcpy(buffer,parm->parms[j++]);
+    for (; j<parm->numparms; j++)
+    {
+        Bstrcat(buffer," ");
+        Bstrcat(buffer,parm->parms[j++]);
+    }
+
+    CONTROL_BindKey(sctokeylut[i].sc, buffer, repeat, sctokeylut[i].key);
+
+    char *cp = buffer;
+
+    // Populate the keyboard config menu based on the bind.
+    // Take care of processing one-to-many bindings properly, too.
+    static char const s_gamefunc_[] = "gamefunc_";
+    int constexpr strlen_gamefunc_  = ARRAY_SIZE(s_gamefunc_) - 1;
+
+    while ((cp = Bstrstr(cp, s_gamefunc_)))
+    {
+        cp += strlen_gamefunc_;
+
+        char *semi = Bstrchr(cp, ';');
+
+        if (semi)
+            *semi = 0;
+
+        j = CONFIG_FunctionNameToNum(cp);
+
+        if (semi)
+            cp = semi+1;
+
+        if (j != -1)
+        {
+            KeyboardKeys[j][1] = KeyboardKeys[j][0];
+            KeyboardKeys[j][0] = sctokeylut[i].sc;
+//            CONTROL_MapKey(j, sctokeylut[i].sc, ud.config.KeyboardKeys[j][0]);
+
+            if (j == gamefunc_Show_Console)
+                OSD_CaptureKey(sctokeylut[i].sc);
+        }
+    }
+
+    if (!OSD_ParsingScript())
+        OSD_Printf("%s\n",parm->raw);
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_unbindall(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_CONST_PARAMETER(parm);
+
+    for (int i = 0; i < MAXBOUNDKEYS; ++i)
+        CONTROL_FreeKeyBind(i);
+
+    for (int i = 0; i < MAXMOUSEBUTTONS; ++i)
+        CONTROL_FreeMouseBind(i);
+
+    for (auto &KeyboardKey : KeyboardKeys)
+        KeyboardKey[0] = KeyboardKey[1] = 0xff;
+
+    if (!OSD_ParsingScript())
+        OSD_Printf("unbound all controls\n");
+
+    return OSDCMD_OK;
+}
+
+static int osdcmd_unbind(osdcmdptr_t parm)
+{
+    if (parm->numparms != 1)
+        return OSDCMD_SHOWHELP;
+
+    for (auto ConsoleKey : sctokeylut)
+    {
+        if (ConsoleKey.key && !Bstrcasecmp(parm->parms[0], ConsoleKey.key))
+        {
+            CONTROL_FreeKeyBind(ConsoleKey.sc);
+            OSD_Printf("unbound key %s\n", ConsoleKey.key);
+            return OSDCMD_OK;
+        }
+    }
+
+    for (int i = 0; i < MAXMOUSEBUTTONS; i++)
+    {
+        if (!Bstrcasecmp(parm->parms[0], ConsoleButtons[i]))
+        {
+            CONTROL_FreeMouseBind(i);
+            OSD_Printf("unbound %s\n", ConsoleButtons[i]);
+            return OSDCMD_OK;
+        }
+    }
+
+    return OSDCMD_SHOWHELP;
+}
+
+static int osdcmd_screenshot(osdcmdptr_t parm)
+{
+    static const char *fn = "blud0000.png";
+
+    if (parm->numparms == 1 && !Bstrcasecmp(parm->parms[0], "tga"))
+        videoCaptureScreenTGA(fn, 0);
+    else videoCaptureScreen(fn, 0);
+
+    return OSDCMD_OK;
+}
+
+#if 0
+static int osdcmd_savestate(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_PARAMETER(parm);
+    G_SaveMapState();
+    return OSDCMD_OK;
+}
+
+static int osdcmd_restorestate(osdcmdptr_t UNUSED(parm))
+{
+    UNREFERENCED_PARAMETER(parm);
+    G_RestoreMapState();
+    return OSDCMD_OK;
+}
+#endif
+
+#if 0
+#ifdef DEBUGGINGAIDS
+static int osdcmd_inittimer(osdcmdptr_t parm)
+{
+    if (parm->numparms != 1)
+    {
+        OSD_Printf("%dHz timer\n",g_timerTicsPerSecond);
+        return OSDCMD_SHOWHELP;
+    }
+
+    G_InitTimer(Batol(parm->parms[0]));
+
+    OSD_Printf("%s\n",parm->raw);
+    return OSDCMD_OK;
+}
+#endif
+#endif
+
+static int osdcmd_cvar_set_game(osdcmdptr_t parm)
+{
+    int const r = osdcmd_cvar_set(parm);
+
+    if (r != OSDCMD_OK) return r;
+
+    if (!Bstrcasecmp(parm->name, "r_upscalefactor"))
+    {
+        if (in3dmode())
+        {
+            videoSetGameMode(fullscreen, xres, yres, bpp, gUpscaleFactor);
+        }
+    }
+    else if (!Bstrcasecmp(parm->name, "r_size"))
+    {
+        //ud.statusbarmode = (ud.screen_size < 8);
+        viewResizeView(gViewSize);
+    }
+    else if (!Bstrcasecmp(parm->name, "r_maxfps") || !Bstrcasecmp(parm->name, "r_maxfpsoffset"))
+    {
+        if (r_maxfps != 0) r_maxfps = clamp(r_maxfps, 30, 1000);
+        g_frameDelay = calcFrameDelay(r_maxfps + r_maxfpsoffset);
+    }
+    else if (!Bstrcasecmp(parm->name, "r_ambientlight"))
+    {
+        if (r_ambientlight == 0)
+            r_ambientlightrecip = 256.f;
+        else r_ambientlightrecip = 1.f/r_ambientlight;
+    }
+    else if (!Bstrcasecmp(parm->name, "in_mouse"))
+    {
+        CONTROL_MouseEnabled = (gSetup.usemouse && CONTROL_MousePresent);
+    }
+    else if (!Bstrcasecmp(parm->name, "in_joystick"))
+    {
+        CONTROL_JoystickEnabled = (gSetup.usejoystick && CONTROL_JoyPresent);
+    }
+    else if (!Bstrcasecmp(parm->name, "vid_gamma"))
+    {
+        gBrightness = GAMMA_CALC;
+        gBrightness <<= 2;
+        videoSetPalette(gBrightness>>2,gLastPal,0);
+    }
+    else if (!Bstrcasecmp(parm->name, "vid_brightness") || !Bstrcasecmp(parm->name, "vid_contrast"))
+    {
+        videoSetPalette(gBrightness>>2,gLastPal,0);
+    }
+#if 0
+    else if (!Bstrcasecmp(parm->name, "hud_scale")
+             || !Bstrcasecmp(parm->name, "hud_statusbarmode")
+             || !Bstrcasecmp(parm->name, "r_rotatespritenowidescreen"))
+    {
+        G_UpdateScreenArea();
+    }
+    else if (!Bstrcasecmp(parm->name, "skill"))
+    {
+        if (numplayers > 1)
+            return r;
+
+        ud.player_skill = ud.m_player_skill;
+    }
+    else if (!Bstrcasecmp(parm->name, "color"))
+    {
+        ud.color = G_CheckPlayerColor(ud.color);
+        g_player[0].ps->palookup = g_player[0].pcolor = ud.color;
+    }
+    else if (!Bstrcasecmp(parm->name, "osdscale"))
+    {
+        osdrscale = 1.f/osdscale;
+
+        if (xdim && ydim)
+            OSD_ResizeDisplay(xdim, ydim);
+    }
+    else if (!Bstrcasecmp(parm->name, "wchoice"))
+    {
+        if (parm->numparms == 1)
+        {
+            if (g_forceWeaponChoice) // rewrite ud.wchoice because osdcmd_cvar_set already changed it
+            {
+                int j = 0;
+
+                while (j < 10)
+                {
+                    ud.wchoice[j] = g_player[myconnectindex].wchoice[j] + '0';
+                    j++;
+                }
+
+                ud.wchoice[j] = 0;
+            }
+            else
+            {
+                char const *c = parm->parms[0];
+
+                if (*c)
+                {
+                    int j = 0;
+
+                    while (*c && j < 10)
+                    {
+                        g_player[myconnectindex].wchoice[j] = *c - '0';
+                        c++;
+                        j++;
+                    }
+
+                    while (j < 10)
+                    {
+                        if (j == 9)
+                            g_player[myconnectindex].wchoice[9] = 1;
+                        else
+                            g_player[myconnectindex].wchoice[j] = 2;
+
+                        j++;
+                    }
+                }
+            }
+
+            g_forceWeaponChoice = 0;
+        }
+
+        /*    Net_SendClientInfo();*/
+    }
+#endif
+
+    return r;
+}
+
+static int osdcmd_cvar_set_multi(osdcmdptr_t parm)
+{
+    int const r = osdcmd_cvar_set_game(parm);
+
+    if (r != OSDCMD_OK) return r;
+
+    //G_UpdatePlayerFromMenu();
+
+    return r;
+}
+
+int32_t registerosdcommands(void)
+{
+    char buffer[256];
+    static osdcvardata_t cvars_game[] =
+    {
+        { "crosshair", "enable/disable crosshair", (void *)&gAimReticle, CVAR_BOOL, 0, 1 },
+
+        { "cl_autoaim", "enable/disable weapon autoaim", (void *)&gAutoAim, CVAR_INT|CVAR_MULTI, 0, 2 },
+//        { "cl_automsg", "enable/disable automatically sending messages to all players", (void *)&ud.automsg, CVAR_BOOL, 0, 1 },
+        { "cl_autorun", "enable/disable autorun", (void *)&gAutoRun, CVAR_BOOL, 0, 1 },
+//
+//        { "cl_autosave", "enable/disable autosaves", (void *) &ud.autosave, CVAR_BOOL, 0, 1 },
+//        { "cl_autosavedeletion", "enable/disable automatic deletion of autosaves", (void *) &ud.autosavedeletion, CVAR_BOOL, 0, 1 },
+//        { "cl_maxautosaves", "number of autosaves to keep before deleting the oldest", (void *) &ud.maxautosaves, CVAR_INT, 1, 100 },
+//
+//        { "cl_autovote", "enable/disable automatic voting", (void *)&ud.autovote, CVAR_INT, 0, 2 },
+//
+//        { "cl_cheatmask", "configure what cheats show in the cheats menu", (void *)&cl_cheatmask, CVAR_UINT, 0, ~0 },
+//
+//        { "cl_obituaries", "enable/disable multiplayer death messages", (void *)&ud.obituaries, CVAR_BOOL, 0, 1 },
+//        { "cl_democams", "enable/disable demo playback cameras", (void *)&ud.democams, CVAR_BOOL, 0, 1 },
+//
+//        { "cl_idplayers", "enable/disable name display when aiming at opponents", (void *)&ud.idplayers, CVAR_BOOL, 0, 1 },
+//
+
+        { "cl_interpolate", "enable/disable view interpolation", (void *)&gViewInterpolate, CVAR_BOOL, 0, 1 },
+        { "cl_viewhbob", "enable/disable view horizontal bobbing", (void *)&gViewHBobbing, CVAR_BOOL, 0, 1 },
+        { "cl_viewvbob", "enable/disable view vertical bobbing", (void *)&gViewVBobbing, CVAR_BOOL, 0, 1 },
+        { "cl_slopetilting", "enable/disable slope tilting", (void *)&gSlopeTilting, CVAR_BOOL, 0, 1 },
+        { "cl_showweapon", "enable/disable show weapons", (void *)&gShowWeapon, CVAR_BOOL, 0, 1 },
+
+        { "cl_runmode", "enable/disable modernized run key operation", (void *)&gRunKeyMode, CVAR_BOOL, 0, 1 },
+//
+//        { "cl_showcoords", "show your position in the game world", (void *)&ud.coords, CVAR_INT, 0,
+//#ifdef USE_OPENGL
+//          2
+//#else
+//          1
+//#endif
+//        },
+//
+//        { "cl_viewbob", "enable/disable player head bobbing", (void *)&ud.viewbob, CVAR_BOOL, 0, 1 },
+//
+//        { "cl_weaponsway", "enable/disable player weapon swaying", (void *)&ud.weaponsway, CVAR_BOOL, 0, 1 },
+        { "cl_weaponswitch", "enable/disable auto weapon switching", (void *)&gWeaponSwitch, CVAR_INT|CVAR_MULTI, 0, 3 },
+//
+//        { "color", "changes player palette", (void *)&ud.color, CVAR_INT|CVAR_MULTI, 0, MAXPALOOKUPS-1 },
+//
+//        { "crosshairscale","changes the size of the crosshair", (void *)&ud.crosshairscale, CVAR_INT, 10, 100 },
+//
+//        { "demorec_diffs","enable/disable diff recording in demos",(void *)&demorec_diffs_cvar, CVAR_BOOL, 0, 1 },
+//        { "demorec_force","enable/disable forced demo recording",(void *)&demorec_force_cvar, CVAR_BOOL|CVAR_NOSAVE, 0, 1 },
+//        {
+//            "demorec_difftics","sets game tic interval after which a diff is recorded",
+//            (void *)&demorec_difftics_cvar, CVAR_INT, 2, 60*REALGAMETICSPERSEC
+//        },
+//        { "demorec_diffcompress","Compression method for diffs. (0: none, 1: KSLZW)",(void *)&demorec_diffcompress_cvar, CVAR_INT, 0, 1 },
+//        { "demorec_synccompress","Compression method for input. (0: none, 1: KSLZW)",(void *)&demorec_synccompress_cvar, CVAR_INT, 0, 1 },
+//        { "demorec_seeds","enable/disable recording of random seed for later sync checking",(void *)&demorec_seeds_cvar, CVAR_BOOL, 0, 1 },
+//        { "demoplay_diffs","enable/disable application of diffs in demo playback",(void *)&demoplay_diffs, CVAR_BOOL, 0, 1 },
+//        { "demoplay_showsync","enable/disable display of sync status",(void *)&demoplay_showsync, CVAR_BOOL, 0, 1 },
+//
+//        { "hud_althud", "enable/disable alternate mini-hud", (void *)&ud.althud, CVAR_BOOL, 0, 1 },
+//        { "hud_custom", "change the custom hud", (void *)&ud.statusbarcustom, CVAR_INT, 0, ud.statusbarrange },
+//        { "hud_position", "aligns the status bar to the bottom/top", (void *)&ud.hudontop, CVAR_BOOL, 0, 1 },
+//        { "hud_bgstretch", "enable/disable background image stretching in wide resolutions", (void *)&ud.bgstretch, CVAR_BOOL, 0, 1 },
+//        { "hud_messagetime", "length of time to display multiplayer chat messages", (void *)&ud.msgdisptime, CVAR_INT, 0, 3600 },
+//        { "hud_numbertile", "first tile in alt hud number set", (void *)&althud_numbertile, CVAR_INT, 0, MAXUSERTILES-10 },
+//        { "hud_numberpal", "pal for alt hud numbers", (void *)&althud_numberpal, CVAR_INT, 0, MAXPALOOKUPS-1 },
+//        { "hud_shadows", "enable/disable althud shadows", (void *)&althud_shadows, CVAR_BOOL, 0, 1 },
+//        { "hud_flashing", "enable/disable althud flashing", (void *)&althud_flashing, CVAR_BOOL, 0, 1 },
+//        { "hud_glowingquotes", "enable/disable \"glowing\" quote text", (void *)&hud_glowingquotes, CVAR_BOOL, 0, 1 },
+//        { "hud_scale","changes the hud scale", (void *)&ud.statusbarscale, CVAR_INT|CVAR_FUNCPTR, 36, 100 },
+//        { "hud_showmapname", "enable/disable map name display on load", (void *)&hud_showmapname, CVAR_BOOL, 0, 1 },
+        { "hud_stats", "enable/disable level statistics display", (void *)&gLevelStats, CVAR_BOOL, 0, 1 },
+        { "hud_powerupduration", "enable/disable displaying the remaining seconds for power-ups", (void *)&gPowerupDuration, CVAR_BOOL, 0, 1 },
+        { "hud_showmaptitle", "enable/disable displaying the map title at the beginning of the maps", (void*)& gShowMapTitle, CVAR_BOOL, 0, 1 },
+//        { "hud_textscale", "sets multiplayer chat message size", (void *)&ud.textscale, CVAR_INT, 100, 400 },
+//        { "hud_weaponscale","changes the weapon scale", (void *)&ud.weaponscale, CVAR_INT, 10, 100 },
+//        { "hud_statusbarmode", "change overlay mode of status bar", (void *)&ud.statusbarmode, CVAR_BOOL|CVAR_FUNCPTR, 0, 1 },
+//
+//#ifdef EDUKE32_TOUCH_DEVICES
+//        { "hud_hidestick", "hide the touch input stick", (void *)&droidinput.hideStick, CVAR_BOOL, 0, 1 },
+//#endif
+//
+        { "horizcenter", "enable/disable centered horizon line", (void *)&gCenterHoriz, CVAR_BOOL, 0, 1 },
+        { "deliriumblur", "enable/disable delirium blur effect(polymost)", (void *)&gDeliriumBlur, CVAR_BOOL, 0, 1 },
+        { "fov", "change the field of view", (void *)&gFov, CVAR_INT|CVAR_FUNCPTR, 75, 120 },
+        { "in_joystick","enables input from the joystick if it is present",(void *)&gSetup.usejoystick, CVAR_BOOL|CVAR_FUNCPTR, 0, 1 },
+        { "in_mouse","enables input from the mouse if it is present",(void *)&gSetup.usemouse, CVAR_BOOL|CVAR_FUNCPTR, 0, 1 },
+
+        { "in_aimmode", "0:toggle, 1:hold to aim", (void *)&gMouseAiming, CVAR_BOOL, 0, 1 },
+        {
+            "in_mousebias", "emulates the original mouse code's weighting of input towards whichever axis is moving the most at any given time",
+            (void *)&MouseBias, CVAR_INT, 0, 32
+        },
+        { "in_mousedeadzone", "amount of mouse movement to filter out", (void *)&MouseDeadZone, CVAR_INT, 0, 512 },
+        { "in_mouseflip", "invert vertical mouse movement", (void *)&gMouseAimingFlipped, CVAR_BOOL, 0, 1 },
+        { "in_mousemode", "toggles vertical mouse view", (void *)&gMouseAim, CVAR_BOOL, 0, 1 },
+        { "in_mousesmoothing", "enable/disable mouse input smoothing", (void *)&SmoothInput, CVAR_BOOL, 0, 1 },
+//
+        { "mus_enabled", "enables/disables music", (void *)&MusicToggle, CVAR_BOOL, 0, 1 },
+        { "mus_restartonload", "restart the music when loading a saved game with the same map or not", (void *)&MusicRestartsOnLoadToggle, CVAR_BOOL, 0, 1 },
+        { "mus_volume", "controls music volume", (void *)&MusicVolume, CVAR_INT, 0, 255 },
+        { "mus_device", "music device", (void *)&MusicDevice, CVAR_INT, 0, 1 },
+        { "mus_redbook", "enables/disables redbook audio", (void *)&CDAudioToggle, CVAR_BOOL, 0, 1 },
+//
+//        { "osdhightile", "enable/disable hires art replacements for console text", (void *)&osdhightile, CVAR_BOOL, 0, 1 },
+//        { "osdscale", "adjust console text size", (void *)&osdscale, CVAR_FLOAT|CVAR_FUNCPTR, 1, 4 },
+//
+//        { "r_camrefreshdelay", "minimum delay between security camera sprite updates, 120 = 1 second", (void *)&ud.camera_time, CVAR_INT, 1, 240 },
+//        { "r_drawweapon", "enable/disable weapon drawing", (void *)&ud.drawweapon, CVAR_INT, 0, 2 },
+        { "r_showfps", "show the frame rate counter", (void *)&gShowFps, CVAR_INT, 0, 3 },
+        { "r_showfpsperiod", "time in seconds before averaging min and max stats for r_showfps 2+", (void *)&gFramePeriod, CVAR_INT, 0, 5 },
+//        { "r_shadows", "enable/disable sprite and model shadows", (void *)&ud.shadows, CVAR_BOOL, 0, 1 },
+        { "r_size", "change size of viewable area", (void *)&gViewSize, CVAR_INT|CVAR_FUNCPTR, 0, 7 },
+//        { "r_rotatespritenowidescreen", "pass bit 1024 to all CON rotatesprite calls", (void *)&g_rotatespriteNoWidescreen, CVAR_BOOL|CVAR_FUNCPTR, 0, 1 },
+        { "r_upscalefactor", "increase performance by rendering at upscalefactor less than the screen resolution and upscale to the full resolution in the software renderer", (void *)&gUpscaleFactor, CVAR_INT|CVAR_FUNCPTR, 1, 16 },
+        { "r_precache", "enable/disable the pre-level caching routine", (void *)&useprecache, CVAR_BOOL, 0, 1 },
+//
+        { "r_ambientlight", "sets the global map light level",(void *)&r_ambientlight, CVAR_FLOAT|CVAR_FUNCPTR, 0, 10 },
+        { "r_maxfps", "limit the frame rate",(void *)&r_maxfps, CVAR_INT|CVAR_FUNCPTR, 0, 1000 },
+        { "r_maxfpsoffset", "menu-controlled offset for r_maxfps",(void *)&r_maxfpsoffset, CVAR_INT|CVAR_FUNCPTR, -10, 10 },
+
+        { "sensitivity","changes the mouse sensitivity", (void *)&CONTROL_MouseSensitivity, CVAR_FLOAT|CVAR_FUNCPTR, 0, 25 },
+//
+//        { "skill","changes the game skill setting", (void *)&ud.m_player_skill, CVAR_INT|CVAR_FUNCPTR|CVAR_NOSAVE/*|CVAR_NOMULTI*/, 0, 5 },
+//
+//        { "snd_ambience", "enables/disables ambient sounds", (void *)&ud.config.AmbienceToggle, CVAR_BOOL, 0, 1 },
+        { "snd_enabled", "enables/disables sound effects", (void *)&SoundToggle, CVAR_BOOL, 0, 1 },
+        { "snd_fxvolume", "controls volume for sound effects", (void *)&FXVolume, CVAR_INT, 0, 255 },
+        { "snd_mixrate", "sound mixing rate", (void *)&MixRate, CVAR_INT, 0, 48000 },
+        { "snd_numchannels", "the number of sound channels", (void *)&NumChannels, CVAR_INT, 0, 2 },
+        { "snd_numvoices", "the number of concurrent sounds", (void *)&NumVoices, CVAR_INT, 1, 128 },
+        { "snd_reversestereo", "reverses the stereo channels", (void *)&ReverseStereo, CVAR_BOOL, 0, 1 },
+        { "snd_doppler", "enable/disable 3d sound", (void *)&gDoppler, CVAR_BOOL, 0, 1 },
+//        { "snd_speech", "enables/disables player speech", (void *)&ud.config.VoiceToggle, CVAR_INT, 0, 5 },
+//
+//        { "team","change team in multiplayer", (void *)&ud.team, CVAR_INT|CVAR_MULTI, 0, 3 },
+//
+//#ifdef EDUKE32_TOUCH_DEVICES
+//        { "touch_sens_move_x","touch input sensitivity for moving forward/back", (void *)&droidinput.forward_sens, CVAR_FLOAT, 1, 9 },
+//        { "touch_sens_move_y","touch input sensitivity for strafing", (void *)&droidinput.strafe_sens, CVAR_FLOAT, 1, 9 },
+//        { "touch_sens_look_x", "touch input sensitivity for turning left/right", (void *) &droidinput.yaw_sens, CVAR_FLOAT, 1, 9 },
+//        { "touch_sens_look_y", "touch input sensitivity for looking up/down", (void *) &droidinput.pitch_sens, CVAR_FLOAT, 1, 9 },
+//        { "touch_invert", "invert look up/down touch input", (void *) &droidinput.invertLook, CVAR_INT, 0, 1 },
+//#endif
+//
+        { "vid_gamma","adjusts gamma component of gamma ramp",(void *)&g_videoGamma, CVAR_FLOAT|CVAR_FUNCPTR, 0, 10 },
+        { "vid_contrast","adjusts contrast component of gamma ramp",(void *)&g_videoContrast, CVAR_FLOAT|CVAR_FUNCPTR, 0, 10 },
+        { "vid_brightness","adjusts brightness component of gamma ramp",(void *)&g_videoBrightness, CVAR_FLOAT|CVAR_FUNCPTR, 0, 10 },
+//        { "wchoice","sets weapon autoselection order", (void *)ud.wchoice, CVAR_STRING|CVAR_FUNCPTR, 0, MAX_WEAPONS },
+    };
+//
+//    osdcmd_cheatsinfo_stat.cheatnum = -1;
+//
+    for (auto & cv : cvars_game)
+    {
+        switch (cv.flags & (CVAR_FUNCPTR|CVAR_MULTI))
+        {
+            case CVAR_FUNCPTR:
+                OSD_RegisterCvar(&cv, osdcmd_cvar_set_game); break;
+            case CVAR_MULTI:
+            case CVAR_FUNCPTR|CVAR_MULTI:
+                OSD_RegisterCvar(&cv, osdcmd_cvar_set_multi); break;
+            default:
+                OSD_RegisterCvar(&cv, osdcmd_cvar_set); break;
+        }
+    }
+//
+//    if (VOLUMEONE)
+//        OSD_RegisterFunction("changelevel","changelevel <level>: warps to the given level", osdcmd_changelevel);
+//    else
+//    {
+    OSD_RegisterFunction("changelevel","changelevel <volume> <level>: warps to the given level", osdcmd_changelevel);
+    OSD_RegisterFunction("map","map <mapfile>: loads the given user map", osdcmd_map);
+    OSD_RegisterFunction("demo","demo <demofile or demonum>: starts the given demo", osdcmd_demo);
+//    }
+//
+//    OSD_RegisterFunction("addpath","addpath <path>: adds path to game filesystem", osdcmd_addpath);
+    OSD_RegisterFunction("bind",R"(bind <key> <string>: associates a keypress with a string of console input. Type "bind showkeys" for a list of keys and "listsymbols" for a list of valid console commands.)", osdcmd_bind);
+//    OSD_RegisterFunction("cmenu","cmenu <#>: jumps to menu", osdcmd_cmenu);
+    OSD_RegisterFunction("crosshaircolor","crosshaircolor: changes the crosshair color", osdcmd_crosshaircolor);
+    OSD_RegisterFunction("crosshairreset", "crosshairreset: restores the original crosshair", osdcmd_resetcrosshair);
+//
+//#if !defined NETCODE_DISABLE
+//    OSD_RegisterFunction("connect","connect: connects to a multiplayer game", osdcmd_connect);
+//    OSD_RegisterFunction("disconnect","disconnect: disconnects from the local multiplayer game", osdcmd_disconnect);
+//#endif
+
+    for (auto & func : gamefunctions)
+    {
+        if (func[0] == '\0')
+            continue;
+
+//        if (!Bstrcmp(gamefunctions[i],"Show_Console")) continue;
+
+        Bsprintf(buffer, "gamefunc_%s", func);
+
+        char *const t = Bstrtolower(Xstrdup(buffer));
+
+        Bstrcat(buffer, ": game button");
+
+        OSD_RegisterFunction(t, Xstrdup(buffer), osdcmd_button);
+    }
+
+    OSD_RegisterFunction("give","give <all|health|weapons|ammo|armor|keys|inventory>: gives requested item", osdcmd_give);
+    OSD_RegisterFunction("god","god: toggles god mode", osdcmd_god);
+//    OSD_RegisterFunction("activatecheat","activatecheat <id>: activates a cheat code", osdcmd_activatecheat);
+//
+//    OSD_RegisterFunction("initgroupfile","initgroupfile <path>: adds a grp file into the game filesystem", osdcmd_initgroupfile);
+//#ifdef DEBUGGINGAIDS
+//    OSD_RegisterFunction("inittimer","debug", osdcmd_inittimer);
+//#endif
+//#if !defined NETCODE_DISABLE
+//    OSD_RegisterFunction("kick","kick <id>: kicks a multiplayer client.  See listplayers.", osdcmd_kick);
+//    OSD_RegisterFunction("kickban","kickban <id>: kicks a multiplayer client and prevents them from reconnecting.  See listplayers.", osdcmd_kickban);
+//
+//    OSD_RegisterFunction("listplayers","listplayers: lists currently connected multiplayer clients", osdcmd_listplayers);
+//#endif
+    OSD_RegisterFunction("music","music E<ep>L<lev>: change music", osdcmd_music);
+//
+//#if !defined NETCODE_DISABLE
+//    OSD_RegisterFunction("name","name: change your multiplayer nickname", osdcmd_name);
+//#endif
+//
+    OSD_RegisterFunction("noclip","noclip: toggles clipping mode", osdcmd_noclip);
+//
+//#if !defined NETCODE_DISABLE
+//    OSD_RegisterFunction("password","password: sets multiplayer game password", osdcmd_password);
+//#endif
+//
+//    OSD_RegisterFunction("printtimes", "printtimes: prints VM timing statistics", osdcmd_printtimes);
+//
+//    OSD_RegisterFunction("purgesaves", "purgesaves: deletes obsolete and unreadable save files", osdcmd_purgesaves);
+//
+//    OSD_RegisterFunction("quicksave","quicksave: performs a quick save", osdcmd_quicksave);
+//    OSD_RegisterFunction("quickload","quickload: performs a quick load", osdcmd_quickload);
+    OSD_RegisterFunction("quit","quit: exits the game immediately", osdcmd_quit);
+    OSD_RegisterFunction("exit","exit: exits the game immediately", osdcmd_quit);
+//
+//    OSD_RegisterFunction("restartmap", "restartmap: restarts the current map", osdcmd_restartmap);
+    OSD_RegisterFunction("restartsound","restartsound: reinitializes the sound system",osdcmd_restartsound);
+    OSD_RegisterFunction("restartvid","restartvid: reinitializes the video mode",osdcmd_restartvid);
+//#if !defined LUNATIC
+//    OSD_RegisterFunction("addlogvar","addlogvar <gamevar>: prints the value of a gamevar", osdcmd_addlogvar);
+//    OSD_RegisterFunction("setvar","setvar <gamevar> <value>: sets the value of a gamevar", osdcmd_setvar);
+//    OSD_RegisterFunction("setvarvar","setvarvar <gamevar1> <gamevar2>: sets the value of <gamevar1> to <gamevar2>", osdcmd_setvar);
+//    OSD_RegisterFunction("setactorvar","setactorvar <actor#> <gamevar> <value>: sets the value of <actor#>'s <gamevar> to <value>", osdcmd_setactorvar);
+//#else
+//    OSD_RegisterFunction("lua", "lua \"Lua code...\": runs Lunatic code", osdcmd_lua);
+//#endif
+    OSD_RegisterFunction("screenshot","screenshot [format]: takes a screenshot.", osdcmd_screenshot);
+//
+//    OSD_RegisterFunction("spawn","spawn <picnum> [palnum] [cstat] [ang] [x y z]: spawns a sprite with the given properties",osdcmd_spawn);
+
+    OSD_RegisterFunction("unbind","unbind <key>: unbinds a key", osdcmd_unbind);
+    OSD_RegisterFunction("unbindall","unbindall: unbinds all keys", osdcmd_unbindall);
+
+    OSD_RegisterFunction("vidmode","vidmode <xdim> <ydim> <bpp> <fullscreen>: change the video mode",osdcmd_vidmode);
+#ifdef USE_OPENGL
+    baselayer_osdcmd_vidmode_func = osdcmd_vidmode;
+#endif
+//
+//#ifndef NETCODE_DISABLE
+//    OSD_RegisterFunction("dumpmapstates", "Dumps current snapshots to CL/Srv_MapStates.bin", osdcmd_dumpmapstate);
+//    OSD_RegisterFunction("playerinfo", "Prints information about the current player", osdcmd_playerinfo);
+//#endif
+
+    return 0;
+}
+
+void GAME_onshowosd(int shown)
+{
+    // G_UpdateScreenArea();
+
+    mouseLockToWindow((!shown) + 2);
+
+    //osdshown = shown;
+
+    // XXX: it's weird to fake a keypress like this.
+//    if (numplayers == 1 && ((shown && !ud.pause_on) || (!shown && ud.pause_on)))
+//        KB_KeyDown[sc_Pause] = 1;
+}
+
+void GAME_clearbackground(int numcols, int numrows)
+{
+    COMMON_clearbackground(numcols, numrows);
+}
+
diff --git a/source/blood/src/osdcmds.h b/source/blood/src/osdcmds.h
new file mode 100644
index 000000000..14fe89edf
--- /dev/null
+++ b/source/blood/src/osdcmds.h
@@ -0,0 +1,50 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//struct osdcmd_cheatsinfo {
+//	int32_t cheatnum;	// -1 = none, else = see DoCheats()
+//	int32_t volume,level;
+//};
+//
+//extern struct osdcmd_cheatsinfo osdcmd_cheatsinfo_stat;
+
+int32_t registerosdcommands(void);
+void onvideomodechange(int32_t newmode);
+void GAME_onshowosd(int32_t shown);
+void GAME_clearbackground(int32_t numcols, int32_t numrows);
+
+// extern float r_ambientlight,r_ambientlightrecip;
+
+extern const char *const ConsoleButtons[];
+
+extern uint32_t cl_cheatmask;
+
+#ifdef __cplusplus
+}
+#endif
+
diff --git a/source/blood/src/player.cpp b/source/blood/src/player.cpp
new file mode 100644
index 000000000..300144b68
--- /dev/null
+++ b/source/blood/src/player.cpp
@@ -0,0 +1,2400 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdlib.h>
+#include <string.h>
+#include "compat.h"
+#include "build.h"
+#include "mmulti.h"
+#include "actor.h"
+#include "blood.h"
+#include "callback.h"
+#include "config.h"
+#include "controls.h"
+#include "demo.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gib.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "map2d.h"
+#include "network.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "tile.h"
+#include "triggers.h"
+#include "trig.h"
+#include "view.h"
+#include "warp.h"
+#include "weapon.h"
+#include "common_game.h"
+
+PROFILE gProfile[kMaxPlayers];
+
+PLAYER gPlayer[kMaxPlayers];
+PLAYER *gMe, *gView;
+
+POWERUPINFO gPowerUpInfo[kMaxPowerUps] = {
+    { -1, 1, 1, 1 },
+    { -1, 1, 1, 1 },
+    { -1, 1, 1, 1 },
+    { -1, 1, 1, 1 },
+    { -1, 1, 1, 1 },
+    { -1, 1, 1, 1 },
+    { -1, 1, 1, 1 },
+    { -1, 0, 100, 100 },
+    { -1, 0, 50, 100 },
+    { -1, 0, 20, 100 },
+    { -1, 0, 100, 200 },
+    { -1, 0, 2, 200 },
+    { 783, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 }, // 13: cloak of invisibility
+    { -1, 1, 3600, 432000 }, // 14: death mask (invulnerability)
+    { 827, 0, 3600, 432000 }, // 15: jump boots
+    { 828, 0, 3600, 432000 },
+    { -1, 0, 3600, 1728000 }, // 17: guns akimbo
+    { -1, 0, 3600, 432000 }, // 18: diving suit
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 }, // 21: crystal ball
+    { -1, 0, 3600, 432000 },
+    { 851, 0, 3600, 432000 },
+    { 2428, 0, 3600, 432000 }, // 24: reflective shots
+    { -1, 0, 3600, 432000 }, // 25: beast vision
+    { -1, 0, 3600, 432000 }, // 26: cloak of shadow
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 900, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 0, 3600, 432000 },
+    { -1, 1, 3600, 432000 },
+    { -1, 0, 1, 432000 },
+    { -1, 0, 1, 432000 },
+    { -1, 0, 1, 432000 },
+    { -1, 0, 1, 432000 },
+    { -1, 0, 1, 432000 },
+    { 0, 0, 0, 0 },
+    { 0, 0, 0, 0 },
+    { 0, 0, 0, 0 },
+    { 0, 0, 0, 0 },
+    { 0, 0, 0, 0 }, // dummy
+    { -1, 1, 1, 1 } // kGDXItemLevelMap
+};
+
+int Handicap[] = {
+    144, 208, 256, 304, 368
+};
+
+POSTURE gPosture[4][3] = {
+    
+    // normal human
+    {
+        {
+            0x4000,
+            0x4000,
+            0x4000,
+            14,
+            17,
+            24,
+            16,
+            32,
+            80,
+            0x1600,
+            0x1200,
+            0xc00,
+            0x90
+        },
+        {
+            0x1200,
+            0x1200,
+            0x1200,
+            14,
+            17,
+            24,
+            16,
+            32,
+            80,
+            0x1400,
+            0x1000,
+            -0x600,
+            0xb0
+        },
+        {
+            0x2000,
+            0x2000,
+            0x2000,
+            22,
+            28,
+            24,
+            16,
+            16,
+            40,
+            0x800,
+            0x600,
+            -0x600,
+            0xb0
+        },
+    },
+
+    // normal beast
+    {
+        {
+            0x4000,
+            0x4000,
+            0x4000,
+            14,
+            17,
+            24,
+            16,
+            32,
+            80,
+            0x1600,
+            0x1200,
+            0xc00,
+            0x90
+        },
+        {
+            0x1200,
+            0x1200,
+            0x1200,
+            14,
+            17,
+            24,
+            16,
+            32,
+            80,
+            0x1400,
+            0x1000,
+            -0x600,
+            0xb0
+        },
+        {
+            0x2000,
+            0x2000,
+            0x2000,
+            22,
+            28,
+            24,
+            16,
+            16,
+            40,
+            0x800,
+            0x600,
+            -0x600,
+            0xb0
+        },
+    },
+
+    // shrink human
+    {
+        {
+            10384, 
+            12384, 
+            12384, 
+            14, 
+            17, 
+            24, 
+            16, 
+            32, 
+            80, 
+            5632, 
+            4608, 
+            3072, 
+            144
+        },
+        {
+            2108, 
+            2108, 
+            2108, 
+            14, 
+            17, 
+            24,
+            16, 
+            32, 
+            80, 
+            5120, 
+            4096, 
+            -1536, 
+            176
+        },
+        {
+            2192, 
+            3192, 
+            4192,
+            22, 
+            28, 
+            24, 
+            16,
+            16,
+            40, 
+            2048, 
+            1536, 
+            -1536,
+            176
+        },
+    },
+
+    // grown human
+    {
+        {
+            19384, 
+            15384, 
+            15384, 
+            14, 
+            17, 
+            24, 
+            16, 
+            32, 
+            80, 
+            5632, 
+            4608, 
+            3072, 
+            144
+        },
+        {
+            5608, 
+            5608, 
+            5608, 
+            14, 
+            17, 
+            24, 
+            16, 
+            32, 
+            80, 
+            5120, 
+            4096, 
+            -1536, 
+            176
+        },
+        {
+            11192, 
+            11192, 
+            11192,
+            22, 
+            28, 
+            24, 
+            16, 
+            16, 
+            40, 
+            2048, 
+            1536, 
+            -1536, 
+            176
+        },
+    },
+};
+
+AMMOINFO gAmmoInfo[] = {
+    { 0, -1 },
+    { 100, -1 },
+    { 100, 4 },
+    { 500, 5 },
+    { 100, -1 },
+    { 50, -1 },
+    { 2880, -1 },
+    { 250, -1 },
+    { 100, -1 },
+    { 100, -1 },
+    { 50, -1 },
+    { 50, -1 },
+};
+
+struct ARMORDATA {
+    int at0;
+    int at4;
+    int at8;
+    int atc;
+    int at10;
+    int at14;
+};
+ARMORDATA armorData[5] = {
+    { 0x320, 0x640, 0x320, 0x640, 0x320, 0x640 },
+    { 0x640, 0x640, 0, 0x640, 0, 0x640 },
+    { 0, 0x640, 0x640, 0x640, 0, 0x640 },
+    { 0, 0x640, 0, 0x640, 0x640, 0x640 },
+    { 0xc80, 0xc80, 0xc80, 0xc80, 0xc80, 0xc80 }
+};
+
+void PlayerSurvive(int, int);
+void PlayerKeelsOver(int, int);
+
+int nPlayerSurviveClient = seqRegisterClient(PlayerSurvive);
+int nPlayerKeelClient = seqRegisterClient(PlayerKeelsOver);
+
+struct VICTORY {
+    const char *at0;
+    int at4;
+};
+
+VICTORY gVictory[] = {
+    { "%s boned %s like a fish", 4100 },
+    { "%s castrated %s", 4101 },
+    { "%s creamed %s", 4102 },
+    { "%s destroyed %s", 4103 },
+    { "%s diced %s", 4104 },
+    { "%s disemboweled %s", 4105 },
+    { "%s flattened %s", 4106 },
+    { "%s gave %s Anal Justice", 4107 },
+    { "%s gave AnAl MaDnEsS to %s", 4108 },
+    { "%s hurt %s real bad", 4109 },
+    { "%s killed %s", 4110 },
+    { "%s made mincemeat out of %s", 4111 },
+    { "%s massacred %s", 4112 },
+    { "%s mutilated %s", 4113 },
+    { "%s reamed %s", 4114 },
+    { "%s ripped %s a new orifice", 4115 },
+    { "%s slaughtered %s", 4116 },
+    { "%s sliced %s", 4117 },
+    { "%s smashed %s", 4118 },
+    { "%s sodomized %s", 4119 },
+    { "%s splattered %s", 4120 },
+    { "%s squashed %s", 4121 },
+    { "%s throttled %s", 4122 },
+    { "%s wasted %s", 4123 },
+    { "%s body bagged %s", 4124 },
+};
+
+struct SUICIDE {
+    const char *at0;
+    int at4;
+};
+
+SUICIDE gSuicide[] = {
+    { "%s is excrement", 4202 },
+    { "%s is hamburger", 4203 },
+    { "%s suffered scrotum separation", 4204 },
+    { "%s volunteered for population control", 4206 },
+    { "%s has suicided", 4207 },
+};
+
+struct DAMAGEINFO {
+    int at0;
+    int at4[3];
+    int at10[3];
+};
+
+DAMAGEINFO damageInfo[7] = {
+    { -1, 731, 732, 733, 710, 710, 710 },
+    { 1, 742, 743, 744, 711, 711, 711 },
+    { 0, 731, 732, 733, 712, 712, 712 },
+    { 1, 731, 732, 733, 713, 713, 713 },
+    { -1, 724, 724, 724, 714, 714, 714 },
+    { 2, 731, 732, 733, 715, 715, 715 },
+    { 0, 0, 0, 0, 0, 0, 0 }
+};
+
+int powerupCheck(PLAYER *pPlayer, int nPowerUp)
+{
+    dassert(pPlayer != NULL);
+    dassert(nPowerUp >= 0 && nPowerUp < kMaxPowerUps);
+    int nPack = powerupToPackItem(nPowerUp);
+    if (nPack >= 0 && !packItemActive(pPlayer, nPack))
+        return 0;
+    return pPlayer->at202[nPowerUp];
+}
+
+bool isGrown(spritetype* pSprite) {
+    return (powerupCheck(&gPlayer[pSprite->type - kDudePlayer1], 29) > 0);
+}
+
+bool isShrinked(spritetype* pSprite) {
+    return (powerupCheck(&gPlayer[pSprite->type - kDudePlayer1], 30) > 0);
+}
+
+bool shrinkPlayerSize(PLAYER* pPlayer, int divider) {
+    pPlayer->pXSprite->scale = -divider;
+    playerSetRace(pPlayer, kModeHumanShrink);
+    return true;
+}
+
+bool growPlayerSize(PLAYER* pPlayer, int multiplier) {
+    pPlayer->pXSprite->scale = multiplier;
+    playerSetRace(pPlayer, kModeHumanGrown);
+    return true;
+}
+
+bool resetPlayerSize(PLAYER* pPlayer) {
+    playerSetRace(pPlayer, kModeHuman);
+    pPlayer->pXSprite->scale = 0;
+    return true;
+}
+
+void deactivateSizeShrooms(PLAYER* pPlayer) {
+    powerupDeactivate(pPlayer, 29);
+    pPlayer->at202[29] = 0;
+
+    powerupDeactivate(pPlayer, 30);
+    pPlayer->at202[30] = 0;
+}
+
+char powerupActivate(PLAYER *pPlayer, int nPowerUp)
+{
+    if (powerupCheck(pPlayer, nPowerUp) > 0 && gPowerUpInfo[nPowerUp].at2)
+        return 0;
+    if (!pPlayer->at202[nPowerUp])
+        pPlayer->at202[nPowerUp] = gPowerUpInfo[nPowerUp].at3;
+    int nPack = powerupToPackItem(nPowerUp);
+    if (nPack >= 0)
+        pPlayer->packInfo[nPack].at0 = 1;
+    switch (nPowerUp+100)
+    {
+    case kGDXItemMapLevel:
+        gFullMap = true;
+        break;
+    case 130:
+        if (isGrown(pPlayer->pSprite)) deactivateSizeShrooms(pPlayer);
+        else shrinkPlayerSize(pPlayer, 2);
+        break;
+    case 129:
+        if (isShrinked(pPlayer->pSprite)) deactivateSizeShrooms(pPlayer);
+        else {
+            growPlayerSize(pPlayer, 2);
+            if (powerupCheck(&gPlayer[pPlayer->pSprite->type - kDudePlayer1], 13) > 0) {
+                powerupDeactivate(pPlayer, 13);
+                pPlayer->at202[13] = 0;
+            }
+
+            if (ceilIsTooLow(pPlayer->pSprite))
+                actDamageSprite(pPlayer->pSprite->xvel, pPlayer->pSprite, DAMAGE_TYPE_3, 65535);
+        }
+        break;
+    case 112:
+    case 115: // jump boots
+        pPlayer->ata1[0]++;
+        break;
+    case 124: // reflective shots
+        if (pPlayer == gMe && gGameOptions.nGameType == 0)
+            sfxSetReverb2(1);
+        break;
+    case 114: // death mask
+        for (int i = 0; i < 7; i++)
+            pPlayer->ata1[i]++;
+        break;
+    case 118: // diving suit
+        pPlayer->ata1[4]++;
+        if (pPlayer == gMe && gGameOptions.nGameType == 0)
+            sfxSetReverb(1);
+        break;
+    case 119:
+        pPlayer->ata1[4]++;
+        break;
+    case 139:
+        pPlayer->ata1[1]++;
+        break;
+    case 117: // guns akimbo
+        pPlayer->atc.newWeapon = pPlayer->atbd;
+        WeaponRaise(pPlayer);
+        break;
+    }
+    sfxPlay3DSound(pPlayer->pSprite, 776, -1, 0);
+    return 1;
+}
+
+void powerupDeactivate(PLAYER *pPlayer, int nPowerUp)
+{
+    int nPack = powerupToPackItem(nPowerUp);
+    if (nPack >= 0)
+        pPlayer->packInfo[nPack].at0 = 0;
+    switch (nPowerUp+100)
+    {
+    case 130:
+        resetPlayerSize(pPlayer);
+        if (ceilIsTooLow(pPlayer->pSprite))
+            actDamageSprite(pPlayer->pSprite->xvel, pPlayer->pSprite, DAMAGE_TYPE_3, 65535);
+        break;
+    case 129:
+        resetPlayerSize(pPlayer);
+        break;
+    case 112:
+    case 115: // jump boots
+        pPlayer->ata1[0]--;
+        break;
+    case 114: // death mask
+        for (int i = 0; i < 7; i++)
+            pPlayer->ata1[i]--;
+        break;
+    case 118: // diving suit
+        pPlayer->ata1[4]--;
+        if (pPlayer == gMe)
+            sfxSetReverb(0);
+        break;
+    case 124: // reflective shots
+        if (pPlayer == gMe)
+            sfxSetReverb(0);
+        break;
+    case 119:
+        pPlayer->ata1[4]--;
+        break;
+    case 139:
+        pPlayer->ata1[1]--;
+        break;
+    case 117: // guns akimbo
+        pPlayer->atc.newWeapon = pPlayer->atbd;
+        WeaponRaise(pPlayer);
+        break;
+    }
+}
+
+void powerupSetState(PLAYER *pPlayer, int nPowerUp, char bState)
+{
+    if (!bState)
+        powerupActivate(pPlayer, nPowerUp);
+    else
+        powerupDeactivate(pPlayer, nPowerUp);
+}
+
+void powerupProcess(PLAYER *pPlayer)
+{
+    pPlayer->at31d = ClipLow(pPlayer->at31d-4, 0);
+    for (int i = kMaxPowerUps-1; i >= 0; i--)
+    {
+        int nPack = powerupToPackItem(i);
+        if (nPack >= 0)
+        {
+            if (pPlayer->packInfo[nPack].at0)
+            {
+                pPlayer->at202[i] = ClipLow(pPlayer->at202[i]-4, 0);
+                if (pPlayer->at202[i])
+                    pPlayer->packInfo[nPack].at1 = (100*pPlayer->at202[i])/gPowerUpInfo[i].at3;
+                else
+                {
+                    powerupDeactivate(pPlayer, i);
+                    if (pPlayer->at321 == nPack)
+                        pPlayer->at321 = 0;
+                }
+            }
+        }
+        else if (pPlayer->at202[i] > 0)
+        {
+            pPlayer->at202[i] = ClipLow(pPlayer->at202[i]-4, 0);
+            if (!pPlayer->at202[i])
+                powerupDeactivate(pPlayer, i);
+        }
+    }
+}
+
+void powerupClear(PLAYER *pPlayer)
+{
+    for (int i = kMaxPowerUps-1; i >= 0; i--)
+    {
+        pPlayer->at202[i] = 0;
+    }
+}
+
+void powerupInit(void)
+{
+}
+
+int packItemToPowerup(int nPack)
+{
+    int nPowerUp = -1;
+    switch (nPack)
+    {
+    case 0:
+        break;
+    case 1:
+        nPowerUp = 18;
+        break;
+    case 2:
+        nPowerUp = 21;
+        break;
+    case 3:
+        nPowerUp = 25;
+        break;
+    case 4:
+        nPowerUp = 15;
+        break;
+    default:
+        ThrowError("Unhandled pack item %d", nPack);
+        break;
+    }
+    return nPowerUp;
+}
+
+int powerupToPackItem(int nPowerUp)
+{
+    const int jumpBoots = 15;
+    const int divingSuit = 18;
+    const int crystalBall = 21;
+    const int beastVision = 25;
+
+    switch (nPowerUp)
+    {
+    case divingSuit:
+        return 1;
+    case crystalBall:
+        return 2;
+    case beastVision:
+        return 3;
+    case jumpBoots:
+        return 4;
+    }
+    return -1;
+}
+
+char packAddItem(PLAYER *pPlayer, unsigned int nPack)
+{
+    if (nPack <= 4)
+    {
+        if (pPlayer->packInfo[nPack].at1 >= 100)
+            return 0;
+        pPlayer->packInfo[nPack].at1 = 100;
+        int nPowerUp = packItemToPowerup(nPack);
+        if (nPowerUp >= 0)
+            pPlayer->at202[nPowerUp] = gPowerUpInfo[nPowerUp].at3;
+        if (pPlayer->at321 == -1)
+            pPlayer->at321 = nPack;
+        if (!pPlayer->packInfo[pPlayer->at321].at1)
+            pPlayer->at321 = nPack;
+    }
+    else
+        ThrowError("Unhandled pack item %d", nPack);
+    return 1;
+}
+
+int packCheckItem(PLAYER *pPlayer, int nPack)
+{
+    return pPlayer->packInfo[nPack].at1;
+}
+
+char packItemActive(PLAYER *pPlayer, int nPack)
+{
+    return pPlayer->packInfo[nPack].at0;
+}
+
+void packUseItem(PLAYER *pPlayer, int nPack)
+{
+    char v4 = 0;
+    int nPowerUp = -1;
+    if (pPlayer->packInfo[nPack].at1 > 0)
+    {
+        switch (nPack)
+        {
+        case 0:
+        {
+            XSPRITE *pXSprite = pPlayer->pXSprite;
+            unsigned int health = pXSprite->health>>4;
+            if (health < 100)
+            {
+                int heal = ClipHigh(100-health, pPlayer->packInfo[0].at1);
+                actHealDude(pXSprite, heal, 100);
+                pPlayer->packInfo[0].at1 -= heal;
+            }
+            break;
+        }
+        case 1:
+            v4 = 1;
+            nPowerUp = 18;
+            break;
+        case 2:
+            v4 = 1;
+            nPowerUp = 21;
+            break;
+        case 3:
+            v4 = 1;
+            nPowerUp = 25;
+            break;
+        case 4:
+            v4 = 1;
+            nPowerUp = 15;
+            break;
+        default:
+            ThrowError("Unhandled pack item %d", nPack);
+            return;
+        }
+    }
+    pPlayer->at31d = 0;
+    if (v4)
+        powerupSetState(pPlayer, nPowerUp, pPlayer->packInfo[nPack].at0);
+}
+
+void packPrevItem(PLAYER *pPlayer)
+{
+    if (pPlayer->at31d > 0)
+    {
+        for (int nPrev = ClipLow(pPlayer->at321-1,0); nPrev >= 0; nPrev--)
+        {
+            if (pPlayer->packInfo[nPrev].at1)
+            {
+                pPlayer->at321 = nPrev;
+                break;
+            }
+        }
+    }
+    pPlayer->at31d = 600;
+}
+
+void packNextItem(PLAYER *pPlayer)
+{
+    if (pPlayer->at31d > 0)
+    {
+        for (int nNext = ClipHigh(pPlayer->at321+1,5); nNext < 5; nNext++)
+        {
+            if (pPlayer->packInfo[nNext].at1)
+            {
+                pPlayer->at321 = nNext;
+                break;
+            }
+        }
+    }
+    pPlayer->at31d = 600;
+}
+
+char playerSeqPlaying(PLAYER * pPlayer, int nSeq)
+{
+    int nCurSeq = seqGetID(3, pPlayer->pSprite->extra);
+    if (pPlayer->pDudeInfo->seqStartID+nSeq == nCurSeq && seqGetStatus(3,pPlayer->pSprite->extra) >= 0)
+        return 1;
+    return 0;
+}
+
+void playerSetRace(PLAYER *pPlayer, int nLifeMode)
+{
+    dassert(nLifeMode >= kModeHuman && nLifeMode <= kModeHumanGrown);
+    DUDEINFO *pDudeInfo = pPlayer->pDudeInfo;
+    *pDudeInfo = gPlayerTemplate[nLifeMode];
+    pPlayer->at5f = nLifeMode;
+    
+    // By NoOne: don't forget to change clipdist for grow and shrink modes
+    pPlayer->pSprite->clipdist = pDudeInfo->clipdist;
+    
+    for (int i = 0; i < 7; i++)
+        pDudeInfo->at70[i] = mulscale8(Handicap[gProfile[pPlayer->at57].skill], pDudeInfo->startDamage[i]);
+}
+
+void playerSetGodMode(PLAYER *pPlayer, char bGodMode)
+{
+    if (bGodMode)
+    {
+        for (int i = 0; i < 7; i++)
+            pPlayer->ata1[i]++;
+    }
+    else
+    {
+        for (int i = 0; i < 7; i++)
+            pPlayer->ata1[i]--;
+    }
+    pPlayer->at31a = bGodMode;
+}
+
+void playerResetInertia(PLAYER *pPlayer)
+{
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][pPlayer->at2f];
+    pPlayer->at67 = pPlayer->pSprite->z-pPosture->at24;
+    pPlayer->at6f = pPlayer->pSprite->z-pPosture->at28;
+    viewBackupView(pPlayer->at57);
+}
+
+void playerResetPowerUps(PLAYER* pPlayer)
+{
+    const int jumpBoots = 15;
+    const int divingSuit = 18;
+    const int crystalBall = 21;
+    const int beastVision = 25;
+
+    for (int i = 0; i < kMaxPowerUps; i++)
+    {
+        if (!VanillaMode()
+            && (i == jumpBoots
+                || i == divingSuit
+                || i == crystalBall
+                || i == beastVision))
+            continue;
+
+        pPlayer->at202[i] = 0;
+    }
+}
+
+void playerStart(int nPlayer)
+{
+    PLAYER* pPlayer = &gPlayer[nPlayer];
+    GINPUT* pInput = &pPlayer->atc;
+    ZONE* pStartZone = NULL;
+
+    // normal start position
+    if (gGameOptions.nGameType <= 1)
+        pStartZone = &gStartZone[nPlayer];
+    
+    // By NoOne: let's check if there is positions of teams is specified
+    // if no, pick position randomly, just like it works in vanilla.
+    else if (gGameOptions.nGameType == 3 && gTeamsSpawnUsed == true) {
+        int maxRetries = 5;
+        while (maxRetries-- > 0) {
+            if (pPlayer->at2ea == 0) pStartZone = &gStartZoneTeam1[Random(3)];
+            else pStartZone = &gStartZoneTeam2[Random(3)];
+
+            if (maxRetries != 0) {
+                // check if there is no spawned player in selected zone
+                for (int i = headspritesect[pStartZone->sectnum]; i >= 0; i = nextspritesect[i]) {
+                    spritetype* pSprite = &sprite[i];
+                    if (pStartZone->x == pSprite->x && pStartZone->y == pSprite->y && IsPlayerSprite(pSprite)) {
+                        pStartZone = NULL;
+                        break;
+                    }
+                }
+            }
+
+            if (pStartZone != NULL)
+                break;
+        }
+    } else {
+        pStartZone = &gStartZone[Random(8)];
+    }
+
+    spritetype *pSprite = actSpawnSprite(pStartZone->sectnum, pStartZone->x, pStartZone->y, pStartZone->z, 6, 1);
+    dassert(pSprite->extra > 0 && pSprite->extra < kMaxXSprites);
+    XSPRITE *pXSprite = &xsprite[pSprite->extra];
+    pPlayer->pSprite = pSprite;
+    pPlayer->pXSprite = pXSprite;
+    pPlayer->at5b = pSprite->index;
+    DUDEINFO *pDudeInfo = &dudeInfo[kDudePlayer1 + nPlayer - kDudeBase];
+    pPlayer->pDudeInfo = pDudeInfo;
+    playerSetRace(pPlayer, kModeHuman);
+    seqSpawn(pDudeInfo->seqStartID, 3, pSprite->extra, -1);
+    if (pPlayer == gMe)
+        SetBitString(show2dsprite, pSprite->index);
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    pSprite->z -= bottom - pSprite->z;
+    pSprite->pal = 11+(pPlayer->at2ea&3);
+    pPlayer->angold = pSprite->ang = pStartZone->ang;
+    pPlayer->q16ang = fix16_from_int(pSprite->ang);
+    pSprite->type = kDudePlayer1+nPlayer;
+    pSprite->clipdist = pDudeInfo->clipdist;
+    pSprite->hitag = 15;
+    pXSprite->burnTime = 0;
+    pXSprite->burnSource = -1;
+    pPlayer->pXSprite->health = pDudeInfo->startHealth<<4;
+    pPlayer->pSprite->cstat &= (unsigned short)~32768;
+    pPlayer->at63 = 0;
+    pPlayer->q16horiz = 0;
+    pPlayer->q16slopehoriz = 0;
+    pPlayer->q16look = 0;
+    pPlayer->at83 = 0;
+    pPlayer->at2ee = -1;
+    pPlayer->at2f2 = 1200;
+    pPlayer->at2f6 = 0;
+    pPlayer->at2fa = 0;
+    pPlayer->at2fe = 0;
+    pPlayer->at302 = 0;
+    pPlayer->at306 = 0;
+    pPlayer->at30a = 0;
+    pPlayer->at30e = 0;
+    pPlayer->at312 = 0;
+    pPlayer->at316 = 0;
+    pPlayer->at2f = 0;
+    pPlayer->voodooTarget = -1;
+    pPlayer->at34e = 0;
+    pPlayer->at352 = 0;
+    pPlayer->at356 = 0;
+    playerResetInertia(pPlayer);
+    pPlayer->at73 = 0;
+    pPlayer->at1ca.dx = 0x4000;
+    pPlayer->at1ca.dy = 0;
+    pPlayer->at1ca.dz = 0;
+    pPlayer->at1d6 = -1;
+    pPlayer->at6b = pPlayer->at73;
+    for (int i = 0; i < 8; i++)
+        pPlayer->at88[i] = gGameOptions.nGameType >= 2;
+    pPlayer->at90 = 0;
+    for (int i = 0; i < 8; i++)
+        pPlayer->at91[i] = -1;
+    for (int i = 0; i < 7; i++)
+        pPlayer->ata1[i] = 0;
+    if (pPlayer->at31a)
+        playerSetGodMode(pPlayer, 1);
+    gInfiniteAmmo = 0;
+    gFullMap = 0;
+    pPlayer->at1ba = 0;
+    pPlayer->at1fe = 0;
+    pPlayer->atbe = 0;
+    xvel[pSprite->index] = yvel[pSprite->index] = zvel[pSprite->index] = 0;
+    pInput->q16turn = 0;
+    pInput->keyFlags.word = 0;
+    pInput->forward = 0;
+    pInput->strafe = 0;
+    pInput->q16mlook = 0;
+    pInput->buttonFlags.byte = 0;
+    pInput->useFlags.byte = 0;
+    pPlayer->at35a = 0;
+    pPlayer->at37f = 0;
+    pPlayer->at35e = 0;
+    pPlayer->at362 = 0;
+    pPlayer->at366 = 0;
+    pPlayer->at36a = 0;
+    pPlayer->at36e = 0;
+    pPlayer->at372 = 0;
+    pPlayer->atbf = 0;
+    pPlayer->atc3 = 0;
+    pPlayer->at26 = -1;
+    pPlayer->at376 = 0;
+    pPlayer->nWaterPal = 0;
+    playerResetPowerUps(pPlayer);
+
+    if (pPlayer == gMe)
+    {
+        viewInitializePrediction();
+        gViewMap.x = pPlayer->pSprite->x;
+        gViewMap.y = pPlayer->pSprite->y;
+        gViewMap.angle = pPlayer->pSprite->ang;
+    }
+    if (IsUnderwaterSector(pSprite->sectnum))
+    {
+        pPlayer->at2f = 1;
+        pPlayer->pXSprite->medium = 1;
+    }
+}
+
+void playerReset(PLAYER *pPlayer)
+{
+    static int dword_136400[] = {
+        3, 4, 2, 8, 9, 10, 7, 1, 1, 1, 1, 1, 1, 1
+    };
+    static int dword_136438[] = {
+        3, 4, 2, 8, 9, 10, 7, 1, 1, 1, 1, 1, 1, 1
+    };
+    dassert(pPlayer != NULL);
+    for (int i = 0; i < 14; i++)
+    {
+        pPlayer->atcb[i] = gInfiniteAmmo;
+        pPlayer->atd9[i] = 0;
+    }
+    pPlayer->atcb[1] = 1;
+    pPlayer->atbd = 0;
+    pPlayer->at2a = -1;
+    pPlayer->atc.newWeapon = 1;
+    for (int i = 0; i < 14; i++)
+    {
+        pPlayer->at111[0][i] = dword_136400[i];
+        pPlayer->at111[1][i] = dword_136438[i];
+    }
+    for (int i = 0; i < 12; i++)
+    {
+        if (gInfiniteAmmo)
+            pPlayer->at181[i] = gAmmoInfo[i].at0;
+        else
+            pPlayer->at181[i] = 0;
+    }
+    for (int i = 0; i < 3; i++)
+        pPlayer->at33e[i] = 0;
+    pPlayer->atbf = 0;
+    pPlayer->atc3 = 0;
+    pPlayer->at26 = -1;
+    pPlayer->at1b1 = 0;
+    pPlayer->at321 = -1;
+    for (int i = 0; i < 5; i++)
+    {
+        pPlayer->packInfo[i].at0 = 0;
+        pPlayer->packInfo[i].at1 = 0;
+    }
+}
+
+int dword_21EFB0[8];
+int dword_21EFD0[8];
+
+void playerInit(int nPlayer, unsigned int a2)
+{
+    PLAYER *pPlayer = &gPlayer[nPlayer];
+    if (!(a2&1))
+        memset(pPlayer, 0, sizeof(PLAYER));
+    pPlayer->at57 = nPlayer;
+    pPlayer->at2ea = nPlayer;
+    if (gGameOptions.nGameType == 3)
+        pPlayer->at2ea = nPlayer&1;
+    pPlayer->at2c6 = 0;
+    memset(dword_21EFB0, 0, sizeof(dword_21EFB0));
+    memset(dword_21EFD0, 0, sizeof(dword_21EFD0));
+    memset(pPlayer->at2ca, 0, sizeof(pPlayer->at2ca));
+    if (!(a2&1))
+        playerReset(pPlayer);
+}
+
+char sub_3A158(PLAYER *a1, spritetype *a2)
+{
+    for (int nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        if (a2 && a2->index == nSprite)
+            continue;
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->type == 431 && actOwnerIdToSpriteId(pSprite->owner) == a1->at5b)
+            return 1;
+    }
+    return 0;
+}
+
+char PickupItem(PLAYER *pPlayer, spritetype *pItem)
+{
+    char buffer[80];
+    int pickupSnd = 775;
+    spritetype *pSprite = pPlayer->pSprite;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+    int nType = pItem->type - 100;
+    switch (pItem->type)
+    {
+    //case 129:
+        //dudeInfo[31].seqStartID = 13568;
+        //if (!powerupActivate(pPlayer, nType))
+            //return 0;
+        //return 1;
+    case 113:
+        if (isGrown(pPlayer->pSprite)) return false;
+    case 130:
+    case 129:
+        switch (pItem->type) {
+        case 130:
+            if (isShrinked(pSprite)) return false;
+            break;
+        case 129:
+            if (isGrown(pSprite)) return false;
+            break;
+        }
+        powerupActivate(pPlayer, nType);
+        break;
+    case 145:
+    case 146:
+        if (gGameOptions.nGameType != 3)
+            return 0;
+        if (pItem->extra > 0)
+        {
+            XSPRITE *pXItem = &xsprite[pItem->extra];
+            if (pItem->type == 145)
+            {
+                if (pPlayer->at2ea == 1)
+                {
+                    if ((pPlayer->at90&1) == 0 && pXItem->state)
+                    {
+                        pPlayer->at90 |= 1;
+                        pPlayer->at91[0] = pItem->index;
+                        trTriggerSprite(pItem->index, pXItem, 0);
+                        sprintf(buffer, "%s stole Blue Flag", gProfile[pPlayer->at57].name);
+                        sndStartSample(8007, 255, 2, 0);
+                        viewSetMessage(buffer);
+                    }
+                }
+                if (pPlayer->at2ea == 0)
+                {
+                    if ((pPlayer->at90&1) != 0 && !pXItem->state)
+                    {
+                        pPlayer->at90 &= ~1;
+                        pPlayer->at91[0] = -1;
+                        trTriggerSprite(pItem->index, pXItem, 1);
+                        sprintf(buffer, "%s returned Blue Flag", gProfile[pPlayer->at57].name);
+                        sndStartSample(8003, 255, 2, 0);
+                        viewSetMessage(buffer);
+                    }
+                    if ((pPlayer->at90&2) != 0 && pXItem->state)
+                    {
+                        pPlayer->at90 &= ~2;
+                        pPlayer->at91[1] = -1;
+                        dword_21EFB0[pPlayer->at2ea] += 10;
+                        dword_21EFD0[pPlayer->at2ea] += 240;
+                        evSend(0, 0, 81, COMMAND_ID_1);
+                        sprintf(buffer, "%s captured Red Flag!", gProfile[pPlayer->at57].name);
+                        sndStartSample(8001, 255, 2, 0);
+                        viewSetMessage(buffer);
+#if 0
+                        if (dword_28E3D4 == 3 && myconnectindex == connecthead)
+                        {
+                            sprintf(buffer, "frag A killed B\n");
+                            sub_7AC28(buffer);
+                        }
+#endif
+                    }
+                }
+            }
+            else if (pItem->type == 146)
+            {
+                if (pPlayer->at2ea == 0)
+                {
+                    if((pPlayer->at90&2) == 0 && pXItem->state)
+                    {
+                        pPlayer->at90 |= 2;
+                        pPlayer->at91[1] = pItem->index;
+                        trTriggerSprite(pItem->index, pXItem, 0);
+                        sprintf(buffer, "%s stole Red Flag", gProfile[pPlayer->at57].name);
+                        sndStartSample(8006, 255, 2, 0);
+                        viewSetMessage(buffer);
+                    }
+                }
+                if (pPlayer->at2ea == 1)
+                {
+                    if ((pPlayer->at90&2) != 0 && !pXItem->state)
+                    {
+                        pPlayer->at90 &= ~2;
+                        pPlayer->at91[1] = -1;
+                        trTriggerSprite(pItem->index, pXItem, 1);
+                        sprintf(buffer, "%s returned Red Flag", gProfile[pPlayer->at57].name);
+                        sndStartSample(8002, 255, 2, 0);
+                        viewSetMessage(buffer);
+                    }
+                    if ((pPlayer->at90&1) != 0 && pXItem->state)
+                    {
+                        pPlayer->at90 &= ~1;
+                        pPlayer->at91[0] = -1;
+                        dword_21EFB0[pPlayer->at2ea] += 10;
+                        dword_21EFD0[pPlayer->at2ea] += 240;
+                        evSend(0, 0, 80, COMMAND_ID_1);
+                        sprintf(buffer, "%s captured Red Flag!", gProfile[pPlayer->at57].name);
+                        sndStartSample(8000, 255, 2, 0);
+                        viewSetMessage(buffer);
+#if 0
+                        if (dword_28E3D4 == 3 && myconnectindex == connecthead)
+                        {
+                            sprintf(buffer, "frag B killed A\n");
+                            sub_7AC28(buffer);
+                        }
+#endif
+                    }
+                }
+            }
+        }
+        return 0;
+    case 147:
+        if (gGameOptions.nGameType != 3)
+            return 0;
+        evKill(pItem->index, 3, CALLBACK_ID_17);
+        pPlayer->at90 |= 1;
+        pPlayer->at91[0] = pItem->index;
+        break;
+    case 148:
+        if (gGameOptions.nGameType != 3)
+            return 0;
+        evKill(pItem->index, 3, CALLBACK_ID_17);
+        pPlayer->at90 |= 2;
+        pPlayer->at91[1] = pItem->index;
+        break;
+    case 140:
+    case 141:
+    case 142:
+    case 143:
+    case 144:
+    {
+        ARMORDATA *pArmorData = &armorData[pItem->type-140];
+        char va = 0;
+        if (pPlayer->at33e[1] < pArmorData->atc)
+        {
+            pPlayer->at33e[1] = ClipHigh(pPlayer->at33e[1]+pArmorData->at8, pArmorData->atc);
+            va = 1;
+        }
+        if (pPlayer->at33e[0] < pArmorData->at4)
+        {
+            pPlayer->at33e[0] = ClipHigh(pPlayer->at33e[0]+pArmorData->at0, pArmorData->at4);
+            va = 1;
+        }
+        if (pPlayer->at33e[2] < pArmorData->at14)
+        {
+            pPlayer->at33e[2] = ClipHigh(pPlayer->at33e[2]+pArmorData->at10, pArmorData->at14);
+            va = 1;
+        }
+        if (!va)
+            return 0;
+        pickupSnd = 779;
+        break;
+    }
+    case 121:
+        if (gGameOptions.nGameType == 0)
+            return 0;
+        if (!packAddItem(pPlayer, gItemData[nType].at8))
+            return 0;
+        break;
+    case 100:
+    case 101:
+    case 102:
+    case 103:
+    case 104:
+    case 105:
+    case 106:
+        if (pPlayer->at88[pItem->type-99])
+            return 0;
+        pPlayer->at88[pItem->type-99] = 1;
+        pickupSnd = 781;
+        break;
+    case 108:
+    case 109:
+    case 110:
+    case 111: 
+    {
+        int addPower = gPowerUpInfo[nType].at3;
+        // by NoOne: allow custom amount for item
+        if (sprite[pItem->xvel].extra >= 0 && xsprite[sprite[pItem->xvel].extra].data1 > 0 && !VanillaMode() && !DemoRecordStatus())
+            addPower = xsprite[sprite[pItem->xvel].extra].data1;
+        if (!actHealDude(pXSprite, addPower, gPowerUpInfo[nType].at7))
+            return 0;
+        return 1;
+    }
+    case 107:
+    case 115:
+    case 118:
+    case 125:
+        if (!packAddItem(pPlayer, gItemData[nType].at8))
+            return 0;
+        break;
+    default:
+        if (!powerupActivate(pPlayer, nType))
+            return 0;
+        return 1;
+    }
+    sfxPlay3DSound(pSprite->x, pSprite->y, pSprite->z, pickupSnd, pSprite->sectnum);
+    return 1;
+}
+
+char PickupAmmo(PLAYER* pPlayer, spritetype* pAmmo)
+{
+    AMMOITEMDATA* pAmmoItemData = &gAmmoItemData[pAmmo->type - 60];
+    int nAmmoType = pAmmoItemData->ata;
+
+    if (pPlayer->at181[nAmmoType] >= gAmmoInfo[nAmmoType].at0) return 0;
+    else if (pAmmo->extra < 0 || xsprite[pAmmo->extra].data1 <= 0 || VanillaMode() || DemoRecordStatus())
+        pPlayer->at181[nAmmoType] = ClipHigh(pPlayer->at181[nAmmoType]+pAmmoItemData->at8, gAmmoInfo[nAmmoType].at0);
+    // by NoOne: allow custom amount for item
+    else
+        pPlayer->at181[nAmmoType] = ClipHigh(pPlayer->at181[nAmmoType] + xsprite[pAmmo->extra].data1, gAmmoInfo[nAmmoType].at0);
+
+    if (pAmmoItemData->atb)
+        pPlayer->atcb[pAmmoItemData->atb] = 1;
+    sfxPlay3DSound(pPlayer->pSprite, 782, -1, 0);
+    return 1;
+}
+
+char PickupWeapon(PLAYER *pPlayer, spritetype *pWeapon)
+{
+    WEAPONITEMDATA *pWeaponItemData = &gWeaponItemData[pWeapon->type-40];
+    int nWeaponType = pWeaponItemData->at8;
+    int nAmmoType = pWeaponItemData->ata;
+    if (!pPlayer->atcb[nWeaponType] || gGameOptions.nWeaponSettings == 2 || gGameOptions.nWeaponSettings == 3)
+    {
+        if (pWeapon->type == 50 && gGameOptions.nGameType > 1 && sub_3A158(pPlayer, NULL))
+            return 0;
+        pPlayer->atcb[nWeaponType] = 1;
+        if (nAmmoType == -1) return 0;
+        // By NoOne: allow to set custom ammo count for weapon pickups
+        if (pWeapon->extra < 0 || xsprite[pWeapon->extra].data1 <= 0 || VanillaMode() || DemoRecordStatus())
+            pPlayer->at181[nAmmoType] = ClipHigh(pPlayer->at181[nAmmoType] + pWeaponItemData->atc, gAmmoInfo[nAmmoType].at0);
+        else
+            pPlayer->at181[nAmmoType] = ClipHigh(pPlayer->at181[nAmmoType] + xsprite[pWeapon->extra].data1, gAmmoInfo[nAmmoType].at0);
+
+        int nNewWeapon = WeaponUpgrade(pPlayer, nWeaponType);
+        if (nNewWeapon != pPlayer->atbd)
+        {
+            pPlayer->atc3 = 0;
+            pPlayer->atbe = nNewWeapon;
+        }
+        sfxPlay3DSound(pPlayer->pSprite, 777, -1, 0);
+        return 1;
+    }
+    if (!actGetRespawnTime(pWeapon))
+        return 0;
+    if (nAmmoType == -1)
+        return 0;
+    if (pPlayer->at181[nAmmoType] >= gAmmoInfo[nAmmoType].at0)
+        return 0;
+    pPlayer->at181[nAmmoType] = ClipHigh(pPlayer->at181[nAmmoType]+pWeaponItemData->atc, gAmmoInfo[nAmmoType].at0);
+    sfxPlay3DSound(pPlayer->pSprite, 777, -1, 0);
+    return 1;
+}
+
+void PickUp(PLAYER *pPlayer, spritetype *pSprite)
+{
+    char buffer[80];
+    int nType = pSprite->type;
+    char pickedUp = 0;
+    int customMsg = -1;
+    if (nType != 40 && nType != 80) { // By NoOne: no pickup for random item generators.
+        
+        XSPRITE* pXSprite = (pSprite->extra >= 0) ? &xsprite[pSprite->extra] : NULL;
+        if (pXSprite != NULL && pXSprite->txID != 3 && pXSprite->lockMsg > 0) // by NoOne: allow custom INI message instead "Picked up"
+            customMsg = pXSprite->lockMsg;
+
+        if (nType >= 100 && nType <= 149)
+        {
+            pickedUp = PickupItem(pPlayer, pSprite);
+            if (pickedUp && customMsg == -1) sprintf(buffer, "Picked up %s", gItemText[nType - 100]);
+        }
+        else if (nType >= 60 && nType < 81)
+        {
+            pickedUp = PickupAmmo(pPlayer, pSprite);
+            if (pickedUp && customMsg == -1) sprintf(buffer, "Picked up %s", gAmmoText[nType - 60]);
+        }
+        else if (nType >= 40 && nType < 51)
+        {
+            pickedUp = PickupWeapon(pPlayer, pSprite);
+            if (pickedUp && customMsg == -1) sprintf(buffer, "Picked up %s", gWeaponText[nType - 40]);
+        }
+    }
+    if (pickedUp)
+    {
+        if (pSprite->extra > 0)
+        {
+            XSPRITE *pXSprite = &xsprite[pSprite->extra];
+            if (pXSprite->Pickup)
+                trTriggerSprite(pSprite->index, pXSprite, 32);
+        }
+        if (!actCheckRespawn(pSprite))
+            actPostSprite(pSprite->index, kStatFree);
+        pPlayer->at377 = 30;
+        if (pPlayer == gMe)
+            if (customMsg > 0) trTextOver(customMsg - 1);
+            else viewSetMessage(buffer);
+    }
+}
+
+void CheckPickUp(PLAYER *pPlayer)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    int x = pSprite->x;
+    int y = pSprite->y;
+    int z = pSprite->z;
+    int nSector = pSprite->sectnum;
+    int nNextSprite;
+    for (int nSprite = headspritestat[3]; nSprite >= 0; nSprite = nNextSprite)
+    {
+        spritetype *pItem = &sprite[nSprite];
+        nNextSprite = nextspritestat[nSprite];
+        if (pItem->hitag&32)
+            continue;
+        int dx = klabs(x-pItem->x)>>4;
+        if (dx > 48)
+            continue;
+        int dy = klabs(y-pItem->y)>>4;
+        if (dy > 48)
+            continue;
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        int vb = 0;
+        if (pItem->z < top)
+            vb = (top-pItem->z)>>8;
+        else if (pItem->z > bottom)
+            vb = (pItem->z-bottom)>>8;
+        if (vb > 32)
+            continue;
+        if (approxDist(dx,dy) > 48)
+            continue;
+        GetSpriteExtents(pItem, &top, &bottom);
+        if (cansee(x, y, z, nSector, pItem->x, pItem->y, pItem->z, pItem->sectnum)
+         || cansee(x, y, z, nSector, pItem->x, pItem->y, top, pItem->sectnum)
+         || cansee(x, y, z, nSector, pItem->x, pItem->y, bottom, pItem->sectnum))
+            PickUp(pPlayer, pItem);
+    }
+}
+
+int ActionScan(PLAYER *pPlayer, int *a2, int *a3)
+{
+    *a2 = 0;
+    *a3 = 0;
+    spritetype *pSprite = pPlayer->pSprite;
+    int x = Cos(pSprite->ang)>>16;
+    int y = Sin(pSprite->ang)>>16;
+    int z = pPlayer->at83;
+    int hit = HitScan(pSprite, pPlayer->at67, x, y, z, 0x10000040, 128);
+    int hitDist = approxDist(pSprite->x-gHitInfo.hitx, pSprite->y-gHitInfo.hity)>>4;
+    if (hitDist < 64)
+    {
+        switch (hit)
+        {
+        case 3:
+            *a2 = gHitInfo.hitsprite;
+            *a3 = sprite[*a2].extra;
+            if (*a3 > 0 && sprite[*a2].statnum == 4)
+            {
+                spritetype *pSprite = &sprite[*a2];
+                XSPRITE *pXSprite = &xsprite[*a3];
+                if (pSprite->type == 431)
+                {
+                    if (gGameOptions.nGameType > 1 && sub_3A158(pPlayer, pSprite))
+                        return -1;
+                    pXSprite->data4 = pPlayer->at57;
+                    pXSprite->isTriggered = 0;
+                }
+            }
+            if (*a3 > 0 && xsprite[*a3].Push)
+                return 3;
+            if (sprite[*a2].statnum == 6)
+            {
+                spritetype *pSprite = &sprite[*a2];
+                XSPRITE *pXSprite = &xsprite[*a3];
+                int nMass = dudeInfo[pSprite->type-kDudeBase].mass;
+                if (nMass)
+                {
+                    int t2 = divscale(0xccccc, nMass, 8);
+                    xvel[*a2] += mulscale16(x, t2);
+                    yvel[*a2] += mulscale16(y, t2);
+                    zvel[*a2] += mulscale16(z, t2);
+                }
+                if (pXSprite->Push && !pXSprite->state && !pXSprite->isTriggered)
+                    trTriggerSprite(*a2, pXSprite, 30);
+            }
+            break;
+        case 0:
+        case 4:
+            *a2 = gHitInfo.hitwall;
+            *a3 = wall[*a2].extra;
+            if (*a3 > 0 && xwall[*a3].triggerPush)
+                return 0;
+            if (wall[*a2].nextsector >= 0)
+            {
+                *a2 = wall[*a2].nextsector;
+                *a3 = sector[*a2].extra;
+                if (*a3 > 0 && xsector[*a3].Wallpush)
+                    return 6;
+            }
+            break;
+        case 1:
+        case 2:
+            *a2 = gHitInfo.hitsect;
+            *a3 = sector[*a2].extra;
+            if (*a3 > 0 && xsector[*a3].Push)
+                return 6;
+            break;
+        }
+    }
+    *a2 = pSprite->sectnum;
+    *a3 = sector[*a2].extra;
+    if (*a3 > 0 && xsector[*a3].Push)
+        return 6;
+    return -1;
+}
+
+void ProcessInput(PLAYER *pPlayer)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+    int nSprite = pPlayer->at5b;
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][pPlayer->at2f];
+    GINPUT *pInput = &pPlayer->atc;
+    pPlayer->at2e = pInput->syncFlags.run;
+    if (pInput->buttonFlags.byte || pInput->forward || pInput->strafe || pInput->q16turn)
+        pPlayer->at30a = 0;
+    else if (pPlayer->at30a >= 0)
+        pPlayer->at30a += 4;
+    WeaponProcess(pPlayer);
+    if (pXSprite->health == 0)
+    {
+        char bSeqStat = playerSeqPlaying(pPlayer, 16);
+        if (pPlayer->at2ee != -1)
+        {
+            pPlayer->angold = pSprite->ang = getangle(sprite[pPlayer->at2ee].x - pSprite->x, sprite[pPlayer->at2ee].y - pSprite->y);
+            pPlayer->q16ang = fix16_from_int(pSprite->ang);
+        }
+        pPlayer->at1fe += 4;
+        if (!bSeqStat)
+        {
+            if (bVanilla)
+                pPlayer->q16horiz = fix16_from_int(mulscale16(0x8000-(Cos(ClipHigh(pPlayer->at1fe*8, 1024))>>15), 120));
+            else
+                pPlayer->q16horiz = mulscale16(0x8000-(Cos(ClipHigh(pPlayer->at1fe*8, 1024))>>15), F16(120));
+        }
+        if (pPlayer->atbd)
+            pInput->newWeapon = pPlayer->atbd;
+        if (pInput->keyFlags.action)
+        {
+            if (bSeqStat)
+            {
+                if (pPlayer->at1fe > 360)
+                    seqSpawn(pPlayer->pDudeInfo->seqStartID+14, 3, pPlayer->pSprite->extra, nPlayerSurviveClient);
+            }
+            else if (seqGetStatus(3, pPlayer->pSprite->extra) < 0)
+            {
+                if (pPlayer->pSprite)
+                    pPlayer->pSprite->type = 426;
+                actPostSprite(pPlayer->at5b, 4);
+                seqSpawn(pPlayer->pDudeInfo->seqStartID+15, 3, pPlayer->pSprite->extra, -1);
+                playerReset(pPlayer);
+                if (gGameOptions.nGameType == 0 && numplayers == 1)
+                {
+                    if (gDemo.at0)
+                        gDemo.Close();
+                    pInput->keyFlags.restart = 1;
+                }
+                else
+                    playerStart(pPlayer->at57);
+            }
+            pInput->keyFlags.action = 0;
+        }
+        return;
+    }
+    if (pPlayer->at2f == 1)
+    {
+        int x = Cos(pSprite->ang);
+        int y = Sin(pSprite->ang);
+        if (pInput->forward)
+        {
+            int forward = pInput->forward;
+            if (forward > 0)
+                forward = mulscale8(pPosture->at0, forward);
+            else
+                forward = mulscale8(pPosture->at8, forward);
+            xvel[nSprite] += mulscale30(forward, x);
+            yvel[nSprite] += mulscale30(forward, y);
+        }
+        if (pInput->strafe)
+        {
+            int strafe = pInput->strafe;
+            strafe = mulscale8(pPosture->at4, strafe);
+            xvel[nSprite] += mulscale30(strafe, y);
+            yvel[nSprite] -= mulscale30(strafe, x);
+        }
+    }
+    else if (pXSprite->height < 256)
+    {
+        int speed = 0x10000;
+        if (pXSprite->height > 0)
+            speed -= divscale16(pXSprite->height, 256);
+        int x = Cos(pSprite->ang);
+        int y = Sin(pSprite->ang);
+        if (pInput->forward)
+        {
+            int forward = pInput->forward;
+            if (forward > 0)
+                forward = mulscale8(pPosture->at0, forward);
+            else
+                forward = mulscale8(pPosture->at8, forward);
+            if (pXSprite->height)
+                forward = mulscale16(forward, speed);
+            xvel[nSprite] += mulscale30(forward, x);
+            yvel[nSprite] += mulscale30(forward, y);
+        }
+        if (pInput->strafe)
+        {
+            int strafe = pInput->strafe;
+            strafe = mulscale8(pPosture->at4, strafe);
+            if (pXSprite->height)
+                strafe = mulscale16(strafe, speed);
+            xvel[nSprite] += mulscale30(strafe, y);
+            yvel[nSprite] -= mulscale30(strafe, x);
+        }
+    }
+    if (pInput->q16turn)
+        pPlayer->q16ang = (pPlayer->q16ang+pInput->q16turn)&0x7ffffff;
+    if (pInput->keyFlags.spin180)
+    {
+        if (!pPlayer->at316)
+            pPlayer->at316 = -1024;
+        pInput->keyFlags.spin180 = 0;
+    }
+    if (pPlayer->at316 < 0)
+    {
+        int speed;
+        if (pPlayer->at2f == 1)
+            speed = 64;
+        else
+            speed = 128;
+        pPlayer->at316 = ClipLow(pPlayer->at316+speed, 0);
+        pPlayer->q16ang += fix16_from_int(speed);
+    }
+    pPlayer->q16ang = (pPlayer->q16ang+fix16_from_int(pSprite->ang-pPlayer->angold))&0x7ffffff;
+    pPlayer->angold = pSprite->ang = fix16_to_int(pPlayer->q16ang);
+    if (!pInput->buttonFlags.jump)
+        pPlayer->at31c = 0;
+
+    switch (pPlayer->at2f)
+    {
+    case 1:
+        if (pInput->buttonFlags.jump)
+            zvel[nSprite] -= 0x5b05;
+        if (pInput->buttonFlags.crouch)
+            zvel[nSprite] += 0x5b05;
+        break;
+    case 2:
+        if (!pInput->buttonFlags.crouch)
+            pPlayer->at2f = 0;
+        break;
+    default:
+        if (!pPlayer->at31c && pInput->buttonFlags.jump && pXSprite->height == 0)
+        {
+            sfxPlay3DSound(pSprite, 700, 0, 0);
+            if (packItemActive(pPlayer, 4))
+                zvel[nSprite] = -0x175555;
+            else
+                zvel[nSprite] = -0xbaaaa;
+            
+
+            if (isShrinked(pPlayer->pSprite)) zvel[nSprite] -= -200000;
+            else if (isGrown(pPlayer->pSprite)) zvel[nSprite] += -250000;
+            
+            pPlayer->at31c = 1;
+        }
+
+
+        if (pInput->buttonFlags.crouch)
+            pPlayer->at2f = 2;
+        break;
+    }
+    if (pInput->keyFlags.action)
+    {
+        int a2, a3;
+        int hit = ActionScan(pPlayer, &a2, &a3);
+        switch (hit)
+        {
+        case 6:
+            if (a3 > 0 && a3 <= 2048)
+            {
+                XSECTOR *pXSector = &xsector[a3];
+                int key = pXSector->Key;
+                if (pXSector->locked && pPlayer == gMe)
+                {
+                    viewSetMessage("It's locked");
+                    sndStartSample(3062, 255, 2, 0);
+                }
+                if (!key || pPlayer->at88[key])
+                    trTriggerSector(a2, pXSector, 30);
+                else if (pPlayer == gMe)
+                {
+                    viewSetMessage("That requires a key.");
+                    sndStartSample(3063, 255, 2, 0);
+                }
+            }
+            break;
+        case 0:
+        {
+            XWALL *pXWall = &xwall[a3];
+            int key = pXWall->key;
+            if (pXWall->locked && pPlayer == gMe)
+            {
+                viewSetMessage("It's locked");
+                sndStartSample(3062, 255, 2, 0);
+            }
+            if (!key || pPlayer->at88[key])
+                trTriggerWall(a2, pXWall, 50);
+            else if (pPlayer == gMe)
+            {
+                viewSetMessage("That requires a key.");
+                sndStartSample(3063, 255, 2, 0);
+            }
+            break;
+        }
+        case 3:
+        {
+            XSPRITE *pXSprite = &xsprite[a3];
+            int key = pXSprite->key;
+            if (pXSprite->locked && pPlayer == gMe && pXSprite->lockMsg)
+                trTextOver(pXSprite->lockMsg);
+            if (!key || pPlayer->at88[key])
+                trTriggerSprite(a2, pXSprite, 30);
+            else if (pPlayer == gMe)
+            {
+                viewSetMessage("That requires a key.");
+                sndStartSample(3063, 255, 2, 0);
+            }
+            break;
+        }
+        }
+        if (pPlayer->at372 > 0)
+            pPlayer->at372 = ClipLow(pPlayer->at372-4*(6-gGameOptions.nDifficulty), 0);
+        if (pPlayer->at372 <= 0 && pPlayer->at376)
+        {
+            spritetype *pSprite2 = actSpawnDude(pPlayer->pSprite, 212, pPlayer->pSprite->clipdist<<1, 0);
+            pSprite2->ang = (pPlayer->pSprite->ang+1024)&2047;
+            int nSprite = pPlayer->pSprite->index;
+            int x = Cos(pPlayer->pSprite->ang)>>16;
+            int y = Sin(pPlayer->pSprite->ang)>>16;
+            xvel[pSprite2->index] = xvel[nSprite] + mulscale14(0x155555, x);
+            yvel[pSprite2->index] = yvel[nSprite] + mulscale14(0x155555, y);
+            zvel[pSprite2->index] = zvel[nSprite];
+            pPlayer->at376 = 0;
+        }
+        pInput->keyFlags.action = 0;
+    }
+    if (bVanilla)
+    {
+        if (pInput->keyFlags.lookCenter && !pInput->buttonFlags.lookUp && !pInput->buttonFlags.lookDown)
+        {
+            if (pPlayer->q16look < 0)
+                pPlayer->q16look = fix16_min(pPlayer->q16look+F16(4), F16(0));
+            if (pPlayer->q16look > 0)
+                pPlayer->q16look = fix16_max(pPlayer->q16look-F16(4), F16(0));
+            if (!pPlayer->q16look)
+                pInput->keyFlags.lookCenter = 0;
+        }
+        else
+        {
+            if (pInput->buttonFlags.lookUp)
+                pPlayer->q16look = fix16_min(pPlayer->q16look+F16(4), F16(60));
+            if (pInput->buttonFlags.lookDown)
+                pPlayer->q16look = fix16_max(pPlayer->q16look-F16(4), F16(-60));
+        }
+        pPlayer->q16look = fix16_clamp(pPlayer->q16look+pInput->q16mlook, F16(-60), F16(60));
+        if (pPlayer->q16look > 0)
+            pPlayer->q16horiz = fix16_from_int(mulscale30(120, Sin(fix16_to_int(pPlayer->q16look)<<3)));
+        else if (pPlayer->q16look < 0)
+            pPlayer->q16horiz = fix16_from_int(mulscale30(180, Sin(fix16_to_int(pPlayer->q16look)<<3)));
+        else
+            pPlayer->q16horiz = 0;
+    }
+    else
+    {
+        CONSTEXPR int upAngle = 289;
+        CONSTEXPR int downAngle = -347;
+        CONSTEXPR double lookStepUp = 4.0*upAngle/60.0;
+        CONSTEXPR double lookStepDown = -4.0*downAngle/60.0;
+        if (pInput->keyFlags.lookCenter && !pInput->buttonFlags.lookUp && !pInput->buttonFlags.lookDown)
+        {
+            if (pPlayer->q16look < 0)
+                pPlayer->q16look = fix16_min(pPlayer->q16look+F16(lookStepDown), F16(0));
+            if (pPlayer->q16look > 0)
+                pPlayer->q16look = fix16_max(pPlayer->q16look-F16(lookStepUp), F16(0));
+            if (!pPlayer->q16look)
+                pInput->keyFlags.lookCenter = 0;
+        }
+        else
+        {
+            if (pInput->buttonFlags.lookUp)
+                pPlayer->q16look = fix16_min(pPlayer->q16look+F16(lookStepUp), F16(upAngle));
+            if (pInput->buttonFlags.lookDown)
+                pPlayer->q16look = fix16_max(pPlayer->q16look-F16(lookStepDown), F16(downAngle));
+        }
+        pPlayer->q16look = fix16_clamp(pPlayer->q16look+(pInput->q16mlook<<3), F16(downAngle), F16(upAngle));
+        pPlayer->q16horiz = fix16_from_float(100.f*tanf(fix16_to_float(pPlayer->q16look)*fPI/1024.f));
+    }
+    int nSector = pSprite->sectnum;
+    int florhit = gSpriteHit[pSprite->extra].florhit & 0xe000;
+    char va;
+    if (pXSprite->height < 16 && (florhit == 0x4000 || florhit == 0))
+        va = 1;
+    else
+        va = 0;
+    if (va && (sector[nSector].floorstat&2))
+    {
+        int z1 = getflorzofslope(nSector, pSprite->x, pSprite->y);
+        int x2 = pSprite->x+mulscale30(64, Cos(pSprite->ang));
+        int y2 = pSprite->y+mulscale30(64, Sin(pSprite->ang));
+        short nSector2 = nSector;
+        updatesector(x2, y2, &nSector2);
+        if (nSector2 == nSector)
+        {
+            int z2 = getflorzofslope(nSector2, x2, y2);
+            pPlayer->q16slopehoriz = interpolate(pPlayer->q16slopehoriz, fix16_from_int(z1-z2)>>3, 0x4000);
+        }
+    }
+    else
+    {
+        pPlayer->q16slopehoriz = interpolate(pPlayer->q16slopehoriz, F16(0), 0x4000);
+        if (klabs(pPlayer->q16slopehoriz) < 4)
+            pPlayer->q16slopehoriz = 0;
+    }
+    pPlayer->at83 = (-fix16_to_int(pPlayer->q16horiz))<<7;
+    if (pInput->keyFlags.prevItem)
+    {
+        pInput->keyFlags.prevItem = 0;
+        packPrevItem(pPlayer);
+    }
+    if (pInput->keyFlags.nextItem)
+    {
+        pInput->keyFlags.nextItem = 0;
+        packNextItem(pPlayer);
+    }
+    if (pInput->keyFlags.useItem)
+    {
+        pInput->keyFlags.useItem = 0;
+        if (pPlayer->packInfo[pPlayer->at321].at1 > 0)
+            packUseItem(pPlayer, pPlayer->at321);
+    }
+    if (pInput->useFlags.useBeastVision)
+    {
+        pInput->useFlags.useBeastVision = 0;
+        if (pPlayer->packInfo[3].at1 > 0)
+            packUseItem(pPlayer, 3);
+    }
+    if (pInput->useFlags.useCrystalBall)
+    {
+        pInput->useFlags.useCrystalBall = 0;
+        if (pPlayer->packInfo[2].at1 > 0)
+            packUseItem(pPlayer, 2);
+    }
+    if (pInput->useFlags.useJumpBoots)
+    {
+        pInput->useFlags.useJumpBoots = 0;
+        if (pPlayer->packInfo[4].at1 > 0)
+            packUseItem(pPlayer, 4);
+    }
+    if (pInput->useFlags.useMedKit)
+    {
+        pInput->useFlags.useMedKit = 0;
+        if (pPlayer->packInfo[0].at1 > 0)
+            packUseItem(pPlayer, 0);
+    }
+    if (pInput->keyFlags.holsterWeapon)
+    {
+        pInput->keyFlags.holsterWeapon = 0;
+        if (pPlayer->atbd)
+        {
+            WeaponLower(pPlayer);
+            viewSetMessage("Holstering weapon");
+        }
+    }
+    CheckPickUp(pPlayer);
+}
+
+void playerProcess(PLAYER *pPlayer)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    int nSprite = pPlayer->at5b;
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][pPlayer->at2f];
+    powerupProcess(pPlayer);
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+    int dzb = (bottom-pSprite->z)/4;
+    int dzt = (pSprite->z-top)/4;
+    int dw = pSprite->clipdist<<2;
+    if (!gNoClip)
+    {
+        short nSector = pSprite->sectnum;
+        if (pushmove_old(&pSprite->x, &pSprite->y, &pSprite->z, &nSector, dw, dzt, dzb, CLIPMASK0) == -1)
+            actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_0, 500<<4);
+        if (pSprite->sectnum != nSector)
+        {
+            if (nSector == -1)
+            {
+                nSector = pSprite->sectnum;
+                actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_0, 500<<4);
+            }
+            dassert(nSector >= 0 && nSector < kMaxSectors);
+            ChangeSpriteSect(nSprite, nSector);
+        }
+    }
+    ProcessInput(pPlayer);
+    int nSpeed = approxDist(xvel[nSprite], yvel[nSprite]);
+    pPlayer->at6b = interpolate(pPlayer->at6b, zvel[nSprite], 0x7000);
+    int dz = pPlayer->pSprite->z-pPosture->at24-pPlayer->at67;
+    if (dz > 0)
+        pPlayer->at6b += mulscale16(dz<<8, 0xa000);
+    else
+        pPlayer->at6b += mulscale16(dz<<8, 0x1800);
+    pPlayer->at67 += pPlayer->at6b>>8;
+    pPlayer->at73 = interpolate(pPlayer->at73, zvel[nSprite], 0x5000);
+    dz = pPlayer->pSprite->z-pPosture->at28-pPlayer->at6f;
+    if (dz > 0)
+        pPlayer->at73 += mulscale16(dz<<8, 0x8000);
+    else
+        pPlayer->at73 += mulscale16(dz<<8, 0xc00);
+    pPlayer->at6f += pPlayer->at73>>8;
+    pPlayer->at37 = ClipLow(pPlayer->at37-4, 0);
+    nSpeed >>= 16;
+    if (pPlayer->at2f == 1)
+    {
+        pPlayer->at3b = (pPlayer->at3b+17)&2047;
+        pPlayer->at4b = (pPlayer->at4b+17)&2047;
+        pPlayer->at3f = mulscale30(pPosture->at14*10, Sin(pPlayer->at3b*2));
+        pPlayer->at43 = mulscale30(pPosture->at18*pPlayer->at37, Sin(pPlayer->at3b-256));
+        pPlayer->at4f = mulscale30(pPosture->at1c*pPlayer->at37, Sin(pPlayer->at4b*2));
+        pPlayer->at53 = mulscale30(pPosture->at20*pPlayer->at37, Sin(pPlayer->at4b-0x155));
+    }
+    else
+    {
+        if (pXSprite->height < 256)
+        {
+            pPlayer->at3b = (pPlayer->at3b+pPosture->atc[pPlayer->at2e]*4) & 2047;
+            pPlayer->at4b = (pPlayer->at4b+(pPosture->atc[pPlayer->at2e]*4)/2) & 2047;
+            if (pPlayer->at2e)
+            {
+                if (pPlayer->at37 < 60)
+                    pPlayer->at37 = ClipHigh(pPlayer->at37+nSpeed, 60);
+            }
+            else
+            {
+                if (pPlayer->at37 < 30)
+                    pPlayer->at37 = ClipHigh(pPlayer->at37+nSpeed, 30);
+            }
+        }
+        pPlayer->at3f = mulscale30(pPosture->at14*pPlayer->at37, Sin(pPlayer->at3b*2));
+        pPlayer->at43 = mulscale30(pPosture->at18*pPlayer->at37, Sin(pPlayer->at3b-256));
+        pPlayer->at4f = mulscale30(pPosture->at1c*pPlayer->at37, Sin(pPlayer->at4b*2));
+        pPlayer->at53 = mulscale30(pPosture->at20*pPlayer->at37, Sin(pPlayer->at4b-0x155));
+    }
+    pPlayer->at35a = 0;
+    pPlayer->at37f = ClipLow(pPlayer->at37f-4, 0);
+    pPlayer->at35e = ClipLow(pPlayer->at35e-4, 0);
+    pPlayer->at362 = ClipLow(pPlayer->at362-4, 0);
+    pPlayer->at366 = ClipLow(pPlayer->at366-4, 0);
+    pPlayer->at36a = ClipLow(pPlayer->at36a-4, 0);
+    pPlayer->at377 = ClipLow(pPlayer->at377-4, 0);
+    if (pPlayer == gMe && gMe->pXSprite->health == 0)
+        pPlayer->at376 = 0;
+    if (!pXSprite->health)
+        return;
+    pPlayer->at87 = 0;
+    if (pPlayer->at2f == 1)
+    {
+        pPlayer->at87 = 1;
+        int nSector = pSprite->sectnum;
+        int nLink = gLowerLink[nSector];
+        if (nLink > 0 && (sprite[nLink].type == 14 || sprite[nLink].type == 10))
+        {
+            if (getceilzofslope(nSector, pSprite->x, pSprite->y) > pPlayer->at67)
+                pPlayer->at87 = 0;
+        }
+    }
+    if (!pPlayer->at87)
+    {
+        pPlayer->at2f2 = 1200;
+        pPlayer->at36e = 0;
+        if (packItemActive(pPlayer, 1))
+            packUseItem(pPlayer, 1);
+    }
+    int nType = kDudePlayer1-kDudeBase;
+    switch (pPlayer->at2f)
+    {
+    case 1:
+        seqSpawn(dudeInfo[nType].seqStartID+9, 3, nXSprite, -1);
+        break;
+    case 2:
+        seqSpawn(dudeInfo[nType].seqStartID+10, 3, nXSprite, -1);
+        break;
+    default:
+        if (!nSpeed)
+            seqSpawn(dudeInfo[nType].seqStartID, 3, nXSprite, -1);
+        else
+            seqSpawn(dudeInfo[nType].seqStartID+8, 3, nXSprite, -1);
+        break;
+    }
+}
+
+spritetype *playerFireMissile(PLAYER *pPlayer, int a2, int a3, int a4, int a5, int a6)
+{
+    return actFireMissile(pPlayer->pSprite, a2, pPlayer->at6f-pPlayer->pSprite->z, a3, a4, a5, a6);
+}
+
+spritetype * playerFireThing(PLAYER *pPlayer, int a2, int a3, int thingType, int a5)
+{
+    dassert(thingType >= kThingBase && thingType < kThingMax);
+    return actFireThing(pPlayer->pSprite, a2, pPlayer->at6f-pPlayer->pSprite->z, pPlayer->at83+a3, thingType, a5);
+}
+
+void playerFrag(PLAYER *pKiller, PLAYER *pVictim)
+{
+    dassert(pKiller != NULL);
+    dassert(pVictim != NULL);
+    
+    char buffer[128] = "";
+    int nKiller = pKiller->pSprite->type-kDudePlayer1;
+    dassert(nKiller >= 0 && nKiller < kMaxPlayers);
+    int nVictim = pVictim->pSprite->type-kDudePlayer1;
+    dassert(nVictim >= 0 && nVictim < kMaxPlayers);
+    if (myconnectindex == connecthead)
+    {
+        sprintf(buffer, "frag %d killed %d\n", pKiller->at57+1, pVictim->at57+1);
+        sub_7AC28(buffer);
+        buffer[0] = 0;
+    }
+    if (nKiller == nVictim)
+    {
+        pVictim->at2ee = -1;
+        pVictim->at2c6--;
+        pVictim->at2ca[nVictim]--;
+        if (gGameOptions.nGameType == 3)
+            dword_21EFB0[pVictim->at2ea]--;
+        int nMessage = Random(5);
+        int nSound = gSuicide[nMessage].at4;
+        if (pVictim == gMe && gMe->at372 <= 0)
+        {
+            sprintf(buffer, "You killed yourself!");
+            if (gGameOptions.nGameType > 0 && nSound >= 0)
+                sndStartSample(nSound, 255, 2, 0);
+        }
+    }
+    else
+    {
+        pKiller->at2c6++;
+        pKiller->at2ca[nKiller]++;
+        if (gGameOptions.nGameType == 3)
+        {
+            if (pKiller->at2ea == pVictim->at2ea)
+                dword_21EFB0[pKiller->at2ea]--;
+            else
+            {
+                dword_21EFB0[pKiller->at2ea]++;
+                dword_21EFD0[pKiller->at2ea]+=120;
+            }
+        }
+        int nMessage = Random(25);
+        int nSound = gVictory[nMessage].at4;
+        const char* pzMessage = gVictory[nMessage].at0;
+        sprintf(buffer, pzMessage, gProfile[nKiller].name, gProfile[nVictim].name);
+        if (gGameOptions.nGameType > 0 && nSound >= 0 && pKiller == gMe)
+            sndStartSample(nSound, 255, 2, 0);
+    }
+    viewSetMessage(buffer);
+}
+
+void FragPlayer(PLAYER *pPlayer, int nSprite)
+{
+    spritetype *pSprite = NULL;
+    if (nSprite >= 0)
+        pSprite = &sprite[nSprite];
+    if (pSprite && IsPlayerSprite(pSprite))
+    {
+        PLAYER *pKiller = &gPlayer[pSprite->type-kDudePlayer1];
+        playerFrag(pKiller, pPlayer);
+        int nTeam1 = pKiller->at2ea&1;
+        int nTeam2 = pPlayer->at2ea&1;
+        if (nTeam1 == 0)
+        {
+            if (nTeam1 != nTeam2)
+                evSend(0, 0, 15, COMMAND_ID_3);
+            else
+                evSend(0, 0, 16, COMMAND_ID_3);
+        }
+        else
+        {
+            if (nTeam1 == nTeam2)
+                evSend(0, 0, 16, COMMAND_ID_3);
+            else
+                evSend(0, 0, 15, COMMAND_ID_3);
+        }
+    }
+}
+
+int playerDamageArmor(PLAYER *pPlayer, DAMAGE_TYPE nType, int nDamage)
+{
+    DAMAGEINFO *pDamageInfo = &damageInfo[nType];
+    int nArmorType = pDamageInfo->at0;
+    if (nArmorType >= 0 && pPlayer->at33e[nArmorType])
+    {
+#if 0
+        int vbp = (nDamage*7)/8-nDamage/4;
+        int v8 = pPlayer->at33e[nArmorType];
+        int t = nDamage/4 + vbp * v8 / 3200;
+        v8 -= t;
+#endif
+        int v8 = pPlayer->at33e[nArmorType];
+        int t = scale(v8, 0, 3200, nDamage/4, (nDamage*7)/8);
+        v8 -= t;
+        nDamage -= t;
+        pPlayer->at33e[nArmorType] = ClipLow(v8, 0);
+    }
+    return nDamage;
+}
+
+spritetype *sub_40A94(PLAYER *pPlayer, int a2)
+{
+    char buffer[80];
+    spritetype *pSprite = NULL;
+    switch (a2)
+    {
+    case 147:
+        pPlayer->at90 &= ~1;
+        pSprite = actDropObject(pPlayer->pSprite, 147);
+        if (pSprite)
+            pSprite->owner = pPlayer->at91[0];
+        sprintf(buffer, "%s dropped Blue Flag", gProfile[pPlayer->at57].name);
+        sndStartSample(8005, 255, 2, 0);
+        viewSetMessage(buffer);
+        break;
+    case 148:
+        pPlayer->at90 &= ~2;
+        pSprite = actDropObject(pPlayer->pSprite, 148);
+        if (pSprite)
+            pSprite->owner = pPlayer->at91[1];
+        sprintf(buffer, "%s dropped Red Flag", gProfile[pPlayer->at57].name);
+        sndStartSample(8004, 255, 2, 0);
+        viewSetMessage(buffer);
+        break;
+    }
+    return pSprite;
+}
+
+int playerDamageSprite(int nSource, PLAYER *pPlayer, DAMAGE_TYPE nDamageType, int nDamage)
+{
+    dassert(nSource < kMaxSprites);
+    dassert(pPlayer != NULL);
+    if (pPlayer->ata1[nDamageType])
+        return 0;
+    nDamage = playerDamageArmor(pPlayer, nDamageType, nDamage);
+    pPlayer->at366 = ClipHigh(pPlayer->at366+(nDamage>>3), 600);
+
+    spritetype *pSprite = pPlayer->pSprite;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+    int nXSprite = pSprite->extra;
+    int nXSector = sector[pSprite->sectnum].extra;
+    DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+    int nDeathSeqID = -1;
+    int v18 = -1;
+    int nSprite = pSprite->index;
+    char va = playerSeqPlaying(pPlayer, 16);
+    if (!pXSprite->health)
+    {
+        if (va)
+        {
+            switch (nDamageType)
+            {
+            case DAMAGE_TYPE_5:
+                nDeathSeqID = 18;
+                sfxPlay3DSound(pSprite, 716, 0, 0);
+                break;
+            case DAMAGE_TYPE_3:
+                GibSprite(pSprite, GIBTYPE_7, NULL, NULL);
+                GibSprite(pSprite, GIBTYPE_15, NULL, NULL);
+                pPlayer->pSprite->cstat |= 32768;
+                nDeathSeqID = 17;
+                break;
+            default:
+            {
+                int top, bottom;
+                GetSpriteExtents(pSprite, &top, &bottom);
+                CGibPosition gibPos(pSprite->x, pSprite->y, top);
+                CGibVelocity gibVel(xvel[pSprite->index]>>1, yvel[pSprite->index]>>1, -0xccccc);
+                GibSprite(pSprite, GIBTYPE_27, &gibPos, &gibVel);
+                GibSprite(pSprite, GIBTYPE_7, NULL, NULL);
+                fxSpawnBlood(pSprite, nDamage<<4);
+                fxSpawnBlood(pSprite, nDamage<<4);
+                nDeathSeqID = 17;
+                break;
+            }
+            }
+        }
+    }
+    else
+    {
+        int nHealth = pXSprite->health-nDamage;
+        pXSprite->health = ClipLow(nHealth, 0);
+        if (pXSprite->health > 0 && pXSprite->health < 16)
+        {
+            nDamageType = DAMAGE_TYPE_2;
+            pXSprite->health = 0;
+            nHealth = -25;
+        }
+        if (pXSprite->health > 0)
+        {
+            DAMAGEINFO *pDamageInfo = &damageInfo[nDamageType];
+            int nSound;
+            if (nDamage >= (10<<4))
+                nSound = pDamageInfo->at4[0];
+            else
+                nSound = pDamageInfo->at4[Random(3)];
+            if (nDamageType == DAMAGE_TYPE_4 && pXSprite->medium == 1 && !pPlayer->at376)
+                nSound = 714;
+            sfxPlay3DSound(pSprite, nSound, 0, 6);
+            return nDamage;
+        }
+        sfxKill3DSound(pPlayer->pSprite, -1, 441);
+        if (gGameOptions.nGameType == 3 && pPlayer->at90)
+        {
+            if (pPlayer->at90&1)
+                sub_40A94(pPlayer, 147);
+            if (pPlayer->at90&2)
+                sub_40A94(pPlayer, 148);
+        }
+        pPlayer->at1fe = 0;
+        pPlayer->at1b1 = 0;
+        pPlayer->atbd = 0;
+        pPlayer->at2ee = nSource;
+        pPlayer->at34e = 0;
+        if (nDamageType == DAMAGE_TYPE_3 && nDamage < (9<<4))
+            nDamageType = DAMAGE_TYPE_0;
+        switch (nDamageType)
+        {
+        case DAMAGE_TYPE_3:
+            sfxPlay3DSound(pSprite, 717, 0, 0);
+            GibSprite(pSprite, GIBTYPE_7, NULL, NULL);
+            GibSprite(pSprite, GIBTYPE_15, NULL, NULL);
+            pPlayer->pSprite->cstat |= 32768;
+            nDeathSeqID = 2;
+            break;
+        case DAMAGE_TYPE_1:
+            sfxPlay3DSound(pSprite, 718, 0, 0);
+            nDeathSeqID = 3;
+            break;
+        case DAMAGE_TYPE_4:
+            nDeathSeqID = 1;
+            break;
+        default:
+            if (nHealth < -20 && gGameOptions.nGameType >= 2 && Chance(0x4000))
+            {
+                DAMAGEINFO *pDamageInfo = &damageInfo[nDamageType];
+                sfxPlay3DSound(pSprite, pDamageInfo->at10[0], 0, 2);
+                nDeathSeqID = 16;
+                v18 = nPlayerKeelClient;
+                powerupActivate(pPlayer, 28);
+                pXSprite->target = nSource;
+                evPost(pSprite->index, 3, 15, CALLBACK_ID_13);
+            }
+            else
+            {
+                sfxPlay3DSound(pSprite, 716, 0, 0);
+                nDeathSeqID = 1;
+            }
+            break;
+        }
+    }
+    if (nDeathSeqID < 0)
+        return nDamage;
+    if (nDeathSeqID != 16)
+    {
+        powerupClear(pPlayer);
+        if (nXSector > 0 && xsector[nXSector].Exit)
+            trTriggerSector(pSprite->sectnum, &xsector[nXSector], 43);
+        pSprite->hitag |= 7;
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            if (gPlayer[p].at2ee == nSprite && gPlayer[p].at1fe > 0)
+                gPlayer[p].at2ee = -1;
+        }
+        FragPlayer(pPlayer, nSource);
+        trTriggerSprite(nSprite, pXSprite, 0);
+    }
+    dassert(gSysRes.Lookup(pDudeInfo->seqStartID + nDeathSeqID, "SEQ") != NULL);
+    seqSpawn(pDudeInfo->seqStartID+nDeathSeqID, 3, nXSprite, v18);
+    return nDamage;
+}
+
+int UseAmmo(PLAYER *pPlayer, int nAmmoType, int nDec)
+{
+    if (gInfiniteAmmo)
+        return 9999;
+    if (nAmmoType == -1)
+        return 9999;
+    pPlayer->at181[nAmmoType] = ClipLow(pPlayer->at181[nAmmoType]-nDec, 0);
+    return pPlayer->at181[nAmmoType];
+}
+
+void sub_41250(PLAYER *pPlayer)
+{
+    int v4 = pPlayer->at1be.dz;
+    int dz = pPlayer->at6f-pPlayer->pSprite->z;
+    if (UseAmmo(pPlayer, 9, 0) < 8)
+    {
+        pPlayer->at34e = 0;
+        return;
+    }
+    for (int i = 0; i < 4; i++)
+    {
+        int ang1 = (pPlayer->at352+pPlayer->at356)&2047;
+        actFireVector(pPlayer->pSprite, 0, dz, Cos(ang1)>>16, Sin(ang1)>>16, v4, VECTOR_TYPE_21);
+        int ang2 = (pPlayer->at352+2048-pPlayer->at356)&2047;
+        actFireVector(pPlayer->pSprite, 0, dz, Cos(ang2)>>16, Sin(ang2)>>16, v4, VECTOR_TYPE_21);
+    }
+    pPlayer->at34e = ClipLow(pPlayer->at34e-1, 0);
+}
+
+void playerLandingSound(PLAYER *pPlayer)
+{
+    static int surfaceSound[] = {
+        -1,
+        600,
+        601,
+        602,
+        603,
+        604,
+        605,
+        605,
+        605,
+        600,
+        605,
+        605,
+        605,
+        604,
+        603
+    };
+    spritetype *pSprite = pPlayer->pSprite;
+    SPRITEHIT *pHit = &gSpriteHit[pSprite->extra];
+    if (pHit->florhit)
+    {
+        char nSurf = tileGetSurfType(pHit->florhit);
+        if (nSurf)
+            sfxPlay3DSound(pSprite, surfaceSound[nSurf], -1, 0);
+    }
+}
+
+void PlayerSurvive(int, int nXSprite)
+{
+    char buffer[80];
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    actHealDude(pXSprite, 1, 2);
+    if (gGameOptions.nGameType > 0 && numplayers > 1)
+    {
+        sfxPlay3DSound(pSprite, 3009, 0, 6);
+        if (IsPlayerSprite(pSprite))
+        {
+            PLAYER *pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+            if (pPlayer == gMe)
+                viewSetMessage("I LIVE...AGAIN!!");
+            else
+            {
+                sprintf(buffer, "%s lives again!", gProfile[pPlayer->at57].name);
+                viewSetMessage(buffer);
+            }
+            pPlayer->atc.newWeapon = 1;
+        }
+    }
+}
+
+void PlayerKeelsOver(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    for (int p = connecthead; p >= 0; p = connectpoint2[p])
+    {
+        if (gPlayer[p].pXSprite == pXSprite)
+        {
+            PLAYER *pPlayer = &gPlayer[p];
+            playerDamageSprite(pPlayer->at2ee, pPlayer, DAMAGE_TYPE_5, 500<<4);
+            return;
+        }
+    }
+}
+
+class PlayerLoadSave : public LoadSave
+{
+public:
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+void PlayerLoadSave::Load(void)
+{
+    Read(dword_21EFB0, sizeof(dword_21EFB0));
+    Read(&gNetPlayers, sizeof(gNetPlayers));
+    Read(&gProfile, sizeof(gProfile));
+    Read(&gPlayer, sizeof(gPlayer));
+    for (int i = 0; i < gNetPlayers; i++)
+    {
+        gPlayer[i].pSprite = &sprite[gPlayer[i].at5b];
+        gPlayer[i].pXSprite = &xsprite[gPlayer[i].pSprite->extra];
+        gPlayer[i].pDudeInfo = &dudeInfo[gPlayer[i].pSprite->type-kDudeBase];
+    }
+}
+
+void PlayerLoadSave::Save(void)
+{
+    Write(dword_21EFB0, sizeof(dword_21EFB0));
+    Write(&gNetPlayers, sizeof(gNetPlayers));
+    Write(&gProfile, sizeof(gProfile));
+    Write(&gPlayer, sizeof(gPlayer));
+}
+
+static PlayerLoadSave *myLoadSave;
+
+void PlayerLoadSaveConstruct(void)
+{
+    myLoadSave = new PlayerLoadSave();
+}
diff --git a/source/blood/src/player.h b/source/blood/src/player.h
new file mode 100644
index 000000000..eaac430c4
--- /dev/null
+++ b/source/blood/src/player.h
@@ -0,0 +1,252 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "fix16.h"
+#include "common_game.h"
+#include "actor.h"
+#include "blood.h"
+#include "config.h"
+#include "controls.h"
+#include "db.h"
+#include "dude.h"
+
+enum LifeMode {
+    kModeHuman = 0,
+    kModeBeast,
+    kModeHumanShrink,
+    kModeHumanGrown,
+};
+
+struct PACKINFO {
+    char at0; // is active (0/1)
+    int at1 = 0; // remaining percent
+};
+
+struct PLAYER {
+    spritetype *pSprite;
+    XSPRITE *pXSprite;
+    DUDEINFO *pDudeInfo;
+    GINPUT atc;
+    //short atc; // INPUT
+    //char at10; // forward
+    //short at11; // turn
+    //char hearDist; // strafe
+    //int at14; // buttonFlags
+    //unsigned int at18; // keyFlags
+    //char at1c; // useFlags;
+    //char at20; // newWeapon
+    //char at21; // mlook
+    int at22;
+    int at26; // weapon qav
+    int at2a; // qav callback
+    char at2e; // run
+    int at2f; // state
+    int at33; // unused?
+    int at37;
+    int at3b;
+    int at3f; // bob height
+    int at43; // bob width
+    int at47;
+    int at4b;
+    int at4f; // bob sway y
+    int at53; // bob sway x
+    int at57; // Connect id
+    int at5b; // spritenum
+    int at5f; // life mode
+    int at63;
+    int at67; // view z
+    int at6b;
+    int at6f; // weapon z
+    int at73;
+    fix16_t q16look;
+    int q16horiz; // horiz
+    int q16slopehoriz; // horizoff
+    int at83;
+    char at87; // underwater
+    char at88[8]; // keys
+    char at90; // flag capture
+    short at91[8];
+    int ata1[7];
+    char atbd; // weapon
+    char atbe; // pending weapon
+    int atbf, atc3, atc7;
+    char atcb[14]; // hasweapon
+    int atd9[14];
+    int at111[2][14];
+    //int at149[14];
+    int at181[12]; // ammo
+    char at1b1;
+    int at1b2;
+    int at1b6;
+    int at1ba;
+    Aim at1be; // world
+    //int at1c6;
+    Aim at1ca; // relative
+    //int at1ca;
+    //int at1ce;
+    //int at1d2;
+    int at1d6; // aim target sprite
+    int at1da;
+    short at1de[16];
+    int at1fe;
+    int at202[kMaxPowerUps]; // [13]: cloak of invisibility, [14]: death mask (invulnerability), [15]: jump boots, [17]: guns akimbo, [18]: diving suit, [21]: crystal ball, [24]: reflective shots, [25]: beast vision, [26]: cloak of shadow
+    int at2c6; // frags
+    int at2ca[8];
+    int at2ea;
+    int at2ee; // killer
+    int at2f2;
+    int at2f6;
+    int at2fa;
+    int at2fe;
+    int at302;
+    int at306;
+    int at30a;
+    int at30e;
+    int at312;
+    int at316;
+    char at31a; // God mode
+    char at31b; // Fall scream
+    char at31c;
+    int at31d; // pack timer
+    int at321; // pack id 1: diving suit, 2: crystal ball, 3: beast vision 4: jump boots
+    PACKINFO packInfo[5]; // at325 [1]: diving suit, [2]: crystal ball, [3]: beast vision [4]: jump boots
+    int at33e[3]; // armor
+    //int at342;
+    //int at346;
+    int voodooTarget; // at34a
+    int at34e;
+    int at352;
+    int at356;
+    int at35a; // quake
+    int at35e;
+    int at362; // light
+    int at366;
+    int at36a; // blind
+    int at36e; // choke
+    int at372;
+    char at376; // hand
+    int at377;
+    char at37b; // weapon flash
+    int at37f; // quake2
+    fix16_t q16ang;
+    int angold;
+    int player_par;
+    int nWaterPal;
+};
+
+struct POSTURE {
+    int at0;
+    int at4;
+    int at8;
+    int atc[2];
+    int at14;
+    int at18;
+    int at1c;
+    int at20;
+    int at24;
+    int at28;
+    int at2c;
+    int at30;
+};
+
+struct PROFILE {
+    int nAutoAim;
+    int nWeaponSwitch;
+    int skill;
+    char name[MAXPLAYERNAME];
+};
+
+struct AMMOINFO {
+    int at0;
+    signed char at4;
+};
+
+struct POWERUPINFO
+{
+    short at0;
+    char at2;
+    int at3; // max value
+    int at7;
+};
+
+extern POSTURE gPosture[4][3];
+
+extern PLAYER gPlayer[kMaxPlayers];
+extern PLAYER *gMe, *gView;
+
+extern PROFILE gProfile[kMaxPlayers];
+
+extern int dword_21EFB0[kMaxPlayers];
+extern int dword_21EFD0[kMaxPlayers];
+extern AMMOINFO gAmmoInfo[];
+extern POWERUPINFO gPowerUpInfo[kMaxPowerUps];
+
+int powerupCheck(PLAYER *pPlayer, int nPowerUp);
+char powerupActivate(PLAYER *pPlayer, int nPowerUp);
+void powerupDeactivate(PLAYER *pPlayer, int nPowerUp);
+void powerupSetState(PLAYER *pPlayer, int nPowerUp, char bState);
+void powerupProcess(PLAYER *pPlayer);
+void powerupClear(PLAYER *pPlayer);
+void powerupInit(void);
+int packItemToPowerup(int nPack);
+int powerupToPackItem(int nPowerUp);
+char packAddItem(PLAYER *pPlayer, unsigned int nPack);
+int packCheckItem(PLAYER *pPlayer, int nPack);
+char packItemActive(PLAYER *pPlayer, int nPack);
+void packUseItem(PLAYER *pPlayer, int nPack);
+void packPrevItem(PLAYER *pPlayer);
+void packNextItem(PLAYER *pPlayer);
+char playerSeqPlaying(PLAYER * pPlayer, int nSeq);
+void playerSetRace(PLAYER *pPlayer, int nLifeMode);
+void playerSetGodMode(PLAYER *pPlayer, char bGodMode);
+void playerResetInertia(PLAYER *pPlayer);
+void playerStart(int nPlayer);
+void playerReset(PLAYER *pPlayer);
+void playerInit(int nPlayer, unsigned int a2);
+char sub_3A158(PLAYER *a1, spritetype *a2);
+char PickupItem(PLAYER *pPlayer, spritetype *pItem);
+char PickupAmmo(PLAYER *pPlayer, spritetype *pAmmo);
+char PickupWeapon(PLAYER *pPlayer, spritetype *pWeapon);
+void PickUp(PLAYER *pPlayer, spritetype *pSprite);
+void CheckPickUp(PLAYER *pPlayer);
+int ActionScan(PLAYER *pPlayer, int *a2, int *a3);
+void ProcessInput(PLAYER *pPlayer);
+void playerProcess(PLAYER *pPlayer);
+spritetype *playerFireMissile(PLAYER *pPlayer, int a2, int a3, int a4, int a5, int a6);
+spritetype *playerFireThing(PLAYER *pPlayer, int a2, int a3, int thingType, int a5);
+void playerFrag(PLAYER *pKiller, PLAYER *pVictim);
+void FragPlayer(PLAYER *pPlayer, int nSprite);
+int playerDamageArmor(PLAYER *pPlayer, DAMAGE_TYPE nType, int nDamage);
+spritetype *sub_40A94(PLAYER *pPlayer, int a2);
+int playerDamageSprite(int nSource, PLAYER *pPlayer, DAMAGE_TYPE nDamageType, int nDamage);
+int UseAmmo(PLAYER *pPlayer, int nAmmoType, int nDec);
+void sub_41250(PLAYER *pPlayer);
+void playerLandingSound(PLAYER *pPlayer);
+void PlayerSurvive(int, int nXSprite);
+void PlayerKeelsOver(int, int nXSprite);
+bool isGrown(spritetype* pSprite);
+bool isShrinked(spritetype* pSprite);
+bool shrinkPlayerSize(PLAYER* pPlayer, int divider);
+bool growPlayerSize(PLAYER* pPlayer, int multiplier);
+bool resetPlayerSize(PLAYER* pPlayer);
+void deactivateSizeShrooms(PLAYER* pPlayer);
diff --git a/source/blood/src/pqueue.cpp b/source/blood/src/pqueue.cpp
new file mode 100644
index 000000000..f411f1215
--- /dev/null
+++ b/source/blood/src/pqueue.cpp
@@ -0,0 +1,147 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "common_game.h"
+#include "pqueue.h"
+
+PriorityQueue::~PriorityQueue()
+{
+}
+
+VanillaPriorityQueue::~VanillaPriorityQueue()
+{
+}
+
+void VanillaPriorityQueue::Clear(void)
+{
+    fNodeCount = 0;
+    memset(queueItems, 0, sizeof(queueItems));
+}
+
+void VanillaPriorityQueue::Upheap(void)
+{
+    queueItem item = queueItems[fNodeCount];
+    queueItems[0].at0 = 0;
+    unsigned int x = fNodeCount;
+    while (queueItems[x>>1] > item)
+    {
+        queueItems[x] = queueItems[x>>1];
+        x >>= 1;
+    }
+    queueItems[x] = item;
+}
+
+void VanillaPriorityQueue::Downheap(unsigned int n)
+{
+    queueItem item = queueItems[n];
+    while (fNodeCount/2 >= n)
+    {
+        unsigned int t = n*2;
+        if (t < fNodeCount && queueItems[t] > queueItems[t+1])
+            t++;
+        if (item <= queueItems[t])
+            break;
+        queueItems[n] = queueItems[t];
+        n = t;
+    }
+    queueItems[n] = item;
+}
+
+void VanillaPriorityQueue::Delete(unsigned int k)
+{
+    dassert(k <= fNodeCount);
+    queueItems[k] = queueItems[fNodeCount--];
+    Downheap(k);
+}
+
+void VanillaPriorityQueue::Insert(unsigned int a1, unsigned int a2)
+{
+    dassert(fNodeCount < kPQueueSize);
+    fNodeCount++;
+    queueItems[fNodeCount].at0 = a1;
+    queueItems[fNodeCount].at4 = a2;
+    Upheap();
+}
+
+unsigned int VanillaPriorityQueue::Remove(void)
+{
+    unsigned int data = queueItems[1].at4;
+    queueItems[1] = queueItems[fNodeCount--];
+    Downheap(1);
+    return data;
+}
+
+unsigned int VanillaPriorityQueue::LowestPriority(void)
+{
+    dassert(fNodeCount > 0);
+    return queueItems[1].at0;
+}
+
+void VanillaPriorityQueue::Kill(std::function<bool(unsigned int)> pMatch)
+{
+    for (unsigned int i = 1; i <= fNodeCount;)
+    {
+        if (pMatch(queueItems[i].at4))
+            Delete(i);
+        else
+            i++;
+    }
+}
+
+StdPriorityQueue::~StdPriorityQueue()
+{
+    stdQueue.clear();
+}
+
+void StdPriorityQueue::Clear(void)
+{
+    stdQueue.clear();
+}
+
+void StdPriorityQueue::Insert(unsigned int nPriority, unsigned int nData)
+{
+    stdQueue.insert({ nPriority, nData });
+}
+
+unsigned int StdPriorityQueue::Remove(void)
+{
+    dassert(stdQueue.size() > 0);
+    int nData = stdQueue.begin()->at4;
+    stdQueue.erase(stdQueue.begin());
+    return nData;
+}
+
+unsigned int StdPriorityQueue::LowestPriority(void)
+{
+    return stdQueue.begin()->at0;
+}
+
+void StdPriorityQueue::Kill(std::function<bool(unsigned int)> pMatch)
+{
+    for (auto i = stdQueue.begin(); i != stdQueue.end();)
+    {
+        if (pMatch(i->at4))
+            i = stdQueue.erase(i);
+        else
+            i++;
+    }
+}
diff --git a/source/blood/src/pqueue.h b/source/blood/src/pqueue.h
new file mode 100644
index 000000000..6979e575f
--- /dev/null
+++ b/source/blood/src/pqueue.h
@@ -0,0 +1,98 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include <set>
+#include <functional>
+#define kPQueueSize 1024
+
+struct queueItem
+{
+    unsigned int at0; // priority
+    unsigned int at4; // data
+    bool operator>(const queueItem& other) const
+    {
+        return at0 > other.at0;
+    }
+    bool operator<(const queueItem& other) const
+    {
+        return at0 < other.at0;
+    }
+    bool operator>=(const queueItem& other) const
+    {
+        return at0 >= other.at0;
+    }
+    bool operator<=(const queueItem& other) const
+    {
+        return at0 <= other.at0;
+    }
+    bool operator!=(const queueItem& other) const
+    {
+        return at0 != other.at0;
+    }
+    bool operator==(const queueItem& other) const
+    {
+        return at0 == other.at0;
+    }
+};
+
+class PriorityQueue
+{
+public:
+    virtual ~PriorityQueue() = 0;
+    virtual unsigned int Size(void) = 0;
+    virtual void Clear(void) = 0;
+    virtual void Insert(unsigned int, unsigned int) = 0;
+    virtual unsigned int Remove(void) = 0;
+    virtual unsigned int LowestPriority(void) = 0;
+    virtual void Kill(std::function<bool(unsigned int)> pMatch) = 0;
+};
+
+class VanillaPriorityQueue : public PriorityQueue
+{
+public:
+    queueItem queueItems[kPQueueSize + 1];
+    unsigned int fNodeCount; // at2008
+    ~VanillaPriorityQueue();
+    unsigned int Size(void) { return fNodeCount; };
+    void Clear(void);
+    void Upheap(void);
+    void Downheap(unsigned int);
+    void Delete(unsigned int);
+    void Insert(unsigned int, unsigned int);
+    unsigned int Remove(void);
+    unsigned int LowestPriority(void);
+    void Kill(std::function<bool(unsigned int)> pMatch);
+};
+
+class StdPriorityQueue : public PriorityQueue
+{
+public:
+    std::multiset<queueItem> stdQueue;
+    ~StdPriorityQueue();
+    unsigned int Size(void) { return stdQueue.size(); };
+    void Clear(void);
+    void Insert(unsigned int, unsigned int);
+    unsigned int Remove(void);
+    unsigned int LowestPriority(void);
+    void Kill(std::function<bool(unsigned int)> pMatch);
+};
diff --git a/source/blood/src/qav.cpp b/source/blood/src/qav.cpp
new file mode 100644
index 000000000..b7a533f34
--- /dev/null
+++ b/source/blood/src/qav.cpp
@@ -0,0 +1,137 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#include "common_game.h"
+#include "qav.h"
+#include "tile.h"
+#include "sfx.h"
+#include "sound.h"
+
+#define kMaxClients 64
+static void (*clientCallback[kMaxClients])(int, void *);
+static int nClients;
+
+
+int qavRegisterClient(void(*pClient)(int, void *))
+{
+    dassert(nClients < kMaxClients);
+    clientCallback[nClients] = pClient;
+
+    return nClients++;
+}
+
+void DrawFrame(int x, int y, TILE_FRAME *pTile, int stat, int shade, int palnum)
+{
+    stat |= pTile->stat;
+    int angle = pTile->angle;
+    if (stat & 0x100)
+    {
+        angle = (angle+1024)&2047;
+        stat &= ~0x100;
+        stat ^= 0x4;
+    }
+    if (stat & kQavOrientationLeft)
+    {
+        stat &= ~kQavOrientationLeft;
+        stat |= 256;
+    }
+    if (palnum <= 0)
+        palnum = pTile->palnum;
+    rotatesprite((x + pTile->x) << 16, (y + pTile->y) << 16, pTile->z, angle,
+                 pTile->picnum, ClipRange(pTile->shade + shade, -128, 127), palnum, stat,
+                 windowxy1.x, windowxy1.y, windowxy2.x, windowxy2.y);
+}
+
+void QAV::Draw(int ticks, int stat, int shade, int palnum)
+{
+    dassert(ticksPerFrame > 0);
+    int nFrame = ticks / ticksPerFrame;
+    dassert(nFrame >= 0 && nFrame < nFrames);
+    FRAMEINFO *pFrame = &frames[nFrame];
+    for (int i = 0; i < 8; i++)
+    {
+        if (pFrame->tiles[i].picnum > 0)
+            DrawFrame(x, y, &pFrame->tiles[i], stat, shade, palnum);
+    }
+}
+
+void QAV::Play(int start, int end, int nCallback, void *pData)
+{
+    dassert(ticksPerFrame > 0);
+    int frame;
+    int ticks;
+    if (start < 0)
+        frame = (start + 1) / ticksPerFrame;
+    else
+        frame = start / ticksPerFrame + 1;
+    
+    for (ticks = ticksPerFrame * frame; ticks <= end; frame++, ticks += ticksPerFrame)
+    {
+        if (frame >= 0 && frame < nFrames)
+        {
+            FRAMEINFO *pFrame = &frames[frame];
+            SOUNDINFO *pSound = &pFrame->sound;
+            
+            // by NoOne: handle Sound kill flags
+            if (pSound->sndFlags > 0 && pSound->sndFlags <= kFlagSoundKillAll) {
+                for (int i = 0; i < nFrames; i++) {
+                    FRAMEINFO* pFrame2 = &frames[i];
+                    SOUNDINFO* pSound2 = &pFrame2->sound;
+                    if (pSound2->sound != 0) {
+                        if (pSound->sndFlags != kFlagSoundKillAll && pSound2->priority != pSound->priority) continue;
+                        else if (nSprite >= 0) {
+                            // We need stop all sounds in a range
+                            for (int a = 0; a <= pSound2->sndRange; a++)
+                                sfxKill3DSound(&sprite[nSprite], -1, pSound2->sound + a);
+                        } else {
+                            sndKillAllSounds();
+                        }
+                    }
+                }
+            }
+
+            if (pSound->sound > 0) {
+                int sndRange = pSound->sndRange;
+                if (nSprite == -1) PlaySound(pSound->sound + Random((sndRange == 1) ? 2 : sndRange));
+                else PlaySound3D(&sprite[nSprite], pSound->sound + Random((sndRange == 1) ? 2 : sndRange), 16+pSound->priority, 6);
+            }
+            
+            if (pFrame->nCallbackId > 0 && nCallback != -1) {
+                clientCallback[nCallback](pFrame->nCallbackId, pData);
+            }
+        }
+    }
+}
+
+void QAV::Preload(void)
+{
+    for (int i = 0; i < nFrames; i++)
+    {
+        for (int j = 0; j < 8; j++)
+        {
+            if (frames[i].tiles[j].picnum >= 0)
+                tilePreloadTile(frames[i].tiles[j].picnum);
+        }
+    }
+}
\ No newline at end of file
diff --git a/source/blood/src/qav.h b/source/blood/src/qav.h
new file mode 100644
index 000000000..d9afbc6bd
--- /dev/null
+++ b/source/blood/src/qav.h
@@ -0,0 +1,90 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common_game.h"
+#include "blood.h"
+
+#define kQavOrientationLeft 4096
+
+#pragma pack(push, 1)
+
+// by NoOne: add sound flags
+enum
+{
+    kFlagSoundKill = 0x01, // mute QAV sounds of same priority
+    kFlagSoundKillAll = 0x02, //  mute all QAV sounds
+
+};
+
+struct TILE_FRAME
+{
+    int picnum;
+    int x;
+    int y;
+    int z;
+    int stat;
+    signed char shade;
+    char palnum;
+    unsigned short angle;
+};
+
+struct SOUNDINFO
+{
+    int sound;
+    unsigned char priority;
+    unsigned char sndFlags; // (by NoOne) Various sound flags
+    unsigned char sndRange; // (by NoOne) Random sound range
+    char reserved[1];
+};
+
+struct FRAMEINFO
+{
+    int nCallbackId; // 0
+    SOUNDINFO sound; // 4
+    TILE_FRAME tiles[8]; // 12
+};
+
+struct QAV
+{
+    char pad1[8]; // 0
+    int nFrames; // 8
+    int ticksPerFrame; // C
+    int at10; // 10
+    int x; // 14
+    int y; // 18
+    int nSprite; // 1c
+    //SPRITE *pSprite; // 1c
+    char pad3[4]; // 20
+    FRAMEINFO frames[1]; // 24
+    void Draw(int ticks, int stat, int shade, int palnum);
+    void Play(int, int, int, void *);
+    void Preload(void);
+
+    void PlaySound(int nSound);
+    void PlaySound3D(spritetype *pSprite, int nSound, int a3, int a4);
+};
+
+#pragma pack(pop)
+
+int qavRegisterClient(void(*pClient)(int, void *));
diff --git a/source/blood/src/qheap.cpp b/source/blood/src/qheap.cpp
new file mode 100644
index 000000000..86955eef5
--- /dev/null
+++ b/source/blood/src/qheap.cpp
@@ -0,0 +1,217 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
+#include "common_game.h"
+
+#include "qheap.h"
+
+void InstallFenceposts(HEAPNODE *n)
+{
+    char *address = (char*)n;
+    memset(address + 0x20, 0xcc, 0x10);
+    memset(address + n->size - 0x10, 0xcc, 0x10);
+}
+
+void CheckFenceposts(HEAPNODE *n)
+{
+    char *data = (char*)n + 0x20;
+    for (int i = 0; i < 0x10; i++)
+    {
+        if (data[i] != 0xcc)
+        {
+            ThrowError("Block underwritten");
+        }
+    }
+    data = (char*)n + n->size - 0x10;
+    for (int i = 0; i < 0x10; i++)
+    {
+        if (data[i] != 0xcc)
+        {
+            ThrowError("Block overwritten");
+        }
+    }
+}
+
+QHeap::QHeap(int heapSize)
+{
+    dassert(heapSize > 0);
+    size = heapSize;
+    while (size > 0 && (heapPtr = malloc(size)) == NULL)
+    {
+        size -= 0x1000;
+    }
+    if (!heapPtr)
+    {
+        ThrowError("Allocation failure\n");
+    }
+    heap.isFree = false;
+    freeHeap.isFree = false;
+    HEAPNODE *node = (HEAPNODE*)(((intptr_t)heapPtr + 0xf) & ~0xf);
+    heap.next = heap.prev = node;
+    node->next = node->prev = &heap;
+    freeHeap.freeNext = freeHeap.freePrev = node;
+    node->freeNext = node->freePrev = &freeHeap;
+    node->isFree = true;
+    node->size = size & ~0xf;
+}
+
+QHeap::~QHeap(void)
+{
+    Check();
+    free(heapPtr);
+    heapPtr = NULL;
+}
+
+void QHeap::Check(void)
+{
+    HEAPNODE *node = heap.next;
+    while (node != &heap)
+    {
+        if (!node->isFree)
+        {
+            CheckFenceposts(node);
+        }
+        node = node->next;
+    }
+}
+
+void QHeap::Debug(void)
+{
+#if 0
+    char s[4];
+    FILE *f = fopen("MEMFRAG.TXT", "wt");
+    if (!f)
+    {
+        return;
+    }
+    HEAPNODE *node = heap.next;
+    while (node != &heap)
+    {
+        if (node->isFree)
+        {
+            fprintf(f, "%P %10d FREE", node, node->size);
+        }
+        else
+        {
+            char *data = (char*)node + 0x20;
+            for (int i = 0; i < 4; i++)
+            {
+                if (isalpha(data[i]))
+                {
+                    s[i] = data[i];
+                }
+                else
+                {
+                    s[i] = '_';
+                }
+            }
+            fprintf(f, "%P %10d %4s", node, node->size, s);
+        }
+    }
+    fclose(f);
+#endif
+}
+
+void *QHeap::Alloc(int blockSize)
+{
+    dassert(blockSize > 0);
+    dassert(heapPtr != NULL);
+    if (blockSize > 0)
+    {
+        blockSize = ((blockSize + 0xf) & ~0xf) + 0x40;
+        HEAPNODE *freeNode = freeHeap.freeNext;
+        while (freeNode != &freeHeap)
+        {
+            dassert(freeNode->isFree);
+            if (blockSize <= freeNode->size)
+            {
+                if (blockSize + 0x30 <= freeNode->size)
+                {
+                    freeNode->size -= blockSize;
+                    HEAPNODE *nextNode = (HEAPNODE *)((char*)freeNode + freeNode->size);
+                    nextNode->size = blockSize;
+                    nextNode->prev = freeNode;
+                    nextNode->next = freeNode->next;
+                    nextNode->prev->next = nextNode;
+                    nextNode->next->prev = nextNode;
+                    nextNode->isFree = false;
+                    InstallFenceposts(nextNode);
+                    return (void*)((char*)nextNode + 0x30);
+                }
+                else
+                {
+                    freeNode->freePrev->freeNext = freeNode->freeNext;
+                    freeNode->freeNext->freePrev = freeNode->freePrev;
+                    freeNode->isFree = false;
+                    InstallFenceposts(freeNode);
+                    return (void*)((char*)freeNode + 0x30);
+                }
+            }
+            freeNode = freeNode->freeNext;
+        }
+    }
+    return NULL;
+}
+
+int QHeap::Free(void *p)
+{
+    if (!p)
+    {
+        return 0;
+    }
+    dassert(heapPtr != NULL);
+    HEAPNODE *node = (HEAPNODE*)((char*)p - 0x30);
+    if (node->isFree)
+    {
+        ThrowError("Free on bad or freed block");
+    }
+    CheckFenceposts(node);
+    if (node->prev->isFree)
+    {
+        node->prev->size += node->size;
+        node->prev->next = node->next;
+        node->next->prev = node->prev;
+        node = node->prev;
+    }
+    else
+    {
+        node->freeNext = freeHeap.freeNext;
+        node->freePrev = &freeHeap;
+        node->freePrev->freeNext = node;
+        node->freeNext->freePrev = node;
+        node->isFree = true;
+    }
+    HEAPNODE *nextNode = node->next;
+    if (nextNode->isFree)
+    {
+        node->size += nextNode->size;
+        nextNode->freePrev->freeNext = nextNode->freeNext;
+        nextNode->freeNext->freePrev = nextNode->freePrev;
+        nextNode->prev->next = nextNode->next;
+        nextNode->next->prev = nextNode->prev;
+    }
+    return node->size - 0x40;
+}
diff --git a/source/blood/src/qheap.h b/source/blood/src/qheap.h
new file mode 100644
index 000000000..6d036f764
--- /dev/null
+++ b/source/blood/src/qheap.h
@@ -0,0 +1,50 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+struct HEAPNODE
+{
+    HEAPNODE *prev;
+    HEAPNODE *next;
+    int size;
+    bool isFree;
+    HEAPNODE *freePrev;
+    HEAPNODE *freeNext;
+};
+
+class QHeap
+{
+public:
+    QHeap(int heapSize);
+    ~QHeap(void);
+
+    void Check(void);
+    void Debug(void);
+    void *Alloc(int);
+    int Free(void *p);
+
+    void *heapPtr;
+    HEAPNODE heap;
+    HEAPNODE freeHeap;
+    int size;
+};
\ No newline at end of file
diff --git a/source/blood/src/replace.cpp b/source/blood/src/replace.cpp
new file mode 100644
index 000000000..a8c6e9171
--- /dev/null
+++ b/source/blood/src/replace.cpp
@@ -0,0 +1,73 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "common_game.h"
+#include "crc32.h"
+
+#include "globals.h"
+#include "screen.h"
+
+int qanimateoffs(int a1, int a2)
+{
+    int offset = 0;
+    if (a1 >= 0 && a1 < kMaxTiles)
+    {
+        int frames = picanm[a1].num;
+        if (frames > 0)
+        {
+            int vd;
+            if ((a2&0xc000) == 0x8000)
+                vd = (Bcrc32(&a2, 2, 0)+gFrameClock)>>(picanm[a1].sf&PICANM_ANIMSPEED_MASK);
+            else
+                vd = gFrameClock>>(picanm[a1].sf&PICANM_ANIMSPEED_MASK);
+            switch (picanm[a1].sf&PICANM_ANIMTYPE_MASK)
+            {
+            case PICANM_ANIMTYPE_OSC:
+                offset = vd % (2*frames);
+                if (offset >= frames)
+                    offset = 2*frames-offset;
+                break;
+            case PICANM_ANIMTYPE_FWD:
+                offset = vd % (frames+1);
+                break;
+            case PICANM_ANIMTYPE_BACK:
+                offset = -(vd % (frames+1));
+                break;
+            }
+        }
+    }
+    return offset;
+}
+
+void qloadpalette()
+{
+    scrLoadPalette();
+}
+
+int32_t qgetpalookup(int32_t a1, int32_t a2)
+{
+    if (gFogMode)
+        return ClipHigh(a1 >> 8, 15) * 16 + ClipRange(a2, 0, 15);
+    else
+        return ClipRange((a1 >> 8) + a2, 0, 63);
+}
\ No newline at end of file
diff --git a/source/blood/src/replace.h b/source/blood/src/replace.h
new file mode 100644
index 000000000..622823eaf
--- /dev/null
+++ b/source/blood/src/replace.h
@@ -0,0 +1,28 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "compat.h"
+
+int qanimateoffs(int a1, int a2);
+void qloadpalette();
+int32_t qgetpalookup(int32_t a1, int32_t a2);
\ No newline at end of file
diff --git a/source/blood/src/resource.cpp b/source/blood/src/resource.cpp
new file mode 100644
index 000000000..d498a7722
--- /dev/null
+++ b/source/blood/src/resource.cpp
@@ -0,0 +1,740 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include "crc32.h"
+#include "compat.h"
+#include "cache1d.h"
+#ifdef WITHKPLIB
+#include "kplib.h"
+#endif
+#include "common_game.h"
+
+#include "misc.h"
+#include "qheap.h"
+#include "resource.h"
+
+#if B_BIG_ENDIAN == 1
+#include "qav.h"
+#include "seq.h"
+#include "sound.h"
+#endif
+
+CACHENODE Resource::purgeHead = { NULL, &purgeHead, &purgeHead, 0 };
+
+QHeap *Resource::heap;
+
+Resource::Resource(void)
+{
+    dict = NULL;
+    indexName = NULL;
+    indexId = NULL;
+    buffSize = 0;
+    count = 0;
+    handle = -1;
+    crypt = true;
+}
+
+Resource::~Resource(void)
+{
+    if (dict)
+    {
+        for (unsigned int i = 0; i < count; i++)
+        {
+            if (dict[i].type)
+                Free(dict[i].type);
+            if (dict[i].name)
+                Free(dict[i].name);
+        }
+        Free(dict);
+        dict = NULL;
+        buffSize = 0;
+        count = 0;
+    }
+    if (handle != -1)
+    {
+        kclose(handle);
+    }
+}
+
+void Resource::Init(const char *filename)
+{
+    RFFHeader header;
+    dassert(heap != NULL);
+
+    if (filename)
+    {
+        handle = kopen4loadfrommod(filename, 0);
+        if (handle != -1)
+        {
+            int nFileLength = kfilelength(handle);
+            dassert(nFileLength != -1);
+            if (kread(handle, &header, sizeof(RFFHeader)) != sizeof(RFFHeader)
+                || memcmp(header.sign, "RFF\x1a", 4))
+            {
+                ThrowError("RFF header corrupted");
+            }
+#if B_BIG_ENDIAN == 1
+            header.version = B_LITTLE16(header.version);
+            header.offset = B_LITTLE32(header.offset);
+            header.filenum = B_LITTLE32(header.filenum);
+#endif
+            switch (header.version & 0xff00)
+            {
+            case 0x200:
+                crypt = 0;
+                break;
+            case 0x300:
+                crypt = 1;
+                break;
+            default:
+                ThrowError("Unknown RFF version");
+                break;
+            }
+            count = header.filenum;
+            if (count)
+            {
+                buffSize = 1;
+                while (count * 2 >= buffSize)
+                {
+                    buffSize *= 2;
+                }
+                dict = (DICTNODE*)Alloc(buffSize * sizeof(DICTNODE));
+                memset(dict, 0, buffSize * sizeof(DICTNODE));
+                DICTNODE_FILE *tdict = (DICTNODE_FILE*)Alloc(count*sizeof(DICTNODE_FILE));
+                int r = klseek(handle, header.offset, SEEK_SET);
+                dassert(r != -1);
+                if ((uint32_t)kread(handle, tdict, count * sizeof(DICTNODE_FILE)) != count*sizeof(DICTNODE_FILE))
+                {
+                    ThrowError("RFF dictionary corrupted");
+                }
+                if (crypt)
+                {
+                    Crypt(tdict, count * sizeof(DICTNODE_FILE),
+                        header.offset + (header.version & 0xff) * header.offset);
+                }
+                for (unsigned int i = 0; i < count; i++)
+                {
+                    dict[i].offset = B_LITTLE32(tdict[i].offset);
+                    dict[i].size = B_LITTLE32(tdict[i].size);
+                    dict[i].flags = tdict[i].flags;
+                    int nTypeLength = strnlen(tdict[i].type, 3);
+                    int nNameLength = strnlen(tdict[i].name, 8);
+                    dict[i].type = (char*)Alloc(nTypeLength+1);
+                    dict[i].name = (char*)Alloc(nNameLength+1);
+                    strncpy(dict[i].type, tdict[i].type, 3);
+                    strncpy(dict[i].name, tdict[i].name, 8);
+                    dict[i].type[nTypeLength] = 0;
+                    dict[i].name[nNameLength] = 0;
+                    dict[i].id = B_LITTLE32(tdict[i].id);
+                }
+                Free(tdict);
+            }
+        }
+    }
+    if (!dict)
+    {
+        buffSize = 16;
+        dict = (DICTNODE*)Alloc(buffSize * sizeof(DICTNODE));
+        memset(dict, 0, buffSize * sizeof(DICTNODE));
+    }
+    Reindex();
+#if 0
+    if (external)
+    {
+        char fname[BMAX_PATH];
+        char type[BMAX_PATH];
+        BDIR *dirr;
+        struct Bdirent *dirent;
+        dirr = Bopendir("./");
+        if (dirr)
+        {
+            while (dirent = Breaddir(dirr))
+            {
+                if (!Bwildmatch(dirent->name, external))
+                    continue;
+                _splitpath(dirent->name, NULL, NULL, fname, type);
+                if (type[0] == '.')
+                {
+                    AddExternalResource(fname, &type[1], dirent->size);
+                }
+                else
+                {
+                    AddExternalResource(fname, "", dirent->size);
+                }
+            }
+            Bclosedir(dirr);
+        }
+#if 0
+        _splitpath2(external, out, &dir, &node, NULL, NULL);
+        _makepath(ext, dir, node, NULL, NULL);
+        int status = _dos_findfirst(external, 0, &info);
+        while (!status)
+        {
+            _splitpath2(info.name, out, NULL, NULL, &fname, &type);
+            if (*type == '.')
+            {
+                AddExternalResource(*fname, (char*)(type + 1), info.size);
+            }
+            else
+            {
+                AddExternalResource(*fname, "", info.size);
+            }
+            status = _dos_findnext(&info);
+        }
+        _dos_findclose(&info);
+#endif
+    }
+#endif
+    for (unsigned int i = 0; i < count; i++)
+    {
+        if (dict[i].flags & DICT_LOCK)
+        {
+            Lock(&dict[i]);
+        }
+    }
+    for (unsigned int i = 0; i < count; i++)
+    {
+        if (dict[i].flags & DICT_LOAD)
+        {
+            Load(&dict[i]);
+        }
+    }
+}
+
+void Resource::Flush(CACHENODE *h)
+{
+    if (h->ptr)
+    {
+        heap->Free(h->ptr);
+        h->ptr = NULL;
+        if (h->lockCount == 0)
+        {
+            RemoveMRU((CACHENODE*)h);
+            return;
+        }
+        h->lockCount = 0;
+    }
+}
+
+void Resource::Purge(void)
+{
+    for (unsigned int i = 0; i < count; i++)
+    {
+        if (dict[i].ptr)
+        {
+            Flush((CACHENODE *)&dict[i]);
+        }
+    }
+}
+
+DICTNODE **Resource::Probe(const char *fname, const char *type)
+{
+    char name[BMAX_PATH];
+    dassert(indexName != NULL);
+    memset(name, 0, sizeof(name));
+    strcpy(name, type);
+    strcat(name, fname);
+    dassert(dict != NULL);
+    unsigned int hash = Bcrc32(name, strlen(name), 0) & (buffSize - 1);
+    unsigned int i = hash;
+    do
+    {
+        if (!indexName[i])
+        {
+            return &indexName[i];
+        }
+        if (!strcmp((*indexName[i]).type, type)
+            && !strcmp((*indexName[i]).name, fname))
+        {
+            return &indexName[i];
+        }
+        if (++i == buffSize)
+        {
+            i = 0;
+        }
+    } while (i != hash);
+    ThrowError("Linear probe failed to find match or unused node!");
+    return NULL;
+}
+
+DICTNODE **Resource::Probe(unsigned int id, const char *type)
+{
+    struct {
+        int id;
+        char type[BMAX_PATH];
+    } name;
+    dassert(indexName != NULL);
+    memset(&name, 0, sizeof(name));
+    strcpy(name.type, type);
+    name.id = id;
+    dassert(dict != NULL);
+    unsigned int hash = Bcrc32(&name, strlen(name.type)+sizeof(name.id), 0) & (buffSize - 1);
+    unsigned int i = hash;
+    do
+    {
+        if (!indexId[i])
+        {
+            return &indexId[i];
+        }
+        if (!strcmp((*indexId[i]).type, type)
+            && (*indexId[i]).id == id)
+        {
+            return &indexId[i];
+        }
+        if (++i == buffSize)
+        {
+            i = 0;
+        }
+    } while (i != hash);
+    ThrowError("Linear probe failed to find match or unused node!");
+    return NULL;
+}
+
+void Resource::Reindex(void)
+{
+    if (indexName)
+    {
+        Free(indexName);
+    }
+    indexName = (DICTNODE **)Alloc(buffSize * sizeof(DICTNODE*));
+    memset(indexName, 0, buffSize * sizeof(DICTNODE*));
+    for (unsigned int i = 0; i < count; i++)
+    {
+        DICTNODE **node = Probe(dict[i].name, dict[i].type);
+        *node = &dict[i];
+    }
+
+    if (indexId)
+    {
+        Free(indexId);
+    }
+    indexId = (DICTNODE **)Alloc(buffSize * sizeof(DICTNODE*));
+    memset(indexId, 0, buffSize * sizeof(DICTNODE*));
+    for (unsigned int i = 0; i < count; i++)
+    {
+        if (dict[i].flags & DICT_ID)
+        {
+            DICTNODE **node = Probe(dict[i].id, dict[i].type);
+            *node = &dict[i];
+        }
+    }
+}
+
+void Resource::Grow(void)
+{
+    buffSize *= 2;
+    void *p = Alloc(buffSize * sizeof(DICTNODE));
+    memset(p, 0, buffSize * sizeof(DICTNODE));
+    memcpy(p, dict, count * sizeof(DICTNODE));
+    Free(dict);
+    dict = (DICTNODE*)p;
+    Reindex();
+}
+
+void Resource::AddExternalResource(const char *name, const char *type, int id)
+{
+    char name2[BMAX_PATH], type2[BMAX_PATH], filename[BMAX_PATH*2];
+    //if (strlen(name) > 8 || strlen(type) > 3) return;
+    sprintf(filename, "%s.%s", name, type);
+    int fhandle = kopen4loadfrommod(filename, 0);
+    if (fhandle == -1)
+        return;
+    int size = kfilelength(fhandle);
+    kclose(fhandle);
+    strcpy(name2, name);
+    strcpy(type2, type);
+    Bstrupr(name2);
+    Bstrupr(type2);
+    dassert(dict != NULL);
+    DICTNODE **index = Probe(name2, type2);
+    dassert(index != NULL);
+    DICTNODE *node = *index;
+    if (node && (node->flags & DICT_EXTERNAL))
+        return;
+    if (!node)
+    {
+        if (2 * count >= buffSize)
+        {
+            Grow();
+        }
+        node = &dict[count++];
+        index = Probe(name2, type2);
+        *index = node;
+        if (node->type)
+            Free(node->type);
+        if (node->name)
+            Free(node->name);
+        int nTypeLength = strlen(type2);
+        int nNameLength = strlen(name2);
+        node->type = (char*)Alloc(nTypeLength+1);
+        node->name = (char*)Alloc(nNameLength+1);
+        strcpy(node->type, type2);
+        strcpy(node->name, name2);
+    }
+    node->size = size;
+    node->flags |= DICT_EXTERNAL;
+    Flush((CACHENODE*)node);
+    if (id != -1)
+    {
+        index = Probe(id, type2);
+        dassert(index != NULL);
+        DICTNODE *node = *index;
+        if (!node)
+        {
+            if (2 * count >= buffSize)
+            {
+                Grow();
+            }
+            node = &dict[count++];
+            index = Probe(id, type2);
+            *index = node;
+        }
+        if (node->type)
+            Free(node->type);
+        if (node->name)
+            Free(node->name);
+        int nTypeLength = strlen(type2);
+        int nNameLength = strlen(name2);
+        node->type = (char*)Alloc(nTypeLength+1);
+        node->name = (char*)Alloc(nNameLength+1);
+        strcpy(node->type, type2);
+        strcpy(node->name, name2);
+        node->id = id;
+        node->size = size;
+        node->flags |= DICT_EXTERNAL;
+        Flush((CACHENODE*)node);
+    }
+}
+
+void *Resource::Alloc(int nSize)
+{
+    dassert(heap != NULL);
+    dassert(nSize != 0);
+    void *p = heap->Alloc(nSize);
+    if (p)
+    {
+        return p;
+    }
+    for (CACHENODE *node = purgeHead.next; node != &purgeHead; node = node->next)
+    {
+        dassert(node->lockCount == 0);
+        dassert(node->ptr != NULL);
+        int nFree = heap->Free(node->ptr);
+        node->ptr = NULL;
+        RemoveMRU(node);
+        if (nSize <= nFree)
+        {
+            p = Alloc(nSize);
+            dassert(p != NULL);
+            return p;
+        }
+    }
+    ThrowError("Out of memory!");
+    return NULL;
+}
+
+void Resource::Free(void *p)
+{
+    dassert(heap != NULL);
+    dassert(p != NULL);
+    heap->Free(p);
+}
+
+DICTNODE *Resource::Lookup(const char *name, const char *type)
+{
+    char name2[BMAX_PATH], type2[BMAX_PATH];
+    dassert(name != NULL);
+    dassert(type != NULL);
+    //if (strlen(name) > 8 || strlen(type) > 3) return NULL;
+    // Try to load external resource first
+    AddExternalResource(name, type);
+    strcpy(name2, name);
+    strcpy(type2, type);
+    Bstrupr(type2);
+    Bstrupr(name2);
+    return *Probe(name2, type2);
+}
+
+DICTNODE *Resource::Lookup(unsigned int id, const char *type)
+{
+    char type2[BMAX_PATH];
+    dassert(type != NULL);
+    //if (strlen(type) > 3) return NULL;
+    strcpy(type2, type);
+    Bstrupr(type2);
+    return *Probe(id, type2);
+}
+
+void Resource::Read(DICTNODE *n)
+{
+    dassert(n != NULL);
+    Read(n, n->ptr);
+}
+
+void Resource::Read(DICTNODE *n, void *p)
+{
+    char filename[BMAX_PATH];
+    dassert(n != NULL);
+    if (n->flags & DICT_EXTERNAL)
+    {
+        sprintf(filename, "%s.%s", n->name, n->type);
+        int fhandle = kopen4loadfrommod(filename, 0);
+        if (fhandle == -1 || (uint32_t)kread(fhandle, p, n->size) != n->size)
+        {
+            ThrowError("Error reading external resource (%i)", errno);
+        }
+        kclose(fhandle);
+    }
+    else
+    {
+        int r = klseek(handle, n->offset, SEEK_SET);
+        if (r == -1)
+        {
+            ThrowError("Error seeking to resource!");
+        }
+        if ((uint32_t)kread(handle, p, n->size) != n->size)
+        {
+            ThrowError("Error loading resource!");
+        }
+        if (n->flags & DICT_CRYPT)
+        {
+            int size;
+            if (n->size > 0x100)
+            {
+                size = 0x100;
+            }
+            else
+            {
+                size = n->size;
+            }
+            Crypt(n->ptr, size, 0);
+        }
+#if B_BIG_ENDIAN == 1
+        if (!Bstrcmp(n->type, "QAV"))
+        {
+            QAV *qav = (QAV*)p;
+            qav->nFrames = B_LITTLE32(qav->nFrames);
+            qav->ticksPerFrame = B_LITTLE32(qav->ticksPerFrame);
+            qav->at10 = B_LITTLE32(qav->at10);
+            qav->x = B_LITTLE32(qav->x);
+            qav->y = B_LITTLE32(qav->y);
+            qav->nSprite = B_LITTLE32(qav->nSprite);
+            for (int i = 0; i < qav->nFrames; i++)
+            {
+                FRAMEINFO *pFrame = &qav->frames[i];
+                SOUNDINFO *pSound = &pFrame->sound;
+                pFrame->nCallbackId = B_LITTLE32(pFrame->nCallbackId);
+                pSound->sound = B_LITTLE32(pSound->sound);
+                for (int j = 0; j < 8; j++)
+                {
+                    TILE_FRAME *pTile = &pFrame->tiles[j];
+                    pTile->picnum = B_LITTLE32(pTile->picnum);
+                    pTile->x = B_LITTLE32(pTile->x);
+                    pTile->y = B_LITTLE32(pTile->y);
+                    pTile->z = B_LITTLE32(pTile->z);
+                    pTile->stat = B_LITTLE32(pTile->stat);
+                    pTile->angle = B_LITTLE16(pTile->angle);
+                }
+            }
+        }
+        else if (!Bstrcmp(n->type, "SEQ"))
+        {
+            Seq *pSeq = (Seq*)p;
+            pSeq->version = B_LITTLE16(pSeq->version);
+            pSeq->nFrames = B_LITTLE16(pSeq->nFrames);
+            pSeq->at8 = B_LITTLE16(pSeq->at8);
+            pSeq->ata = B_LITTLE16(pSeq->ata);
+            pSeq->atc = B_LITTLE32(pSeq->atc);
+            for (int i = 0; i < pSeq->nFrames; i++)
+            {
+                SEQFRAME *pFrame = &pSeq->frames[i];
+                BitReader bitReader((char *)pFrame, sizeof(SEQFRAME));
+                SEQFRAME swapFrame;
+                swapFrame.tile = bitReader.readUnsigned(12);
+                swapFrame.at1_4 = bitReader.readBit();
+                swapFrame.at1_5 = bitReader.readBit();
+                swapFrame.at1_6 = bitReader.readBit();
+                swapFrame.at1_7 = bitReader.readBit();
+                swapFrame.at2_0 = bitReader.readUnsigned(8);
+                swapFrame.at3_0 = bitReader.readUnsigned(8);
+                swapFrame.at4_0 = bitReader.readSigned(8);
+                swapFrame.at5_0 = bitReader.readUnsigned(5);
+                swapFrame.at5_5 = bitReader.readBit();
+                swapFrame.at5_6 = bitReader.readBit();
+                swapFrame.at5_7 = bitReader.readBit();
+                swapFrame.at6_0 = bitReader.readBit();
+                swapFrame.at6_1 = bitReader.readBit();
+                swapFrame.at6_2 = bitReader.readBit();
+                swapFrame.at6_3 = bitReader.readBit();
+                swapFrame.at6_4 = bitReader.readBit();
+                swapFrame.tile2 = bitReader.readUnsigned(4);
+                swapFrame.pad = bitReader.readUnsigned(7);
+                *pFrame = swapFrame;
+            }
+        }
+        else if (!Bstrcmp(n->type, "SFX"))
+        {
+            SFX *pSFX = (SFX*)p;
+            pSFX->relVol = B_LITTLE32(pSFX->relVol);
+            pSFX->pitch = B_LITTLE32(pSFX->pitch);
+            pSFX->pitchRange = B_LITTLE32(pSFX->pitchRange);
+            pSFX->format = B_LITTLE32(pSFX->format);
+            pSFX->loopStart = B_LITTLE32(pSFX->loopStart);
+        }
+#endif
+    }
+}
+
+void *Resource::Load(DICTNODE *h)
+{
+    dassert(h != NULL);
+    if (h->ptr)
+    {
+        if (!h->lockCount)
+        {
+            RemoveMRU((CACHENODE*)h);
+
+            h->prev = purgeHead.prev;
+            purgeHead.prev->next = (CACHENODE*)h;
+            h->next = &purgeHead;
+            purgeHead.prev = (CACHENODE*)h;
+        }
+    }
+    else
+    {
+        h->ptr = Alloc(h->size);
+        Read(h);
+
+        h->prev = purgeHead.prev;
+        purgeHead.prev->next = (CACHENODE*)h;
+        h->next = &purgeHead;
+        purgeHead.prev = (CACHENODE*)h;
+    }
+    return h->ptr;
+}
+
+void *Resource::Load(DICTNODE *h, void *p)
+{
+    dassert(h != NULL);
+    if (p)
+    {
+        Read(h, p);
+    }
+    return p;
+}
+
+void *Resource::Lock(DICTNODE *h)
+{
+    dassert(h != NULL);
+    if (h->ptr)
+    {
+        if (h->lockCount == 0)
+        {
+            RemoveMRU((CACHENODE*)h);
+        }
+    }
+    else
+    {
+        h->ptr = Alloc(h->size);
+        Read(h);
+    }
+
+    h->lockCount++;
+    return h->ptr;
+}
+
+void Resource::Unlock(DICTNODE *h)
+{
+    dassert(h != NULL);
+    dassert(h->ptr != NULL);
+    if (h->lockCount > 0)
+    {
+        h->lockCount--;
+        if (h->lockCount == 0)
+        {
+            h->prev = purgeHead.prev;
+            purgeHead.prev->next = (CACHENODE*)h;
+            h->next = &purgeHead;
+            purgeHead.prev = (CACHENODE*)h;
+        }
+    }
+}
+
+void Resource::Crypt(void *p, int length, unsigned short key)
+{
+    char *cp = (char*)p;
+    for (int i = 0; i < length; i++, key++)
+    {
+        cp[i] ^= (key >> 1);
+    }
+}
+
+void Resource::RemoveMRU(CACHENODE *h)
+{
+    h->prev->next = h->next;
+    h->next->prev = h->prev;
+}
+
+void Resource::FNAddFiles(fnlist_t * fnlist, const char *pattern)
+{
+    char filename[BMAX_PATH];
+    for (unsigned int i = 0; i < count; i++)
+    {
+        DICTNODE *pNode = &dict[i];
+        if (pNode->flags & DICT_EXTERNAL)
+            continue;
+        sprintf(filename, "%s.%s", pNode->name, pNode->type);
+        if (!Bwildmatch(filename, pattern))
+            continue;
+        switch (klistaddentry(&fnlist->findfiles, filename, CACHE1D_FIND_FILE, CACHE1D_SOURCE_GRP))
+        {
+        case -1:
+            return;
+        case 0:
+            fnlist->numfiles++;
+            break;
+        }
+    }
+}
+
+void Resource::PrecacheSounds(void)
+{
+    for (unsigned int i = 0; i < count; i++)
+    {
+        DICTNODE *pNode = &dict[i];
+        if ((!strcmp(pNode->type, "RAW") || !strcmp(pNode->type, "SFX")) && !pNode->ptr)
+        {
+            Load(pNode);
+            G_HandleAsync();
+        }
+    }
+}
+
+void Resource::RemoveNode(DICTNODE* pNode)
+{
+    *pNode = dict[--count];
+    Reindex();
+}
diff --git a/source/blood/src/resource.h b/source/blood/src/resource.h
new file mode 100644
index 000000000..233191be9
--- /dev/null
+++ b/source/blood/src/resource.h
@@ -0,0 +1,127 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "qheap.h"
+
+#pragma pack(push, 1)
+
+enum DICTFLAGS {
+    DICT_ID = 1,
+    DICT_EXTERNAL = 2,
+    DICT_LOAD = 4,
+    DICT_LOCK = 8,
+    DICT_CRYPT = 16,
+};
+
+struct RFFHeader
+{
+    char sign[4];
+    short version;
+    short pad1;
+    unsigned int offset;
+    unsigned int filenum;
+    int pad2[4];
+};
+
+struct DICTNODE_FILE
+{
+    char unused1[16];
+    unsigned int offset;
+    unsigned int size;
+    char unused2[8];
+    char flags;
+    char type[3];
+    char name[8];
+    int id;
+};
+
+#pragma pack(pop)
+
+struct CACHENODE
+{
+    void *ptr;
+    CACHENODE *prev;
+    CACHENODE *next;
+    int lockCount;
+};
+
+struct DICTNODE
+{
+    void *ptr;
+    CACHENODE *prev;
+    CACHENODE *next;
+    int lockCount;
+    unsigned int offset;
+    unsigned int size;
+    char flags;
+    //char type[3];
+    //char name[8];
+    char *type;
+    char *name;
+    unsigned int id;
+};
+
+class Resource
+{
+public:
+    Resource(void);
+    ~Resource(void);
+
+    void Init(const char *filename);
+    static void Flush(CACHENODE *h);
+    void Purge(void);
+    DICTNODE **Probe(const char *fname, const char *type);
+    DICTNODE **Probe(unsigned int id, const char *type);
+    void Reindex(void);
+    void Grow(void);
+    void AddExternalResource(const char *name, const char *type, int id = -1);
+    static void *Alloc(int nSize);
+    static void Free(void *p);
+    DICTNODE *Lookup(const char *name, const char *type);
+    DICTNODE *Lookup(unsigned int id, const char *type);
+    void Read(DICTNODE *n);
+    void Read(DICTNODE *n, void *p);
+    void *Load(DICTNODE *h);
+    void *Load(DICTNODE *h, void *p);
+    void *Lock(DICTNODE *h);
+    void Unlock(DICTNODE *h);
+    void Crypt(void *p, int length, unsigned short key);
+    static void RemoveMRU(CACHENODE *h);
+    int Size(DICTNODE*h) { return h->size; }
+    void FNAddFiles(fnlist_t *fnlist, const char *pattern);
+    void PrecacheSounds(void);
+    void RemoveNode(DICTNODE* pNode);
+
+    DICTNODE *dict;
+    DICTNODE **indexName;
+    DICTNODE **indexId;
+    unsigned int buffSize;
+    unsigned int count;
+    //FILE *handle;
+    int handle;
+    bool crypt;
+
+    static QHeap *heap;
+    static CACHENODE purgeHead;
+};
diff --git a/source/blood/src/screen.cpp b/source/blood/src/screen.cpp
new file mode 100644
index 000000000..f365bfc39
--- /dev/null
+++ b/source/blood/src/screen.cpp
@@ -0,0 +1,286 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <string.h>
+#include "a.h"
+#include "build.h"
+#include "colmatch.h"
+#include "common_game.h"
+
+#include "blood.h"
+#include "config.h"
+#include "resource.h"
+#include "screen.h"
+
+LOADITEM PLU[15] = {
+    { 0, "NORMAL" },
+    { 1, "SATURATE" },
+    { 2, "BEAST" },
+    { 3, "TOMMY" },
+    { 4, "SPIDER3" },
+    { 5, "GRAY" },
+    { 6, "GRAYISH" },
+    { 7, "SPIDER1" },
+    { 8, "SPIDER2" },
+    { 9, "FLAME" },
+    { 10, "COLD" },
+    { 11, "P1" },
+    { 12, "P2" },
+    { 13, "P3" },
+    { 14, "P4" }
+};
+
+LOADITEM PAL[5] = {
+    { 0, "BLOOD" },
+    { 1, "WATER" },
+    { 2, "BEAST" },
+    { 3, "SEWER" },
+    { 4, "INVULN1" }
+};
+
+
+bool DacInvalid = true;
+static char(*gammaTable)[256];
+RGB curDAC[256];
+RGB baseDAC[256];
+static RGB fromDAC[256];
+static RGB toRGB;
+static RGB *palTable[5];
+static int curPalette;
+static int curGamma;
+int gGammaLevels;
+bool gFogMode = false;
+
+void scrResetPalette(void)
+{
+    paletteSetColorTable(0, (uint8_t*)palTable[0]);
+}
+
+void gSetDacRange(int start, int end, RGB *pPal)
+{
+    UNREFERENCED_PARAMETER(start);
+    UNREFERENCED_PARAMETER(end);
+    if (videoGetRenderMode() == REND_CLASSIC)
+    {
+        memcpy(palette, pPal, sizeof(palette));
+        videoSetPalette(gBrightness>>2, 0, 0);
+    }
+}
+
+void scrLoadPLUs(void)
+{
+    if (gFogMode)
+    {
+        DICTNODE *pFog = gSysRes.Lookup("FOG", "FLU");
+        if (!pFog)
+            ThrowError("FOG.FLU not found");
+        palookup[0] = (char*)gSysRes.Lock(pFog);
+        for (int i = 0; i < 15; i++)
+            palookup[PLU[i].id] = palookup[0];
+        parallaxvisibility = 3072;
+        return;
+    }
+    for (int i = 0; i < 15; i++)
+    {
+        DICTNODE *pPlu = gSysRes.Lookup(PLU[i].name, "PLU");
+        if (!pPlu)
+            ThrowError("%s.PLU not found", PLU[i].name);
+        if (pPlu->size / 256 != 64)
+            ThrowError("Incorrect PLU size");
+        palookup[PLU[i].id] = (char*)gSysRes.Lock(pPlu);
+    }
+#ifdef USE_OPENGL
+    palookupfog[1].r = 255;
+    palookupfog[1].g = 255;
+    palookupfog[1].b = 255;
+#endif
+}
+
+#ifdef USE_OPENGL
+glblend_t const bloodglblend =
+{
+    {
+        { 1.f/3.f, BLENDFACTOR_SRC_ALPHA, BLENDFACTOR_ONE_MINUS_SRC_ALPHA, 0 },
+        { 2.f/3.f, BLENDFACTOR_SRC_ALPHA, BLENDFACTOR_ONE_MINUS_SRC_ALPHA, 0 },
+    },
+};
+#endif
+
+void scrLoadPalette(void)
+{
+    paletteloaded = 0;
+    initprintf("Loading palettes\n");
+    for (int i = 0; i < 5; i++)
+    {
+        DICTNODE *pPal = gSysRes.Lookup(PAL[i].name, "PAL");
+        if (!pPal)
+            ThrowError("%s.PAL not found (RFF files may be wrong version)", PAL[i].name);
+        palTable[PAL[i].id] = (RGB*)gSysRes.Lock(pPal);
+        paletteSetColorTable(PAL[i].id, (uint8_t*)palTable[PAL[i].id]);
+    }
+    memcpy(palette, palTable[0], sizeof(palette));
+    numshades = 64;
+    paletteloaded |= PALETTE_MAIN;
+    scrLoadPLUs();
+    paletteloaded |= PALETTE_SHADE;
+    initprintf("Loading translucency table\n");
+    DICTNODE *pTrans = gSysRes.Lookup("TRANS", "TLU");
+    if (!pTrans)
+        ThrowError("TRANS.TLU not found");
+    blendtable[0] = (char*)gSysRes.Lock(pTrans);
+    paletteloaded |= PALETTE_TRANSLUC;
+
+#ifdef USE_OPENGL
+    for (auto & x : glblend)
+        x = bloodglblend;
+#endif
+
+    initfastcolorlookup_palette(palette);
+    palettePostLoadTables();
+}
+
+void scrSetPalette(int palId)
+{
+    curPalette = palId;
+    scrSetGamma(0/*curGamma*/);
+}
+
+void scrSetGamma(int nGamma)
+{
+    dassert(nGamma < gGammaLevels);
+    curGamma = nGamma;
+    for (int i = 0; i < 256; i++)
+    {
+        baseDAC[i].red = gammaTable[curGamma][palTable[curPalette][i].red];
+        baseDAC[i].green = gammaTable[curGamma][palTable[curPalette][i].green];
+        baseDAC[i].blue = gammaTable[curGamma][palTable[curPalette][i].blue];
+    }
+    DacInvalid = 1;
+}
+
+void scrSetupFade(char red, char green, char blue)
+{
+    memcpy(fromDAC, curDAC, sizeof(fromDAC));
+    toRGB.red = red;
+    toRGB.green = green;
+    toRGB.blue = blue;
+}
+
+void scrSetupUnfade(void)
+{
+    memcpy(fromDAC, baseDAC, sizeof(fromDAC));
+}
+
+void scrFadeAmount(int amount)
+{
+	for (int i = 0; i < 256; i++)
+	{
+		curDAC[i].red = interpolate(fromDAC[i].red, toRGB.red, amount);
+        curDAC[i].green = interpolate(fromDAC[i].green, toRGB.green, amount);
+        curDAC[i].blue = interpolate(fromDAC[i].blue, toRGB.blue, amount);
+	}
+	gSetDacRange(0, 256, curDAC);
+}
+
+void scrSetDac(void)
+{
+	if (DacInvalid)
+		gSetDacRange(0, 256, baseDAC);
+	DacInvalid = 0;
+}
+
+void scrInit(void)
+{
+    initprintf("Initializing engine\n");
+#ifdef USE_OPENGL
+    glrendmode = REND_POLYMOST;
+#endif
+    engineInit();
+    curPalette = 0;
+    curGamma = 0;
+    initprintf("Loading gamma correction table\n");
+    DICTNODE *pGamma = gSysRes.Lookup("gamma", "DAT");
+    if (!pGamma)
+        ThrowError("Gamma table not found");
+    gGammaLevels = pGamma->size / 256;
+    gammaTable = (char(*)[256])gSysRes.Lock(pGamma);
+}
+
+void scrUnInit(void)
+{
+    memset(palookup, 0, sizeof(palookup));
+    memset(blendtable, 0, sizeof(blendtable));
+    engineUnInit();
+}
+
+
+void scrSetGameMode(int vidMode, int XRes, int YRes, int nBits)
+{
+    videoResetMode();
+    //videoSetGameMode(vidMode, XRes, YRes, nBits, 0);
+    if (videoSetGameMode(vidMode, XRes, YRes, nBits, 0) < 0)
+    {
+        initprintf("Failure setting video mode %dx%dx%d %s! Trying next mode...\n", XRes, YRes,
+                    nBits, vidMode ? "fullscreen" : "windowed");
+
+        int resIdx = 0;
+
+        for (int i=0; i < validmodecnt; i++)
+        {
+            if (validmode[i].xdim == XRes && validmode[i].ydim == YRes)
+            {
+                resIdx = i;
+                break;
+            }
+        }
+
+        int const savedIdx = resIdx;
+        int bpp = nBits;
+
+        while (videoSetGameMode(0, validmode[resIdx].xdim, validmode[resIdx].ydim, bpp, 0) < 0)
+        {
+            initprintf("Failure setting video mode %dx%dx%d windowed! Trying next mode...\n",
+                        validmode[resIdx].xdim, validmode[resIdx].ydim, bpp);
+
+            if (++resIdx == validmodecnt)
+            {
+                if (bpp == 8)
+                    ThrowError("Fatal error: unable to set any video mode!");
+
+                resIdx = savedIdx;
+                bpp = 8;
+            }
+        }
+
+        gSetup.xdim = validmode[resIdx].xdim;
+        gSetup.ydim = validmode[resIdx].ydim;
+        gSetup.bpp  = bpp;
+    }
+    videoClearViewableArea(0);
+    scrNextPage();
+    scrSetPalette(curPalette);
+}
+
+void scrNextPage(void)
+{
+    videoNextPage();
+}
diff --git a/source/blood/src/screen.h b/source/blood/src/screen.h
new file mode 100644
index 000000000..84ab93ff3
--- /dev/null
+++ b/source/blood/src/screen.h
@@ -0,0 +1,55 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+
+struct LOADITEM {
+    int id;
+    const char *name;
+};
+
+#pragma pack(push, 1)
+struct RGB {
+    char red, green, blue;
+};
+#pragma pack(pop)
+
+extern bool DacInvalid;
+extern RGB curDAC[256];
+extern RGB baseDAC[256];
+extern int gGammaLevels;
+extern bool gFogMode;
+void scrResetPalette(void);
+void gSetDacRange(int start, int end, RGB *pPal);
+void scrLoadPLUs(void);
+void scrLoadPalette(void);
+void scrSetPalette(int palId);
+void scrSetGamma(int nGamma);
+void scrSetupFade(char red, char green, char blue);
+void scrSetupUnfade(void);
+void scrFadeAmount(int amount);
+void scrSetDac(void);
+void scrInit(void);
+void scrUnInit(void);
+void scrSetGameMode(int vidMode, int XRes, int YRes, int nBits);
+void scrNextPage(void);
\ No newline at end of file
diff --git a/source/blood/src/sdlmusic.cpp b/source/blood/src/sdlmusic.cpp
new file mode 100644
index 000000000..48a42c983
--- /dev/null
+++ b/source/blood/src/sdlmusic.cpp
@@ -0,0 +1,544 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010 EDuke32 developers and contributors
+
+This file is part of EDuke32.
+
+EDuke32 is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+/*
+ * A reimplementation of Jim Dose's FX_MAN routines, using  SDL_mixer 1.2.
+ *   Whee. FX_MAN is also known as the "Apogee Sound System", or "ASS" for
+ *   short. How strangely appropriate that seems.
+ */
+
+// This object is shared by all Build games with MIDI playback!
+
+#define NEED_SDL_MIXER
+
+#include "compat.h"
+
+#include "common_game.h"
+#include "cache1d.h"
+
+#include "sdlayer.h"
+#include "music.h"
+#include "al_midi.h"
+#include "oplmidi.h"
+
+#if !defined _WIN32 && !defined(GEKKO)
+//# define FORK_EXEC_MIDI 1
+#endif
+
+#if defined FORK_EXEC_MIDI  // fork/exec based external midi player
+#include <signal.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+static char **external_midi_argv;
+static pid_t external_midi_pid=-1;
+static int8_t external_midi_restart=0;
+#endif
+
+#ifdef __ANDROID__ //TODO fix
+static char const *external_midi_tempfn = APPBASENAME "-music.mid";
+#else
+static char const *external_midi_tempfn = "/tmp/" APPBASENAME "-music.mid";
+#endif
+
+static int32_t external_midi = 0;
+
+int32_t MUSIC_SoundDevice = MIDIDEVICE_NONE;
+int32_t MUSIC_ErrorCode = MUSIC_Ok;
+
+static OPLMusic::midifuncs MUSIC_MidiFunctions;
+#define MUSIC_SetErrorCode(status) MUSIC_ErrorCode = (status);
+
+static char warningMessage[80];
+static char errorMessage[80];
+
+static int32_t music_initialized = 0;
+static int32_t music_context = 0;
+static int32_t music_loopflag = MUSIC_PlayOnce;
+static Mix_Music *music_musicchunk = NULL;
+
+static void setErrorMessage(const char *msg)
+{
+    Bstrncpyz(errorMessage, msg, sizeof(errorMessage));
+}
+
+// The music functions...
+
+const char *MUSIC_ErrorString(int32_t ErrorNumber)
+{
+    switch (ErrorNumber)
+    {
+    case MUSIC_Warning:
+        return warningMessage;
+
+    case MUSIC_Error:
+        return errorMessage;
+
+    case MUSIC_Ok:
+        return "OK; no error.";
+
+    case MUSIC_MidiError:
+        return "MIDI error.";
+
+    default:
+        return "Unknown error.";
+    } // switch
+
+    return NULL;
+} // MUSIC_ErrorString
+
+int32_t MUSIC_Init(int32_t SoundCard, int32_t Address)
+{
+    MUSIC_SoundDevice = SoundCard;
+    if (SoundCard == MIDIDEVICE_OPL)
+    {
+        return OPLMusic::MUSIC_InitMidi(SoundCard, &MUSIC_MidiFunctions, Address);
+    }
+#ifdef __ANDROID__
+    music_initialized = 1;
+    return MUSIC_Ok;
+#endif
+    // Use an external MIDI player if the user has specified to do so
+    char *command = getenv("EDUKE32_MUSIC_CMD");
+    const SDL_version *linked = Mix_Linked_Version();
+
+    UNREFERENCED_PARAMETER(SoundCard);
+    UNREFERENCED_PARAMETER(Address);
+
+    if (music_initialized)
+    {
+        setErrorMessage("Music system is already initialized.");
+        return MUSIC_Error;
+    } // if
+
+    if (SDL_VERSIONNUM(linked->major,linked->minor,linked->patch) < MIX_REQUIREDVERSION)
+    {
+        // reject running with SDL_Mixer versions older than what is stated in sdl_inc.h
+        initprintf("You need at least v%d.%d.%d of SDL_mixer for music\n",SDL_MIXER_MIN_X,SDL_MIXER_MIN_Y,SDL_MIXER_MIN_Z);
+        return MUSIC_Error;
+    }
+
+    external_midi = (command != NULL && command[0] != 0);
+
+    if (external_midi)
+    {
+#if defined FORK_EXEC_MIDI
+        int32_t ws=1, numargs=0, pagesize=sysconf(_SC_PAGE_SIZE);
+        char *c, *cmd;
+        size_t sz;
+#endif
+
+        initprintf("Setting music command to \"%s\".\n", command);
+
+#if !defined FORK_EXEC_MIDI
+        if (Mix_SetMusicCMD(command)==-1)
+        {
+            perror("Mix_SetMusicCMD");
+            goto fallback;
+        }
+#else
+
+        if (pagesize==-1)
+            goto fallback;
+
+        for (c=command; *c; c++)
+        {
+            if (isspace(*c))
+                ws = 1;
+            else if (ws)
+            {
+                ws = 0;
+                numargs++;
+            }
+        }
+
+        if (numargs==0)
+            goto fallback;
+
+        sz = (numargs+2)*sizeof(char *) + (c-command+1);
+        sz = ((sz+pagesize-1)/pagesize)*pagesize;
+#if defined(__APPLE__) || defined(__ANDROID__)
+        external_midi_argv = Xcalloc(1,sz+pagesize);
+        external_midi_argv = (char **)((intptr_t)external_midi_argv + (pagesize-(((intptr_t)external_midi_argv)&(pagesize-1))));
+#else
+        if (posix_memalign((void **)&external_midi_argv, pagesize, sz))
+            goto fallback;
+#endif
+        cmd = (char *)external_midi_argv + (numargs+2)*sizeof(char *);
+        Bmemcpy(cmd, command, c-command+1);
+
+        ws = 1;
+        numargs = 0;
+        for (c=cmd; *c; c++)
+        {
+            if (isspace(*c))
+            {
+                ws = 1;
+                *c = 0;
+            }
+            else if (ws)
+            {
+                ws = 0;
+                external_midi_argv[numargs++] = c;
+            }
+        }
+        external_midi_argv[numargs] = external_midi_tempfn;
+        external_midi_argv[numargs+1] = NULL;
+
+        if (mprotect(external_midi_argv, sz, PROT_READ)==-1)  // make argv and command string read-only
+        {
+            perror("MUSIC_Init: mprotect");
+            goto fallback;
+        }
+# if 0
+        {
+            int i;
+            initprintf("----Music argv:\n");
+            for (i=0; i<numargs+1; i++)
+                initprintf("  %s\n", external_midi_argv[i]);
+            initprintf("----\n");
+        }
+# endif
+#endif
+        music_initialized = 1;
+        return MUSIC_Ok;
+
+fallback:
+        initprintf("Error setting music command, falling back to timidity.\n");
+    }
+
+    {
+        static const char *s[] = { "/etc/timidity.cfg", "/etc/timidity/timidity.cfg", "/etc/timidity/freepats.cfg" };
+        FILE *fp;
+        int32_t i;
+
+        for (i = ARRAY_SIZE(s)-1; i>=0; i--)
+        {
+            fp = Bfopen(s[i], "r");
+            if (fp == NULL)
+            {
+                if (i == 0)
+                {
+                    initprintf("Error: couldn't open any of the following files:\n");
+                    for (i = ARRAY_SIZE(s)-1; i>=0; i--)
+                        initprintf("%s\n",s[i]);
+                    return MUSIC_Error;
+                }
+                continue;
+            }
+            else break;
+        }
+        Bfclose(fp);
+    }
+
+    music_initialized = 1;
+    return MUSIC_Ok;
+} // MUSIC_Init
+
+
+int32_t MUSIC_Shutdown(void)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::MIDI_StopSong();
+
+        return MUSIC_Ok;  
+    }
+    // TODO - make sure this is being called from the menu -- SA
+#if !defined FORK_EXEC_MIDI
+    if (external_midi)
+        Mix_SetMusicCMD(NULL);
+#endif
+
+    MUSIC_StopSong();
+    music_context = 0;
+    music_initialized = 0;
+    music_loopflag = MUSIC_PlayOnce;
+
+    return MUSIC_Ok;
+} // MUSIC_Shutdown
+
+
+void MUSIC_SetMaxFMMidiChannel(int32_t channel)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::AL_SetMaxMidiChannel(channel);
+    }
+    // UNREFERENCED_PARAMETER(channel);
+} // MUSIC_SetMaxFMMidiChannel
+
+
+void MUSIC_SetVolume(int32_t volume)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::MIDI_SetVolume(min(max(0, volume), 255));
+        return;
+    }
+    volume = max(0, volume);
+    volume = min(volume, 255);
+
+    Mix_VolumeMusic(volume >> 1);  // convert 0-255 to 0-128.
+} // MUSIC_SetVolume
+
+
+int32_t MUSIC_GetVolume(void)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        return OPLMusic::MIDI_GetVolume();
+    }
+    return (Mix_VolumeMusic(-1) << 1);  // convert 0-128 to 0-255.
+} // MUSIC_GetVolume
+
+
+void MUSIC_SetLoopFlag(int32_t loopflag)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::MIDI_SetLoopFlag(loopflag);
+        return;
+    }
+    music_loopflag = loopflag;
+} // MUSIC_SetLoopFlag
+
+
+void MUSIC_Continue(void)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::MIDI_ContinueSong();
+        return;
+    }
+    if (Mix_PausedMusic())
+        Mix_ResumeMusic();
+} // MUSIC_Continue
+
+
+void MUSIC_Pause(void)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::MIDI_PauseSong();
+        return;
+    }
+    Mix_PauseMusic();
+} // MUSIC_Pause
+
+int32_t MUSIC_StopSong(void)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        OPLMusic::MIDI_StopSong();
+        MUSIC_SetErrorCode(MUSIC_Ok);
+        return MUSIC_Ok;
+    }
+#if defined FORK_EXEC_MIDI
+    if (external_midi)
+    {
+        if (external_midi_pid > 0)
+        {
+            int32_t ret;
+            struct timespec ts;
+
+            external_midi_restart = 0;  // make SIGCHLD handler a no-op
+
+            ts.tv_sec = 0;
+            ts.tv_nsec = 5000000;  // sleep 5ms at most
+
+            kill(external_midi_pid, SIGTERM);
+            nanosleep(&ts, NULL);
+            ret = waitpid(external_midi_pid, NULL, WNOHANG|WUNTRACED);
+//            printf("(%d)", ret);
+
+            if (ret != external_midi_pid)
+            {
+                if (ret==-1)
+                    perror("waitpid");
+                else
+                {
+                    // we tried to be nice, but no...
+                    kill(external_midi_pid, SIGKILL);
+                    initprintf("%s: wait for SIGTERM timed out.\n", __func__);
+                    if (waitpid(external_midi_pid, NULL, WUNTRACED)==-1)
+                        perror("waitpid (2)");
+                }
+            }
+
+            external_midi_pid = -1;
+        }
+
+        return MUSIC_Ok;
+    }
+#endif
+
+    //if (!fx_initialized)
+    if (!Mix_QuerySpec(NULL, NULL, NULL))
+    {
+        setErrorMessage("Need FX system initialized, too. Sorry.");
+        return MUSIC_Error;
+    } // if
+
+    if ((Mix_PlayingMusic()) || (Mix_PausedMusic()))
+        Mix_HaltMusic();
+
+    if (music_musicchunk)
+        Mix_FreeMusic(music_musicchunk);
+
+    music_musicchunk = NULL;
+
+    return MUSIC_Ok;
+} // MUSIC_StopSong
+
+#if defined FORK_EXEC_MIDI
+static int32_t playmusic()
+{
+    pid_t pid = vfork();
+
+    if (pid==-1)  // error
+    {
+        initprintf("%s: vfork: %s\n", __func__, strerror(errno));
+        return MUSIC_Error;
+    }
+    else if (pid==0)  // child
+    {
+        // exec without PATH lookup
+        if (execv(external_midi_argv[0], external_midi_argv) < 0)
+        {
+            perror("execv");
+            _exit(1);
+        }
+    }
+    else  // parent
+    {
+        external_midi_pid = pid;
+    }
+
+    return MUSIC_Ok;
+}
+
+static void sigchld_handler(int signo)
+{
+    if (signo==SIGCHLD && external_midi_restart)
+    {
+        int status;
+
+        if (external_midi_pid > 0)
+        {
+            if (waitpid(external_midi_pid, &status, WUNTRACED)==-1)
+                perror("waitpid (3)");
+
+            if (WIFEXITED(status) && WEXITSTATUS(status)==0)
+            {
+                // loop ...
+                playmusic();
+            }
+        }
+    }
+}
+#endif
+
+// Duke3D-specific.  --ryan.
+// void MUSIC_PlayMusic(char *_filename)
+int32_t MUSIC_PlaySong(char *song, int32_t songsize, int32_t loopflag)
+{
+    if (MUSIC_SoundDevice == MIDIDEVICE_OPL)
+    {
+        MUSIC_SetErrorCode(MUSIC_Ok)
+
+        if (OPLMusic::MIDI_PlaySong(song, loopflag) != OPLMusic::MIDI_Ok)
+        {
+            MUSIC_SetErrorCode(MUSIC_MidiError);
+            return MUSIC_Warning;
+        }
+
+        return MUSIC_Ok;
+    }
+    if (external_midi)
+    {
+        FILE *fp;
+
+#if defined FORK_EXEC_MIDI
+        static int32_t sigchld_handler_set = 0;
+
+        if (!sigchld_handler_set)
+        {
+            struct sigaction sa;
+            sa.sa_handler=sigchld_handler;
+            sa.sa_flags=0;
+            sigemptyset(&sa.sa_mask);
+
+            if (sigaction(SIGCHLD, &sa, NULL)==-1)
+                initprintf("%s: sigaction: %s\n", __func__, strerror(errno));
+
+            sigchld_handler_set = 1;
+        }
+#endif
+
+        fp = Bfopen(external_midi_tempfn, "wb");
+        if (fp)
+        {
+            fwrite(song, 1, songsize, fp);
+            Bfclose(fp);
+
+#if defined FORK_EXEC_MIDI
+            external_midi_restart = loopflag;
+            int32_t retval = playmusic();
+            if (retval != MUSIC_Ok)
+                return retval;
+#else
+            music_musicchunk = Mix_LoadMUS(external_midi_tempfn);
+            if (!music_musicchunk)
+            {
+                initprintf("Mix_LoadMUS: %s\n", Mix_GetError());
+                return MUSIC_Error;
+            }
+#endif
+        }
+        else
+        {
+            initprintf("%s: fopen: %s\n", __func__, strerror(errno));
+            return MUSIC_Error;
+        }
+    }
+    else
+        music_musicchunk = Mix_LoadMUS_RW(SDL_RWFromMem(song, songsize)
+#if (SDL_MAJOR_VERSION > 1)
+            , SDL_FALSE
+#endif
+            );
+
+    if (music_musicchunk == NULL)
+        return MUSIC_Error;
+
+    if (Mix_PlayMusic(music_musicchunk, (loopflag == MUSIC_LoopSong)?-1:0) == -1)
+    {
+        initprintf("Mix_PlayMusic: %s\n", Mix_GetError());
+        return MUSIC_Error;
+    }
+
+    return MUSIC_Ok;
+}
+
+
+void MUSIC_Update(void)
+{}
diff --git a/source/blood/src/sectorfx.cpp b/source/blood/src/sectorfx.cpp
new file mode 100644
index 000000000..9ac60299a
--- /dev/null
+++ b/source/blood/src/sectorfx.cpp
@@ -0,0 +1,424 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "compat.h"
+#include "build.h"
+#include "pragmas.h"
+#include "common_game.h"
+
+#include "blood.h"
+#include "db.h"
+#include "gameutil.h"
+#include "globals.h"
+#include "trig.h"
+
+char flicker1[] = {
+    0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0,
+    1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1,
+    0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1,
+    0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1
+};
+
+char flicker2[] = {
+    1, 2, 4, 2, 3, 4, 3, 2, 0, 0, 1, 2, 4, 3, 2, 0,
+    2, 1, 0, 1, 0, 2, 3, 4, 3, 2, 1, 1, 2, 0, 0, 1,
+    1, 2, 3, 4, 4, 3, 2, 1, 2, 3, 4, 4, 2, 1, 0, 1,
+    0, 0, 0, 0, 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 3, 2
+};
+
+char flicker3[] = {
+    4, 4, 4, 4, 3, 4, 4, 4, 4, 4, 4, 2, 4, 3, 4, 4,
+    4, 4, 2, 1, 3, 3, 3, 4, 3, 4, 4, 4, 4, 4, 2, 4,
+    4, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2, 1, 0, 1,
+    0, 1, 0, 1, 0, 2, 3, 4, 4, 4, 4, 4, 4, 4, 3, 4
+};
+
+char flicker4[] = {
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    4, 0, 0, 3, 0, 1, 0, 1, 0, 4, 4, 4, 4, 4, 2, 0,
+    0, 0, 0, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 1,
+    0, 0, 0, 0, 0, 2, 1, 2, 1, 2, 1, 2, 1, 4, 3, 2,
+    0, 0, 0, 0, 0, 0, 0, 0, 0 ,0 ,0 ,0, 0, 0 ,0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0 ,0 ,0 ,0, 0, 0 ,0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0 ,0 ,0 ,0, 0, 0 ,0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0 ,0 ,0 ,0, 0, 0 ,0, 0
+};
+
+char strobe[] = {
+    64, 64, 64, 48, 36, 27, 20, 15, 11, 9, 6, 5, 4, 3, 2, 2,
+    1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+int GetWaveValue(int a, int b, int c)
+{
+    b &= 2047;
+    switch (a)
+    {
+    case 0:
+        return c;
+    case 1:
+        return (b>>10)*c;
+    case 2:
+        return (klabs(128-(b>>3))*c)>>7;
+    case 3:
+        return ((b>>3)*c)>>8;
+    case 4:
+        return ((255-(b>>3))*c)>>8;
+    case 5:
+        return (c+mulscale30(c,Sin(b)))>>1;
+    case 6:
+        return flicker1[b>>5]*c;
+    case 7:
+        return (flicker2[b>>5]*c)>>2;
+    case 8:
+        return (flicker3[b>>5]*c)>>2;
+    case 9:
+        return (flicker4[b>>4]*c)>>2;
+    case 10:
+        return (strobe[b>>5]*c)>>6;
+    case 11:
+        if (b*4 > 2048)
+            return 0;
+        return (c-mulscale30(c, Cos(b*4)))>>1;
+    }
+    return 0;
+}
+
+int shadeCount;
+short shadeList[512];
+
+void DoSectorLighting(void)
+{
+    for (int i = 0; i < shadeCount; i++)
+    {
+        int nXSector = shadeList[i];
+        XSECTOR *pXSector = &xsector[nXSector];
+        int nSector = pXSector->reference;
+        dassert(sector[nSector].extra == nXSector);
+        if (pXSector->shade)
+        {
+            int v4 = pXSector->shade;
+            if (pXSector->shadeFloor)
+            {
+                sector[nSector].floorshade -= v4;
+                if (pXSector->color)
+                {
+                    int nTemp = pXSector->floorpal;
+                    pXSector->floorpal = sector[nSector].floorpal;
+                    sector[nSector].floorpal = nTemp;
+                }
+            }
+            if (pXSector->shadeCeiling)
+            {
+                sector[nSector].ceilingshade -= v4;
+                if (pXSector->color)
+                {
+                    int nTemp = pXSector->ceilpal;
+                    pXSector->ceilpal = sector[nSector].ceilingpal;
+                    sector[nSector].ceilingpal = nTemp;
+                }
+            }
+            if (pXSector->shadeWalls)
+            {
+                int nStartWall = sector[nSector].wallptr;
+                int nEndWall = nStartWall + sector[nSector].wallnum;
+                for (int j = nStartWall; j < nEndWall; j++)
+                {
+                    wall[j].shade -= v4;
+                    if (pXSector->color)
+                    {
+                        wall[j].pal = sector[nSector].floorpal;
+                    }
+                }
+            }
+            pXSector->shade = 0;
+        }
+        if (pXSector->shadeAlways || pXSector->busy)
+        {
+            int t1 = pXSector->wave;
+            int t2 = pXSector->amplitude;
+            if (!pXSector->shadeAlways && pXSector->busy)
+            {
+                t2 = mulscale16(t2, pXSector->busy);
+            }
+            int v4 = GetWaveValue(t1, pXSector->phase*8+pXSector->freq*gGameClock, t2);
+            if (pXSector->shadeFloor)
+            {
+                sector[nSector].floorshade = ClipRange(sector[nSector].floorshade+v4, -128, 127);
+                if (pXSector->color && v4 != 0)
+                {
+                    int nTemp = pXSector->floorpal;
+                    pXSector->floorpal = sector[nSector].floorpal;
+                    sector[nSector].floorpal = nTemp;
+                }
+            }
+            if (pXSector->shadeCeiling)
+            {
+                sector[nSector].ceilingshade = ClipRange(sector[nSector].ceilingshade+v4, -128, 127);
+                if (pXSector->color && v4 != 0)
+                {
+                    int nTemp = pXSector->ceilpal;
+                    pXSector->ceilpal = sector[nSector].ceilingpal;
+                    sector[nSector].ceilingpal = nTemp;
+                }
+            }
+            if (pXSector->shadeWalls)
+            {
+                int nStartWall = sector[nSector].wallptr;
+                int nEndWall = nStartWall + sector[nSector].wallnum;
+                for (int j = nStartWall; j < nEndWall; j++)
+                {
+                    wall[j].shade = ClipRange(wall[j].shade+v4, -128, 127);
+                    if (pXSector->color && v4 != 0)
+                    {
+                        wall[j].pal = sector[nSector].floorpal;
+                    }
+                }
+            }
+            pXSector->shade = v4;
+        }
+    }
+}
+
+void UndoSectorLighting(void)
+{
+    for (int i = 0; i < numsectors; i++)
+    {
+        int nXSprite = sector[i].extra;
+        if (nXSprite > 0)
+        {
+            XSECTOR *pXSector = &xsector[i];
+            if (pXSector->shade)
+            {
+                int v4 = pXSector->shade;
+                if (pXSector->shadeFloor)
+                {
+                    sector[i].floorshade -= v4;
+                    if (pXSector->color)
+                    {
+                        int nTemp = pXSector->floorpal;
+                        pXSector->floorpal = sector[i].floorpal;
+                        sector[i].floorpal = nTemp;
+                    }
+                }
+                if (pXSector->shadeCeiling)
+                {
+                    sector[i].ceilingshade -= v4;
+                    if (pXSector->color)
+                    {
+                        int nTemp = pXSector->ceilpal;
+                        pXSector->ceilpal = sector[i].ceilingpal;
+                        sector[i].ceilingpal = nTemp;
+                    }
+                }
+                if (pXSector->shadeWalls)
+                {
+                    int nStartWall = sector[i].wallptr;
+                    int nEndWall = nStartWall + sector[i].wallnum;
+                    for (int j = nStartWall; j < nEndWall; j++)
+                    {
+                        wall[j].shade -= v4;
+                        if (pXSector->color)
+                        {
+                            wall[j].pal = sector[i].floorpal;
+                        }
+                    }
+                }
+                pXSector->shade = 0;
+            }
+        }
+    }
+}
+
+int panCount;
+short panList[kMaxXSectors];
+short wallPanList[kMaxXWalls];
+int wallPanCount;
+
+void DoSectorPanning(void)
+{
+    for (int i = 0; i < panCount; i++)
+    {
+        int nXSector = panList[i];
+        XSECTOR *pXSector = &xsector[nXSector];
+        int nSector = pXSector->reference;
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        sectortype *pSector = &sector[nSector];
+        dassert(pSector->extra == nXSector);
+        if (pXSector->panAlways || pXSector->busy)
+        {
+            int angle = pXSector->panAngle+1024;
+            int speed = pXSector->panVel<<10;
+            if (!pXSector->panAlways && (pXSector->busy&0xffff))
+                speed = mulscale16(speed, pXSector->busy);
+
+            if (pXSector->panFloor) // Floor
+            {
+                int nTile = pSector->floorpicnum;
+                int px = (pSector->floorxpanning<<8)+pXSector->at32_1;
+                int py = (pSector->floorypanning<<8)+pXSector->at34_0;
+                if (pSector->floorstat&64)
+                    angle -= 512;
+                int xBits = (picsiz[nTile]&15)-((pSector->floorstat&8)!=0);
+                px += mulscale30(speed<<2, Cos(angle))>>xBits;
+                int yBits = (picsiz[nTile]/16)-((pSector->floorstat&8)!=0);
+                py -= mulscale30(speed<<2, Sin(angle))>>yBits;
+                pSector->floorxpanning = px>>8;
+                pSector->floorypanning = py>>8;
+                pXSector->at32_1 = px&255;
+                pXSector->at34_0 = py&255;
+            }
+            if (pXSector->panCeiling) // Ceiling
+            {
+                int nTile = pSector->ceilingpicnum;
+                int px = (pSector->ceilingxpanning<<8)+pXSector->at30_1;
+                int py = (pSector->ceilingypanning<<8)+pXSector->at31_1;
+                if (pSector->ceilingstat&64)
+                    angle -= 512;
+                int xBits = (picsiz[nTile]&15)-((pSector->ceilingstat&8)!=0);
+                px += mulscale30(speed<<2, Cos(angle))>>xBits;
+                int yBits = (picsiz[nTile]/16)-((pSector->ceilingstat&8)!=0);
+                py -= mulscale30(speed<<2, Sin(angle))>>yBits;
+                pSector->ceilingxpanning = px>>8;
+                pSector->ceilingypanning = py>>8;
+                pXSector->at30_1 = px&255;
+                pXSector->at31_1 = py&255;
+            }
+        }
+    }
+    for (int i = 0; i < wallPanCount; i++)
+    {
+        int nXWall = wallPanList[i];
+        XWALL *pXWall = &xwall[nXWall];
+        int nWall = pXWall->reference;
+        dassert(wall[nWall].extra == nXWall);
+        if (pXWall->panAlways || pXWall->busy)
+        {
+            int psx = pXWall->panXVel<<10;
+            int psy = pXWall->panYVel<<10;
+            if (!pXWall->panAlways && (pXWall->busy & 0xffff))
+            {
+                psx = mulscale16(psx, pXWall->busy);
+                psy = mulscale16(psy, pXWall->busy);
+            }
+            int nTile = wall[nWall].picnum;
+            int px = (wall[nWall].xpanning<<8)+pXWall->xpanFrac;
+            int py = (wall[nWall].ypanning<<8)+pXWall->ypanFrac;
+            px += (psx<<2)>>((uint8_t)picsiz[nTile]&15);
+            py += (psy<<2)>>((uint8_t)picsiz[nTile]/16);
+            wall[nWall].xpanning = px>>8;
+            wall[nWall].ypanning = py>>8;
+            pXWall->xpanFrac = px&255;
+            pXWall->ypanFrac = py&255;
+        }
+    }
+}
+
+void InitSectorFX(void)
+{
+    shadeCount = 0;
+    panCount = 0;
+    wallPanCount = 0;
+    for (int i = 0; i < numsectors; i++)
+    {
+        int nXSector = sector[i].extra;
+        if (nXSector > 0)
+        {
+            XSECTOR *pXSector = &xsector[nXSector];
+            if (pXSector->amplitude)
+                shadeList[shadeCount++] = nXSector;
+            if (pXSector->panVel)
+                panList[panCount++] = nXSector;
+        }
+    }
+    for (int i = 0; i < numwalls; i++)
+    {
+        int nXWall = wall[i].extra;
+        if (nXWall > 0)
+        {
+            XWALL *pXWall = &xwall[nXWall];
+            if (pXWall->panXVel || pXWall->panYVel)
+                wallPanList[wallPanCount++] = nXWall;
+        }
+    }
+}
+
+class CSectorListMgr
+{
+public:
+    CSectorListMgr();
+    int CreateList(short);
+    void AddSector(int, short);
+    int GetSectorCount(int);
+    short *GetSectorList(int);
+private:
+    int nLists;
+    int nListSize[32];
+    int nListStart[32];
+    short nSectors[kMaxSectors];
+};
+
+CSectorListMgr::CSectorListMgr()
+{
+    nLists = 0;
+}
+
+int CSectorListMgr::CreateList(short nSector)
+{
+    int nStart = 0;
+    if (nLists)
+        nStart = nListStart[nLists-1]+nListStart[nLists-1];
+    int nList = nLists;
+    nListStart[nList] = nStart;
+    nListSize[nList] = 1;
+    nLists++;
+    short *pList = GetSectorList(nList);
+    pList[0] = nSector;
+    return nList;
+}
+
+void CSectorListMgr::AddSector(int nList, short nSector)
+{
+    for (int i = nLists; i > nList; i--)
+    {
+        short *pList = GetSectorList(i);
+        int nCount = GetSectorCount(i);
+        memmove(pList+1,pList,nCount*sizeof(short));
+        nListStart[i]++;
+    }
+    short *pList = GetSectorList(nList);
+    int nCount = GetSectorCount(nList);
+    pList[nCount] = nSector;
+    nListSize[nList]++;
+}
+
+int CSectorListMgr::GetSectorCount(int nList)
+{
+    return nListSize[nList];
+}
+
+short * CSectorListMgr::GetSectorList(int nList)
+{
+    return nSectors+nListStart[nList];
+}
diff --git a/source/blood/src/sectorfx.h b/source/blood/src/sectorfx.h
new file mode 100644
index 000000000..b1ed0979a
--- /dev/null
+++ b/source/blood/src/sectorfx.h
@@ -0,0 +1,28 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+void DoSectorLighting(void);
+void UndoSectorLighting(void);
+void DoSectorPanning(void);
+void InitSectorFX(void);
\ No newline at end of file
diff --git a/source/blood/src/seq.cpp b/source/blood/src/seq.cpp
new file mode 100644
index 000000000..2c15e25a0
--- /dev/null
+++ b/source/blood/src/seq.cpp
@@ -0,0 +1,598 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <string.h>
+#include "build.h"
+#include "common_game.h"
+
+#include "blood.h"
+#include "db.h"
+#include "eventq.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "sfx.h"
+#include "sound.h"
+#include "seq.h"
+#include "gameutil.h"
+#include "actor.h"
+#include "tile.h"
+
+#define kMaxClients 256
+#define kMaxSequences 1024
+
+static ACTIVE activeList[kMaxSequences];
+static int activeCount = 0;
+static int nClients = 0;
+static void(*clientCallback[kMaxClients])(int, int);
+
+int seqRegisterClient(void(*pClient)(int, int))
+{
+    dassert(nClients < kMaxClients);
+    clientCallback[nClients] = pClient;
+    return nClients++;
+}
+
+void Seq::Preload(void)
+{
+    if (memcmp(signature, "SEQ\x1a", 4) != 0)
+        ThrowError("Invalid sequence");
+    if ((version & 0xff00) != 0x300)
+        ThrowError("Obsolete sequence version");
+    for (int i = 0; i < nFrames; i++)
+        tilePreloadTile(seqGetTile(&frames[i]));
+}
+
+void Seq::Precache(void)
+{
+    if (memcmp(signature, "SEQ\x1a", 4) != 0)
+        ThrowError("Invalid sequence");
+    if ((version & 0xff00) != 0x300)
+        ThrowError("Obsolete sequence version");
+    for (int i = 0; i < nFrames; i++)
+        tilePrecacheTile(seqGetTile(&frames[i]));
+}
+
+void seqPrecacheId(int id)
+{
+    DICTNODE *hSeq = gSysRes.Lookup(id, "SEQ");
+    if (!hSeq)
+        return;
+    Seq *pSeq = (Seq*)gSysRes.Lock(hSeq);
+    pSeq->Precache();
+    gSysRes.Unlock(hSeq);
+}
+
+SEQINST siWall[kMaxXWalls];
+SEQINST siCeiling[kMaxXSectors];
+SEQINST siFloor[kMaxXSectors];
+SEQINST siSprite[kMaxXSprites];
+SEQINST siMasked[kMaxXWalls];
+
+void UpdateSprite(int nXSprite, SEQFRAME *pFrame)
+{
+    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+    int nSprite = xsprite[nXSprite].reference;
+    dassert(nSprite >= 0 && nSprite < kMaxSprites);
+    spritetype *pSprite = &sprite[nSprite];
+    dassert(pSprite->extra == nXSprite);
+    if (pSprite->hitag & 2)
+    {
+        if (tilesiz[pSprite->picnum].y != tilesiz[seqGetTile(pFrame)].y || picanm[pSprite->picnum].yofs != picanm[seqGetTile(pFrame)].yofs
+            || (pFrame->at3_0 && pFrame->at3_0 != pSprite->yrepeat))
+            pSprite->hitag |= 4;
+    }
+    pSprite->picnum = seqGetTile(pFrame);
+    if (pFrame->at5_0)
+        pSprite->pal = pFrame->at5_0;
+    pSprite->shade = pFrame->at4_0;
+    
+    int scale = xsprite[nXSprite].scale; // SEQ size scaling
+    if (pFrame->at2_0) {
+        if (scale < 0) pSprite->xrepeat = pFrame->at2_0 / abs(scale);
+        else if (scale > 0) pSprite->xrepeat = pFrame->at2_0 * scale;
+        else pSprite->xrepeat = pFrame->at2_0;
+    }
+
+    if (pFrame->at3_0) {
+        if (scale < 0) pSprite->yrepeat = pFrame->at3_0 / abs(scale);
+        else if (scale > 0) pSprite->yrepeat = pFrame->at3_0 * scale;
+        else pSprite->yrepeat = pFrame->at3_0;
+    }
+
+    if (pFrame->at1_4)
+        pSprite->cstat |= 2;
+    else
+        pSprite->cstat &= ~2;
+    if (pFrame->at1_5)
+        pSprite->cstat |= 512;
+    else
+        pSprite->cstat &= ~512;
+    if (pFrame->at1_6)
+        pSprite->cstat |= 1;
+    else
+        pSprite->cstat &= ~1;
+    if (pFrame->at1_7)
+        pSprite->cstat |= 256;
+    else
+        pSprite->cstat &= ~256;
+    if (pFrame->at6_2)
+        pSprite->cstat |= 32768;
+    else
+        pSprite->cstat &= (unsigned short)~32768;
+    if (pFrame->at6_0)
+        pSprite->cstat |= 4096;
+    else
+        pSprite->cstat &= ~4096;
+    if (pFrame->at5_6)
+        pSprite->hitag |= 256;
+    else
+        pSprite->hitag &= ~256;
+    if (pFrame->at5_7)
+        pSprite->hitag |= 8;
+    else
+        pSprite->hitag &= ~8;
+    if (pFrame->at6_3)
+        pSprite->hitag |= 1024;
+    else
+        pSprite->hitag &= ~1024;
+    if (pFrame->at6_4)
+        pSprite->hitag |= 2048;
+    else
+        pSprite->hitag &= ~2048;
+}
+
+void UpdateWall(int nXWall, SEQFRAME *pFrame)
+{
+    dassert(nXWall > 0 && nXWall < kMaxXWalls);
+    int nWall = xwall[nXWall].reference;
+    dassert(nWall >= 0 && nWall < kMaxWalls);
+    walltype *pWall = &wall[nWall];
+    dassert(pWall->extra == nXWall);
+    pWall->picnum = seqGetTile(pFrame);
+    if (pFrame->at5_0)
+        pWall->pal = pFrame->at5_0;
+    if (pFrame->at1_4)
+        pWall->cstat |= 128;
+    else
+        pWall->cstat &= ~128;
+    if (pFrame->at1_5)
+        pWall->cstat |= 512;
+    else
+        pWall->cstat &= ~512;
+    if (pFrame->at1_6)
+        pWall->cstat |= 1;
+    else
+        pWall->cstat &= ~1;
+    if (pFrame->at1_7)
+        pWall->cstat |= 64;
+    else
+        pWall->cstat &= ~64;
+}
+
+void UpdateMasked(int nXWall, SEQFRAME *pFrame)
+{
+    dassert(nXWall > 0 && nXWall < kMaxXWalls);
+    int nWall = xwall[nXWall].reference;
+    dassert(nWall >= 0 && nWall < kMaxWalls);
+    walltype *pWall = &wall[nWall];
+    dassert(pWall->extra == nXWall);
+    dassert(pWall->nextwall >= 0);
+    walltype *pWallNext = &wall[pWall->nextwall];
+    pWall->overpicnum = pWallNext->overpicnum = seqGetTile(pFrame);
+    if (pFrame->at5_0)
+        pWall->pal = pWallNext->pal = pFrame->at5_0;
+    if (pFrame->at1_4)
+    {
+        pWall->cstat |= 128;
+        pWallNext->cstat |= 128;
+    }
+    else
+    {
+        pWall->cstat &= ~128;
+        pWallNext->cstat &= ~128;
+    }
+    if (pFrame->at1_5)
+    {
+        pWall->cstat |= 512;
+        pWallNext->cstat |= 512;
+    }
+    else
+    {
+        pWall->cstat &= ~512;
+        pWallNext->cstat &= ~512;
+    }
+    if (pFrame->at1_6)
+    {
+        pWall->cstat |= 1;
+        pWallNext->cstat |= 1;
+    }
+    else
+    {
+        pWall->cstat &= ~1;
+        pWallNext->cstat &= ~1;
+    }
+    if (pFrame->at1_7)
+    {
+        pWall->cstat |= 64;
+        pWallNext->cstat |= 64;
+    }
+    else
+    {
+        pWall->cstat &= ~64;
+        pWallNext->cstat &= ~64;
+    }
+}
+
+void UpdateFloor(int nXSector, SEQFRAME *pFrame)
+{
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    int nSector = xsector[nXSector].reference;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    sectortype *pSector = &sector[nSector];
+    dassert(pSector->extra == nXSector);
+    pSector->floorpicnum = seqGetTile(pFrame);
+    pSector->floorshade = pFrame->at4_0;
+    if (pFrame->at5_0)
+        pSector->floorpal = pFrame->at5_0;
+}
+
+void UpdateCeiling(int nXSector, SEQFRAME *pFrame)
+{
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    int nSector = xsector[nXSector].reference;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    sectortype *pSector = &sector[nSector];
+    dassert(pSector->extra == nXSector);
+    pSector->ceilingpicnum = seqGetTile(pFrame);
+    pSector->ceilingshade = pFrame->at4_0;
+    if (pFrame->at5_0)
+        pSector->ceilingpal = pFrame->at5_0;
+}
+
+void SEQINST::Update(ACTIVE *pActive)
+{
+    dassert(frameIndex < pSequence->nFrames);
+    switch (pActive->type)
+    {
+    case 0:
+        UpdateWall(pActive->xindex, &pSequence->frames[frameIndex]);
+        break;
+    case 1:
+        UpdateCeiling(pActive->xindex , &pSequence->frames[frameIndex]);
+        break;
+    case 2:
+        UpdateFloor(pActive->xindex, &pSequence->frames[frameIndex]);
+        break;
+    case 3: 
+    {
+
+        UpdateSprite(pActive->xindex, &pSequence->frames[frameIndex]);
+        if (pSequence->frames[frameIndex].at6_1)
+            sfxPlay3DSound(&sprite[xsprite[pActive->xindex].reference], pSequence->ata + Random(pSequence->frames[frameIndex].soundRange), -1, 0);
+
+        spritetype* pSprite = &sprite[xsprite[pActive->xindex].reference];
+        if (pSequence->frames[frameIndex].surfaceSound && zvel[pSprite->xvel] == 0 && xvel[pSprite->xvel] != 0) {
+
+            // by NoOne: add surfaceSound trigger feature
+            if (gUpperLink[pSprite->sectnum] >= 0) break; // don't play surface sound for stacked sectors
+            int surf = tileGetSurfType(pSprite->sectnum + 0x4000); if (!surf) break;
+            static int surfSfxMove[15][4] = {
+                /* {snd1, snd2, gameVolume, myVolume} */
+                {800,801,80,25},
+                {802,803,80,25},
+                {804,805,80,25},
+                {806,807,80,25},
+                {808,809,80,25},
+                {810,811,80,25},
+                {812,813,80,25},
+                {814,815,80,25},
+                {816,817,80,25},
+                {818,819,80,25},
+                {820,821,80,25},
+                {822,823,80,25},
+                {824,825,80,25},
+                {826,827,80,25},
+                {828,829,80,25},
+            };
+
+            int sndId = surfSfxMove[surf][Random(2)];
+            DICTNODE * hRes = gSoundRes.Lookup(sndId, "SFX"); SFX * pEffect = (SFX*)gSoundRes.Load(hRes);
+            sfxPlay3DSoundCP(pSprite, sndId, -1, 0, 0, (surfSfxMove[surf][2] != pEffect->relVol) ? pEffect->relVol : surfSfxMove[surf][3]);
+        }
+        
+
+        break;
+    }
+    case 4:
+        UpdateMasked(pActive->xindex, &pSequence->frames[frameIndex]);
+        break;
+    }
+    if (pSequence->frames[frameIndex].at5_5 && atc != -1)
+        clientCallback[atc](pActive->type, pActive->xindex);
+}
+
+SEQINST * GetInstance(int a1, int a2)
+{
+    switch (a1)
+    {
+    case 0:
+        if (a2 > 0 && a2 < kMaxXWalls) return &siWall[a2];
+        break;
+    case 1:
+        if (a2 > 0 && a2 < kMaxXSectors) return &siCeiling[a2];
+        break;
+    case 2:
+        if (a2 > 0 && a2 < kMaxXSectors) return &siFloor[a2];
+        break;
+    case 3:
+        if (a2 > 0 && a2 < kMaxXSprites) return &siSprite[a2];
+        break;
+    case 4:
+        if (a2 > 0 && a2 < kMaxWalls) return &siMasked[a2];
+        break;
+    }
+    return NULL;
+}
+
+void UnlockInstance(SEQINST *pInst)
+{
+    dassert(pInst != NULL);
+    dassert(pInst->hSeq != NULL);
+    dassert(pInst->pSequence != NULL);
+    gSysRes.Unlock(pInst->hSeq);
+    pInst->hSeq = NULL;
+    pInst->pSequence = NULL;
+    pInst->at13 = 0;
+}
+
+void seqSpawn(int a1, int a2, int a3, int a4)
+{
+    SEQINST *pInst = GetInstance(a2, a3);
+    if (!pInst)
+        return;
+    DICTNODE *hSeq = gSysRes.Lookup(a1, "SEQ");
+    if (!hSeq)
+        ThrowError("Missing sequence #%d", a1);
+    int i = activeCount;
+    if (pInst->at13)
+    {
+        if (hSeq == pInst->hSeq)
+            return;
+        UnlockInstance(pInst);
+        for (i = 0; i < activeCount; i++)
+        {
+            if (activeList[i].type == a2 && activeList[i].xindex == a3)
+                break;
+        }
+        dassert(i < activeCount);
+    }
+    Seq *pSeq = (Seq*)gSysRes.Lock(hSeq);
+    if (memcmp(pSeq->signature, "SEQ\x1a", 4) != 0)
+        ThrowError("Invalid sequence %d", a1);
+    if ((pSeq->version & 0xff00) != 0x300)
+        ThrowError("Sequence %d is obsolete version", a1);
+    if ((pSeq->version & 0xff) == 0x00)
+    {
+        for (int i = 0; i < pSeq->nFrames; i++)
+            pSeq->frames[i].tile2 = 0;
+    }
+    pInst->at13 = 1;
+    pInst->hSeq = hSeq;
+    pInst->pSequence = pSeq;
+    pInst->at8 = a1;
+    pInst->atc = a4;
+    pInst->at10 = pSeq->at8;
+    pInst->frameIndex = 0;
+    if (i == activeCount)
+    {
+        dassert(activeCount < kMaxSequences);
+        activeList[activeCount].type = a2;
+        activeList[activeCount].xindex = a3;
+        activeCount++;
+    }
+    pInst->Update(&activeList[i]);
+}
+
+void seqKill(int a1, int a2)
+{
+    SEQINST *pInst = GetInstance(a1, a2);
+    if (!pInst || !pInst->at13)
+        return;
+    int i;
+    for (i = 0; i < activeCount; i++)
+    {
+        if (activeList[i].type == a1 && activeList[i].xindex == a2)
+            break;
+    }
+    dassert(i < activeCount);
+    activeCount--;
+    activeList[i] = activeList[activeCount];
+    pInst->at13 = 0;
+    UnlockInstance(pInst);
+}
+
+void seqKillAll(void)
+{
+    for (int i = 0; i < kMaxXWalls; i++)
+    {
+        if (siWall[i].at13)
+            UnlockInstance(&siWall[i]);
+        if (siMasked[i].at13)
+            UnlockInstance(&siMasked[i]);
+    }
+    for (int i = 0; i < kMaxXSectors; i++)
+    {
+        if (siCeiling[i].at13)
+            UnlockInstance(&siCeiling[i]);
+        if (siFloor[i].at13)
+            UnlockInstance(&siFloor[i]);
+    }
+    for (int i = 0; i < kMaxXSprites; i++)
+    {
+        if (siSprite[i].at13)
+            UnlockInstance(&siSprite[i]);
+    }
+    activeCount = 0;
+}
+
+int seqGetStatus(int a1, int a2)
+{
+    SEQINST *pInst = GetInstance(a1, a2);
+    if (pInst && pInst->at13)
+        return pInst->frameIndex;
+    return -1;
+}
+
+int seqGetID(int a1, int a2)
+{
+    SEQINST *pInst = GetInstance(a1, a2);
+    if (pInst)
+        return pInst->at8;
+    return -1;
+}
+
+void seqProcess(int a1)
+{
+    for (int i = 0; i < activeCount; i++)
+    {
+        SEQINST *pInst = GetInstance(activeList[i].type, activeList[i].xindex);
+        Seq *pSeq = pInst->pSequence;
+        dassert(pInst->frameIndex < pSeq->nFrames);
+        pInst->at10 -= a1;
+        while (pInst->at10 < 0)
+        {
+            pInst->at10 += pSeq->at8;
+            pInst->frameIndex++;
+            if (pInst->frameIndex == pSeq->nFrames)
+            {
+                if (pSeq->atc & 1)
+                    pInst->frameIndex = 0;
+                else
+                {
+                    UnlockInstance(pInst);
+                    if (pSeq->atc & 2)
+                    {
+                        switch (activeList[i].type)
+                        {
+                        case 3:
+                        {
+                            int nXSprite = activeList[i].xindex;
+                            int nSprite = xsprite[nXSprite].reference;
+                            dassert(nSprite >= 0 && nSprite < kMaxSprites);
+                            evKill(nSprite, 3);
+                            if ((sprite[nSprite].hitag & 16) && sprite[nSprite].zvel >= 200 && sprite[nSprite].zvel < 254)
+                                evPost(nSprite, 3, gGameOptions.nMonsterSettings, (COMMAND_ID)9);
+                            else
+                                DeleteSprite(nSprite);
+                            break;
+                        }
+                        case 4:
+                        {
+                            int nXWall = activeList[i].xindex;
+                            int nWall = xwall[nXWall].reference;
+                            dassert(nWall >= 0 && nWall < kMaxWalls);
+                            wall[nWall].cstat &= ~(8 + 16 + 32);
+                            if (wall[nWall].nextwall != -1)
+                                wall[wall[nWall].nextwall].cstat &= ~(8 + 16 + 32);
+                            break;
+                        }
+                        }
+                    }
+                    activeList[i--] = activeList[--activeCount];
+                    break;
+                }
+            }
+            pInst->Update(&activeList[i]);
+        }
+    }
+}
+
+class SeqLoadSave : public LoadSave {
+    virtual void Load(void);
+    virtual void Save(void);
+};
+
+void SeqLoadSave::Load(void)
+{
+    Read(&siWall, sizeof(siWall));
+    Read(&siMasked, sizeof(siMasked));
+    Read(&siCeiling, sizeof(siCeiling));
+    Read(&siFloor, sizeof(siFloor));
+    Read(&siSprite, sizeof(siSprite));
+    Read(&activeList, sizeof(activeList));
+    Read(&activeCount, sizeof(activeCount));
+    for (int i = 0; i < kMaxXWalls; i++)
+    {
+        siWall[i].hSeq = NULL;
+        siMasked[i].hSeq = NULL;
+        siWall[i].pSequence = NULL;
+        siMasked[i].pSequence = NULL;
+    }
+    for (int i = 0; i < kMaxXSectors; i++)
+    {
+        siCeiling[i].hSeq = NULL;
+        siFloor[i].hSeq = NULL;
+        siCeiling[i].pSequence = NULL;
+        siFloor[i].pSequence = NULL;
+    }
+    for (int i = 0; i < kMaxXSprites; i++)
+    {
+        siSprite[i].hSeq = NULL;
+        siSprite[i].pSequence = NULL;
+    }
+    for (int i = 0; i < activeCount; i++)
+    {
+        SEQINST *pInst = GetInstance(activeList[i].type, activeList[i].xindex);
+        if (pInst->at13)
+        {
+            int nSeq = pInst->at8;
+            DICTNODE *hSeq = gSysRes.Lookup(nSeq, "SEQ");
+            if (!hSeq)
+                ThrowError("Missing sequence #%d", nSeq);
+            Seq *pSeq = (Seq*)gSysRes.Lock(hSeq);
+            if (memcmp(pSeq->signature, "SEQ\x1a", 4) != 0)
+                ThrowError("Invalid sequence %d", nSeq);
+            if ((pSeq->version & 0xff00) != 0x300)
+                ThrowError("Sequence %d is obsolete version", nSeq);
+            pInst->hSeq = hSeq;
+            pInst->pSequence = pSeq;
+        }
+    }
+}
+
+void SeqLoadSave::Save(void)
+{
+    Write(&siWall, sizeof(siWall));
+    Write(&siMasked, sizeof(siMasked));
+    Write(&siCeiling, sizeof(siCeiling));
+    Write(&siFloor, sizeof(siFloor));
+    Write(&siSprite, sizeof(siSprite));
+    Write(&activeList, sizeof(activeList));
+    Write(&activeCount, sizeof(activeCount));
+}
+
+static SeqLoadSave *myLoadSave;
+
+void SeqLoadSaveConstruct(void)
+{
+    myLoadSave = new SeqLoadSave();
+}
diff --git a/source/blood/src/seq.h b/source/blood/src/seq.h
new file mode 100644
index 000000000..d62e6ceaa
--- /dev/null
+++ b/source/blood/src/seq.h
@@ -0,0 +1,94 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "resource.h"
+
+struct SEQFRAME {
+    unsigned int tile : 12;
+    unsigned int at1_4 : 1; // transparent
+    unsigned int at1_5 : 1; // transparent
+    unsigned int at1_6 : 1; // blockable
+    unsigned int at1_7 : 1; // hittable
+    unsigned int at2_0 : 8; // xrepeat
+    unsigned int at3_0 : 8; // yrepeat
+    signed int at4_0 : 8; // shade
+    unsigned int at5_0 : 5; // palette
+    unsigned int at5_5 : 1; //
+    unsigned int at5_6 : 1; //
+    unsigned int at5_7 : 1; //
+    unsigned int at6_0 : 1; //
+    unsigned int at6_1 : 1; //
+    unsigned int at6_2 : 1; // invisible
+    unsigned int at6_3 : 1; //
+    unsigned int at6_4 : 1; //
+    unsigned int tile2 : 4;
+    unsigned soundRange : 4; // (by NoOne) random sound range relative to global SEQ sound
+    unsigned surfaceSound : 1; // (by NoOne) trigger surface sound when moving / touching
+    unsigned reserved : 2;
+};
+
+struct Seq {
+    char signature[4];
+    short version;
+    short nFrames; // at6
+    short at8;
+    short ata;
+    int atc;
+    SEQFRAME frames[1]; // at10
+    void Preload(void);
+    void Precache(void);
+};
+
+struct ACTIVE
+{
+    unsigned char type;
+    unsigned short xindex;
+};
+
+struct SEQINST
+{
+    DICTNODE *hSeq;
+    Seq *pSequence; // mass
+    int at8;
+    int atc;
+    short at10;
+    unsigned char frameIndex; // at12
+    char at13;
+    void Update(ACTIVE *pActive);
+};
+
+inline int seqGetTile(SEQFRAME* pFrame)
+{
+    return pFrame->tile+(pFrame->tile2<<12);
+}
+
+int seqRegisterClient(void(*pClient)(int, int));
+void seqPrecacheId(int id);
+SEQINST * GetInstance(int a1, int a2);
+void UnlockInstance(SEQINST *pInst);
+void seqSpawn(int a1, int a2, int a3, int a4 = -1);
+void seqKill(int a1, int a2);
+void seqKillAll(void);
+int seqGetStatus(int a1, int a2);
+int seqGetID(int a1, int a2);
+void seqProcess(int a1);
\ No newline at end of file
diff --git a/source/blood/src/sfx.cpp b/source/blood/src/sfx.cpp
new file mode 100644
index 000000000..c861481ec
--- /dev/null
+++ b/source/blood/src/sfx.cpp
@@ -0,0 +1,528 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <string.h>
+#include "build.h"
+#include "compat.h"
+#include "common_game.h"
+#include "fx_man.h"
+
+#include "config.h"
+#include "gameutil.h"
+#include "player.h"
+#include "resource.h"
+#include "sfx.h"
+#include "sound.h"
+#include "trig.h"
+
+POINT2D earL, earR, earL0, earR0; // Ear position
+VECTOR2D earVL, earVR; // Ear velocity ?
+int lPhase, rPhase, lVol, rVol, lPitch, rPitch;
+
+struct BONKLE
+{
+    int at0;
+    int at4;
+    DICTNODE *at8;
+    int atc;
+    spritetype *at10;
+    int at14;
+    int at18;
+    int at1c;
+    POINT3D at20;
+    POINT3D at2c;
+    //int at20;
+    //int at24;
+    //int at28;
+    //int at2c;
+    //int at30;
+    //int at34;
+    int at38;
+    int at3c;
+};
+
+BONKLE Bonkle[256];
+BONKLE *BonkleCache[256];
+
+int nBonkles;
+
+void sfxInit(void)
+{
+    for (int i = 0; i < 256; i++)
+        BonkleCache[i] = &Bonkle[i];
+    nBonkles = 0;
+}
+
+void sfxTerm()
+{
+}
+
+int Vol3d(int angle, int dist)
+{
+    return dist - mulscale16(dist, 0x2000 - mulscale30(0x2000, Cos(angle)));
+}
+
+void Calc3DValues(BONKLE *pBonkle)
+{
+    int dx = pBonkle->at20.x - gMe->pSprite->x;
+    int dy = pBonkle->at20.y - gMe->pSprite->y;
+    int dz = pBonkle->at20.z - gMe->pSprite->z;
+    int angle = getangle(dx, dy);
+    dx >>= 4;
+    dy >>= 4;
+    dz >>= 8;
+    int distance = ksqrt(dx*dx + dy * dy + dz * dz);
+    distance = ClipLow((distance >> 2) + (distance >> 3), 64);
+    int v14, v18;
+    v14 = v18 = scale(pBonkle->at1c, 80, distance);
+    int sinVal = Sin(angle);
+    int cosVal = Cos(angle);
+    int v8 = dmulscale30r(cosVal, pBonkle->at20.x - pBonkle->at2c.x, sinVal, pBonkle->at20.y - pBonkle->at2c.y);
+
+    int distanceL = approxDist(pBonkle->at20.x - earL.x, pBonkle->at20.y - earL.y);
+    lVol = Vol3d(angle - (gMe->pSprite->ang - 85), v18);
+    int phaseLeft = mulscale16r(distanceL, pBonkle->at3c == 1 ? 4114 : 8228);
+    lPitch = scale(pBonkle->at18, dmulscale30r(cosVal, earVL.dx, sinVal, earVL.dy) + 5853, v8 + 5853);
+
+    int distanceR = approxDist(pBonkle->at20.x - earR.x, pBonkle->at20.y - earR.y);
+    rVol = Vol3d(angle - (gMe->pSprite->ang + 85), v14);
+    int phaseRight = mulscale16r(distanceR, pBonkle->at3c == 1 ? 4114 : 8228);
+    rPitch = scale(pBonkle->at18, dmulscale30r(cosVal, earVR.dx, sinVal, earVR.dy) + 5853, v8 + 5853);
+
+    int phaseMin = ClipHigh(phaseLeft, phaseRight);
+    lPhase = phaseRight - phaseMin;
+    rPhase = phaseLeft - phaseMin;
+}
+
+void sfxPlay3DSound(int x, int y, int z, int soundId, int nSector)
+{
+    if (!SoundToggle)
+        return;
+    if (soundId < 0)
+        ThrowError("Invalid sound ID");
+    DICTNODE *hRes = gSoundRes.Lookup(soundId, "SFX");
+    if (!hRes)
+        return;
+
+    SFX *pEffect = (SFX*)gSoundRes.Load(hRes);
+    hRes = gSoundRes.Lookup(pEffect->rawName, "RAW");
+    if (!hRes)
+        return;
+    int v1c, v18;
+    v1c = v18 = mulscale16(pEffect->pitch, sndGetRate(pEffect->format));
+    if (nBonkles >= 256)
+        return;
+    BONKLE *pBonkle = BonkleCache[nBonkles++];
+    pBonkle->at10 = NULL;
+    pBonkle->at20.x = x;
+    pBonkle->at20.y = y;
+    pBonkle->at20.z = z;
+    pBonkle->at38 = nSector;
+    FindSector(x, y, z, &pBonkle->at38);
+    pBonkle->at2c = pBonkle->at20;
+    pBonkle->atc = soundId;
+    pBonkle->at8 = hRes;
+    pBonkle->at1c = pEffect->relVol;
+    pBonkle->at18 = v18;
+    pBonkle->at3c = pEffect->format;
+    int size = hRes->size;
+    char *pData = (char*)gSoundRes.Lock(hRes);
+    Calc3DValues(pBonkle);
+    int priority = 1;
+    if (priority < lVol)
+        priority = lVol;
+    if (priority < rVol)
+        priority = rVol;
+    if (gDoppler)
+    {
+        DisableInterrupts();
+        pBonkle->at0 = FX_PlayRaw(pData + lPhase, size - lPhase, lPitch, 0, lVol, lVol, 0, priority, 1.f, (intptr_t)&pBonkle->at0);
+        pBonkle->at4 = FX_PlayRaw(pData + rPhase, size - rPhase, rPitch, 0, rVol, 0, rVol, priority, 1.f, (intptr_t)&pBonkle->at4);
+        RestoreInterrupts();
+    }
+    else
+    {
+        pBonkle->at0 = FX_PlayRaw(pData + lPhase, size - lPhase, v1c, 0, lVol, lVol, rVol, priority, 1.f, (intptr_t)&pBonkle->at0);
+        pBonkle->at4 = 0;
+    }
+}
+
+void sfxPlay3DSound(spritetype *pSprite, int soundId, int a3, int a4)
+{
+    if (!SoundToggle)
+        return;
+    if (!pSprite)
+        return;
+    if (soundId < 0)
+        return;
+    DICTNODE *hRes = gSoundRes.Lookup(soundId, "SFX");
+    if (!hRes)
+        return;
+
+    SFX *pEffect = (SFX*)gSoundRes.Load(hRes);
+    hRes = gSoundRes.Lookup(pEffect->rawName, "RAW");
+    if (!hRes)
+        return;
+    int size = hRes->size;
+    if (size <= 0)
+        return;
+    int v14;
+    v14 = mulscale16(pEffect->pitch, sndGetRate(pEffect->format));
+    BONKLE *pBonkle = NULL;
+    if (a3 >= 0)
+    {
+        int i;
+        for (i = 0; i < nBonkles; i++)
+        {
+            pBonkle = BonkleCache[i];
+            if (pBonkle->at14 == a3 && (pBonkle->at10 == pSprite || (a4 & 1) != 0))
+            {
+                if ((a4 & 4) != 0 && pBonkle->at14 == a3)
+                    return;
+                if ((a4 & 2) != 0 && pBonkle->atc == soundId)
+                    return;
+                if (pBonkle->at0 > 0)
+                    FX_StopSound(pBonkle->at0);
+                if (pBonkle->at4 > 0)
+                    FX_StopSound(pBonkle->at4);
+                if (pBonkle->at8)
+                {
+                    gSoundRes.Unlock(pBonkle->at8);
+                    pBonkle->at8 = NULL;
+                }
+                break;
+            }
+        }
+        if (i == nBonkles)
+        {
+            if (nBonkles >= 256)
+                return;
+            pBonkle = BonkleCache[nBonkles++];
+        }
+        pBonkle->at10 = pSprite;
+        pBonkle->at14 = a3;
+    }
+    else
+    {
+        if (nBonkles >= 256)
+            return;
+        pBonkle = BonkleCache[nBonkles++];
+        pBonkle->at10 = NULL;
+    }
+    pBonkle->at20.x = pSprite->x;
+    pBonkle->at20.y = pSprite->y;
+    pBonkle->at20.z = pSprite->z;
+    pBonkle->at38 = pSprite->sectnum;
+    pBonkle->at2c = pBonkle->at20;
+    pBonkle->atc = soundId;
+    pBonkle->at8 = hRes;
+    pBonkle->at1c = pEffect->relVol;
+    pBonkle->at18 = v14;
+    Calc3DValues(pBonkle);
+    int priority = 1;
+    if (priority < lVol)
+        priority = lVol;
+    if (priority < rVol)
+        priority = rVol;
+    int loopStart = pEffect->loopStart;
+    int loopEnd = ClipLow(size - 1, 0);
+    if (a3 < 0)
+        loopStart = -1;
+    DisableInterrupts();
+    char *pData = (char*)gSoundRes.Lock(hRes);
+    if (loopStart >= 0)
+    {
+        if (gDoppler)
+        {
+            pBonkle->at0 = FX_PlayLoopedRaw(pData + lPhase, size - lPhase, pData + loopStart, pData + loopEnd, lPitch, 0, lVol, lVol, 0, priority, 1.f, (intptr_t)&pBonkle->at0);
+            pBonkle->at4 = FX_PlayLoopedRaw(pData + rPhase, size - rPhase, pData + loopStart, pData + loopEnd, rPitch, 0, rVol, 0, rVol, priority, 1.f, (intptr_t)&pBonkle->at4);
+        }
+        else
+        {
+            pBonkle->at0 = FX_PlayLoopedRaw(pData + lPhase, size - lPhase, pData + loopStart, pData + loopEnd, v14, 0, lVol, lVol, rVol, priority, 1.f, (intptr_t)&pBonkle->at0);
+            pBonkle->at4 = 0;
+        }
+    }
+    else
+    {
+        pData = (char*)gSoundRes.Lock(pBonkle->at8);
+        if (gDoppler)
+        {
+            pBonkle->at0 = FX_PlayRaw(pData + lPhase, size - lPhase, lPitch, 0, lVol, lVol, 0, priority, 1.f, (intptr_t)&pBonkle->at0);
+            pBonkle->at4 = FX_PlayRaw(pData + rPhase, size - rPhase, rPitch, 0, rVol, 0, rVol, priority, 1.f, (intptr_t)&pBonkle->at4);
+        }
+        else
+        {
+            pBonkle->at0 = FX_PlayRaw(pData + lPhase, size - lPhase, v14, 0, lVol, lVol, rVol, priority, 1.f, (intptr_t)&pBonkle->at0);
+            pBonkle->at4 = 0;
+        }
+    }
+    RestoreInterrupts();
+}
+
+// By NoOne: same as previous, but allows to set custom pitch for sound AND volume. Used by SFX gen now.
+void sfxPlay3DSoundCP(spritetype* pSprite, int soundId, int a3, int a4, int pitch, int volume)
+{
+    if (!SoundToggle || !pSprite || soundId < 0) return;
+    DICTNODE* hRes = gSoundRes.Lookup(soundId, "SFX");
+    if (!hRes) return;
+
+    SFX* pEffect = (SFX*)gSoundRes.Load(hRes);
+    hRes = gSoundRes.Lookup(pEffect->rawName, "RAW");
+    if (!hRes) return;
+    int size = hRes->size;
+    if (size <= 0) return;
+    
+    if (pitch <= 0) pitch = pEffect->pitch;
+    else pitch -= Random(pEffect->pitchRange);
+
+    int v14;
+    v14 = mulscale16(pitch, sndGetRate(pEffect->format));
+    
+    BONKLE * pBonkle = NULL;
+    if (a3 >= 0)
+    {
+        int i;
+        for (i = 0; i < nBonkles; i++)
+        {
+            pBonkle = BonkleCache[i];
+            if (pBonkle->at14 == a3 && (pBonkle->at10 == pSprite || (a4 & 1) != 0))
+            {
+                if ((a4 & 4) != 0 && pBonkle->at14 == a3)
+                    return;
+                if ((a4 & 2) != 0 && pBonkle->atc == soundId)
+                    return;
+                if (pBonkle->at0 > 0)
+                    FX_StopSound(pBonkle->at0);
+                if (pBonkle->at4 > 0)
+                    FX_StopSound(pBonkle->at4);
+                if (pBonkle->at8)
+                {
+                    gSoundRes.Unlock(pBonkle->at8);
+                    pBonkle->at8 = NULL;
+                }
+                break;
+            }
+        }
+        if (i == nBonkles)
+        {
+            if (nBonkles >= 256)
+                return;
+            pBonkle = BonkleCache[nBonkles++];
+        }
+        pBonkle->at10 = pSprite;
+        pBonkle->at14 = a3;
+    }
+    else
+    {
+        if (nBonkles >= 256)
+            return;
+        pBonkle = BonkleCache[nBonkles++];
+        pBonkle->at10 = NULL;
+    }
+    pBonkle->at20.x = pSprite->x;
+    pBonkle->at20.y = pSprite->y;
+    pBonkle->at20.z = pSprite->z;
+    pBonkle->at38 = pSprite->sectnum;
+    pBonkle->at2c = pBonkle->at20;
+    pBonkle->atc = soundId;
+    pBonkle->at8 = hRes;
+    pBonkle->at1c = (volume <= 0) ? pEffect->relVol : volume;
+    pBonkle->at18 = v14;
+    Calc3DValues(pBonkle);
+    int priority = 1;
+    if (priority < lVol)
+        priority = lVol;
+    if (priority < rVol)
+        priority = rVol;
+    int loopStart = pEffect->loopStart;
+    int loopEnd = ClipLow(size - 1, 0);
+    if (a3 < 0)
+        loopStart = -1;
+    DisableInterrupts();
+    char* pData = (char*)gSoundRes.Lock(hRes);
+    if (loopStart >= 0)
+    {
+        if (gDoppler)
+        {
+            pBonkle->at0 = FX_PlayLoopedRaw(pData + lPhase, size - lPhase, pData + loopStart, pData + loopEnd, lPitch, 0, lVol, lVol, 0, priority, 1.f, (intptr_t)& pBonkle->at0);
+            pBonkle->at4 = FX_PlayLoopedRaw(pData + rPhase, size - rPhase, pData + loopStart, pData + loopEnd, rPitch, 0, rVol, 0, rVol, priority, 1.f, (intptr_t)& pBonkle->at4);
+        }
+        else
+        {
+            pBonkle->at0 = FX_PlayLoopedRaw(pData + lPhase, size - lPhase, pData + loopStart, pData + loopEnd, v14, 0, lVol, lVol, rVol, priority, 1.f, (intptr_t)& pBonkle->at0);
+            pBonkle->at4 = 0;
+        }
+    }
+    else
+    {
+        pData = (char*)gSoundRes.Lock(pBonkle->at8);
+        if (gDoppler)
+        {
+            pBonkle->at0 = FX_PlayRaw(pData + lPhase, size - lPhase, lPitch, 0, lVol, lVol, 0, priority, 1.f, (intptr_t)& pBonkle->at0);
+            pBonkle->at4 = FX_PlayRaw(pData + rPhase, size - rPhase, rPitch, 0, rVol, 0, rVol, priority, 1.f, (intptr_t)& pBonkle->at4);
+        }
+        else
+        {
+            pBonkle->at0 = FX_PlayRaw(pData + lPhase, size - lPhase, v14, 0, lVol, lVol, rVol, priority, 1.f, (intptr_t)& pBonkle->at0);
+            pBonkle->at4 = 0;
+        }
+    }
+    RestoreInterrupts();
+}
+
+
+void sfxKill3DSound(spritetype *pSprite, int a2, int a3)
+{
+    if (!pSprite)
+        return;
+    for (int i = nBonkles - 1; i >= 0; i--)
+    {
+        BONKLE *pBonkle = BonkleCache[i];
+        if (pBonkle->at10 == pSprite && (a2 < 0 || a2 == pBonkle->at14) && (a3 < 0 || a3 == pBonkle->atc))
+        {
+            if (pBonkle->at0 > 0)
+            {
+                FX_EndLooping(pBonkle->at0);
+                FX_StopSound(pBonkle->at0);
+            }
+            if (pBonkle->at4 > 0)
+            {
+                FX_EndLooping(pBonkle->at4);
+                FX_StopSound(pBonkle->at4);
+            }
+            if (pBonkle->at8)
+            {
+                gSoundRes.Unlock(pBonkle->at8);
+                pBonkle->at8 = NULL;
+            }
+            BonkleCache[i] = BonkleCache[--nBonkles];
+            BonkleCache[nBonkles] = pBonkle;
+            break;
+        }
+    }
+}
+
+void sfxKillAllSounds(void)
+{
+    for (int i = nBonkles - 1; i >= 0; i--)
+    {
+        BONKLE *pBonkle = BonkleCache[i];
+        if (pBonkle->at0 > 0)
+        {
+            FX_EndLooping(pBonkle->at0);
+            FX_StopSound(pBonkle->at0);
+        }
+        if (pBonkle->at4 > 0)
+        {
+            FX_EndLooping(pBonkle->at4);
+            FX_StopSound(pBonkle->at4);
+        }
+        if (pBonkle->at8)
+        {
+            gSoundRes.Unlock(pBonkle->at8);
+            pBonkle->at8 = NULL;
+        }
+        BonkleCache[i] = BonkleCache[--nBonkles];
+        BonkleCache[nBonkles] = pBonkle;
+    }
+}
+
+void sfxUpdate3DSounds(void)
+{
+    int dx = mulscale30(Cos(gMe->pSprite->ang + 512), 43);
+    earL0 = earL;
+    int dy = mulscale30(Sin(gMe->pSprite->ang + 512), 43);
+    earR0 = earR;
+    earL.x = gMe->pSprite->x - dx;
+    earL.y = gMe->pSprite->y - dy;
+    earR.x = gMe->pSprite->x + dx;
+    earR.y = gMe->pSprite->y + dy;
+    earVL.dx = earL.x - earL0.x;
+    earVL.dy = earL.y - earL0.y;
+    earVR.dx = earR.x - earR0.x;
+    earVR.dy = earR.y - earR0.y;
+    for (int i = nBonkles - 1; i >= 0; i--)
+    {
+        BONKLE *pBonkle = BonkleCache[i];
+        if (pBonkle->at0 > 0 || pBonkle->at4 > 0)
+        {
+            if (!pBonkle->at8)
+                continue;
+            if (pBonkle->at10)
+            {
+                pBonkle->at2c = pBonkle->at20;
+                pBonkle->at20.x = pBonkle->at10->x;
+                pBonkle->at20.y = pBonkle->at10->y;
+                pBonkle->at20.z = pBonkle->at10->z;
+                pBonkle->at38 = pBonkle->at10->sectnum;
+            }
+            Calc3DValues(pBonkle);
+            DisableInterrupts();
+            if (pBonkle->at0 > 0)
+            {
+                if (pBonkle->at4 > 0)
+                {
+                    FX_SetPan(pBonkle->at0, lVol, lVol, 0);
+                    FX_SetFrequency(pBonkle->at0, lPitch);
+                }
+                else
+                    FX_SetPan(pBonkle->at0, lVol, lVol, rVol);
+            }
+            if (pBonkle->at4 > 0)
+            {
+                FX_SetPan(pBonkle->at4, rVol, 0, rVol);
+                FX_SetFrequency(pBonkle->at4, rPitch);
+            }
+            RestoreInterrupts();
+        }
+        else
+        {
+            gSoundRes.Unlock(pBonkle->at8);
+            pBonkle->at8 = NULL;
+            BonkleCache[i] = BonkleCache[--nBonkles];
+            BonkleCache[nBonkles] = pBonkle;
+        }
+    }
+}
+
+void sfxSetReverb(bool toggle)
+{
+    if (toggle)
+    {
+        FX_SetReverb(128);
+        FX_SetReverbDelay(10);
+    }
+    else
+        FX_SetReverb(0);
+}
+
+void sfxSetReverb2(bool toggle)
+{
+    if (toggle)
+    {
+        FX_SetReverb(128);
+        FX_SetReverbDelay(20);
+    }
+    else
+        FX_SetReverb(0);
+}
diff --git a/source/blood/src/sfx.h b/source/blood/src/sfx.h
new file mode 100644
index 000000000..3545118ac
--- /dev/null
+++ b/source/blood/src/sfx.h
@@ -0,0 +1,35 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "db.h"
+
+void sfxInit(void);
+void sfxTerm(void);
+void sfxPlay3DSound(int x, int y, int z, int soundId, int nSector);
+void sfxPlay3DSound(spritetype *pSprite, int soundId, int a3 = -1, int a4 = 0);
+void sfxPlay3DSoundCP(spritetype* pSprite, int soundId, int a3 = -1, int a4 = 0, int pitch = 0, int volume = 0);
+void sfxKill3DSound(spritetype *pSprite, int a2 = -1, int a3 = -1);
+void sfxKillAllSounds(void);
+void sfxUpdate3DSounds(void);
+void sfxSetReverb(bool toggle);
+void sfxSetReverb2(bool toggle);
\ No newline at end of file
diff --git a/source/blood/src/sound.cpp b/source/blood/src/sound.cpp
new file mode 100644
index 000000000..e94448de4
--- /dev/null
+++ b/source/blood/src/sound.cpp
@@ -0,0 +1,511 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#include "music.h"
+#include "fx_man.h"
+#include "common_game.h"
+#include "config.h"
+#include "levels.h"
+#include "resource.h"
+#include "sound.h"
+#include "renderlayer.h"
+
+Resource gSoundRes;
+
+int soundRates[13] = {
+    11025,
+    11025,
+    11025,
+    11025,
+    11025,
+    22050,
+    22050,
+    22050,
+    22050,
+    44100,
+    44100,
+    44100,
+    44100,
+};
+#define kMaxChannels 32
+
+int sndGetRate(int format)
+{
+    if (format < 13)
+        return soundRates[format];
+    return 11025;
+}
+
+SAMPLE2D Channel[kMaxChannels];
+
+SAMPLE2D * FindChannel(void)
+{
+    for (int i = kMaxChannels - 1; i >= 0; i--)
+        if (Channel[i].at5 == 0)
+            return &Channel[i];
+    ThrowError("No free channel available for sample");
+    return NULL;
+}
+
+DICTNODE *hSong;
+char *pSongPtr;
+int nSongSize;
+bool bWaveMusic;
+int nWaveMusicHandle;
+
+int sndPlaySong(const char *songName, bool bLoop)
+{
+    if (!MusicToggle)
+        return 0;
+    if (!songName || strlen(songName) == 0)
+        return 1;
+
+    int32_t fp = S_OpenAudio(songName, 0, 1);
+    if (EDUKE32_PREDICT_FALSE(fp < 0))
+    {
+        hSong = gSoundRes.Lookup(songName, "MID");
+        if (!hSong)
+        {
+            OSD_Printf(OSD_ERROR "sndPlaySong(): error: can't open \"%s\" for playback!\n", songName);
+            return 2;
+        }
+        int nNewSongSize = hSong->size;
+        char *pNewSongPtr = (char *)Xaligned_alloc(16, nNewSongSize);
+        gSoundRes.Load(hSong, pNewSongPtr);
+        MUSIC_SetVolume(MusicVolume);
+        int32_t retval = MUSIC_PlaySong(pNewSongPtr, nNewSongSize, bLoop);
+
+        if (retval != MUSIC_Ok)
+        {
+            ALIGNED_FREE_AND_NULL(pNewSongPtr);
+            return 5;
+        }
+
+        if (bWaveMusic && nWaveMusicHandle >= 0)
+        {
+            FX_StopSound(nWaveMusicHandle);
+            nWaveMusicHandle = -1;
+        }
+
+        bWaveMusic = false;
+        ALIGNED_FREE_AND_NULL(pSongPtr);
+        pSongPtr = pNewSongPtr;
+        nSongSize = nNewSongSize;
+        return 0;
+    }
+
+    int32_t nSongLen = kfilelength(fp);
+
+    if (EDUKE32_PREDICT_FALSE(nSongLen < 4))
+    {
+        OSD_Printf(OSD_ERROR "sndPlaySong(): error: empty music file \"%s\"\n", songName);
+        kclose(fp);
+        return 3;
+    }
+
+    char * pNewSongPtr = (char *)Xaligned_alloc(16, nSongLen);
+    int nNewSongSize = kread(fp, pNewSongPtr, nSongLen);
+
+    if (EDUKE32_PREDICT_FALSE(nNewSongSize != nSongLen))
+    {
+        OSD_Printf(OSD_ERROR "sndPlaySong(): error: read %d bytes from \"%s\", expected %d\n",
+            nNewSongSize, songName, nSongLen);
+        kclose(fp);
+        ALIGNED_FREE_AND_NULL(pNewSongPtr);
+        return 4;
+    }
+
+    kclose(fp);
+
+    if (!Bmemcmp(pNewSongPtr, "MThd", 4))
+    {
+        int32_t retval = MUSIC_PlaySong(pNewSongPtr, nNewSongSize, bLoop);
+
+        if (retval != MUSIC_Ok)
+        {
+            ALIGNED_FREE_AND_NULL(pNewSongPtr);
+            return 5;
+        }
+
+        if (bWaveMusic && nWaveMusicHandle >= 0)
+        {
+            FX_StopSound(nWaveMusicHandle);
+            nWaveMusicHandle = -1;
+        }
+
+        bWaveMusic = false;
+        ALIGNED_FREE_AND_NULL(pSongPtr);
+        pSongPtr = pNewSongPtr;
+        nSongSize = nNewSongSize;
+    }
+    else
+    {
+        int nNewWaveMusicHandle = FX_Play(pNewSongPtr, bLoop ? nNewSongSize : -1, 0, 0, 0, MusicVolume, MusicVolume, MusicVolume,
+                                   FX_MUSIC_PRIORITY, 1.f, (intptr_t)&nWaveMusicHandle);
+
+        if (nNewWaveMusicHandle <= FX_Ok)
+        {
+            ALIGNED_FREE_AND_NULL(pNewSongPtr);
+            return 5;
+        }
+
+        if (bWaveMusic && nWaveMusicHandle >= 0)
+            FX_StopSound(nWaveMusicHandle);
+
+        MUSIC_StopSong();
+
+        nWaveMusicHandle = nNewWaveMusicHandle;
+        bWaveMusic = true;
+        ALIGNED_FREE_AND_NULL(pSongPtr);
+        pSongPtr = pNewSongPtr;
+        nSongSize = nNewSongSize;
+    }
+
+    return 0;
+}
+
+int sndTryPlaySpecialMusic(int nMusic)
+{
+    int nEpisode = nMusic/kMaxLevels;
+    int nLevel = nMusic%kMaxLevels;
+    if (!sndPlaySong(gEpisodeInfo[nEpisode].at28[nLevel].atd0, true))
+    {
+        strncpy(gGameOptions.zLevelSong, gEpisodeInfo[nEpisode].at28[nLevel].atd0, BMAX_PATH);
+        return 0;
+    }
+    return 1;
+}
+
+void sndPlaySpecialMusicOrNothing(int nMusic)
+{
+    int nEpisode = nMusic/kMaxLevels;
+    int nLevel = nMusic%kMaxLevels;
+    if (sndTryPlaySpecialMusic(nMusic))
+    {
+        sndStopSong();
+        strncpy(gGameOptions.zLevelSong, gEpisodeInfo[nEpisode].at28[nLevel].atd0, BMAX_PATH);
+    }
+}
+
+bool sndIsSongPlaying(void)
+{
+    //return MUSIC_SongPlaying();
+    return false;
+}
+
+void sndFadeSong(int nTime)
+{
+    UNREFERENCED_PARAMETER(nTime);
+    // NUKE-TODO:
+    //if (MusicDevice == -1)
+    //    return;
+    //if (gEightyTwoFifty && sndMultiPlayer)
+    //    return;
+    //MUSIC_FadeVolume(0, nTime);
+    if (bWaveMusic && nWaveMusicHandle >= 0)
+    {
+        FX_StopSound(nWaveMusicHandle);
+        nWaveMusicHandle = -1;
+        bWaveMusic = false;
+    }
+    MUSIC_SetVolume(0);
+    MUSIC_StopSong();
+}
+
+void sndSetMusicVolume(int nVolume)
+{
+    MusicVolume = nVolume;
+    if (bWaveMusic && nWaveMusicHandle >= 0)
+        FX_SetPan(nWaveMusicHandle, nVolume, nVolume, nVolume);
+    MUSIC_SetVolume(nVolume);
+}
+
+void sndSetFXVolume(int nVolume)
+{
+    FXVolume = nVolume;
+    FX_SetVolume(nVolume);
+}
+
+void sndStopSong(void)
+{
+    if (bWaveMusic && nWaveMusicHandle >= 0)
+    {
+        FX_StopSound(nWaveMusicHandle);
+        nWaveMusicHandle = -1;
+        bWaveMusic = false;
+    }
+
+    MUSIC_StopSong();
+
+    ALIGNED_FREE_AND_NULL(pSongPtr);
+    nSongSize = 0;
+}
+
+void SoundCallback(intptr_t val)
+{
+    SAMPLE2D *pChannel = (SAMPLE2D*)val;
+    pChannel->at0 = 0;
+}
+
+void sndKillSound(SAMPLE2D *pChannel);
+
+void sndStartSample(const char *pzSound, int nVolume, int nChannel)
+{
+    if (!SoundToggle)
+        return;
+    if (!strlen(pzSound))
+        return;
+    dassert(nChannel >= -1 && nChannel < kMaxChannels);
+    SAMPLE2D *pChannel;
+    if (nChannel == -1)
+        pChannel = FindChannel();
+    else
+        pChannel = &Channel[nChannel];
+    if (pChannel->at0 > 0)
+        sndKillSound(pChannel);
+    pChannel->at5 = gSoundRes.Lookup(pzSound, "RAW");
+    if (!pChannel->at5)
+        return;
+    int nSize = pChannel->at5->size;
+    char *pData = (char*)gSoundRes.Lock(pChannel->at5);
+    pChannel->at0 = FX_PlayRaw(pData, nSize, sndGetRate(1), 0, nVolume, nVolume, nVolume, nVolume, 1.f, (intptr_t)&pChannel->at0);
+}
+
+void sndStartSample(unsigned int nSound, int nVolume, int nChannel, bool bLoop)
+{
+    if (!SoundToggle)
+        return;
+    dassert(nChannel >= -1 && nChannel < kMaxChannels);
+    DICTNODE *hSfx = gSoundRes.Lookup(nSound, "SFX");
+    if (!hSfx)
+        return;
+    SFX *pEffect = (SFX*)gSoundRes.Lock(hSfx);
+    dassert(pEffect != NULL);
+    SAMPLE2D *pChannel;
+    if (nChannel == -1)
+        pChannel = FindChannel();
+    else
+        pChannel = &Channel[nChannel];
+    if (pChannel->at0 > 0)
+        sndKillSound(pChannel);
+    pChannel->at5 = gSoundRes.Lookup(pEffect->rawName, "RAW");
+    if (!pChannel->at5)
+        return;
+    if (nVolume < 0)
+        nVolume = pEffect->relVol;
+    int nSize = pChannel->at5->size;
+    int nLoopEnd = nSize - 1;
+    if (nLoopEnd < 0)
+        nLoopEnd = 0;
+    if (nSize <= 0)
+        return;
+    char *pData = (char*)gSoundRes.Lock(pChannel->at5);
+    if (nChannel < 0)
+        bLoop = false;
+    if (bLoop)
+    {
+        pChannel->at0 = FX_PlayLoopedRaw(pData, nSize, pData + pEffect->loopStart, pData + nLoopEnd, sndGetRate(pEffect->format),
+            0, nVolume, nVolume, nVolume, nVolume, 1.f, (intptr_t)&pChannel->at0);
+        pChannel->at4 |= 1;
+    }
+    else
+    {
+        pChannel->at0 = FX_PlayRaw(pData, nSize, sndGetRate(pEffect->format), 0, nVolume, nVolume, nVolume, nVolume, 1.f, (intptr_t)&pChannel->at0);
+        pChannel->at4 &= ~1;
+    }
+}
+
+void sndStartWavID(unsigned int nSound, int nVolume, int nChannel)
+{
+    if (!SoundToggle)
+        return;
+    dassert(nChannel >= -1 && nChannel < kMaxChannels);
+    SAMPLE2D *pChannel;
+    if (nChannel == -1)
+        pChannel = FindChannel();
+    else
+        pChannel = &Channel[nChannel];
+    if (pChannel->at0 > 0)
+        sndKillSound(pChannel);
+    pChannel->at5 = gSoundRes.Lookup(nSound, "WAV");
+    if (!pChannel->at5)
+        return;
+    char *pData = (char*)gSoundRes.Lock(pChannel->at5);
+    pChannel->at0 = FX_Play(pData, pChannel->at5->size, 0, -1, 0, nVolume, nVolume, nVolume, nVolume, 1.f, (intptr_t)&pChannel->at0);
+}
+
+void sndKillSound(SAMPLE2D *pChannel)
+{
+    if (pChannel->at4 & 1)
+    {
+        FX_EndLooping(pChannel->at0);
+        pChannel->at4 &= ~1;
+    }
+    FX_StopSound(pChannel->at0);
+}
+
+void sndStartWavDisk(const char *pzFile, int nVolume, int nChannel)
+{
+    dassert(nChannel >= -1 && nChannel < kMaxChannels);
+    SAMPLE2D *pChannel;
+    if (nChannel == -1)
+        pChannel = FindChannel();
+    else
+        pChannel = &Channel[nChannel];
+    if (pChannel->at0 > 0)
+        sndKillSound(pChannel);
+    int hFile = kopen4loadfrommod(pzFile, 0);
+    if (hFile == -1)
+        return;
+    int nLength = kfilelength(hFile);
+    char *pData = (char*)gSoundRes.Alloc(nLength);
+    if (!pData)
+    {
+        kclose(hFile);
+        return;
+    }
+    kread(hFile, pData, kfilelength(hFile));
+    kclose(hFile);
+    pChannel->at5 = (DICTNODE*)pData;
+    pChannel->at4 |= 2;
+    pChannel->at0 = FX_Play(pData, nLength, 0, -1, 0, nVolume, nVolume, nVolume, nVolume, 1.f, (intptr_t)&pChannel->at0);
+}
+
+void sndKillAllSounds(void)
+{
+    for (int i = 0; i < kMaxChannels; i++)
+    {
+        SAMPLE2D *pChannel = &Channel[i];
+        if (pChannel->at0 > 0)
+            sndKillSound(pChannel);
+        if (pChannel->at5)
+        {
+            if (pChannel->at4 & 2)
+            {
+#if 0
+                free(pChannel->at5);
+#else
+                gSoundRes.Free(pChannel->at5);
+#endif
+                pChannel->at4 &= ~2;
+            }
+            else
+            {
+                gSoundRes.Unlock(pChannel->at5);
+            }
+            pChannel->at5 = 0;
+        }
+    }
+}
+
+void sndProcess(void)
+{
+    for (int i = 0; i < kMaxChannels; i++)
+    {
+        if (Channel[i].at0 <= 0 && Channel[i].at5)
+        {
+            if (Channel[i].at4 & 2)
+            {
+                gSoundRes.Free(Channel[i].at5);
+                Channel[i].at4 &= ~2;
+            }
+            else
+            {
+                gSoundRes.Unlock(Channel[i].at5);
+            }
+            Channel[i].at5 = 0;
+        }
+    }
+}
+
+void InitSoundDevice(void)
+{
+#ifdef MIXERTYPEWIN
+    void *initdata = (void *)win_gethwnd(); // used for DirectSound
+#else
+    void *initdata = NULL;
+#endif
+    int nStatus;
+    nStatus = FX_Init(NumVoices, NumChannels, MixRate, initdata);
+    if (nStatus != 0)
+    {
+        initprintf("InitSoundDevice: %s\n", FX_ErrorString(nStatus));
+        return;
+    }
+    if (ReverseStereo == 1)
+        FX_SetReverseStereo(!FX_GetReverseStereo());
+    FX_SetVolume(FXVolume);
+    FX_SetCallBack(SoundCallback);
+}
+
+void DeinitSoundDevice(void)
+{
+    int nStatus = FX_Shutdown();
+    if (nStatus != 0)
+        ThrowError(FX_ErrorString(nStatus));
+}
+
+void InitMusicDevice(void)
+{
+    int nStatus = MUSIC_Init(MusicDevice, 0);
+    if (nStatus != 0)
+    {
+        initprintf("InitMusicDevice: %s\n", MUSIC_ErrorString(nStatus));
+        return;
+    }
+    MUSIC_SetVolume(MusicVolume);
+}
+
+void DeinitMusicDevice(void)
+{
+    FX_StopAllSounds();
+    int nStatus = MUSIC_Shutdown();
+    if (nStatus != 0)
+        ThrowError(MUSIC_ErrorString(nStatus));
+}
+
+bool sndActive = false;
+
+void sndTerm(void)
+{
+    if (!sndActive)
+        return;
+    sndActive = false;
+    sndStopSong();
+    DeinitSoundDevice();
+    DeinitMusicDevice();
+}
+extern char *pUserSoundRFF;
+void sndInit(void)
+{
+    memset(Channel, 0, sizeof(Channel));
+    pSongPtr = NULL;
+    nSongSize = 0;
+    bWaveMusic = false;
+    nWaveMusicHandle = -1;
+    InitSoundDevice();
+    InitMusicDevice();
+    //atexit(sndTerm);
+    sndActive = true;
+}
diff --git a/source/blood/src/sound.h b/source/blood/src/sound.h
new file mode 100644
index 000000000..931b29fdc
--- /dev/null
+++ b/source/blood/src/sound.h
@@ -0,0 +1,61 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "resource.h"
+struct SAMPLE2D
+{
+    int at0;
+    char at4;
+    DICTNODE *at5;
+}; // 9 bytes
+
+struct SFX
+{
+    int relVol;
+    int pitch;
+    int pitchRange;
+    int format;
+    int loopStart;
+    char rawName[9];
+};
+
+int sndGetRate(int format);
+int sndPlaySong(const char *songName, bool bLoop);
+int sndTryPlaySpecialMusic(int nMusic);
+void sndPlaySpecialMusicOrNothing(int nMusic);
+bool sndIsSongPlaying(void);
+void sndFadeSong(int nTime);
+void sndSetMusicVolume(int nVolume);
+void sndSetFXVolume(int nVolume);
+void sndStopSong(void);
+void sndStartSample(const char *pzSound, int nVolume, int nChannel = -1);
+void sndStartSample(unsigned int nSound, int nVolume, int nChannel = -1, bool bLoop = false);
+void sndStartWavID(unsigned int nSound, int nVolume, int nChannel = -1);
+void sndStartWavDisk(const char *pzFile, int nVolume, int nChannel = -1);
+void sndKillAllSounds(void);
+void sndProcess(void);
+void sndTerm(void);
+void sndInit(void);
+
+extern Resource gSoundRes;
diff --git a/source/blood/src/startgtk.game.cpp b/source/blood/src/startgtk.game.cpp
new file mode 100644
index 000000000..997e1a5ad
--- /dev/null
+++ b/source/blood/src/startgtk.game.cpp
@@ -0,0 +1,746 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010 EDuke32 developers and contributors
+
+This file is part of EDuke32.
+
+EDuke32 is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#include "build.h"
+#include "common.h"
+#include "common_game.h"
+#include "compat.h"
+#include "dynamicgtk.h"
+#include "blood.h"
+#include "gtkpixdata.h"
+
+enum
+{
+    NONE,
+    ALL,
+    POPULATE_VIDEO,
+    POPULATE_CONFIG,
+    POPULATE_GAME,
+};
+
+enum
+{
+    TAB_CONFIG,
+    TAB_GAME,
+    TAB_MESSAGES,
+};
+
+enum
+{
+    INPUT_KB,
+    INPUT_MOUSE,
+    INPUT_JOYSTICK,
+    INPUT_ALL,
+};
+
+static struct
+{
+    GtkWidget *startwin;
+    GtkWidget *hlayout;
+    GtkWidget *banner;
+    GtkWidget *vlayout;
+    GtkWidget *tabs;
+    GtkWidget *configtlayout;
+    GtkWidget *displayvlayout;
+    GtkWidget *vmode3dlabel;
+    GtkWidget *vmode3dcombo;
+    GtkWidget *fullscreencheck;
+    GtkWidget *inputdevlabel;
+    GtkWidget *inputdevcombo;
+    GtkWidget *custommodlabel;
+    GtkWidget *custommodcombo;
+    GtkWidget *emptyhlayout;
+    GtkWidget *autoloadcheck;
+    GtkWidget *alwaysshowcheck;
+    GtkWidget *configtab;
+    GtkWidget *messagesscroll;
+    GtkWidget *messagestext;
+    GtkWidget *messagestab;
+    GtkWidget *buttons;
+    GtkWidget *cancelbutton;
+    GtkWidget *cancelbuttonalign;
+    GtkWidget *cancelbuttonlayout;
+    GtkWidget *cancelbuttonicon;
+    GtkWidget *cancelbuttonlabel;
+    GtkWidget *startbutton;
+    GtkWidget *startbuttonalign;
+    GtkWidget *startbuttonlayout;
+    GtkWidget *startbuttonicon;
+    GtkWidget *startbuttonlabel;
+} stwidgets;
+
+static struct
+{
+    char *gamedir;
+    ud_setup_t shared;
+#ifdef POLYMER
+    int polymer;
+#endif
+} settings;
+
+static int32_t retval = -1, mode = TAB_MESSAGES;
+extern int32_t gtkenabled;
+static void PopulateForm(unsigned char pgs);
+
+
+// -- EVENT CALLBACKS AND CREATION STUFF --------------------------------------
+
+static void on_vmode3dcombo_changed(GtkComboBox *combobox, gpointer user_data)
+{
+    GtkTreeModel *data;
+    GtkTreeIter iter;
+    int32_t val;
+    UNREFERENCED_PARAMETER(user_data);
+
+    if (!gtk_combo_box_get_active_iter(combobox, &iter)) return;
+    if (!(data = gtk_combo_box_get_model(combobox))) return;
+    gtk_tree_model_get(data, &iter, 1, &val, -1);
+    settings.shared.xdim = validmode[val].xdim;
+    settings.shared.ydim = validmode[val].ydim;
+    settings.shared.bpp = validmode[val].bpp;
+}
+
+static void on_fullscreencheck_toggled(GtkToggleButton *togglebutton, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(user_data);
+    settings.shared.fullscreen = gtk_toggle_button_get_active(togglebutton);
+    PopulateForm(POPULATE_VIDEO);
+}
+
+static void on_inputdevcombo_changed(GtkComboBox *combobox, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(user_data);
+    switch (gtk_combo_box_get_active(combobox))
+    {
+    case 0: settings.shared.usemouse = 0; settings.shared.usejoystick = 0; break;
+    case 1:	settings.shared.usemouse = 1; settings.shared.usejoystick = 0; break;
+    case 2:	settings.shared.usemouse = 0; settings.shared.usejoystick = 1; break;
+    case 3:	settings.shared.usemouse = 1; settings.shared.usejoystick = 1; break;
+    }
+}
+
+static void on_custommodcombo_changed(GtkComboBox *combobox, gpointer user_data)
+{
+    GtkTreeIter iter;
+    GtkTreeModel *model;
+    GtkTreePath *path;
+    char *value;
+    UNREFERENCED_PARAMETER(user_data);
+
+    if (gtk_combo_box_get_active_iter(combobox, &iter))
+    {
+        model = gtk_combo_box_get_model(combobox);
+        gtk_tree_model_get(model, &iter, 0,&value, -1);
+        path = gtk_tree_model_get_path(model, &iter);
+
+        if (*gtk_tree_path_get_indices(path) == NONE)
+            settings.gamedir = NULL;
+        else settings.gamedir = value;
+    }
+}
+
+static void on_autoloadcheck_toggled(GtkToggleButton *togglebutton, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(user_data);
+    settings.shared.noautoload = !gtk_toggle_button_get_active(togglebutton);
+}
+
+static void on_alwaysshowcheck_toggled(GtkToggleButton *togglebutton, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(user_data);
+    settings.shared.forcesetup = gtk_toggle_button_get_active(togglebutton);
+}
+
+static void on_cancelbutton_clicked(GtkButton *button, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(button);
+    UNREFERENCED_PARAMETER(user_data);
+    if (mode == TAB_CONFIG) { retval = 0; gtk_main_quit(); }
+    else quitevent++;
+}
+
+static void on_startbutton_clicked(GtkButton *button, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(button);
+    UNREFERENCED_PARAMETER(user_data);
+    retval = 1;
+    gtk_main_quit();
+}
+
+static gboolean on_startwin_delete_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+    UNREFERENCED_PARAMETER(widget);
+    UNREFERENCED_PARAMETER(event);
+    UNREFERENCED_PARAMETER(user_data);
+    if (mode == TAB_CONFIG) { retval = 0; gtk_main_quit(); }
+    else quitevent++;
+    return TRUE;	// FALSE would let the event go through. we want the game to decide when to close
+}
+
+
+// -- SUPPORT FUNCTIONS -------------------------------------------------------
+
+static GdkPixbuf *load_banner(void)
+{
+    return gdk_pixbuf_from_pixdata((GdkPixdata const *)&startbanner_pixdata, FALSE, NULL);
+}
+
+static void SetPage(int32_t n)
+{
+    if (!gtkenabled || !stwidgets.startwin) return;
+    mode = n;
+    gtk_notebook_set_current_page(GTK_NOTEBOOK(stwidgets.tabs), n);
+
+    // each control in the config page vertical layout plus the start button should be made (in)sensitive
+    if (n == TAB_CONFIG) n = TRUE; else n = FALSE;
+    gtk_widget_set_sensitive(stwidgets.startbutton, n);
+    gtk_container_foreach(GTK_CONTAINER(stwidgets.configtlayout),
+                          (GtkCallback)gtk_widget_set_sensitive,
+                          (gpointer)&n);
+}
+
+static unsigned char GetModsDirNames(GtkListStore *list)
+{
+    char *homedir;
+    char pdir[BMAX_PATH];
+    unsigned char iternumb = 0;
+    CACHE1D_FIND_REC *dirs = NULL;
+    GtkTreeIter iter;
+
+    pathsearchmode = 1;
+
+    if ((homedir = Bgethomedir()))
+    {
+        Bsnprintf(pdir, sizeof(pdir), "%s/" ".blood", homedir);
+        dirs = klistpath(pdir, "*", CACHE1D_FIND_DIR);
+        for (; dirs != NULL; dirs=dirs->next)
+        {
+            if ((Bstrcmp(dirs->name, "autoload") == 0) ||
+                    (Bstrcmp(dirs->name, "..") == 0) ||
+                    (Bstrcmp(dirs->name, ".") == 0))
+                continue;
+            else
+            {
+                gtk_list_store_append(list, &iter);
+                gtk_list_store_set(list, &iter, 0,dirs->name, -1);
+                iternumb++;
+            }
+        }
+    }
+
+    klistfree(dirs);
+    dirs = NULL;
+
+    return iternumb;
+}
+
+static void PopulateForm(unsigned char pgs)
+{
+    if ((pgs == ALL) || (pgs == POPULATE_VIDEO))
+    {
+        int32_t mode3d, i;
+        GtkListStore *modes3d;
+        GtkTreeIter iter;
+        char buf[64];
+
+        mode3d = videoCheckMode(&settings.shared.xdim, &settings.shared.ydim, settings.shared.bpp, settings.shared.fullscreen, 1);
+        if (mode3d < 0)
+        {
+            int32_t i, cd[] = { 32, 24, 16, 15, 8, 0 };
+
+            for (i=0; cd[i];) { if (cd[i] >= settings.shared.bpp) i++; else break; }
+            for (; cd[i]; i++)
+            {
+                mode3d = videoCheckMode(&settings.shared.xdim, &settings.shared.ydim, cd[i], settings.shared.fullscreen, 1);
+                if (mode3d < 0) continue;
+                settings.shared.bpp = cd[i];
+                break;
+            }
+        }
+
+        modes3d = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(stwidgets.vmode3dcombo)));
+        gtk_list_store_clear(modes3d);
+
+        for (i=0; i<validmodecnt; i++)
+        {
+            if (validmode[i].fs != settings.shared.fullscreen) continue;
+
+            // all modes get added to the 3D mode list
+            Bsprintf(buf, "%dx%d %s", validmode[i].xdim, validmode[i].ydim, validmode[i].bpp == 8 ? "software" : "OpenGL");
+            gtk_list_store_append(modes3d, &iter);
+            gtk_list_store_set(modes3d, &iter, 0,buf, 1,i, -1);
+            if (i == mode3d)
+            {
+                g_signal_handlers_block_by_func(stwidgets.vmode3dcombo, (gpointer)on_vmode3dcombo_changed, NULL);
+                gtk_combo_box_set_active_iter(GTK_COMBO_BOX(stwidgets.vmode3dcombo), &iter);
+                g_signal_handlers_unblock_by_func(stwidgets.vmode3dcombo, (gpointer)on_vmode3dcombo_changed, NULL);
+            }
+        }
+    }
+
+    if ((pgs == ALL) || (pgs == POPULATE_CONFIG))
+    {
+        GtkListStore *devlist, *modsdir;
+        GtkTreeIter iter;
+        GtkTreePath *path;
+        char *value;
+        unsigned char i, r = 0;
+        const char *availabledev[] =
+        {
+            "Keyboard only",
+            "Keyboard and mouse",
+            "Keyboard and joystick",
+            "All supported devices"
+        };
+
+        // populate input devices combo
+        devlist = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(stwidgets.inputdevcombo)));
+        gtk_list_store_clear(devlist);
+
+        for (i=0; i<(int32_t)G_N_ELEMENTS(availabledev); i++)
+        {
+            gtk_list_store_append(devlist, &iter);
+            gtk_list_store_set(devlist, &iter, 0,availabledev[i], -1);
+        }
+        switch (settings.shared.usemouse)
+        {
+        case 0: if (settings.shared.usejoystick)
+                gtk_combo_box_set_active(GTK_COMBO_BOX(stwidgets.inputdevcombo), INPUT_JOYSTICK);
+            else
+                gtk_combo_box_set_active(GTK_COMBO_BOX(stwidgets.inputdevcombo), INPUT_KB);
+            break;
+        case 1:	if (settings.shared.usejoystick)
+                gtk_combo_box_set_active(GTK_COMBO_BOX(stwidgets.inputdevcombo), INPUT_ALL);
+            else
+                gtk_combo_box_set_active(GTK_COMBO_BOX(stwidgets.inputdevcombo), INPUT_MOUSE);
+            break;
+        }
+
+        // populate custom mod combo
+        modsdir = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(stwidgets.custommodcombo)));
+        gtk_list_store_clear(modsdir);
+
+        gtk_list_store_append(modsdir, &iter);
+        gtk_list_store_set(modsdir, &iter, 0,"None", -1);
+        r = GetModsDirNames(modsdir);
+
+        for (i=0; i<=r; i++)
+        {
+            path = gtk_tree_path_new_from_indices(i, -1);
+            gtk_tree_model_get_iter(GTK_TREE_MODEL(modsdir), &iter, path);
+            gtk_tree_model_get(GTK_TREE_MODEL(modsdir), &iter, 0,&value, -1);
+
+            if (Bstrcmp(settings.gamedir, "/") == 0)
+            {
+                gtk_combo_box_set_active(GTK_COMBO_BOX(stwidgets.custommodcombo), NONE);
+                settings.gamedir = NULL;
+
+                break;
+            }
+            if (Bstrcmp(settings.gamedir, value) == 0)
+            {
+                gtk_combo_box_set_active_iter(GTK_COMBO_BOX(stwidgets.custommodcombo),
+                                              &iter);
+
+                break;
+            }
+        }
+
+        // populate check buttons
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(stwidgets.fullscreencheck), settings.shared.fullscreen);
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(stwidgets.autoloadcheck), !settings.shared.noautoload);
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(stwidgets.alwaysshowcheck), settings.shared.forcesetup);
+    }
+
+#if 0
+    if ((pgs == ALL) || (pgs == POPULATE_GAME))
+    {
+        GtkListStore *list;
+        GtkTreeIter iter;
+        GtkTreeView *gamelist;
+
+        gamelist = GTK_TREE_VIEW(stwidgets.gamelist);
+        list = GTK_LIST_STORE(gtk_tree_view_get_model(gamelist));
+        gtk_list_store_clear(list);
+
+        for (grpfile_t const * fg = foundgrps; fg; fg=fg->next)
+        {
+            gtk_list_store_append(list, &iter);
+            gtk_list_store_set(list, &iter, 0, fg->type->name, 1, fg->filename, 2, (void const *)fg, -1);
+            if (settings.grp == fg)
+            {
+                GtkTreeSelection *sel = gtk_tree_view_get_selection(gamelist);
+                g_signal_handlers_block_by_func(sel, (gpointer)on_gamelist_selection_changed, NULL);
+                gtk_tree_selection_select_iter(sel, &iter);
+                g_signal_handlers_unblock_by_func(sel, (gpointer)on_gamelist_selection_changed, NULL);
+            }
+        }
+    }
+#endif
+}
+
+static GtkWidget *create_window(void)
+{
+    // Basic window
+    stwidgets.startwin = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+    gtk_window_set_title(GTK_WINDOW(stwidgets.startwin), apptitle);	// NOTE: use global app title
+    gtk_window_set_position(GTK_WINDOW(stwidgets.startwin), GTK_WIN_POS_CENTER);
+    gtk_window_set_resizable(GTK_WINDOW(stwidgets.startwin), FALSE);
+    gtk_window_set_type_hint(GTK_WINDOW(stwidgets.startwin), GDK_WINDOW_TYPE_HINT_DIALOG);
+
+    // Horizontal layout of banner and controls
+    stwidgets.hlayout = gtk_hbox_new(FALSE, 0);
+    gtk_container_add(GTK_CONTAINER(stwidgets.startwin), stwidgets.hlayout);
+
+    // banner
+    {
+        GdkPixbuf *pixbuf = load_banner();
+        stwidgets.banner = gtk_image_new_from_pixbuf(pixbuf);
+        g_object_unref((gpointer)pixbuf);
+    }
+    gtk_box_pack_start(GTK_BOX(stwidgets.hlayout), stwidgets.banner, FALSE, FALSE, 0);
+    gtk_misc_set_alignment(GTK_MISC(stwidgets.banner), 0.5, 0);
+
+    // Vertical layout of tab control and start+cancel buttons
+    stwidgets.vlayout = gtk_vbox_new(FALSE, 0);
+    gtk_box_pack_start(GTK_BOX(stwidgets.hlayout), stwidgets.vlayout, TRUE, TRUE, 0);
+
+    // Tab control
+    stwidgets.tabs = gtk_notebook_new();
+    gtk_box_pack_start(GTK_BOX(stwidgets.vlayout), stwidgets.tabs, TRUE, TRUE, 0);
+    gtk_container_set_border_width(GTK_CONTAINER(stwidgets.tabs), 4);
+
+    // layout table of config page
+    stwidgets.configtlayout = gtk_table_new(6, 3, FALSE);
+    gtk_container_add(GTK_CONTAINER(stwidgets.tabs), stwidgets.configtlayout);
+
+    // 3D video mode LabelText
+    stwidgets.vmode3dlabel = gtk_label_new_with_mnemonic("_Video mode:");
+    gtk_misc_set_alignment(GTK_MISC(stwidgets.vmode3dlabel), 0.3, 0);
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.vmode3dlabel, 0,1, 0,1, GTK_FILL, (GtkAttachOptions)0, 4, 7);
+
+    // 3D video mode combo
+    {
+        GtkListStore *list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT);
+        GtkCellRenderer *cell;
+
+        stwidgets.vmode3dcombo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list));
+        g_object_unref(G_OBJECT(list));
+
+        cell = gtk_cell_renderer_text_new();
+        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(stwidgets.vmode3dcombo), cell, FALSE);
+        gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(stwidgets.vmode3dcombo), cell, "text", 0, NULL);
+    }
+
+   gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.vmode3dcombo, 1,2, 0,1,
+       (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), (GtkAttachOptions)0, 4, 0);
+
+    // Fullscreen checkbox
+    stwidgets.displayvlayout = gtk_vbox_new(TRUE, 0);
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.displayvlayout, 2,3, 0,1, GTK_FILL, (GtkAttachOptions)0, 4, 0);
+
+    stwidgets.fullscreencheck = gtk_check_button_new_with_mnemonic("_Fullscreen");
+    gtk_box_pack_start(GTK_BOX(stwidgets.displayvlayout), stwidgets.fullscreencheck, FALSE, FALSE, 0);
+
+    // Input devices LabelText
+    stwidgets.inputdevlabel = gtk_label_new_with_mnemonic("_Input devices:");
+    gtk_misc_set_alignment(GTK_MISC(stwidgets.inputdevlabel), 0.3, 0);
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.inputdevlabel, 0,1, 1,2, GTK_FILL, (GtkAttachOptions)0, 4, 0);
+
+    // Input devices combo
+    {
+        GtkListStore *list = gtk_list_store_new(1, G_TYPE_STRING);
+        GtkCellRenderer *cell;
+
+        stwidgets.inputdevcombo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list));
+        g_object_unref(G_OBJECT(list));
+
+        cell = gtk_cell_renderer_text_new();
+        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(stwidgets.inputdevcombo), cell, FALSE);
+        gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(stwidgets.inputdevcombo), cell, "text", 0, NULL);
+    }
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.inputdevcombo, 1,2, 1,2,
+        (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), (GtkAttachOptions)0, 4, 0);
+
+    // Custom mod LabelText
+    stwidgets.custommodlabel = gtk_label_new_with_mnemonic("Custom _game:");
+    gtk_misc_set_alignment(GTK_MISC(stwidgets.custommodlabel), 0.3, 0);
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.custommodlabel, 0,1, 2,3, GTK_FILL, (GtkAttachOptions)0, 4, 7);
+
+    // Custom mod combo
+    {
+        GtkListStore *list = gtk_list_store_new(1, G_TYPE_STRING);
+        GtkCellRenderer *cell;
+
+        stwidgets.custommodcombo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list));
+        g_object_unref(G_OBJECT(list));
+
+        cell = gtk_cell_renderer_text_new();
+        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(stwidgets.custommodcombo), cell, FALSE);
+        gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(stwidgets.custommodcombo), cell, "text", 0, NULL);
+    }
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.custommodcombo, 1,2, 2,3,
+        (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), (GtkAttachOptions)0, 4, 7);
+
+    // Empty horizontal layout
+    stwidgets.emptyhlayout = gtk_hbox_new(TRUE, 0);
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.emptyhlayout, 0,3, 3,4, (GtkAttachOptions)0,
+        (GtkAttachOptions)(GTK_EXPAND | GTK_FILL), 4, 0);
+
+    // Autoload checkbox
+    stwidgets.autoloadcheck = gtk_check_button_new_with_mnemonic("_Enable \"autoload\" folder");
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.autoloadcheck, 0,3, 4,5, GTK_FILL, (GtkAttachOptions)0, 2, 2);
+
+    // Always show config checkbox
+    stwidgets.alwaysshowcheck = gtk_check_button_new_with_mnemonic("_Always show this window at startup");
+    gtk_table_attach(GTK_TABLE(stwidgets.configtlayout), stwidgets.alwaysshowcheck, 0,3, 5,6, GTK_FILL, (GtkAttachOptions)0, 2, 2);
+
+    // Configuration tab
+    stwidgets.configtab = gtk_label_new("Configuration");
+    gtk_notebook_set_tab_label(GTK_NOTEBOOK(stwidgets.tabs), gtk_notebook_get_nth_page(GTK_NOTEBOOK(stwidgets.tabs), 0), stwidgets.configtab);
+
+    // Messages scrollable area
+    stwidgets.messagesscroll = gtk_scrolled_window_new(NULL, NULL);
+    gtk_container_add(GTK_CONTAINER(stwidgets.tabs), stwidgets.messagesscroll);
+    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(stwidgets.messagesscroll), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
+
+    // Messages text area
+    stwidgets.messagestext = gtk_text_view_new();
+    gtk_container_add(GTK_CONTAINER(stwidgets.messagesscroll), stwidgets.messagestext);
+    gtk_text_view_set_editable(GTK_TEXT_VIEW(stwidgets.messagestext), FALSE);
+    gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(stwidgets.messagestext), GTK_WRAP_WORD);
+    gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(stwidgets.messagestext), FALSE);
+    gtk_text_view_set_left_margin(GTK_TEXT_VIEW(stwidgets.messagestext), 2);
+    gtk_text_view_set_right_margin(GTK_TEXT_VIEW(stwidgets.messagestext), 2);
+
+    // Messages tab
+    stwidgets.messagestab = gtk_label_new("Messages");
+    gtk_notebook_set_tab_label(GTK_NOTEBOOK(stwidgets.tabs), gtk_notebook_get_nth_page(GTK_NOTEBOOK(stwidgets.tabs), 1), stwidgets.messagestab);
+
+    // Dialogue box buttons layout
+    stwidgets.buttons = gtk_hbutton_box_new();
+    gtk_box_pack_start(GTK_BOX(stwidgets.vlayout), stwidgets.buttons, FALSE, TRUE, 0);
+    gtk_container_set_border_width(GTK_CONTAINER(stwidgets.buttons), 3);
+    gtk_button_box_set_layout(GTK_BUTTON_BOX(stwidgets.buttons), GTK_BUTTONBOX_END);
+
+    // Cancel button
+    stwidgets.cancelbutton = gtk_button_new();
+    gtk_container_add(GTK_CONTAINER(stwidgets.buttons), stwidgets.cancelbutton);
+    GTK_WIDGET_SET_FLAGS(stwidgets.cancelbutton, GTK_CAN_DEFAULT);
+
+    stwidgets.cancelbuttonalign = gtk_alignment_new(0.5, 0.5, 0, 0);
+    gtk_container_add(GTK_CONTAINER(stwidgets.cancelbutton), stwidgets.cancelbuttonalign);
+
+    stwidgets.cancelbuttonlayout = gtk_hbox_new(FALSE, 2);
+    gtk_container_add(GTK_CONTAINER(stwidgets.cancelbuttonalign), stwidgets.cancelbuttonlayout);
+
+    stwidgets.cancelbuttonicon = gtk_image_new_from_stock("gtk-cancel", GTK_ICON_SIZE_BUTTON);
+    gtk_box_pack_start(GTK_BOX(stwidgets.cancelbuttonlayout), stwidgets.cancelbuttonicon, FALSE, FALSE, 0);
+
+    stwidgets.cancelbuttonlabel = gtk_label_new_with_mnemonic("_Cancel");
+    gtk_box_pack_start(GTK_BOX(stwidgets.cancelbuttonlayout), stwidgets.cancelbuttonlabel, FALSE, FALSE, 0);
+
+    // Start button
+    stwidgets.startbutton = gtk_button_new();
+    gtk_container_add(GTK_CONTAINER(stwidgets.buttons), stwidgets.startbutton);
+    GTK_WIDGET_SET_FLAGS(stwidgets.startbutton, GTK_CAN_DEFAULT);
+
+    gtk_window_set_default(GTK_WINDOW(stwidgets.startwin), stwidgets.startbutton);
+
+    stwidgets.startbuttonalign = gtk_alignment_new(0.5, 0.5, 0, 0);
+    gtk_container_add(GTK_CONTAINER(stwidgets.startbutton), stwidgets.startbuttonalign);
+
+    stwidgets.startbuttonlayout = gtk_hbox_new(FALSE, 2);
+    gtk_container_add(GTK_CONTAINER(stwidgets.startbuttonalign), stwidgets.startbuttonlayout);
+
+    stwidgets.startbuttonicon = gtk_image_new_from_stock("gtk-execute", GTK_ICON_SIZE_BUTTON);
+    gtk_box_pack_start(GTK_BOX(stwidgets.startbuttonlayout), stwidgets.startbuttonicon, FALSE, FALSE, 0);
+
+    stwidgets.startbuttonlabel = gtk_label_new_with_mnemonic("_Start");
+    gtk_box_pack_start(GTK_BOX(stwidgets.startbuttonlayout), stwidgets.startbuttonlabel, FALSE, FALSE, 0);
+
+    // Wire up the signals
+    g_signal_connect((gpointer) stwidgets.startwin, "delete_event",
+                     G_CALLBACK(on_startwin_delete_event),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.vmode3dcombo, "changed",
+                     G_CALLBACK(on_vmode3dcombo_changed),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.fullscreencheck, "toggled",
+                     G_CALLBACK(on_fullscreencheck_toggled),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.inputdevcombo, "changed",
+                     G_CALLBACK(on_inputdevcombo_changed),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.custommodcombo, "changed",
+                     G_CALLBACK(on_custommodcombo_changed),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.autoloadcheck, "toggled",
+                     G_CALLBACK(on_autoloadcheck_toggled),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.alwaysshowcheck, "toggled",
+                     G_CALLBACK(on_alwaysshowcheck_toggled),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.cancelbutton, "clicked",
+                     G_CALLBACK(on_cancelbutton_clicked),
+                     NULL);
+    g_signal_connect((gpointer) stwidgets.startbutton, "clicked",
+                     G_CALLBACK(on_startbutton_clicked),
+                     NULL);
+
+    // Associate labels with their controls
+    gtk_label_set_mnemonic_widget(GTK_LABEL(stwidgets.vmode3dlabel), stwidgets.vmode3dcombo);
+    gtk_label_set_mnemonic_widget(GTK_LABEL(stwidgets.inputdevlabel), stwidgets.inputdevcombo);
+    gtk_label_set_mnemonic_widget(GTK_LABEL(stwidgets.custommodlabel), stwidgets.custommodcombo);
+
+    return stwidgets.startwin;
+}
+
+
+// -- BUILD ENTRY POINTS ------------------------------------------------------
+
+int32_t startwin_open(void)
+{
+    if (!gtkenabled) return 0;
+    if (stwidgets.startwin) return 1;
+
+    stwidgets.startwin = create_window();
+    if (stwidgets.startwin)
+    {
+        SetPage(TAB_MESSAGES);
+        gtk_widget_show_all(stwidgets.startwin);
+        gtk_main_iteration_do(FALSE);
+        return 0;
+    }
+    return -1;
+}
+
+int32_t startwin_close(void)
+{
+    if (!gtkenabled) return 0;
+    if (!stwidgets.startwin) return 1;
+    gtk_widget_destroy(stwidgets.startwin);
+    stwidgets.startwin = NULL;
+    return 0;
+}
+
+int32_t startwin_puts(const char *str)
+{
+    GtkWidget *textview;
+    GtkTextBuffer *textbuffer;
+    GtkTextIter enditer;
+    GtkTextMark *mark;
+    const char *aptr, *bptr;
+
+    if (!gtkenabled || !str) return 0;
+    if (!stwidgets.startwin) return 1;
+    if (!(textview = stwidgets.messagestext)) return -1;
+    textbuffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview));
+
+    gtk_text_buffer_get_end_iter(textbuffer, &enditer);
+    for (aptr = bptr = str; *aptr != 0;)
+    {
+        switch (*bptr)
+        {
+        case '\b':
+            if (bptr > aptr)
+                gtk_text_buffer_insert(textbuffer, &enditer, (const gchar *)aptr, (gint)(bptr-aptr)-1);
+#if GTK_CHECK_VERSION(2,6,0)
+            gtk_text_buffer_backspace(textbuffer, &enditer, FALSE, TRUE);
+#else
+            {
+                GtkTextIter iter2 = enditer;
+                gtk_text_iter_backward_cursor_position(&iter2);
+                //FIXME: this seems be deleting one too many chars somewhere!
+                if (!gtk_text_iter_equal(&iter2, &enditer))
+                    gtk_text_buffer_delete_interactive(textbuffer, &iter2, &enditer, TRUE);
+            }
+#endif
+            aptr = ++bptr;
+            break;
+        case 0:
+            if (bptr > aptr)
+                gtk_text_buffer_insert(textbuffer, &enditer, (const gchar *)aptr, (gint)(bptr-aptr));
+            aptr = bptr;
+            break;
+        case '\r':	// FIXME
+        default:
+            bptr++;
+            break;
+        }
+    }
+
+    mark = gtk_text_buffer_create_mark(textbuffer, NULL, &enditer, 1);
+    gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(textview), mark, 0.0, FALSE, 0.0, 1.0);
+    gtk_text_buffer_delete_mark(textbuffer, mark);
+
+    return 0;
+}
+
+int32_t startwin_settitle(const char *title)
+{
+    if (!gtkenabled) return 0;
+    if (!stwidgets.startwin) return 1;
+    gtk_window_set_title(GTK_WINDOW(stwidgets.startwin), title);
+    return 0;
+}
+
+int32_t startwin_idle(void *s)
+{
+    UNREFERENCED_PARAMETER(s);
+    if (!gtkenabled) return 0;
+    //if (!stwidgets.startwin) return 1;
+    gtk_main_iteration_do(FALSE);
+    return 0;
+}
+
+int32_t startwin_run(void)
+{
+    if (!gtkenabled) return 1;
+    if (!stwidgets.startwin) return 1;
+
+    SetPage(TAB_CONFIG);
+
+    settings.shared = gSetup;
+    settings.gamedir = g_modDir;
+    //settings.grp = g_selectedGrp;
+#ifdef POLYMER
+    settings.polymer = 0;
+#endif
+    PopulateForm(ALL);
+
+    gtk_main();
+
+    SetPage(TAB_MESSAGES);
+    if (retval) // launch the game with these parameters
+    {
+        gSetup = settings.shared;
+#ifdef POLYMER
+        glrendmode = (settings.polymer) ? REND_POLYMER : REND_POLYMOST;
+#endif
+        //g_selectedGrp = settings.grp;
+
+        Bstrcpy(g_modDir, (gNoSetup == 0 && settings.gamedir != NULL) ? settings.gamedir : "/");
+    }
+
+    return retval;
+}
diff --git a/source/blood/src/startosx.game.mm b/source/blood/src/startosx.game.mm
new file mode 100644
index 000000000..25e0d29cd
--- /dev/null
+++ b/source/blood/src/startosx.game.mm
@@ -0,0 +1,856 @@
+
+#define Rect CocoaRect
+
+#import <Cocoa/Cocoa.h>
+
+#undef Rect
+
+#include "blood.h"
+#include "common.h"
+#include "common_game.h"
+#include "build.h"
+#include "compat.h"
+#include "baselayer.h"
+
+#ifndef MAC_OS_X_VERSION_10_5
+# define NSImageScaleNone NSScaleNone
+#endif
+
+#ifndef MAC_OS_X_VERSION_10_12
+# define NSEventModifierFlagOption NSAlternateKeyMask
+# define NSEventModifierFlagCommand NSCommandKeyMask
+# define NSEventMaskAny NSAnyEventMask
+# define NSWindowStyleMaskTitled NSTitledWindowMask
+# define NSWindowStyleMaskClosable NSClosableWindowMask
+# define NSWindowStyleMaskMiniaturizable NSMiniaturizableWindowMask
+# define NSWindowStyleMaskResizable NSResizableWindowMask
+# define NSAlertStyleInformational NSInformationalAlertStyle
+# define NSControlSizeSmall NSSmallControlSize
+#endif
+
+@interface GameEntry : NSObject
+{
+    NSString *namestring;
+    NSString *inifilestring;
+    INICHAIN const *fg;
+}
+
+- (id)initWithINICHAIN:(INICHAIN const *)inichain;
+- (void)dealloc;
+
+- (NSString *)name;
+- (NSString *)inifile;
+- (INICHAIN const *)entryptr;
+
+@end
+
+@implementation GameEntry
+
+- (id)initWithINICHAIN:(INICHAIN const *)inichain
+{
+    self = [super init];
+
+    if (self)
+    {
+        fg = inichain;
+
+        char const *const name = nullptr == fg->pDescription ? fg->zName : fg->pDescription->pzName;
+
+        namestring = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
+        [namestring retain];
+
+        inifilestring = [NSString stringWithCString:fg->zName encoding:NSUTF8StringEncoding];
+        [inifilestring retain];
+    }
+
+    return self;
+}
+
+- (void)dealloc
+{
+    [namestring release];
+    [inifilestring release];
+    [super dealloc];
+}
+
+- (NSString *)name
+{
+    return namestring;
+}
+
+- (NSString *)inifile
+{
+    return inifilestring;
+}
+
+- (INICHAIN const *)entryptr
+{
+    return fg;
+}
+
+@end
+
+@interface GameListSource : NSObject <NSComboBoxDataSource>
+{
+    NSMutableArray *list;
+}
+
+- (id)init;
+- (void)dealloc;
+
+- (GameEntry *)entryAtIndex:(int)index;
+- (int)findIndexForINI:(NSString*)inifile;
+
+- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex;
+- (int)numberOfRowsInTableView:(NSTableView *)aTableView;
+
+@end
+
+@implementation GameListSource
+
+- (id)init
+{
+    self = [super init];
+
+    if (self)
+    {
+        list = [[NSMutableArray alloc] init];
+
+        for (auto fg = pINIChain; nullptr != fg; fg = fg->pNext)
+        {
+            [list addObject:[[GameEntry alloc] initWithINICHAIN:fg]];
+        }
+    }
+
+    return self;
+}
+
+- (void)dealloc
+{
+    [list release];
+    [super dealloc];
+}
+
+- (GameEntry *)entryAtIndex:(int)index
+{
+    return [list objectAtIndex:index];
+}
+
+- (int)findIndexForINI:(NSString*)inifile
+{
+    for (NSUInteger i = 0, count = [list count]; i < count; ++i)
+    {
+        if ([[[list objectAtIndex:i] inifile] isEqual:inifile])
+        {
+            return i;
+        }
+    }
+
+    return -1;
+}
+
+- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
+{
+    UNREFERENCED_PARAMETER(aTableView);
+    NSParameterAssert((NSUInteger)rowIndex < [list count]);
+
+    switch ([[aTableColumn identifier] intValue])
+    {
+        case 0:    // name column
+            return [[list objectAtIndex:rowIndex] name];
+        case 1:    // ini file column
+            return [[list objectAtIndex:rowIndex] inifile];
+        default:
+            return nil;
+    }
+}
+
+- (int)numberOfRowsInTableView:(NSTableView *)aTableView
+{
+    UNREFERENCED_PARAMETER(aTableView);
+
+    return [list count];
+}
+
+@end
+
+static NSRect NSRectChangeXY(NSRect const rect, CGFloat const x, CGFloat const y)
+{
+    return NSMakeRect(x, y, rect.size.width, rect.size.height);
+}
+static NSRect NSSizeAddXY(NSSize const size, CGFloat const x, CGFloat const y)
+{
+    return NSMakeRect(x, y, size.width, size.height);
+}
+#if 0
+static CGFloat NSRightEdge(NSRect rect)
+{
+    return rect.origin.x + rect.size.width;
+}
+#endif
+static CGFloat NSTopEdge(NSRect rect)
+{
+    return rect.origin.y + rect.size.height;
+}
+
+static void setFontToSmall(id control)
+{
+    [control setFont:[NSFont fontWithDescriptor:[[control font] fontDescriptor] size:[NSFont smallSystemFontSize]]];
+}
+
+static void setControlToSmall(id control)
+{
+#ifdef MAC_OS_X_VERSION_10_12
+    [control setControlSize:NSControlSizeSmall];
+#else
+    [control setControlSize:NSControlSizeSmall];
+#endif
+}
+
+static NSTextField * makeLabel(NSString * labelText)
+{
+    NSTextField *textField = [[NSTextField alloc] init];
+    setFontToSmall(textField);
+    setControlToSmall([textField cell]);
+    [textField setStringValue:labelText];
+    [textField setBezeled:NO];
+    [textField setDrawsBackground:NO];
+    [textField setEditable:NO];
+    [textField setSelectable:NO];
+    [textField sizeToFit];
+    return textField;
+}
+
+static NSButton * makeCheckbox(NSString * labelText)
+{
+    NSButton *checkbox = [[NSButton alloc] init];
+    setFontToSmall(checkbox);
+    setControlToSmall([checkbox cell]);
+    [checkbox setTitle:labelText];
+    [checkbox setButtonType:NSSwitchButton];
+    [checkbox sizeToFit];
+    return checkbox;
+}
+
+static NSPopUpButton * makeComboBox(void)
+{
+    NSPopUpButton *comboBox = [[NSPopUpButton alloc] init];
+    [comboBox setPullsDown:NO];
+    setFontToSmall(comboBox);
+    setControlToSmall([comboBox cell]);
+    [comboBox setBezelStyle:NSRoundedBezelStyle];
+    [comboBox setPreferredEdge:NSMaxYEdge];
+    [[comboBox cell] setArrowPosition:NSPopUpArrowAtCenter];
+    [comboBox sizeToFit];
+    return comboBox;
+}
+
+static id nsapp;
+
+/* setAppleMenu disappeared from the headers in 10.4 */
+@interface NSApplication(NSAppleMenu)
+- (void)setAppleMenu:(NSMenu *)menu;
+@end
+
+static NSString * GetApplicationName(void)
+{
+    NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
+    if (!appName)
+        appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
+    if (![appName length])
+        appName = [[NSProcessInfo processInfo] processName];
+
+    return appName;
+}
+
+static void CreateApplicationMenus(void)
+{
+    NSString *appName;
+    NSString *title;
+    NSMenu *rootMenu;
+    NSMenu *serviceMenu;
+    NSMenuItem *menuItem;
+
+    NSMenu *mainMenu = [[NSMenu alloc] init];
+
+    /* Create the application menu */
+    appName = GetApplicationName();
+    rootMenu = [[NSMenu alloc] init];
+
+    /* Put menu into the menubar */
+    menuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""];
+    [menuItem setSubmenu:rootMenu];
+    [mainMenu addItem:menuItem];
+    [menuItem release];
+
+    /* Add menu items */
+    title = [@"About " stringByAppendingString:appName];
+    [rootMenu addItemWithTitle:title action:@selector(orderFrontStandardAboutPanel:) keyEquivalent:@""];
+
+    [rootMenu addItem:[NSMenuItem separatorItem]];
+
+    serviceMenu = [[NSMenu alloc] init];
+    menuItem = (NSMenuItem *)[rootMenu addItemWithTitle:@"Services" action:nil keyEquivalent:@""];
+    [menuItem setSubmenu:serviceMenu];
+
+    [nsapp setServicesMenu:serviceMenu];
+    [serviceMenu release];
+
+    [rootMenu addItem:[NSMenuItem separatorItem]];
+
+    title = [@"Hide " stringByAppendingString:appName];
+    [rootMenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"];
+
+    menuItem = (NSMenuItem *)[rootMenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"];
+    [menuItem setKeyEquivalentModifierMask:(NSEventModifierFlagOption|NSEventModifierFlagCommand)];
+
+    [rootMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""];
+
+    [rootMenu addItem:[NSMenuItem separatorItem]];
+
+    title = [@"Quit " stringByAppendingString:appName];
+    [rootMenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
+
+    /* Create the main menu bar */
+    [nsapp setMainMenu:mainMenu];
+    [mainMenu release];  /* we're done with it, let NSApp own it. */
+
+    /* Tell the application object that this is now the application menu */
+    [nsapp setAppleMenu:rootMenu];
+    [rootMenu release];
+}
+
+static int retval = -1;
+
+static struct
+{
+    INICHAIN const * ini;
+    char *gamedir;
+    ud_setup_t shared;
+    int polymer;
+}
+settings;
+
+@interface StartupWindow : NSWindow <NSWindowDelegate>
+{
+    NSMutableArray *modeslist3d;
+    GameListSource *gamelistsrc;
+
+    NSButton *alwaysShowButton;
+    NSButton *fullscreenButton;
+    NSTextView *messagesView;
+    NSTabView *tabView;
+    NSTabViewItem *tabViewItemSetup;
+    NSTabViewItem *tabViewItemMessageLog;
+    NSPopUpButton *videoMode3DPUButton;
+    NSScrollView *gameList;
+
+    NSButton *cancelButton;
+    NSButton *startButton;
+}
+
+- (StartupWindow *)init;
+
+- (void)dealloc;
+- (void)populateVideoModes:(BOOL)firstTime;
+
+- (void)fullscreenClicked:(id)sender;
+
+- (void)cancel:(id)sender;
+- (void)start:(id)sender;
+
+- (void)setupRunMode;
+- (void)setupMessagesMode;
+
+- (void)putsMessage:(NSString *)str;
+
+@end
+
+@implementation StartupWindow : NSWindow
+
+- (StartupWindow *)init
+{
+    NSUInteger const style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
+    NSRect const windowFrame = NSMakeRect(0, 0, 480, 280);
+    self = [super initWithContentRect:windowFrame styleMask:style backing:NSBackingStoreBuffered defer:NO];
+
+    if (self)
+    {
+        // window properties
+        [self setDelegate:self];
+        [self setReleasedWhenClosed:NO];
+#if defined MAC_OS_X_VERSION_10_3 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3
+        [self setContentMinSize:[[self contentView] frame].size];
+#else
+        [self setMinSize:[NSWindow frameRectForContentRect:[[self contentView] frame] styleMask:[self styleMask]].size];
+#endif
+
+
+        // image on the left
+        NSRect const imageFrame = NSMakeRect(0, 0, 100, 280);
+        NSImageView * imageView = [[NSImageView alloc] initWithFrame:imageFrame];
+        [imageView setImageScaling:NSImageScaleNone];
+        [imageView setImage:[NSImage imageNamed:@"game"]];
+        [[self contentView] addSubview:imageView];
+        [imageView setAutoresizingMask:NSViewMaxXMargin | NSViewHeightSizable];
+
+
+        // buttons
+        CGFloat const buttonWidth = 80;
+        CGFloat const buttonHeight = 32;
+
+        NSRect const startButtonFrame = NSMakeRect(windowFrame.size.width - buttonWidth, 0, buttonWidth, buttonHeight);
+        startButton = [[NSButton alloc] initWithFrame:startButtonFrame];
+        [[self contentView] addSubview:startButton];
+        [startButton setTitle:@"Start"];
+        [startButton setTarget:self];
+        [startButton setAction:@selector(start:)];
+        [startButton setBezelStyle:NSRoundedBezelStyle];
+        [startButton setKeyEquivalent:@"\r"];
+        [startButton setAutoresizingMask:NSViewMinXMargin | NSViewMaxYMargin];
+
+        NSRect const cancelButtonFrame = NSMakeRect(startButtonFrame.origin.x - buttonWidth, 0, buttonWidth, buttonHeight);
+        cancelButton = [[NSButton alloc] initWithFrame:cancelButtonFrame];
+        [[self contentView] addSubview:cancelButton];
+        [cancelButton setTitle:@"Cancel"];
+        [cancelButton setTarget:self];
+        [cancelButton setAction:@selector(cancel:)];
+        [cancelButton setBezelStyle:NSRoundedBezelStyle];
+        [cancelButton setAutoresizingMask:NSViewMinXMargin | NSViewMaxYMargin];
+
+
+        // tab frame
+        NSRect const tabViewFrame = NSMakeRect(imageFrame.size.width, buttonHeight, windowFrame.size.width - imageFrame.size.width, windowFrame.size.height - buttonHeight - 5);
+        tabView = [[NSTabView alloc] initWithFrame:tabViewFrame];
+        [[self contentView] addSubview:tabView];
+        setFontToSmall(tabView);
+        setControlToSmall(tabView);
+        [tabView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+
+        // setup tab
+
+        tabViewItemSetup = [[NSTabViewItem alloc] init];
+        [tabView addTabViewItem:tabViewItemSetup];
+        [tabViewItemSetup setLabel:@"Setup"];
+        NSRect const tabViewItemSetupFrame = [[tabViewItemSetup view] frame];
+
+
+        // always show checkbox
+        alwaysShowButton = makeCheckbox(@"Always show this window at startup");
+        [[tabViewItemSetup view] addSubview:alwaysShowButton];
+        NSSize const alwaysShowButtonSize = [alwaysShowButton frame].size;
+        NSRect const alwaysShowButtonFrame = NSSizeAddXY(alwaysShowButtonSize, tabViewItemSetupFrame.size.width - alwaysShowButtonSize.width, 0);
+        [alwaysShowButton setFrame:alwaysShowButtonFrame];
+        [alwaysShowButton setAutoresizingMask:NSViewMinXMargin | NSViewMaxYMargin];
+
+
+        // video mode selectors and labels
+        NSTextField * labelVideoMode = makeLabel(@"Video mode:");
+        [[tabViewItemSetup view] addSubview:labelVideoMode];
+        NSSize const labelVideoModeSize = [labelVideoMode frame].size;
+        [labelVideoMode setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
+
+        fullscreenButton = makeCheckbox(@"Fullscreen");
+        [[tabViewItemSetup view] addSubview:fullscreenButton];
+        NSSize const fullscreenButtonSize = [fullscreenButton frame].size;
+        [fullscreenButton setAction:@selector(fullscreenClicked:)];
+        [fullscreenButton setAutoresizingMask:NSViewMinXMargin | NSViewMinYMargin];
+
+        videoMode3DPUButton = makeComboBox();
+        [[tabViewItemSetup view] addSubview:videoMode3DPUButton];
+        NSSize const videoMode3DPUButtonSize = [videoMode3DPUButton frame].size;
+        CGFloat const videoMode3DButtonX = labelVideoModeSize.width; // NSRightEdge(labelVideoModeFrame);
+        NSRect const videoMode3DPUButtonFrame = NSMakeRect(videoMode3DButtonX, tabViewItemSetupFrame.size.height - videoMode3DPUButtonSize.height, tabViewItemSetupFrame.size.width - videoMode3DButtonX - fullscreenButtonSize.width, videoMode3DPUButtonSize.height);
+        [videoMode3DPUButton setFrame:videoMode3DPUButtonFrame];
+        [videoMode3DPUButton setAutoresizingMask:NSViewWidthSizable | NSViewMinYMargin];
+
+        NSRect const labelVideoModeFrame = NSSizeAddXY(labelVideoModeSize, 0, videoMode3DPUButtonFrame.origin.y + rintf((videoMode3DPUButtonSize.height - labelVideoModeSize.height) * 0.5f) + 1);
+        [labelVideoMode setFrame:labelVideoModeFrame];
+
+        NSRect const fullscreenButtonFrame = NSSizeAddXY(fullscreenButtonSize, tabViewItemSetupFrame.size.width - fullscreenButtonSize.width, videoMode3DPUButtonFrame.origin.y + rintf((videoMode3DPUButtonSize.height - fullscreenButtonSize.height) * 0.5f) + 1);
+        [fullscreenButton setFrame:fullscreenButtonFrame];
+
+
+        // game selector and label
+        NSTextField * labelGame = makeLabel(@"Game:");
+        [[tabViewItemSetup view] addSubview:labelGame];
+        NSSize const labelGameSize = [labelGame frame].size;
+        NSRect const labelGameFrame = NSSizeAddXY(labelGameSize, 0, videoMode3DPUButtonFrame.origin.y - labelGameSize.height);
+        [labelGame setFrame:labelGameFrame];
+        [labelGame setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
+
+        CGFloat const gameListVerticalPadding = 3;
+        CGFloat const gameListY = NSTopEdge(alwaysShowButtonFrame) + gameListVerticalPadding;
+        NSRect const gameListFrame = NSMakeRect(0, gameListY, tabViewItemSetupFrame.size.width, labelGameFrame.origin.y - gameListY - gameListVerticalPadding);
+        gameList = [[NSScrollView alloc] initWithFrame:gameListFrame];
+        [[tabViewItemSetup view] addSubview:gameList];
+        [gameList setBorderType:NSBezelBorder];
+        [gameList setHasVerticalScroller:YES];
+        [gameList setHasHorizontalScroller:NO];
+        setControlToSmall([[gameList verticalScroller] cell]);
+        NSSize const gameListContentSize = [gameList contentSize];
+        [gameList setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+        NSTableView * gameListTable = [[NSTableView alloc] initWithFrame:NSMakeRect(0, 0, gameListContentSize.width, gameListContentSize.height)];
+        [gameList setDocumentView:gameListTable];
+
+        NSTableColumn * nameColumn = [[NSTableColumn alloc] initWithIdentifier:@"0"];
+        [gameListTable addTableColumn:nameColumn];
+        NSTableColumn * fileColumn = [[NSTableColumn alloc] initWithIdentifier:@"1"];
+        [gameListTable addTableColumn:fileColumn];
+        [nameColumn setEditable:NO];
+        [[nameColumn headerCell] setStringValue:@"Name"];
+        [nameColumn setWidth:gameListContentSize.width * (2.f/3.f)];
+        [fileColumn setEditable:NO];
+        [[fileColumn headerCell] setStringValue:@"File"];
+        [gameListTable sizeLastColumnToFit];
+        [gameListTable setAutoresizingMask:NSViewWidthSizable];
+
+
+        // message log tab
+
+        tabViewItemMessageLog = [[NSTabViewItem alloc] init];
+        [tabView addTabViewItem:tabViewItemMessageLog];
+        [tabViewItemMessageLog setLabel:@"Message Log"];
+        NSRect const tabViewItemMessageLogFrame = [[tabViewItemMessageLog view] frame];
+
+
+        // message log
+        NSScrollView * messagesScrollView = [[NSScrollView alloc] initWithFrame:NSRectChangeXY(tabViewItemMessageLogFrame, 0, 0)];
+        [[tabViewItemMessageLog view] addSubview:messagesScrollView];
+        [messagesScrollView setBorderType:NSBezelBorder];
+        [messagesScrollView setHasVerticalScroller:YES];
+        [messagesScrollView setHasHorizontalScroller:NO];
+        setControlToSmall([[messagesScrollView verticalScroller] cell]);
+        NSSize const messagesScrollViewContentSize = [messagesScrollView contentSize];
+        [messagesScrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+
+        messagesView = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, messagesScrollViewContentSize.width, messagesScrollViewContentSize.height)];
+        [messagesScrollView setDocumentView:messagesView];
+        [messagesView setEditable:NO];
+        [messagesView setRichText:NO];
+        setFontToSmall(messagesView);
+        [messagesView setMinSize:NSMakeSize(0.0, messagesScrollViewContentSize.height)];
+        [messagesView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
+        [messagesView setVerticallyResizable:YES];
+        [messagesView setHorizontallyResizable:NO];
+        [messagesView setAutoresizingMask:NSViewWidthSizable];
+
+        [[messagesView textContainer] setContainerSize:NSMakeSize(messagesScrollViewContentSize.width, FLT_MAX)];
+        [[messagesView textContainer] setWidthTracksTextView:YES];
+    }
+
+    return self;
+}
+
+- (BOOL)canBecomeKeyWindow
+{
+    return YES;
+}
+
+- (BOOL)canBecomeMainWindow
+{
+    return YES;
+}
+
+- (BOOL) windowShouldClose:(id)sender
+{
+    UNREFERENCED_PARAMETER(sender);
+
+    retval = 0;
+
+    return YES;
+}
+
+- (void)dealloc
+{
+    [gamelistsrc release];
+    [modeslist3d release];
+    [super dealloc];
+}
+
+- (void)populateVideoModes:(BOOL)firstTime
+{
+    int i, mode3d, fullscreen = ([fullscreenButton state] == NSOnState);
+    int idx3d = -1;
+    int xdim = 0, ydim = 0, bpp = 0;
+
+    if (firstTime) {
+        xdim = settings.shared.xdim;
+        ydim = settings.shared.ydim;
+        bpp  = settings.shared.bpp;
+    } else {
+        mode3d = [[modeslist3d objectAtIndex:[videoMode3DPUButton indexOfSelectedItem]] intValue];
+        if (mode3d >= 0) {
+            xdim = validmode[mode3d].xdim;
+            ydim = validmode[mode3d].ydim;
+            bpp = validmode[mode3d].bpp;
+        }
+
+    }
+    mode3d = videoCheckMode(&xdim, &ydim, bpp, fullscreen, 1);
+    if (mode3d < 0) {
+        int i, cd[] = { 32, 24, 16, 15, 8, 0 };
+        for (i=0; cd[i]; ) { if (cd[i] >= bpp) i++; else break; }
+        for ( ; cd[i]; i++) {
+            mode3d = videoCheckMode(&xdim, &ydim, cd[i], fullscreen, 1);
+            if (mode3d < 0) continue;
+            break;
+        }
+    }
+
+    [modeslist3d release];
+    [videoMode3DPUButton removeAllItems];
+
+    modeslist3d = [[NSMutableArray alloc] init];
+
+    for (i = 0; i < validmodecnt; i++) {
+        if (fullscreen == validmode[i].fs) {
+            if (i == mode3d) idx3d = [modeslist3d count];
+            [modeslist3d addObject:[NSNumber numberWithInt:i]];
+            [videoMode3DPUButton addItemWithTitle:[NSString stringWithFormat:@"%d %C %d %d-bpp",
+                                                   validmode[i].xdim, 0xd7, validmode[i].ydim, validmode[i].bpp]];
+        }
+    }
+
+    if (idx3d >= 0) [videoMode3DPUButton selectItemAtIndex:idx3d];
+}
+
+- (void)fullscreenClicked:(id)sender
+{
+    UNREFERENCED_PARAMETER(sender);
+
+    [self populateVideoModes:NO];
+}
+
+- (void)cancel:(id)sender
+{
+    UNREFERENCED_PARAMETER(sender);
+
+    retval = 0;
+}
+
+- (void)start:(id)sender
+{
+    UNREFERENCED_PARAMETER(sender);
+
+    int mode = [[modeslist3d objectAtIndex:[videoMode3DPUButton indexOfSelectedItem]] intValue];
+    if (mode >= 0) {
+        settings.shared.xdim = validmode[mode].xdim;
+        settings.shared.ydim = validmode[mode].ydim;
+        settings.shared.bpp = validmode[mode].bpp;
+        settings.shared.fullscreen = validmode[mode].fs;
+    }
+
+    int row = [[gameList documentView] selectedRow];
+    if (row >= 0)
+    {
+        settings.ini = [[gamelistsrc entryAtIndex:row] entryptr];
+    }
+
+    settings.shared.forcesetup = [alwaysShowButton state] == NSOnState;
+
+    retval = 1;
+}
+
+- (void)setupRunMode
+{
+    videoGetModes();
+
+    [fullscreenButton setState: (settings.shared.fullscreen ? NSOnState : NSOffState)];
+    [alwaysShowButton setState: (settings.shared.forcesetup ? NSOnState : NSOffState)];
+    [self populateVideoModes:YES];
+
+    // enable all the controls on the Configuration page
+    NSEnumerator *enumerator = [[[tabViewItemSetup view] subviews] objectEnumerator];
+    NSControl *control;
+    while ((control = [enumerator nextObject]))
+    {
+        if ([control respondsToSelector:@selector(setEnabled:)])
+            [control setEnabled:true];
+    }
+
+    gamelistsrc = [[GameListSource alloc] init];
+    [[gameList documentView] setDataSource:gamelistsrc];
+    [[gameList documentView] deselectAll:nil];
+
+    if (settings.ini)
+    {
+        int row = [gamelistsrc findIndexForINI:[NSString stringWithUTF8String:settings.ini->zName]];
+        if (row >= 0)
+        {
+            [[gameList documentView] scrollRowToVisible:row];
+#if defined MAC_OS_X_VERSION_10_3 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_3
+            [[gameList documentView] selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
+#else
+            [[gameList documentView] selectRow:row byExtendingSelection:NO];
+#endif
+        }
+    }
+
+    [cancelButton setEnabled:true];
+    [startButton setEnabled:true];
+
+    [tabView selectTabViewItem:tabViewItemSetup];
+    [NSCursor unhide]; // Why should I need to do this?
+}
+
+- (void)setupMessagesMode
+{
+    [tabView selectTabViewItem:tabViewItemMessageLog];
+
+    // disable all the controls on the Configuration page except "always show", so the
+    // user can enable it if they want to while waiting for something else to happen
+    NSEnumerator *enumerator = [[[tabViewItemSetup view] subviews] objectEnumerator];
+    NSControl *control;
+    while ((control = [enumerator nextObject]))
+    {
+        if (control != alwaysShowButton && [control respondsToSelector:@selector(setEnabled:)])
+            [control setEnabled:false];
+    }
+
+    [cancelButton setEnabled:false];
+    [startButton setEnabled:false];
+}
+
+- (void)putsMessage:(NSString *)str
+{
+    NSRange end;
+    NSTextStorage *text = [messagesView textStorage];
+    BOOL shouldAutoScroll;
+
+    shouldAutoScroll = ((int)NSMaxY([messagesView bounds]) == (int)NSMaxY([messagesView visibleRect]));
+
+    end.location = [text length];
+    end.length = 0;
+
+    [text beginEditing];
+    [messagesView replaceCharactersInRange:end withString:str];
+    [text endEditing];
+
+    if (shouldAutoScroll) {
+        end.location = [text length];
+        end.length = 0;
+        [messagesView scrollRangeToVisible:end];
+    }
+}
+
+@end
+
+static StartupWindow *startwin = nil;
+
+int startwin_open(void)
+{
+    // fix for "ld: absolute address to symbol _NSApp in a different linkage unit not supported"
+    // (OS X 10.6) when building for PPC
+    nsapp = [NSApplication sharedApplication];
+
+    if (startwin != nil) return 1;
+
+    startwin = [[StartupWindow alloc] init];
+    if (startwin == nil) return -1;
+
+    [startwin setupMessagesMode];
+
+    [nsapp finishLaunching];
+
+    [startwin center];
+    [startwin makeKeyAndOrderFront:nil];
+
+    CreateApplicationMenus();
+
+    return 0;
+}
+
+int startwin_close(void)
+{
+    if (startwin == nil) return 1;
+
+    [startwin close];
+    [startwin release];
+    startwin = nil;
+
+    return 0;
+}
+
+int startwin_puts(const char *s)
+{
+    NSString *ns;
+
+    if (!s) return -1;
+    if (startwin == nil) return 1;
+
+    ns = [NSString stringWithUTF8String:s];
+    [startwin putsMessage:ns];
+    [ns release];
+
+    return 0;
+}
+
+int startwin_settitle(const char *s)
+{
+    NSString *ns;
+
+    if (!s) return -1;
+    if (startwin == nil) return 1;
+
+    ns = [NSString stringWithUTF8String:s];
+    [startwin setTitle:ns];
+    [ns release];
+
+    return 0;
+}
+
+int startwin_idle(void *v)
+{
+    UNREFERENCED_PARAMETER(v);
+
+    if (startwin)
+    {
+        NSEvent *event;
+        do
+        {
+            event = [nsapp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate date] inMode:NSDefaultRunLoopMode dequeue:YES];
+            [nsapp sendEvent:event];
+        }
+        while (event != nil);
+
+        [startwin displayIfNeeded];
+        [nsapp updateWindows];
+    }
+
+    return 0;
+}
+
+
+int startwin_run(void)
+{
+    if (startwin == nil) return 0;
+
+    settings.shared = gSetup;
+    settings.ini = pINISelected;
+    settings.gamedir = g_modDir;
+
+    [startwin setupRunMode];
+
+    do
+    {
+        NSEvent *event = [nsapp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES];
+        [nsapp sendEvent:event];
+        [nsapp updateWindows];
+    }
+    while (retval == -1);
+
+    [startwin setupMessagesMode];
+    [nsapp updateWindows];
+
+    if (retval) {
+        gSetup = settings.shared;
+        glrendmode = settings.polymer ? REND_POLYMER : REND_POLYMOST;
+        pINISelected = settings.ini;
+        Bstrcpy(g_modDir, (gNoSetup == 0 && settings.gamedir != NULL) ? settings.gamedir : "/");
+    }
+
+    return retval;
+}
diff --git a/source/blood/src/startwin.game.cpp b/source/blood/src/startwin.game.cpp
new file mode 100644
index 000000000..ab6717b2f
--- /dev/null
+++ b/source/blood/src/startwin.game.cpp
@@ -0,0 +1,719 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#ifndef _WIN32
+#error Only for Windows
+#endif
+
+#include "renderlayer.h"
+
+#ifdef STARTUP_SETUP_WINDOW
+
+#define NEED_WINDOWSX_H
+#define NEED_COMMCTRL_H
+#define ONLY_USERDEFS
+
+#include "_control.h"
+#include "build.h"
+#include "cache1d.h"
+//#include "cmdline.h"
+#include "common_game.h"
+#include "compat.h"
+#include "control.h"
+#include "config.h"
+//#include "function.h"
+//#include "game.h"
+//#include "grpscan.h"
+//#include "inv.h"
+#include "blood.h"
+#include "keyboard.h"
+#include "startwin.game.h"
+#include "windows_inc.h"
+
+#define TAB_CONFIG 0
+#define TAB_MESSAGES 1
+
+static struct
+{
+    INICHAIN const * ini;
+    char *gamedir;
+    ud_setup_t shared;
+    int polymer;
+}
+settings;
+
+static HWND startupdlg;
+static HWND pages[3];
+static int done = -1;
+static int mode = TAB_CONFIG;
+
+static CACHE1D_FIND_REC *finddirs;
+
+static inline void clearfilenames(void)
+{
+    klistfree(finddirs);
+    finddirs = NULL;
+}
+
+static inline void getfilenames(char const *path)
+{
+    clearfilenames();
+    finddirs = klistpath(path,"*",CACHE1D_FIND_DIR);
+}
+
+#define POPULATE_VIDEO 1
+#define POPULATE_CONFIG 2
+#define POPULATE_GAME 4
+#define POPULATE_GAMEDIRS 8
+
+#ifdef INPUT_MOUSE
+#undef INPUT_MOUSE
+#endif
+
+#define INPUT_KB 0
+#define INPUT_MOUSE 1
+#define INPUT_JOYSTICK 2
+#define INPUT_ALL 3
+
+const char *controlstrings[] = { "Keyboard only", "Keyboard and mouse", "Keyboard and joystick", "All supported devices" };
+
+static void PopulateForm(int32_t pgs)
+{
+    char buf[512];
+
+    if (pgs & POPULATE_GAMEDIRS)
+    {
+        HWND hwnd = GetDlgItem(pages[TAB_CONFIG], IDCGAMEDIR);
+
+        getfilenames("/");
+        (void)ComboBox_ResetContent(hwnd);
+        int const r = ComboBox_AddString(hwnd, "None");
+        (void)ComboBox_SetItemData(hwnd, r, 0);
+        (void)ComboBox_SetCurSel(hwnd, r);
+        auto dirs = finddirs;
+        for (int i=1, j=1; dirs != NULL; dirs=dirs->next)
+        {
+            if (Bstrcasecmp(dirs->name, "autoload") == 0)
+            {
+                j++;
+                continue;
+            }
+
+            (void)ComboBox_AddString(hwnd, dirs->name);
+            (void)ComboBox_SetItemData(hwnd, i, j);
+            if (Bstrcasecmp(dirs->name, settings.gamedir) == 0)
+                (void)ComboBox_SetCurSel(hwnd, i);
+
+            i++;
+            j++;
+        }
+    }
+
+    if (pgs & POPULATE_VIDEO)
+    {
+        HWND hwnd = GetDlgItem(pages[TAB_CONFIG], IDCVMODE);
+        int mode = videoCheckMode(&settings.shared.xdim, &settings.shared.ydim, settings.shared.bpp, settings.shared.fullscreen, 1);
+
+        if (mode < 0 || (settings.shared.bpp < 15 && (settings.polymer)))
+        {
+            int CONSTEXPR cd[] = { 32, 24, 16, 15, 8, 0 };
+            int i;
+
+            for (i=0; cd[i];)
+            {
+                if (cd[i] >= settings.shared.bpp) i++;
+                else break;
+            }
+            for (; cd[i]; i++)
+            {
+                mode = videoCheckMode(&settings.shared.xdim, &settings.shared.ydim, cd[i], settings.shared.fullscreen, 1);
+                if (mode < 0) continue;
+                settings.shared.bpp = cd[i];
+                break;
+            }
+        }
+
+        Button_SetCheck(GetDlgItem(pages[TAB_CONFIG], IDCFULLSCREEN), ((settings.shared.fullscreen) ? BST_CHECKED : BST_UNCHECKED));
+        //Button_SetCheck(GetDlgItem(pages[TAB_CONFIG], IDCPOLYMER), ((settings.polymer) ? BST_CHECKED : BST_UNCHECKED));
+
+        (void)ComboBox_ResetContent(hwnd);
+
+        for (int i=0; i<validmodecnt; i++)
+        {
+            if (validmode[i].fs != (settings.shared.fullscreen)) continue;
+            if ((validmode[i].bpp < 15) && (settings.polymer)) continue;
+
+            // all modes get added to the 3D mode list
+            Bsprintf(buf, "%dx%d %s", validmode[i].xdim, validmode[i].ydim, validmode[i].bpp == 8 ? "software" : "OpenGL");
+            int const j = ComboBox_AddString(hwnd, buf);
+            (void)ComboBox_SetItemData(hwnd, j, i);
+            if (i == mode)(void)ComboBox_SetCurSel(hwnd, j);
+        }
+    }
+
+    if (pgs & POPULATE_CONFIG)
+    {
+        Button_SetCheck(GetDlgItem(pages[TAB_CONFIG], IDCALWAYSSHOW), (settings.shared.forcesetup ? BST_CHECKED : BST_UNCHECKED));
+        Button_SetCheck(GetDlgItem(pages[TAB_CONFIG], IDCAUTOLOAD), (!(settings.shared.noautoload) ? BST_CHECKED : BST_UNCHECKED));
+
+        HWND hwnd = GetDlgItem(pages[TAB_CONFIG], IDCINPUT);
+
+        (void)ComboBox_ResetContent(hwnd);
+        (void)ComboBox_SetCurSel(hwnd, 0);
+
+        int j = 4;
+
+#ifdef RENDERTYPEWIN
+        if (di_disabled) j = 2;
+#endif
+
+        for (int i=0; i<j; i++)
+        {
+            (void)ComboBox_InsertString(hwnd, i, controlstrings[i]);
+            (void)ComboBox_SetItemData(hwnd, i, i);
+
+            switch (i)
+            {
+            case INPUT_MOUSE:
+                if (settings.shared.usemouse && !settings.shared.usejoystick)(void)ComboBox_SetCurSel(hwnd, i);
+                break;
+            case INPUT_JOYSTICK:
+                if (!settings.shared.usemouse && settings.shared.usejoystick)(void)ComboBox_SetCurSel(hwnd, i);
+                break;
+            case INPUT_ALL:
+                if (settings.shared.usemouse && settings.shared.usejoystick)(void)ComboBox_SetCurSel(hwnd, i);
+                break;
+            }
+        }
+    }
+
+    if (pgs & POPULATE_GAME)
+    {
+        HWND hwnd = GetDlgItem(pages[TAB_CONFIG], IDCDATA);
+
+        for (auto fg = pINIChain; fg; fg=fg->pNext)
+        {
+            if (fg->pDescription)
+                Bsprintf(buf, "%s\t%s", fg->pDescription->pzName, fg->zName);
+            else
+                Bsprintf(buf, "%s", fg->zName);
+            int const j = ListBox_AddString(hwnd, buf);
+            (void)ListBox_SetItemData(hwnd, j, (LPARAM)fg);
+            if (settings.ini == fg)
+                (void)ListBox_SetCurSel(hwnd, j);
+        }
+    }
+}
+
+static INT_PTR CALLBACK ConfigPageProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+    switch (uMsg)
+    {
+    case WM_COMMAND:
+        switch (LOWORD(wParam))
+        {
+        case IDCFULLSCREEN:
+            settings.shared.fullscreen = !settings.shared.fullscreen;
+            PopulateForm(POPULATE_VIDEO);
+            return TRUE;
+        //case IDCPOLYMER:
+        //    settings.polymer = !settings.polymer;
+        //    if (settings.shared.bpp == 8) settings.shared.bpp = 32;
+        //    PopulateForm(POPULATE_VIDEO);
+        //    return TRUE;
+        case IDCVMODE:
+            if (HIWORD(wParam) == CBN_SELCHANGE)
+            {
+                int i = ComboBox_GetCurSel((HWND)lParam);
+                if (i != CB_ERR) i = ComboBox_GetItemData((HWND)lParam, i);
+                if (i != CB_ERR)
+                {
+                    settings.shared.xdim = validmode[i].xdim;
+                    settings.shared.ydim = validmode[i].ydim;
+                    settings.shared.bpp  = validmode[i].bpp;
+                }
+            }
+            return TRUE;
+        case IDCALWAYSSHOW:
+            settings.shared.forcesetup = IsDlgButtonChecked(hwndDlg, IDCALWAYSSHOW) == BST_CHECKED;
+            return TRUE;
+        case IDCAUTOLOAD:
+            settings.shared.noautoload = (IsDlgButtonChecked(hwndDlg, IDCAUTOLOAD) != BST_CHECKED);
+            return TRUE;
+        case IDCINPUT:
+            if (HIWORD(wParam) == CBN_SELCHANGE)
+            {
+                int i = ComboBox_GetCurSel((HWND)lParam);
+                if (i != CB_ERR) i = ComboBox_GetItemData((HWND)lParam, i);
+                if (i != CB_ERR)
+                {
+                    switch (i)
+                    {
+                    case INPUT_KB:
+                        settings.shared.usemouse = settings.shared.usejoystick = 0;
+                        break;
+                    case INPUT_MOUSE:
+                        settings.shared.usemouse = 1;
+                        settings.shared.usejoystick = 0;
+                        break;
+                    case INPUT_JOYSTICK:
+                        settings.shared.usemouse = 0;
+                        settings.shared.usejoystick = 1;
+                        break;
+                    case INPUT_ALL:
+                        settings.shared.usemouse = settings.shared.usejoystick = 1;
+                        break;
+                    }
+                }
+            }
+            return TRUE;
+
+        case IDCGAMEDIR:
+            if (HIWORD(wParam) == CBN_SELCHANGE)
+            {
+                int i = ComboBox_GetCurSel((HWND)lParam);
+                if (i != CB_ERR) i = ComboBox_GetItemData((HWND)lParam, i);
+                if (i != CB_ERR)
+                {
+                    if (i==0)
+                        settings.gamedir = NULL;
+                    else
+                    {
+                        CACHE1D_FIND_REC *dir = finddirs;
+                        for (int j = 1; dir != NULL; dir = dir->next, j++)
+                        {
+                            if (j == i)
+                            {
+                                settings.gamedir = dir->name;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+            return TRUE;
+        case IDCDATA:
+        {
+            if (HIWORD(wParam) != LBN_SELCHANGE) break;
+            intptr_t i = ListBox_GetCurSel((HWND)lParam);
+            if (i != CB_ERR) i = ListBox_GetItemData((HWND)lParam, i);
+            if (i != CB_ERR)
+            {
+                settings.ini = (INICHAIN const *)i;
+            }
+            return TRUE;
+        }
+        default:
+            break;
+        }
+        break;
+    default:
+        break;
+    }
+    return FALSE;
+}
+
+
+static void SetPage(int pageNum)
+{
+    HWND tab = GetDlgItem(startupdlg, WIN_STARTWIN_TABCTL);
+    auto const cur = SendMessage(tab, TCM_GETCURSEL, 0, 0);
+    ShowWindow(pages[cur], SW_HIDE);
+    SendMessage(tab, TCM_SETCURSEL, pageNum, 0);
+    ShowWindow(pages[pageNum], SW_SHOW);
+    mode = pageNum;
+
+    SetFocus(GetDlgItem(startupdlg, WIN_STARTWIN_TABCTL));
+}
+
+static void EnableConfig(bool n)
+{
+    //EnableWindow(GetDlgItem(startupdlg, WIN_STARTWIN_CANCEL), n);
+    EnableWindow(GetDlgItem(startupdlg, WIN_STARTWIN_START), n);
+    EnableWindow(GetDlgItem(pages[TAB_CONFIG], IDCDATA), n);
+    EnableWindow(GetDlgItem(pages[TAB_CONFIG], IDCFULLSCREEN), n);
+    EnableWindow(GetDlgItem(pages[TAB_CONFIG], IDCGAMEDIR), n);
+    EnableWindow(GetDlgItem(pages[TAB_CONFIG], IDCINPUT), n);
+    //EnableWindow(GetDlgItem(pages[TAB_CONFIG], IDCPOLYMER), n);
+    EnableWindow(GetDlgItem(pages[TAB_CONFIG], IDCVMODE), n);
+}
+
+static INT_PTR CALLBACK startup_dlgproc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+    static HBITMAP hbmp = NULL;
+
+    switch (uMsg)
+    {
+    case WM_INITDIALOG:
+    {
+        // Fetch the positions (in screen coordinates) of all the windows we need to tweak
+        RECT chrome = {};
+        AdjustWindowRect(&chrome, GetWindowLong(hwndDlg, GWL_STYLE), FALSE);
+        RECT rdlg;
+        GetWindowRect(hwndDlg, &rdlg);
+
+        // Knock off the non-client area of the main dialogue to give just the client area
+        rdlg.left -= chrome.left;
+        rdlg.top -= chrome.top;
+        rdlg.right -= chrome.right;
+        rdlg.bottom -= chrome.bottom;
+
+        RECT rtab;
+        GetWindowRect(GetDlgItem(hwndDlg, WIN_STARTWIN_TABCTL), &rtab);
+
+        // Translate them to client-relative coordinates wrt the main dialogue window
+        rtab.right -= rtab.left - 1;
+        rtab.bottom -= rtab.top - 1;
+        rtab.left  -= rdlg.left;
+        rtab.top -= rdlg.top;
+
+        RECT rcancel;
+        GetWindowRect(GetDlgItem(hwndDlg, WIN_STARTWIN_CANCEL), &rcancel);
+
+        rcancel.right -= rcancel.left - 1;
+        rcancel.bottom -= rcancel.top - 1;
+        rcancel.left -= rdlg.left;
+        rcancel.top -= rdlg.top;
+
+        RECT rstart;
+        GetWindowRect(GetDlgItem(hwndDlg, WIN_STARTWIN_START), &rstart);
+
+        rstart.right -= rstart.left - 1;
+        rstart.bottom -= rstart.top - 1;
+        rstart.left -= rdlg.left;
+        rstart.top -= rdlg.top;
+
+        // And then convert the main dialogue coordinates to just width/length
+        rdlg.right -= rdlg.left - 1;
+        rdlg.bottom -= rdlg.top - 1;
+        rdlg.left = 0;
+        rdlg.top = 0;
+
+        // Load the bitmap into the bitmap control and fetch its dimensions
+        hbmp = LoadBitmap((HINSTANCE)win_gethinstance(), MAKEINTRESOURCE(RSRC_BMP));
+
+        HWND hwnd = GetDlgItem(hwndDlg, WIN_STARTWIN_BITMAP);
+        SendMessage(hwnd, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hbmp);
+
+        RECT r;
+        GetClientRect(hwnd, &r);
+
+        int const xoffset = r.right;
+        int const yoffset = r.bottom - rdlg.bottom;
+
+        // Shift and resize the controls that require it
+        rtab.left += xoffset;
+        rtab.bottom += yoffset;
+        rcancel.left += xoffset;
+        rcancel.top += yoffset;
+        rstart.left += xoffset;
+        rstart.top += yoffset;
+        rdlg.right += xoffset;
+        rdlg.bottom += yoffset;
+
+        // Move the controls to their new positions
+        MoveWindow(GetDlgItem(hwndDlg, WIN_STARTWIN_TABCTL), rtab.left, rtab.top, rtab.right, rtab.bottom, FALSE);
+        MoveWindow(GetDlgItem(hwndDlg, WIN_STARTWIN_CANCEL), rcancel.left, rcancel.top, rcancel.right, rcancel.bottom, FALSE);
+        MoveWindow(GetDlgItem(hwndDlg, WIN_STARTWIN_START), rstart.left, rstart.top, rstart.right, rstart.bottom, FALSE);
+
+        // Move the main dialogue to the centre of the screen
+        HDC hdc = GetDC(NULL);
+        rdlg.left = (GetDeviceCaps(hdc, HORZRES) - rdlg.right) / 2;
+        rdlg.top = (GetDeviceCaps(hdc, VERTRES) - rdlg.bottom) / 2;
+        ReleaseDC(NULL, hdc);
+        MoveWindow(hwndDlg, rdlg.left + chrome.left, rdlg.top + chrome.left,
+                   rdlg.right + (-chrome.left+chrome.right), rdlg.bottom + (-chrome.top+chrome.bottom), TRUE);
+
+        // Add tabs to the tab control
+        {
+            static char textSetup[] = TEXT("Setup");
+            static char textMessageLog[] = TEXT("Message Log");
+
+            hwnd = GetDlgItem(hwndDlg, WIN_STARTWIN_TABCTL);
+
+            TCITEM tab = {};
+            tab.mask = TCIF_TEXT;
+            tab.pszText = textSetup;
+            SendMessage(hwnd, TCM_INSERTITEM, (WPARAM)TAB_CONFIG, (LPARAM)&tab);
+            tab.mask = TCIF_TEXT;
+            tab.pszText = textMessageLog;
+            SendMessage(hwnd, TCM_INSERTITEM, (WPARAM)TAB_MESSAGES, (LPARAM)&tab);
+
+            // Work out the position and size of the area inside the tab control for the pages
+            ZeroMemory(&r, sizeof(r));
+            GetClientRect(hwnd, &r);
+            SendMessage(hwnd, TCM_ADJUSTRECT, FALSE, (LPARAM)&r);
+            r.right -= r.left-1;
+            r.bottom -= r.top-1;
+            r.top += rtab.top;
+            r.left += rtab.left;
+
+            // Create the pages and position them in the tab control, but hide them
+            pages[TAB_CONFIG] = CreateDialog((HINSTANCE)win_gethinstance(), MAKEINTRESOURCE(WIN_STARTWINPAGE_CONFIG), hwndDlg, ConfigPageProc);
+            SetWindowPos(pages[TAB_CONFIG], hwnd, r.left, r.top, r.right, r.bottom, SWP_HIDEWINDOW);
+
+            pages[TAB_MESSAGES] = GetDlgItem(hwndDlg, WIN_STARTWIN_MESSAGES);
+            SetWindowPos(pages[TAB_MESSAGES], hwnd, r.left, r.top, r.right, r.bottom, SWP_HIDEWINDOW);
+
+            // Tell the editfield acting as the console to exclude the width of the scrollbar
+            GetClientRect(pages[TAB_MESSAGES], &r);
+            r.right -= GetSystemMetrics(SM_CXVSCROLL)+4;
+            r.left = r.top = 0;
+            SendMessage(pages[TAB_MESSAGES], EM_SETRECTNP,0,(LPARAM)&r);
+
+            // Set a tab stop in the game data listbox
+            {
+                DWORD tabs[1] = { 150 };
+                (void)ListBox_SetTabStops(GetDlgItem(pages[TAB_CONFIG], IDCDATA), 1, tabs);
+            }
+
+            SetFocus(GetDlgItem(hwndDlg, WIN_STARTWIN_START));
+            SetWindowText(hwndDlg, apptitle);
+        }
+        return FALSE;
+    }
+
+    case WM_NOTIFY:
+    {
+        auto nmhdr = (LPNMHDR)lParam;
+        if (nmhdr->idFrom != WIN_STARTWIN_TABCTL) break;
+        int const cur = SendMessage(nmhdr->hwndFrom, TCM_GETCURSEL,0,0);
+        switch (nmhdr->code)
+        {
+            case TCN_SELCHANGING:
+            case TCN_SELCHANGE:
+                if (cur < 0 || !pages[cur])
+                    break;
+                ShowWindow(pages[cur], nmhdr->code == TCN_SELCHANGING ? SW_HIDE : SW_SHOW);
+                return TRUE;
+        }
+        break;
+    }
+
+    case WM_CLOSE:
+        if (mode == TAB_CONFIG) done = 0;
+        else quitevent++;
+        return TRUE;
+
+    case WM_DESTROY:
+        if (hbmp)
+        {
+            DeleteObject(hbmp);
+            hbmp = NULL;
+        }
+
+        if (pages[TAB_CONFIG])
+        {
+            DestroyWindow(pages[TAB_CONFIG]);
+            pages[TAB_CONFIG] = NULL;
+        }
+
+        startupdlg = NULL;
+        return TRUE;
+
+    case WM_COMMAND:
+        switch (LOWORD(wParam))
+        {
+        case WIN_STARTWIN_CANCEL:
+            if (mode == TAB_CONFIG) done = 0;
+            else quitevent++;
+            return TRUE;
+        case WIN_STARTWIN_START:
+            done = 1;
+            return TRUE;
+        }
+        return FALSE;
+
+    case WM_CTLCOLORSTATIC:
+        if ((HWND)lParam == pages[TAB_MESSAGES])
+            return (BOOL)(intptr_t)GetSysColorBrush(COLOR_WINDOW);
+        break;
+
+    default:
+        break;
+    }
+
+    return FALSE;
+}
+
+
+int32_t startwin_open(void)
+{
+    if (startupdlg) return 1;
+    INITCOMMONCONTROLSEX icc = { sizeof(icc), ICC_TAB_CLASSES };
+    InitCommonControlsEx(&icc);
+    startupdlg = CreateDialog((HINSTANCE)win_gethinstance(), MAKEINTRESOURCE(WIN_STARTWIN), NULL, startup_dlgproc);
+    if (startupdlg)
+    {
+        SetPage(TAB_MESSAGES);
+        EnableConfig(0);
+        return 0;
+    }
+    return -1;
+}
+
+int32_t startwin_close(void)
+{
+    if (!startupdlg) return 1;
+    DestroyWindow(startupdlg);
+    startupdlg = NULL;
+    return 0;
+}
+
+int32_t startwin_puts(const char *buf)
+{
+    if (!startupdlg) return 1;
+
+    const HWND edctl = pages[TAB_MESSAGES];
+
+    if (!edctl) return -1;
+
+    static HWND dactrl = NULL;
+    if (!dactrl) dactrl = GetDlgItem(startupdlg, WIN_STARTWIN_TABCTL);
+
+    int const vis = ((int)SendMessage(dactrl, TCM_GETCURSEL,0,0) == TAB_MESSAGES);
+
+    if (vis)
+        SendMessage(edctl, WM_SETREDRAW, FALSE, 0);
+
+    int const curlen = SendMessage(edctl, WM_GETTEXTLENGTH, 0,0);
+    SendMessage(edctl, EM_SETSEL, (WPARAM)curlen, (LPARAM)curlen);
+
+    int const   numlines = SendMessage(edctl, EM_GETLINECOUNT, 0, 0);
+    static bool newline  = false;
+    const char *p        = buf;
+
+    while (*p)
+    {
+        if (newline)
+        {
+            SendMessage(edctl, EM_REPLACESEL, 0, (LPARAM)"\r\n");
+            newline = false;
+        }
+        const char *q = p;
+        while (*q && *q != '\n') q++;
+        static char workbuf[1024];
+        Bmemcpy(workbuf, p, q-p);
+        if (*q == '\n')
+        {
+            if (!q[1])
+            {
+                newline = true;
+                workbuf[q-p] = 0;
+            }
+            else
+            {
+                workbuf[q-p] = '\r';
+                workbuf[q-p+1] = '\n';
+                workbuf[q-p+2] = 0;
+            }
+            p = q+1;
+        }
+        else
+        {
+            workbuf[q-p] = 0;
+            p = q;
+        }
+        SendMessage(edctl, EM_REPLACESEL, 0, (LPARAM)workbuf);
+    }
+
+    int const newnumlines = SendMessage(edctl, EM_GETLINECOUNT, 0, 0);
+    SendMessage(edctl, EM_LINESCROLL, 0, newnumlines - numlines);
+
+    if (vis)
+        SendMessage(edctl, WM_SETREDRAW, TRUE, 0);
+
+    return 0;
+}
+
+int32_t startwin_settitle(const char *str)
+{
+    if (!startupdlg) return 1;
+    SetWindowText(startupdlg, str);
+    return 0;
+}
+
+int32_t startwin_idle(void *v)
+{
+    if (!startupdlg || !IsWindow(startupdlg)) return 0;
+    if (IsDialogMessage(startupdlg, (MSG *)v)) return 1;
+    return 0;
+}
+
+int32_t startwin_run(void)
+{
+    if (!startupdlg) return 1;
+
+    done = -1;
+
+    SetPage(TAB_CONFIG);
+    EnableConfig(1);
+
+#ifdef POLYMER
+    settings.polymer = (glrendmode == REND_POLYMER);
+#else
+    settings.polymer = 0;
+#endif
+
+    settings.shared = gSetup;
+    settings.ini = pINISelected;
+    settings.gamedir = g_modDir;
+
+    PopulateForm(-1);
+
+    do
+    {
+        MSG msg;
+
+        switch (GetMessage(&msg, NULL, 0,0))
+        {
+        case 0:
+            done = 1;
+            break;
+        case -1:
+            return -1;
+        default:
+            if (IsWindow(startupdlg) && IsDialogMessage(startupdlg, &msg))
+                break;
+            TranslateMessage(&msg);
+            DispatchMessage(&msg);
+            break;
+        }
+    }
+    while (done < 0);
+
+    SetPage(TAB_MESSAGES);
+    EnableConfig(0);
+
+    if (done)
+    {
+        gSetup = settings.shared;
+        glrendmode = (settings.polymer) ? REND_POLYMER : REND_POLYMOST;
+        pINISelected = settings.ini;
+        Bstrcpy(g_modDir, (gNoSetup == 0 && settings.gamedir != NULL) ? settings.gamedir : "/");
+    }
+
+    return done;
+}
+
+#endif // STARTUP_SETUP_WINDOW
diff --git a/source/blood/src/startwin.game.h b/source/blood/src/startwin.game.h
new file mode 100644
index 000000000..d97dc31cb
--- /dev/null
+++ b/source/blood/src/startwin.game.h
@@ -0,0 +1,48 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+// resource ids
+#define WIN_STARTWIN		1000
+#define WIN_STARTWINPAGE_CONFIG 2000
+#define WIN_STARTWIN_BITMAP	100	// banner bitmap
+#define WIN_STARTWIN_TABCTL	101
+#define WIN_STARTWIN_CANCEL	IDCANCEL
+#define WIN_STARTWIN_START	IDOK
+
+#define WIN_STARTWIN_MESSAGES	104	// output list box
+
+#define RSRC_ICON		100
+#define RSRC_BMP		200
+
+// config page
+#define IDCFULLSCREEN	100
+#define IDCVMODE	    101
+#define IDCSOUNDDRV	    102
+#define IDCMIDIDEV	    103
+#define IDCCDADEV	    104
+#define IDCALWAYSSHOW	105
+#define IDCDATA         106
+#define IDCGAMEDIR      107
+#define IDCPOLYMER      108
+#define IDCAUTOLOAD     109
+#define IDCINPUT	    110
diff --git a/source/blood/src/tile.cpp b/source/blood/src/tile.cpp
new file mode 100644
index 000000000..dab517a14
--- /dev/null
+++ b/source/blood/src/tile.cpp
@@ -0,0 +1,278 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "compat.h"
+#include "build.h"
+#include "common.h"
+#include "common_game.h"
+
+#include "blood.h"
+#include "config.h"
+#include "resource.h"
+#include "tile.h"
+
+void qloadvoxel(int32_t nVoxel)
+{
+    static int nLastVoxel = 0;
+    dassert(nVoxel >= 0 && nVoxel < kMaxVoxels);
+    DICTNODE *hVox = gSysRes.Lookup(nVoxel, "KVX");
+    if (!hVox)
+        ThrowError("Missing voxel #%d", nVoxel);
+    if (!hVox->lockCount)
+        voxoff[nLastVoxel][0] = 0;
+    nLastVoxel = nVoxel;
+    char *pVox = (char*)gSysRes.Lock(hVox);
+    for (int i = 0; i < MAXVOXMIPS; i++)
+    {
+        int nSize = *((int*)pVox);
+#if B_BIG_ENDIAN == 1
+        nSize = B_LITTLE32(nSize);
+#endif
+        pVox += 4;
+        voxoff[nVoxel][i] = (intptr_t)pVox;
+        pVox += nSize;
+    }
+}
+
+void CalcPicsiz(int a1, int a2, int a3)
+{
+    int nP = 0;
+    for (int i = 2; i <= a2; i<<= 1)
+        nP++;
+    for (int i = 2; i <= a3; i<<= 1)
+        nP+=1<<4;
+    picsiz[a1] = nP;
+}
+
+CACHENODE tileNode[kMaxTiles];
+
+bool artLoaded = false;
+int nTileFiles = 0;
+
+int tileStart[256];
+int tileEnd[256];
+int hTileFile[256];
+
+char surfType[kMaxTiles];
+signed char tileShade[kMaxTiles];
+short voxelIndex[kMaxTiles];
+
+const char *pzBaseFileName = "TILES%03i.ART"; //"TILES%03i.ART";
+
+int tileInit(char a1, const char *a2)
+{
+    UNREFERENCED_PARAMETER(a1);
+    if (artLoaded)
+        return 1;
+    artLoadFiles(a2 ? a2 : pzBaseFileName, MAXCACHE1DSIZE);
+    for (int i = 0; i < kMaxTiles; i++)
+        voxelIndex[i] = 0;
+
+    int hFile = kopen4loadfrommod("SURFACE.DAT", 0);
+    if (hFile != -1)
+    {
+        kread(hFile, surfType, sizeof(surfType));
+        kclose(hFile);
+    }
+    hFile = kopen4loadfrommod("VOXEL.DAT", 0);
+    if (hFile != -1)
+    {
+        kread(hFile, voxelIndex, sizeof(voxelIndex));
+#if B_BIG_ENDIAN == 1
+        for (int i = 0; i < kMaxTiles; i++)
+            voxelIndex[i] = B_LITTLE16(voxelIndex[i]);
+#endif
+        kclose(hFile);
+    }
+    hFile = kopen4loadfrommod("SHADE.DAT", 0);
+    if (hFile != -1)
+    {
+        kread(hFile, tileShade, sizeof(tileShade));
+        kclose(hFile);
+    }
+    for (int i = 0; i < kMaxTiles; i++)
+    {
+        if (voxelIndex[i] >= 0 && voxelIndex[i] < kMaxVoxels)
+            SetBitString((char*)voxreserve, voxelIndex[i]);
+    }
+
+    artLoaded = 1;
+
+    #ifdef USE_OPENGL
+    PolymostProcessVoxels_Callback = tileProcessGLVoxels;
+    #endif
+
+    return 1;
+}
+
+#ifdef USE_OPENGL
+void tileProcessGLVoxels(void)
+{
+    static bool voxInit = false;
+    if (voxInit)
+        return;
+    voxInit = true;
+    for (int i = 0; i < kMaxVoxels; i++)
+    {
+        DICTNODE *hVox = gSysRes.Lookup(i, "KVX");
+        if (!hVox)
+            continue;
+        char *pVox = (char*)gSysRes.Load(hVox);
+        voxmodels[i] = loadkvxfrombuf(pVox, hVox->size);
+    }
+}
+#endif
+
+char * tileLoadTile(int nTile)
+{
+    if (!waloff[nTile]) tileLoad(nTile);
+    return (char*)waloff[nTile];
+}
+
+char * tileAllocTile(int nTile, int x, int y, int ox, int oy)
+{
+    dassert(nTile >= 0 && nTile < kMaxTiles);
+    char *p = (char*)tileCreate(nTile, x, y);
+    dassert(p != NULL);
+    picanm[nTile].xofs = ClipRange(ox, -127, 127);
+    picanm[nTile].yofs = ClipRange(oy, -127, 127);
+    return (char*)waloff[nTile];
+}
+
+void tilePreloadTile(int nTile)
+{
+    int n = 0;
+    switch (picanm[nTile].extra&7)
+    {
+    case 0:
+        n = 1;
+        break;
+    case 1:
+        n = 5;
+        break;
+    case 2:
+        n = 8;
+        break;
+    case 3:
+        n = 2;
+        break;
+    case 6:
+    case 7:
+        if (voxelIndex[nTile] < 0 || voxelIndex[nTile] >= kMaxVoxels)
+        {
+            voxelIndex[nTile] = -1;
+            picanm[nTile].extra &= ~7;
+        }
+        else
+            qloadvoxel(voxelIndex[nTile]);
+        break;
+    }
+    while(n--)
+    {
+        if (picanm[nTile].sf&PICANM_ANIMTYPE_MASK)
+        {
+            for (int frame = picanm[nTile].num; frame >= 0; frame--)
+            {
+                if ((picanm[nTile].sf&PICANM_ANIMTYPE_MASK) == PICANM_ANIMTYPE_BACK)
+                    tileLoadTile(nTile-frame);
+                else
+                    tileLoadTile(nTile+frame);
+            }
+        }
+        else
+            tileLoadTile(nTile);
+        nTile += 1+picanm[nTile].num;
+    }
+}
+
+extern int nPrecacheCount;
+extern char precachehightile[2][(MAXTILES+7)>>3];
+
+void tilePrecacheTile(int nTile, int nType)
+{
+    int n = 0;
+    switch (picanm[nTile].extra&7)
+    {
+    case 0:
+        n = 1;
+        break;
+    case 1:
+        n = 5;
+        break;
+    case 2:
+        n = 8;
+        break;
+    case 3:
+        n = 2;
+        break;
+    }
+    while(n--)
+    {
+        if (picanm[nTile].sf&PICANM_ANIMTYPE_MASK)
+        {
+            for (int frame = picanm[nTile].num; frame >= 0; frame--)
+            {
+                int tile;
+                if ((picanm[nTile].sf&PICANM_ANIMTYPE_MASK) == PICANM_ANIMTYPE_BACK)
+                    tile = nTile-frame;
+                else
+                    tile = nTile+frame;
+                if (!TestBitString(gotpic, tile))
+                {
+                    nPrecacheCount++;
+                    SetBitString(gotpic, tile);
+                }
+                SetBitString(precachehightile[nType], tile);
+            }
+        }
+        else
+        {
+            if (!TestBitString(gotpic, nTile))
+            {
+                nPrecacheCount++;
+                SetBitString(gotpic, nTile);
+            }
+            SetBitString(precachehightile[nType], nTile);
+        }
+        nTile += 1+picanm[nTile].num;
+    }
+}
+
+char tileGetSurfType(int hit)
+{
+    int n = hit &0x1fff;
+    switch (hit&0xe000)
+    {
+    case 0x4000:
+        return surfType[sector[n].floorpicnum];
+    case 0x6000:
+        return surfType[sector[n].ceilingpicnum];
+    case 0x8000:
+        return surfType[wall[n].picnum];
+    case 0xc000:
+        return surfType[sprite[n].picnum];
+    }
+    return 0;
+}
diff --git a/source/blood/src/tile.h b/source/blood/src/tile.h
new file mode 100644
index 000000000..3a9261ca4
--- /dev/null
+++ b/source/blood/src/tile.h
@@ -0,0 +1,61 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common_game.h"
+#include "blood.h"
+
+enum SurfaceType {
+    kSurf0 = 0,
+    kSurf1,
+    kSurf2,
+    kSurf3,
+    kSurf4,
+    kSurf5,
+    kSurf6,
+    kSurf7,
+    kSurf8,
+    kSurf9,
+    kSurf10,
+    kSurf11,
+    kSurf12,
+    kSurf13,
+    kSurf14,
+    kSurfMax
+};
+
+extern char surfType[kMaxTiles];
+extern signed char tileShade[kMaxTiles];
+extern short voxelIndex[kMaxTiles];
+
+void qloadvoxel(int32_t nVoxel);
+void CalcPicsiz(int a1, int a2, int a3);
+int tileInit(char a1, const char *a2);
+#ifdef USE_OPENGL
+void tileProcessGLVoxels(void);
+#endif
+char * tileLoadTile(int nTile);
+char * tileAllocTile(int nTile, int x, int y, int ox, int oy);
+void tilePreloadTile(int nTile);
+void tilePrecacheTile(int nTile, int nType = 1);
+char tileGetSurfType(int hit);
diff --git a/source/blood/src/trig.cpp b/source/blood/src/trig.cpp
new file mode 100644
index 000000000..0fd0b4115
--- /dev/null
+++ b/source/blood/src/trig.cpp
@@ -0,0 +1,78 @@
+#include "build.h"
+#include "pragmas.h"
+#include "common_game.h"
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "resource.h"
+#include "trig.h"
+
+int costable[2048];
+
+int OctantTable[8] = { 5, 6, 2, 1, 4, 7, 3, 0 };
+
+int GetOctant(int x, int y)
+{
+    int vc = klabs(x)-klabs(y);
+    return OctantTable[7-(x<0)-(y<0)*2-(vc<0)*4];
+}
+
+void RotateVector(int *dx, int *dy, int nAngle)
+{
+    int ox = *dx;
+    int oy = *dy;
+    *dx = dmulscale30r(ox, Cos(nAngle), -oy, Sin(nAngle));
+    *dy = dmulscale30r(ox, Sin(nAngle), oy, Cos(nAngle));
+}
+
+void RotatePoint(int *x, int *y, int nAngle, int ox, int oy)
+{
+    int dx = *x-ox;
+    int dy = *y-oy;
+    *x = ox+dmulscale30r(dx, Cos(nAngle), -dy, Sin(nAngle));
+    *y = oy+dmulscale30r(dx, Sin(nAngle), dy, Cos(nAngle));
+}
+
+void trigInit(Resource &Res)
+{
+    DICTNODE *pTable = Res.Lookup("cosine","dat");
+    if (!pTable)
+        ThrowError("Cosine table not found");
+    if (pTable->size != 2048)
+        ThrowError("Cosine table incorrect size");
+    memcpy(costable, Res.Load(pTable), pTable->size);
+#if B_BIG_ENDIAN == 1
+    for (int i = 0; i < 512; i++)
+    {
+        costable[i] = B_LITTLE32(costable[i]);
+    }
+#endif
+    costable[512] = 0;
+    for (int i = 513; i <= 1024; i++)
+    {
+        costable[i] = -costable[1024-i];
+    }
+    for (int i = 1025; i < 2048; i++)
+    {
+        costable[i] = costable[2048 - i];
+    }
+}
diff --git a/source/blood/src/trig.h b/source/blood/src/trig.h
new file mode 100644
index 000000000..96f882d43
--- /dev/null
+++ b/source/blood/src/trig.h
@@ -0,0 +1,42 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "resource.h"
+
+extern int costable[2048];
+
+int GetOctant(int x, int y);
+void RotateVector(int *dx, int *dy, int nAngle);
+void RotatePoint(int *x, int *y, int nAngle, int ox, int oy);
+void trigInit(Resource &Res);
+
+inline int Sin(int ang)
+{
+    return costable[(ang - 512) & 2047];
+}
+
+inline int Cos(int ang)
+{
+    return costable[ang & 2047];
+}
\ No newline at end of file
diff --git a/source/blood/src/triggers.cpp b/source/blood/src/triggers.cpp
new file mode 100644
index 000000000..d7f583a11
--- /dev/null
+++ b/source/blood/src/triggers.cpp
@@ -0,0 +1,4033 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <random>
+#include <iostream>
+
+#include "build.h"
+#include "compat.h"
+#include "mmulti.h"
+#include "common_game.h"
+
+#include "ai.h"
+#include "actor.h"
+#include "blood.h"
+#include "db.h"
+#include "endgame.h"
+#include "eventq.h"
+
+#include "aiunicult.h"
+#include "fx.h"
+#include "gameutil.h"
+#include "gib.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "player.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "triggers.h"
+#include "trig.h"
+#include "view.h"
+
+int basePath[kMaxSectors];
+
+void FireballTrapSeqCallback(int, int);
+void UniMissileTrapSeqCallback(int, int);
+void MGunFireSeqCallback(int, int);
+void MGunOpenSeqCallback(int, int);
+
+int nFireballTrapClient = seqRegisterClient(FireballTrapSeqCallback);
+int nUniMissileTrapClient = seqRegisterClient(UniMissileTrapSeqCallback);
+int nMGunFireClient = seqRegisterClient(MGunFireSeqCallback);
+int nMGunOpenClient = seqRegisterClient(MGunOpenSeqCallback);
+
+unsigned int GetWaveValue(unsigned int nPhase, int nType)
+{
+    switch (nType)
+    {
+    case 0:
+        return 0x8000-(Cos((nPhase<<10)>>16)>>15);
+    case 1:
+        return nPhase;
+    case 2:
+        return 0x10000-(Cos((nPhase<<9)>>16)>>14);
+    case 3:
+        return Sin((nPhase<<9)>>16)>>14;
+    }
+    return nPhase;
+}
+
+char SetSpriteState(int nSprite, XSPRITE *pXSprite, int nState)
+{
+    if ((pXSprite->busy&0xffff) == 0 && pXSprite->state == nState)
+        return 0;
+    pXSprite->busy = nState<<16;
+    pXSprite->state = nState;
+    evKill(nSprite, 3);
+    if ((sprite[nSprite].hitag & 16) != 0 && sprite[nSprite].type >= kDudeBase && sprite[nSprite].type < kDudeMax)
+    {
+        pXSprite->respawnPending = 3;
+        evPost(nSprite, 3, gGameOptions.nMonsterRespawnTime, CALLBACK_ID_9);
+        return 1;
+    }
+    if (pXSprite->restState != nState && pXSprite->waitTime > 0)
+        evPost(nSprite, 3, (pXSprite->waitTime*120) / 10, pXSprite->restState ? COMMAND_ID_1 : COMMAND_ID_0);
+    if (pXSprite->txID)
+    {
+        if (pXSprite->command != 5 && pXSprite->triggerOn && pXSprite->state)
+            evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID)pXSprite->command);
+        if (pXSprite->command != 5 && pXSprite->triggerOff && !pXSprite->state)
+            evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID)pXSprite->command);
+    }
+    return 1;
+}
+
+char SetWallState(int nWall, XWALL *pXWall, int nState)
+{
+    if ((pXWall->busy&0xffff) == 0 && pXWall->state == nState)
+        return 0;
+    pXWall->busy = nState<<16;
+    pXWall->state = nState;
+    evKill(nWall, 0);
+    if (pXWall->restState != nState && pXWall->waitTime > 0)
+        evPost(nWall, 0, (pXWall->waitTime*120) / 10, pXWall->restState ? COMMAND_ID_1 : COMMAND_ID_0);
+    if (pXWall->txID)
+    {
+        if (pXWall->command != 5 && pXWall->triggerOn && pXWall->state)
+            evSend(nWall, 0, pXWall->txID, (COMMAND_ID)pXWall->command);
+        if (pXWall->command != 5 && pXWall->triggerOff && !pXWall->state)
+            evSend(nWall, 0, pXWall->txID, (COMMAND_ID)pXWall->command);
+    }
+    return 1;
+}
+
+char SetSectorState(int nSector, XSECTOR *pXSector, int nState)
+{
+    if ((pXSector->busy&0xffff) == 0 && pXSector->state == nState)
+        return 0;
+    pXSector->busy = nState<<16;
+    pXSector->state = nState;
+    evKill(nSector, 6);
+    if (nState == 1)
+    {
+        if (pXSector->command != 5 && pXSector->triggerOn && pXSector->txID)
+            evSend(nSector, 6, pXSector->txID, (COMMAND_ID)pXSector->command);
+        if (pXSector->at1b_2)
+        {
+            pXSector->at1b_2 = 0;
+            pXSector->at1b_3 = 0;
+        }
+        else if (pXSector->atf_6)
+            evPost(nSector, 6, (pXSector->waitTimeA * 120) / 10, COMMAND_ID_0);
+    }
+    else
+    {
+        if (pXSector->command != 5 && pXSector->triggerOff && pXSector->txID)
+            evSend(nSector, 6, pXSector->txID, (COMMAND_ID)pXSector->command);
+        if (pXSector->at1b_3)
+        {
+            pXSector->at1b_2 = 0;
+            pXSector->at1b_3 = 0;
+        }
+        else if (pXSector->atf_7)
+            evPost(nSector, 6, (pXSector->waitTimeB * 120) / 10, COMMAND_ID_1);
+    }
+    return 1;
+}
+
+int gBusyCount = 0;
+
+enum BUSYID {
+    BUSYID_0 = 0,
+    BUSYID_1,
+    BUSYID_2,
+    BUSYID_3,
+    BUSYID_4,
+    BUSYID_5,
+    BUSYID_6,
+    BUSYID_7,
+};
+
+struct BUSY {
+    int at0;
+    int at4;
+    int at8;
+    BUSYID atc;
+};
+
+BUSY gBusy[128];
+
+void AddBusy(int a1, BUSYID a2, int nDelta)
+{
+    dassert(nDelta != 0);
+    int i;
+    for (i = 0; i < gBusyCount; i++)
+    {
+        if (gBusy[i].at0 == a1 && gBusy[i].atc == a2)
+            break;
+    }
+    if (i == gBusyCount)
+    {
+        if (gBusyCount == 128)
+            return;
+        gBusy[i].at0 = a1;
+        gBusy[i].atc = a2;
+        gBusy[i].at8 = nDelta > 0 ? 0 : 65536;
+        gBusyCount++;
+    }
+    gBusy[i].at4 = nDelta;
+}
+
+void ReverseBusy(int a1, BUSYID a2)
+{
+    int i;
+    for (i = 0; i < gBusyCount; i++)
+    {
+        if (gBusy[i].at0 == a1 && gBusy[i].atc == a2)
+        {
+            gBusy[i].at4 = -gBusy[i].at4;
+            break;
+        }
+    }
+}
+
+unsigned int GetSourceBusy(EVENT a1)
+{
+    int nIndex = a1.index;
+    switch (a1.type)
+    {
+    case 6:
+    {
+        int nXIndex = sector[nIndex].extra;
+        dassert(nXIndex > 0 && nXIndex < kMaxXSectors);
+        return xsector[nXIndex].busy;
+    }
+    case 0:
+    {
+        int nXIndex = wall[nIndex].extra;
+        dassert(nXIndex > 0 && nXIndex < kMaxXWalls);
+        return xwall[nXIndex].busy;
+    }
+    case 3:
+    {
+        int nXIndex = sprite[nIndex].extra;
+        dassert(nXIndex > 0 && nXIndex < kMaxXSprites);
+        return xsprite[nXIndex].busy;
+    }
+    }
+    return 0;
+}
+
+void sub_43CF8(spritetype *pSprite, XSPRITE *pXSprite, EVENT a3)
+{
+    switch (a3.cmd)
+    {
+    case 30:
+    {
+        int nPlayer = pXSprite->data4;
+        if (nPlayer >= 0 && nPlayer < gNetPlayers)
+        {
+            PLAYER *pPlayer = &gPlayer[nPlayer];
+            if (pPlayer->pXSprite->health > 0)
+            {
+                pPlayer->at181[8] = ClipHigh(pPlayer->at181[8]+pXSprite->data3, gAmmoInfo[8].at0);
+                pPlayer->atcb[9] = 1;
+                if (pPlayer->atbd != 9)
+                {
+                    pPlayer->atc3 = 0;
+                    pPlayer->atbe = 9;
+                }
+                evKill(pSprite->index, 3);
+            }
+        }
+        break;
+    }
+    case 35:
+    {
+        int nTarget = pXSprite->target;
+        if (nTarget >= 0 && nTarget < kMaxSprites)
+        {
+            if (!pXSprite->stateTimer)
+            {
+                spritetype *pTarget = &sprite[nTarget];
+                if (pTarget->statnum == 6 && !(pTarget->hitag&32) && pTarget->extra > 0 && pTarget->extra < kMaxXSprites)
+                {
+                    int top, bottom;
+                    GetSpriteExtents(pSprite, &top, &bottom);
+                    int nType = pTarget->type-kDudeBase;
+                    DUDEINFO *pDudeInfo = &dudeInfo[nType];
+                    int z1 = (top-pSprite->z)-256;
+                    int x = pTarget->x;
+                    int y = pTarget->y;
+                    int z = pTarget->z;
+                    int nDist = approxDist(x - pSprite->x, y - pSprite->y);
+                    if (nDist != 0 && cansee(pSprite->x, pSprite->y, top, pSprite->sectnum, x, y, z, pTarget->sectnum))
+                    {
+                        int t = divscale(nDist, 0x1aaaaa, 12);
+                        x += (xvel[nTarget]*t)>>12;
+                        y += (yvel[nTarget]*t)>>12;
+                        int angBak = pSprite->ang;
+                        pSprite->ang = getangle(x-pSprite->x, y-pSprite->y);
+                        int dx = Cos(pSprite->ang)>>16;
+                        int dy = Sin(pSprite->ang)>>16;
+                        int tz = pTarget->z - (pTarget->yrepeat * pDudeInfo->aimHeight) * 4;
+                        int dz = divscale(tz - top - 256, nDist, 10);
+                        int nMissileType = 316+(pXSprite->data3 ? 1 : 0);
+                        int t2;
+                        if (!pXSprite->data3)
+                            t2 = 120 / 10.0;
+                        else
+                            t2 = (3*120) / 10.0;
+                        spritetype *pMissile = actFireMissile(pSprite, 0, z1, dx, dy, dz, nMissileType);
+                        if (pMissile)
+                        {
+                            pMissile->owner = pSprite->owner;
+                            pXSprite->stateTimer = 1;
+                            evPost(pSprite->index, 3, t2, CALLBACK_ID_20);
+                            pXSprite->data3 = ClipLow(pXSprite->data3-1, 0);
+                        }
+                        pSprite->ang = angBak;
+                    }
+                }
+            }
+        }
+        return;
+    }
+    }
+    actPostSprite(pSprite->index, kStatFree);
+}
+
+void ActivateGenerator(int);
+
+void OperateSprite(int nSprite, XSPRITE *pXSprite, EVENT a3)
+{
+    spritetype *pSprite = &sprite[nSprite];
+    switch (a3.cmd)
+    {
+    case 6:
+        pXSprite->locked = 1;
+        switch (pSprite->type) {
+        case kGDXWindGenerator:
+            stopWindOnSectors(pXSprite);
+            break;
+        }
+        return;
+    case 7:
+        pXSprite->locked = 0;
+        return;
+    case 8:
+        pXSprite->locked = pXSprite->locked ^ 1;
+        switch(pSprite->type) {
+         case kGDXWindGenerator:
+            if (pXSprite->locked == 1) stopWindOnSectors(pXSprite);
+            break;
+        }
+        return;
+    }
+    if (pSprite->statnum == 6 && pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
+    {
+        switch (a3.cmd)
+        {
+        case 0:
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case 35:
+            if (pXSprite->state)
+                break;
+            fallthrough__;
+        case 1:
+        case 30:
+        case 33:
+            if (!pXSprite->state)
+                SetSpriteState(nSprite, pXSprite, 1);
+            aiActivateDude(pSprite, pXSprite);
+            break;
+        }
+        return;
+    }
+    switch (pSprite->type)
+    {
+    
+    /* - Random Event Switch takes random data field and uses it as TX ID - */
+    /* - ranged TX ID is now supported also - */
+    case kGDXRandomTX:
+    {
+        std::default_random_engine rng; int tx = 0;
+        // set range of TX ID if data2 and data3 is empty.
+        if (pXSprite->data1 > 0 && pXSprite->data2 <= 0 && pXSprite->data3 <= 0 && pXSprite->data4 > 0) {
+
+            // data1 must be less than data4
+            if (pXSprite->data1 > pXSprite->data4) {
+                int tmp = pXSprite->data1;
+                pXSprite->data1 = pXSprite->data4;
+                pXSprite->data4 = tmp;
+            }
+            
+            int total = pXSprite->data4 - pXSprite->data1;
+            int data1 = pXSprite->data1; int result = 0;
+
+            // use true random only for single player mode
+            if (gGameOptions.nGameType == 0 && !VanillaMode() && !DemoRecordStatus()) {
+                rng.seed(std::random_device()());
+                pXSprite->txID = (int)my_random(pXSprite->data1, pXSprite->data4);
+            
+            // otherwise use Blood's default one. In the future it maybe possible to make
+            // host send info to clients about what was generated.
+            } else {
+                pXSprite->txID = Random(total) + data1;
+            }
+
+        } else if ((tx = GetRandDataVal(NULL,pSprite)) > 0) { 
+            pXSprite->txID = tx; 
+        }
+
+        switch (a3.cmd)
+        {
+        case COMMAND_ID_0:
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case COMMAND_ID_1:
+            SetSpriteState(nSprite, pXSprite, 1);
+            break;
+        default:
+            SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1);
+            break;
+        }
+
+        break;
+    }
+    /* - Sequential Switch takes values from data fields starting from data1 and uses it as TX ID - */
+    /* - ranged TX ID is now supported also - */
+    case kGDXSequentialTX:
+    {
+        bool range = false; int cnt = 3; int tx = 0;
+        // set range of TX ID if data2 and data3 is empty.
+        if (pXSprite->data1 > 0 && pXSprite->data2 <= 0 && pXSprite->data3 <= 0 && pXSprite->data4 > 0) {
+
+            // data1 must be less than data4
+            if (pXSprite->data1 > pXSprite->data4) {
+                int tmp = pXSprite->data1;
+                pXSprite->data1 = (short)pXSprite->data4;
+                pXSprite->data4 = tmp;
+            }
+            
+            // force send command to all TX id in a range
+            if (pSprite->hitag == 1) {
+                for (int i = pXSprite->data1; i <= pXSprite->data4; i++) {
+                    evSend(nSprite, 3, i, (COMMAND_ID) pXSprite->command);
+                }
+
+                pXSprite->txIndex = 0;
+                SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1);
+                break;
+            }
+
+            // Make sure txIndex is correct as we store current index of TX ID here.
+            if (pXSprite->txIndex < pXSprite->data1) pXSprite->txIndex = pXSprite->data1;
+            else if (pXSprite->txIndex > pXSprite->data4) pXSprite->txIndex = pXSprite->data4;
+
+            range = true;
+
+        } else {
+            // Make sure txIndex is correct as we store current index of data field here.
+            if (pXSprite->txIndex > 3) pXSprite->txIndex = 0;
+            else if (pXSprite->txIndex < 0) pXSprite->txIndex = 3;
+        }
+
+        switch (a3.cmd) {
+            case COMMAND_ID_0:
+                if (range == false) {
+                    while (cnt-- >= 0) { // skip empty data fields
+                        pXSprite->txIndex--;
+                        if (pXSprite->txIndex < 0) pXSprite->txIndex = 3;
+                        tx = GetDataVal(pSprite, pXSprite->txIndex);
+                        if (tx < 0) ThrowError(" -- Current data index is negative");
+                        if (tx > 0) break;
+                        continue;
+                    }
+                } else {
+                    pXSprite->txIndex--;
+                    if (pXSprite->txIndex < pXSprite->data1) {
+                        pXSprite->txIndex = pXSprite->data4;
+                    }
+                    tx = pXSprite->txIndex;
+                }
+                break;
+
+            default:
+                if (range == false) {
+                    while (cnt-- >= 0) { // skip empty data fields
+                        if (pXSprite->txIndex > 3) pXSprite->txIndex = 0;
+                        tx = GetDataVal(pSprite, pXSprite->txIndex);
+                        if (tx < 0) ThrowError(" ++ Current data index is negative");
+                        pXSprite->txIndex++;
+                        if (tx > 0) break;
+                        continue;
+                    }
+                } else {
+                    tx = pXSprite->txIndex;
+                    if (pXSprite->txIndex >= pXSprite->data4) {
+                        pXSprite->txIndex = pXSprite->data1;
+                        break;
+                    }
+                    pXSprite->txIndex++;
+                }
+                break;
+        }
+
+        pXSprite->txID = (short)tx;
+        SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1);
+        break;
+    }
+    case 413:
+        if (pXSprite->health > 0)
+        {
+            if (a3.cmd == 1)
+            {
+                if (SetSpriteState(nSprite, pXSprite, 1))
+                {
+                    seqSpawn(38, 3, pSprite->extra, nMGunOpenClient);
+                    if (pXSprite->data1 > 0)
+                        pXSprite->data2 = pXSprite->data1;
+                }
+            }
+            else if (a3.cmd == 0)
+            {
+                if (SetSpriteState(nSprite, pXSprite, 0))
+                    seqSpawn(40, 3, pSprite->extra, -1);
+            }
+        }
+        break;
+    case 414:
+        if (SetSpriteState(nSprite, pXSprite, 1))
+            pSprite->hitag |= 7;
+        break;
+    case 408:
+        if (SetSpriteState(nSprite, pXSprite, 0))
+            actPostSprite(nSprite, kStatFree);
+        break;
+    case 405:
+        if (SetSpriteState(nSprite, pXSprite, 0))
+            actPostSprite(nSprite, kStatFree);
+        break;
+    case 456:
+        switch (a3.cmd)
+        {
+        case 0:
+            pXSprite->state = 0;
+            pSprite->cstat |= 32768;
+            pSprite->cstat &= ~1;
+            break;
+        case 1:
+            pXSprite->state = 1;
+            pSprite->cstat &= (unsigned short)~32768;
+            pSprite->cstat |= 1;
+            break;
+        case 3:
+            pXSprite->state ^= 1;
+            pSprite->cstat ^= 32768;
+            pSprite->cstat ^= 1;
+            break;
+        }
+        break;
+    case 452:
+        if (a3.cmd == 1)
+        {
+            if (SetSpriteState(nSprite, pXSprite, 1))
+            {
+                seqSpawn(38, 3, pSprite->extra, -1);
+                sfxPlay3DSound(pSprite, 441, 0, 0);
+            }
+        }
+        else if (a3.cmd == 0)
+        {
+            if (SetSpriteState(nSprite, pXSprite, 0))
+            {
+                seqSpawn(40, 3, pSprite->extra, -1);
+                sfxKill3DSound(pSprite, 0, -1);
+            }
+        }
+        break;
+    case 23:
+        switch (a3.cmd)
+        {
+        case 0:
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case 1:
+            if (SetSpriteState(nSprite, pXSprite, 1))
+                seqSpawn(37, 3, pSprite->extra, -1);
+            break;
+        default:
+            SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1);
+            if (pXSprite->state)
+                seqSpawn(37, 3, pSprite->extra, -1);
+            break;
+        }
+        break;
+    case 20:
+        switch (a3.cmd)
+        {
+        case 0:
+            if (SetSpriteState(nSprite, pXSprite, 0))
+                sfxPlay3DSound(pSprite, pXSprite->data2, 0, 0);
+            break;
+        case 1:
+            if (SetSpriteState(nSprite, pXSprite, 1))
+                sfxPlay3DSound(pSprite, pXSprite->data1, 0, 0);
+            break;
+        default:
+            if (SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1))
+            {
+                if (pXSprite->state)
+                    sfxPlay3DSound(pSprite, pXSprite->data1, 0, 0);
+                else
+                    sfxPlay3DSound(pSprite, pXSprite->data2, 0, 0);
+            }
+            break;
+        }
+        break;
+    case 21:
+        switch (a3.cmd)
+        {
+        case 0:
+            if (SetSpriteState(nSprite, pXSprite, 0))
+                sfxPlay3DSound(pSprite, pXSprite->data2, 0, 0);
+            break;
+        case 1:
+            if (SetSpriteState(nSprite, pXSprite, 1))
+                sfxPlay3DSound(pSprite, pXSprite->data1, 0, 0);
+            break;
+        default:
+            if (SetSpriteState(nSprite, pXSprite, pXSprite->restState ^ 1))
+            {
+                if (pXSprite->state)
+                    sfxPlay3DSound(pSprite, pXSprite->data1, 0, 0);
+                else
+                    sfxPlay3DSound(pSprite, pXSprite->data2, 0, 0);
+            }
+            break;
+        }
+        break;
+    // By NoOne: add linking for path markers and stacks feature
+    case kMarkerLowWater:
+    case kMarkerUpWater:
+    case kMarkerUpGoo:
+    case kMarkerLowGoo:
+    case kMarkerUpLink:
+    case kMarkerLowLink:
+    case kMarkerUpStack:
+    case kMarkerLowStack:
+    case kMarkerPath:
+        if (pXSprite->command == 5 && pXSprite->txID != 0)
+            evSend(nSprite, 3, pXSprite->txID, COMMAND_ID_5);
+        break;
+    case 22:
+        switch (a3.cmd)
+        {
+        case 0:
+            pXSprite->data1--;
+            if (pXSprite->data1 < 0)
+                pXSprite->data1 += pXSprite->data3;
+            break;
+        default:
+            pXSprite->data1++;
+            if (pXSprite->data1 >= pXSprite->data3)
+                pXSprite->data1 -= pXSprite->data3;
+            break;
+        }
+        if (pXSprite->command == 5 && pXSprite->txID)
+            evSend(nSprite, 3, pXSprite->txID, COMMAND_ID_5);
+        sfxPlay3DSound(pSprite, pXSprite->data4, -1, 0);
+        if (pXSprite->data1 == pXSprite->data2)
+            SetSpriteState(nSprite, pXSprite, 1);
+        else
+            SetSpriteState(nSprite, pXSprite, 0);
+        break;
+    case kGDXObjPropertiesChanger:
+    case kGDXObjPicnumChanger:
+    case kGDXObjSizeChanger:
+    case kGDXSectorFXChanger:
+    case kGDXObjDataChanger:
+        // by NoOne: Sending new command instead of link is *required*, because types above 
+        //are universal and can paste properties in different objects.
+        if (pXSprite->command == COMMAND_ID_5)
+            evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+        else {
+            evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste); // send first command to change properties
+            evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID) pXSprite->command); // then send normal command
+        }
+        break;
+    // this type damages sprite with given damageType
+    case kGDXSpriteDamager:
+        evSend(nSprite, 3, pXSprite->txID, kGDXCommandSpriteDamage);
+        break;
+    case 40: // Random weapon
+    case 80: // Random ammo
+        // let's first search for previously dropped items and remove it
+        if (pXSprite->dropMsg > 0) {
+            for (short nItem = headspritestat[3]; nItem >= 0; nItem = nextspritestat[nItem]) {
+                spritetype* pItem = &sprite[nItem];
+                if (pItem->lotag == pXSprite->dropMsg && pItem->x == pSprite->x && pItem->y == pSprite->y && pItem->z == pSprite->z) {
+                    gFX.fxSpawn((FX_ID) 29, pSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, 0);
+                    deletesprite(nItem);
+                    break;
+                }
+            }
+        }
+        // then drop item
+        DropRandomPickupObject(pSprite, pXSprite->dropMsg);
+        break;
+    case kGDXCustomDudeSpawn:
+        if (gGameOptions.nMonsterSettings && actSpawnCustomDude(pSprite, -1) != NULL)
+            gKillMgr.sub_263E0(1);
+        break;
+    case 18:
+        if (gGameOptions.nMonsterSettings && pXSprite->data1 >= kDudeBase && pXSprite->data1 < kDudeMax)
+        {
+            
+            spritetype* pSpawn = NULL;
+            // By NoOne: add spawn random dude feature - works only if at least 2 data fields are not empty.
+            if (!VanillaMode()) {
+                if ((pSpawn = spawnRandomDude(pSprite)) == NULL)
+                    pSpawn = actSpawnDude(pSprite, pXSprite->data1, -1, 0);
+            } else {
+              pSpawn = actSpawnDude(pSprite, pXSprite->data1, -1, 0);
+            }
+
+            if (pSpawn)
+            {
+                XSPRITE *pXSpawn = &xsprite[pSpawn->extra];
+                gKillMgr.sub_263E0(1);
+                switch (pXSprite->data1)
+                {
+                case 239:
+                case 240:
+                case 242:
+                case 252:
+                case 253:
+                {
+                    pXSpawn->health = dudeInfo[pXSprite->data1 - kDudeBase].startHealth << 4;
+                    pXSpawn->burnTime = 10;
+                    pXSpawn->target = -1;
+                    aiActivateDude(pSpawn, pXSpawn);
+                    break;
+                }
+                }
+            }
+        }
+        break;
+    case 19:
+        pXSprite->triggerOn = 0;
+        pXSprite->isTriggered = 1;
+        SetSpriteState(nSprite, pXSprite, 1);
+        for (int p = connecthead; p >= 0; p = connectpoint2[p])
+        {
+            spritetype *pPlayerSprite = gPlayer[p].pSprite;
+            int dx = (pSprite->x - pPlayerSprite->x)>>4;
+            int dy = (pSprite->y - pPlayerSprite->y)>>4;
+            int dz = (pSprite->z - pPlayerSprite->z)>>8;
+            int nDist = dx*dx+dy*dy+dz*dz+0x40000;
+            gPlayer[p].at37f = divscale16(pXSprite->data1, nDist);
+        }
+        break;
+    case 400:
+        if (pSprite->hitag&16)
+            return;
+        fallthrough__;
+    case 418:
+    case 419:
+    case 420:
+        actExplodeSprite(pSprite);
+        break;
+    case 459:
+        switch (a3.cmd)
+        {
+        case 1:
+            SetSpriteState(nSprite, pXSprite, 1);
+            break;
+        default:
+            pSprite->cstat &= (unsigned short)~32768;
+            actExplodeSprite(pSprite);
+            break;
+        }
+        break;
+    case kGDXSeqSpawner:
+    case kGDXEffectSpawner:
+        switch (a3.cmd) {
+            case COMMAND_ID_0:
+                SetSpriteState(nSprite, pXSprite, 0);
+                break;
+            case COMMAND_ID_1:
+                if (pXSprite->state == 1) break;
+                fallthrough__;
+            case COMMAND_ID_21:
+                SetSpriteState(nSprite, pXSprite, 1);
+                if (pXSprite->txID <= 0)
+                    (pSprite->type == kGDXSeqSpawner) ? useSeqSpawnerGen(pXSprite, NULL) : useEffectGen(pXSprite, NULL);
+                else if (pXSprite->command == COMMAND_ID_5)
+                    evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+                else {
+                    evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+                    evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID) pXSprite->command);
+                }
+            
+                if (pXSprite->busyTime > 0)
+                    evPost(nSprite, 3, ClipLow((int(pXSprite->busyTime) + Random2(pXSprite->data1)) * 120 / 10, 0), COMMAND_ID_21);
+                break;
+
+            case COMMAND_ID_3:
+                if (pXSprite->state == 0) evPost(nSprite, 3, 0, COMMAND_ID_21);
+                else evPost(nSprite, 3, 0, COMMAND_ID_0);
+                break;
+        }
+
+        break;
+    case 402:
+        if (pSprite->statnum == 8)
+            break;
+        if (a3.cmd != 1)
+            actExplodeSprite(pSprite);
+        else
+        {
+            sfxPlay3DSound(pSprite, 454, 0, 0);
+            evPost(nSprite, 3, 18, COMMAND_ID_0);
+        }
+        break;
+    case 401:
+    case kGDXThingTNTProx:
+        if (pSprite->statnum == 8)
+            break;
+        switch (a3.cmd)
+        {
+        case 35:
+            if (!pXSprite->state)
+            {
+                sfxPlay3DSound(pSprite, 452, 0, 0);
+                evPost(nSprite, 3, 30, COMMAND_ID_0);
+                pXSprite->state = 1;
+            }
+            break;
+        case 1:
+            sfxPlay3DSound(pSprite, 451, 0, 0);
+            pXSprite->Proximity = 1;
+            break;
+        default:
+            actExplodeSprite(pSprite);
+            break;
+        }
+        break;
+    case 431:
+        sub_43CF8(pSprite, pXSprite, a3);
+        break;
+    case kGDXThingCustomDudeLifeLeech:
+        dudeLeechOperate(pSprite, pXSprite, a3);
+        break;
+    case kGDXWindGenerator:
+        switch (a3.cmd) {
+        case COMMAND_ID_0:
+            stopWindOnSectors(pXSprite);
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case COMMAND_ID_1:
+            if (pXSprite->state == 1) break;
+            fallthrough__;
+        case COMMAND_ID_21:
+            SetSpriteState(nSprite, pXSprite, 1);
+            if (pXSprite->txID <= 0) useSectorWindGen(pXSprite, NULL);
+            else if (pXSprite->command == COMMAND_ID_5)
+                evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+            else {
+                evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+                evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID) pXSprite->command);
+            }
+
+            if (pXSprite->busyTime > 0)
+                evPost(nSprite, 3, pXSprite->busyTime, COMMAND_ID_21);
+            break;
+        case COMMAND_ID_3:
+            if (pXSprite->state == 0) evPost(nSprite, 3, 0, COMMAND_ID_21);
+            else evPost(nSprite, 3, 0, COMMAND_ID_0);
+            break;
+        }
+
+        break;
+    case kGDXDudeTargetChanger:
+    {
+        // this one is required if data4 of generator was dynamically changed
+        // it turns monsters in normal idle state instead of genIdle, so they
+        // not ignore the world.
+        bool activated = false;
+        if (pXSprite->dropMsg == 3 && 3 != pXSprite->data4) {
+            activateDudes(pXSprite->txID);
+            activated = true;
+        }
+
+        switch (a3.cmd)
+        {
+        case COMMAND_ID_0:
+            if (pXSprite->data4 == 3 && activated == false) activateDudes(pXSprite->txID);
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case COMMAND_ID_1:
+            if (pXSprite->state == 1) break;
+            fallthrough__;
+        case COMMAND_ID_21:
+            SetSpriteState(nSprite, pXSprite, 1);
+            if (pXSprite->txID <= 0 || !getDudesForTargetChg(pXSprite)) {
+                evPost(nSprite, 3, 0, COMMAND_ID_0);
+                break;
+            }
+            else if (pXSprite->command == COMMAND_ID_5)
+                evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+            else {
+                evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+                evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID)pXSprite->command);
+            }
+
+            if (pXSprite->busyTime > 0) evPost(nSprite, 3, pXSprite->busyTime, COMMAND_ID_21);
+            break;
+        case COMMAND_ID_3:
+            if (pXSprite->state == 0) evPost(nSprite, 3, 0, COMMAND_ID_21);
+            else evPost(nSprite, 3, 0, COMMAND_ID_0);
+            break;
+        }
+
+        pXSprite->dropMsg = (short)pXSprite->data4;
+        break;
+    }
+    case kGDXObjDataAccumulator:
+            switch (a3.cmd) {
+            case COMMAND_ID_0:
+                SetSpriteState(nSprite, pXSprite, 0);
+                break;
+            case COMMAND_ID_1:
+                if (pXSprite->state == 1) break;
+            case COMMAND_ID_21:
+                SetSpriteState(nSprite, pXSprite, 1);
+
+                // force OFF after *all* TX objects reach the goal value
+                if (pSprite->hitag == 0 && goalValueIsReached(pXSprite)) {
+                    evPost(nSprite, 3, 0, COMMAND_ID_0);
+                    break;
+                }
+
+                if (pXSprite->data1 > 0 && pXSprite->data1 <= 4 && pXSprite->data2 > 0) {
+                    if (pXSprite->txID != 0) {
+                        if (pXSprite->command == COMMAND_ID_5)
+                            evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+                        else {
+                            evSend(nSprite, 3, pXSprite->txID, kGDXCommandPaste);
+                            evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID) pXSprite->command);
+                        }
+                    }
+                    if (pXSprite->busyTime > 0) evPost(nSprite, 3, pXSprite->busyTime, COMMAND_ID_21);
+                }
+                break;
+            case COMMAND_ID_3:
+                if (pXSprite->state == 0) evPost(nSprite, 3, 0, COMMAND_ID_21);
+                else evPost(nSprite, 3, 0, COMMAND_ID_0);
+                break;
+            }
+        break;
+    case 700:
+    case 701:
+    case 702:
+    case 703:
+    case 704:
+    case 705:
+    case 706:
+    case 707:
+    case 708:
+        switch (a3.cmd)
+        {
+        case 0:
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case 21:
+            if (pSprite->type != 700)
+                ActivateGenerator(nSprite);
+            if (pXSprite->txID)
+                evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID)pXSprite->command);
+            if (pXSprite->busyTime > 0)
+            {
+                int nRand = Random2(pXSprite->data1);
+                evPost(nSprite, 3, 120*(nRand+pXSprite->busyTime) / 10, COMMAND_ID_21);
+            }
+            break;
+        default:
+            if (!pXSprite->state)
+            {
+                SetSpriteState(nSprite, pXSprite, 1);
+                evPost(nSprite, 3, 0, COMMAND_ID_21);
+            }
+            break;
+        }
+        break;
+    case 711:
+        if (gGameOptions.nGameType == 0)
+        {
+            if (gMe->pXSprite->health <= 0)
+                break;
+            gMe->at30a = 0;
+        }
+        sndStartSample(pXSprite->data1, -1, 1, 0);
+        break;
+    case 416:
+    case 417:
+    case 425:
+    case 426:
+    case 427:
+        switch (a3.cmd)
+        {
+        case 0:
+            if (SetSpriteState(nSprite, pXSprite, 0))
+                actActivateGibObject(pSprite, pXSprite);
+            break;
+        case 1:
+            if (SetSpriteState(nSprite, pXSprite, 1))
+                actActivateGibObject(pSprite, pXSprite);
+            break;
+        default:
+            if (SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1))
+                actActivateGibObject(pSprite, pXSprite);
+            break;
+        }
+        break;
+    default:
+        switch (a3.cmd)
+        {
+        case 0:
+            SetSpriteState(nSprite, pXSprite, 0);
+            break;
+        case 1:
+            SetSpriteState(nSprite, pXSprite, 1);
+            break;
+        default:
+            SetSpriteState(nSprite, pXSprite, pXSprite->state ^ 1);
+            break;
+        }
+        break;
+    }
+}
+
+// by NoOne: this function stops wind on all TX sectors affected by WindGen after it goes off state.
+void stopWindOnSectors(XSPRITE* pXSource) {
+    for (int i = bucketHead[pXSource->txID]; i < bucketHead[pXSource->txID + 1]; i++) {
+        if (rxBucket[i].type != 3) continue;
+        XSECTOR * pXSector = &xsector[sector[rxBucket[i].index].extra];
+        if ((pXSector->state == 1 && !pXSector->windAlways) || sprite[pXSource->reference].hitag == 1)
+            pXSector->windVel = 0;
+    }
+}
+
+void useSectorWindGen(XSPRITE* pXSource, sectortype* pSector) {
+    
+    spritetype* pSource = &sprite[pXSource->reference];
+    XSECTOR* pXSector = NULL; bool forceWind = false;
+
+    if (pSector == NULL) {
+        
+        if (sector[pSource->sectnum].extra < 0) {
+            int nXSector = dbInsertXSector(pSource->sectnum);
+            if (nXSector > 0) pXSector = &xsector[nXSector];
+            else return;
+
+            forceWind = true;
+
+        } else {
+            pXSector = &xsector[sector[pSource->sectnum].extra];
+        }
+    } else {
+        pXSector = &xsector[pSector->extra];
+    }
+    
+    if (pSource->hitag) {
+        pXSector->panAlways = 1;
+        pXSector->windAlways = 1;
+    } else if (forceWind) 
+        pXSector->windAlways = 1;
+
+    if (pXSource->data2 > 32766) pXSource->data2 = 32767;
+
+    if (pXSource->data1 == 1 || pXSource->data1 == 3) pXSector->windVel = Random(pXSource->data2);
+    else pXSector->windVel = pXSource->data2;
+
+    if (pXSource->data1 == 2 || pXSource->data1 == 3) {
+        short ang = pSource->ang;
+        while (pSource->ang == ang)
+            pSource->ang = Random3(kAng360);
+    }
+
+    pXSector->windAng = pSource->ang;
+
+    if (pXSource->data3 > 0 && pXSource->data3 < 4) {
+        switch (pXSource->data3) {
+        case 1:
+            pXSector->panFloor = true;
+            pXSector->panCeiling = false;
+            break;
+        case 2:
+            pXSector->panFloor = false;
+            pXSector->panCeiling = true;
+            break;
+        case 3:
+            pXSector->panFloor = true;
+            pXSector->panCeiling = true;
+            break;
+        }
+
+        pXSector->panAngle = pXSector->windAng;
+        pXSector->panVel = pXSector->windVel;
+    }
+}
+
+void useSeqSpawnerGen(XSPRITE* pXSource, spritetype* pSprite) {
+    if (pSprite == NULL) pSprite = &sprite[pXSource->reference];
+    if (pSprite->extra < 0) return;
+    
+    seqSpawn(pXSource->data2, 3, pSprite->extra, (pXSource->data3 > 0) ? pXSource->data3 : -1);
+    if (pXSource->data4 > 0)
+        sfxPlay3DSound(pSprite, pXSource->data4, -1, 0);
+}
+
+void useEffectGen(XSPRITE* pXSource, spritetype* pSprite) {
+    if (pSprite == NULL) pSprite = &sprite[pXSource->reference];
+    if (pSprite->extra < 0) return;
+
+    int top, bottom; GetSpriteExtents(pSprite, &top, &bottom); int cnt = pXSource->data4;
+    spritetype* pEffect = NULL; if (cnt > 32) cnt = 32;
+
+    while (cnt-- >= 0) {
+        if (cnt > 0) {
+
+            int dx = Random3(250);
+            int dy = Random3(150);
+
+            pEffect = gFX.fxSpawn((FX_ID)pXSource->data2, pSprite->sectnum, pSprite->x + dx, pSprite->y + dy, top, 0);
+
+        }
+        else {
+            pEffect = gFX.fxSpawn((FX_ID)pXSource->data2, pSprite->sectnum, pSprite->x, pSprite->y, top, 0);
+        }
+
+        if (pEffect != NULL) {
+            if (pEffect->pal <= 0) pEffect->pal = pSprite->pal;
+            if (pEffect->xrepeat <= 0) pEffect->xrepeat = pSprite->xrepeat;
+            if (pEffect->yrepeat <= 0) pEffect->yrepeat = pSprite->yrepeat;
+            if (pEffect->shade == 0) pEffect->shade = pSprite->shade;
+        }
+    }
+
+    if (pXSource->data3 > 0)
+        sfxPlay3DSound(pSprite, pXSource->data3, -1, 0);
+
+}
+
+
+
+void SetupGibWallState(walltype *pWall, XWALL *pXWall)
+{
+    walltype *pWall2 = NULL;
+    if (pWall->nextwall >= 0)
+        pWall2 = &wall[pWall->nextwall];
+    if (pXWall->state)
+    {
+        pWall->cstat &= ~65;
+        if (pWall2)
+        {
+            pWall2->cstat &= ~65;
+            pWall->cstat &= ~16;
+            pWall2->cstat &= ~16;
+        }
+        return;
+    }
+    char bVector = pXWall->triggerVector != 0;
+    pWall->cstat |= 1;
+    if (bVector)
+        pWall->cstat |= 64;
+    if (pWall2)
+    {
+        pWall2->cstat |= 1;
+        if (bVector)
+            pWall2->cstat |= 64;
+        pWall->cstat |= 16;
+        pWall2->cstat |= 16;
+    }
+}
+
+void OperateWall(int nWall, XWALL *pXWall, EVENT a3)
+{
+    walltype *pWall = &wall[nWall];
+    switch (a3.cmd)
+    {
+    case 6:
+        pXWall->locked = 1;
+        return;
+    case 7:
+        pXWall->locked = 0;
+        return;
+    case 8:
+        pXWall->locked ^= 1;
+        return;
+    }
+    if (pWall->lotag == 511)
+    {
+        char bStatus;
+        switch (a3.cmd)
+        {
+        case 1:
+        case 51:
+            bStatus = SetWallState(nWall, pXWall, 1);
+            break;
+        case 0:
+            bStatus = SetWallState(nWall, pXWall, 0);
+            break;
+        default:
+            bStatus = SetWallState(nWall, pXWall, pXWall->state^1);
+            break;
+        }
+        if (bStatus)
+        {
+            SetupGibWallState(pWall, pXWall);
+            if (pXWall->state)
+            {
+                CGibVelocity vel(100, 100, 250);
+                int nType = ClipRange(pXWall->data, 0, 31);
+                if (nType > 0)
+                    GibWall(nWall, (GIBTYPE)nType, &vel);
+            }
+        }
+        return;
+    }
+    switch (a3.cmd)
+    {
+    case 0:
+        SetWallState(nWall, pXWall, 0);
+        break;
+    case 1:
+        SetWallState(nWall, pXWall, 1);
+        break;
+    default:
+        SetWallState(nWall, pXWall, pXWall->state ^ 1);
+        break;
+    }
+}
+
+void SectorStartSound(int nSector, int nState)
+{
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->statnum == 0 && pSprite->type == 709)
+        {
+            int nXSprite = pSprite->extra;
+            dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (nState)
+            {
+                if (pXSprite->data3)
+                    sfxPlay3DSound(pSprite, pXSprite->data3, 0, 0);
+            }
+            else
+            {
+                if (pXSprite->data1)
+                    sfxPlay3DSound(pSprite, pXSprite->data1, 0, 0);
+            }
+        }
+    }
+}
+
+void SectorEndSound(int nSector, int nState)
+{
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->statnum == 0 && pSprite->type == 709)
+        {
+            int nXSprite = pSprite->extra;
+            dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (nState)
+            {
+                if (pXSprite->data2)
+                    sfxPlay3DSound(pSprite, pXSprite->data2, 0, 0);
+            }
+            else
+            {
+                if (pXSprite->data4)
+                    sfxPlay3DSound(pSprite, pXSprite->data4, 0, 0);
+            }
+        }
+    }
+}
+
+void PathSound(int nSector, int nSound)
+{
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->statnum == 0 && pSprite->type == 709)
+            sfxPlay3DSound(pSprite, nSound, 0, 0);
+    }
+}
+
+void DragPoint(int nWall, int x, int y)
+{
+    viewInterpolateWall(nWall, &wall[nWall]);
+    wall[nWall].x = x;
+    wall[nWall].y = y;
+
+    int vsi = numwalls;
+    int vb = nWall;
+    do
+    {
+        if (wall[vb].nextwall >= 0)
+        {
+            vb = wall[wall[vb].nextwall].point2;
+            viewInterpolateWall(vb, &wall[vb]);
+            wall[vb].x = x;
+            wall[vb].y = y;
+        }
+        else
+        {
+            vb = nWall;
+            do
+            {
+                if (wall[lastwall(vb)].nextwall >= 0)
+                {
+                    vb = wall[lastwall(vb)].nextwall;
+                    viewInterpolateWall(vb, &wall[vb]);
+                    wall[vb].x = x;
+                    wall[vb].y = y;
+                }
+                else
+                    break;
+                vsi--;
+            } while (vb != nWall && vsi > 0);
+            break;
+        }
+        vsi--;
+    } while (vb != nWall && vsi > 0);
+}
+
+void TranslateSector(int nSector, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11, char a12)
+{
+    int x, y;
+    int nXSector = sector[nSector].extra;
+    XSECTOR *pXSector = &xsector[nXSector];
+    int v20 = interpolate(a6, a9, a2);
+    int vc = interpolate(a6, a9, a3);
+    int v28 = vc - v20;
+    int v24 = interpolate(a7, a10, a2);
+    int v8 = interpolate(a7, a10, a3);
+    int v2c = v8 - v24;
+    int v44 = interpolate(a8, a11, a2);
+    int vbp = interpolate(a8, a11, a3);
+    int v14 = vbp - v44;
+    int nWall = sector[nSector].wallptr;
+    if (a12)
+    {
+        for (int i = 0; i < sector[nSector].wallnum; nWall++, i++)
+        {
+            x = baseWall[nWall].x;
+            y = baseWall[nWall].y;
+            if (vbp)
+                RotatePoint((int*)&x, (int*)&y, vbp, a4, a5);
+            DragPoint(nWall, x+vc-a4, y+v8-a5);
+        }
+    }
+    else
+    {
+        for (int i = 0; i < sector[nSector].wallnum; nWall++, i++)
+        {
+            int v10 = wall[nWall].point2;
+            x = baseWall[nWall].x;
+            y = baseWall[nWall].y;
+            if (wall[nWall].cstat&16384)
+            {
+                if (vbp)
+                    RotatePoint((int*)&x, (int*)&y, vbp, a4, a5);
+                DragPoint(nWall, x+vc-a4, y+v8-a5);
+                if ((wall[v10].cstat&49152) == 0)
+                {
+                    x = baseWall[v10].x;
+                    y = baseWall[v10].y;
+                    if (vbp)
+                        RotatePoint((int*)&x, (int*)&y, vbp, a4, a5);
+                    DragPoint(v10, x+vc-a4, y+v8-a5);
+                }
+                continue;
+            }
+            if (wall[nWall].cstat&32768)
+            {
+                if (vbp)
+                    RotatePoint((int*)&x, (int*)&y, -vbp, a4, a5);
+                DragPoint(nWall, x-(vc-a4), y-(v8-a5));
+                if ((wall[v10].cstat&49152) == 0)
+                {
+                    x = baseWall[v10].x;
+                    y = baseWall[v10].y;
+                    if (vbp)
+                        RotatePoint((int*)&x, (int*)&y, -vbp, a4, a5);
+                    DragPoint(v10, x-(vc-a4), y-(v8-a5));
+                }
+                continue;
+            }
+        }
+    }
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        // By NoOne: allow to move markers by sector movements in game if hitag 1 is added in editor.
+        if (pSprite->statnum == 10 || pSprite->statnum == 16) {
+            if (!(pSprite->hitag&kHitagExtBit)) continue;
+        }
+        x = baseSprite[nSprite].x;
+        y = baseSprite[nSprite].y;
+        if (sprite[nSprite].cstat&8192)
+        {
+            if (vbp)
+                RotatePoint((int*)&x, (int*)&y, vbp, a4, a5);
+            viewBackupSpriteLoc(nSprite, pSprite);
+            pSprite->ang = (pSprite->ang+v14)&2047;
+            pSprite->x = x+vc-a4;
+            pSprite->y = y+v8-a5;
+        }
+        else if (sprite[nSprite].cstat&16384)
+        {
+            if (vbp)
+                RotatePoint((int*)& x, (int*)& y, -vbp, a4, a4);
+            viewBackupSpriteLoc(nSprite, pSprite);
+            pSprite->ang = (pSprite->ang-v14)&2047;
+            pSprite->x = x-(vc-a4);
+            pSprite->y = y-(v8-a5);
+        }
+        else if (pXSector->Drag)
+        {
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            int floorZ = getflorzofslope(nSector, pSprite->x, pSprite->y);
+            if (!(pSprite->cstat&48) && floorZ <= bottom)
+            {
+                if (v14)
+                    RotatePoint((int*)&pSprite->x, (int*)&pSprite->y, v14, v20, v24);
+                viewBackupSpriteLoc(nSprite, pSprite);
+                pSprite->ang = (pSprite->ang+v14)&2047;
+                pSprite->x += v28;
+                pSprite->y += v2c;
+            }
+        }
+    }
+}
+
+void ZTranslateSector(int nSector, XSECTOR *pXSector, int a3, int a4)
+{
+    sectortype *pSector = &sector[nSector];
+    viewInterpolateSector(nSector, pSector);
+    int dz = pXSector->at28_0-pXSector->at24_0;
+    if (dz != 0)
+    {
+        int oldZ = pSector->floorz;
+        baseFloor[nSector] = pSector->floorz = pXSector->at24_0 + mulscale16(dz, GetWaveValue(a3, a4));
+        velFloor[nSector] += (pSector->floorz-oldZ)<<8;
+        for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            if (pSprite->statnum == 10 || pSprite->statnum == 16)
+                continue;
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            if (pSprite->cstat&8192)
+            {
+                viewBackupSpriteLoc(nSprite, pSprite);
+                pSprite->z += pSector->floorz-oldZ;
+            }
+            else if (pSprite->hitag&2)
+                pSprite->hitag |= 4;
+            else if (oldZ <= bottom && !(pSprite->cstat&48))
+            {
+                viewBackupSpriteLoc(nSprite, pSprite);
+                pSprite->z += pSector->floorz-oldZ;
+            }
+        }
+    }
+    dz = pXSector->at20_0-pXSector->at1c_0;
+    if (dz != 0)
+    {
+        int oldZ = pSector->ceilingz;
+        baseCeil[nSector] = pSector->ceilingz = pXSector->at1c_0 + mulscale16(dz, GetWaveValue(a3, a4));
+        velCeil[nSector] += (pSector->ceilingz-oldZ)<<8;
+        for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            if (pSprite->statnum == 10 || pSprite->statnum == 16)
+                continue;
+            if (pSprite->cstat&16384)
+            {
+                viewBackupSpriteLoc(nSprite, pSprite);
+                pSprite->z += pSector->ceilingz-oldZ;
+            }
+        }
+    }
+}
+
+int GetHighestSprite(int nSector, int nStatus, int *a3)
+{
+    *a3 = sector[nSector].floorz;
+    int v8 = -1;
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        if (sprite[nSprite].statnum == nStatus || nStatus == 1024)
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            if (top-pSprite->z > *a3)
+            {
+                *a3 = top-pSprite->z;
+                v8 = nSprite;
+            }
+        }
+    }
+    return v8;
+}
+
+int GetCrushedSpriteExtents(unsigned int nSector, int *pzTop, int *pzBot)
+{
+    dassert(pzTop != NULL && pzBot != NULL);
+    dassert(nSector < (unsigned int)numsectors);
+    int vc = -1;
+    sectortype *pSector = &sector[nSector];
+    int vbp = pSector->ceilingz;
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->statnum == 6 || pSprite->statnum == 4)
+        {
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            if (vbp > top)
+            {
+                vbp = top;
+                *pzTop = top;
+                *pzBot = bottom;
+                vc = nSprite;
+            }
+        }
+    }
+    return vc;
+}
+
+int VCrushBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    int nXSector = sector[nSector].extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    int nWave;
+    if (pXSector->busy < a2)
+        nWave = pXSector->at7_2;
+    else
+        nWave = pXSector->at7_5;
+    int dz1 = pXSector->at20_0 - pXSector->at1c_0;
+    int vc = pXSector->at1c_0;
+    if (dz1 != 0)
+        vc += mulscale16(dz1, GetWaveValue(a2, nWave));
+    int dz2 = pXSector->at28_0 - pXSector->at24_0;
+    int v10 = pXSector->at24_0;
+    if (dz2 != 0)
+        v10 += mulscale16(dz2, GetWaveValue(a2, nWave));
+    int v18;
+    if (GetHighestSprite(nSector, 6, &v18) >= 0 && vc >= v18)
+        return 1;
+    viewInterpolateSector(nSector, &sector[nSector]);
+    if (dz1 != 0)
+        sector[nSector].ceilingz = vc;
+    if (dz2 != 0)
+        sector[nSector].floorz = v10;
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        return 3;
+    }
+    return 0;
+}
+
+int VSpriteBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    int nXSector = sector[nSector].extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    int nWave;
+    if (pXSector->busy < a2)
+        nWave = pXSector->at7_2;
+    else
+        nWave = pXSector->at7_5;
+    int dz1 = pXSector->at28_0 - pXSector->at24_0;
+    if (dz1 != 0)
+    {
+        for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            if (pSprite->cstat&8192)
+            {
+                viewBackupSpriteLoc(nSprite, pSprite);
+                pSprite->z = baseSprite[nSprite].z+mulscale16(dz1, GetWaveValue(a2, nWave));
+            }
+        }
+    }
+    int dz2 = pXSector->at20_0 - pXSector->at1c_0;
+    if (dz2 != 0)
+    {
+        for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            if (pSprite->cstat&16384)
+            {
+                viewBackupSpriteLoc(nSprite, pSprite);
+                pSprite->z = baseSprite[nSprite].z+mulscale16(dz2, GetWaveValue(a2, nWave));
+            }
+        }
+    }
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        return 3;
+    }
+    return 0;
+}
+
+int VDoorBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    int nXSector = sector[nSector].extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    int vbp;
+    if (pXSector->state)
+        vbp = 65536/ClipLow((120*pXSector->busyTimeA)/10, 1);
+    else
+        vbp = -65536/ClipLow((120*pXSector->busyTimeB)/10, 1);
+    int top, bottom;
+    int nSprite = GetCrushedSpriteExtents(nSector,&top,&bottom);
+    if (nSprite >= 0 && a2 > pXSector->busy)
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        dassert(pSprite->extra > 0 && pSprite->extra < kMaxXSprites);
+        XSPRITE *pXSprite = &xsprite[pSprite->extra];
+        if (pXSector->at20_0 > pXSector->at1c_0 || pXSector->at28_0 < pXSector->at24_0)
+        {
+            if (pXSector->interruptable)
+            {
+                if (pXSector->Crush)
+                {
+                    if (pXSprite->health <= 0)
+                        return 2;
+                    int nDamage;
+                    if (pXSector->data == 0)
+                        nDamage = 500;
+                    else
+                        nDamage = pXSector->data;
+                    actDamageSprite(nSprite, &sprite[nSprite], DAMAGE_TYPE_0, nDamage<<4);
+                }
+                a2 = ClipRange(a2-(vbp/2)*4, 0, 65536);
+            }
+            else if (pXSector->Crush && pXSprite->health > 0)
+            {
+                int nDamage;
+                if (pXSector->data == 0)
+                    nDamage = 500;
+                else
+                    nDamage = pXSector->data;
+                actDamageSprite(nSprite, &sprite[nSprite], DAMAGE_TYPE_0, nDamage<<4);
+                a2 = ClipRange(a2-(vbp/2)*4, 0, 65536);
+            }
+        }
+    }
+    else if (nSprite >= 0 && a2 < pXSector->busy)
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        dassert(pSprite->extra > 0 && pSprite->extra < kMaxXSprites);
+        XSPRITE *pXSprite = &xsprite[pSprite->extra];
+        if (pXSector->at1c_0 > pXSector->at20_0 || pXSector->at24_0 < pXSector->at28_0)
+        {
+            if (pXSector->interruptable)
+            {
+                if (pXSector->Crush)
+                {
+                    if (pXSprite->health <= 0)
+                        return 2;
+                    int nDamage;
+                    if (pXSector->data == 0)
+                        nDamage = 500;
+                    else
+                        nDamage = pXSector->data;
+                    actDamageSprite(nSprite, &sprite[nSprite], DAMAGE_TYPE_0, nDamage<<4);
+                }
+                a2 = ClipRange(a2+(vbp/2)*4, 0, 65536);
+            }
+            else if (pXSector->Crush && pXSprite->health > 0)
+            {
+                int nDamage;
+                if (pXSector->data == 0)
+                    nDamage = 500;
+                else
+                    nDamage = pXSector->data;
+                actDamageSprite(nSprite, &sprite[nSprite], DAMAGE_TYPE_0, nDamage<<4);
+                a2 = ClipRange(a2+(vbp/2)*4, 0, 65536);
+            }
+        }
+    }
+    int nWave;
+    if (pXSector->busy < a2)
+        nWave = pXSector->at7_2;
+    else
+        nWave = pXSector->at7_5;
+    ZTranslateSector(nSector, pXSector, a2, nWave);
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        return 3;
+    }
+    return 0;
+}
+
+int HDoorBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    int nWave;
+    if (pXSector->busy < a2)
+        nWave = pXSector->at7_2;
+    else
+        nWave = pXSector->at7_5;
+    spritetype *pSprite1 = &sprite[pXSector->at2c_0];
+    spritetype *pSprite2 = &sprite[pXSector->at2e_0];
+    TranslateSector(nSector, GetWaveValue(pXSector->busy, nWave), GetWaveValue(a2, nWave), pSprite1->x, pSprite1->y, pSprite1->x, pSprite1->y, pSprite1->ang, pSprite2->x, pSprite2->y, pSprite2->ang, pSector->lotag == 616);
+    ZTranslateSector(nSector, pXSector, a2, nWave);
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        return 3;
+    }
+    return 0;
+}
+
+int RDoorBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    int nWave;
+    if (pXSector->busy < a2)
+        nWave = pXSector->at7_2;
+    else
+        nWave = pXSector->at7_5;
+    spritetype *pSprite = &sprite[pXSector->at2c_0];
+    TranslateSector(nSector, GetWaveValue(pXSector->busy, nWave), GetWaveValue(a2, nWave), pSprite->x, pSprite->y, pSprite->x, pSprite->y, 0, pSprite->x, pSprite->y, pSprite->ang, pSector->lotag == 617);
+    ZTranslateSector(nSector, pXSector, a2, nWave);
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        return 3;
+    }
+    return 0;
+}
+
+int StepRotateBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    spritetype *pSprite = &sprite[pXSector->at2c_0];
+    int vbp;
+    if (pXSector->busy < a2)
+    {
+        vbp = pXSector->data+pSprite->ang;
+        int nWave = pXSector->at7_2;
+        TranslateSector(nSector, GetWaveValue(pXSector->busy, nWave), GetWaveValue(a2, nWave), pSprite->x, pSprite->y, pSprite->x, pSprite->y, pXSector->data, pSprite->x, pSprite->y, vbp, 1);
+    }
+    else
+    {
+        vbp = pXSector->data-pSprite->ang;
+        int nWave = pXSector->at7_5;
+        TranslateSector(nSector, GetWaveValue(pXSector->busy, nWave), GetWaveValue(a2, nWave), pSprite->x, pSprite->y, pSprite->x, pSprite->y, vbp, pSprite->x, pSprite->y, pXSector->data, 1);
+    }
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        pXSector->data = vbp&2047;
+        return 3;
+    }
+    return 0;
+}
+
+int GenSectorBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    pXSector->busy = a2;
+    if (pXSector->command == 5 && pXSector->txID)
+        evSend(nSector, 6, pXSector->txID, COMMAND_ID_5);
+    if ((a2&0xffff) == 0)
+    {
+        SetSectorState(nSector, pXSector, a2>>16);
+        SectorEndSound(nSector, a2>>16);
+        return 3;
+    }
+    return 0;
+}
+
+int PathBusy(unsigned int nSector, unsigned int a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    dassert(nXSector > 0 && nXSector < kMaxXSectors);
+    XSECTOR *pXSector = &xsector[nXSector];
+    spritetype *pSprite = &sprite[basePath[nSector]];
+    spritetype *pSprite1 = &sprite[pXSector->at2c_0];
+    XSPRITE *pXSprite1 = &xsprite[pSprite1->extra];
+    spritetype *pSprite2 = &sprite[pXSector->at2e_0];
+    XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+    int nWave = pXSprite1->wave;
+    TranslateSector(nSector, GetWaveValue(pXSector->busy, nWave), GetWaveValue(a2, nWave), pSprite->x, pSprite->y, pSprite1->x, pSprite1->y, pSprite1->ang, pSprite2->x, pSprite2->y, pSprite2->ang, 1);
+    ZTranslateSector(nSector, pXSector, a2, nWave);
+    pXSector->busy = a2;
+    if ((a2&0xffff) == 0)
+    {
+        evPost(nSector, 6, (120*pXSprite2->waitTime)/10, COMMAND_ID_1);
+        pXSector->state = 0;
+        pXSector->busy = 0;
+        if (pXSprite1->data4)
+            PathSound(nSector, pXSprite1->data4);
+        pXSector->at2c_0 = pXSector->at2e_0;
+        pXSector->data = pXSprite2->data1;
+        return 3;
+    }
+    return 0;
+}
+
+void OperateDoor(unsigned int nSector, XSECTOR *pXSector, EVENT a3, BUSYID a4) 
+{
+    switch (a3.cmd)
+    {
+    case 0:
+        if (pXSector->busy)
+        {
+            AddBusy(nSector, a4, -65536/ClipLow((pXSector->busyTimeB*120)/10, 1));
+            SectorStartSound(nSector, 1);
+        }
+        break;
+    case 1:
+        if (pXSector->busy != 0x10000)
+        {
+            AddBusy(nSector, a4, 65536/ClipLow((pXSector->busyTimeA*120)/10, 1));
+            SectorStartSound(nSector, 0);
+        }
+        break;
+    default:
+        if (pXSector->busy&0xffff)
+        {
+            if (pXSector->interruptable)
+            {
+                ReverseBusy(nSector, a4);
+                pXSector->state = !pXSector->state;
+            }
+        }
+        else
+        {
+            char t = !pXSector->state;
+            int nDelta;
+            if (t)
+                nDelta = 65536/ClipLow((pXSector->busyTimeA*120)/10, 1);
+            else
+                nDelta = -65536/ClipLow((pXSector->busyTimeB*120)/10, 1);
+            AddBusy(nSector, a4, nDelta);
+            SectorStartSound(nSector, pXSector->state);
+        }
+        break;
+    }
+}
+
+char SectorContainsDudes(int nSector)
+{
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        if (sprite[nSprite].statnum == 6)
+            return 1;
+    }
+    return 0;
+}
+
+void TeleFrag(int nKiller, int nSector)
+{
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->statnum == 6)
+            actDamageSprite(nKiller, pSprite, DAMAGE_TYPE_3, 4000);
+        else if (pSprite->statnum == 4)
+            actDamageSprite(nKiller, pSprite, DAMAGE_TYPE_3, 4000);
+    }
+}
+
+void OperateTeleport(unsigned int nSector, XSECTOR *pXSector)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    int nDest = pXSector->at2c_0;
+    dassert(nDest < kMaxSprites);
+    spritetype *pDest = &sprite[nDest];
+    dassert(pDest->statnum == kStatMarker);
+    dassert(pDest->type == kMarkerWarpDest);
+    dassert(pDest->sectnum >= 0 && pDest->sectnum < kMaxSectors);
+    for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->statnum == 6)
+        {
+            PLAYER *pPlayer;
+            char bPlayer = IsPlayerSprite(pSprite);
+            if (bPlayer)
+                pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+            else
+                pPlayer = NULL;
+            if (bPlayer || !SectorContainsDudes(pDest->sectnum))
+            {
+                if (!(gGameOptions.uNetGameFlags&2))
+                    TeleFrag(pXSector->data, pDest->sectnum);
+                pSprite->x = pDest->x;
+                pSprite->y = pDest->y;
+                pSprite->z += sector[pDest->sectnum].floorz-sector[nSector].floorz;
+                pSprite->ang = pDest->ang;
+                ChangeSpriteSect(nSprite, pDest->sectnum);
+                sfxPlay3DSound(pDest, 201, -1, 0);
+                xvel[nSprite] = yvel[nSprite] = zvel[nSprite] = 0;
+                ClearBitString(gInterpolateSprite, nSprite);
+                viewBackupSpriteLoc(nSprite, pSprite);
+                if (pPlayer)
+                {
+                    playerResetInertia(pPlayer);
+                    pPlayer->at6b = pPlayer->at73 = 0;
+                }
+            }
+        }
+    }
+}
+
+void OperatePath(unsigned int nSector, XSECTOR *pXSector, EVENT a3)
+{
+    int nSprite;
+    spritetype *pSprite = NULL;
+    XSPRITE *pXSprite;
+    dassert(nSector < (unsigned int)numsectors);
+    spritetype *pSprite2 = &sprite[pXSector->at2c_0];
+    XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+    int nId = pXSprite2->data2;
+    for (nSprite = headspritestat[16]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        pSprite = &sprite[nSprite];
+        if (pSprite->type == 15)
+        {
+            pXSprite = &xsprite[pSprite->extra];
+            if (pXSprite->data1 == nId)
+                break;
+        }
+    }
+    if (nSprite < 0)
+        ThrowError("Unable to find path marker with id #%d", nId);
+    pXSector->at2e_0 = nSprite;
+    pXSector->at24_0 = pSprite2->z;
+    pXSector->at28_0 = pSprite->z;
+    switch (a3.cmd)
+    {
+    case 1:
+        pXSector->state = 0;
+        pXSector->busy = 0;
+        AddBusy(nSector, BUSYID_7, 65536/ClipLow((120*pXSprite2->busyTime)/10,1));
+        if (pXSprite2->data3)
+            PathSound(nSector, pXSprite2->data3);
+        break;
+    }
+}
+
+void OperateSector(unsigned int nSector, XSECTOR *pXSector, EVENT a3)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    sectortype *pSector = &sector[nSector];
+    switch (a3.cmd)
+    {
+    case 6:
+        pXSector->locked = 1;
+        break;
+    case 7:
+        pXSector->locked = 0;
+        // By NoOne: reset counter sector state and make it work again after unlock, so it can be used again.
+        // See callback.cpp for more info.
+        if (pSector->lotag == kSecCounter) {
+            pXSector->state = 0;
+            evPost(nSector, 6, 0, CALLBACK_ID_12);
+        }
+        break;
+    case 8:
+        pXSector->locked ^= 1;
+        // same as above...
+        if (pSector->lotag == kSecCounter && pXSector->locked != 1) {
+            pXSector->state = 0;
+            evPost(nSector, 6, 0, CALLBACK_ID_12);
+        }
+        break;
+    case 9:
+        pXSector->at1b_2 = 0;
+        pXSector->at1b_3 = 1;
+        break;
+    case 10:
+        pXSector->at1b_2 = 1;
+        pXSector->at1b_3 = 0;
+        break;
+    case 11:
+        pXSector->at1b_2 = 1;
+        pXSector->at1b_3 = 1;
+        break;
+    default:
+        switch (pSector->lotag)
+        {
+        case 602:
+            OperateDoor(nSector, pXSector, a3, BUSYID_1);
+            break;
+        case 600:
+            OperateDoor(nSector, pXSector, a3, BUSYID_2);
+            break;
+        case 614:
+        case 616:
+            OperateDoor(nSector, pXSector, a3, BUSYID_3);
+            break;
+        case 615:
+        case 617:
+            OperateDoor(nSector, pXSector, a3, BUSYID_4);
+            break;
+        case 613:
+            switch (a3.cmd)
+            {
+            case 1:
+                pXSector->state = 0;
+                pXSector->busy = 0;
+                AddBusy(nSector, BUSYID_5, 65536/ClipLow((120*pXSector->busyTimeA)/10, 1));
+                SectorStartSound(nSector, 0);
+                break;
+            case 0:
+                pXSector->state = 1;
+                pXSector->busy = 65536;
+                AddBusy(nSector, BUSYID_5, -65536/ClipLow((120*pXSector->busyTimeB)/10, 1));
+                SectorStartSound(nSector, 1);
+                break;
+            }
+            break;
+        case 604:
+            OperateTeleport(nSector, pXSector);
+            break;
+        case 612:
+            OperatePath(nSector, pXSector, a3);
+            break;
+        default:
+            if (pXSector->busyTimeA || pXSector->busyTimeB)
+                OperateDoor(nSector, pXSector, a3, BUSYID_6);
+            else
+            {
+                switch (a3.cmd)
+                {
+                case 0:
+                    SetSectorState(nSector, pXSector, 0);
+                    break;
+                case 1:
+                    SetSectorState(nSector, pXSector, 1);
+                    break;
+                default:
+                    SetSectorState(nSector, pXSector, pXSector->state^1);
+                    break;
+                }
+            }
+            break;
+        }
+        break;
+    }
+}
+
+void InitPath(unsigned int nSector, XSECTOR *pXSector)
+{
+    int nSprite;
+    spritetype *pSprite;
+    XSPRITE *pXSprite;
+    dassert(nSector < (unsigned int)numsectors);
+    int nId = pXSector->data;
+    for (nSprite = headspritestat[16]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        pSprite = &sprite[nSprite];
+        if (pSprite->type == 15)
+        {
+            pXSprite = &xsprite[pSprite->extra];
+            if (pXSprite->data1 == nId)
+                break;
+        }
+    }
+    if (nSprite < 0)
+        ThrowError("Unable to find path marker with id #%d", nId);
+    pXSector->at2c_0 = nSprite;
+    basePath[nSector] = nSprite;
+    if (pXSector->state)
+        evPost(nSector, 6, 0, COMMAND_ID_1);
+}
+
+void LinkSector(int nSector, XSECTOR *pXSector, EVENT a3)
+{
+    sectortype *pSector = &sector[nSector];
+    int nBusy = GetSourceBusy(a3);
+    switch (pSector->lotag)
+    {
+    case 602:
+        VSpriteBusy(nSector, nBusy);
+        break;
+    case 600:
+        VDoorBusy(nSector, nBusy);
+        break;
+    case 614:
+    case 616:
+        HDoorBusy(nSector, nBusy);
+        break;
+    case 615:
+    case 617:
+        RDoorBusy(nSector, nBusy);
+        break;
+     /* By NoOne: add link support for counter sectors so they can change necessary type and count of types*/
+    case kSecCounter:
+    {
+        int nXIndex;
+        nXIndex = sector[a3.index].extra;
+        XSECTOR* pXSector2 = &xsector[nXIndex];
+        pXSector->waitTimeA = pXSector2->waitTimeA;
+        pXSector->data = pXSector2->data;
+        break;
+    }
+    default:
+        pXSector->busy = nBusy;
+        if ((pXSector->busy&0xffff) == 0)
+            SetSectorState(nSector, pXSector, nBusy>>16);
+        break;
+    }
+}
+
+void LinkSprite(int nSprite, XSPRITE *pXSprite, EVENT a3)
+{
+    spritetype *pSprite = &sprite[nSprite];
+    int nBusy = GetSourceBusy(a3);
+    switch (pSprite->type)
+    {
+    
+    //By NoOne: these can be linked too now, so it's possible to change palette, underwater status and more...
+    case kMarkerLowWater:
+    case kMarkerUpWater:
+    case kMarkerUpGoo:
+    case kMarkerLowGoo:
+    case kMarkerUpLink:
+    case kMarkerLowLink:
+    case kMarkerUpStack:
+    case kMarkerLowStack:
+    {
+        if (a3.type != 3) break;
+        spritetype *pSprite2 = &sprite[a3.index];
+        if (pSprite2->extra < 0) break;
+        XSPRITE *pXSprite2 = &xsprite[pSprite2->extra];
+
+        // Only lower to lower and upper to upper linking allowed.
+        switch (pSprite->type) {
+        case kMarkerLowWater:
+        case kMarkerLowLink:
+        case kMarkerLowStack:
+        case kMarkerLowGoo:
+            switch (pSprite2->type) {
+            case kMarkerLowWater:
+            case kMarkerLowLink:
+            case kMarkerLowStack:
+            case kMarkerLowGoo:
+                break;
+            default:
+                return;
+            }
+            break;
+
+        case kMarkerUpWater:
+        case kMarkerUpLink:
+        case kMarkerUpStack:
+        case kMarkerUpGoo:
+            switch (pSprite2->type) {
+            case kMarkerUpWater:
+            case kMarkerUpLink:
+            case kMarkerUpStack:
+            case kMarkerUpGoo:
+                break;
+            default:
+                return;
+            }
+            break;
+        }
+
+        // swap link location
+        /*short tmp1 = pXSprite2.data1;*/
+        /*pXSprite2.data1 = pXSprite.data1;*/
+        /*pXSprite.data1 = tmp1;*/
+
+        if (pXSprite->data2 < kMaxPAL && pXSprite2->data2 < kMaxPAL)
+        {
+            // swap medium
+            int tmp2 = pXSprite2->data2;
+            pXSprite2->data2 = pXSprite->data2;
+            pXSprite->data2 = tmp2;
+        }
+
+
+        // swap link type                       // swap link owners (sectors)
+        short tmp3 = pSprite2->type;			//short tmp7 = pSprite2.owner;
+        pSprite2->type = pSprite->type;			//pSprite2.owner = pSprite.owner;
+        pSprite->type = tmp3;					//pSprite.owner = tmp7;
+
+        // Deal with linked sectors
+        sectortype *pSector = &sector[pSprite->sectnum];
+        sectortype *pSector2 = &sector[pSprite2->sectnum];
+
+        // Check for underwater
+        XSECTOR *pXSector = NULL;	XSECTOR *pXSector2 = NULL;
+        if (pSector->extra > 0) pXSector = &xsector[pSector->extra];
+        if (pSector2->extra > 0) pXSector2 = &xsector[pSector2->extra];
+        if (pXSector != NULL && pXSector2 != NULL) {
+            bool tmp6 = pXSector->Underwater;
+            pXSector->Underwater = pXSector2->Underwater;
+            pXSector2->Underwater = tmp6;
+        }
+
+        // optionally swap floorpic
+        if (pXSprite2->data3 == 1) {
+            short tmp4 = pSector->floorpicnum;
+            pSector->floorpicnum = pSector2->floorpicnum;
+            pSector2->floorpicnum = tmp4;
+        }
+
+        // optionally swap ceilpic
+        if (pXSprite2->data4 == 1) {
+            short tmp5 = pSector->ceilingpicnum;
+            pSector->ceilingpicnum = pSector2->ceilingpicnum;
+            pSector2->ceilingpicnum = tmp5;
+        }
+    }
+    break;
+    // By NoOne: add a way to link between path markers, so path sectors can change their path on the fly.
+    case kMarkerPath:
+    {
+        // only path marker to path marker link allowed
+        if (a3.type == 3)
+        {
+            int nXSprite2 = sprite[a3.index].extra;
+            // get master path marker data fields
+            pXSprite->data1 = xsprite[nXSprite2].data1;
+            pXSprite->data2 = xsprite[nXSprite2].data2;
+            pXSprite->data3 = xsprite[nXSprite2].data3; // include soundId(?)
+
+            // get master path marker busy and wait times
+            pXSprite->busyTime = xsprite[nXSprite2].busyTime;
+            pXSprite->waitTime = xsprite[nXSprite2].waitTime;
+
+        }
+    }
+    break;
+    case kSwitchCombo:
+    {
+        if (a3.type == 3)
+        {
+            int nSprite2 = a3.index;
+            int nXSprite2 = sprite[nSprite2].extra;
+            dassert(nXSprite2 > 0 && nXSprite2 < kMaxXSprites);
+            pXSprite->data1 = xsprite[nXSprite2].data1;
+            if (pXSprite->data1 == pXSprite->data2)
+                SetSpriteState(nSprite, pXSprite, 1);
+            else
+                SetSpriteState(nSprite, pXSprite, 0);
+        }
+    }
+    break;
+    default:
+    {
+        pXSprite->busy = nBusy;
+        if ((pXSprite->busy & 0xffff) == 0)
+            SetSpriteState(nSprite, pXSprite, nBusy >> 16);
+    }
+    break;
+    }
+}
+
+void LinkWall(int nWall, XWALL *pXWall, EVENT a3)
+{
+    int nBusy = GetSourceBusy(a3);
+    pXWall->busy = nBusy;
+    if ((pXWall->busy & 0xffff) == 0)
+        SetWallState(nWall, pXWall, nBusy>>16);
+}
+
+void trTriggerSector(unsigned int nSector, XSECTOR *pXSector, int a3)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    if (!pXSector->locked && !pXSector->at16_6)
+    {
+        if (pXSector->triggerOnce)
+            pXSector->at16_6 = 1;
+        if (pXSector->decoupled)
+        {
+            if (pXSector->txID)
+                evSend(nSector, 6, pXSector->txID, (COMMAND_ID)pXSector->command);
+        }
+        else
+        {
+            EVENT evnt;
+            evnt.cmd = a3;
+            OperateSector(nSector, pXSector, evnt);
+        }
+    }
+}
+
+void trMessageSector(unsigned int nSector, EVENT a2)
+{
+    dassert(nSector < (unsigned int)numsectors);
+    dassert(sector[nSector].extra > 0 && sector[nSector].extra < kMaxXSectors);
+    int nXSector = sector[nSector].extra;
+    XSECTOR *pXSector = &xsector[nXSector];
+    if (!pXSector->locked || a2.cmd == 7 || a2.cmd == 8)
+    {
+        if (a2.cmd == 5)
+            LinkSector(nSector, pXSector, a2);
+        else if (a2.cmd == kGDXCommandPaste)
+            pastePropertiesInObj(6, nSector, a2);
+        else
+            OperateSector(nSector, pXSector, a2);
+    }
+}
+
+void trTriggerWall(unsigned int nWall, XWALL *pXWall, int a3)
+{
+    dassert(nWall < (unsigned int)numwalls);
+    if (!pXWall->locked && !pXWall->isTriggered)
+    {
+        if (pXWall->triggerOnce)
+            pXWall->isTriggered = 1;
+        if (pXWall->decoupled)
+        {
+            if (pXWall->txID)
+                evSend(nWall, 0, pXWall->txID, (COMMAND_ID)pXWall->command);
+        }
+        else
+        {
+            EVENT evnt;
+            evnt.cmd = a3;
+            OperateWall(nWall, pXWall, evnt);
+        }
+    }
+}
+
+void trMessageWall(unsigned int nWall, EVENT a2)
+{
+    dassert(nWall < (unsigned int)numwalls);
+    dassert(wall[nWall].extra > 0 && wall[nWall].extra < kMaxXWalls);
+    int nXWall = wall[nWall].extra;
+    XWALL *pXWall = &xwall[nXWall];
+    if (!pXWall->locked || a2.cmd == 7 || a2.cmd == 8)
+    {
+        if (a2.cmd == 5)
+            LinkWall(nWall, pXWall, a2);
+        else if (a2.cmd == kGDXCommandPaste)
+            pastePropertiesInObj(0, nWall, a2);
+        else
+            OperateWall(nWall, pXWall, a2);
+    }
+}
+
+void trTriggerSprite(unsigned int nSprite, XSPRITE *pXSprite, int a3)
+{
+    if (!pXSprite->locked && !pXSprite->isTriggered)
+    {
+        if (pXSprite->triggerOnce)
+            pXSprite->isTriggered = 1;
+        if (pXSprite->Decoupled)
+        {
+            if (pXSprite->txID)
+                evSend(nSprite, 3, pXSprite->txID, (COMMAND_ID)pXSprite->command);
+        }
+        else
+        {
+            EVENT evnt;
+            evnt.cmd = a3;
+            OperateSprite(nSprite, pXSprite, evnt);
+        }
+    }
+}
+
+void trMessageSprite(unsigned int nSprite, EVENT a2)
+{
+    if (sprite[nSprite].statnum == kStatFree)
+        return;
+    int nXSprite = sprite[nSprite].extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    if (!pXSprite->locked || a2.cmd == 7 || a2.cmd == 8)
+    {
+        if (a2.cmd == 5)
+            LinkSprite(nSprite, pXSprite, a2);
+        else if (a2.cmd == kGDXCommandPaste)
+            pastePropertiesInObj(3, nSprite, a2);
+        else if (a2.cmd == kGDXCommandSpriteDamage)
+            trDamageSprite(3, nSprite, a2);
+        else
+            OperateSprite(nSprite, pXSprite, a2);
+    }
+}
+
+// By NoOne: this function damages sprite
+void trDamageSprite(int type, int nDest, EVENT event) {
+    UNREFERENCED_PARAMETER(type);
+
+    /* - damages xsprite via TX ID	- */
+    /* - data3 = damage type		- */
+    /* - data4 = damage amount		- */
+
+    if (event.type == 3) {
+        spritetype* pSource = NULL; pSource = &sprite[event.index];
+        XSPRITE* pXSource = &xsprite[pSource->extra];
+        if (xsprite[sprite[nDest].extra].health > 0) {
+            if (pXSource->data4 == 0)
+                pXSource->data4 = 65535;
+
+            int dmgType = pXSource->data3;
+            if (pXSource->data3 >= 7)
+                dmgType = Random(6);
+
+            actDamageSprite(pSource->xvel, &sprite[nDest], (DAMAGE_TYPE) dmgType, pXSource->data4);
+        }
+    }
+}
+
+bool valueIsBetween(int val, int min, int max) {
+    return (val > min && val < max);
+}
+// By NoOne: this function used by various new GDX types.
+void pastePropertiesInObj(int type, int nDest, EVENT event) {
+    spritetype* pSource = NULL; pSource = &sprite[event.index];
+    if (pSource == NULL || event.type != 3) return;
+    XSPRITE* pXSource = &xsprite[pSource->extra];
+    
+    if (pSource->type == kGDXEffectSpawner) {
+        /* - Effect Spawner can spawn any effect passed in data2 on it's or txID sprite - */
+        if (pXSource->data2 < 0 || pXSource->data2 >= kFXMax) return;
+        else if (type == 3)  useEffectGen(pXSource, &sprite[nDest]);
+        return;
+
+    } else if (pSource->type == kGDXSeqSpawner) {
+        /* - SEQ Spawner takes data2 as SEQ ID and spawns it on it's or TX ID sprite - */
+        if (pXSource->data2 <= 0 || !gSysRes.Lookup(pXSource->data2, "SEQ")) return;
+        else if (type == 3) useSeqSpawnerGen(pXSource, &sprite[nDest]);
+        return;
+
+    } else if (pSource->type == kGDXObjDataAccumulator) {
+        /* - Object Data Accumulator allows to perform sum and sub operations in data fields of object - */
+        /* - data1 = destination data index 															- */
+        /* - data2 = step value																			- */
+        /* - data3 = min value																			- */
+        /* - data4 = max value																			- */
+        /* - min > max = sub, 	min < max = sum															- */
+
+        /* - hitag: 0 = force OFF if goal value was reached for all objects		     					- */
+        /* - hitag: 2 = force swap min and max if goal value was reached								- */
+        /* - hitag: 3 = force reset counter	                                           					- */
+
+        if (pXSource->data3 < 0) pXSource->data3 = 0;
+        else if (pXSource->data3 > 32766) pXSource->data3 = 32767;
+        if (pXSource->data4 < 0) pXSource->data4 = 0;
+        else if (pXSource->data4 > 32766) pXSource->data4 = 32767;
+
+        long data = getDataFieldOfObject(type, nDest, pXSource->data1);
+        if (data == -65535) return;
+        else if (pXSource->data3 < pXSource->data4) {
+
+            if (data < pXSource->data3) data = pXSource->data3;
+            if (data > pXSource->data4) data = pXSource->data4;
+
+            if ((data += pXSource->data2) >= pXSource->data4) {
+                switch (pSource->hitag) {
+                    case 0:
+                    case 1:
+                        if (data > pXSource->data4) data = pXSource->data4;
+                        break;
+                    case 2:
+                    {
+                        
+                        if (data > pXSource->data4) data = pXSource->data4;
+                        if (!goalValueIsReached(pXSource)) break;
+                        int tmp = pXSource->data4;
+                        pXSource->data4 = pXSource->data3;
+                        pXSource->data3 = tmp;
+                        
+                    }
+                    break;
+                    case 3:
+                        if (data > pXSource->data4) data = pXSource->data3;
+                        break;
+                }
+            }
+
+        }
+        else if (pXSource->data3 > pXSource->data4) {
+
+            if (data > pXSource->data3) data = pXSource->data3;
+            if (data < pXSource->data4) data = pXSource->data4;
+
+            if ((data -= pXSource->data2) <= pXSource->data4) {
+                switch (pSource->hitag) {
+                    case 0:
+                    case 1:
+                        if (data < pXSource->data4) data = pXSource->data4;
+                        break;
+                    case 2:
+                    {
+                        if (data < pXSource->data4) data = pXSource->data4;
+                        int tmp = pXSource->data4;
+                        pXSource->data4 = pXSource->data3;
+                        pXSource->data3 = tmp;
+                        break;
+                    }
+                    case 3:
+                        if (data < pXSource->data4) data = pXSource->data3;
+                        break;
+                }
+            }
+        }
+        
+        setDataValueOfObject(type, nDest, pXSource->data1, data);
+        return;
+
+    } else if (pSource->type == kGDXWindGenerator) {
+
+        /* - Wind generator via TX or for current sector if TX ID not specified - */
+        /* - sprite.ang = sector wind direction									- */
+        /* - data1 = randomness settings										- */
+        /* - 		 0: no randomness											- */
+        /* - 		 1: randomize wind velocity in data2						- */
+        /* - 		 2: randomize current generator sprite angle				- */
+        /* - 		 3: randomize both wind velocity and sprite angle			- */
+        /* - data2 = wind velocity												- */
+        /* - data3 = enable panning according current wind speed and direction	- */
+        /* - data4 = pan floor and ceiling settings								- */
+        /* - 		 0: use sector pan settings									- */
+        /* - 		 1: pan only floor											- */
+        /* - 		 2: pan only ceiling										- */
+        /* - 		 3: pan both												- */
+
+        /* - hi-tag = 1: force windAlways and panAlways							- */
+        
+        if (pXSource->data2 < 0) return;
+        else if (type == 6) useSectorWindGen(pXSource, &sector[nDest]);
+        return;
+
+
+    } else if (pSource->type == kGDXObjDataChanger) {
+
+        /* - Data field changer via TX - */
+        /* - data1 = sprite data1 / sector data / wall data	- */
+        /* - data2 = sprite data2	- */
+        /* - data3 = sprite data3	- */
+        /* - data4 = sprite data4	- */
+
+        switch (type) {
+            // for sectors
+            case 6:
+            {
+                XSECTOR* pXSector = &xsector[sector[nDest].extra];
+
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    pXSector->data = pXSource->data1;
+
+                break;
+            }
+            // for sprites
+            case 3:
+            {
+                XSPRITE* pXSprite = &xsprite[sprite[nDest].extra];
+
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    pXSprite->data1 = pXSource->data1;
+
+                if (valueIsBetween(pXSource->data2, -1, 32767))
+                    pXSprite->data2 = pXSource->data2;
+
+                if (valueIsBetween(pXSource->data3, -1, 32767))
+                    pXSprite->data3 = pXSource->data3;
+
+                if (valueIsBetween(pXSource->data4, -1, 65535))
+                    pXSprite->data4 = pXSource->data4;
+
+                break;
+            }
+            // for walls
+            case 0:
+            {
+                XWALL* pXWall = &xwall[wall[nDest].extra];
+
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    pXWall->data = pXSource->data1;
+
+                break;
+            }
+        }
+
+    } else if (pSource->type == kGDXSectorFXChanger) {
+
+        /* - FX Wave changer for sector via TX - */
+        /* - data1 = Wave 	- */
+        /* - data2 = Amplitude	- */
+        /* - data3 = Freq	- */
+        /* - data4 = Phase	- */
+
+        if (type == 6) {
+            XSECTOR* pXSector = &xsector[sector[nDest].extra];
+            if (valueIsBetween(pXSource->data1, -1, 32767))
+                pXSector->wave = pXSource->data1;
+
+            if (pXSource->data2 >= 0) {
+
+                if (pXSource->data2 > 127) pXSector->amplitude = 127;
+                else pXSector->amplitude = pXSource->data2;
+
+            }
+            else if (pXSource->data2 < -1) {
+
+                if (pXSource->data2 < -127) pXSector->amplitude = -127;
+                else pXSector->amplitude = pXSource->data2;
+
+            }
+
+            if (valueIsBetween(pXSource->data3, -1, 32767)) {
+                if (pXSource->data3 > 255) pXSector->freq = 255;
+                else pXSector->freq = pXSource->data3;
+            }
+
+            if (valueIsBetween(pXSource->data4, -1, 65535)) {
+                if (pXSource->data4 > 255) pXSector->phase = 255;
+                else pXSector->phase = (short)pXSource->data4;
+            }
+
+            if ((pSource->hitag & kHitagExtBit) != 0)
+                pXSector->shadeAlways = true;
+
+        }
+
+    } else if (pSource->type == kGDXDudeTargetChanger) {
+
+        /* - Target changer for dudes via TX																		- */
+
+        /* - data1 = target dude data1 value (can be zero)															- */
+        /* 			 666: attack everyone, even if data1 id does not fit, except mates (if any)						- */
+        /* - data2 = 0: AI deathmatch mode																			- */
+        /*			 1: AI team deathmatch mode																		- */
+        /* - data3 = 0: do not force target to fight dude back and *do not* awake some inactive monsters in sight	- */
+        /* 			 1: force target to fight dude back	and *do not* awake some inactive monsters in sight			- */
+        /*			 2: force target to fight dude back	and awake some inactive monsters in sight					- */
+        /* - data4 = 0: do not ignore player(s) (even if enough targets in sight)									- */
+        /*			 1: try to ignore player(s) (while enough targets in sight)										- */
+        /*			 2: ignore player(s) (attack only when no targets in sight at all)								- */
+        /*			 3: go to idle state if no targets in sight and ignore player(s) always							- */
+        /*			 4: follow player(s) when no targets in sight, attack targets if any in sight					- */
+
+        if (type != 3 || !IsDudeSprite(&sprite[nDest]) || sprite[nDest].statnum != 6) return;
+        spritetype* pSprite = &sprite[nDest]; XSPRITE* pXSprite = &xsprite[pSprite->extra];
+        spritetype* pTarget = NULL; XSPRITE* pXTarget = NULL; int receiveHp = 33 + Random(33);
+        DUDEINFO* pDudeInfo = &dudeInfo[pSprite->lotag - kDudeBase]; int matesPerEnemy = 1;
+
+        // dude is burning?
+        if (pXSprite->burnTime > 0 && pXSprite->burnSource >= 0 && pXSprite->burnSource < kMaxSprites) {
+            if (!IsBurningDude(pSprite))
+            {
+                spritetype* pBurnSource = &sprite[pXSprite->burnSource];
+                if (pBurnSource->extra >= 0) {
+                    if (pXSource->data2 == 1 && isMateOf(pXSprite, &xsprite[pBurnSource->extra])) {
+                        pXSprite->burnTime = 0;
+
+                        // heal dude a bit in case of friendly fire
+                        if (pXSprite->data4 > 0 && pXSprite->health < pXSprite->data4)
+                            actHealDude(pXSprite, receiveHp, pXSprite->data4);
+                        else if (pXSprite->health < pDudeInfo->startHealth)
+                            actHealDude(pXSprite, receiveHp, pDudeInfo->startHealth);
+                    }
+                    else if (xsprite[pBurnSource->extra].health <= 0) {
+                        pXSprite->burnTime = 0;
+                    }
+                }
+            }
+            else {
+                actKillDude(pSource->xvel, pSprite, DAMAGE_TYPE_0, 65535);
+                return;
+            }
+        }
+
+        spritetype* pPlayer = targetIsPlayer(pXSprite);
+        // special handling for player(s) if target changer data4 > 2.
+        if (pPlayer != NULL) {
+            if (pXSource->data4 == 3) {
+                aiSetTarget(pXSprite, pSprite->x, pSprite->y, pSprite->z);
+                aiSetGenIdleState(pSprite, pXSprite);
+                if (pSprite->lotag == kGDXDudeUniversalCultist)
+                    removeLeech(leechIsDropped(pSprite));
+            }
+            else if (pXSource->data4 == 4) {
+                aiSetTarget(pXSprite, pPlayer->x, pPlayer->y, pPlayer->z);
+                if (pSprite->lotag == kGDXDudeUniversalCultist)
+                    removeLeech(leechIsDropped(pSprite));
+            }
+        }
+
+        int maxAlarmDudes = 8 + Random(8);
+        if (pXSprite->target > -1 && sprite[pXSprite->target].extra > -1 && pPlayer == NULL) {
+            pTarget = &sprite[pXSprite->target]; pXTarget = &xsprite[pTarget->extra];
+
+            if (unitCanFly(pSprite) && isMeleeUnit(pTarget) && !unitCanFly(pTarget))
+                pSprite->hitag |= 0x0002;
+            else if (unitCanFly(pSprite))
+                pSprite->hitag &= ~0x0002;
+
+            if (!IsDudeSprite(pTarget) || pXTarget->health < 1 || !dudeCanSeeTarget(pXSprite, pDudeInfo, pTarget)) {
+                aiSetTarget(pXSprite, pSprite->x, pSprite->y, pSprite->z);
+            }
+            // dude attack or attacked by target that does not fit by data id?
+            else if (pXSource->data1 != 666 && pXTarget->data1 != pXSource->data1) {
+                if (affectedByTargetChg(pXTarget)) {
+
+                    // force stop attack target
+                    aiSetTarget(pXSprite, pSprite->x, pSprite->y, pSprite->z);
+                    if (pXSprite->burnSource == pTarget->xvel) {
+                        pXSprite->burnTime = 0;
+                        pXSprite->burnSource = -1;
+                    }
+
+                    // force stop attack dude
+                    aiSetTarget(pXTarget, pTarget->x, pTarget->y, pTarget->z);
+                    if (pXTarget->burnSource == pSprite->xvel) {
+                        pXTarget->burnTime = 0;
+                        pXTarget->burnSource = -1;
+                    }
+                }
+                
+            }
+            // instantly kill annoying spiders, rats, hands etc if dude is big enough
+            else if (isAnnoyingUnit(pTarget) && !isAnnoyingUnit(pSprite) && tilesiz[pSprite->picnum].y >= 60 &&
+                getTargetDist(pSprite, pDudeInfo, pTarget) < 2) {
+
+                actKillDude(pSource->xvel, pTarget, DAMAGE_TYPE_0, 65535);
+                aiSetTarget(pXSprite, pSprite->x, pSprite->y, pSprite->z);
+
+            }
+            else if (pXSource->data2 == 1 && isMateOf(pXSprite, pXTarget)) {
+                spritetype* pMate = pTarget; XSPRITE* pXMate = pXTarget;
+
+                // heal dude
+                if (pXSprite->data4 > 0 && pXSprite->health < pXSprite->data4)
+                    actHealDude(pXSprite, receiveHp, pXSprite->data4);
+                else if (pXSprite->health < pDudeInfo->startHealth)
+                    actHealDude(pXSprite, receiveHp, pDudeInfo->startHealth);
+
+                // heal mate
+                if (pXMate->data4 > 0 && pXMate->health < pXMate->data4)
+                    actHealDude(pXMate, receiveHp, pXMate->data4);
+                else {
+                    DUDEINFO* pTDudeInfo = &dudeInfo[pMate->lotag - kDudeBase];
+                    if (pXMate->health < pTDudeInfo->startHealth)
+                        actHealDude(pXMate, receiveHp, pTDudeInfo->startHealth);
+                }
+
+                if (pXMate->target > -1 && sprite[pXMate->target].extra >= 0) {
+                    pTarget = &sprite[pXMate->target];
+                    // force mate stop attack dude, if he does
+                    if (pXMate->target == pSprite->xvel) {
+                        aiSetTarget(pXMate, pMate->x, pMate->y, pMate->z);
+                    }
+                    else if (!isMateOf(pXSprite, &xsprite[pTarget->extra])) {
+                        // force dude to attack same target that mate have
+                        aiSetTarget(pXSprite, pTarget->xvel);
+                        return;
+
+                    }
+                    else {
+                        // force mate to stop attack another mate
+                        aiSetTarget(pXMate, pMate->x, pMate->y, pMate->z);
+                    }
+                }
+
+                // force dude stop attack mate, if target was not changed previously
+                if (pXSprite->target == pMate->xvel)
+                    aiSetTarget(pXSprite, pSprite->x, pSprite->y, pSprite->z);
+
+                
+            }
+            // check if targets aims player then force this target to fight with dude
+            else if (targetIsPlayer(pXTarget) != NULL) {
+                aiSetTarget(pXTarget, pSprite->xvel);
+            }
+
+            int mDist = 3; if (isMeleeUnit(pSprite)) mDist = 2;
+            if (pXSprite->target >= 0 && getTargetDist(pSprite, pDudeInfo, &sprite[pXSprite->target]) < mDist) {
+                if (!isActive(pSprite->xvel)) aiActivateDude(pSprite, pXSprite);
+                return;
+            }
+            // lets try to look for target that fits better by distance
+            else if ((gFrameClock & 256) != 0 && (pXSprite->target < 0 || getTargetDist(pSprite, pDudeInfo, pTarget) >= mDist)) {
+                pTarget = getTargetInRange(pSprite, 0, mDist, pXSource->data1, pXSource->data2);
+                if (pTarget != NULL) {
+                    pXTarget = &xsprite[pTarget->extra];
+
+                    // Make prev target not aim in dude
+                    if (pXSprite->target > -1) {
+                        spritetype* prvTarget = &sprite[pXSprite->target];
+                        aiSetTarget(&xsprite[prvTarget->extra], prvTarget->x, prvTarget->y, prvTarget->z);
+                        if (!isActive(pTarget->xvel))
+                            aiActivateDude(pTarget, pXTarget);
+                    }
+
+                    // Change target for dude
+                    aiSetTarget(pXSprite, pTarget->xvel);
+                    if (!isActive(pSprite->xvel))
+                        aiActivateDude(pSprite, pXSprite);
+
+                    // ...and change target of target to dude to force it fight
+                    if (pXSource->data3 > 0 && pXTarget->target != pSprite->xvel) {
+                        aiSetTarget(pXTarget, pSprite->xvel);
+                        if (!isActive(pTarget->xvel))
+                            aiActivateDude(pTarget, pXTarget);
+                    }
+                    return;
+                }
+            }
+        }
+
+        if ((pXSprite->target < 0 || pPlayer != NULL) && (gFrameClock & 32) != 0) {
+            // try find first target that dude can see
+            for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+                pTarget = &sprite[nSprite]; pXTarget = &xsprite[pTarget->extra];
+
+                if (pXTarget->target == pSprite->xvel) {
+                    aiSetTarget(pXSprite, pTarget->xvel);
+                    return;
+                }
+
+                // skip non-dudes and players
+                if (!IsDudeSprite(pTarget) || (IsPlayerSprite(pTarget) && pXSource->data4 > 0) || pTarget->owner == pSprite->xvel) continue;
+                // avoid self aiming, those who dude can't see, and those who dude own
+                else if (!dudeCanSeeTarget(pXSprite, pDudeInfo, pTarget) || pSprite->xvel == pTarget->xvel) continue;
+                // if Target Changer have data1 = 666, everyone can be target, except AI team mates.
+                else if (pXSource->data1 != 666 && pXSource->data1 != pXTarget->data1) continue;
+                // don't attack immortal, burning dudes and mates
+                if (IsBurningDude(pTarget) || !IsKillableDude(pTarget, true) || (pXSource->data2 == 1 && isMateOf(pXSprite, pXTarget)))
+                    continue;
+
+                if (pXSource->data2 == 0 || (pXSource->data2 == 1 && !isMatesHaveSameTarget(pXSprite, pTarget, matesPerEnemy))) {
+
+                    // Change target for dude
+                    aiSetTarget(pXSprite, pTarget->xvel);
+                    if (!isActive(pSprite->xvel))
+                        aiActivateDude(pSprite, pXSprite);
+
+                    // ...and change target of target to dude to force it fight
+                    if (pXSource->data3 > 0 && pXTarget->target != pSprite->xvel) {
+                        aiSetTarget(pXTarget, pSprite->xvel);
+                        if (!isActive(pTarget->xvel))
+                            aiActivateDude(pTarget, pXTarget);
+
+                        if (pXSource->data3 == 2)
+                            disturbDudesInSight(pTarget, maxAlarmDudes);
+                    }
+                    return;
+                }
+                break;
+            }
+        }
+
+        // got no target - let's ask mates if they have targets
+        if ((pXSprite->target < 0 || pPlayer != NULL) && pXSource->data2 == 1 && (gFrameClock & 64) != 0) {
+            spritetype* pMateTarget = NULL;
+            if ((pMateTarget = getMateTargets(pXSprite)) != NULL && pMateTarget->extra > 0) {
+                XSPRITE* pXMateTarget = &xsprite[pMateTarget->extra];
+                if (dudeCanSeeTarget(pXSprite, pDudeInfo, pMateTarget)) {
+                    if (pXMateTarget->target < 0) {
+                        aiSetTarget(pXMateTarget, pSprite->xvel);
+                        if (IsDudeSprite(pMateTarget) && !isActive(pMateTarget->xvel))
+                            aiActivateDude(pMateTarget, pXMateTarget);
+                    }
+
+                    aiSetTarget(pXSprite, pMateTarget->xvel);
+                    if (!isActive(pSprite->xvel))
+                        aiActivateDude(pSprite, pXSprite);
+                    return;
+
+                    // try walk in mate direction in case if not see the target
+                }
+                else if (pXMateTarget->target >= 0 && dudeCanSeeTarget(pXSprite, pDudeInfo, &sprite[pXMateTarget->target])) {
+                    spritetype* pMate = &sprite[pXMateTarget->target];
+                    pXSprite->target = pMateTarget->xvel;
+                    pXSprite->targetX = pMate->x;
+                    pXSprite->targetY = pMate->y;
+                    pXSprite->targetZ = pMate->z;
+                    if (!isActive(pSprite->xvel))
+                        aiActivateDude(pSprite, pXSprite);
+                    return;
+                }
+            }
+        }
+
+    } else if (pSource->type == kGDXObjSizeChanger) {
+
+        /* - size and pan changer of sprite/wall/sector via TX ID 	- */
+        /* - data1 = sprite xrepeat / wall xrepeat / floor xpan 	- */
+        /* - data2 = sprite yrepeat / wall yrepeat / floor ypan 	- */
+        /* - data3 = sprite xoffset / wall xoffset / ceil xpan 		- */
+        /* - data3 = sprite yoffset / wall yoffset / ceil ypan 		- */
+
+        if (pXSource->data1 > 255) pXSource->data1 = 255;
+        if (pXSource->data2 > 255) pXSource->data2 = 255;
+        if (pXSource->data3 > 255) pXSource->data3 = 255;
+        if (valueIsBetween(pXSource->data4, 255, 65535))
+            pXSource->data4 = 255;
+
+        switch (type) {
+            // for sectors
+            case 6:
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    sector[nDest].floorxpanning = pXSource->data1;
+
+                if (valueIsBetween(pXSource->data2, -1, 32767))
+                    sector[nDest].floorypanning = pXSource->data2;
+
+                if (valueIsBetween(pXSource->data3, -1, 32767))
+                    sector[nDest].ceilingxpanning = pXSource->data3;
+
+                if (valueIsBetween(pXSource->data4, -1, 65535))
+                    sector[nDest].ceilingypanning = (short)pXSource->data4;
+                break;
+            // for sprites
+            case 3:
+
+                if (valueIsBetween(pXSource->data1, -1, 32767) &&
+                    valueIsBetween(pXSource->data2, -1, 32767) &&
+                    pXSource->data1 < 4 && pXSource->data2 < 4)
+                {
+
+                    sprite[nDest].xrepeat = 4;
+                    sprite[nDest].yrepeat = 4;
+                    sprite[nDest].cstat |= kSprInvisible;
+
+                }
+                else {
+
+                    if (valueIsBetween(pXSource->data1, -1, 32767)) {
+                        if (pXSource->data1 < 4)
+                            sprite[nDest].xrepeat = 4;
+                        else
+                            sprite[nDest].xrepeat = pXSource->data1;
+                    }
+
+                    if (valueIsBetween(pXSource->data2, -1, 32767)) {
+                        if (pXSource->data2 < 4)
+                            sprite[nDest].yrepeat = 4;
+                        else
+                            sprite[nDest].yrepeat = pXSource->data2;
+                    }
+                }
+
+                if (valueIsBetween(pXSource->data3, -1, 32767))
+                    sprite[nDest].xoffset = pXSource->data3;
+
+                if (valueIsBetween(pXSource->data4, -1, 65535))
+                    sprite[nDest].yoffset = (short)pXSource->data4;
+
+                break;
+            // for walls
+            case 0:
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    wall[nDest].xrepeat = pXSource->data1;
+
+                if (valueIsBetween(pXSource->data2, -1, 32767))
+                    wall[nDest].yrepeat = pXSource->data2;
+
+                if (valueIsBetween(pXSource->data3, -1, 32767))
+                    wall[nDest].xpanning = (short)pXSource->data3;
+
+                if (valueIsBetween(pXSource->data4, -1, 65535))
+                    wall[nDest].ypanning = (short)pXSource->data4;
+
+                break;
+        }
+
+    } else if (pSource->type == kGDXObjPicnumChanger) {
+
+        /* - picnum changer can change picnum of sprite/wall/sector via TX ID - */
+        /* - data1 = sprite pic / wall pic / sector floor pic 				 - */
+        /* - data3 = sprite pal / wall pal / sector floor pic				- */
+
+        switch (type) {
+            // for sectors
+            case 6:
+            {
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    sector[nDest].floorpicnum = pXSource->data1;
+
+                if (valueIsBetween(pXSource->data2, -1, 32767))
+                    sector[nDest].ceilingpicnum = pXSource->data2;
+
+                XSECTOR *pXSector = &xsector[sector[nDest].extra];
+                if (valueIsBetween(pXSource->data3, -1, 32767)) {
+                    sector[nDest].floorpal = pXSource->data3;
+                    if ((pSource->hitag & kHitagExtBit) != 0)
+                        pXSector->floorpal = pXSource->data3;
+                }
+
+                if (valueIsBetween(pXSource->data4, -1, 65535)) {
+                    sector[nDest].ceilingpal = (short)pXSource->data4;
+                    if ((pSource->hitag & kHitagExtBit) != 0)
+                        pXSector->ceilpal = (short)pXSource->data4;
+                }
+                break;
+            }
+            // for sprites
+            case 3:
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    sprite[nDest].picnum = pXSource->data1;
+
+                if (valueIsBetween(pXSource->data3, -1, 32767))
+                    sprite[nDest].pal = pXSource->data3;
+                break;
+            // for walls
+            case 0:
+                if (valueIsBetween(pXSource->data1, -1, 32767))
+                    wall[nDest].picnum = pXSource->data1;
+
+                if (valueIsBetween(pXSource->data2, -1, 32767))
+                    wall[nDest].overpicnum = pXSource->data2;
+
+                if (valueIsBetween(pXSource->data3, -1, 32767))
+                    wall[nDest].pal = pXSource->data3;
+                break;
+        }
+
+    } else if (pSource->type == kGDXObjPropertiesChanger) {
+
+        /* - properties changer can change various properties of sprite/wall/sector via TX ID 	- */
+        /* - data1 = sector underwater status													- */
+        /* - data2 = sector visibility 															- */
+        /* - data3 = sector ceiling cstat / sprite / wall hitag 								- */
+        /* - data4 = sector floor / sprite / wall cstat 										- */
+
+        switch (type) {
+        // for sectors
+        case 6:
+        {
+            XSECTOR* pXSector = &xsector[sector[nDest].extra];
+
+            switch (pXSource->data1) {
+            case 0:
+                pXSector->Underwater = false;
+                break;
+            case 1:
+                pXSector->Underwater = true;
+                break;
+            case 2:
+                pXSector->Depth = 0;
+                break;
+            case 3:
+                pXSector->Depth = 1;
+                break;
+            case 4:
+                pXSector->Depth = 2;
+                break;
+            case 5:
+                pXSector->Depth = 3;
+                break;
+            case 6:
+                pXSector->Depth = 4;
+                break;
+            case 7:
+                pXSector->Depth = 5;
+                break;
+            case 8:
+                pXSector->Depth = 6;
+                break;
+            case 9:
+                pXSector->Depth = 7;
+                break;
+            }
+
+            if (valueIsBetween(pXSource->data2, -1, 32767)) {
+                if (pXSource->data2 > 255) sector[nDest].visibility = 255;
+                else sector[nDest].visibility = pXSource->data2;
+            }
+
+            if (valueIsBetween(pXSource->data3, -1, 32767))
+                sector[nDest].ceilingstat = pXSource->data3;
+
+            if (valueIsBetween(pXSource->data4, -1, 65535))
+                sector[nDest].floorstat = pXSource->data4;
+            break;
+        }
+        // for sprites
+        case 3:
+            if (valueIsBetween(pXSource->data3, -1, 32767))
+                sprite[nDest].hitag = pXSource->data3;
+
+            if (valueIsBetween(pXSource->data4, -1, 65535)) {
+                pXSource->data4 |= kSprOriginAlign;
+                sprite[nDest].cstat = pXSource->data4;
+            }
+            break;
+            // for walls
+        case 0:
+            if (valueIsBetween(pXSource->data3, -1, 32767))
+                wall[nDest].hitag = pXSource->data3;
+
+            if (valueIsBetween(pXSource->data4, -1, 65535))
+                wall[nDest].cstat = pXSource->data4;
+            break;
+        }
+    }
+}
+// By NoOne: the following functions required for kGDXDudeTargetChanger
+//---------------------------------------
+spritetype* getTargetInRange(spritetype* pSprite, int minDist, int maxDist, short data, short teamMode) {
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->type - kDudeBase]; XSPRITE* pXSprite = &xsprite[pSprite->extra];
+    spritetype* pTarget = NULL; XSPRITE* pXTarget = NULL; spritetype* cTarget = NULL;
+    for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        pTarget = &sprite[nSprite];  pXTarget = &xsprite[pTarget->extra];
+        if (!dudeCanSeeTarget(pXSprite, pDudeInfo, pTarget)) continue;
+
+        int dist = getTargetDist(pSprite, pDudeInfo, pTarget);
+        if (dist < minDist || dist > maxDist) continue;
+        else if (pXSprite->target == pTarget->xvel) return pTarget;
+        else if (!IsDudeSprite(pTarget) || pTarget->xvel == pSprite->xvel || IsPlayerSprite(pTarget)) continue;
+        else if (IsBurningDude(pTarget) || !IsKillableDude(pTarget, true) || pTarget->owner == pSprite->xvel) continue;
+        else if ((teamMode == 1 && isMateOf(pXSprite, pXTarget)) || isMatesHaveSameTarget(pXSprite,pTarget,1)) continue;
+        else if (data == 666 || pXTarget->data1 == data) {
+
+            if (pXSprite->target > 0) {
+                cTarget = &sprite[pXSprite->target];
+                int fineDist1 = getFineTargetDist(pSprite, cTarget);
+                int fineDist2 = getFineTargetDist(pSprite, pTarget);
+                if (fineDist1 < fineDist2)
+                    continue;
+            }
+            return pTarget;
+        }
+    }
+
+    return NULL;
+}
+
+bool isMateOf(XSPRITE* pXDude, XSPRITE* pXSprite) {
+    return (pXDude->rxID == pXSprite->rxID);
+}
+
+spritetype* targetIsPlayer(XSPRITE* pXSprite) {
+
+    if (pXSprite->target >= 0) {
+        if (IsPlayerSprite(&sprite[pXSprite->target]))
+            return &sprite[pXSprite->target];
+    }
+
+    return NULL;
+}
+
+bool isTargetAimsDude(XSPRITE* pXTarget, spritetype* pDude) {
+    return (pXTarget->target == pDude->xvel);
+}
+
+spritetype* getMateTargets(XSPRITE* pXSprite) {
+    int rx = pXSprite->rxID; spritetype* pMate = NULL; XSPRITE* pXMate = NULL;
+
+    for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) {
+        if (rxBucket[i].type == 3) {
+            pMate = &sprite[rxBucket[i].index];
+            if (pMate->extra < 0 || pMate->xvel == sprite[pXSprite->reference].xvel || !IsDudeSprite(pMate))
+                continue;
+
+            pXMate = &xsprite[pMate->extra];
+            if (pXMate->target > -1) {
+                if (!IsPlayerSprite(&sprite[pXMate->target]))
+                    return &sprite[pXMate->target];
+            }
+
+        }
+    }
+
+    return NULL;
+}
+
+bool isMatesHaveSameTarget(XSPRITE* pXLeader, spritetype* pTarget, int allow) {
+    int rx = pXLeader->rxID; spritetype* pMate = NULL; XSPRITE* pXMate = NULL;
+
+    for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) {
+
+        if (rxBucket[i].type != 3)
+            continue;
+
+        pMate = &sprite[rxBucket[i].index];
+        if (pMate->extra < 0 || pMate->xvel == sprite[pXLeader->reference].xvel || !IsDudeSprite(pMate))
+            continue;
+
+        pXMate = &xsprite[pMate->extra];
+        if (pXMate->target == pTarget->xvel && allow-- <= 0)
+            return true;
+    }
+
+    return false;
+
+}
+
+bool isActive(int nSprite) {
+    spritetype* pDude = &sprite[nSprite]; XSPRITE* pXDude = &xsprite[pDude->extra];
+    int stateType = pXDude->aiState->stateType;
+    switch (stateType) {
+        case kAiStateIdle:
+        case kAiStateGenIdle:
+        case kAiStateSearch:
+        case kAiStateMove:
+        case kAiStateOther:
+            return false;
+    }
+    return true;
+}
+
+bool dudeCanSeeTarget(XSPRITE* pXDude, DUDEINFO* pDudeInfo, spritetype* pTarget) {
+    spritetype* pDude = &sprite[pXDude->reference];
+    int dx = pTarget->x - pDude->x; int dy = pTarget->y - pDude->y;
+    
+    // check target
+    if (approxDist(dx, dy) < pDudeInfo->seeDist) {
+        int eyeAboveZ = pDudeInfo->eyeHeight * pDude->yrepeat << 2;
+
+        // is there a line of sight to the target?
+        if (cansee(pDude->x, pDude->y, pDude->z, pDude->sectnum, pTarget->x, pTarget->y, pTarget->z - eyeAboveZ, pTarget->sectnum)) {
+            /*int nAngle = getangle(dx, dy);
+            int losAngle = ((1024 + nAngle - pDude->ang) & 2047) - 1024;
+
+            // is the target visible?
+            if (klabs(losAngle) < 2048) // 360 deg periphery here*/
+                return true;
+        }
+    }
+
+    return false;
+
+}
+
+// by NoOne: this function required if monsters in genIdle ai state. It wakes up monsters
+// when kGDXDudeTargetChanger goes to off state, so they won't ignore the world.
+void activateDudes(int rx) {
+    for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) {
+        if (rxBucket[i].type != 3) continue;
+        spritetype * pDude = &sprite[rxBucket[i].index]; XSPRITE * pXDude = &xsprite[pDude->extra];
+        if (!IsDudeSprite(pDude) || pXDude->aiState->stateType != kAiStateGenIdle) continue;
+            aiInitSprite(pDude);
+    }
+}
+
+bool affectedByTargetChg(XSPRITE* pXDude) {
+    if (pXDude->rxID <= 0 || pXDude->locked == 1) return false;
+    for (int nSprite = headspritestat[kStatGDXDudeTargetChanger]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        XSPRITE* pXSprite = (sprite[nSprite].extra >= 0) ? &xsprite[sprite[nSprite].extra] : NULL;
+        if (pXSprite == NULL || pXSprite->txID <= 0 || pXSprite->state != 1) continue;
+        for (int i = bucketHead[pXSprite->txID]; i < bucketHead[pXSprite->txID + 1]; i++) {
+            if (rxBucket[i].type != 3) continue;
+
+            spritetype* pSprite = &sprite[rxBucket[i].index];
+            if (pSprite->extra < 0 || !IsDudeSprite(pSprite)) continue;
+            else if (pSprite->xvel == sprite[pXDude->reference].xvel) return true;
+        }
+    }
+    return false;
+}
+
+int getDataFieldOfObject(int objType, int objIndex, int dataIndex) {
+    int data = -65535;
+    switch (objType) {
+    case 3:
+        switch (dataIndex) {
+        case 1:
+            return xsprite[sprite[objIndex].extra].data1;
+        case 2:
+            return xsprite[sprite[objIndex].extra].data2;
+        case 3:
+            return xsprite[sprite[objIndex].extra].data3;
+        case 4:
+            return xsprite[sprite[objIndex].extra].data4;
+        default:
+            return data;
+        }
+    case 0:
+        return xsector[sector[objIndex].extra].data;
+    case 6:
+        return xwall[wall[objIndex].extra].data;
+    default:
+        return data;
+    }
+}
+
+bool setDataValueOfObject(int objType, int objIndex, int dataIndex, int value) {
+    switch (objType) {
+    case 3:
+        switch (dataIndex) {
+        case 1:
+            xsprite[sprite[objIndex].extra].data1 = value;
+            return true;
+        case 2:
+            xsprite[sprite[objIndex].extra].data2 = value;
+            return true;
+        case 3:
+            xsprite[sprite[objIndex].extra].data3 = value;
+            return true;
+        case 4:
+            xsprite[sprite[objIndex].extra].data4 = value;
+            return true;
+        default:
+            return false;
+        }
+    case 0:
+        xsector[sector[objIndex].extra].data = value;
+        return true;
+    case 6:
+        xwall[wall[objIndex].extra].data = value;
+        return true;
+    default:
+        return false;
+    }
+}
+
+// by NoOne: this function checks if all TX objects have the same value
+bool goalValueIsReached(XSPRITE* pXSprite) {
+    for (int i = bucketHead[pXSprite->txID]; i < bucketHead[pXSprite->txID + 1]; i++) {
+        if (getDataFieldOfObject(rxBucket[i].type, rxBucket[i].index, pXSprite->data1) != pXSprite->data4)
+            return false;
+    }
+    return true;
+}
+
+// by NoOne: this function tells if there any dude found for kGDXDudeTargetChanger
+bool getDudesForTargetChg(XSPRITE* pXSprite) {
+    for (int i = bucketHead[pXSprite->txID]; i < bucketHead[pXSprite->txID + 1]; i++) {
+        if (rxBucket[i].type != 3) continue;
+        else if (IsDudeSprite(&sprite[rxBucket[i].index]) &&
+            xsprite[sprite[rxBucket[i].index].extra].health > 0) return true;
+    }
+
+    return false;
+}
+
+void disturbDudesInSight(spritetype* pSprite, int max) {
+    spritetype* pDude = NULL; XSPRITE* pXDude = NULL;
+    XSPRITE* pXSprite = &xsprite[pSprite->extra];
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->lotag - kDudeBase];
+    for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
+        pDude = &sprite[nSprite];
+        if (pDude->xvel == pSprite->xvel || !IsDudeSprite(pDude) || pDude->extra < 0)
+            continue;
+        pXDude = &xsprite[pDude->extra];
+        if (dudeCanSeeTarget(pXSprite, pDudeInfo, pDude)) {
+            if (pXDude->target != -1 || pXDude->rxID > 0)
+                continue;
+
+            aiSetTarget(pXDude, pDude->x, pDude->y, pDude->z);
+            aiActivateDude(pDude, pXDude);
+            if (max-- < 1)
+                break;
+        }
+    }
+}
+
+int getTargetDist(spritetype* pSprite, DUDEINFO* pDudeInfo, spritetype* pTarget) {
+    int x = pTarget->x; int y = pTarget->y;
+    int dx = x - pSprite->x; int dy = y - pSprite->y;
+
+    int dist = approxDist(dx, dy);
+    if (dist <= pDudeInfo->meleeDist) return 0;
+    if (dist >= pDudeInfo->seeDist) return 13;
+    if (dist <= pDudeInfo->seeDist / 12) return 1;
+    if (dist <= pDudeInfo->seeDist / 11) return 2;
+    if (dist <= pDudeInfo->seeDist / 10) return 3;
+    if (dist <= pDudeInfo->seeDist / 9) return 4;
+    if (dist <= pDudeInfo->seeDist / 8) return 5;
+    if (dist <= pDudeInfo->seeDist / 7) return 6;
+    if (dist <= pDudeInfo->seeDist / 6) return 7;
+    if (dist <= pDudeInfo->seeDist / 5) return 8;
+    if (dist <= pDudeInfo->seeDist / 4) return 9;
+    if (dist <= pDudeInfo->seeDist / 3) return 10;
+    if (dist <= pDudeInfo->seeDist / 2) return 11;
+    return 12;
+}
+
+int getFineTargetDist(spritetype* pSprite, spritetype* pTarget) {
+    int x = pTarget->x; int y = pTarget->y;
+    int dx = x - pSprite->x; int dy = y - pSprite->y;
+
+    int dist = approxDist(dx, dy);
+    return dist;
+}
+
+bool IsBurningDude(spritetype* pSprite) {
+    if (pSprite == NULL) return false;
+    switch (pSprite->type) {
+    case 239: // burning dude
+    case 240: // cultist burning
+    case 241: // axe zombie burning
+    case 242: // fat zombie burning
+    case 252: // tiny caleb burning
+    case 253: // beast burning
+    case kGDXGenDudeBurning:
+        return true;
+    }
+
+    return false;
+}
+
+bool IsKillableDude(spritetype* pSprite, bool locked) {
+    if (!IsDudeSprite(pSprite)) return false;
+    DUDEINFO* pDudeInfo = &dudeInfo[pSprite->lotag - kDudeBase];
+
+    // Optionally check if dude is locked
+    if (locked && xsprite[pSprite->extra].locked == 1)
+        return false;
+
+
+    // Make sure damage shift is greater than 0;
+    int a = 0;
+    for (int i = 0; i <= 6; i++) {
+        a += pDudeInfo->startDamage[i];
+    }
+
+    if (a == 0) return false;
+    return true;
+}
+
+bool isAnnoyingUnit(spritetype* pDude) {
+    switch (pDude->lotag) {
+    case 212: // hand
+    case 213: // brown spider
+    case 214: // red spider
+    case 215: // black spider
+    case 216: // mother spider
+    case 218: // eel
+    case 219: // bat
+    case 220: // rat
+    case 222: // green tentacle
+    case 224: // fire tentacle
+    case 226: // mother tentacle
+    case 223: // green pod
+    case 225: // fire pod
+        return true;
+    default:
+        return false;
+    }
+}
+
+bool unitCanFly(spritetype* pDude) {
+    switch (pDude->lotag) {
+    case 219: // bat
+    case 206: // gargoyle
+    case 207: // stone gargoyle
+    case 210: // phantasm
+        return true;
+    default:
+        return false;
+    }
+}
+
+bool isMeleeUnit(spritetype* pDude) {
+    switch (pDude->lotag) {
+    case 203: // axe zombie
+    case 205: // earth zombie
+    case 206: // gargoyle
+    case 212: // hand
+    case 213: // brown spider
+    case 214: // red spider
+    case 215: // black spider
+    case 216: // mother spider
+    case 217: // gill beast
+    case 218: // eel
+    case 219: // bat
+    case 220: // rat
+    case 222: // green tentacle
+    case 224: // fire tentacle
+    case 226: // mother tentacle
+    case 244: // sleep zombie
+    case 245: // innocent
+    case 250: // tiny caleb
+    case 251: // beast
+        return true;
+    case kGDXDudeUniversalCultist:
+        return (pDude->extra >= 0 && dudeIsMelee(&xsprite[pDude->extra]));
+    default:
+        return false;
+    }
+}
+//---------------------------------------
+
+void ProcessMotion(void)
+{
+    sectortype *pSector;
+    int nSector;
+    for (pSector = sector, nSector = 0; nSector < numsectors; nSector++, pSector++)
+    {
+        int nXSector = pSector->extra;
+        if (nXSector <= 0)
+            continue;
+        XSECTOR *pXSector = &xsector[nXSector];
+        if (pXSector->bobSpeed != 0)
+        {
+            if (pXSector->bobAlways)
+                pXSector->bobTheta += pXSector->bobSpeed;
+            else if (pXSector->busy == 0)
+                continue;
+            else
+                pXSector->bobTheta += mulscale16(pXSector->bobSpeed, pXSector->busy);
+            int vdi = mulscale30(Sin(pXSector->bobTheta), pXSector->bobZRange<<8);
+            for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+            {
+                spritetype *pSprite = &sprite[nSprite];
+                if (pSprite->cstat&24576)
+                {
+                    viewBackupSpriteLoc(nSprite, pSprite);
+                    pSprite->z += vdi;
+                }
+            }
+            if (pXSector->bobFloor)
+            {
+                int floorZ = pSector->floorz;
+                viewInterpolateSector(nSector, pSector);
+                pSector->floorz = baseFloor[nSector]+vdi;
+                for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+                {
+                    spritetype *pSprite = &sprite[nSprite];
+                    if (pSprite->hitag&2)
+                        pSprite->hitag |= 4;
+                    else
+                    {
+                        int top, bottom;
+                        GetSpriteExtents(pSprite, &top, &bottom);
+                        if (bottom >= floorZ && (pSprite->cstat&48) == 0)
+                        {
+                            viewBackupSpriteLoc(nSprite, pSprite);
+                            pSprite->z += vdi;
+                        }
+                    }
+                }
+            }
+            if (pXSector->bobCeiling)
+            {
+                int ceilZ = pSector->ceilingz;
+                viewInterpolateSector(nSector, pSector);
+                pSector->ceilingz = baseCeil[nSector]+vdi;
+                for (int nSprite = headspritesect[nSector]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+                {
+                    spritetype *pSprite = &sprite[nSprite];
+                    int top, bottom;
+                    GetSpriteExtents(pSprite, &top, &bottom);
+                    if (top <= ceilZ && (pSprite->cstat&48) == 0)
+                    {
+                        viewBackupSpriteLoc(nSprite, pSprite);
+                        pSprite->z += vdi;
+                    }
+                }
+            }
+        }
+    }
+}
+
+void AlignSlopes(void)
+{
+    sectortype *pSector;
+    int nSector;
+    for (pSector = sector, nSector = 0; nSector < numsectors; nSector++, pSector++)
+    {
+        if (qsector_filler[nSector])
+        {
+            walltype *pWall = &wall[pSector->wallptr+qsector_filler[nSector]];
+            walltype *pWall2 = &wall[pWall->point2];
+            int nNextSector = pWall->nextsector;
+            if (nNextSector >= 0)
+            {
+                int x = (pWall->x+pWall2->x)/2;
+                int y = (pWall->y+pWall2->y)/2;
+                viewInterpolateSector(nSector, pSector);
+                alignflorslope(nSector, x, y, getflorzofslope(nNextSector, x, y));
+                alignceilslope(nSector, x, y, getceilzofslope(nNextSector, x, y));
+            }
+        }
+    }
+}
+
+int(*gBusyProc[])(unsigned int, unsigned int) =
+{
+    VCrushBusy,
+    VSpriteBusy,
+    VDoorBusy,
+    HDoorBusy,
+    RDoorBusy,
+    StepRotateBusy,
+    GenSectorBusy,
+    PathBusy
+};
+
+void trProcessBusy(void)
+{
+    memset(velFloor, 0, sizeof(velFloor));
+    memset(velCeil, 0, sizeof(velCeil));
+    for (int i = gBusyCount-1; i >= 0; i--)
+    {
+        int oldBusy = gBusy[i].at8;
+        gBusy[i].at8 = ClipRange(oldBusy+gBusy[i].at4*4, 0, 65536);
+        int nStatus = gBusyProc[gBusy[i].atc](gBusy[i].at0, gBusy[i].at8);
+        switch (nStatus)
+        {
+        case 1:
+            gBusy[i].at8 = oldBusy;
+            break;
+        case 2:
+            gBusy[i].at8 = oldBusy;
+            gBusy[i].at4 = -gBusy[i].at4;
+            break;
+        case 3:
+            gBusy[i] = gBusy[--gBusyCount];
+            break;
+        }
+    }
+    ProcessMotion();
+    AlignSlopes();
+}
+
+void InitGenerator(int);
+
+void trInit(void)
+{
+    gBusyCount = 0;
+    for (int i = 0; i < numwalls; i++)
+    {
+        baseWall[i].x = wall[i].x;
+        baseWall[i].y = wall[i].y;
+    }
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        if (sprite[i].statnum < kStatFree)
+        {
+            sprite[i].zvel = sprite[i].type;
+            baseSprite[i].x = sprite[i].x;
+            baseSprite[i].y = sprite[i].y;
+            baseSprite[i].z = sprite[i].z;
+        }
+        else
+            sprite[i].zvel = -1;
+    }
+    for (int i = 0; i < numwalls; i++)
+    {
+        int nXWall = wall[i].extra;
+        dassert(nXWall < kMaxXWalls);
+        if (nXWall > 0)
+        {
+            XWALL *pXWall = &xwall[nXWall];
+            if (pXWall->state)
+                pXWall->busy = 65536;
+        }
+    }
+    dassert((numsectors >= 0) && (numsectors < kMaxSectors));
+    for (int i = 0; i < numsectors; i++)
+    {
+        sectortype *pSector = &sector[i];
+        baseFloor[i] = pSector->floorz;
+        baseCeil[i] = pSector->ceilingz;
+        int nXSector = pSector->extra;
+        if (nXSector > 0)
+        {
+            dassert(nXSector < kMaxXSectors);
+            XSECTOR *pXSector = &xsector[nXSector];
+            if (pXSector->state)
+                pXSector->busy = 65536;
+            switch (pSector->lotag)
+            {
+            case kSecCounter:
+                //By NoOne: no need to trigger once it, instead lock so it can be unlocked and used again.
+                //pXSector->triggerOnce = 1;
+                evPost(i, 6, 0, CALLBACK_ID_12);
+                break;
+            case 600:
+            case 602:
+                ZTranslateSector(i, pXSector, pXSector->busy, 1);
+                break;
+            case 614:
+            case 616:
+            {
+                spritetype *pSprite1 = &sprite[pXSector->at2c_0];
+                spritetype *pSprite2 = &sprite[pXSector->at2e_0];
+                TranslateSector(i, 0, -65536, pSprite1->x, pSprite1->y, pSprite1->x, pSprite1->y, pSprite1->ang, pSprite2->x, pSprite2->y, pSprite2->ang, pSector->lotag == 616);
+                for (int j = 0; j < pSector->wallnum; j++)
+                {
+                    baseWall[pSector->wallptr+j].x = wall[pSector->wallptr+j].x;
+                    baseWall[pSector->wallptr+j].y = wall[pSector->wallptr+j].y;
+                }
+                for (int nSprite = headspritesect[i]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+                {
+                    baseSprite[nSprite].x = sprite[nSprite].x;
+                    baseSprite[nSprite].y = sprite[nSprite].y;
+                    baseSprite[nSprite].z = sprite[nSprite].z;
+                }
+                TranslateSector(i, 0, pXSector->busy, pSprite1->x, pSprite1->y, pSprite1->x, pSprite1->y, pSprite1->ang, pSprite2->x, pSprite2->y, pSprite2->ang, pSector->lotag == 616);
+                ZTranslateSector(i, pXSector, pXSector->busy, 1);
+                break;
+            }
+            case 615:
+            case 617:
+            {
+                spritetype *pSprite1 = &sprite[pXSector->at2c_0];
+                TranslateSector(i, 0, -65536, pSprite1->x, pSprite1->y, pSprite1->x, pSprite1->y, 0, pSprite1->x, pSprite1->y, pSprite1->ang, pSector->lotag == 617);
+                for (int j = 0; j < pSector->wallnum; j++)
+                {
+                    baseWall[pSector->wallptr+j].x = wall[pSector->wallptr+j].x;
+                    baseWall[pSector->wallptr+j].y = wall[pSector->wallptr+j].y;
+                }
+                for (int nSprite = headspritesect[i]; nSprite >= 0; nSprite = nextspritesect[nSprite])
+                {
+                    baseSprite[nSprite].x = sprite[nSprite].x;
+                    baseSprite[nSprite].y = sprite[nSprite].y;
+                    baseSprite[nSprite].z = sprite[nSprite].z;
+                }
+                TranslateSector(i, 0, pXSector->busy, pSprite1->x, pSprite1->y, pSprite1->x, pSprite1->y, 0, pSprite1->x, pSprite1->y, pSprite1->ang, pSector->lotag == 617);
+                ZTranslateSector(i, pXSector, pXSector->busy, 1);
+                break;
+            }
+            case 612:
+                InitPath(i, pXSector);
+                break;
+            default:
+                break;
+            }
+        }
+    }
+    for (int i = 0; i < kMaxSprites; i++)
+    {
+        int nXSprite = sprite[i].extra;
+        if (sprite[i].statnum < kStatFree && nXSprite > 0)
+        {
+            dassert(nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (pXSprite->state)
+                pXSprite->busy = 65536;
+            switch (sprite[i].type)
+            {
+            case 23:
+                pXSprite->triggerOnce = 1;
+                break;
+            case kGDXSequentialTX:
+                break;
+            case kGDXSeqSpawner:
+            case kGDXDudeTargetChanger:
+            case kGDXEffectSpawner:
+            case kGDXWindGenerator:
+            case 700:
+            case 701:
+            case 702:
+            case 703:
+            case 704:
+            case 705:
+            case 706:
+            case 707:
+            case 708:
+                InitGenerator(i);
+                break;
+            case 401:
+            case kGDXThingTNTProx:
+                pXSprite->Proximity = 1;
+                break;
+            case 414:
+                if (pXSprite->state)
+                    sprite[i].hitag |= 7;
+                else
+                    sprite[i].hitag &= ~7;
+                break;
+            }
+            if (pXSprite->Vector)
+                sprite[i].cstat |= 256;
+            if (pXSprite->Push)
+                sprite[i].cstat |= 4096;
+        }
+    }
+    evSend(0, 0, 7, COMMAND_ID_1);
+    if (gGameOptions.nGameType == 1)
+        evSend(0, 0, 9, COMMAND_ID_1);
+    else if (gGameOptions.nGameType == 2)
+        evSend(0, 0, 8, COMMAND_ID_1);
+    else if (gGameOptions.nGameType == 3)
+    {
+        evSend(0, 0, 8, COMMAND_ID_1);
+        evSend(0, 0, 10, COMMAND_ID_1);
+    }
+}
+
+void trTextOver(int nId)
+{
+    char *pzMessage = levelGetMessage(nId);
+    if (pzMessage)
+        viewSetMessage(pzMessage);
+}
+
+void InitGenerator(int nSprite)
+{
+    dassert(nSprite < kMaxSprites);
+    spritetype *pSprite = &sprite[nSprite];
+    dassert(pSprite->statnum != kMaxStatus);
+    int nXSprite = pSprite->extra;
+    dassert(nXSprite > 0);
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    switch (sprite[nSprite].type)
+    {
+    // By NoOne: intialize GDX generators
+    case kGDXDudeTargetChanger:
+        pSprite->cstat &= ~kSprBlock;
+        pSprite->cstat |= kSprInvisible;
+        if (pXSprite->busyTime <= 0) pXSprite->busyTime = 5;
+        if (pXSprite->state != pXSprite->restState)
+            evPost(nSprite, 3, 0, COMMAND_ID_21);
+        return;
+    case kGDXObjDataAccumulator:
+    case kGDXSeqSpawner:
+    case kGDXEffectSpawner:
+        pSprite->cstat &= ~kSprBlock;
+        pSprite->cstat |= kSprInvisible;
+        if (pXSprite->state != pXSprite->restState)
+            evPost(nSprite, 3, 0, COMMAND_ID_21);
+        return;
+    case 700:
+        pSprite->cstat &= ~kSprBlock;
+        pSprite->cstat |= kSprInvisible;
+        break;
+    }
+    if (pXSprite->state != pXSprite->restState && pXSprite->busyTime > 0)
+        evPost(nSprite, 3, (120*(pXSprite->busyTime+Random2(pXSprite->data1)))/10, COMMAND_ID_21);
+}
+
+void ActivateGenerator(int nSprite)
+{
+    dassert(nSprite < kMaxSprites);
+    spritetype *pSprite = &sprite[nSprite];
+    dassert(pSprite->statnum != kMaxStatus);
+    int nXSprite = pSprite->extra;
+    dassert(nXSprite > 0);
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    switch (pSprite->type) {
+    case 701:
+    {
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        actSpawnThing(pSprite->sectnum, pSprite->x, pSprite->y, bottom, 423);
+        break;
+    }
+    case 702:
+    {
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        actSpawnThing(pSprite->sectnum, pSprite->x, pSprite->y, bottom, 424);
+        break;
+    }
+    case 708:
+    {
+        // By NoOne: allow custom pitch for sounds in SFX gen.
+        int pitch = pXSprite->data4 << 1; if (pitch < 2000) pitch = 0;
+        sfxPlay3DSoundCP(pSprite, pXSprite->data2, -1, 0, pitch);
+        break;
+    }
+    case 703:
+        switch (pXSprite->data2)
+        {
+        case 0:
+            FireballTrapSeqCallback(3, nXSprite);
+            break;
+        case 1:
+            seqSpawn(35, 3, nXSprite, nFireballTrapClient);
+            break;
+        case 2:
+            seqSpawn(36, 3, nXSprite, nFireballTrapClient);
+            break;
+        }
+        break;
+    // By NoOne: EctoSkull gen can now fire any missile
+    case 704:
+        switch (pXSprite->data2)
+        {
+        case 0:
+            UniMissileTrapSeqCallback(3, nXSprite);
+            break;
+        case 1:
+            seqSpawn(35, 3, nXSprite, nUniMissileTrapClient);
+            break;
+        case 2:
+            seqSpawn(36, 3, nXSprite, nUniMissileTrapClient);
+            break;
+        }
+        break;
+    case 706:
+    {
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        gFX.fxSpawn(FX_23, pSprite->sectnum, pSprite->x, pSprite->y, top, 0);
+        break;
+    }
+    case 707:
+    {
+        int top, bottom;
+        GetSpriteExtents(pSprite, &top, &bottom);
+        gFX.fxSpawn(FX_26, pSprite->sectnum, pSprite->x, pSprite->y, top, 0);
+        break;
+    }
+    }
+}
+
+void FireballTrapSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+    if (pSprite->cstat&32)
+        actFireMissile(pSprite, 0, 0, 0, 0, (pSprite->cstat&8) ? 0x4000 : -0x4000, 305);
+    else
+        actFireMissile(pSprite, 0, 0, Cos(pSprite->ang)>>16, Sin(pSprite->ang)>>16, 0, 305);
+}
+
+// By NoOne: Callback for trap that can fire any missile specified in data3
+void UniMissileTrapSeqCallback(int, int nXSprite)
+{
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    int nSprite = pXSprite->reference;
+    spritetype *pSprite = &sprite[nSprite];
+
+    int nMissile = 307;
+    if (pXSprite->data3 >= kMissileBase && pXSprite->data3 < kMissileMax)
+        nMissile = pXSprite->data3;
+    else
+        return;
+
+    if (pSprite->cstat&32)
+        actFireMissile(pSprite, 0, 0, 0, 0, (pSprite->cstat&8) ? 0x4000 : -0x4000, nMissile);
+    else
+        actFireMissile(pSprite, 0, 0, Cos(pSprite->ang)>>16, Sin(pSprite->ang)>>16, 0, nMissile);
+}
+
+void MGunFireSeqCallback(int, int nXSprite)
+{
+    int nSprite = xsprite[nXSprite].reference;
+    spritetype *pSprite = &sprite[nSprite];
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    if (pXSprite->data2 > 0 || pXSprite->data1 == 0)
+    {
+        if (pXSprite->data2 > 0)
+        {
+            pXSprite->data2--;
+            if (pXSprite->data2 == 0)
+                evPost(nSprite, 3, 1, COMMAND_ID_0);
+        }
+        int dx = (Cos(pSprite->ang)>>16)+Random2(1000);
+        int dy = (Sin(pSprite->ang)>>16)+Random2(1000);
+        int dz = Random2(1000);
+        actFireVector(pSprite, 0, 0, dx, dy, dz, VECTOR_TYPE_2);
+        sfxPlay3DSound(pSprite, 359, -1, 0);
+    }
+}
+
+void MGunOpenSeqCallback(int, int nXSprite)
+{
+    seqSpawn(39, 3, nXSprite, nMGunFireClient);
+}
+
+class TriggersLoadSave : public LoadSave
+{
+public:
+    virtual void Load();
+    virtual void Save();
+};
+
+void TriggersLoadSave::Load()
+{
+    Read(&gBusyCount, sizeof(gBusyCount));
+    Read(gBusy, sizeof(gBusy));
+    Read(basePath, sizeof(basePath));
+}
+
+void TriggersLoadSave::Save()
+{
+    Write(&gBusyCount, sizeof(gBusyCount));
+    Write(gBusy, sizeof(gBusy));
+    Write(basePath, sizeof(basePath));
+}
+
+static TriggersLoadSave *myLoadSave;
+
+void TriggersLoadSaveConstruct(void)
+{
+    myLoadSave = new TriggersLoadSave();
+}
diff --git a/source/blood/src/triggers.h b/source/blood/src/triggers.h
new file mode 100644
index 000000000..498f42659
--- /dev/null
+++ b/source/blood/src/triggers.h
@@ -0,0 +1,73 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "build.h"
+#include "common.h"
+#include "common_game.h"
+
+#include "blood.h"
+#include "db.h"
+#include "eventq.h"
+#include "dude.h"
+
+void trTriggerSector(unsigned int nSector, XSECTOR *pXSector, int a3);
+void trMessageSector(unsigned int nSector, EVENT a2);
+void trTriggerWall(unsigned int nWall, XWALL *pXWall, int a3);
+void trMessageWall(unsigned int nWall, EVENT a2);
+void trTriggerSprite(unsigned int nSprite, XSPRITE *pXSprite, int a3);
+void trMessageSprite(unsigned int nSprite, EVENT a2);
+void trProcessBusy(void);
+void trInit(void);
+void trTextOver(int nId);
+
+// By NoOne: functions required for new features
+// -------------------------------------------------------
+void pastePropertiesInObj(int type, int nDest, EVENT event);
+void trDamageSprite(int type, int nDest, EVENT event);
+spritetype* getTargetInRange(spritetype* pSprite, int minDist, int maxDist, short data, short teamMode);
+bool isMateOf(XSPRITE* pXDude, XSPRITE* pXSprite);
+spritetype* targetIsPlayer(XSPRITE* pXSprite);
+bool isTargetAimsDude(XSPRITE* pXTarget, spritetype* pDude);
+spritetype* getMateTargets(XSPRITE* pXSprite);
+bool isMatesHaveSameTarget(XSPRITE* pXLeader, spritetype* pTarget, int allow);
+bool isActive(int nSprite);
+bool dudeCanSeeTarget(XSPRITE* pXDude, DUDEINFO* pDudeInfo, spritetype* pTarget);
+void disturbDudesInSight(spritetype* pSprite, int max);
+int getTargetDist(spritetype* pSprite, DUDEINFO* pDudeInfo, spritetype* pTarget);
+int getFineTargetDist(spritetype* pSprite, spritetype* pTarget);
+bool IsBurningDude(spritetype* pSprite);
+bool IsKillableDude(spritetype* pSprite, bool locked);
+bool isAnnoyingUnit(spritetype* pDude);
+bool unitCanFly(spritetype* pDude);
+bool isMeleeUnit(spritetype* pDude);
+void activateDudes(int rx);
+bool affectedByTargetChg(XSPRITE* pXDude);
+int getDataFieldOfObject(int objType, int objIndex, int dataIndex);
+bool setDataValueOfObject(int objType, int objIndex, int dataIndex, int value);
+bool goalValueIsReached(XSPRITE* pXSprite);
+bool getDudesForTargetChg(XSPRITE* pXSprite);
+void stopWindOnSectors(XSPRITE* pXSource);
+void useSectorWindGen(XSPRITE* pXSource, sectortype* pSector);
+void useEffectGen(XSPRITE* pXSource, spritetype* pSprite);
+void useSeqSpawnerGen(XSPRITE* pXSource, spritetype* pSprite);
+// -------------------------------------------------------
\ No newline at end of file
diff --git a/source/blood/src/view.cpp b/source/blood/src/view.cpp
new file mode 100644
index 000000000..71812b023
--- /dev/null
+++ b/source/blood/src/view.cpp
@@ -0,0 +1,3818 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdlib.h>
+#include <string.h>
+
+#include "compat.h"
+#include "a.h"
+#include "build.h"
+#include "colmatch.h"
+#include "pragmas.h"
+#include "mmulti.h"
+#include "osd.h"
+#include "common_game.h"
+
+#include "aihand.h"
+#include "blood.h"
+#include "choke.h"
+#include "config.h"
+#include "db.h"
+#include "endgame.h"
+#include "gamemenu.h"
+#include "gameutil.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "map2d.h"
+#include "messages.h"
+#include "menu.h"
+#include "mirrors.h"
+#include "network.h"
+#include "player.h"
+#include "replace.h"
+#include "screen.h"
+#include "sectorfx.h"
+#include "tile.h"
+#include "trig.h"
+#include "view.h"
+#include "warp.h"
+#include "weapon.h"
+
+struct VIEW {
+	int at0;
+	int at4;
+	int at8; // bob height
+	int atc; // bob width
+	int at10;
+	int at14;
+	int at18; // bob sway y
+	int at1c; // bob sway x
+	fix16_t at20;
+	fix16_t at24; // horiz
+	int at28; // horizoff
+	int at2c;
+	fix16_t at30; // angle
+	int at34; // weapon z
+	int at38; // view z
+	int at3c;
+	int at40;
+	int at44;
+	int at48; // posture
+	int at4c; // spin
+	int at50; // x
+	int at54; // y
+	int at58; // z
+	int at5c; //xvel
+	int at60; //yvel
+	int at64; //zvel
+	short at68; // sectnum
+	unsigned int at6a; // floordist
+	char at6e; // look center
+	char at6f;
+	char at70; // run
+	char at71; // jump
+	char at72; // underwater
+	short at73; // sprite flags
+	SPRITEHIT at75;
+};
+
+VIEW gPrevView[kMaxPlayers];
+VIEWPOS gViewPos;
+int gViewIndex;
+
+struct INTERPOLATE {
+    void *pointer;
+    int value;
+    int value2;
+    INTERPOLATE_TYPE type;
+};
+
+int pcBackground;
+int gViewMode = 3;
+int gViewSize = 2;
+
+VIEW predict, predictOld;
+
+VIEW predictFifo[256];
+
+int gInterpolate;
+int nInterpolations;
+char gInterpolateSprite[512];
+char gInterpolateWall[1024];
+char gInterpolateSector[128];
+
+INTERPOLATE gInterpolation[4096];
+
+int gViewXCenter, gViewYCenter;
+int gViewX0, gViewY0, gViewX1, gViewY1;
+int gViewX0S, gViewY0S, gViewX1S, gViewY1S;
+int xscale, xscalecorrect, yscale, xstep, ystep;
+
+int gScreenTilt;
+
+CGameMessageMgr gGameMessageMgr;
+
+bool bLoadScreenCrcMatch = false;
+
+void RotateYZ(int *pX, int *pY, int *pZ, int ang)
+{
+    UNREFERENCED_PARAMETER(pX);
+	int oY, oZ, angSin, angCos;
+	oY = *pY;
+	oZ = *pZ;
+	angSin = Sin(ang);
+	angCos = Cos(ang);
+	*pY = dmulscale30r(oY,angCos,oZ,-angSin);
+	*pZ = dmulscale30r(oY,angSin,oZ,angCos);
+}
+
+void RotateXZ(int *pX, int *pY, int *pZ, int ang)
+{
+    UNREFERENCED_PARAMETER(pY);
+	int oX, oZ, angSin, angCos;
+	oX = *pX;
+	oZ = *pZ;
+    angSin = Sin(ang);
+    angCos = Cos(ang);
+	*pX = dmulscale30r(oX,angCos,oZ,-angSin);
+	*pZ = dmulscale30r(oX,angSin,oZ,angCos);
+}
+
+void RotateXY(int *pX, int *pY, int *pZ, int ang)
+{
+    UNREFERENCED_PARAMETER(pZ);
+	int oX, oY, angSin, angCos;
+	oX = *pX;
+	oY = *pY;
+    angSin = Sin(ang);
+    angCos = Cos(ang);
+	*pX = dmulscale30r(oX,angCos,oY,-angSin);
+	*pY = dmulscale30r(oX,angSin,oY,angCos);
+}
+
+FONT gFont[5];
+
+void FontSet(int id, int tile, int space)
+{
+	if (id < 0 || id >= 5 || tile < 0 || tile >= kMaxTiles)
+		return;
+
+	FONT *pFont = &gFont[id];
+	int xSize = 0;
+	int ySize = 0;
+	pFont->tile = tile;
+	for (int i = 0; i < 96; i++)
+	{
+		if (tilesiz[tile+i].x > xSize)
+			xSize = tilesiz[tile+i].x;
+		if (tilesiz[tile+i].y > ySize)
+			ySize = tilesiz[tile+i].y;
+	}
+	pFont->xSize = xSize;
+	pFont->ySize = ySize;
+	pFont->space = space;
+}
+
+void viewGetFontInfo(int id, const char *unk1, int *pXSize, int *pYSize)
+{
+	if (id < 0 || id >= 5)
+		return;
+	FONT *pFont = &gFont[id];
+	if (!unk1)
+	{
+		if (pXSize)
+			*pXSize = pFont->xSize;
+		if (pYSize)
+			*pYSize = pFont->ySize;
+	}
+	else
+	{
+		int width = -pFont->space;
+		for (const char *pBuf = unk1; *pBuf != 0; pBuf++)
+		{
+			int tile = ((*pBuf-32)&127)+pFont->tile;
+			if (tilesiz[tile].x != 0 && tilesiz[tile].y != 0)
+				width += tilesiz[tile].x+pFont->space;
+		}
+		if (pXSize)
+			*pXSize = width;
+		if (pYSize)
+			*pYSize = pFont->ySize;
+	}
+}
+
+void viewUpdatePages(void)
+{
+	pcBackground = numpages;
+}
+
+void viewToggle(int viewMode)
+{
+	if (viewMode == 3)
+		gViewMode = 4;
+	else
+	{
+		gViewMode = 3;
+		viewResizeView(gViewSize);
+	}
+}
+
+void viewInitializePrediction(void)
+{
+	predict.at30 = gMe->q16ang;
+	predict.at20 = gMe->q16look;
+	predict.at24 = gMe->q16horiz;
+	predict.at28 = gMe->q16slopehoriz;
+	predict.at2c = gMe->at83;
+	predict.at6f = gMe->at31c;
+	predict.at70 = gMe->at2e;
+	predict.at72 = gMe->at87;
+	predict.at71 = gMe->atc.buttonFlags.jump;
+	predict.at50 = gMe->pSprite->x;
+	predict.at54 = gMe->pSprite->y;
+	predict.at58 = gMe->pSprite->z;
+	predict.at68 = gMe->pSprite->sectnum;
+	predict.at73 = gMe->pSprite->hitag;
+	predict.at5c = xvel[gMe->pSprite->index];
+	predict.at60 = yvel[gMe->pSprite->index];
+	predict.at64 = zvel[gMe->pSprite->index];
+	predict.at6a = gMe->pXSprite->height;
+	predict.at48 = gMe->at2f;
+	predict.at4c = gMe->at316;
+	predict.at6e = gMe->atc.keyFlags.lookCenter;
+	memcpy(&predict.at75,&gSpriteHit[gMe->pSprite->extra],sizeof(SPRITEHIT));
+	predict.at0 = gMe->at37;
+	predict.at4 = gMe->at3b;
+	predict.at8 = gMe->at3f;
+	predict.atc = gMe->at43;
+	predict.at10 = gMe->at47;
+	predict.at14 = gMe->at4b;
+	predict.at18 = gMe->at4f;
+	predict.at1c = gMe->at53;
+	predict.at34 = gMe->at6f-gMe->at67-(12<<8);
+	predict.at38 = gMe->at67;
+	predict.at3c = gMe->at6b;
+	predict.at40 = gMe->at6f;
+	predict.at44 = gMe->at73;
+    predictOld = predict;
+}
+
+void viewUpdatePrediction(GINPUT *pInput)
+{
+    predictOld = predict;
+	short bakCstat = gMe->pSprite->cstat;
+    gMe->pSprite->cstat = 0;
+    fakePlayerProcess(gMe, pInput);
+    fakeActProcessSprites();
+    gMe->pSprite->cstat = bakCstat;
+    predictFifo[gPredictTail&255] = predict;
+    gPredictTail++;
+}
+
+void sub_158B4(PLAYER *pPlayer)
+{
+    predict.at38 = predict.at58 - gPosture[pPlayer->at5f][predict.at48].at24;
+    predict.at40 = predict.at58 - gPosture[pPlayer->at5f][predict.at48].at28;
+}
+
+void fakeProcessInput(PLAYER *pPlayer, GINPUT *pInput)
+{
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][predict.at48];
+    predict.at70 = pInput->syncFlags.run;
+    predict.at70 = 0;
+    predict.at71 = pInput->buttonFlags.jump;
+    if (predict.at48 == 1)
+    {
+        int x = Cos(fix16_to_int(predict.at30));
+        int y = Sin(fix16_to_int(predict.at30));
+        if (pInput->forward)
+        {
+            int forward = pInput->forward;
+            if (forward > 0)
+                forward = mulscale8(pPosture->at0, forward);
+            else
+                forward = mulscale8(pPosture->at8, forward);
+            predict.at5c += mulscale30(forward, x);
+            predict.at60 += mulscale30(forward, y);
+        }
+        if (pInput->strafe)
+        {
+            int strafe = pInput->strafe;
+            strafe = mulscale8(pPosture->at4, strafe);
+            predict.at5c += mulscale30(strafe, y);
+            predict.at60 -= mulscale30(strafe, x);
+        }
+    }
+    else if (predict.at6a < 0x100)
+    {
+        int speed = 0x10000;
+        if (predict.at6a > 0)
+            speed -= divscale16(predict.at6a, 0x100);
+        int x = Cos(fix16_to_int(predict.at30));
+        int y = Sin(fix16_to_int(predict.at30));
+        if (pInput->forward)
+        {
+            int forward = pInput->forward;
+            if (forward > 0)
+                forward = mulscale8(pPosture->at0, forward);
+            else
+                forward = mulscale8(pPosture->at8, forward);
+            if (predict.at6a)
+                forward = mulscale16(forward, speed);
+            predict.at5c += mulscale30(forward, x);
+            predict.at60 += mulscale30(forward, y);
+        }
+        if (pInput->strafe)
+        {
+            int strafe = pInput->strafe;
+            strafe = mulscale8(pPosture->at4, strafe);
+            if (predict.at6a)
+                strafe = mulscale16(strafe, speed);
+            predict.at5c += mulscale30(strafe, y);
+            predict.at60 -= mulscale30(strafe, x);
+        }
+    }
+    if (pInput->q16turn)
+        predict.at30 = (predict.at30+pInput->q16turn)&0x7ffffff;
+    if (pInput->keyFlags.spin180)
+        if (!predict.at4c)
+            predict.at4c = -1024;
+    if (predict.at4c < 0)
+    {
+        int speed;
+        if (predict.at48 == 1)
+            speed = 64;
+        else
+            speed = 128;
+
+        predict.at4c = ClipLow(predict.at4c+speed, 0);
+        predict.at30 += fix16_from_int(speed);
+    }
+
+    if (!predict.at71)
+        predict.at6f = 0;
+
+    switch (predict.at48)
+    {
+    case 1:
+        if (predict.at71)
+            predict.at64 -= 0x5b05;
+        if (pInput->buttonFlags.crouch)
+            predict.at64 += 0x5b05;
+        break;
+    case 2:
+        if (!pInput->buttonFlags.crouch)
+            predict.at48 = 0;
+        break;
+    default:
+        if (!predict.at6f && predict.at71 && predict.at6a == 0)
+        {
+            if (packItemActive(pPlayer, 4))
+                predict.at64 = -0x175555;
+            else
+                predict.at64 = -0xbaaaa;
+            predict.at6f = 1;
+        }
+        if (pInput->buttonFlags.crouch)
+            predict.at48 = 2;
+        break;
+    }
+#if 0
+    if (predict.at6e && !pInput->buttonFlags.lookUp && !pInput->buttonFlags.lookDown)
+    {
+        if (predict.at20 < 0)
+            predict.at20 = fix16_min(predict.at20+F16(4), F16(0));
+        if (predict.at20 > 0)
+            predict.at20 = fix16_max(predict.at20-F16(4), F16(0));
+        if (predict.at20 == 0)
+            predict.at6e = 0;
+    }
+    else
+    {
+        if (pInput->buttonFlags.lookUp)
+            predict.at20 = fix16_min(predict.at20+F16(4), F16(60));
+        if (pInput->buttonFlags.lookDown)
+            predict.at20 = fix16_max(predict.at20-F16(4), F16(-60));
+    }
+    predict.at20 = fix16_clamp(predict.at20+pInput->q16mlook, F16(-60), F16(60));
+
+    if (predict.at20 > 0)
+        predict.at24 = mulscale30(F16(120), Sin(fix16_to_int(predict.at20<<3)));
+    else if (predict.at20 < 0)
+        predict.at24 = mulscale30(F16(180), Sin(fix16_to_int(predict.at20<<3)));
+    else
+        predict.at24 = 0;
+#endif
+    CONSTEXPR int upAngle = 289;
+    CONSTEXPR int downAngle = -347;
+    CONSTEXPR double lookStepUp = 4.0*upAngle/60.0;
+    CONSTEXPR double lookStepDown = -4.0*downAngle/60.0;
+    if (predict.at6e && !pInput->buttonFlags.lookUp && !pInput->buttonFlags.lookDown)
+    {
+        if (predict.at20 < 0)
+            predict.at20 = fix16_min(predict.at20+F16(lookStepDown), F16(0));
+        if (predict.at20 > 0)
+            predict.at20 = fix16_max(predict.at20-F16(lookStepUp), F16(0));
+        if (predict.at20 == 0)
+            predict.at6e = 0;
+    }
+    else
+    {
+        if (pInput->buttonFlags.lookUp)
+            predict.at20 = fix16_min(predict.at20+F16(lookStepUp), F16(upAngle));
+        if (pInput->buttonFlags.lookDown)
+            predict.at20 = fix16_max(predict.at20-F16(lookStepDown), F16(downAngle));
+    }
+    predict.at20 = fix16_clamp(predict.at20+(pInput->q16mlook<<3), F16(downAngle), F16(upAngle));
+    predict.at24 = fix16_from_float(100.f*tanf(fix16_to_float(predict.at20)*fPI/1024.f));
+
+    int nSector = predict.at68;
+    int florhit = predict.at75.florhit & 0xe000;
+    char va;
+    if (predict.at6a < 16 && (florhit == 0x4000 || florhit == 0))
+        va = 1;
+    else
+        va = 0;
+    if (va && (sector[nSector].floorstat&2) != 0)
+    {
+        int z1 = getflorzofslope(nSector, predict.at50, predict.at54);
+        int x2 = predict.at50+mulscale30(64, Cos(fix16_to_int(predict.at30)));
+        int y2 = predict.at54+mulscale30(64, Sin(fix16_to_int(predict.at30)));
+        short nSector2 = nSector;
+        updatesector(x2, y2, &nSector2);
+        if (nSector2 == nSector)
+        {
+            int z2 = getflorzofslope(nSector2, x2, y2);
+            predict.at28 = interpolate(predict.at28, fix16_from_int(z1-z2)>>3, 0x4000);
+        }
+    }
+    else
+    {
+        predict.at28 = interpolate(predict.at28, 0, 0x4000);
+        if (klabs(predict.at28) < 4)
+            predict.at28 = 0;
+    }
+    predict.at2c = (-fix16_to_int(predict.at24))<<7;
+}
+
+void fakePlayerProcess(PLAYER *pPlayer, GINPUT *pInput)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][predict.at48];
+
+    int top, bottom;
+    GetSpriteExtents(pSprite, &top, &bottom);
+
+    top += predict.at58-pSprite->z;
+    bottom += predict.at58-pSprite->z;
+
+    int dzb = (bottom-predict.at58)/4;
+    int dzt = (predict.at58-top)/4;
+
+    int dw = pSprite->clipdist<<2;
+    short nSector = predict.at68;
+    if (!gNoClip)
+    {
+        pushmove_old((int32_t*)&predict.at50, (int32_t*)&predict.at54, (int32_t*)&predict.at58, &predict.at68, dw, dzt, dzb, CLIPMASK0);
+        if (predict.at68 == -1)
+            predict.at68 = nSector;
+    }
+    fakeProcessInput(pPlayer, pInput);
+
+    int nSpeed = approxDist(predict.at5c, predict.at60);
+
+    predict.at3c = interpolate(predict.at3c, predict.at64, 0x7000);
+    int dz = predict.at58-pPosture->at24-predict.at38;
+    if (dz > 0)
+        predict.at3c += mulscale16(dz<<8, 0xa000);
+    else
+        predict.at3c += mulscale16(dz<<8, 0x1800);
+    predict.at38 += predict.at3c>>8;
+
+    predict.at44 = interpolate(predict.at44, predict.at64, 0x5000);
+    dz = predict.at58-pPosture->at28-predict.at40;
+    if (dz > 0)
+        predict.at44 += mulscale16(dz<<8, 0x8000);
+    else
+        predict.at44 += mulscale16(dz<<8, 0xc00);
+    predict.at40 += predict.at44>>8;
+
+    predict.at34 = predict.at40 - predict.at38 - (12<<8);
+
+    predict.at0 = ClipLow(predict.at0-4, 0);
+
+    nSpeed >>= 16;
+	if (predict.at48 == 1)
+	{
+		predict.at4 = (predict.at4+17)&2047;
+		predict.at14 = (predict.at14+17)&2047;
+		predict.at8 = mulscale30(10*pPosture->at14,Sin(predict.at4*2));
+		predict.atc = mulscale30(predict.at0*pPosture->at18,Sin(predict.at4-256));
+		predict.at18 = mulscale30(predict.at0*pPosture->at1c,Sin(predict.at14*2));
+		predict.at1c = mulscale30(predict.at0*pPosture->at20,Sin(predict.at14-0x155));
+	}
+	else
+	{
+		if (pXSprite->height < 256)
+		{
+			predict.at4 = (predict.at4+(pPosture->atc[predict.at70]*4))&2047;
+			predict.at14 = (predict.at14+(pPosture->atc[predict.at70]*4)/2)&2047;
+			if (predict.at70)
+			{
+				if (predict.at0 < 60)
+                    predict.at0 = ClipHigh(predict.at0 + nSpeed, 60);
+			}
+			else
+			{
+				if (predict.at0 < 30)
+                    predict.at0 = ClipHigh(predict.at0 + nSpeed, 30);
+			}
+		}
+		predict.at8 = mulscale30(predict.at0*pPosture->at14,Sin(predict.at4*2));
+		predict.atc = mulscale30(predict.at0*pPosture->at18,Sin(predict.at4-256));
+		predict.at18 = mulscale30(predict.at0*pPosture->at1c,Sin(predict.at14*2));
+		predict.at1c = mulscale30(predict.at0*pPosture->at20,Sin(predict.at14-0x155));
+	}
+	if (!pXSprite->health)
+        return;
+	predict.at72 = 0;
+	if (predict.at48 == 1)
+	{
+		predict.at72 = 1;
+        int nSector = predict.at68;
+        int nLink = gLowerLink[nSector];
+		if (nLink > 0 && (sprite[nLink].type == 14 || sprite[nLink].type == 10))
+		{
+			if (getceilzofslope(nSector, predict.at50, predict.at54) > predict.at38)
+				predict.at72 = 0;
+		}
+	}
+}
+
+void fakeMoveDude(spritetype *pSprite)
+{
+    PLAYER *pPlayer = NULL;
+    int bottom, top;
+    if (IsPlayerSprite(pSprite))
+        pPlayer = &gPlayer[pSprite->type-kDudePlayer1];
+    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
+    GetSpriteExtents(pSprite, &top, &bottom);
+	top += predict.at58 - pSprite->z;
+	bottom += predict.at58 - pSprite->z;
+    int bz = (bottom-predict.at58)/4;
+    int tz = (predict.at58-top)/4;
+    int wd = pSprite->clipdist*4;
+    int nSector = predict.at68;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    if (predict.at5c || predict.at60)
+    {
+        if (pPlayer && gNoClip)
+        {
+            predict.at50 += predict.at5c>>12;
+            predict.at54 += predict.at60>>12;
+            if (!FindSector(predict.at50, predict.at54, &nSector))
+                nSector = predict.at68;
+        }
+        else
+        {
+            short bakCstat = pSprite->cstat;
+            pSprite->cstat &= ~257;
+            predict.at75.hit = ClipMove(&predict.at50, &predict.at54, &predict.at58, &nSector, predict.at5c >> 12, predict.at60 >> 12, wd, tz, bz, 0x13001);
+            if (nSector == -1)
+                nSector = predict.at68;
+                    
+            if (sector[nSector].lotag >= 612 && sector[nSector].lotag <= 617)
+            {
+                short nSector2 = nSector;
+                pushmove_old((int32_t*)&predict.at50, (int32_t*)&predict.at54, (int32_t*)&predict.at58, &nSector2, wd, tz, bz, 0x10001);
+                if (nSector2 != -1)
+                    nSector = nSector2;
+            }
+
+            dassert(nSector >= 0);
+
+            pSprite->cstat = bakCstat;
+        }
+        switch (predict.at75.hit&0xe000)
+        {
+        case 0x8000:
+        {
+            int nHitWall = predict.at75.hit&0x1fff;
+            walltype *pHitWall = &wall[nHitWall];
+            if (pHitWall->nextsector != -1)
+            {
+                sectortype *pHitSector = &sector[pHitWall->nextsector];
+                if (top < pHitSector->ceilingz || bottom > pHitSector->floorz)
+                {
+                    // ???
+                }
+            }
+            actWallBounceVector(&predict.at5c, &predict.at60, nHitWall, 0);
+            break;
+        }
+        }
+    }
+    if (predict.at68 != nSector)
+    {
+        dassert(nSector >= 0 && nSector < kMaxSectors);
+        predict.at68 = nSector;
+    }
+    char bUnderwater = 0;
+    char bDepth = 0;
+    int nXSector = sector[nSector].extra;
+    if (nXSector > 0)
+    {
+        XSECTOR *pXSector = &xsector[nXSector];
+        if (pXSector->Underwater)
+            bUnderwater = 1;
+        if (pXSector->Depth)
+            bDepth = 1;
+    }
+    int nUpperLink = gUpperLink[nSector];
+    int nLowerLink = gLowerLink[nSector];
+    if (nUpperLink >= 0 && (sprite[nUpperLink].type == 9 || sprite[nUpperLink].type == 13))
+        bDepth = 1;
+    if (nLowerLink >= 0 && (sprite[nLowerLink].type == 10 || sprite[nLowerLink].type == 14))
+        bDepth = 1;
+    if (pPlayer)
+        wd += 16;
+
+    if (predict.at64)
+        predict.at58 += predict.at64 >> 8;
+
+    spritetype pSpriteBak = *pSprite;
+    spritetype *pTempSprite = pSprite;
+    pTempSprite->x = predict.at50;
+    pTempSprite->y = predict.at54;
+    pTempSprite->z = predict.at58;
+    pTempSprite->sectnum = predict.at68;
+    int ceilZ, ceilHit, floorZ, floorHit;
+    GetZRange(pTempSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, wd, 0x10001);
+    GetSpriteExtents(pTempSprite, &top, &bottom);
+    if (predict.at73 & 2)
+    {
+        int vc = 58254;
+        if (bDepth)
+        {
+            if (bUnderwater)
+            {
+                int cz = getceilzofslope(nSector, predict.at50, predict.at54);
+                if (cz > top)
+                    vc += ((bottom-cz)*-80099) / (bottom-top);
+                else
+                    vc = 0;
+            }
+            else
+            {
+                int fz = getflorzofslope(nSector, predict.at50, predict.at54);
+                if (fz < bottom)
+                    vc += ((bottom-fz)*-80099) / (bottom-top);
+            }
+        }
+        else
+        {
+            if (bUnderwater)
+                vc = 0;
+            else if (bottom >= floorZ)
+                vc = 0;
+        }
+        if (vc)
+        {
+            predict.at58 += ((vc*4)/2)>>8;
+            predict.at64 += vc;
+        }
+    }
+    GetSpriteExtents(pTempSprite, &top, &bottom);
+    if (bottom >= floorZ)
+    {
+        int floorZ2 = floorZ;
+        int floorHit2 = floorHit;
+        GetZRange(pTempSprite, &ceilZ, &ceilHit, &floorZ, &floorHit, pSprite->clipdist<<2, 0x13001);
+        if (bottom <= floorZ && predict.at58-floorZ2 < bz)
+        {
+            floorZ = floorZ2;
+            floorHit = floorHit2;
+        }
+    }
+    if (floorZ <= bottom)
+    {
+        predict.at75.florhit = floorHit;
+        predict.at58 += floorZ-bottom;
+        int var44 = predict.at64-velFloor[predict.at68];
+        if (var44 > 0)
+        {
+            actFloorBounceVector(&predict.at5c, &predict.at60, &var44, predict.at68, 0);
+            predict.at64 = var44;
+            if (klabs(predict.at64) < 0x10000)
+            {
+                predict.at64 = velFloor[predict.at68];
+                predict.at73 &= ~4;
+            }
+            else
+                predict.at73 |= 4;
+        }
+        else if (predict.at64 == 0)
+            predict.at73 &= ~4;
+    }
+    else
+    {
+        predict.at75.florhit = 0;
+        if (predict.at73 & 2)
+            predict.at73 |= 4;
+    }
+    if (top <= ceilZ)
+    {
+        predict.at75.ceilhit = ceilHit;
+        predict.at58 += ClipLow(ceilZ-top, 0);
+        if (predict.at64 <= 0 && (predict.at73&4))
+            predict.at64 = mulscale16(-predict.at64, 0x2000);
+    }
+    else
+        predict.at75.ceilhit = 0;
+
+    GetSpriteExtents(pTempSprite, &top, &bottom);
+    *pSprite = pSpriteBak;
+    predict.at6a = ClipLow(floorZ-bottom, 0)>>8;
+    if (predict.at5c || predict.at60)
+    {
+        if ((floorHit & 0xe000) == 0xc000)
+        {
+            int nHitSprite = floorHit & 0x1fff;
+            if ((sprite[nHitSprite].cstat & 0x30) == 0)
+            {
+                predict.at5c += mulscale(4, predict.at50 - sprite[nHitSprite].x, 2);
+                predict.at60 += mulscale(4, predict.at54 - sprite[nHitSprite].y, 2);
+                return;
+            }
+        }
+        int nXSector = sector[pSprite->sectnum].extra;
+        if (nXSector > 0 && xsector[nXSector].Underwater)
+            return;
+        if (predict.at6a >= 0x100)
+            return;
+        int nDrag = gDudeDrag;
+        if (predict.at6a > 0)
+            nDrag -= scale(gDudeDrag, predict.at6a, 0x100);
+        predict.at5c -= mulscale16r(predict.at5c, nDrag);
+        predict.at60 -= mulscale16r(predict.at60, nDrag);
+        if (approxDist(predict.at5c, predict.at60) < 0x1000)
+            predict.at5c = predict.at60 = 0;
+    }
+}
+
+void fakeActAirDrag(spritetype *pSprite, int num)
+{
+    UNREFERENCED_PARAMETER(pSprite);
+    int xvec = 0;
+    int yvec = 0;
+    int nSector = predict.at68;
+    dassert(nSector >= 0 && nSector < kMaxSectors);
+    sectortype *pSector = &sector[nSector];
+    int nXSector = pSector->extra;
+    if (nXSector > 0)
+    {
+        dassert(nXSector < kMaxXSectors);
+        XSECTOR *pXSector = &xsector[nXSector];
+        if (pXSector->windVel && (pXSector->windAlways || pXSector->busy))
+        {
+            int vel = pXSector->windVel<<12;
+            if (!pXSector->windAlways && pXSector->busy)
+                vel = mulscale16(vel, pXSector->busy);
+            xvec = mulscale30(vel, Cos(pXSector->windAng));
+            yvec = mulscale30(vel, Sin(pXSector->windAng));
+        }
+    }
+    predict.at5c += mulscale16(xvec-predict.at5c, num);
+    predict.at60 += mulscale16(yvec-predict.at60, num);
+    predict.at64 -= mulscale16(predict.at64, num);
+}
+
+void fakeActProcessSprites(void)
+{
+	spritetype *pSprite = gMe->pSprite;
+	if (pSprite->statnum == 6)
+	{
+		int nXSprite = pSprite->extra;
+		dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+		int nSector = predict.at68;
+		int nXSector = sector[nSector].extra;
+        XSECTOR *pXSector = NULL;
+        if (nXSector > 0)
+        {
+            dassert(nXSector > 0 && nXSector < kMaxXSectors);
+            dassert(xsector[nXSector].reference == nSector);
+            pXSector = &xsector[nXSector];
+        }
+		if (pXSector)
+		{
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+			top += predict.at58 - pSprite->z;
+			bottom += predict.at58 - pSprite->z;
+			if (getflorzofslope(nSector, predict.at50, predict.at54) < bottom)
+			{
+				int angle = pXSector->panAngle;
+                int speed = 0;
+				if (pXSector->panAlways || pXSector->state || pXSector->busy)
+				{
+					speed = pXSector->panVel << 9;
+					if (!pXSector->panAlways && pXSector->busy)
+						speed = mulscale16(speed, pXSector->busy);
+				}
+				if (sector[nSector].floorstat&64)
+					angle = (GetWallAngle(sector[nSector].wallptr)+512)&2047;
+				predict.at5c += mulscale30(speed,Cos(angle));
+				predict.at60 += mulscale30(speed,Sin(angle));
+			}
+		}
+        if (pXSector && pXSector->Underwater)
+            fakeActAirDrag(pSprite, 5376);
+        else
+            fakeActAirDrag(pSprite, 128);
+
+        if ((predict.at73 & 4) != 0 || predict.at5c != 0 || predict.at60 != 0 || predict.at64 != 0 || velFloor[predict.at68] != 0 || velCeil[predict.at68] != 0)
+        {
+            fakeMoveDude(pSprite);
+        }
+	}
+}
+
+void viewCorrectPrediction(void)
+{
+    if (gGameOptions.nGameType == 0) return;
+    spritetype *pSprite = gMe->pSprite;
+    VIEW *pView = &predictFifo[(gNetFifoTail-1)&255];
+    if (gMe->q16ang != pView->at30 || pView->at24 != gMe->q16horiz || pView->at50 != pSprite->x || pView->at54 != pSprite->y || pView->at58 != pSprite->z)
+    {
+        viewInitializePrediction();
+        predictOld = gPrevView[myconnectindex];
+        gPredictTail = gNetFifoTail;
+        while (gPredictTail < gNetFifoHead[myconnectindex])
+        {
+            viewUpdatePrediction(&gFifoInput[gPredictTail&255][myconnectindex]);
+        }
+    }
+}
+
+void viewBackupView(int nPlayer)
+{
+    PLAYER *pPlayer = &gPlayer[nPlayer];
+    VIEW *pView = &gPrevView[nPlayer];
+    pView->at30 = pPlayer->q16ang;
+    pView->at50 = pPlayer->pSprite->x;
+    pView->at54 = pPlayer->pSprite->y;
+    pView->at38 = pPlayer->at67;
+    pView->at34 = pPlayer->at6f-pPlayer->at67-0xc00;
+    pView->at24 = pPlayer->q16horiz;
+    pView->at28 = pPlayer->q16slopehoriz;
+    pView->at2c = pPlayer->at83;
+    pView->at8 = pPlayer->at3f;
+    pView->atc = pPlayer->at43;
+    pView->at18 = pPlayer->at4f;
+    pView->at1c = pPlayer->at53;
+}
+
+void viewClearInterpolations(void)
+{
+    nInterpolations = 0;
+    memset(gInterpolateSprite, 0, sizeof(gInterpolateSprite));
+    memset(gInterpolateWall, 0, sizeof(gInterpolateWall));
+    memset(gInterpolateSector, 0, sizeof(gInterpolateSector));
+}
+
+void viewAddInterpolation(void *data, INTERPOLATE_TYPE type)
+{
+    if (nInterpolations == 4096)
+        ThrowError("Too many interpolations");
+    INTERPOLATE *pInterpolate = &gInterpolation[nInterpolations++];
+    pInterpolate->pointer = data;
+    pInterpolate->type = type;
+    switch (type)
+    {
+    case INTERPOLATE_TYPE_INT:
+        pInterpolate->value = *((int*)data);
+        break;
+    case INTERPOLATE_TYPE_SHORT:
+        pInterpolate->value = *((short*)data);
+        break;
+    }
+}
+
+void CalcInterpolations(void)
+{
+    int i;
+    INTERPOLATE *pInterpolate = gInterpolation;
+    for (i = 0; i < nInterpolations; i++, pInterpolate++)
+    {
+        switch (pInterpolate->type)
+        {
+        case INTERPOLATE_TYPE_INT:
+        {
+            pInterpolate->value2 = *((int*)pInterpolate->pointer);
+            int newValue = interpolate(pInterpolate->value, *((int*)pInterpolate->pointer), gInterpolate);
+            *((int*)pInterpolate->pointer) = newValue;
+            break;
+        }
+        case INTERPOLATE_TYPE_SHORT:
+        {
+            pInterpolate->value2 = *((short*)pInterpolate->pointer);
+            int newValue = interpolate(pInterpolate->value, *((short*)pInterpolate->pointer), gInterpolate);
+            *((short*)pInterpolate->pointer) = newValue;
+            break;
+        }
+        }
+    }
+}
+
+void RestoreInterpolations(void)
+{
+    int i;
+    INTERPOLATE *pInterpolate = gInterpolation;
+    for (i = 0; i < nInterpolations; i++, pInterpolate++)
+    {
+        switch (pInterpolate->type)
+        {
+        case INTERPOLATE_TYPE_INT:
+            *((int*)pInterpolate->pointer) = pInterpolate->value2;
+            break;
+        case INTERPOLATE_TYPE_SHORT:
+            *((short*)pInterpolate->pointer) = pInterpolate->value2;
+            break;
+        }
+    }
+}
+
+void viewDrawText(int nFont, const char *pString, int x, int y, int nShade, int nPalette, int position, char shadow, unsigned int nStat, uint8_t alpha)
+{
+    if (nFont < 0 || nFont >= 5 || !pString) return;
+    FONT *pFont = &gFont[nFont];
+
+    if (position)
+    {
+        const char *s = pString;
+        int width = -pFont->space;
+        while (*s)
+        {
+            int nTile = ((*s-' ')&127)+pFont->tile;
+            if (tilesiz[nTile].x && tilesiz[nTile].y)
+                width += tilesiz[nTile].x+pFont->space;
+            s++;
+        }
+        if (position == 1)
+            width >>= 1;
+        x -= width;
+    }
+    const char *s = pString;
+    while (*s)
+    {
+        int nTile = ((*s-' ')&127) + pFont->tile;
+        if (tilesiz[nTile].x && tilesiz[nTile].y)
+        {
+            if (shadow)
+            {
+                rotatesprite_fs_alpha((x+1)<<16, (y+1)<<16, 65536, 0, nTile, 127, nPalette, 26|nStat, alpha);
+            }
+            rotatesprite_fs_alpha(x<<16, y<<16, 65536, 0, nTile, nShade, nPalette, 26|nStat, alpha);
+            x += tilesiz[nTile].x+pFont->space;
+        }
+        s++;
+    }
+}
+
+void viewTileSprite(int nTile, int nShade, int nPalette, int x1, int y1, int x2, int y2)
+{
+    Rect rect1 = Rect(x1, y1, x2, y2);
+    Rect rect2 = Rect(0, 0, xdim, ydim);
+    rect1 &= rect2;
+
+    if (!rect1)
+        return;
+
+    dassert(nTile >= 0 && nTile < kMaxTiles);
+    int width = tilesiz[nTile].x;
+    int height = tilesiz[nTile].y;
+    int bx1 = DecBy(rect1.x1+1, width);
+    int by1 = DecBy(rect1.y1+1, height);
+    int bx2 = IncBy(rect1.x2-1, width);
+    int by2 = IncBy(rect1.y2-1, height);
+    for (int x = bx1; x < bx2; x += width)
+        for (int y = by1; y < by2; y += height)
+            rotatesprite(x<<16, y<<16, 65536, 0, nTile, nShade, nPalette, 64+16+8, x1, y1, x2-1, y2-1);
+}
+
+void InitStatusBar(void)
+{
+    tileLoadTile(2200);
+}
+void DrawStatSprite(int nTile, int x, int y, int nShade, int nPalette, unsigned int nStat, int nScale)
+{
+    rotatesprite(x<<16, y<<16, nScale, 0, nTile, nShade, nPalette, nStat | 74, 0, 0, xdim-1, ydim-1);
+}
+void DrawStatMaskedSprite(int nTile, int x, int y, int nShade, int nPalette, unsigned int nStat, int nScale)
+{
+    rotatesprite(x<<16, y<<16, nScale, 0, nTile, nShade, nPalette, nStat | 10, 0, 0, xdim-1, ydim-1);
+}
+
+void DrawStatNumber(const char *pFormat, int nNumber, int nTile, int x, int y, int nShade, int nPalette, unsigned int nStat, int nScale)
+{
+    char tempbuf[80];
+    int width = tilesiz[nTile].x+1;
+    x <<= 16;
+    sprintf(tempbuf, pFormat, nNumber);
+    for (unsigned int i = 0; i < strlen(tempbuf); i++, x += width*nScale)
+    {
+        if (tempbuf[i] == ' ') continue;
+        rotatesprite(x, y<<16, nScale, 0, nTile+tempbuf[i]-'0', nShade, nPalette, nStat | 10, 0, 0, xdim-1, ydim-1);
+    }
+}
+
+void TileHGauge(int nTile, int x, int y, int nMult, int nDiv, int nStat, int nScale)
+{
+    int bx = scale(mulscale16(tilesiz[nTile].x,nScale),nMult,nDiv)+x;
+    int sbx;
+    switch (nStat&(512+256))
+    {
+    case 256:
+        sbx = mulscale16(bx, xscalecorrect)-1;
+        break;
+    case 512:
+        bx -= 320;
+        sbx = xdim+mulscale16(bx, xscalecorrect)-1;
+        break;
+    default:
+        bx -= 160;
+        sbx = (xdim>>1)+mulscale16(bx, xscalecorrect)-1;
+        break;
+    }
+    rotatesprite(x<<16, y<<16, nScale, 0, nTile, 0, 0, nStat|90, 0, 0, sbx, ydim-1);
+}
+
+int gPackIcons[5] = {
+    2569, 2564, 2566, 2568, 2560
+};
+
+struct PACKICON2 {
+    short nTile;
+    int nScale;
+    int nYOffs;
+};
+
+PACKICON2 gPackIcons2[] = {
+    { 519, (int)(65536*0.5), 0 },
+    { 830, (int)(65536*0.3), 0 },
+    { 760, (int)(65536*0.6), 0 },
+    { 839, (int)(65536*0.5), -4 },
+    { 827, (int)(65536*0.4), 0 },
+};
+
+struct AMMOICON {
+    short nTile;
+    int nScale;
+    int nYOffs;
+};
+
+AMMOICON gAmmoIcons[] = {
+    { -1, 0, 0 },
+    { 816, (int)(65536 * 0.5), 0 },
+    { 619, (int)(65536 * 0.8), 0 },
+    { 817, (int)(65536 * 0.7), 3 },
+    { 801, (int)(65536 * 0.5), -6 },
+    { 589, (int)(65536 * 0.7), 2 },
+    { 618, (int)(65536 * 0.5), 4 },
+    { 548, (int)(65536 * 0.3), -6 },
+    { 820, (int)(65536 * 0.3), -6 },
+    { 525, (int)(65536 * 0.6), -6 },
+    { 811, (int)(65536 * 0.5), 2 },
+    { 810, (int)(65536 * 0.45), 2 },
+};
+
+struct WEAPONICON {
+    short nTile;
+    char xRepeat;
+    char yRepeat;
+};
+
+WEAPONICON gWeaponIcon[] = {
+    { -1, 0, 0 },
+    { -1, 0, 0 },
+    { 524, 32, 32 },
+    { 559, 32, 32 },
+    { 558, 32, 32 },
+    { 526, 32, 32 },
+    { 589, 32, 32 },
+    { 618, 32, 32 },
+    { 539, 32, 32 },
+    { 800, 32, 32 },
+    { 525, 32, 32 },
+    { 811, 32, 32 },
+    { 810, 32, 32 },
+    { -1, 0, 0 },
+};
+
+int dword_14C508;
+
+void viewDrawStats(PLAYER *pPlayer, int x, int y)
+{
+    const int nFont = 3;
+    char buffer[128];
+    if (!gLevelStats)
+        return;
+
+    int nHeight;
+    viewGetFontInfo(nFont, NULL, NULL, &nHeight);
+    sprintf(buffer, "T:%d:%02d.%02d",
+        (gLevelTime/(kTicsPerSec*60)),
+        (gLevelTime/kTicsPerSec)%60,
+        ((gLevelTime%kTicsPerSec)*33)/10
+        );
+    viewDrawText(3, buffer, x, y, 20, 0, 0, true, 256);
+    y += nHeight+1;
+    if (gGameOptions.nGameType == 0 || gGameOptions.nGameType == 2)
+        sprintf(buffer, "K:%d/%d", gKillMgr.at4, gKillMgr.at0);
+    else
+        sprintf(buffer, "K:%d", pPlayer->at2c6);
+    viewDrawText(3, buffer, x, y, 20, 0, 0, true, 256);
+    y += nHeight+1;
+    sprintf(buffer, "S:%d/%d", gSecretMgr.at4+gSecretMgr.at8, gSecretMgr.at0);
+    viewDrawText(3, buffer, x, y, 20, 0, 0, true, 256);
+}
+
+#define kSBarNumberHealth 9220
+#define kSBarNumberAmmo 9230
+#define kSBarNumberInv 9240
+#define kSBarNumberArmor1 9250
+#define kSBarNumberArmor2 9260
+#define kSBarNumberArmor3 9270
+
+struct POWERUPDISPLAY
+{
+    int nTile;
+    float nScaleRatio;
+    int yOffset;
+    int remainingDuration;
+};
+
+void sortPowerUps(POWERUPDISPLAY* powerups) {
+    for (int i = 1; i < 5; i++)
+    {
+        for (int j = 0; j < 5-i; j++)
+        {
+            if (powerups[j].remainingDuration > powerups[j+1].remainingDuration)
+            {
+                POWERUPDISPLAY temp = powerups[j];
+                powerups[j] = powerups[j+1];
+                powerups[j+1] = temp;
+            }
+        }
+    }
+}
+
+void viewDrawPowerUps(PLAYER* pPlayer)
+{
+    if (!gPowerupDuration)
+        return;
+
+    const int nCloakOfInvisibility = 13;
+    const int nReflectiveShots = 24;
+    const int nDeathMask = 14; // invulnerability
+    const int nGunsAkimbo = 17;
+    const int nCloakOfShadow = 26; // does nothing, only appears at near the end of Cryptic Passage's Lost Monastery (CP04)
+
+    POWERUPDISPLAY powerups[5];
+    powerups[0] = { 896, 0.4f, 0, pPlayer->at202[nCloakOfInvisibility] };
+    powerups[1] = { 2428, 0.4f, 5, pPlayer->at202[nReflectiveShots] };
+    powerups[2] = { 825, 0.3f, 9, pPlayer->at202[nDeathMask] };
+    powerups[3] = { 829, 0.3f, 5, pPlayer->at202[nGunsAkimbo] };
+    powerups[4] = { 768, 0.4f, 9, pPlayer->at202[nCloakOfShadow] };
+
+    sortPowerUps(powerups);
+
+    const int x = 15;
+    int y = 50;
+    for (int i = 0; i < 5; i++)
+    {
+        if (powerups[i].remainingDuration)
+        {
+            int remainingSeconds = powerups[i].remainingDuration / 100;
+            if (remainingSeconds > 5 || (gGameClock & 32))
+            {
+                DrawStatMaskedSprite(powerups[i].nTile, x, y + powerups[i].yOffset, 0, 0, 256, (int)(65536 * powerups[i].nScaleRatio));
+            }
+
+            DrawStatNumber("%d", remainingSeconds, kSBarNumberInv, x + 15, y, 0, 0, 256, 65536 * 0.5);
+            y += 20;
+        }
+    }
+}
+
+void viewDrawMapTitle(void)
+{
+    if (!gShowMapTitle || gGameMenuMgr.m_bActive)
+        return;
+
+    int seconds = (gLevelTime / kTicsPerSec);
+    int millisecs = (gLevelTime % kTicsPerSec) * 33;
+    if (seconds > 3)
+        return;
+
+    const int noAlphaForSecs = 1;
+    uint8_t alpha = videoGetRenderMode() != REND_CLASSIC || numalphatabs >= 15 ?
+        seconds < noAlphaForSecs ? 0 : clamp(((seconds-noAlphaForSecs)*1000+millisecs)/4, 0, 255)
+        : 0;
+
+    if (alpha != 255)
+    {
+        viewDrawText(1, levelGetTitle(), 160, 50, -128, 0, 1, 1, 0, alpha);
+    }
+}
+
+void viewDrawPack(PLAYER *pPlayer, int x, int y)
+{
+    int packs[5];
+    if (pPlayer->at31d)
+    {
+        int nPacks = 0;
+        int width = 0;
+        for (int i = 0; i < 5; i++)
+        {
+            if (pPlayer->packInfo[i].at1)
+            {
+                packs[nPacks++] = i;
+                width += tilesiz[gPackIcons[i]].x + 1;
+            }
+        }
+        width /= 2;
+        x -= width;
+        for (int i = 0; i < nPacks; i++)
+        {
+            int nPack = packs[i];
+            DrawStatSprite(2568, x+1, y-8);
+            DrawStatSprite(2568, x+1, y-6);
+            DrawStatSprite(gPackIcons[nPack], x+1, y+1);
+            if (nPack == pPlayer->at321)
+                DrawStatMaskedSprite(2559, x+1, y+1);
+            int nShade;
+            if (pPlayer->packInfo[nPack].at0)
+                nShade = 4;
+            else
+                nShade = 24;
+            DrawStatNumber("%3d", pPlayer->packInfo[nPack].at1, 2250, x-4, y-13, nShade, 0);
+            x += tilesiz[gPackIcons[nPack]].x + 1;
+        }
+    }
+    if (pPlayer->at31d != dword_14C508)
+    {
+        viewUpdatePages();
+    }
+    dword_14C508 = pPlayer->at31d;
+}
+
+void DrawPackItemInStatusBar(PLAYER *pPlayer, int x, int y, int x2, int y2, int nStat)
+{
+    if (pPlayer->at321 < 0) return;
+
+    DrawStatSprite(gPackIcons[pPlayer->at321], x, y, 0, 0, nStat);
+    DrawStatNumber("%3d", pPlayer->packInfo[pPlayer->at321].at1, 2250, x2, y2, 0, 0, nStat);
+}
+
+void DrawPackItemInStatusBar2(PLAYER *pPlayer, int x, int y, int x2, int y2, int nStat, int nScale)
+{
+    if (pPlayer->at321 < 0) return;
+
+    DrawStatMaskedSprite(gPackIcons2[pPlayer->at321].nTile, x, y+gPackIcons2[pPlayer->at321].nYOffs, 0, 0, nStat, gPackIcons2[pPlayer->at321].nScale);
+    DrawStatNumber("%3d", pPlayer->packInfo[pPlayer->at321].at1, kSBarNumberInv, x2, y2, 0, 0, nStat, nScale);
+}
+
+char gTempStr[128];
+
+void UpdateStatusBar(int arg)
+{
+    PLAYER *pPlayer = gView;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+
+    int nPalette = 0;
+
+    if (gGameOptions.nGameType == 3)
+    {
+        if (pPlayer->at2ea & 1)
+            nPalette = 7;
+        else
+            nPalette = 10;
+    }
+
+    if (gViewSize < 0) return;
+
+    if (gViewSize == 1)
+    {
+        DrawStatMaskedSprite(2169, 12, 195, 0, 0, 256, (int)(65536*0.56));
+        DrawStatNumber("%d", pXSprite->health>>4, kSBarNumberHealth, 28, 187, 0, 0, 256);
+        if (pPlayer->at33e[1])
+        {
+            DrawStatMaskedSprite(2578, 70, 186, 0, 0, 256, (int)(65536*0.5));
+            DrawStatNumber("%3d", pPlayer->at33e[1]>>4, kSBarNumberArmor2, 83, 187, 0, 0, 256, (int)(65536*0.65));
+        }
+        if (pPlayer->at33e[0])
+        {
+            DrawStatMaskedSprite(2586, 112, 195, 0, 0, 256, (int)(65536*0.5));
+            DrawStatNumber("%3d", pPlayer->at33e[0]>>4, kSBarNumberArmor1, 125, 187, 0, 0, 256, (int)(65536*0.65));
+        }
+        if (pPlayer->at33e[2])
+        {
+            DrawStatMaskedSprite(2602, 155, 196, 0, 0, 256, (int)(65536*0.5));
+            DrawStatNumber("%3d", pPlayer->at33e[2]>>4, kSBarNumberArmor3, 170, 187, 0, 0, 256, (int)(65536*0.65));
+        }
+
+        DrawPackItemInStatusBar2(pPlayer, 225, 194, 240, 187, 512, (int)(65536*0.7));
+
+        if (pPlayer->atbd && pPlayer->atc7 != -1)
+        {
+            int num = pPlayer->at181[pPlayer->atc7];
+            if (pPlayer->atc7 == 6)
+                num /= 10;
+            if ((unsigned int)gAmmoIcons[pPlayer->atc7].nTile < kMaxTiles)
+                DrawStatMaskedSprite(gAmmoIcons[pPlayer->atc7].nTile, 304, 192+gAmmoIcons[pPlayer->atc7].nYOffs,
+                    0, 0, 512, gAmmoIcons[pPlayer->atc7].nScale);
+            DrawStatNumber("%3d", num, kSBarNumberAmmo, 267, 187, 0, 0, 512);
+        }
+
+        for (int i = 0; i < 6; i++)
+        {
+            if (pPlayer->at88[i+1])
+                DrawStatMaskedSprite(2552+i, 260+10*i, 170, 0, 0, 512, (int)(65536*0.25));
+        }
+
+        if (pPlayer->at1ba)
+            TileHGauge(2260, 124, 175-10, pPlayer->at1ba, 65536);
+        else
+            viewDrawPack(pPlayer, 166, 200-tilesiz[2201].y/2-30);
+        viewDrawStats(pPlayer, 2, 140);
+        viewDrawPowerUps(pPlayer);
+    }
+    else if (gViewSize <= 2)
+    {
+        if (pPlayer->at1ba)
+            TileHGauge(2260, 124, 175, pPlayer->at1ba, 65536);
+        else
+            viewDrawPack(pPlayer, 166, 200-tilesiz[2201].y/2);
+    }
+    if (gViewSize == 2)
+    {
+        DrawStatSprite(2201, 34, 187, 16, nPalette, 256);
+        if (pXSprite->health >= 16 || (gGameClock&16) || pXSprite->health == 0)
+        {
+            DrawStatNumber("%3d", pXSprite->health>>4, 2190, 8, 183, 0, 0, 256);
+        }
+        if (pPlayer->atbd && pPlayer->atc7 != -1)
+        {
+            int num = pPlayer->at181[pPlayer->atc7];
+            if (pPlayer->atc7 == 6)
+                num /= 10;
+            DrawStatNumber("%3d", num, 2240, 42, 183, 0, 0, 256);
+        }
+        DrawStatSprite(2173, 284, 187, 16, nPalette, 512);
+        if (pPlayer->at33e[1])
+        {
+            TileHGauge(2207, 250, 175, pPlayer->at33e[1], 3200, 512);
+            DrawStatNumber("%3d", pPlayer->at33e[1]>>4, 2230, 255, 178, 0, 0, 512);
+        }
+        if (pPlayer->at33e[0])
+        {
+            TileHGauge(2209, 250, 183, pPlayer->at33e[0], 3200, 512);
+            DrawStatNumber("%3d", pPlayer->at33e[0]>>4, 2230, 255, 186, 0, 0, 512);
+        }
+        if (pPlayer->at33e[2])
+        {
+            TileHGauge(2208, 250, 191, pPlayer->at33e[2], 3200, 512);
+            DrawStatNumber("%3d", pPlayer->at33e[2]>>4, 2230, 255, 194, 0, 0, 512);
+        }
+        DrawPackItemInStatusBar(pPlayer, 286, 186, 302, 183, 512);
+
+        for (int i = 0; i < 6; i++)
+        {
+            int nTile = 2220+i;
+            int x, nStat = 0;
+            int y = 200-6;
+            if (i&1)
+            {
+                x = 320-(78+(i>>1)*10);
+                nStat |= 512;
+            }
+            else
+            {
+                x = 73+(i>>1)*10;
+                nStat |= 256;
+            }
+            if (pPlayer->at88[i+1])
+                DrawStatSprite(nTile, x, y, 0, 0, nStat);
+#if 0
+            else
+                DrawStatSprite(nTile, x, y, 40, 5, nStat);
+#endif
+        }
+        viewDrawStats(pPlayer, 2, 140);
+        viewDrawPowerUps(pPlayer);
+    }
+    else if (gViewSize > 2)
+    {
+        viewDrawPack(pPlayer, 160, 200-tilesiz[2200].y);
+        DrawStatMaskedSprite(2200, 160, 172, 16, nPalette);
+        DrawPackItemInStatusBar(pPlayer, 265, 186, 260, 172);
+        if (pXSprite->health >= 16 || (gGameClock&16) || pXSprite->health == 0)
+        {
+            DrawStatNumber("%3d", pXSprite->health>>4, 2190, 86, 183, 0, 0);
+        }
+        if (pPlayer->atbd && pPlayer->atc7 != -1)
+        {
+            int num = pPlayer->at181[pPlayer->atc7];
+            if (pPlayer->atc7 == 6)
+                num /= 10;
+            DrawStatNumber("%3d", num, 2240, 216, 183, 0, 0);
+        }
+        for (int i = 9; i >= 1; i--)
+        {
+            int x = 135+((i-1)/3)*23;
+            int y = 182+((i-1)%3)*6;
+            int num = pPlayer->at181[i];
+            if (i == 6)
+                num /= 10;
+            if (i == pPlayer->atc7)
+            {
+                DrawStatNumber("%3d", num, 2230, x, y, -128, 10);
+            }
+            else
+            {
+                DrawStatNumber("%3d", num, 2230, x, y, 32, 10);
+            }
+        }
+
+        if (pPlayer->atc7 == 10)
+        {
+            DrawStatNumber("%2d", pPlayer->at181[10], 2230, 291, 194, -128, 10);
+        }
+        else
+        {
+            DrawStatNumber("%2d", pPlayer->at181[10], 2230, 291, 194, 32, 10);
+        }
+
+        if (pPlayer->atc7 == 11)
+        {
+            DrawStatNumber("%2d", pPlayer->at181[11], 2230, 309, 194, -128, 10);
+        }
+        else
+        {
+            DrawStatNumber("%2d", pPlayer->at181[11], 2230, 309, 194, 32, 10);
+        }
+
+        if (pPlayer->at33e[1])
+        {
+            TileHGauge(2207, 44, 174, pPlayer->at33e[1], 3200);
+            DrawStatNumber("%3d", pPlayer->at33e[1]>>4, 2230, 50, 177, 0, 0);
+        }
+        if (pPlayer->at33e[0])
+        {
+            TileHGauge(2209, 44, 182, pPlayer->at33e[0], 3200);
+            DrawStatNumber("%3d", pPlayer->at33e[0]>>4, 2230, 50, 185, 0, 0);
+        }
+        if (pPlayer->at33e[2])
+        {
+            TileHGauge(2208, 44, 190, pPlayer->at33e[2], 3200);
+            DrawStatNumber("%3d", pPlayer->at33e[2]>>4, 2230, 50, 193, 0, 0);
+        }
+        sprintf(gTempStr, "v%s", GetVersionString());
+        viewDrawText(3, gTempStr, 20, 191, 32, 0, 1, 0);
+
+        for (int i = 0; i < 6; i++)
+        {
+            int nTile = 2220+i;
+            int x = 73+(i&1)*173;
+            int y = 171+(i>>1)*11;
+            if (pPlayer->at88[i+1])
+                DrawStatSprite(nTile, x, y);
+            else
+                DrawStatSprite(nTile, x, y, 40, 5);
+        }
+        DrawStatMaskedSprite(2202, 118, 185, pPlayer->at2e ? 16 : 40);
+        DrawStatMaskedSprite(2202, 201, 185, pPlayer->at2e ? 16 : 40);
+        if (pPlayer->at1ba)
+        {
+            TileHGauge(2260, 124, 175, pPlayer->at1ba, 65536);
+        }
+        viewDrawStats(pPlayer, 2, 140);
+        viewDrawPowerUps(pPlayer);
+    }
+
+    viewDrawMapTitle();
+
+    if (gGameOptions.nGameType < 1) return;
+
+    if (gGameOptions.nGameType == 3)
+    {
+        int x = 1, y = 1;
+        if (dword_21EFD0[0] == 0 || (gGameClock & 8))
+        {
+            viewDrawText(0, "BLUE", x, y, -128, 10, 0, 0, 256);
+            dword_21EFD0[0] = ClipLow(dword_21EFD0[0]-arg, 0);
+            sprintf(gTempStr, "%-3d", dword_21EFB0[0]);
+            viewDrawText(0, gTempStr, x, y+10, -128, 10, 0, 0, 256);
+        }
+        x = 319;
+        if (dword_21EFD0[1] == 0 || (gGameClock & 8))
+        {
+            viewDrawText(0, "RED", x, y, -128, 7, 2, 0, 512);
+            dword_21EFD0[1] = ClipLow(dword_21EFD0[1]-arg, 0);
+            sprintf(gTempStr, "%3d", dword_21EFB0[1]);
+            viewDrawText(0, gTempStr, x, y+10, -128, 7, 2, 0, 512);
+        }
+        return;
+    }
+    for (int nRows = (gNetPlayers-1) / 4; nRows >= 0; nRows--)
+    {
+        for (int nCol = 0; nCol < 4; nCol++)
+        {
+            DrawStatSprite(2229, 40+nCol*80, 4+nRows*9, 16);
+        }
+    }
+    for (int i = 0, p = connecthead; p >= 0; i++, p = connectpoint2[p])
+    {
+        int x = 80*(i&3);
+        int y = 9*(i/4);
+        int col = gPlayer[p].at2ea&3;
+        char *name = gProfile[p].name;
+        if (gProfile[p].skill == 2)
+            sprintf(gTempStr, "%s", name);
+        else
+            sprintf(gTempStr, "%s [%d]", name, gProfile[p].skill);
+        Bstrupr(gTempStr);
+        viewDrawText(4, gTempStr, x+4, y+1, -128, 11+col, 0, 0);
+        sprintf(gTempStr, "%2d", gPlayer[p].at2c6);
+        viewDrawText(4, gTempStr, x+76, y+1, -128, 11+col, 2, 0);
+    }
+}
+
+int *lensTable;
+
+int gZoom = 1024;
+
+int dword_172CE0[16][3];
+
+void viewInit(void)
+{
+    initprintf("Initializing status bar\n");
+    InitStatusBar();
+    FontSet(0, 4096, 0);
+    FontSet(1, 4192, 1);
+    FontSet(2, 4288, 1);
+    FontSet(3, 4384, 1);
+    FontSet(4, 4480, 0);
+
+    DICTNODE *hLens = gSysRes.Lookup("LENS", "DAT");
+    dassert(hLens != NULL);
+    dassert(gSysRes.Size(hLens) == kLensSize * kLensSize * sizeof(int));
+
+    lensTable = (int*)gSysRes.Lock(hLens);
+#if B_BIG_ENDIAN == 1
+    for (int i = 0; i < kLensSize*kLensSize; i++)
+    {
+        lensTable[i] = B_LITTLE32(lensTable[i]);
+    }
+#endif
+    char *data = tileAllocTile(4077, kLensSize, kLensSize, 0, 0);
+    memset(data, 255, kLensSize*kLensSize);
+    gGameMessageMgr.SetState(gMessageState);
+    gGameMessageMgr.SetCoordinates(1, 1);
+    char nFont;
+    if (gMessageFont == 0)
+        nFont = 3;
+    else
+        nFont = 0;
+
+    gGameMessageMgr.SetFont(nFont);
+    gGameMessageMgr.SetMaxMessages(gMessageCount);
+    gGameMessageMgr.SetMessageTime(gMessageTime);
+
+    for (int i = 0; i < 16; i++)
+    {
+        dword_172CE0[i][0] = mulscale16(wrand(), 2048);
+        dword_172CE0[i][1] = mulscale16(wrand(), 2048);
+        dword_172CE0[i][2] = mulscale16(wrand(), 2048);
+    }
+    gViewMap.sub_25C38(0, 0, gZoom, 0, gFollowMap);
+
+    g_frameDelay = calcFrameDelay(r_maxfps + r_maxfpsoffset);
+
+    bLoadScreenCrcMatch = tileCRC(kLoadScreen) == kLoadScreenCRC;
+}
+
+void viewResizeView(int size)
+{
+    int xdimcorrect = ClipHigh(scale(ydim, 4, 3), xdim);
+    gViewXCenter = xdim-xdim/2;
+    gViewYCenter = ydim-ydim/2;
+    xscale = divscale16(xdim, 320);
+    xscalecorrect = divscale16(xdimcorrect, 320);
+    yscale = divscale16(ydim, 200);
+    xstep = divscale16(320, xdim);
+    ystep = divscale16(200, ydim);
+    gViewSize = ClipRange(size, 0, 7);
+    if (gViewSize <= 2)
+    {
+        gViewX0 = 0;
+        gViewX1 = xdim-1;
+        gViewY0 = 0;
+        gViewY1 = ydim-1;
+        if (gGameOptions.nGameType > 0 && gGameOptions.nGameType < 3)
+        {
+            gViewY0 = (tilesiz[2229].y*ydim*((gNetPlayers+3)/4))/200;
+        }
+        gViewX0S = divscale16(gViewX0, xscalecorrect);
+        gViewY0S = divscale16(gViewY0, yscale);
+        gViewX1S = divscale16(gViewX1, xscalecorrect);
+        gViewY1S = divscale16(gViewY1, yscale);
+    }
+    else
+    {
+        gViewX0 = 0;
+        gViewY0 = 0;
+        gViewX1 = xdim-1;
+        gViewY1 = ydim-1-(25*ydim)/200;
+        if (gGameOptions.nGameType > 0 && gGameOptions.nGameType < 3)
+        {
+            gViewY0 = (tilesiz[2229].y*ydim*((gNetPlayers+3)/4))/200;
+        }
+
+        int height = gViewY1-gViewY0;
+        gViewX0 += mulscale16(xdim*(gViewSize-3),4096);
+        gViewX1 -= mulscale16(xdim*(gViewSize-3),4096);
+        gViewY0 += mulscale16(height*(gViewSize-3),4096);
+        gViewY1 -= mulscale16(height*(gViewSize-3),4096);
+        gViewX0S = divscale16(gViewX0, xscalecorrect);
+        gViewY0S = divscale16(gViewY0, yscale);
+        gViewX1S = divscale16(gViewX1, xscalecorrect);
+        gViewY1S = divscale16(gViewY1, yscale);
+    }
+    videoSetViewableArea(gViewX0, gViewY0, gViewX1, gViewY1);
+    gGameMessageMgr.SetCoordinates(gViewX0S + 1, gViewY0S + 1);
+    viewSetCrosshairColor(CrosshairColors.r, CrosshairColors.g, CrosshairColors.b);
+    viewUpdatePages();
+}
+
+#define kBackTile 253
+
+void UpdateFrame(void)
+{
+    viewTileSprite(kBackTile, 0, 0, 0, 0, xdim, gViewY0-3);
+    viewTileSprite(kBackTile, 0, 0, 0, gViewY1+4, xdim, ydim);
+    viewTileSprite(kBackTile, 0, 0, 0, gViewY0-3, gViewX0-3, gViewY1+4);
+    viewTileSprite(kBackTile, 0, 0, gViewX1+4, gViewY0-3, xdim, gViewY1+4);
+
+    viewTileSprite(kBackTile, 20, 0, gViewX0-3, gViewY0-3, gViewX0, gViewY1+1);
+    viewTileSprite(kBackTile, 20, 0, gViewX0, gViewY0-3, gViewX1+4, gViewY0);
+    viewTileSprite(kBackTile, 10, 1, gViewX1+1, gViewY0, gViewX1+4, gViewY1+4);
+    viewTileSprite(kBackTile, 10, 1, gViewX0-3, gViewY1+1, gViewX1+1, gViewY1+4);
+}
+
+void viewDrawInterface(int arg)
+{
+    if (gViewMode == 3/* && gViewSize >= 3*/ && (pcBackground != 0 || videoGetRenderMode() >= REND_POLYMOST))
+    {
+        UpdateFrame();
+        pcBackground--;
+    }
+    UpdateStatusBar(arg);
+}
+
+uspritetype *viewInsertTSprite(int nSector, int nStatnum, uspritetype *pSprite)
+{
+    int nTSprite = spritesortcnt;
+    uspritetype *pTSprite = &tsprite[nTSprite];
+    memset(pTSprite, 0, sizeof(uspritetype));
+    pTSprite->cstat = 128;
+    pTSprite->xrepeat = 64;
+    pTSprite->yrepeat = 64;
+    pTSprite->owner = -1;
+    pTSprite->extra = -1;
+    pTSprite->type = -spritesortcnt;
+    pTSprite->statnum = nStatnum;
+    pTSprite->sectnum = nSector;
+    spritesortcnt++;
+    if (pSprite)
+    {
+        pTSprite->x = pSprite->x;
+        pTSprite->y = pSprite->y;
+        pTSprite->z = pSprite->z;
+        pTSprite->owner = pSprite->owner;
+        pTSprite->ang = pSprite->ang;
+    }
+    if (videoGetRenderMode() >= REND_POLYMOST)
+    {
+        int nAngle = getangle(pTSprite->x-gView->pSprite->x, pTSprite->y-gView->pSprite->y);
+        pTSprite->x += Cos(nAngle)>>25;
+        pTSprite->y += Sin(nAngle)>>25;
+    }
+    return &tsprite[nTSprite];
+}
+
+int effectDetail[] = {
+    4, 4, 4, 4, 0, 0, 0, 0, 0, 1, 4, 4, 0, 0, 0, 1, 0, 0, 0
+};
+
+uspritetype *viewAddEffect(int nTSprite, VIEW_EFFECT nViewEffect)
+{
+    dassert(nViewEffect >= 0 && nViewEffect < kViewEffectMax);
+    uspritetype *pTSprite = &tsprite[nTSprite];
+    if (gDetail < effectDetail[nViewEffect] || nTSprite >= kMaxViewSprites) return NULL;
+    switch (nViewEffect)
+    {
+    case VIEW_EFFECT_18:
+        for (int i = 0; i < 16; i++)
+        {
+            uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+            int ang = (gFrameClock*2048)/120;
+            int nRand1 = dword_172CE0[i][0];
+            int nRand2 = dword_172CE0[i][1];
+            int nRand3 = dword_172CE0[i][2];
+            ang += nRand3;
+            int x = mulscale30(512, Cos(ang));
+            int y = mulscale30(512, Sin(ang));
+            int z = 0;
+            RotateYZ(&x, &y, &z, nRand1);
+            RotateXZ(&x, &y, &z, nRand2);
+            pNSprite->x = pTSprite->x + x;
+            pNSprite->y = pTSprite->y + y;
+            pNSprite->z = pTSprite->z + (z<<4);
+            pNSprite->picnum = 1720;
+            pNSprite->shade = -128;
+        }
+        break;
+    case VIEW_EFFECT_16:
+    case VIEW_EFFECT_17:
+    {
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->shade = -128;
+        pNSprite->pal = 0;
+        pNSprite->z = top;
+        if (nViewEffect == VIEW_EFFECT_16)
+            pNSprite->xrepeat = pNSprite->yrepeat = 24;
+        else
+            pNSprite->xrepeat = pNSprite->yrepeat = 64;
+        pNSprite->picnum = 3558;
+        return pNSprite;
+    }
+    case VIEW_EFFECT_15:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->z = pTSprite->z;
+        pNSprite->cstat |= 2;
+        pNSprite->shade = -128;
+        pNSprite->xrepeat = pTSprite->xrepeat;
+        pNSprite->yrepeat = pTSprite->yrepeat;
+        pNSprite->picnum = 2135;
+        break;
+    }
+    case VIEW_EFFECT_14:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->shade = -128;
+        pNSprite->pal = 0;
+        pNSprite->xrepeat = pNSprite->yrepeat = 64;
+        pNSprite->picnum = 2605;
+        return pNSprite;
+    }
+    case VIEW_EFFECT_13:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->shade = 26;
+        pNSprite->pal = 0;
+        pNSprite->cstat |= 2;
+        pNSprite->xrepeat = pNSprite->yrepeat = 64;
+        pNSprite->picnum = 2089;
+        break;
+    }
+    case VIEW_EFFECT_11:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        pNSprite->shade = 26;
+        pNSprite->pal = 0;
+        pNSprite->cstat |= 2;
+        pNSprite->xrepeat = pNSprite->yrepeat = 24;
+        pNSprite->picnum = 626;
+        pNSprite->z = top;
+        break;
+    }
+    case VIEW_EFFECT_10:
+    {
+        int nAng = pTSprite->ang;
+        if (pTSprite->cstat & 16)
+        {
+            nAng = (nAng+512)&2047;
+        }
+        else
+        {
+            nAng = (nAng+1024)&2047;
+        }
+        for (int i = 0; i < 5 && spritesortcnt < kMaxViewSprites; i++)
+        {
+            int nSector = pTSprite->sectnum;
+            uspritetype *pNSprite = viewInsertTSprite(nSector, 32767, NULL);
+            int nLen = 128+(i<<7);
+            int x = mulscale30(nLen, Cos(nAng));
+            pNSprite->x = pTSprite->x + x;
+            int y = mulscale30(nLen, Sin(nAng));
+            pNSprite->y = pTSprite->y + y;
+            pNSprite->z = pTSprite->z;
+            dassert(nSector >= 0 && nSector < kMaxSectors);
+            FindSector(pNSprite->x, pNSprite->y, pNSprite->z, &nSector);
+            pNSprite->sectnum = nSector;
+            pNSprite->owner = pTSprite->owner;
+            pNSprite->picnum = pTSprite->picnum;
+            pNSprite->cstat |= 2;
+            if (i < 2)
+                pNSprite->cstat |= 514;
+            pNSprite->shade = ClipLow(pTSprite->shade-16, -128);
+            pNSprite->xrepeat = pTSprite->xrepeat;
+            pNSprite->yrepeat = pTSprite->yrepeat;
+            pNSprite->picnum = pTSprite->picnum;
+        }
+        break;
+    }
+    case VIEW_EFFECT_8:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->shade = -128;
+        pNSprite->z = pTSprite->z;
+        pNSprite->picnum = 908;
+        pNSprite->statnum = 0;
+        pNSprite->xrepeat = pNSprite->yrepeat = (tilesiz[pTSprite->picnum].x*pTSprite->xrepeat)/64;
+        break;
+    }
+    case VIEW_EFFECT_6:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        pNSprite->z = top;
+        if (IsDudeSprite((spritetype *)pTSprite))
+            pNSprite->picnum = 672;
+        else
+            pNSprite->picnum = 754;
+        pNSprite->cstat |= 2;
+        pNSprite->shade = 8;
+        pNSprite->xrepeat = pTSprite->xrepeat;
+        pNSprite->yrepeat = pTSprite->yrepeat;
+        break;
+    }
+    case VIEW_EFFECT_7:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        pNSprite->z = bottom;
+        if (pTSprite->type >= kDudeBase && pTSprite->type < kDudeMax)
+            pNSprite->picnum = 672;
+        else
+            pNSprite->picnum = 754;
+        pNSprite->cstat |= 2;
+        pNSprite->shade = 8;
+        pNSprite->xrepeat = pTSprite->xrepeat;
+        pNSprite->yrepeat = pTSprite->yrepeat;
+        break;
+    }
+    case VIEW_EFFECT_4:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        pNSprite->z = top;
+        pNSprite->picnum = 2101;
+        pNSprite->shade = -128;
+        pNSprite->xrepeat = pNSprite->yrepeat = (tilesiz[pTSprite->picnum].x*pTSprite->xrepeat)/32;
+        break;
+    }
+    case VIEW_EFFECT_5:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        pNSprite->z = bottom;
+        pNSprite->picnum = 2101;
+        pNSprite->shade = -128;
+        pNSprite->xrepeat = pNSprite->yrepeat = (tilesiz[pTSprite->picnum].x*pTSprite->xrepeat)/32;
+        break;
+    }
+    case VIEW_EFFECT_0:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->z = getflorzofslope(pTSprite->sectnum, pNSprite->x, pNSprite->y);
+        pNSprite->shade = 127;
+        pNSprite->cstat |= 2;
+        pNSprite->xrepeat = pTSprite->xrepeat;
+        pNSprite->yrepeat = pTSprite->yrepeat>>2;
+        pNSprite->picnum = pTSprite->picnum;
+        pNSprite->pal = 5;
+        int height = tilesiz[pNSprite->picnum].y;
+        int center = height/2+picanm[pNSprite->picnum].yofs;
+        pNSprite->z -= (pNSprite->yrepeat<<2)*(height-center);
+        break;
+    }
+    case VIEW_EFFECT_1:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->shade = -128;
+        pNSprite->pal = 2;
+        pNSprite->cstat |= 2;
+        pNSprite->z = pTSprite->z;
+        pNSprite->xrepeat = pTSprite->xrepeat;
+        pNSprite->yrepeat = pTSprite->yrepeat;
+        pNSprite->picnum = 2427;
+        break;
+    }
+    case VIEW_EFFECT_2:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        sectortype *pSector = &sector[pTSprite->sectnum];
+        pNSprite->x = pTSprite->x;
+        pNSprite->y = pTSprite->y;
+        pNSprite->z = pSector->ceilingz;
+        pNSprite->picnum = 624;
+        pNSprite->shade = ((pTSprite->z-pSector->ceilingz)>>8)-64;
+        pNSprite->pal = 2;
+        pNSprite->xrepeat = pNSprite->yrepeat = 64;
+        pNSprite->cstat |= 106;
+        pNSprite->ang = pTSprite->ang;
+        pNSprite->owner = pTSprite->owner;
+        break;
+    }
+    case VIEW_EFFECT_3:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        sectortype *pSector = &sector[pTSprite->sectnum];
+        pNSprite->x = pTSprite->x;
+        pNSprite->y = pTSprite->y;
+        pNSprite->z = pSector->floorz;
+        pNSprite->picnum = 624;
+        char nShade = (pSector->floorz-pTSprite->z)>>8; 
+        pNSprite->shade = nShade-32;
+        pNSprite->pal = 2;
+        pNSprite->xrepeat = pNSprite->yrepeat = nShade;
+        pNSprite->cstat |= 98;
+        pNSprite->ang = pTSprite->ang;
+        pNSprite->owner = pTSprite->owner;
+        break;
+    }
+    case VIEW_EFFECT_9:
+    {
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        pNSprite->z = pTSprite->z;
+        if (gDetail > 1)
+            pNSprite->cstat |= 514;
+        pNSprite->shade = ClipLow(pTSprite->shade-32, -128);
+        pNSprite->xrepeat = pTSprite->xrepeat;
+        pNSprite->yrepeat = 64;
+        pNSprite->picnum = 775;
+        break;
+    }
+    case VIEW_EFFECT_12:
+    {
+        dassert(pTSprite->type >= kDudePlayer1 && pTSprite->type <= kDudePlayer8);
+        PLAYER *pPlayer = &gPlayer[pTSprite->type-kDudePlayer1];
+        if (gWeaponIcon[pPlayer->atbd].nTile < 0) break;
+        uspritetype *pNSprite = viewInsertTSprite(pTSprite->sectnum, 32767, pTSprite);
+        int top, bottom;
+        GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+        pNSprite->x = pTSprite->x;
+        pNSprite->y = pTSprite->y;
+        pNSprite->z = pTSprite->z-(32<<8);
+        pNSprite->picnum = gWeaponIcon[pPlayer->atbd].nTile;
+        pNSprite->shade = pTSprite->shade;
+        pNSprite->xrepeat = gWeaponIcon[pPlayer->atbd].xRepeat;
+        pNSprite->yrepeat = gWeaponIcon[pPlayer->atbd].yRepeat;
+        break;
+    }
+    }
+    return NULL;
+}
+
+LOCATION gPrevSpriteLoc[kMaxSprites];
+
+void viewProcessSprites(int cX, int cY, int cZ)
+{
+    dassert(spritesortcnt <= kMaxViewSprites);
+    int nViewSprites = spritesortcnt;
+    for (int nTSprite = nViewSprites-1; nTSprite >= 0; nTSprite--)
+    {
+        uspritetype *pTSprite = &tsprite[nTSprite];
+        //int nXSprite = pTSprite->extra;
+        int nXSprite = sprite[pTSprite->owner].extra;
+        XSPRITE *pTXSprite = NULL;
+        if (qsprite_filler[pTSprite->owner] > gDetail)
+        {
+            pTSprite->xrepeat = 0;
+            continue;
+        }
+        if (nXSprite > 0)
+        {
+            pTXSprite = &xsprite[nXSprite];
+        }
+        int nTile = pTSprite->picnum;
+        if (nTile < 0 || nTile >= kMaxTiles)
+        {
+            continue;
+        }
+
+        int nSprite = pTSprite->owner;
+        if (gViewInterpolate && TestBitString(gInterpolateSprite, nSprite) && !(pTSprite->hitag&512))
+        {
+            LOCATION *pPrevLoc = &gPrevSpriteLoc[nSprite];
+            pTSprite->x = interpolate(pPrevLoc->x, pTSprite->x, gInterpolate);
+            pTSprite->y = interpolate(pPrevLoc->y, pTSprite->y, gInterpolate);
+            pTSprite->z = interpolate(pPrevLoc->z, pTSprite->z, gInterpolate);
+            pTSprite->ang = pPrevLoc->ang+mulscale16(((pTSprite->ang-pPrevLoc->ang+1024)&2047)-1024, gInterpolate);
+        }
+        int nAnim = 0;
+        switch (picanm[nTile].extra&7)
+        {
+            case 0:
+                if (nXSprite > 0)
+                {
+                    dassert(nXSprite < kMaxXSprites);
+                    switch (pTSprite->type)
+                    {
+                    case 20:
+                    case 21:
+                        if (xsprite[nXSprite].state)
+                        {
+                            nAnim = 1;
+                        }
+                        break;
+                    case 22:
+                        nAnim = xsprite[nXSprite].data1;
+                        break;
+                    }
+                }
+                break;
+            case 1:
+            {
+#ifdef USE_OPENGL
+                if (videoGetRenderMode() >= REND_POLYMOST && usemodels && md_tilehasmodel(pTSprite->picnum, pTSprite->pal) >= 0 && !(spriteext[nSprite].flags&SPREXT_NOTMD))
+                {
+                    pTSprite->cstat &= ~4;
+                    break;
+                }
+#endif
+                int dX = cX - pTSprite->x;
+                int dY = cY - pTSprite->y;
+                RotateVector(&dX, &dY, 128-pTSprite->ang);
+                nAnim = GetOctant(dX, dY);
+                if (nAnim <= 4)
+                {
+                    pTSprite->cstat &= ~4;
+                }
+                else
+                {
+                    nAnim = 8 - nAnim;
+                    pTSprite->cstat |= 4;
+                }
+                break;
+            }
+            case 2:
+            {
+#ifdef USE_OPENGL
+                if (videoGetRenderMode() >= REND_POLYMOST && usemodels && md_tilehasmodel(pTSprite->picnum, pTSprite->pal) >= 0 && !(spriteext[nSprite].flags&SPREXT_NOTMD))
+                {
+                    pTSprite->cstat &= ~4;
+                    break;
+                }
+#endif
+                int dX = cX - pTSprite->x;
+                int dY = cY - pTSprite->y;
+                RotateVector(&dX, &dY, 128-pTSprite->ang);
+                nAnim = GetOctant(dX, dY);
+                break;
+            }
+            case 3:
+            {
+                if (nXSprite > 0)
+                {
+                    if (gSpriteHit[nXSprite].florhit == 0)
+                        nAnim = 1;
+                }
+                else
+                {
+                    int top, bottom;
+                    GetSpriteExtents((spritetype *)pTSprite, &top, &bottom);
+                    if (getflorzofslope(pTSprite->sectnum, pTSprite->x, pTSprite->y) > bottom)
+                        nAnim = 1;
+                }
+                break;
+            }
+            case 6:
+            case 7:
+            {
+#ifdef USE_OPENGL
+                if (videoGetRenderMode() >= REND_POLYMOST && usemodels && md_tilehasmodel(pTSprite->picnum, pTSprite->pal) >= 0 && !(spriteext[nSprite].flags&SPREXT_NOTMD))
+                    break;
+#endif
+                // Can be overridden by def script
+                if (usevoxels && gDetail >= 4 && videoGetRenderMode() != REND_POLYMER && tiletovox[pTSprite->picnum] == -1 && voxelIndex[pTSprite->picnum] != -1)
+                {
+                    if ((pTSprite->hitag&16) == 0)
+                    {
+                        pTSprite->cstat |= 48;
+                        pTSprite->yoffset += picanm[pTSprite->picnum].yofs;
+                        pTSprite->picnum = voxelIndex[pTSprite->picnum];
+                        if (!voxoff[pTSprite->picnum])
+                            qloadvoxel(pTSprite->picnum);
+                        if ((picanm[nTile].extra&7) == 7)
+                        {
+                            pTSprite->ang = (gGameClock<<3)&2047;
+                        }
+                    }
+                }
+                break;
+            }
+        }
+        while (nAnim > 0)
+        {
+            pTSprite->picnum += picanm[pTSprite->picnum].num+1;
+            nAnim--;
+        }
+
+        if (usevoxels && videoGetRenderMode() != REND_POLYMER && tiletovox[pTSprite->picnum] != -1)
+        {
+            pTSprite->yoffset += picanm[pTSprite->picnum].yofs;
+        }
+
+        sectortype *pSector = &sector[pTSprite->sectnum];
+        XSECTOR *pXSector;
+        int nShade = pTSprite->shade;
+        if (pSector->extra)
+        {
+            pXSector = &xsector[pSector->extra];
+        }
+        else
+        {
+            pXSector = NULL;
+        }
+        if ((pSector->ceilingstat&1) && (pSector->floorstat&32768) == 0)
+        {
+            nShade += tileShade[pSector->ceilingpicnum]+pSector->ceilingshade;
+        }
+        else
+        {
+            nShade += tileShade[pSector->floorpicnum]+pSector->floorshade;
+        }
+        nShade += tileShade[pTSprite->picnum];
+        pTSprite->shade = ClipRange(nShade, -128, 127);
+        if ((pTSprite->hitag&16) && sprite[pTSprite->owner].owner == 3)
+        {
+            dassert(pTXSprite != NULL);
+            pTSprite->xrepeat = 48;
+            pTSprite->yrepeat = 48;
+            pTSprite->shade = -128;
+            pTSprite->picnum = 2272 + 2*pTXSprite->respawnPending;
+            pTSprite->cstat &= ~514;
+            if (((IsItemSprite((spritetype *)pTSprite) || IsAmmoSprite((spritetype *)pTSprite)) && gGameOptions.nItemSettings == 2)
+                || (IsWeaponSprite((spritetype *)pTSprite) && gGameOptions.nWeaponSettings == 3))
+            {
+                pTSprite->xrepeat = pTSprite->yrepeat = 48;
+            }
+            else
+            {
+                pTSprite->xrepeat = pTSprite->yrepeat = 0;
+            }
+        }
+        if (spritesortcnt >= kMaxViewSprites) continue;
+        if (pTXSprite && pTXSprite->burnTime > 0)
+        {
+            pTSprite->shade = ClipRange(pTSprite->shade-16-QRandom(8), -128, 127);
+        }
+        if (pTSprite->hitag&256)
+        {
+            viewAddEffect(nTSprite, VIEW_EFFECT_6);
+        }
+        if (pTSprite->hitag&1024)
+        {
+            pTSprite->cstat |= 4;
+        }
+        if (pTSprite->hitag&2048)
+        {
+            pTSprite->cstat |= 8;
+        }
+        switch (pTSprite->statnum)
+        {
+        case 0:
+        {
+            switch (pTSprite->type)
+            {
+            case 32:
+                if (pTXSprite)
+                {
+                    if (pTXSprite->state > 0)
+                    {
+                        pTSprite->shade = -128;
+                        viewAddEffect(nTSprite, VIEW_EFFECT_11);
+                    }
+                    else
+                    {
+                        pTSprite->shade = -8;
+                    }
+                }
+                else
+                {
+                    pTSprite->shade = -128;
+                    viewAddEffect(nTSprite, VIEW_EFFECT_11);
+                }
+                break;
+            case 30:
+                if (pTXSprite)
+                {
+                    if (pTXSprite->state > 0)
+                    {
+                        pTSprite->picnum++;
+                        viewAddEffect(nTSprite, VIEW_EFFECT_4);
+                    }
+                    else
+                    {
+                        viewAddEffect(nTSprite, VIEW_EFFECT_6);
+                    }
+                }
+                else
+                {
+                    pTSprite->picnum++;
+                    viewAddEffect(nTSprite, VIEW_EFFECT_4);
+                }
+                break;
+            default:
+                if (pXSector && pXSector->color)
+                {
+                    pTSprite->pal = pSector->floorpal;
+                }
+                break;
+            }
+            break;
+        }
+        case 3:
+        {
+            switch (pTSprite->type)
+            {
+            case 145:
+                if (pTXSprite && pTXSprite->state > 0 && gGameOptions.nGameType == 3)
+                {
+                    uspritetype *pNTSprite = viewAddEffect(nTSprite, VIEW_EFFECT_17);
+                    if (pNTSprite)
+                        pNTSprite->pal = 10;
+                }
+                break;
+            case 146:
+                if (pTXSprite && pTXSprite->state > 0 && gGameOptions.nGameType == 3)
+                {
+                    uspritetype *pNTSprite = viewAddEffect(nTSprite, VIEW_EFFECT_17);
+                    if (pNTSprite)
+                        pNTSprite->pal = 7;
+                }
+                break;
+            case 147:
+                pTSprite->pal = 10;
+                pTSprite->cstat |= 1024;
+                break;
+            case 148:
+                pTSprite->pal = 7;
+                pTSprite->cstat |= 1024;
+                break;
+            default:
+                if (pTSprite->type >= 100 && pTSprite->type <= 106)
+                    pTSprite->shade = -128;
+                if (pXSector && pXSector->color)
+                {
+                    pTSprite->pal = pSector->floorpal;
+                }
+                break;
+            }
+            break;
+        }
+        case 5:
+        {
+            switch (pTSprite->type)
+            {
+            case 302:
+                pTSprite->yrepeat = 128;
+                pTSprite->cstat |= 32;
+                break;
+            case 306:
+                viewAddEffect(nTSprite, VIEW_EFFECT_15);
+                break;
+            case 300:
+                viewAddEffect(nTSprite, VIEW_EFFECT_10);
+                break;
+            case 301:
+            case 303:
+                if (pTSprite->statnum == 14)
+                {
+                    dassert(pTXSprite != NULL);
+                    if (pTXSprite->target == gView->at5b)
+                    {
+                        pTSprite->xrepeat = 0;
+                        break;
+                    }
+                }
+                viewAddEffect(nTSprite, VIEW_EFFECT_1);
+                if (pTSprite->type == 301)
+                {
+                    sectortype *pSector = &sector[pTSprite->sectnum];
+                    int zDiff = (pTSprite->z-pSector->ceilingz)>>8;
+                    if ((pSector->ceilingstat&1) == 0 && zDiff < 64)
+                    {
+                        viewAddEffect(nTSprite, VIEW_EFFECT_2);
+                    }
+                    zDiff = (pSector->floorz-pTSprite->z)>>8;
+                    if ((pSector->floorstat&1) == 0 && zDiff < 64)
+                    {
+                        viewAddEffect(nTSprite, VIEW_EFFECT_3);
+                    }
+                }
+                break;
+            }
+            break;
+        }
+        case 6:
+        {
+            if (pTSprite->type == 212 && pTXSprite->aiState == &hand13A3B4)
+            {
+                spritetype *pTTarget = &sprite[pTXSprite->target];
+                dassert(pTXSprite != NULL && pTTarget != NULL);
+                if (IsPlayerSprite(pTTarget))
+                {
+                    pTSprite->xrepeat = 0;
+                    break;
+                }
+            }
+            if (pXSector && pXSector->color)
+            {
+                pTSprite->pal = pSector->floorpal;
+            }
+            if (powerupCheck(gView, 25) > 0)
+            {
+                pTSprite->shade = -128;
+            }
+            if (IsPlayerSprite((spritetype *)pTSprite))
+            {
+                PLAYER *pPlayer = &gPlayer[pTSprite->type-kDudePlayer1];
+                if (powerupCheck(pPlayer, 13) && !powerupCheck(gView, 25))
+                {
+                    pTSprite->cstat |= 2;
+                    pTSprite->pal = 5;
+                }
+                else if (powerupCheck(pPlayer, 14))
+                {
+                    pTSprite->shade = -128;
+                    pTSprite->pal = 5;
+                }
+                else if (powerupCheck(pPlayer, 23))
+                {
+                    pTSprite->pal = 11+(gView->at2ea&3);
+                }
+                if (powerupCheck(pPlayer, 24))
+                {
+                    viewAddEffect(nTSprite, VIEW_EFFECT_13);
+                }
+                if (gShowWeapon && gGameOptions.nGameType > 0 && gView)
+                {
+                    viewAddEffect(nTSprite, VIEW_EFFECT_12);
+                }
+                if (pPlayer->at37b && (gView != pPlayer || gViewPos != VIEWPOS_0))
+                {
+                    uspritetype *pNTSprite = viewAddEffect(nTSprite, VIEW_EFFECT_14);
+                    if (pNTSprite)
+                    {
+                        POSTURE *pPosture = &gPosture[pPlayer->at5f][pPlayer->at2f];
+                        pNTSprite->x += mulscale28(pPosture->at30, Cos(pTSprite->ang));
+                        pNTSprite->y += mulscale28(pPosture->at30, Sin(pTSprite->ang));
+                        pNTSprite->z = pPlayer->pSprite->z-pPosture->at2c;
+                    }
+                }
+                if (pPlayer->at90 > 0 && gGameOptions.nGameType == 3)
+                {
+                    if (pPlayer->at90&1)
+                    {
+                        uspritetype *pNTSprite = viewAddEffect(nTSprite, VIEW_EFFECT_16);
+                        if (pNTSprite)
+                        {
+                            pNTSprite->pal = 10;
+                            pNTSprite->cstat |= 4;
+                        }
+                    }
+                    if (pPlayer->at90&2)
+                    {
+                        uspritetype *pNTSprite = viewAddEffect(nTSprite, VIEW_EFFECT_16);
+                        if (pNTSprite)
+                        {
+                            pNTSprite->pal = 7;
+                            pNTSprite->cstat |= 4;
+                        }
+                    }
+                }
+            }
+            if (pTSprite->owner != gView->pSprite->index || gViewPos != VIEWPOS_0)
+            {
+                if (getflorzofslope(pTSprite->sectnum, pTSprite->x, pTSprite->y) >= cZ)
+                {
+                    viewAddEffect(nTSprite, VIEW_EFFECT_0);
+                }
+            }
+            break;
+        }
+        case 11:
+        {
+            if (pTSprite->type == 454)
+            {
+                if (pTXSprite->state)
+                {
+                    if (pTXSprite->data1)
+                    {
+                        pTSprite->picnum = 772;
+                        if (pTXSprite->data2)
+                        {
+                            viewAddEffect(nTSprite, VIEW_EFFECT_9);
+                        }
+                    }
+                }
+                else
+                {
+                    if (pTXSprite->data1)
+                    {
+                        pTSprite->picnum = 773;
+                    }
+                    else
+                    {
+                        pTSprite->picnum = 656;
+                    }
+                }
+            }
+            break;
+        }
+        case 4:
+        {
+            if (pXSector && pXSector->color)
+            {
+                pTSprite->pal = pSector->floorpal;
+            }
+            if (pTSprite->hitag&1)
+            {
+                if (getflorzofslope(pTSprite->sectnum, pTSprite->x, pTSprite->y) >= cZ)
+                {
+                    if (pTSprite->type < 400 || pTSprite->type >= 433 || !gSpriteHit[nXSprite].florhit)
+                    {
+                        viewAddEffect(nTSprite, VIEW_EFFECT_0);
+                    }
+                }
+            }
+            break;
+        }
+        }
+    }
+
+    for (int nTSprite = spritesortcnt-1; nTSprite >= nViewSprites; nTSprite--)
+    {
+        uspritetype *pTSprite = &tsprite[nTSprite];
+        int nAnim = 0;
+        switch (picanm[pTSprite->picnum].extra&7)
+        {
+            case 1:
+            {
+                int dX = cX - pTSprite->x;
+                int dY = cY - pTSprite->y;
+                RotateVector(&dX, &dY, 128-pTSprite->ang);
+                nAnim = GetOctant(dX, dY);
+                if (nAnim <= 4)
+                {
+                    pTSprite->cstat &= ~4;
+                }
+                else
+                {
+                    nAnim = 8 - nAnim;
+                    pTSprite->cstat |= 4;
+                }
+                break;
+            }
+            case 2:
+            {
+                int dX = cX - pTSprite->x;
+                int dY = cY - pTSprite->y;
+                RotateVector(&dX, &dY, 128-pTSprite->ang);
+                nAnim = GetOctant(dX, dY);
+                break;
+            }
+        }
+        while (nAnim > 0)
+        {
+            pTSprite->picnum += picanm[pTSprite->picnum].num+1;
+            nAnim--;
+        }
+    }
+}
+
+int othercameradist = 1280;
+int cameradist = -1;
+int othercameraclock;
+int cameraclock;
+
+void CalcOtherPosition(spritetype *pSprite, int *pX, int *pY, int *pZ, int *vsectnum, int nAng, fix16_t zm)
+{
+    int vX = mulscale30(-Cos(nAng), 1280);
+    int vY = mulscale30(-Sin(nAng), 1280);
+    int vZ = fix16_to_int(mulscale(zm, 1280, 3))-(16<<8);
+    int bakCstat = pSprite->cstat;
+    pSprite->cstat &= ~256;
+    dassert(*vsectnum >= 0 && *vsectnum < kMaxSectors);
+    FindSector(*pX, *pY, *pZ, vsectnum);
+    short nHSector;
+    int hX, hY;
+    vec3_t pos = {*pX, *pY, *pZ};
+    hitdata_t hitdata;
+    hitscan(&pos, *vsectnum, vX, vY, vZ, &hitdata, CLIPMASK1);
+    nHSector = hitdata.sect;
+    hX = hitdata.pos.x;
+    hY = hitdata.pos.y;
+    int dX = hX-*pX;
+    int dY = hY-*pY;
+    if (klabs(vX)+klabs(vY) > klabs(dX)+klabs(dY))
+    {
+        *vsectnum = nHSector;
+        dX -= ksgn(vX)<<6;
+        dY -= ksgn(vY)<<6;
+        int nDist;
+        if (klabs(vX) > klabs(vY))
+        {
+            nDist = ClipHigh(divscale16(dX,vX), othercameradist);
+        }
+        else
+        {
+            nDist = ClipHigh(divscale16(dY,vY), othercameradist);
+        }
+        othercameradist = nDist;
+    }
+    *pX += mulscale16(vX, othercameradist);
+    *pY += mulscale16(vY, othercameradist);
+    *pZ += mulscale16(vZ, othercameradist);
+    othercameradist = ClipHigh(othercameradist+((gGameClock-othercameraclock)<<10), 65536);
+    othercameraclock = gGameClock;
+    dassert(*vsectnum >= 0 && *vsectnum < kMaxSectors);
+    FindSector(*pX, *pY, *pZ, vsectnum);
+    pSprite->cstat = bakCstat;
+}
+
+void CalcPosition(spritetype *pSprite, int *pX, int *pY, int *pZ, int *vsectnum, int nAng, fix16_t zm)
+{
+    int vX = mulscale30(-Cos(nAng), 1280);
+    int vY = mulscale30(-Sin(nAng), 1280);
+    int vZ = fix16_to_int(mulscale(zm, 1280, 3))-(16<<8);
+    int bakCstat = pSprite->cstat;
+    pSprite->cstat &= ~256;
+    dassert(*vsectnum >= 0 && *vsectnum < kMaxSectors);
+    FindSector(*pX, *pY, *pZ, vsectnum);
+    short nHSector;
+    int hX, hY;
+    hitscangoal.x = hitscangoal.y = 0x1fffffff;
+    vec3_t pos = { *pX, *pY, *pZ };
+    hitdata_t hitdata;
+    hitscan(&pos, *vsectnum, vX, vY, vZ, &hitdata, CLIPMASK1);
+    nHSector = hitdata.sect;
+    hX = hitdata.pos.x;
+    hY = hitdata.pos.y;
+    int dX = hX-*pX;
+    int dY = hY-*pY;
+    if (klabs(vX)+klabs(vY) > klabs(dX)+klabs(dY))
+    {
+        *vsectnum = nHSector;
+        dX -= ksgn(vX)<<6;
+        dY -= ksgn(vY)<<6;
+        int nDist;
+        if (klabs(vX) > klabs(vY))
+        {
+            nDist = ClipHigh(divscale16(dX,vX), cameradist);
+        }
+        else
+        {
+            nDist = ClipHigh(divscale16(dY,vY), cameradist);
+        }
+        cameradist = nDist;
+    }
+    *pX += mulscale16(vX, cameradist);
+    *pY += mulscale16(vY, cameradist);
+    *pZ += mulscale16(vZ, cameradist);
+    cameradist = ClipHigh(cameradist+((gGameClock-cameraclock)<<10), 65536);
+    cameraclock = gGameClock;
+    dassert(*vsectnum >= 0 && *vsectnum < kMaxSectors);
+    FindSector(*pX, *pY, *pZ, vsectnum);
+    pSprite->cstat = bakCstat;
+}
+
+struct {
+    short nTile;
+    unsigned char nStat;
+    unsigned char nPal;
+    int nScale;
+    short nX, nY;
+} burnTable[9] = {
+     { 2101, 2, 0, 118784, 10, 220 },
+     { 2101, 2, 0, 110592, 40, 220 },
+     { 2101, 2, 0, 81920, 85, 220 },
+     { 2101, 2, 0, 69632, 120, 220 },
+     { 2101, 2, 0, 61440, 160, 220 },
+     { 2101, 2, 0, 73728, 200, 220 },
+     { 2101, 2, 0, 77824, 235, 220 },
+     { 2101, 2, 0, 110592, 275, 220 },
+     { 2101, 2, 0, 122880, 310, 220 }
+};
+
+void viewBurnTime(int gScale)
+{
+    if (!gScale) return;
+
+    for (int i = 0; i < 9; i++)
+    {
+        int nTile = burnTable[i].nTile+qanimateoffs(burnTable[i].nTile,32768+i);
+        int nScale = burnTable[i].nScale;
+        if (gScale < 600)
+        {
+            nScale = scale(nScale, gScale, 600);
+        }
+        rotatesprite(burnTable[i].nX<<16, burnTable[i].nY<<16, nScale, 0, nTile,
+            0, burnTable[i].nPal, burnTable[i].nStat, windowxy1.x, windowxy1.y, windowxy2.x, windowxy2.y);
+    }
+}
+
+void viewSetMessage(const char *pMessage)
+{
+    OSD_Printf("%s\n", pMessage);
+    gGameMessageMgr.Add(pMessage, 15);
+}
+
+void viewDisplayMessage(void)
+{
+    gGameMessageMgr.Display();
+}
+
+char errMsg[256];
+
+void viewSetErrorMessage(const char *pMessage)
+{
+    if (!pMessage)
+    {
+        strcpy(errMsg, "");
+    }
+    else
+    {
+        strcpy(errMsg, pMessage);
+    }
+}
+
+void DoLensEffect(void)
+{
+    char *d = (char*)waloff[4077];
+    dassert(d != NULL);
+    char *s = (char*)waloff[4079];
+    dassert(s != NULL);
+    for (int i = 0; i < kLensSize*kLensSize; i++, d++)
+        if (lensTable[i] >= 0)
+            *d = s[lensTable[i]];
+    tileInvalidate(4077, -1, -1);
+}
+
+void UpdateDacs(int nPalette, bool bNoTint)
+{
+    static RGB newDAC[256];
+    static int oldPalette;
+    if (oldPalette != nPalette)
+    {
+        scrSetPalette(nPalette);
+        oldPalette = nPalette;
+    }
+
+#ifdef USE_OPENGL
+    if (videoGetRenderMode() >= REND_POLYMOST)
+    {
+        gLastPal = 0;
+        polytint_t *tint = &hictinting[MAXPALOOKUPS-1];
+        int nRed = 0;
+        int nGreen = 0;
+        int nBlue = 0;
+        tint->f = 0;
+        switch (nPalette)
+        {
+        case 0:
+        default:
+            tint->r = 255;
+            tint->g = 255;
+            tint->b = 255;
+            break;
+        case 1:
+            tint->r = 132;
+            tint->g = 164;
+            tint->b = 255;
+            break;
+        case 2:
+            tint->r = 255;
+            tint->g = 126;
+            tint->b = 105;
+            break;
+        case 3:
+            tint->r = 162;
+            tint->g = 186;
+            tint->b = 15;
+            break;
+        case 4:
+            tint->r = 255;
+            tint->g = 255;
+            tint->b = 255;
+            break;
+        }
+        if (!bNoTint)
+        {
+            nRed += gView->at377;
+            nGreen += gView->at377;
+            nBlue -= gView->at377;
+
+            nRed += ClipHigh(gView->at366, 85)*2;
+            nGreen -= ClipHigh(gView->at366, 85)*3;
+            nBlue -= ClipHigh(gView->at366, 85)*3;
+
+            nRed -= gView->at36a;
+            nGreen -= gView->at36a;
+            nBlue -= gView->at36a;
+
+            nRed -= gView->at36e>>6;
+            nGreen -= gView->at36e>>5;
+            nBlue -= gView->at36e>>6;
+        }
+        nRed = ClipRange(nRed, -255, 255);
+        nGreen = ClipRange(nGreen, -255, 255);
+        nBlue = ClipRange(nBlue, -255, 255);
+
+        videoSetPalette(0, nPalette, 2);
+        videoTintBlood(nRed, nGreen, nBlue);
+    }
+    else
+#endif
+    {
+        gLastPal = nPalette;
+        if (bNoTint)
+        {
+            memcpy(newDAC, baseDAC, sizeof(newDAC));
+        }
+        else
+        {
+            for (int i = 0; i < 256; i++)
+            {
+                int nRed = baseDAC[i].red;
+                int nGreen = baseDAC[i].green;
+                int nBlue = baseDAC[i].blue;
+                nRed += gView->at377;
+                nGreen += gView->at377;
+                nBlue -= gView->at377;
+
+                nRed += ClipHigh(gView->at366, 85)*2;
+                nGreen -= ClipHigh(gView->at366, 85)*3;
+                nBlue -= ClipHigh(gView->at366, 85)*3;
+
+                nRed -= gView->at36a;
+                nGreen -= gView->at36a;
+                nBlue -= gView->at36a;
+
+                nRed -= gView->at36e>>6;
+                nGreen -= gView->at36e>>5;
+                nBlue -= gView->at36e>>6;
+
+                newDAC[i].red = ClipRange(nRed, 0, 255);
+                newDAC[i].green = ClipRange(nGreen, 0, 255);
+                newDAC[i].blue = ClipRange(nBlue, 0, 255);
+            }
+        }
+        if (memcmp(newDAC, curDAC, 768) != 0)
+        {
+            memcpy(curDAC, newDAC, 768);
+            gSetDacRange(0, 256, curDAC);
+        }
+    }
+}
+
+bool gPrediction = true;
+
+char otherMirrorGotpic[2];
+char bakMirrorGotpic[2];
+// int gVisibility;
+
+int deliriumTilt, deliriumTurn, deliriumPitch;
+int gScreenTiltO, deliriumTurnO, deliriumPitchO;
+
+int gShowFrameRate = 1;
+
+void viewUpdateDelirium(void)
+{
+    gScreenTiltO = gScreenTilt;
+    deliriumTurnO = deliriumTurn;
+    deliriumPitchO = deliriumPitch;
+	int powerCount;
+	if ((powerCount = powerupCheck(gView,28)) != 0)
+	{
+		int tilt1 = 170, tilt2 = 170, pitch = 20;
+        int timer = gFrameClock*4;
+		if (powerCount < 512)
+		{
+			int powerScale = (powerCount<<16) / 512;
+			tilt1 = mulscale16(tilt1, powerScale);
+			tilt2 = mulscale16(tilt2, powerScale);
+			pitch = mulscale16(pitch, powerScale);
+		}
+		int sin2 = costable[(2*timer-512)&2047] / 2;
+		int sin3 = costable[(3*timer-512)&2047] / 2;
+		gScreenTilt = mulscale30(sin2+sin3,tilt1);
+		int sin4 = costable[(4*timer-512)&2047] / 2;
+		deliriumTurn = mulscale30(sin3+sin4,tilt2);
+		int sin5 = costable[(5*timer-512)&2047] / 2;
+		deliriumPitch = mulscale30(sin4+sin5,pitch);
+		return;
+	}
+	gScreenTilt = ((gScreenTilt+1024)&2047)-1024;
+	if (gScreenTilt > 0)
+	{
+		gScreenTilt -= 8;
+		if (gScreenTilt < 0)
+			gScreenTilt = 0;
+	}
+	else if (gScreenTilt < 0)
+	{
+		gScreenTilt += 8;
+		if (gScreenTilt >= 0)
+			gScreenTilt = 0;
+	}
+}
+
+int shakeHoriz, shakeAngle, shakeX, shakeY, shakeZ, shakeBobX, shakeBobY;
+
+void viewUpdateShake(void)
+{
+    shakeHoriz = 0;
+    shakeAngle = 0;
+    shakeX = 0;
+    shakeY = 0;
+    shakeZ = 0;
+    shakeBobX = 0;
+    shakeBobY = 0;
+    if (gView->at35a)
+    {
+        int nValue = ClipHigh(gView->at35a * 8, 2000);
+        shakeHoriz += QRandom2(nValue >> 8);
+        shakeAngle += QRandom2(nValue >> 8);
+        shakeX += QRandom2(nValue >> 4);
+        shakeY += QRandom2(nValue >> 4);
+        shakeZ += QRandom2(nValue);
+        shakeBobX += QRandom2(nValue);
+        shakeBobY += QRandom2(nValue);
+    }
+    if (gView->at37f)
+    {
+        int nValue = ClipHigh(gView->at37f * 8, 2000);
+        shakeHoriz += QRandom2(nValue >> 8);
+        shakeAngle += QRandom2(nValue >> 8);
+        shakeX += QRandom2(nValue >> 4);
+        shakeY += QRandom2(nValue >> 4);
+        shakeZ += QRandom2(nValue);
+        shakeBobX += QRandom2(nValue);
+        shakeBobY += QRandom2(nValue);
+    }
+}
+
+int32_t r_maxfps = 60;
+int32_t r_maxfpsoffset = 0;
+double g_frameDelay = 0.0;
+
+int viewFPSLimit(void)
+{
+    static auto nextPageTicks = (double)timerGetTicksU64();
+    static unsigned frameWaiting  = 0;
+
+    if (frameWaiting)
+    {
+        frameWaiting--;
+        videoNextPage();
+    }
+
+    auto const frameTicks = (double)timerGetTicksU64();
+
+    if (!r_maxfps || frameTicks >= nextPageTicks)
+    {
+        if (frameTicks >= nextPageTicks + g_frameDelay)
+            nextPageTicks = frameTicks;
+
+        nextPageTicks += g_frameDelay;
+        frameWaiting++;
+    }
+
+    return frameWaiting;
+}
+
+float r_ambientlight = 1.0, r_ambientlightrecip = 1.0;
+
+int gLastPal = 0;
+
+int32_t g_frameRate;
+
+void viewDrawScreen(void)
+{
+    int nPalette = 0;
+    static int lastUpdate;
+    int defaultHoriz = gCenterHoriz ? 100 : 90;
+
+#ifdef USE_OPENGL
+    polymostcenterhoriz = defaultHoriz;
+#endif
+
+    timerUpdate();
+    int delta = ClipLow(gGameClock - lastUpdate, 0);
+    lastUpdate = gGameClock;
+    if (!gPaused && (!CGameMenuMgr::m_bActive || gGameOptions.nGameType != 0))
+    {
+        gInterpolate = divscale16(gGameClock-gNetFifoClock+4, 4);
+    }
+    if (gInterpolate < 0 || gInterpolate > 65536)
+    {
+        gInterpolate = ClipRange(gInterpolate, 0, 65536);
+    }
+    if (gViewInterpolate)
+    {
+        CalcInterpolations();
+    }
+
+    if (gViewMode == 3 || gViewMode == 4 || gOverlayMap)
+    {
+        DoSectorLighting();
+    }
+    if (gViewMode == 3 || gOverlayMap)
+    {
+        int yxAspect = yxaspect;
+        int viewingRange = viewingrange;
+        if (r_usenewaspect)
+        {
+            newaspect_enable = 1;
+            videoSetCorrectedAspect();
+        }
+        renderSetAspect(Blrintf(float(viewingrange) * tanf(gFov * (PI/360.f))), yxaspect);
+        int cX = gView->pSprite->x;
+        int cY = gView->pSprite->y;
+        int cZ = gView->at67;
+        int zDelta = gView->at6f-gView->at67-(12<<8);
+        fix16_t cA = gView->q16ang;
+        fix16_t q16horiz = gView->q16horiz;
+        fix16_t q16slopehoriz = gView->q16slopehoriz;
+        int v74 = gView->at43;
+        int v8c = gView->at3f;
+        int v4c = gView->at53;
+        int v48 = gView->at4f;
+        int nSectnum = gView->pSprite->sectnum;
+        if (gViewInterpolate)
+        {
+            if (numplayers > 1 && gView == gMe && gPrediction && gMe->pXSprite->health > 0)
+            {
+                nSectnum = predict.at68;
+                cX = interpolate(predictOld.at50, predict.at50, gInterpolate);
+                cY = interpolate(predictOld.at54, predict.at54, gInterpolate);
+                cZ = interpolate(predictOld.at38, predict.at38, gInterpolate);
+                zDelta = interpolate(predictOld.at34, predict.at34, gInterpolate);
+                cA = interpolateangfix16(predictOld.at30, predict.at30, gInterpolate);
+                q16horiz = interpolate(predictOld.at24, predict.at24, gInterpolate);
+                q16slopehoriz = interpolate(predictOld.at28, predict.at28, gInterpolate);
+                v74 = interpolate(predictOld.atc, predict.atc, gInterpolate);
+                v8c = interpolate(predictOld.at8, predict.at8, gInterpolate);
+                v4c = interpolate(predictOld.at1c, predict.at1c, gInterpolate);
+                v48 = interpolate(predictOld.at18, predict.at18, gInterpolate);
+            }
+            else
+            {
+                VIEW *pView = &gPrevView[gViewIndex];
+                cX = interpolate(pView->at50, cX, gInterpolate);
+                cY = interpolate(pView->at54, cY, gInterpolate);
+                cZ = interpolate(pView->at38, cZ, gInterpolate);
+                zDelta = interpolate(pView->at34, zDelta, gInterpolate);
+                cA = interpolateangfix16(pView->at30, cA, gInterpolate);
+                q16horiz = interpolate(pView->at24, q16horiz, gInterpolate);
+                q16slopehoriz = interpolate(pView->at28, q16slopehoriz, gInterpolate);
+                v74 = interpolate(pView->atc, v74, gInterpolate);
+                v8c = interpolate(pView->at8, v8c, gInterpolate);
+                v4c = interpolate(pView->at1c, v4c, gInterpolate);
+                v48 = interpolate(pView->at18, v48, gInterpolate);
+            }
+        }
+        viewUpdateShake();
+        q16horiz += fix16_from_int(shakeHoriz);
+        cA += fix16_from_int(shakeAngle);
+        cX += shakeX;
+        cY += shakeY;
+        cZ += shakeZ;
+        v4c += shakeBobX;
+        v48 += shakeBobY;
+        q16horiz += fix16_from_int(mulscale30(0x40000000-Cos(gView->at35e<<2), 30));
+        if (gViewPos == 0)
+        {
+            if (gViewHBobbing)
+            {
+                cX -= mulscale30(v74, Sin(fix16_to_int(cA)))>>4;
+                cY += mulscale30(v74, Cos(fix16_to_int(cA)))>>4;
+            }
+            if (gViewVBobbing)
+            {
+                cZ += v8c;
+            }
+            if (gSlopeTilting)
+            {
+                q16horiz += q16slopehoriz;
+            }
+            cZ += fix16_to_int(q16horiz*10);
+            cameradist = -1;
+            cameraclock = gGameClock;
+        }
+        else
+        {
+            CalcPosition(gView->pSprite, (int*)&cX, (int*)&cY, (int*)&cZ, &nSectnum, fix16_to_int(cA), q16horiz);
+        }
+        CheckLink((int*)&cX, (int*)&cY, (int*)&cZ, &nSectnum);
+        int v78 = interpolateang(gScreenTiltO, gScreenTilt, gInterpolate);
+        char v14 = 0;
+        char v10 = 0;
+        bool bDelirium = powerupCheck(gView, 28) > 0;
+        static bool bDeliriumOld = false;
+        int tiltcs, tiltdim;
+        char v4 = powerupCheck(gView, 21) > 0;
+#ifdef USE_OPENGL
+        renderSetRollAngle(0);
+#endif
+        if (v78 || bDelirium)
+        {
+            if (videoGetRenderMode() == REND_CLASSIC)
+            {
+                int vr = viewingrange;
+                walock[TILTBUFFER] = 255;
+                if (!waloff[TILTBUFFER])
+                {
+                    tileAllocTile(TILTBUFFER, 640, 640, 0, 0);
+                }
+                if (xdim >= 640 && ydim >= 640)
+                {
+                    tiltcs = 1;
+                    tiltdim = 640;
+                }
+                else
+                {
+                    tiltcs = 0;
+                    tiltdim = 320;
+                }
+                renderSetTarget(TILTBUFFER, tiltdim, tiltdim);
+                int nAng = v78&511;
+                if (nAng > 256)
+                {
+                    nAng = 512-nAng;
+                }
+                renderSetAspect(mulscale16(vr, dmulscale32(Cos(nAng), 262144, Sin(nAng), 163840)), yxaspect);
+            }
+#ifdef USE_OPENGL
+            else
+                renderSetRollAngle(v78);
+#endif
+        }
+        else if (v4 && gNetPlayers > 1)
+        {
+            int tmp = (gGameClock/240)%(gNetPlayers-1);
+            int i = connecthead;
+            while (1)
+            {
+                if (i == gViewIndex)
+                    i = connectpoint2[i];
+                if (tmp == 0)
+                    break;
+                i = connectpoint2[i];
+                tmp--;
+            }
+            PLAYER *pOther = &gPlayer[i];
+            //othercameraclock = gGameClock;
+            if (!waloff[4079])
+            {
+                tileAllocTile(4079, 128, 128, 0, 0);
+            }
+            renderSetTarget(4079, 128, 128);
+            renderSetAspect(65536, 78643);
+            int vd8 = pOther->pSprite->x;
+            int vd4 = pOther->pSprite->y;
+            int vd0 = pOther->at67;
+            int vcc = pOther->pSprite->sectnum;
+            int v50 = pOther->pSprite->ang;
+            int v54 = 0;
+            if (pOther->at35a)
+            {
+                int nValue = ClipHigh(pOther->at35a*8, 2000);
+                v54 += QRandom2(nValue>>8);
+                v50 += QRandom2(nValue>>8);
+                vd8 += QRandom2(nValue>>4);
+                vd4 += QRandom2(nValue>>4);
+                vd0 += QRandom2(nValue);
+            }
+            if (pOther->at37f)
+            {
+                int nValue = ClipHigh(pOther->at37f*8, 2000);
+                v54 += QRandom2(nValue >> 8);
+                v50 += QRandom2(nValue >> 8);
+                vd8 += QRandom2(nValue >> 4);
+                vd4 += QRandom2(nValue >> 4);
+                vd0 += QRandom2(nValue);
+            }
+            CalcOtherPosition(pOther->pSprite, &vd8, &vd4, &vd0, &vcc, v50, 0);
+            CheckLink(&vd8, &vd4, &vd0, &vcc);
+            if (IsUnderwaterSector(vcc))
+            {
+                v14 = 10;
+            }
+            memcpy(bakMirrorGotpic, gotpic+510, 2);
+            memcpy(gotpic+510, otherMirrorGotpic, 2);
+            g_visibility = (int32_t)(ClipLow(gVisibility-32*pOther->at362, 0) * (numplayers > 1 ? 1.f : r_ambientlightrecip));
+            int vc4, vc8;
+            getzsofslope(vcc, vd8, vd4, &vc8, &vc4);
+            if (vd0 >= vc4)
+            {
+                vd0 = vc4-(8<<4);
+            }
+            if (vd0 <= vc8)
+            {
+                vd0 = vc8+(8<<4);
+            }
+            v54 = ClipRange(v54, -200, 200);
+RORHACKOTHER:
+            int ror_status[16];
+            for (int i = 0; i < 16; i++)
+                ror_status[i] = TestBitString(gotpic, 4080 + i);
+            DrawMirrors(vd8, vd4, vd0, fix16_from_int(v50), fix16_from_int(v54 + defaultHoriz));
+            drawrooms(vd8, vd4, vd0, v50, v54 + defaultHoriz, vcc);
+            bool do_ror_hack = false;
+            for (int i = 0; i < 16; i++)
+                if (!ror_status[i] && TestBitString(gotpic, 4080 + i))
+                    do_ror_hack = true;
+            if (do_ror_hack)
+                goto RORHACKOTHER;
+            memcpy(otherMirrorGotpic, gotpic+510, 2);
+            memcpy(gotpic+510, bakMirrorGotpic, 2);
+            viewProcessSprites(vd8, vd4, vd0);
+            renderDrawMasks();
+            renderRestoreTarget();
+        }
+        else
+        {
+            othercameraclock = gGameClock;
+        }
+
+        if (!bDelirium)
+        {
+            deliriumTilt = 0;
+            deliriumTurn = 0;
+            deliriumPitch = 0;
+        }
+        int nSprite = headspritestat[2];
+        int unk = 0;
+        while (nSprite >= 0)
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            int nXSprite = pSprite->extra;
+            dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            if (TestBitString(gotsector, pSprite->sectnum))
+            {
+                unk += pXSprite->data3*32;
+            }
+            nSprite = nextspritestat[nSprite];
+        }
+        nSprite = headspritestat[5];
+        while (nSprite >= 0)
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            switch (pSprite->type)
+            {
+            case 301:
+            case 302:
+            case 303:
+            case 306:
+                if (TestBitString(gotsector, pSprite->sectnum))
+                {
+                    unk += 256;
+                }
+                break;
+            }
+            nSprite = nextspritestat[nSprite];
+        }
+        g_visibility = (int32_t)(ClipLow(gVisibility - 32 * gView->at362 - unk, 0) * (numplayers > 1 ? 1.f : r_ambientlightrecip));
+        cA = (cA + interpolateangfix16(fix16_from_int(deliriumTurnO), fix16_from_int(deliriumTurn), gInterpolate)) & 0x7ffffff;
+        int vfc, vf8;
+        getzsofslope(nSectnum, cX, cY, &vfc, &vf8);
+        if (cZ >= vf8)
+        {
+            cZ = vf8-(8<<8);
+        }
+        if (cZ <= vfc)
+        {
+            cZ = vfc+(8<<8);
+        }
+        q16horiz = ClipRange(q16horiz, F16(-200), F16(200));
+RORHACK:
+        int ror_status[16];
+        for (int i = 0; i < 16; i++)
+            ror_status[i] = TestBitString(gotpic, 4080+i);
+        fix16_t deliriumPitchI = interpolate(fix16_from_int(deliriumPitchO), fix16_from_int(deliriumPitch), gInterpolate);
+        DrawMirrors(cX, cY, cZ, cA, q16horiz + fix16_from_int(defaultHoriz) + deliriumPitchI);
+        int bakCstat = gView->pSprite->cstat;
+        if (gViewPos == 0)
+        {
+            gView->pSprite->cstat |= 32768;
+        }
+        else
+        {
+            gView->pSprite->cstat |= 514;
+        }
+        renderDrawRoomsQ16(cX, cY, cZ, cA, q16horiz + fix16_from_int(defaultHoriz) + deliriumPitchI, nSectnum);
+        bool do_ror_hack = false;
+        for (int i = 0; i < 16; i++)
+            if (!ror_status[i] && TestBitString(gotpic, 4080+i))
+                do_ror_hack = true;
+        if (do_ror_hack)
+        {
+            gView->pSprite->cstat = bakCstat;
+            goto RORHACK;
+        }
+
+        viewProcessSprites(cX, cY, cZ);
+        sub_5571C(1);
+        renderDrawMasks();
+        sub_5571C(0);
+        sub_557C4(cX, cY, gInterpolate);
+        renderDrawMasks();
+        gView->pSprite->cstat = bakCstat;
+
+        if (v78 || bDelirium)
+        {
+            if (videoGetRenderMode() == REND_CLASSIC)
+            {
+                dassert(waloff[ TILTBUFFER ] != 0);
+                renderRestoreTarget();
+                int vrc = 64+4+2+1024;
+                if (bDelirium)
+                {
+                    vrc = 64+32+4+2+1+1024;
+                }
+                int nAng = v78 & 511;
+                if (nAng > 256)
+                {
+                    nAng = 512 - nAng;
+                }
+                int nScale = dmulscale32(Cos(nAng), 262144, Sin(nAng), 163840)>>tiltcs;
+                rotatesprite(160<<16, 100<<16, nScale, v78+512, TILTBUFFER, 0, 0, vrc, gViewX0, gViewY0, gViewX1, gViewY1);
+            }
+#ifdef USE_OPENGL
+            else
+            {
+                if (videoGetRenderMode() == REND_POLYMOST && gDeliriumBlur)
+                {
+                    if (!bDeliriumOld)
+                    {
+                        glAccum(GL_LOAD, 1.f);
+                    }
+                    else
+                    {
+                        const float fBlur = pow(1.f/3.f, 30.f/g_frameRate);
+                        glAccum(GL_MULT, fBlur);
+                        glAccum(GL_ACCUM, 1.f-fBlur);
+                        glAccum(GL_RETURN, 1.f);
+                    }
+                }
+            }
+#endif
+        }
+
+        bDeliriumOld = bDelirium && gDeliriumBlur;
+
+        if (r_usenewaspect)
+            newaspect_enable = 0;
+        renderSetAspect(viewingRange, yxAspect);
+        int nClipDist = gView->pSprite->clipdist<<2;
+        int ve8, vec, vf0, vf4;
+        GetZRange(gView->pSprite, &vf4, &vf0, &vec, &ve8, nClipDist, 0);
+#if 0
+        int tmpSect = nSectnum;
+        if ((vf0 & 0xe000) == 0x4000)
+        {
+            tmpSect = vf0 & (kMaxWalls-1);
+        }
+        int v8 = byte_1CE5C2 > 0 && (sector[tmpSect].ceilingstat&1);
+        if (gWeather.at12d8 > 0 || v8)
+        {
+            gWeather.Draw(cX, cY, cZ, cA, q16horiz + defaultHoriz + deliriumPitch, gWeather.at12d8);
+            if (v8)
+            {
+                gWeather.at12d8 = ClipRange(delta*8+gWeather.at12d8, 0, 4095);
+            }
+            else
+            {
+                gWeather.at12d8 = ClipRange(gWeather.at12d8-delta*64, 0, 4095);
+            }
+        }
+#endif
+        if (gViewPos == 0)
+        {
+            if (gAimReticle)
+            {
+                rotatesprite(160<<16, defaultHoriz<<16, 65536, 0, kCrosshairTile, 0, CROSSHAIR_PAL, 2, gViewX0, gViewY0, gViewX1, gViewY1);
+            }
+            cX = (v4c>>8)+160;
+            cY = (v48>>8)+220+(zDelta>>7);
+            int nShade = sector[nSectnum].floorshade;
+            int nPalette = 0;
+            if (sector[gView->pSprite->sectnum].extra > 0)
+            {
+                sectortype *pSector = &sector[gView->pSprite->sectnum];
+                XSECTOR *pXSector = &xsector[pSector->extra];
+                if (pXSector->color)
+                {
+                    nPalette = pSector->floorpal;
+                }
+            }
+            WeaponDraw(gView, nShade, cX, cY, nPalette);
+        }
+        if (gViewPos == 0 && gView->pXSprite->burnTime > 60)
+        {
+            viewBurnTime(gView->pXSprite->burnTime);
+        }
+        if (packItemActive(gView, 1))
+        {
+            rotatesprite(0, 0, 65536, 0, 2344, 0, 0, 256+18, gViewX0, gViewY0, gViewX1, gViewY1);
+            rotatesprite(320<<16, 0, 65536, 1024, 2344, 0, 0, 512+22, gViewX0, gViewY0, gViewX1, gViewY1);
+            rotatesprite(0, 200<<16, 65536, 0, 2344, 0, 0, 256+22, gViewX0, gViewY0, gViewX1, gViewY1);
+            rotatesprite(320<<16, 200<<16, 65536, 1024, 2344, 0, 0, 512+18, gViewX0, gViewY0, gViewX1, gViewY1);
+            if (gDetail >= 4)
+            {
+                rotatesprite(15<<16, 3<<16, 65536, 0, 2346, 32, 0, 256+19, gViewX0, gViewY0, gViewX1, gViewY1);
+                rotatesprite(212<<16, 77<<16, 65536, 0, 2347, 32, 0, 512+19, gViewX0, gViewY0, gViewX1, gViewY1);
+            }
+        }
+        if (powerupCheck(gView, 39) > 0)
+        {
+            rotatesprite(0, 200<<16, 65536, 0, 2358, 0, 0, 256+22, gViewX0, gViewY0, gViewX1, gViewY1);
+            rotatesprite(320<<16, 200<<16, 65536, 1024, 2358, 0, 0, 512+18, gViewX0, gViewY0, gViewX1, gViewY1);
+        }
+        if (v4 && gNetPlayers > 1)
+        {
+            DoLensEffect();
+            viewingRange = viewingrange;
+            yxAspect = yxaspect;
+            renderSetAspect(65536, 54613);
+            rotatesprite(280<<16, 35<<16, 53248, 512, 4077, v10, v14, 512+6, gViewX0, gViewY0, gViewX1, gViewY1);
+            rotatesprite(280<<16, 35<<16, 53248, 0, 1683, v10, 0, 512+35, gViewX0, gViewY0, gViewX1, gViewY1);
+            renderSetAspect(viewingRange, yxAspect);
+        }
+        if (powerupCheck(gView, 14) > 0)
+        {
+            nPalette = 4;
+        }
+        else if(powerupCheck(gView, 24) > 0)
+        {
+            nPalette = 1;
+        }
+        else if (gView->at87)
+        {
+            if (gView->nWaterPal)
+                nPalette = gView->nWaterPal;
+            else
+            {
+                if (gView->pXSprite->medium == 1)
+                {
+                    nPalette = 1;
+                }
+                else if (gView->pXSprite->medium == 2)
+                {
+                    nPalette = 3;
+                }
+                else
+                {
+                    nPalette = 2;
+                }
+            }
+        }
+    }
+    if (gViewMode == 4)
+    {
+        gViewMap.sub_25DB0(gView->pSprite);
+    }
+    viewDrawInterface(delta);
+    int zn = ((gView->at6f-gView->at67-(12<<8))>>7)+220;
+    PLAYER *pPSprite = &gPlayer[gMe->pSprite->type-kDudePlayer1];
+    if (pPSprite->at376 == 1)
+    {
+        //static int lastClock;
+        gChoke.sub_84110(160, zn);
+        //if ((gGameClock % 5) == 0 && gGameClock != lastClock)
+        //{
+        //    gChoke.at1c(pPSprite);
+        //}
+        //lastClock = gGameClock;
+    }
+    if (byte_1A76C6)
+    {
+        DrawStatSprite(2048, xdim-15, 20);
+    }
+    viewDisplayMessage();
+    CalcFrameRate();
+#if 0
+    if (gShowFrameRate)
+    {
+        int fX, fY;
+        if (gViewMode == 3)
+        {
+            fX = gViewX1;
+        }
+        else
+        {
+            fX = xdim;
+        }
+        if (gViewMode == 3)
+        {
+            fY = gViewY0;
+        }
+        else
+        {
+            fY = 0;
+        }
+        if (gViewMode == 4)
+        {
+            fY += mulscale16(20, yscale);
+        }
+        sprintf(gTempStr, "%3i", gFrameRate);
+        printext256(fX-12, fY, 31, -1, gTempStr, 1);
+        fY += 8;
+        sprintf(gTempStr, "pos=%d,%d,%d", gView->pSprite->x, gView->pSprite->y, gView->pSprite->z);
+        printext256(fX-strlen(gTempStr)*4, fY, 31, -1, gTempStr, 1);
+    }
+#endif
+    viewPrintFPS();
+    if (gPaused)
+    {
+        viewDrawText(1, "PAUSED", 160, 10, 0, 0, 1, 0);
+    }
+    else if (gView != gMe)
+    {
+        sprintf(gTempStr, "] %s [", gProfile[gView->at57].name);
+        viewDrawText(0, gTempStr, 160, 10, 0, 0, 1, 0);
+    }
+    if (errMsg[0])
+    {
+        viewDrawText(0, errMsg, 160, 20, 0, 0, 1, 0);
+    }
+    if (gViewInterpolate)
+    {
+        RestoreInterpolations();
+    }
+    UpdateDacs(nPalette);
+}
+
+int nLoadingScreenTile;
+char pzLoadingScreenText1[256], pzLoadingScreenText2[256], pzLoadingScreenText3[256];
+
+void viewLoadingScreenWide(void)
+{
+    videoClearScreen(0);
+#ifdef USE_OPENGL
+    if ((blood_globalflags&BLOOD_FORCE_WIDELOADSCREEN) || (bLoadScreenCrcMatch && !(usehightile && h_xsize[kLoadScreen])))
+#else
+    if ((blood_globalflags&BLOOD_FORCE_WIDELOADSCREEN) || bLoadScreenCrcMatch)
+#endif
+    {
+        if (yxaspect >= 65536)
+        {
+            rotatesprite(160<<16, 100<<16, 65536, 0, kLoadScreen, 0, 0, 1024+64+8+2, 0, 0, xdim-1, ydim-1);
+        }
+        else
+        {
+            int width = roundscale(xdim, 240, ydim);
+            int nCount = (width+kLoadScreenWideBackWidth-1)/kLoadScreenWideBackWidth;
+            for (int i = 0; i < nCount; i++)
+            {
+                rotatesprite_fs((i*kLoadScreenWideBackWidth)<<16, 0, 65536, 0, kLoadScreenWideBack, 0, 0, 256+64+16+8+2);
+            }
+            rotatesprite_fs((kLoadScreenWideSideWidth>>1)<<16, 200<<15, 65536, 0, kLoadScreenWideLeft, 0, 0, 256+8+2);
+            rotatesprite_fs((320-(kLoadScreenWideSideWidth>>1))<<16, 200<<15, 65536, 0, kLoadScreenWideRight, 0, 0, 512+8+2);
+            rotatesprite_fs(320<<15, 200<<15, 65536, 0, kLoadScreenWideMiddle, 0, 0, 8+2);
+        }
+    }
+    else
+        rotatesprite(160<<16, 100<<16, 65536, 0, kLoadScreen, 0, 0, 64+8+2, 0, 0, xdim-1, ydim-1);
+}
+
+void viewLoadingScreenUpdate(const char *pzText4, int nPercent)
+{
+    int vc;
+    gMenuTextMgr.GetFontInfo(1, NULL, NULL, &vc);
+    if (nLoadingScreenTile == kLoadScreen)
+        viewLoadingScreenWide();
+    else if (nLoadingScreenTile)
+    {
+        videoClearScreen(0);
+        rotatesprite(160<<16, 100<<16, 65536, 0, nLoadingScreenTile, 0, 0, 74, 0, 0, xdim-1, ydim-1);
+    }
+    if (pzLoadingScreenText1[0])
+    {
+        rotatesprite(160<<16, 20<<16, 65536, 0, 2038, -128, 0, 78, 0, 0, xdim-1, ydim-1);
+        viewDrawText(1, pzLoadingScreenText1, 160, 20-vc/2, -128, 0, 1, 1);
+    }
+    if (pzLoadingScreenText2[0])
+    {
+        viewDrawText(1, pzLoadingScreenText2, 160, 50, -128, 0, 1, 1);
+    }
+    if (pzLoadingScreenText3[0])
+    {
+        viewDrawText(1, pzLoadingScreenText3, 160, 70, -128, 0, 1, 1);
+    }
+    if (pzText4)
+    {
+        viewDrawText(3, pzText4, 160, 124, -128, 0, 1, 1);
+    }
+
+    if (nPercent != -1)
+        TileHGauge(2260, 86, 110, nPercent, 100, 0, 131072);
+
+    viewDrawText(3, "Please Wait", 160, 134, -128, 0, 1, 1);
+    scrNextPage();
+}
+
+void viewLoadingScreen(int nTile, const char *pText, const char *pText2, const char *pText3)
+{
+    UpdateDacs(0, true);
+    nLoadingScreenTile = nTile;
+    if (pText)
+        strncpy(pzLoadingScreenText1, pText, 256);
+    else
+        pzLoadingScreenText1[0] = 0;
+    if (pText2)
+        strncpy(pzLoadingScreenText2, pText2, 256);
+    else
+        pzLoadingScreenText2[0] = 0;
+    if (pText3)
+        strncpy(pzLoadingScreenText3, pText3, 256);
+    else
+        pzLoadingScreenText3[0] = 0;
+    viewLoadingScreenUpdate(NULL, -1);
+}
+
+palette_t CrosshairColors = { 255, 255, 255, 0 };
+bool g_isAlterDefaultCrosshair = false;
+
+void viewSetCrosshairColor(int32_t r, int32_t g, int32_t b)
+{
+    if (!g_isAlterDefaultCrosshair)
+        return;
+
+    CrosshairColors.r = r;
+    CrosshairColors.g = g;
+    CrosshairColors.b = b;
+
+    tileLoad(kCrosshairTile);
+
+    if (!waloff[kCrosshairTile])
+        return;
+
+    char *ptr = (char *)waloff[kCrosshairTile];
+
+    int32_t ii = tilesiz[kCrosshairTile].x * tilesiz[kCrosshairTile].y;
+
+    dassert(ii > 0);
+
+    int32_t i = (videoGetRenderMode() == REND_CLASSIC)
+        ? paletteGetClosestColor(CrosshairColors.r, CrosshairColors.g, CrosshairColors.b)
+        : paletteGetClosestColor(255, 255, 255);  // use white in GL so we can tint it to the right color
+
+    do
+    {
+        if (*ptr != 255)
+            *ptr = i;
+        ptr++;
+    } while (--ii);
+
+    paletteMakeLookupTable(CROSSHAIR_PAL, NULL, CrosshairColors.r, CrosshairColors.g, CrosshairColors.b, 1);
+
+#ifdef USE_OPENGL
+    // XXX: this makes us also load all hightile textures tinted with the crosshair color!
+    polytint_t & crosshairtint = hictinting[CROSSHAIR_PAL];
+    crosshairtint.r = CrosshairColors.r;
+    crosshairtint.g = CrosshairColors.g;
+    crosshairtint.b = CrosshairColors.b;
+    crosshairtint.f = HICTINT_USEONART | HICTINT_GRAYSCALE;
+#endif
+    tileInvalidate(kCrosshairTile, -1, -1);
+}
+
+void viewResetCrosshairToDefault(void)
+{
+    paletteFreeLookupTable(CROSSHAIR_PAL);
+    tileLoad(kCrosshairTile);
+}
+
+#define COLOR_RED redcol
+#define COLOR_WHITE whitecol
+
+#define LOW_FPS 60
+#define SLOW_FRAME_TIME 20
+
+#if defined GEKKO
+# define FPS_YOFFSET 16
+#else
+# define FPS_YOFFSET 0
+#endif
+
+#define FPS_COLOR(x) ((x) ? COLOR_RED : COLOR_WHITE)
+
+int32_t gShowFps, gFramePeriod;
+
+void viewPrintFPS(void)
+{
+    char tempbuf[128];
+    static int32_t frameCount;
+    static double cumulativeFrameDelay;
+    static double lastFrameTime;
+    static float lastFPS, minFPS = FLT_MAX, maxFPS;
+    static double minGameUpdate = DBL_MAX, maxGameUpdate;
+
+    double frameTime = timerGetHiTicks();
+    double frameDelay = frameTime - lastFrameTime;
+    cumulativeFrameDelay += frameDelay;
+
+    if (frameDelay >= 0)
+    {
+        int32_t x = (xdim <= 640);
+
+        if (gShowFps)
+        {
+            int32_t chars = Bsprintf(tempbuf, "%.1f ms, %5.1f fps", frameDelay, lastFPS);
+
+            printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+            printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+1+FPS_YOFFSET,
+                FPS_COLOR(lastFPS < LOW_FPS), -1, tempbuf, x);
+
+            if (gShowFps > 1)
+            {
+                chars = Bsprintf(tempbuf, "max: %5.1f fps", maxFPS);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+10+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+10+FPS_YOFFSET,
+                    FPS_COLOR(maxFPS < LOW_FPS), -1, tempbuf, x);
+
+                chars = Bsprintf(tempbuf, "min: %5.1f fps", minFPS);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+20+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+20+FPS_YOFFSET,
+                    FPS_COLOR(minFPS < LOW_FPS), -1, tempbuf, x);
+            }
+            if (gShowFps > 2)
+            {
+                if (g_gameUpdateTime > maxGameUpdate) maxGameUpdate = g_gameUpdateTime;
+                if (g_gameUpdateTime < minGameUpdate) minGameUpdate = g_gameUpdateTime;
+
+                chars = Bsprintf(tempbuf, "Game Update: %2.2f ms + draw: %2.2f ms", g_gameUpdateTime, g_gameUpdateAndDrawTime);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+30+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+30+FPS_YOFFSET,
+                    FPS_COLOR(g_gameUpdateAndDrawTime >= SLOW_FRAME_TIME), -1, tempbuf, x);
+
+                chars = Bsprintf(tempbuf, "GU min/max/avg: %5.2f/%5.2f/%5.2f ms", minGameUpdate, maxGameUpdate, g_gameUpdateAvgTime);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+40+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+40+FPS_YOFFSET,
+                    FPS_COLOR(maxGameUpdate >= SLOW_FRAME_TIME), -1, tempbuf, x);
+                
+                chars = Bsprintf(tempbuf, "bufferjitter: %i", gBufferJitter);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+50+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+50+FPS_YOFFSET,
+                    COLOR_WHITE, -1, tempbuf, x);
+#if 0
+                chars = Bsprintf(tempbuf, "G_MoveActors(): %.3e ms", g_moveActorsTime);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+50+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+50+FPS_YOFFSET,
+                    COLOR_WHITE, -1, tempbuf, x);
+
+                chars = Bsprintf(tempbuf, "G_MoveWorld(): %.3e ms", g_moveWorldTime);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+60+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+60+FPS_YOFFSET,
+                    COLOR_WHITE, -1, tempbuf, x);
+#endif
+            }
+#if 0
+            // lag meter
+            if (g_netClientPeer)
+            {
+                chars = Bsprintf(tempbuf, "%d +- %d ms", (g_netClientPeer->lastRoundTripTime + g_netClientPeer->roundTripTime)/2,
+                    (g_netClientPeer->lastRoundTripTimeVariance + g_netClientPeer->roundTripTimeVariance)/2);
+
+                printext256(windowxy2.x-(chars<<(3-x))+1, windowxy1.y+30+2+FPS_YOFFSET, 0, -1, tempbuf, x);
+                printext256(windowxy2.x-(chars<<(3-x)), windowxy1.y+30+1+FPS_YOFFSET, FPS_COLOR(g_netClientPeer->lastRoundTripTime > 200), -1, tempbuf, x);
+            }
+#endif
+        }
+
+        if (cumulativeFrameDelay >= 1000.0)
+        {
+            lastFPS = 1000.f * frameCount / cumulativeFrameDelay;
+            g_frameRate = Blrintf(lastFPS);
+            frameCount = 0;
+            cumulativeFrameDelay = 0.0;
+
+            if (gShowFps > 1)
+            {
+                if (lastFPS > maxFPS) maxFPS = lastFPS;
+                if (lastFPS < minFPS) minFPS = lastFPS;
+
+                static int secondCounter;
+
+                if (++secondCounter >= gFramePeriod)
+                {
+                    maxFPS = (lastFPS + maxFPS) * .5f;
+                    minFPS = (lastFPS + minFPS) * .5f;
+                    maxGameUpdate = (g_gameUpdateTime + maxGameUpdate) * 0.5;
+                    minGameUpdate = (g_gameUpdateTime + minGameUpdate) * 0.5;
+                    secondCounter = 0;
+                }
+            }
+        }
+        frameCount++;
+    }
+    lastFrameTime = frameTime;
+}
+
+#undef FPS_COLOR
+
+class ViewLoadSave : public LoadSave {
+public:
+    void Load(void);
+    void Save(void);
+};
+
+static ViewLoadSave *myLoadSave;
+
+static int messageTime;
+static char message[256];
+
+void ViewLoadSave::Load(void)
+{
+    Read(&messageTime, sizeof(messageTime));
+    Read(message, sizeof(message));
+    Read(otherMirrorGotpic, sizeof(otherMirrorGotpic));
+    Read(bakMirrorGotpic, sizeof(bakMirrorGotpic));
+    Read(&gScreenTilt, sizeof(gScreenTilt));
+    Read(&deliriumTilt, sizeof(deliriumTilt));
+    Read(&deliriumTurn, sizeof(deliriumTurn));
+    Read(&deliriumPitch, sizeof(deliriumPitch));
+}
+
+void ViewLoadSave::Save(void)
+{
+    Write(&messageTime, sizeof(messageTime));
+    Write(message, sizeof(message));
+    Write(otherMirrorGotpic, sizeof(otherMirrorGotpic));
+    Write(bakMirrorGotpic, sizeof(bakMirrorGotpic));
+    Write(&gScreenTilt, sizeof(gScreenTilt));
+    Write(&deliriumTilt, sizeof(deliriumTilt));
+    Write(&deliriumTurn, sizeof(deliriumTurn));
+    Write(&deliriumPitch, sizeof(deliriumPitch));
+}
+
+void ViewLoadSaveConstruct(void)
+{
+    myLoadSave = new ViewLoadSave();
+}
diff --git a/source/blood/src/view.h b/source/blood/src/view.h
new file mode 100644
index 000000000..232f2b311
--- /dev/null
+++ b/source/blood/src/view.h
@@ -0,0 +1,193 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+
+#include "palette.h"
+#include "common_game.h"
+#include "controls.h"
+#include "messages.h"
+#include "player.h"
+
+enum VIEW_EFFECT {
+    VIEW_EFFECT_0 = 0,
+    VIEW_EFFECT_1,
+    VIEW_EFFECT_2,
+    VIEW_EFFECT_3,
+    VIEW_EFFECT_4,
+    VIEW_EFFECT_5,
+    VIEW_EFFECT_6,
+    VIEW_EFFECT_7,
+    VIEW_EFFECT_8,
+    VIEW_EFFECT_9,
+    VIEW_EFFECT_10,
+    VIEW_EFFECT_11,
+    VIEW_EFFECT_12,
+    VIEW_EFFECT_13,
+    VIEW_EFFECT_14,
+    VIEW_EFFECT_15,
+    VIEW_EFFECT_16,
+    VIEW_EFFECT_17,
+    VIEW_EFFECT_18,
+};
+
+enum VIEWPOS {
+    VIEWPOS_0 = 0,
+    VIEWPOS_1
+};
+
+enum INTERPOLATE_TYPE {
+    INTERPOLATE_TYPE_INT = 0,
+    INTERPOLATE_TYPE_SHORT,
+};
+
+#define CROSSHAIR_PAL (MAXPALOOKUPS-RESERVEDPALS-1)
+#define kCrosshairTile 2319
+#define kLoadScreen 2049
+#define kLoadScreenCRC -2051908571
+#define kLoadScreenWideBackWidth 256
+#define kLoadScreenWideSideWidth 128
+#define kLoadScreenWideBack 9216
+#define kLoadScreenWideLeft 9217
+#define kLoadScreenWideRight 9218
+#define kLoadScreenWideMiddle 9219
+
+struct FONT {
+    int tile, xSize, ySize, space;
+};
+
+extern int gZoom;
+extern FONT gFont[5];
+extern int gViewMode;
+extern VIEWPOS gViewPos;
+extern int gViewIndex;
+extern int gScreenTilt;
+extern int deliriumTilt, deliriumTurn, deliriumPitch;
+extern int gScreenTiltO, deliriumTurnO, deliriumPitchO;
+extern int gShowFrameRate;
+extern char gInterpolateSprite[512];
+extern char gInterpolateWall[1024];
+extern char gInterpolateSector[128];
+extern LOCATION gPrevSpriteLoc[kMaxSprites];
+extern int gViewSize;
+extern CGameMessageMgr gGameMessageMgr;
+extern int gViewXCenter, gViewYCenter;
+extern int gViewX0, gViewY0, gViewX1, gViewY1;
+extern int gViewX0S, gViewY0S, gViewX1S, gViewY1S;
+extern palette_t CrosshairColors;
+extern bool g_isAlterDefaultCrosshair;
+extern int32_t r_maxfps;
+extern int32_t r_maxfpsoffset;
+extern double g_frameDelay;
+extern float r_ambientlight, r_ambientlightrecip;
+extern int gLastPal;
+extern int32_t gShowFps, gFramePeriod;
+
+
+static inline double calcFrameDelay(int maxFPS) { return maxFPS ? ((double)timerGetFreqU64() / (double)(maxFPS)) : 0.0; }
+
+void viewGetFontInfo(int id, const char *unk1, int *pXSize, int *pYSize);
+void viewUpdatePages(void);
+void viewToggle(int viewMode);
+void viewInitializePrediction(void);
+void viewUpdatePrediction(GINPUT *pInput);
+void sub_158B4(PLAYER *pPlayer);
+void fakeProcessInput(PLAYER *pPlayer, GINPUT *pInput);
+void fakePlayerProcess(PLAYER *pPlayer, GINPUT *pInput);
+void fakeMoveDude(spritetype *pSprite);
+void fakeActAirDrag(spritetype *pSprite, int num);
+void fakeActProcessSprites(void);
+void viewCorrectPrediction(void);
+void viewBackupView(int nPlayer);
+void viewClearInterpolations(void);
+void viewAddInterpolation(void *data, INTERPOLATE_TYPE type);
+void CalcInterpolations(void);
+void RestoreInterpolations(void);
+void viewDrawText(int nFont, const char *pString, int x, int y, int nShade, int nPalette, int position, char shadow, unsigned int nStat = 0, uint8_t alpha = 0);
+void viewTileSprite(int nTile, int nShade, int nPalette, int x1, int y1, int x2, int y2);
+void InitStatusBar(void);
+void DrawStatSprite(int nTile, int x, int y, int nShade = 0, int nPalette = 0, unsigned int nStat = 0, int nScale = 65536);
+void DrawStatMaskedSprite(int nTile, int x, int y, int nShade = 0, int nPalette = 0, unsigned int nStat = 0, int nScale = 65536);
+void DrawStatNumber(const char *pFormat, int nNumber, int nTile, int x, int y, int nShade, int nPalette, unsigned int nStat = 0, int nScale = 65536);
+void TileHGauge(int nTile, int x, int y, int nMult, int nDiv, int nStat = 0, int nScale = 65536);
+void viewDrawPack(PLAYER *pPlayer, int x, int y);
+void DrawPackItemInStatusBar(PLAYER *pPlayer, int x, int y, int x2, int y2, int nStat = 0);
+void UpdateStatusBar(int arg);
+void viewInit(void);
+void viewResizeView(int size);
+void UpdateFrame(void);
+void viewDrawInterface(int arg);
+uspritetype *viewInsertTSprite(int nSector, int nStatnum, uspritetype *pSprite);
+uspritetype *viewAddEffect(int nTSprite, VIEW_EFFECT nViewEffect);
+void viewProcessSprites(int cX, int cY, int cZ);
+void CalcOtherPosition(spritetype *pSprite, int *pX, int *pY, int *pZ, int *vsectnum, int nAng, int zm);
+void CalcPosition(spritetype *pSprite, int *pX, int *pY, int *pZ, int *vsectnum, int nAng, int zm);
+void viewSetMessage(const char *pMessage);
+void viewDisplayMessage(void);
+void viewSetErrorMessage(const char *pMessage);
+void DoLensEffect(void);
+void UpdateDacs(int nPalette, bool bNoTint = false);
+int viewFPSLimit(void);
+void viewDrawScreen(void);
+void viewLoadingScreenWide(void);
+void viewLoadingScreenUpdate(const char *pzText4 = NULL, int nPercent = -1);
+void viewLoadingScreen(int nTile, const char *pText, const char *pText2, const char *pText3);
+void viewUpdateDelirium(void);
+void viewUpdateShake(void);
+void viewSetCrosshairColor(int32_t r, int32_t g, int32_t b);
+void viewResetCrosshairToDefault(void);
+void viewPrintFPS(void);
+
+
+inline void viewInterpolateSector(int nSector, sectortype *pSector)
+{
+    if (!TestBitString(gInterpolateSector, nSector))
+    {
+        viewAddInterpolation(&pSector->floorz, INTERPOLATE_TYPE_INT);
+        viewAddInterpolation(&pSector->ceilingz, INTERPOLATE_TYPE_INT);
+        viewAddInterpolation(&pSector->floorheinum, INTERPOLATE_TYPE_SHORT);
+        SetBitString(gInterpolateSector, nSector);
+    }
+}
+
+inline void viewInterpolateWall(int nWall, walltype *pWall)
+{
+    if (!TestBitString(gInterpolateWall, nWall))
+    {
+        viewAddInterpolation(&pWall->x, INTERPOLATE_TYPE_INT);
+        viewAddInterpolation(&pWall->y, INTERPOLATE_TYPE_INT);
+        SetBitString(gInterpolateWall, nWall);
+    }
+}
+
+inline void viewBackupSpriteLoc(int nSprite, spritetype *pSprite)
+{
+    if (!TestBitString(gInterpolateSprite, nSprite))
+    {
+        LOCATION *pPrevLoc = &gPrevSpriteLoc[nSprite];
+        pPrevLoc->x = pSprite->x;
+        pPrevLoc->y = pSprite->y;
+        pPrevLoc->z = pSprite->z;
+        pPrevLoc->ang = pSprite->ang;
+        SetBitString(gInterpolateSprite, nSprite);
+    }
+}
diff --git a/source/blood/src/warp.cpp b/source/blood/src/warp.cpp
new file mode 100644
index 000000000..ce7a0ca3a
--- /dev/null
+++ b/source/blood/src/warp.cpp
@@ -0,0 +1,322 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include "build.h"
+#include "compat.h"
+#include "common_game.h"
+#include "blood.h"
+#include "db.h"
+#include "gameutil.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "view.h"
+#include "warp.h"
+
+ZONE gStartZone[8];
+ZONE gStartZoneTeam1[8];
+ZONE gStartZoneTeam2[8];
+
+bool gTeamsSpawnUsed = false;
+
+void warpInit(void)
+{
+    for (int i = 0; i < kMaxSectors; i++)
+    {
+        gUpperLink[i] = -1;
+        gLowerLink[i] = -1;
+    }
+    int team1 = 0; int team2 = 0; // increment if team start positions specified.
+    for (int nSprite = 0; nSprite < kMaxSprites; nSprite++)
+    {
+        if (sprite[nSprite].statnum < kMaxStatus)
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            int nXSprite = pSprite->extra;
+            if (nXSprite > 0)
+            {
+                XSPRITE *pXSprite = &xsprite[nXSprite];
+                switch (pSprite->type)
+                {
+                case 1:
+                    if (gGameOptions.nGameType < 2 && pXSprite->data1 >= 0 && pXSprite->data1 < 8)
+                    {
+                        ZONE *pZone = &gStartZone[pXSprite->data1];
+                        pZone->x = pSprite->x;
+                        pZone->y = pSprite->y;
+                        pZone->z = pSprite->z;
+                        pZone->sectnum = pSprite->sectnum;
+                        pZone->ang = pSprite->ang;
+                    }
+                    DeleteSprite(nSprite);
+                    break;
+                case 2:
+                    if (pXSprite->data1 >= 0 && pXSprite->data2 < 8) {
+                        if (gGameOptions.nGameType >= 2)
+                        {
+                            // default if BB or teams without data2 specified
+                            ZONE* pZone = &gStartZone[pXSprite->data1];
+                            pZone->x = pSprite->x;
+                            pZone->y = pSprite->y;
+                            pZone->z = pSprite->z;
+                            pZone->sectnum = pSprite->sectnum;
+                            pZone->ang = pSprite->ang;
+                            
+                            // By NoOne: fill player spawn position according team of player in TEAMS mode.
+                            if (gGameOptions.nGameType == 3) {
+                                if (pXSprite->data2 == 1) {
+                                    pZone = &gStartZoneTeam1[team1];
+                                    pZone->x = pSprite->x;
+                                    pZone->y = pSprite->y;
+                                    pZone->z = pSprite->z;
+                                    pZone->sectnum = pSprite->sectnum;
+                                    pZone->ang = pSprite->ang;
+                                    team1++;
+
+                                } else if (pXSprite->data2 == 2) {
+                                    pZone = &gStartZoneTeam2[team2];
+                                    pZone->x = pSprite->x;
+                                    pZone->y = pSprite->y;
+                                    pZone->z = pSprite->z;
+                                    pZone->sectnum = pSprite->sectnum;
+                                    pZone->ang = pSprite->ang;
+                                    team2++;
+                                }
+                            }
+                        }
+                        DeleteSprite(nSprite);
+                    }
+                    break;
+                case 7:
+                    gUpperLink[pSprite->sectnum] = nSprite;
+                    pSprite->cstat |= 32768;
+                    pSprite->cstat &= ~257;
+                    break;
+                case 6:
+                    gLowerLink[pSprite->sectnum] = nSprite;
+                    pSprite->cstat |= 32768;
+                    pSprite->cstat &= ~257;
+                    break;
+                case 9:
+                case 11:
+                case 13:
+                    gUpperLink[pSprite->sectnum] = nSprite;
+                    pSprite->cstat |= 32768;
+                    pSprite->cstat &= ~257;
+                    pSprite->z = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                    break;
+                case 10:
+                case 12:
+                case 14:
+                    gLowerLink[pSprite->sectnum] = nSprite;
+                    pSprite->cstat |= 32768;
+                    pSprite->cstat &= ~257;
+                    pSprite->z = getceilzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+                    break;
+                }
+            }
+        }
+    }
+    // check if there is enough start positions for teams, if any used
+    if (team1 > 0 || team2 > 0) {
+        gTeamsSpawnUsed = true;
+        //if (team1 < kMaxPlayers / 2 || team2 < kMaxPlayers / 2)
+           //_ShowMessageWindow("At least 4 spawn positions for each team is recommended.");
+    }
+
+    for (int i = 0; i < kMaxSectors; i++)
+    {
+        int nSprite = gUpperLink[i];
+        if (nSprite >= 0)
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            int nXSprite = pSprite->extra;
+            dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+            XSPRITE *pXSprite = &xsprite[nXSprite];
+            int nLink = pXSprite->data1;
+            for (int j = 0; j < kMaxSectors; j++)
+            {
+                int nSprite2 = gLowerLink[j];
+                if (nSprite2 >= 0)
+                {
+                    spritetype *pSprite2 = &sprite[nSprite2];
+                    int nXSprite = pSprite2->extra;
+                    dassert(nXSprite > 0 && nXSprite < kMaxXSprites);
+                    XSPRITE *pXSprite2 = &xsprite[nXSprite];
+                    if (pXSprite2->data1 == nLink)
+                    {
+                        pSprite->owner = gLowerLink[j];
+                        pSprite2->owner = gUpperLink[i];
+                    }
+                }
+            }
+        }
+    }
+}
+
+int CheckLink(spritetype *pSprite)
+{
+    int nSector = pSprite->sectnum;
+    int nUpper = gUpperLink[nSector];
+    int nLower = gLowerLink[nSector];
+    if (nUpper >= 0)
+    {
+        spritetype *pUpper = &sprite[nUpper];
+        int z;
+        if (pUpper->type == 7)
+            z = pUpper->z;
+        else
+            z = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+        if (z <= pSprite->z)
+        {
+            nLower = pUpper->owner;
+            dassert(nLower >= 0 && nLower < kMaxSprites);
+            spritetype *pLower = &sprite[nLower];
+            dassert(pLower->sectnum >= 0 && pLower->sectnum < kMaxSectors);
+            ChangeSpriteSect(pSprite->index, pLower->sectnum);
+            pSprite->x += pLower->x-pUpper->x;
+            pSprite->y += pLower->y-pUpper->y;
+            int z2;
+            if (pLower->type == 6)
+                z2 = pLower->z;
+            else
+                z2 = getceilzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+            pSprite->z += z2-z;
+            ClearBitString(gInterpolateSprite, pSprite->index);
+            return pUpper->type;
+        }
+    }
+    if (nLower >= 0)
+    {
+        spritetype *pLower = &sprite[nLower];
+        int z;
+        if (pLower->type == 6)
+            z = pLower->z;
+        else
+            z = getceilzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+        if (z >= pSprite->z)
+        {
+            nUpper = pLower->owner;
+            dassert(nUpper >= 0 && nUpper < kMaxSprites);
+            spritetype *pUpper = &sprite[nUpper];
+            dassert(pUpper->sectnum >= 0 && pUpper->sectnum < kMaxSectors);
+            ChangeSpriteSect(pSprite->index, pUpper->sectnum);
+            pSprite->x += pUpper->x-pLower->x;
+            pSprite->y += pUpper->y-pLower->y;
+            int z2;
+            if (pUpper->type == 7)
+                z2 = pUpper->z;
+            else
+                z2 = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
+            pSprite->z += z2-z;
+            ClearBitString(gInterpolateSprite, pSprite->index);
+            return pLower->type;
+        }
+    }
+    return 0;
+}
+
+int CheckLink(int *x, int *y, int *z, int *nSector)
+{
+    int nUpper = gUpperLink[*nSector];
+    int nLower = gLowerLink[*nSector];
+    if (nUpper >= 0)
+    {
+        spritetype *pUpper = &sprite[nUpper];
+        int z1;
+        if (pUpper->type == 7)
+            z1 = pUpper->z;
+        else
+            z1 = getflorzofslope(*nSector, *x, *y);
+        if (z1 <= *z)
+        {
+            nLower = pUpper->owner;
+            dassert(nLower >= 0 && nLower < kMaxSprites);
+            spritetype *pLower = &sprite[nLower];
+            dassert(pLower->sectnum >= 0 && pLower->sectnum < kMaxSectors);
+            *nSector = pLower->sectnum;
+            *x += pLower->x-pUpper->x;
+            *y += pLower->y-pUpper->y;
+            int z2;
+            if (pUpper->type == 6)
+                z2 = pLower->z;
+            else
+                z2 = getceilzofslope(*nSector, *x, *y);
+            *z += z2-z1;
+            return pUpper->type;
+        }
+    }
+    if (nLower >= 0)
+    {
+        spritetype *pLower = &sprite[nLower];
+        int z1;
+        if (pLower->type == 6)
+            z1 = pLower->z;
+        else
+            z1 = getceilzofslope(*nSector, *x, *y);
+        if (z1 >= *z)
+        {
+            nUpper = pLower->owner;
+            dassert(nUpper >= 0 && nUpper < kMaxSprites);
+            spritetype *pUpper = &sprite[nUpper];
+            dassert(pUpper->sectnum >= 0 && pUpper->sectnum < kMaxSectors);
+            *nSector = pUpper->sectnum;
+            *x += pUpper->x-pLower->x;
+            *y += pUpper->y-pLower->y;
+            int z2;
+            if (pLower->type == 7)
+                z2 = pUpper->z;
+            else
+                z2 = getflorzofslope(*nSector, *x, *y);
+            *z += z2-z1;
+            return pLower->type;
+        }
+    }
+    return 0;
+}
+
+class WarpLoadSave : public LoadSave
+{
+public:
+    virtual void Load();
+    virtual void Save();
+};
+
+void WarpLoadSave::Load()
+{
+    Read(gStartZone, sizeof(gStartZone));
+    Read(gUpperLink, sizeof(gUpperLink));
+    Read(gLowerLink, sizeof(gLowerLink));
+}
+
+void WarpLoadSave::Save()
+{
+    Write(gStartZone, sizeof(gStartZone));
+    Write(gUpperLink, sizeof(gUpperLink));
+    Write(gLowerLink, sizeof(gLowerLink));
+}
+
+static WarpLoadSave *myLoadSave;
+
+void WarpLoadSaveConstruct(void)
+{
+    myLoadSave = new WarpLoadSave();
+}
diff --git a/source/blood/src/warp.h b/source/blood/src/warp.h
new file mode 100644
index 000000000..48d4c0641
--- /dev/null
+++ b/source/blood/src/warp.h
@@ -0,0 +1,37 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "common_game.h"
+
+struct ZONE {
+    int x, y, z;
+    short sectnum, ang;
+};
+extern ZONE gStartZone[8];
+extern ZONE gStartZoneTeam1[8];
+extern ZONE gStartZoneTeam2[8];
+extern bool gTeamsSpawnUsed;
+
+void warpInit(void);
+int CheckLink(spritetype *pSprite);
+int CheckLink(int *x, int *y, int *z, int *nSector);
diff --git a/source/blood/src/weapon.cpp b/source/blood/src/weapon.cpp
new file mode 100644
index 000000000..cb57638fb
--- /dev/null
+++ b/source/blood/src/weapon.cpp
@@ -0,0 +1,2473 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "compat.h"
+#include "build.h"
+#include "mmulti.h"
+#include "common_game.h"
+#include "actor.h"
+#include "blood.h"
+#include "db.h"
+#include "callback.h"
+#include "config.h"
+#include "eventq.h"
+#include "fx.h"
+#include "gameutil.h"
+#include "globals.h"
+#include "levels.h"
+#include "loadsave.h"
+#include "player.h"
+#include "qav.h"
+#include "resource.h"
+#include "seq.h"
+#include "sfx.h"
+#include "sound.h"
+#include "trig.h"
+#include "triggers.h"
+#include "view.h"
+
+void FirePitchfork(int, PLAYER *pPlayer);
+void FireSpray(int, PLAYER *pPlayer);
+void ThrowCan(int, PLAYER *pPlayer);
+void DropCan(int, PLAYER *pPlayer);
+void ExplodeCan(int, PLAYER *pPlayer);
+void ThrowBundle(int, PLAYER *pPlayer);
+void DropBundle(int, PLAYER *pPlayer);
+void ExplodeBundle(int, PLAYER *pPlayer);
+void ThrowProx(int, PLAYER *pPlayer);
+void DropProx(int, PLAYER *pPlayer);
+void ThrowRemote(int, PLAYER *pPlayer);
+void DropRemote(int, PLAYER *pPlayer);
+void FireRemote(int, PLAYER *pPlayer);
+void FireShotgun(int nTrigger, PLAYER *pPlayer);
+void EjectShell(int, PLAYER *pPlayer);
+void FireTommy(int nTrigger, PLAYER *pPlayer);
+void FireSpread(int nTrigger, PLAYER *pPlayer);
+void AltFireSpread(int nTrigger, PLAYER *pPlayer);
+void AltFireSpread2(int nTrigger, PLAYER *pPlayer);
+void FireFlare(int nTrigger, PLAYER *pPlayer);
+void AltFireFlare(int nTrigger, PLAYER *pPlayer);
+void FireVoodoo(int nTrigger, PLAYER *pPlayer);
+void AltFireVoodoo(int nTrigger, PLAYER *pPlayer);
+void DropVoodoo(int nTrigger, PLAYER *pPlayer);
+void FireTesla(int nTrigger, PLAYER *pPlayer);
+void AltFireTesla(int nTrigger, PLAYER *pPlayer);
+void FireNapalm(int nTrigger, PLAYER *pPlayer);
+void FireNapalm2(int nTrigger, PLAYER *pPlayer);
+void AltFireNapalm(int nTrigger, PLAYER *pPlayer);
+void FireLifeLeech(int nTrigger, PLAYER *pPlayer);
+void AltFireLifeLeech(int nTrigger, PLAYER *pPlayer);
+void FireBeast(int nTrigger, PLAYER * pPlayer);
+
+typedef void(*QAVTypeCast)(int, void *);
+
+int nClientFirePitchfork = qavRegisterClient((QAVTypeCast)FirePitchfork);
+int nClientFireSpray = qavRegisterClient((QAVTypeCast)FireSpray);
+int nClientThrowCan = qavRegisterClient((QAVTypeCast)ThrowCan);
+int nClientDropCan = qavRegisterClient((QAVTypeCast)DropCan);
+int nClientExplodeCan = qavRegisterClient((QAVTypeCast)ExplodeCan);
+int nClientThrowBundle = qavRegisterClient((QAVTypeCast)ThrowBundle);
+int nClientDropBundle = qavRegisterClient((QAVTypeCast)DropBundle);
+int nClientExplodeBundle = qavRegisterClient((QAVTypeCast)ExplodeBundle);
+int nClientThrowProx = qavRegisterClient((QAVTypeCast)ThrowProx);
+int nClientDropProx = qavRegisterClient((QAVTypeCast)DropProx);
+int nClientThrowRemote = qavRegisterClient((QAVTypeCast)ThrowRemote);
+int nClientDropRemote = qavRegisterClient((QAVTypeCast)DropRemote);
+int nClientFireRemote = qavRegisterClient((QAVTypeCast)FireRemote);
+int nClientFireShotgun = qavRegisterClient((QAVTypeCast)FireShotgun);
+int nClientEjectShell = qavRegisterClient((QAVTypeCast)EjectShell);
+int nClientFireTommy = qavRegisterClient((QAVTypeCast)FireTommy);
+int nClientAltFireSpread2 = qavRegisterClient((QAVTypeCast)AltFireSpread2);
+int nClientFireSpread = qavRegisterClient((QAVTypeCast)FireSpread);
+int nClientAltFireSpread = qavRegisterClient((QAVTypeCast)AltFireSpread);
+int nClientFireFlare = qavRegisterClient((QAVTypeCast)FireFlare);
+int nClientAltFireFlare = qavRegisterClient((QAVTypeCast)AltFireFlare);
+int nClientFireVoodoo = qavRegisterClient((QAVTypeCast)FireVoodoo);
+int nClientAltFireVoodoo = qavRegisterClient((QAVTypeCast)AltFireVoodoo);
+int nClientFireTesla = qavRegisterClient((QAVTypeCast)FireTesla);
+int nClientAltFireTesla = qavRegisterClient((QAVTypeCast)AltFireTesla);
+int nClientFireNapalm = qavRegisterClient((QAVTypeCast)FireNapalm);
+int nClientFireNapalm2 = qavRegisterClient((QAVTypeCast)FireNapalm2);
+int nClientFireLifeLeech = qavRegisterClient((QAVTypeCast)FireLifeLeech);
+int nClientFireBeast = qavRegisterClient((QAVTypeCast)FireBeast);
+int nClientAltFireLifeLeech = qavRegisterClient((QAVTypeCast)AltFireLifeLeech);
+int nClientDropVoodoo = qavRegisterClient((QAVTypeCast)DropVoodoo);
+int nClientAltFireNapalm = qavRegisterClient((QAVTypeCast)AltFireNapalm);
+
+#define kQAVEnd 125
+
+QAV *weaponQAV[kQAVEnd];
+
+void QAV::PlaySound(int nSound)
+{
+    sndStartSample(nSound, -1, -1, 0);
+}
+
+void QAV::PlaySound3D(spritetype *pSprite, int nSound, int a3, int a4)
+{
+    sfxPlay3DSound(pSprite, nSound, a3, a4);
+}
+
+char sub_4B1A4(PLAYER *pPlayer)
+{
+    switch (pPlayer->atbd)
+    {
+    case 7:
+        switch (pPlayer->atc3)
+        {
+        case 5:
+        case 6:
+            return 1;
+        }
+        break;
+    case 6:
+        switch (pPlayer->atc3)
+        {
+        case 4:
+        case 5:
+        case 6:
+            return 1;
+        }
+        break;
+    }
+    return 0;
+}
+
+char BannedUnderwater(int nWeapon)
+{
+    return nWeapon == 7 || nWeapon == 6;
+}
+
+char sub_4B1FC(PLAYER *pPlayer, int a2, int a3, int a4)
+{
+    if (gInfiniteAmmo)
+        return 1;
+    if (a3 == -1)
+        return 1;
+    if (a2 == 12 && pPlayer->atc7 == 11 && pPlayer->atc3 == 11)
+        return 1;
+    if (a2 == 9 && pPlayer->pXSprite->health > 0)
+        return 1;
+    return pPlayer->at181[a3] >= a4;
+}
+
+char CheckAmmo(PLAYER *pPlayer, int a2, int a3)
+{
+    if (gInfiniteAmmo)
+        return 1;
+    if (a2 == -1)
+        return 1;
+    if (pPlayer->atbd == 12 && pPlayer->atc7 == 11 && pPlayer->atc3 == 11)
+        return 1;
+    if (pPlayer->atbd == 9 && pPlayer->pXSprite->health >= (a3<<4))
+        return 1;
+    return pPlayer->at181[a2] >= a3;
+}
+
+char sub_4B2C8(PLAYER *pPlayer, int a2, int a3)
+{
+    if (gInfiniteAmmo)
+        return 1;
+    if (a2 == -1)
+        return 1;
+    return pPlayer->at181[a2] >= a3;
+}
+
+void SpawnBulletEject(PLAYER *pPlayer, int a2, int a3)
+{
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][pPlayer->at2f];
+    pPlayer->at67 = pPlayer->pSprite->z-pPosture->at24;
+    int dz = pPlayer->at6f-(pPlayer->at6f-pPlayer->at67)/2;
+    sub_74818(pPlayer->pSprite, dz, a2, a3);
+}
+
+void SpawnShellEject(PLAYER *pPlayer, int a2, int a3)
+{
+    POSTURE *pPosture = &gPosture[pPlayer->at5f][pPlayer->at2f];
+    pPlayer->at67 = pPlayer->pSprite->z-pPosture->at24;
+    int t = pPlayer->at6f - pPlayer->at67;
+    int dz = pPlayer->at6f-t+(t>>2);
+    sub_74A18(pPlayer->pSprite, dz, a2, a3);
+}
+
+void WeaponInit(void)
+{
+    for (int i = 0; i < kQAVEnd; i++)
+    {
+        DICTNODE *hRes = gSysRes.Lookup(i, "QAV");
+        if (!hRes)
+            ThrowError("Could not load QAV %d\n", i);
+        weaponQAV[i] = (QAV*)gSysRes.Lock(hRes);
+        weaponQAV[i]->nSprite = -1;
+    }
+}
+
+void WeaponDraw(PLAYER *pPlayer, int a2, int a3, int a4, int a5)
+{
+    dassert(pPlayer != NULL);
+    if (pPlayer->at26 == -1)
+        return;
+    QAV *pQAV = weaponQAV[pPlayer->at26];
+    int v4;
+    if (pPlayer->atbf == 0)
+        v4 = gGameClock%pQAV->at10;
+    else
+        v4 = pQAV->at10-pPlayer->atbf;
+    pQAV->x = a3;
+    pQAV->y = a4;
+    int flags = 2;
+    int nInv = powerupCheck(pPlayer, 13);
+    if (nInv >= 120*8 || (nInv != 0 && (gGameClock&32)))
+    {
+        a2 = -128;
+        flags |= 1;
+    }
+    pQAV->Draw(v4, flags, a2, a5);
+}
+
+void WeaponPlay(PLAYER *pPlayer)
+{
+    dassert(pPlayer != NULL);
+    if (pPlayer->at26 == -1)
+        return;
+    QAV *pQAV = weaponQAV[pPlayer->at26];
+    pQAV->nSprite = pPlayer->pSprite->index;
+    int nTicks = pQAV->at10 - pPlayer->atbf;
+    pQAV->Play(nTicks-4, nTicks, pPlayer->at2a, pPlayer);
+}
+
+void StartQAV(PLAYER *pPlayer, int nWeaponQAV, int a3 = -1, char a4 = 0)
+{
+    dassert(nWeaponQAV < kQAVEnd);
+    pPlayer->at26 = nWeaponQAV;
+    pPlayer->atbf = weaponQAV[nWeaponQAV]->at10;
+    pPlayer->at2a = a3;
+    pPlayer->at1b1 = a4;
+    weaponQAV[nWeaponQAV]->Preload();
+    WeaponPlay(pPlayer);
+    pPlayer->atbf -= 4;
+}
+
+struct WEAPONTRACK
+{
+    int at0; // x aim speed
+    int at4; // y aim speed
+    int at8; // angle range
+    int atc;
+    int at10; // predict
+    bool bIsProjectile;
+};
+
+WEAPONTRACK gWeaponTrack[] = {
+    { 0, 0, 0, 0, 0, false },
+    { 0x6000, 0x6000, 0x71, 0x55, 0x111111, false },
+    { 0x8000, 0x8000, 0x71, 0x55, 0x2aaaaa, true },
+    { 0x10000, 0x10000, 0x38, 0x1c, 0, false },
+    { 0x6000, 0x8000, 0x38, 0x1c, 0, false },
+    { 0x6000, 0x6000, 0x38, 0x1c, 0x2aaaaa, true },
+    { 0x6000, 0x6000, 0x71, 0x55, 0, true },
+    { 0x6000, 0x6000, 0x71, 0x38, 0, true },
+    { 0x8000, 0x10000, 0x71, 0x55, 0x255555, true },
+    { 0x10000, 0x10000, 0x71, 0, 0, true },
+    { 0x10000, 0x10000, 0xaa, 0, 0, false },
+    { 0x6000, 0x6000, 0x71, 0x55, 0, true },
+    { 0x6000, 0x6000, 0x71, 0x55, 0, true },
+    { 0x6000, 0x6000, 0x71, 0x55, 0, false },
+};
+
+void UpdateAimVector(PLAYER * pPlayer)
+{
+    short nSprite;
+    spritetype *pSprite;
+    dassert(pPlayer != NULL);
+    spritetype *pPSprite = pPlayer->pSprite;
+    int x = pPSprite->x;
+    int y = pPSprite->y;
+    int z = pPlayer->at6f;
+    Aim aim;
+    aim.dx = Cos(pPSprite->ang)>>16;
+    aim.dy = Sin(pPSprite->ang)>>16;
+    aim.dz = pPlayer->at83;
+    WEAPONTRACK *pWeaponTrack = &gWeaponTrack[pPlayer->atbd];
+    int nTarget = -1;
+    pPlayer->at1da = 0;
+    if (gProfile[pPlayer->at57].nAutoAim == 1 || (gProfile[pPlayer->at57].nAutoAim == 2 && !pWeaponTrack->bIsProjectile) || pPlayer->atbd == 10 || pPlayer->atbd == 9)
+    {
+        int nClosest = 0x7fffffff;
+        for (nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+        {
+            pSprite = &sprite[nSprite];
+            if (pSprite == pPSprite)
+                continue;
+            if (pSprite->hitag&32)
+                continue;
+            if (!(pSprite->hitag&8))
+                continue;
+            int x2 = pSprite->x;
+            int y2 = pSprite->y;
+            int z2 = pSprite->z;
+            int nDist = approxDist(x2-x, y2-y);
+            if (nDist == 0 || nDist > 51200)
+                continue;
+            if (pWeaponTrack->at10)
+            {
+                int t = divscale(nDist,pWeaponTrack->at10, 12);
+                x2 += (xvel[nSprite]*t)>>12;
+                y2 += (yvel[nSprite]*t)>>12;
+                z2 += (zvel[nSprite]*t)>>8;
+            }
+            int lx = x + mulscale30(Cos(pPSprite->ang), nDist);
+            int ly = y + mulscale30(Sin(pPSprite->ang), nDist);
+            int lz = z + mulscale(pPlayer->at83, nDist, 10);
+            int zRange = mulscale(9460, nDist, 10);
+            int top, bottom;
+            GetSpriteExtents(pSprite, &top, &bottom);
+            if (lz-zRange>bottom || lz+zRange<top)
+                continue;
+            int angle = getangle(x2-x,y2-y);
+            if (klabs(((angle-pPSprite->ang+1024)&2047)-1024) > pWeaponTrack->at8)
+                continue;
+            if (pPlayer->at1da < 16 && cansee(x,y,z,pPSprite->sectnum,x2,y2,z2,pSprite->sectnum))
+                pPlayer->at1de[pPlayer->at1da++] = nSprite;
+            // Inlined?
+            int dz = (lz-z2)>>8;
+            int dy = (ly-y2)>>4;
+            int dx = (lx-x2)>>4;
+            int nDist2 = ksqrt(dx*dx+dy*dy+dz*dz);
+            if (nDist2 >= nClosest)
+                continue;
+            DUDEINFO *pDudeInfo = &dudeInfo[pSprite->type-kDudeBase];
+            int center = (pSprite->yrepeat*pDudeInfo->aimHeight)<<2;
+            int dzCenter = (z2-center)-z;
+            if (cansee(x, y, z, pPSprite->sectnum, x2, y2, z2, pSprite->sectnum))
+            {
+                nClosest = nDist2;
+                aim.dx = Cos(angle)>>16;
+                aim.dy = Sin(angle)>>16;
+                aim.dz = divscale(dzCenter, nDist, 10);
+                nTarget = nSprite;
+            }
+        }
+        if (pWeaponTrack->atc > 0)
+        {
+            for (nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+            {
+                pSprite = &sprite[nSprite];
+                if (!(pSprite->hitag&8))
+                    continue;
+                int x2 = pSprite->x;
+                int y2 = pSprite->y;
+                int z2 = pSprite->z;
+                int dx = x2-x;
+                int dy = y2-y;
+                int dz = z2-z;
+                int nDist = approxDist(dx, dy);
+                if (nDist == 0 || nDist > 51200)
+                    continue;
+                int lx = x + mulscale30(Cos(pPSprite->ang), nDist);
+                int ly = y + mulscale30(Sin(pPSprite->ang), nDist);
+                int lz = z + mulscale(pPlayer->at83, nDist, 10);
+                int zRange = mulscale10(9460, nDist);
+                int top, bottom;
+                GetSpriteExtents(pSprite, &top, &bottom);
+                if (lz-zRange>bottom || lz+zRange<top)
+                    continue;
+                int angle = getangle(dx,dy);
+                if (klabs(((angle-pPSprite->ang+1024)&2047)-1024) > pWeaponTrack->atc)
+                    continue;
+                if (pPlayer->at1da < 16 && cansee(x,y,z,pPSprite->sectnum,pSprite->x,pSprite->y,pSprite->z,pSprite->sectnum))
+                    pPlayer->at1de[pPlayer->at1da++] = nSprite;
+                // Inlined?
+                int dz2 = (lz-z2)>>8;
+                int dy2 = (ly-y2)>>4;
+                int dx2 = (lx-x2)>>4;
+                int nDist2 = ksqrt(dx2*dx2+dy2*dy2+dz2*dz2);
+                if (nDist2 >= nClosest)
+                    continue;
+                if (cansee(x, y, z, pPSprite->sectnum, pSprite->x, pSprite->y, pSprite->z, pSprite->sectnum))
+                {
+                    nClosest = nDist2;
+                    aim.dx = Cos(angle)>>16;
+                    aim.dy = Sin(angle)>>16;
+                    aim.dz = divscale(dz, nDist, 10);
+                    nTarget = nSprite;
+                }
+            }
+        }
+    }
+    Aim aim2;
+    aim2 = aim;
+    RotateVector((int*)&aim2.dx, (int*)&aim2.dy, -pPSprite->ang);
+    aim2.dz -= pPlayer->at83;
+    pPlayer->at1ca.dx = interpolate(pPlayer->at1ca.dx, aim2.dx, pWeaponTrack->at0);
+    pPlayer->at1ca.dy = interpolate(pPlayer->at1ca.dy, aim2.dy, pWeaponTrack->at0);
+    pPlayer->at1ca.dz = interpolate(pPlayer->at1ca.dz, aim2.dz, pWeaponTrack->at4);
+    pPlayer->at1be = pPlayer->at1ca;
+    RotateVector((int*)&pPlayer->at1be.dx, (int*)&pPlayer->at1be.dy, pPSprite->ang);
+    pPlayer->at1be.dz += pPlayer->at83;
+    pPlayer->at1d6 = nTarget;
+}
+
+struct t_WeaponModes
+{
+    int at0;
+    int at4;
+};
+
+t_WeaponModes weaponModes[] = {
+    { 0, -1 },
+    { 1, -1 },
+    { 1, 1 },
+    { 1, 2 },
+    { 1, 3 },
+    { 1, 4 },
+    { 1, 5 },
+    { 1, 6 },
+    { 1, 7 },
+    { 1, 8 },
+    { 1, 9 },
+    { 1, 10 },
+    { 1, 11 },
+    { 0, -1 },
+};
+
+void WeaponRaise(PLAYER *pPlayer)
+{
+    dassert(pPlayer != NULL);
+    int prevWeapon = pPlayer->atbd;
+    pPlayer->atbd = pPlayer->atc.newWeapon;
+    pPlayer->atc.newWeapon = 0;
+    pPlayer->atc7 = weaponModes[pPlayer->atbd].at4;
+    switch (pPlayer->atbd)
+    {
+    case 1: // pitchfork
+        pPlayer->atc3 = 0;
+        StartQAV(pPlayer, 0, -1, 0);
+        break;
+    case 7: // spraycan
+        if (pPlayer->atc3 == 2)
+        {
+            pPlayer->atc3 = 3;
+            StartQAV(pPlayer, 8, -1, 0);
+        }
+        else
+        {
+            pPlayer->atc3 = 0;
+            StartQAV(pPlayer, 4, -1, 0);
+        }
+        break;
+    case 6: // dynamite
+        if (gInfiniteAmmo || sub_4B2C8(pPlayer, 5, 1))
+        {
+            pPlayer->atc3 = 3;
+            if (prevWeapon == 7)
+                StartQAV(pPlayer, 16, -1, 0);
+            else
+                StartQAV(pPlayer, 18, -1, 0);
+        }
+        break;
+    case 11: // proximity
+        if (gInfiniteAmmo || sub_4B2C8(pPlayer, 10, 1))
+        {
+            pPlayer->atc3 = 7;
+            StartQAV(pPlayer, 25, -1, 0);
+        }
+        break;
+    case 12: // remote
+        if (gInfiniteAmmo || sub_4B2C8(pPlayer, 11, 1))
+        {
+            pPlayer->atc3 = 10;
+            StartQAV(pPlayer, 31, -1, 0);
+        }
+        else
+        {
+            StartQAV(pPlayer, 32, -1, 0);
+            pPlayer->atc3 = 11;
+        }
+        break;
+    case 3: // sawed off
+        if (powerupCheck(pPlayer, 17))
+        {
+            if (gInfiniteAmmo || pPlayer->at181[2] >= 4)
+                StartQAV(pPlayer, 59, -1, 0);
+            else
+                StartQAV(pPlayer, 50, -1, 0);
+            if (gInfiniteAmmo || pPlayer->at181[2] >= 4)
+                pPlayer->atc3 = 7;
+            else if (pPlayer->at181[2] > 1)
+                pPlayer->atc3 = 3;
+            else if (pPlayer->at181[2] > 0)
+                pPlayer->atc3 = 2;
+            else
+                pPlayer->atc3 = 1;
+        }
+        else
+        {
+            if (gInfiniteAmmo || pPlayer->at181[2] > 1)
+                pPlayer->atc3 = 3;
+            else if (pPlayer->at181[2] > 0)
+                pPlayer->atc3 = 2;
+            else
+                pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 50, -1, 0);
+        }
+        break;
+    case 4: // tommy gun
+        if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 3, 2))
+        {
+            pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 69, -1, 0);
+        }
+        else
+        {
+            pPlayer->atc3 = 0;
+            StartQAV(pPlayer, 64, -1, 0);
+        }
+        break;
+    case 10: // voodoo
+        if (gInfiniteAmmo || sub_4B2C8(pPlayer, 9, 1))
+        {
+            pPlayer->atc3 = 2;
+            StartQAV(pPlayer, 100, -1, 0);
+        }
+        break;
+    case 2: // flaregun
+        if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 1, 2))
+        {
+            StartQAV(pPlayer, 45, -1, 0);
+            pPlayer->atc3 = 3;
+        }
+        else
+        {
+            StartQAV(pPlayer, 41, -1, 0);
+            pPlayer->atc3 = 2;
+        }
+        break;
+    case 8: // tesla cannon
+        if (sub_4B2C8(pPlayer, 7, 1))
+        {
+            pPlayer->atc3 = 2;
+            if (powerupCheck(pPlayer, 17))
+                StartQAV(pPlayer, 82, -1, 0);
+            else
+                StartQAV(pPlayer, 74, -1, 0);
+        }
+        else
+        {
+            pPlayer->atc3 = 3;
+            StartQAV(pPlayer, 74, -1, 0);
+        }
+        break;
+    case 5: // napalm
+        if (powerupCheck(pPlayer, 17))
+        {
+            StartQAV(pPlayer, 120, -1, 0);
+            pPlayer->atc3 = 3;
+        }
+        else
+        {
+            StartQAV(pPlayer, 89, -1, 0);
+            pPlayer->atc3 = 2;
+        }
+        break;
+    case 9: // life leech
+        pPlayer->atc3 = 2;
+        StartQAV(pPlayer, 111, -1, 0);
+        break;
+    case 13: // beast
+        pPlayer->atc3 = 2;
+        StartQAV(pPlayer, 93, -1, 0);
+        break;
+    }
+}
+
+void WeaponLower(PLAYER *pPlayer)
+{
+    dassert(pPlayer != NULL);
+    if (sub_4B1A4(pPlayer))
+        return;
+    pPlayer->at1ba = 0;
+    int prevState = pPlayer->atc3;
+    switch (pPlayer->atbd)
+    {
+    case 1:
+        StartQAV(pPlayer, 3, -1, 0);
+        break;
+    case 7:
+        sfxKill3DSound(pPlayer->pSprite, -1, 441);
+        switch (prevState)
+        {
+        case 1:
+            StartQAV(pPlayer, 7, -1, 0);
+            break;
+        case 2:
+            pPlayer->atc3 = 1;
+            WeaponRaise(pPlayer);
+            return;
+        case 4:
+            pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 11, -1, 0);
+            pPlayer->atc.newWeapon = 0;
+            WeaponLower(pPlayer);
+            break;
+        case 3:
+            if (pPlayer->atc.newWeapon == 6)
+            {
+                pPlayer->atc3 = 2;
+                StartQAV(pPlayer, 11, -1, 0);
+                return;
+            }
+            else if (pPlayer->atc.newWeapon == 7)
+            {
+                pPlayer->atc3 = 1;
+                StartQAV(pPlayer, 11, -1, 0);
+                pPlayer->atc.newWeapon = 0;
+                WeaponLower(pPlayer);
+            }
+            else
+            {
+                pPlayer->atc3 = 1;
+                StartQAV(pPlayer, 11, -1, 0);
+            }
+            break;
+        }
+        break;
+    case 6:
+        switch (prevState)
+        {
+        case 1:
+            StartQAV(pPlayer, 7, -1, 0);
+            break;
+        case 2:
+            WeaponRaise(pPlayer);
+            break;
+        case 3:
+            if (pPlayer->atc.newWeapon == 7)
+            {
+                pPlayer->atc3 = 2;
+                StartQAV(pPlayer, 17, -1, 0);
+            }
+            else
+            {
+                StartQAV(pPlayer, 19, -1, 0);
+            }
+            break;
+        default:
+            break;
+        }
+        break;
+    case 11:
+        switch (prevState)
+        {
+        case 7:
+            StartQAV(pPlayer, 26, -1, 0);
+            break;
+        }
+        break;
+    case 12:
+        switch (prevState)
+        {
+        case 10:
+            StartQAV(pPlayer, 34, -1, 0);
+            break;
+        case 11:
+            StartQAV(pPlayer, 35, -1, 0);
+            break;
+        }
+        break;
+    case 3:
+        if (powerupCheck(pPlayer, 17))
+            StartQAV(pPlayer, 63, -1, 0);
+        else
+            StartQAV(pPlayer, 58, -1, 0);
+        break;
+    case 4:
+        if (powerupCheck(pPlayer, 17) && pPlayer->atc3 == 1)
+            StartQAV(pPlayer, 72, -1, 0);
+        else
+            StartQAV(pPlayer, 68, -1, 0);
+        break;
+    case 2:
+        if (powerupCheck(pPlayer, 17) && pPlayer->atc3 == 3)
+            StartQAV(pPlayer, 49, -1, 0);
+        else
+            StartQAV(pPlayer, 44, -1, 0);
+        break;
+    case 10:
+        StartQAV(pPlayer, 109, -1, 0);
+        break;
+    case 8:
+        if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+            StartQAV(pPlayer, 88, -1, 0);
+        else
+            StartQAV(pPlayer, 81, -1, 0);
+        break;
+    case 5:
+        if (powerupCheck(pPlayer, 17))
+            StartQAV(pPlayer, 124, -1, 0);
+        else
+            StartQAV(pPlayer, 92, -1, 0);
+        break;
+    case 9:
+        StartQAV(pPlayer, 119, -1, 0);
+        break;
+    case 13:
+        StartQAV(pPlayer, 99, -1, 0);
+        break;
+    }
+    pPlayer->atbd = 0;
+    pPlayer->at1b1 = 0;
+}
+
+void WeaponUpdateState(PLAYER *pPlayer)
+{
+    static int lastWeapon = 0;
+    static int lastState = 0;
+    XSPRITE *pXSprite = pPlayer->pXSprite;
+    int va = pPlayer->atbd;
+    int vb = pPlayer->atc3;
+    if (va != lastWeapon || vb != lastState)
+    {
+        lastWeapon = va;
+        lastState = vb;
+    }
+    switch (lastWeapon)
+    {
+    case 1:
+        pPlayer->at26 = 1;
+        break;
+    case 7:
+        switch (vb)
+        {
+        case 0:
+            pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 5, -1, 0);
+            break;
+        case 1:
+            if (CheckAmmo(pPlayer, 6, 1))
+            {
+                pPlayer->atc3 = 3;
+                StartQAV(pPlayer, 8, -1, 0);
+            }
+            else
+                pPlayer->at26 = 6;
+            break;
+        case 3:
+            pPlayer->at26 = 9;
+            break;
+        case 4:
+            if (CheckAmmo(pPlayer, 6, 1))
+            {
+                pPlayer->at26 = 9;
+                pPlayer->atc3 = 3;
+            }
+            else
+            {
+                pPlayer->atc3 = 1;
+                StartQAV(pPlayer, 11, -1, 0);
+            }
+            sfxKill3DSound(pPlayer->pSprite, -1, 441);
+            break;
+        }
+        break;
+    case 6:
+        switch (vb)
+        {
+        case 1:
+            if (pPlayer->atc7 == 5 && CheckAmmo(pPlayer, 5, 1))
+            {
+                pPlayer->atc3 = 3;
+                StartQAV(pPlayer, 16, -1, 0);
+            }
+            break;
+        case 0:
+            pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 5, -1, 0);
+            break;
+        case 2:
+            if (pPlayer->at181[5] > 0)
+            {
+                pPlayer->atc3 = 3;
+                StartQAV(pPlayer, 16, -1, 0);
+            }
+            else
+                pPlayer->at26 = 6;
+            break;
+        case 3:
+            pPlayer->at26 = 20;
+            break;
+        }
+        break;
+    case 11:
+        switch (vb)
+        {
+        case 7:
+            pPlayer->at26 = 27;
+            break;
+        case 8:
+            pPlayer->atc3 = 7;
+            StartQAV(pPlayer, 25, -1, 0);
+            break;
+        }
+        break;
+    case 12:
+        switch (vb)
+        {
+        case 10:
+            pPlayer->at26 = 36;
+            break;
+        case 11:
+            pPlayer->at26 = 37;
+            break;
+        case 12:
+            if (pPlayer->at181[11] > 0)
+            {
+                pPlayer->atc3 = 10;
+                StartQAV(pPlayer, 31, -1, 0);
+            }
+            else
+                pPlayer->atc3 = -1;
+            break;
+        }
+        break;
+    case 3:
+        switch (vb)
+        {
+        case 6:
+            if (powerupCheck(pPlayer, 17) && (gInfiniteAmmo || CheckAmmo(pPlayer, 2, 4)))
+                pPlayer->atc3 = 7;
+            else
+                pPlayer->atc3 = 1;
+            break;
+        case 7:
+            pPlayer->at26 = 60;
+            break;
+        case 1:
+            if (CheckAmmo(pPlayer, 2, 1))
+            {
+                sfxPlay3DSound(pPlayer->pSprite, 410, 3, 2);
+                StartQAV(pPlayer, 57, nClientEjectShell, 0);
+                if (gInfiniteAmmo || pPlayer->at181[2] > 1)
+                    pPlayer->atc3 = 3;
+                else
+                    pPlayer->atc3 = 2;
+            }
+            else
+                pPlayer->at26 = 51;
+            break;
+        case 2:
+            pPlayer->at26 = 52;
+            break;
+        case 3:
+            pPlayer->at26 = 53;
+            break;
+        }
+        break;
+    case 4:
+        if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 3, 2))
+        {
+            pPlayer->at26 = 70;
+            pPlayer->atc3 = 1;
+        }
+        else
+        {
+            pPlayer->at26 = 65;
+            pPlayer->atc3 = 0;
+        }
+        break;
+    case 2:
+        if (powerupCheck(pPlayer, 17))
+        {
+            if (vb == 3 && sub_4B2C8(pPlayer, 1, 2))
+                pPlayer->at26 = 46;
+            else
+            {
+                pPlayer->at26 = 42;
+                pPlayer->atc3 = 2;
+            }
+        }
+        else
+            pPlayer->at26 = 42;
+        break;
+    case 10:
+        if (pXSprite->height < 256 && klabs(pPlayer->at4f) > 768)
+            pPlayer->at26 = 102;
+        else
+            pPlayer->at26 = 101;
+        break;
+    case 8:
+        switch (vb)
+        {
+        case 2:
+            if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+                pPlayer->at26 = 83;
+            else
+                pPlayer->at26 = 75;
+            break;
+        case 3:
+            pPlayer->at26 = 76;
+            break;
+        }
+        break;
+    case 5:
+        switch (vb)
+        {
+        case 3:
+            if (powerupCheck(pPlayer, 17) && (gInfiniteAmmo || CheckAmmo(pPlayer,4, 4)))
+                pPlayer->at26 = 121;
+            else
+                pPlayer->at26 = 90;
+            break;
+        case 2:
+            pPlayer->at26 = 90;
+            break;
+        }
+        break;
+    case 9:
+        switch (vb)
+        {
+        case 2:
+            pPlayer->at26 = 112;
+            break;
+        }
+        break;
+    case 13:
+        pPlayer->at26 = 94;
+        break;
+    }
+}
+
+void FirePitchfork(int, PLAYER *pPlayer)
+{
+    Aim *aim = &pPlayer->at1be;
+    int r1 = Random2(2000);
+    int r2 = Random2(2000);
+    int r3 = Random2(2000);
+    for (int i = 0; i < 4; i++)
+        actFireVector(pPlayer->pSprite, (2*i-3)*40, pPlayer->at6f-pPlayer->pSprite->z, aim->dx+r1, aim->dy+r2, aim->dz+r3, VECTOR_TYPE_0);
+}
+
+void FireSpray(int, PLAYER *pPlayer)
+{
+    playerFireMissile(pPlayer, 0, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 304);
+    UseAmmo(pPlayer, 6, 4);
+    if (CheckAmmo(pPlayer, 6, 1))
+        sfxPlay3DSound(pPlayer->pSprite, 441, 1, 2);
+    else
+        sfxKill3DSound(pPlayer->pSprite, -1, 441);
+}
+
+void ThrowCan(int, PLAYER *pPlayer)
+{
+    sfxKill3DSound(pPlayer->pSprite, -1, 441);
+    int nSpeed = mulscale16(pPlayer->at1ba, 0x177777)+0x66666;
+    sfxPlay3DSound(pPlayer->pSprite, 455, 1, 0);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, -9460, 420, nSpeed);
+    if (pSprite)
+    {
+        sfxPlay3DSound(pSprite, 441, 0, 0);
+        evPost(pSprite->index, 3, pPlayer->at1b2, COMMAND_ID_1);
+        int nXSprite = pSprite->extra;
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        pXSprite->Impact = 1;
+        UseAmmo(pPlayer, 6, gAmmoItemData[0].at8);
+        pPlayer->at1ba = 0;
+    }
+}
+
+void DropCan(int, PLAYER *pPlayer)
+{
+    sfxKill3DSound(pPlayer->pSprite, -1, 441);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, 0, 420, 0);
+    if (pSprite)
+    {
+        evPost(pSprite->index, 3, pPlayer->at1b2, COMMAND_ID_1);
+        UseAmmo(pPlayer, 6, gAmmoItemData[0].at8);
+    }
+}
+
+void ExplodeCan(int, PLAYER *pPlayer)
+{
+    sfxKill3DSound(pPlayer->pSprite, -1, 441);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, 0, 420, 0);
+    evPost(pSprite->index, 3, 0, COMMAND_ID_1);
+    UseAmmo(pPlayer, 6, gAmmoItemData[0].at8);
+    StartQAV(pPlayer, 15, -1);
+    pPlayer->atbd = 0;
+    pPlayer->at1ba = 0;
+}
+
+void ThrowBundle(int, PLAYER *pPlayer)
+{
+    sfxKill3DSound(pPlayer->pSprite, 16, -1);
+    int nSpeed = mulscale16(pPlayer->at1ba, 0x177777)+0x66666;
+    sfxPlay3DSound(pPlayer->pSprite, 455, 1, 0);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, -9460, 419, nSpeed);
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    if (pPlayer->at1b2 < 0)
+        pXSprite->Impact = 1;
+    else
+        evPost(pSprite->index, 3, pPlayer->at1b2, COMMAND_ID_1);
+    UseAmmo(pPlayer, 5, 1);
+    pPlayer->at1ba = 0;
+}
+
+void DropBundle(int, PLAYER *pPlayer)
+{
+    sfxKill3DSound(pPlayer->pSprite, 16, -1);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, 0, 419, 0);
+    evPost(pSprite->index, 3, pPlayer->at1b2, COMMAND_ID_1);
+    UseAmmo(pPlayer, 5, 1);
+}
+
+void ExplodeBundle(int, PLAYER *pPlayer)
+{
+    sfxKill3DSound(pPlayer->pSprite, 16, -1);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, 0, 419, 0);
+    evPost(pSprite->index, 3, 0, COMMAND_ID_1);
+    UseAmmo(pPlayer, 5, 1);
+    StartQAV(pPlayer, 24, -1, 0);
+    pPlayer->atbd = 0;
+    pPlayer->at1ba = 0;
+}
+
+void ThrowProx(int, PLAYER *pPlayer)
+{
+    int nSpeed = mulscale16(pPlayer->at1ba, 0x177777)+0x66666;
+    sfxPlay3DSound(pPlayer->pSprite, 455, 1, 0);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, -9460, 401, nSpeed);
+    evPost(pSprite->index, 3, 240, COMMAND_ID_1);
+    UseAmmo(pPlayer, 10, 1);
+    pPlayer->at1ba = 0;
+}
+
+void DropProx(int, PLAYER *pPlayer)
+{
+    spritetype *pSprite = playerFireThing(pPlayer, 0, 0, 401, 0);
+    evPost(pSprite->index, 3, 240, COMMAND_ID_1);
+    UseAmmo(pPlayer, 10, 1);
+}
+
+void ThrowRemote(int, PLAYER *pPlayer)
+{
+    int nSpeed = mulscale16(pPlayer->at1ba, 0x177777)+0x66666;
+    sfxPlay3DSound(pPlayer->pSprite, 455, 1, 0);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, -9460, 402, nSpeed);
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    pXSprite->rxID = 90+(pPlayer->pSprite->type-kDudePlayer1);
+    UseAmmo(pPlayer, 11, 1);
+    pPlayer->at1ba = 0;
+}
+
+void DropRemote(int, PLAYER *pPlayer)
+{
+    spritetype *pSprite = playerFireThing(pPlayer, 0, 0, 402, 0);
+    int nXSprite = pSprite->extra;
+    XSPRITE *pXSprite = &xsprite[nXSprite];
+    pXSprite->rxID = 90+(pPlayer->pSprite->type-kDudePlayer1);
+    UseAmmo(pPlayer, 11, 1);
+}
+
+void FireRemote(int, PLAYER *pPlayer)
+{
+    evSend(0, 0, 90+(pPlayer->pSprite->type-kDudePlayer1), COMMAND_ID_1);
+}
+
+#define kMaxShotgunBarrels 4
+
+void FireShotgun(int nTrigger, PLAYER *pPlayer)
+{
+    dassert(nTrigger > 0 && nTrigger <= kMaxShotgunBarrels);
+    if (nTrigger == 1)
+    {
+        sfxPlay3DSound(pPlayer->pSprite, 411, 2, 0);
+        pPlayer->at35e = 30;
+        pPlayer->at362 = 20;
+    }
+    else
+    {
+        sfxPlay3DSound(pPlayer->pSprite, 412, 2, 0);
+        pPlayer->at35e = 50;
+        pPlayer->at362 = 40;
+    }
+    int n = nTrigger<<4;
+    for (int i = 0; i < n; i++)
+    {
+        int r1, r2, r3;
+        VECTOR_TYPE nType;
+        if (nTrigger == 1)
+        {
+            r1 = Random3(1500);
+            r2 = Random3(1500);
+            r3 = Random3(500);
+            nType = VECTOR_TYPE_1;
+        }
+        else
+        {
+            r1 = Random3(2500);
+            r2 = Random3(2500);
+            r3 = Random3(1500);
+            nType = VECTOR_TYPE_4;
+        }
+        actFireVector(pPlayer->pSprite, 0, pPlayer->at6f-pPlayer->pSprite->z, pPlayer->at1be.dx+r1, pPlayer->at1be.dy+r2, pPlayer->at1be.dz+r3, nType);
+    }
+    UseAmmo(pPlayer, pPlayer->atc7, nTrigger);
+    pPlayer->at37b = 1;
+}
+
+void EjectShell(int, PLAYER *pPlayer)
+{
+    SpawnShellEject(pPlayer, 25, 35);
+    SpawnShellEject(pPlayer, 48, 35);
+}
+
+void FireTommy(int nTrigger, PLAYER *pPlayer)
+{
+    Aim *aim = &pPlayer->at1be;
+    sfxPlay3DSound(pPlayer->pSprite, 431, -1, 0);
+    switch (nTrigger)
+    {
+    case 1:
+    {
+        int r1 = Random3(400);
+        int r2 = Random3(1200);
+        int r3 = Random3(1200);
+        actFireVector(pPlayer->pSprite, 0, pPlayer->at6f-pPlayer->pSprite->z, aim->dx+r3, aim->dy+r2, aim->dz+r1, VECTOR_TYPE_5);
+        SpawnBulletEject(pPlayer, -15, -45);
+        pPlayer->at362 = 20;
+        break;
+    }
+    case 2:
+    {
+        int r1 = Random3(400);
+        int r2 = Random3(1200);
+        int r3 = Random3(1200);
+        actFireVector(pPlayer->pSprite, -120, pPlayer->at6f-pPlayer->pSprite->z, aim->dx+r3, aim->dy+r2, aim->dz+r1, VECTOR_TYPE_5);
+        SpawnBulletEject(pPlayer, -140, -45);
+        r1 = Random3(400);
+        r2 = Random3(1200);
+        r3 = Random3(1200);
+        actFireVector(pPlayer->pSprite, 120, pPlayer->at6f-pPlayer->pSprite->z, aim->dx+r3, aim->dy+r2, aim->dz+r1, VECTOR_TYPE_5);
+        SpawnBulletEject(pPlayer, 140, 45);
+        pPlayer->at362 = 30;
+        break;
+    }
+    }
+    UseAmmo(pPlayer, pPlayer->atc7, nTrigger);
+    pPlayer->at37b = 1;
+}
+
+#define kMaxSpread 14
+
+void FireSpread(int nTrigger, PLAYER *pPlayer)
+{
+    dassert(nTrigger > 0 && nTrigger <= kMaxSpread);
+    Aim *aim = &pPlayer->at1be;
+    int angle = (getangle(aim->dx, aim->dy)+((112*(nTrigger-1))/14-56))&2047;
+    int dx = Cos(angle)>>16;
+    int dy = Sin(angle)>>16;
+    sfxPlay3DSound(pPlayer->pSprite, 431, -1, 0);
+    int r1, r2, r3;
+    r1 = Random3(300);
+    r2 = Random3(600);
+    r3 = Random3(600);
+    actFireVector(pPlayer->pSprite, 0, pPlayer->at6f-pPlayer->pSprite->z, dx+r3, dy+r2, aim->dz+r1, VECTOR_TYPE_3);
+    r1 = Random2(90);
+    r2 = Random2(30);
+    SpawnBulletEject(pPlayer, r2, r1);
+    pPlayer->at362 = 20;
+    UseAmmo(pPlayer, pPlayer->atc7, 1);
+    pPlayer->at37b = 1;
+}
+
+void AltFireSpread(int nTrigger, PLAYER *pPlayer)
+{
+    dassert(nTrigger > 0 && nTrigger <= kMaxSpread);
+    Aim *aim = &pPlayer->at1be;
+    int angle = (getangle(aim->dx, aim->dy)+((112*(nTrigger-1))/14-56))&2047;
+    int dx = Cos(angle)>>16;
+    int dy = Sin(angle)>>16;
+    sfxPlay3DSound(pPlayer->pSprite, 431, -1, 0);
+    int r1, r2, r3;
+    r1 = Random3(300);
+    r2 = Random3(600);
+    r3 = Random3(600);
+    actFireVector(pPlayer->pSprite, -120, pPlayer->at6f-pPlayer->pSprite->z, dx+r3, dy+r2, aim->dz+r1, VECTOR_TYPE_3);
+    r1 = Random2(45);
+    r2 = Random2(120);
+    SpawnBulletEject(pPlayer, r2, r1);
+    r1 = Random3(300);
+    r2 = Random3(600);
+    r3 = Random3(600);
+    actFireVector(pPlayer->pSprite, 120, pPlayer->at6f-pPlayer->pSprite->z, dx+r3, dy+r2, aim->dz+r1, VECTOR_TYPE_3);
+    r1 = Random2(-45);
+    r2 = Random2(-120);
+    SpawnBulletEject(pPlayer, r2, r1);
+    pPlayer->at35e = 20;
+    pPlayer->at362 = 30;
+    UseAmmo(pPlayer, pPlayer->atc7, 2);
+    pPlayer->at37b = 1;
+}
+
+void AltFireSpread2(int nTrigger, PLAYER *pPlayer)
+{
+    dassert(nTrigger > 0 && nTrigger <= kMaxSpread);
+    Aim *aim = &pPlayer->at1be;
+    int angle = (getangle(aim->dx, aim->dy)+((112*(nTrigger-1))/14-56))&2047;
+    int dx = Cos(angle)>>16;
+    int dy = Sin(angle)>>16;
+    sfxPlay3DSound(pPlayer->pSprite, 431, -1, 0);
+    if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 3, 2))
+    {
+        int r1, r2, r3;
+        r1 = Random3(300);
+        r2 = Random3(600);
+        r3 = Random3(600);
+        actFireVector(pPlayer->pSprite, -120, pPlayer->at6f-pPlayer->pSprite->z, dx+r3, dy+r2, aim->dz+r1, VECTOR_TYPE_3);
+        r1 = Random2(45);
+        r2 = Random2(120);
+        SpawnBulletEject(pPlayer, r2, r1);
+        r1 = Random3(300);
+        r2 = Random3(600);
+        r3 = Random3(600);
+        actFireVector(pPlayer->pSprite, 120, pPlayer->at6f-pPlayer->pSprite->z, dx+r3, dy+r2, aim->dz+r1, VECTOR_TYPE_3);
+        r1 = Random2(-45);
+        r2 = Random2(-120);
+        SpawnBulletEject(pPlayer, r2, r1);
+        pPlayer->at35e = 30;
+        pPlayer->at362 = 45;
+        UseAmmo(pPlayer, pPlayer->atc7, 2);
+    }
+    else
+    {
+        int r1, r2, r3;
+        r1 = Random3(300);
+        r2 = Random3(600);
+        r3 = Random3(600);
+        actFireVector(pPlayer->pSprite, 0, pPlayer->at6f-pPlayer->pSprite->z, dx+r3, dy+r2, aim->dz+r1, VECTOR_TYPE_3);
+        r1 = Random2(90);
+        r2 = Random2(30);
+        SpawnBulletEject(pPlayer, r2, r1);
+        pPlayer->at35e = 20;
+        pPlayer->at362 = 30;
+        UseAmmo(pPlayer, pPlayer->atc7, 1);
+    }
+    pPlayer->at37b = 1;
+    if (!sub_4B2C8(pPlayer, 3, 1))
+    {
+        WeaponLower(pPlayer);
+        pPlayer->atc3 = -1;
+    }
+}
+
+void FireFlare(int nTrigger, PLAYER *pPlayer)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    int offset = 0;
+    switch (nTrigger)
+    {
+    case 2:
+        offset = -120;
+        break;
+    case 3:
+        offset = 120;
+        break;
+    }
+    playerFireMissile(pPlayer, offset, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 301);
+    UseAmmo(pPlayer, 1, 1);
+    sfxPlay3DSound(pSprite, 420, 2, 0);
+    pPlayer->at362 = 30;
+    pPlayer->at37b = 1;
+}
+
+void AltFireFlare(int nTrigger, PLAYER *pPlayer)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    int offset = 0;
+    switch (nTrigger)
+    {
+    case 2:
+        offset = -120;
+        break;
+    case 3:
+        offset = 120;
+        break;
+    }
+    playerFireMissile(pPlayer, offset, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 303);
+    UseAmmo(pPlayer, 1, 8);
+    sfxPlay3DSound(pSprite, 420, 2, 0);
+    pPlayer->at362 = 45;
+    pPlayer->at37b = 1;
+}
+
+void FireVoodoo(int nTrigger, PLAYER *pPlayer)
+{
+    nTrigger--;
+    int nSprite = pPlayer->at5b;
+    spritetype *pSprite = pPlayer->pSprite;
+    if (nTrigger == 4)
+    {
+        actDamageSprite(nSprite, pSprite, DAMAGE_TYPE_2, 1<<4);
+        return;
+    }
+    dassert(pPlayer->voodooTarget >= 0);
+    spritetype *pTarget = &sprite[pPlayer->voodooTarget];
+    switch (nTrigger)
+    {
+    case 0:
+    {
+        sfxPlay3DSound(pSprite, 460, 2, 0);
+        fxSpawnBlood(pTarget, 17<<4);
+        int nDamage = actDamageSprite(nSprite, pTarget, DAMAGE_TYPE_5, 17<<4);
+        UseAmmo(pPlayer, 9, nDamage/4);
+        break;
+    }
+    case 1:
+    {
+        sfxPlay3DSound(pSprite, 460, 2, 0);
+        fxSpawnBlood(pTarget, 17<<4);
+        int nDamage = actDamageSprite(nSprite, pTarget, DAMAGE_TYPE_5, 9<<4);
+        if (pTarget->type >= kDudePlayer1 && pTarget->type <= kDudePlayer8)
+            WeaponLower(&gPlayer[pTarget->type-kDudePlayer1]);
+        UseAmmo(pPlayer, 9, nDamage/4);
+        break;
+    }
+    case 3:
+    {
+        sfxPlay3DSound(pSprite, 463, 2, 0);
+        fxSpawnBlood(pTarget, 17<<4);
+        int nDamage = actDamageSprite(nSprite, pTarget, DAMAGE_TYPE_5, 49<<4);
+        UseAmmo(pPlayer, 9, nDamage/4);
+        break;
+    }
+    case 2:
+    {
+        sfxPlay3DSound(pSprite, 460, 2, 0);
+        fxSpawnBlood(pTarget, 17<<4);
+        int nDamage = actDamageSprite(nSprite, pTarget, DAMAGE_TYPE_5, 11<<4);
+        if (pTarget->type >= kDudePlayer1 && pTarget->type <= kDudePlayer8)
+        {
+            PLAYER *pOtherPlayer = &gPlayer[pTarget->type-kDudePlayer1];
+            pOtherPlayer->at36a = 128;
+        }
+        UseAmmo(pPlayer, 9, nDamage/4);
+        break;
+    }
+    }
+}
+
+void AltFireVoodoo(int nTrigger, PLAYER *pPlayer)
+{
+    if (nTrigger != 2)
+        return;
+    //int nAmmo = pPlayer->at181[9];
+    int nCount = ClipHigh(pPlayer->at181[9], pPlayer->at1da);
+    if (nCount > 0)
+    {
+        int v4 = pPlayer->at181[9] - (pPlayer->at181[9] / nCount)*nCount;
+        for (int i = 0; i < pPlayer->at1da; i++)
+        {
+            int nTarget = pPlayer->at1de[i];
+            spritetype *pTarget = &sprite[nTarget];
+            if (v4 > 0)
+                v4--;
+            int nDist = approxDist(pTarget->x-pPlayer->pSprite->x, pTarget->y-pPlayer->pSprite->y);
+            if (nDist > 0 && nDist < 51200)
+            {
+                int vc = pPlayer->at181[9]>>3;
+                int v8 = pPlayer->at181[9]<<1;
+                int nDamage = (v8+Random2(vc))<<4;
+                nDamage = (nDamage*((51200-nDist)+1))/51200;
+                nDamage = actDamageSprite(pPlayer->at5b, pTarget, DAMAGE_TYPE_5, nDamage);
+                UseAmmo(pPlayer, 9, nDamage);
+                if (pTarget->type >= kDudePlayer1 && pTarget->type <= kDudePlayer8)
+                {
+                    PLAYER *pOtherPlayer = &gPlayer[pTarget->type-kDudePlayer1];
+                    if (!pOtherPlayer->at31a || !powerupCheck(pOtherPlayer,14))
+                        powerupActivate(pOtherPlayer, 28);
+                }
+                fxSpawnBlood(pTarget, 0);
+            }
+        }
+    }
+    UseAmmo(pPlayer, 9, pPlayer->at181[9]);
+    pPlayer->atcb[10] = 0;
+    pPlayer->atc3 = -1;
+}
+
+void DropVoodoo(int nTrigger, PLAYER *pPlayer)
+{
+    UNREFERENCED_PARAMETER(nTrigger);
+    sfxPlay3DSound(pPlayer->pSprite, 455, 2, 0);
+    spritetype *pSprite = playerFireThing(pPlayer, 0, -4730, 432, 0xccccc);
+    if (pSprite)
+    {
+        int nXSprite = pSprite->extra;
+        XSPRITE *pXSprite = &xsprite[nXSprite];
+        pXSprite->data1 = pPlayer->at181[9];
+        evPost(pSprite->index, 3, 90, CALLBACK_ID_21);
+        UseAmmo(pPlayer, 6, gAmmoItemData[0].at8);
+        UseAmmo(pPlayer, 9, pPlayer->at181[9]);
+        pPlayer->atcb[10] = 0;
+    }
+}
+
+struct TeslaMissile
+{
+    int at0; // offset
+    int at4; // id
+    int at8; // ammo use
+    int atc; // sound
+    int at10; // light
+    int at14; // weapon flash
+};
+
+void FireTesla(int nTrigger, PLAYER *pPlayer)
+{
+    TeslaMissile teslaMissile[6] = 
+    {
+        { 0, 306, 1, 470, 20, 1 },
+        { -140, 306, 1, 470, 30, 1 },
+        { 140, 306, 1, 470, 30, 1 },
+        { 0, 302, 35, 471, 40, 1 },
+        { -140, 302, 35, 471, 50, 1 },
+        { 140, 302, 35, 471, 50, 1 },
+    };
+    if (nTrigger > 0 && nTrigger <= 6)
+    {
+        nTrigger--;
+        spritetype *pSprite = pPlayer->pSprite;
+        TeslaMissile *pMissile = &teslaMissile[nTrigger];
+        if (!sub_4B2C8(pPlayer, 7, pMissile->at8))
+        {
+            pMissile = &teslaMissile[0];
+            if (!sub_4B2C8(pPlayer, 7, pMissile->at8))
+            {
+                pPlayer->atc3 = -1;
+                pPlayer->at26 = 76;
+                pPlayer->at37b = 0;
+                return;
+            }
+        }
+        playerFireMissile(pPlayer, pMissile->at0, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, pMissile->at4);
+        UseAmmo(pPlayer, 7, pMissile->at8);
+        sfxPlay3DSound(pSprite, pMissile->atc, 1, 0);
+        pPlayer->at362 = pMissile->at10;
+        pPlayer->at37b = pMissile->at14;
+    }
+}
+
+void AltFireTesla(int nTrigger, PLAYER *pPlayer)
+{
+    UNREFERENCED_PARAMETER(nTrigger);
+    spritetype *pSprite = pPlayer->pSprite;
+    playerFireMissile(pPlayer, 0, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 302);
+    UseAmmo(pPlayer, pPlayer->atc7, 35);
+    sfxPlay3DSound(pSprite, 471, 2, 0);
+    pPlayer->at362 = 40;
+    pPlayer->at37b = 1;
+}
+
+void FireNapalm(int nTrigger, PLAYER *pPlayer)
+{
+    spritetype *pSprite = pPlayer->pSprite;
+    int offset = 0;
+    switch (nTrigger)
+    {
+    case 2:
+        offset = -50;
+        break;
+    case 3:
+        offset = 50;
+        break;
+    }
+    playerFireMissile(pPlayer, offset, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 312);
+    sfxPlay3DSound(pSprite, 480, 2, 0);
+    UseAmmo(pPlayer, 4, 1);
+    pPlayer->at37b = 1;
+}
+
+void FireNapalm2(int nTrigger, PLAYER *pPlayer)
+{
+    UNREFERENCED_PARAMETER(nTrigger);
+    spritetype *pSprite = pPlayer->pSprite;
+    playerFireMissile(pPlayer, -120, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 312);
+    playerFireMissile(pPlayer, 120, pPlayer->at1be.dx, pPlayer->at1be.dy, pPlayer->at1be.dz, 312);
+    sfxPlay3DSound(pSprite, 480, 2, 0);
+    UseAmmo(pPlayer, 4, 2);
+    pPlayer->at37b = 1;
+}
+
+void AltFireNapalm(int nTrigger, PLAYER *pPlayer)
+{
+    UNREFERENCED_PARAMETER(nTrigger);
+    char UNUSED(bAkimbo) = powerupCheck(pPlayer, 17);
+    int nSpeed = mulscale16(0x8000, 0x177777)+0x66666;
+    spritetype *pMissile = playerFireThing(pPlayer, 0, -4730, 428, nSpeed);
+    if (pMissile)
+    {
+        XSPRITE *pXSprite = &xsprite[pMissile->extra];
+        pXSprite->data4 = ClipHigh(pPlayer->at181[4], 12);
+        UseAmmo(pPlayer, 4, pXSprite->data4);
+        seqSpawn(22, 3, pMissile->extra, -1);
+        actBurnSprite(actSpriteIdToOwnerId(pPlayer->pSprite->index), pXSprite, 600);
+        evPost(pMissile->index, 3, 0, CALLBACK_ID_0);
+        sfxPlay3DSound(pMissile, 480, 2, 0);
+        pPlayer->at362 = 30;
+        pPlayer->at37b = 1;
+    }
+}
+
+void FireLifeLeech(int nTrigger, PLAYER *pPlayer)
+{
+    if (!CheckAmmo(pPlayer, 8, 1))
+        return;
+    int r1 = Random2(2000);
+    int r2 = Random2(2000);
+    int r3 = Random2(1000);
+    spritetype *pMissile = playerFireMissile(pPlayer, 0, pPlayer->at1be.dx+r1, pPlayer->at1be.dy+r2, pPlayer->at1be.dz+r3, 315);
+    if (pMissile)
+    {
+        XSPRITE *pXSprite = &xsprite[pMissile->extra];
+        pXSprite->target = pPlayer->at1d6;
+        pMissile->ang = (nTrigger==2) ? 1024 : 0;
+    }
+    if (sub_4B2C8(pPlayer, 8, 1))
+        UseAmmo(pPlayer, 8, 1);
+    else
+        actDamageSprite(pPlayer->at5b, pPlayer->pSprite, DAMAGE_TYPE_5, 16);
+    pPlayer->at362 = ClipHigh(pPlayer->at362+5, 50);
+}
+
+void AltFireLifeLeech(int nTrigger, PLAYER *pPlayer)
+{
+    UNREFERENCED_PARAMETER(nTrigger);
+    sfxPlay3DSound(pPlayer->pSprite, 455, 2, 0);
+    spritetype *pMissile = playerFireThing(pPlayer, 0, -4730, 431, 0x19999);
+    if (pMissile)
+    {
+        pMissile->cstat |= 4096;
+        XSPRITE *pXSprite = &xsprite[pMissile->extra];
+        pXSprite->Push = 1;
+        pXSprite->Proximity = 1;
+        pXSprite->DudeLockout = 1;
+        pXSprite->data4 = ClipHigh(pPlayer->at181[4], 12);
+        pXSprite->stateTimer = 1;
+        evPost(pMissile->index, 3, 120, CALLBACK_ID_20);
+        if (gGameOptions.nGameType <= 1)
+        {
+            int nAmmo = pPlayer->at181[8];
+            if (nAmmo < 25 && pPlayer->pXSprite->health > ((25-nAmmo)<<4))
+            {
+                actDamageSprite(pPlayer->at5b, pPlayer->pSprite, DAMAGE_TYPE_5, ((25-nAmmo)<<4));
+                nAmmo = 25;
+            }
+            pXSprite->data3 = nAmmo;
+            UseAmmo(pPlayer, 8, nAmmo);
+        }
+        else
+        {
+            pXSprite->data3 = pPlayer->at181[8];
+            pPlayer->at181[8] = 0;
+        }
+        pPlayer->atcb[9] = 0;
+    }
+}
+
+void FireBeast(int nTrigger, PLAYER * pPlayer)
+{
+    UNREFERENCED_PARAMETER(nTrigger);
+    int r1 = Random2(2000);
+    int r2 = Random2(2000);
+    int r3 = Random2(2000);
+    actFireVector(pPlayer->pSprite, 0, pPlayer->at6f-pPlayer->pSprite->z, pPlayer->at1be.dx+r1, pPlayer->at1be.dy+r2, pPlayer->at1be.dz+r3, VECTOR_TYPE_9);
+}
+
+char gWeaponUpgrade[][13] = {
+    { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
+    { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
+    { 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 },
+    { 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0 },
+    { 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0 },
+    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0 },
+    { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
+    { 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0 },
+    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0 },
+    { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
+};
+
+char WeaponUpgrade(PLAYER *pPlayer, char newWeapon)
+{
+    char weapon = pPlayer->atbd;
+    if (!sub_4B1A4(pPlayer) && (gProfile[pPlayer->at57].nWeaponSwitch&1) && (gWeaponUpgrade[pPlayer->atbd][newWeapon] || (gProfile[pPlayer->at57].nWeaponSwitch&2)))
+        weapon = newWeapon;
+    return weapon;
+}
+
+int OrderNext[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 1 };
+int OrderPrev[] = { 12, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 1 };
+
+char WeaponFindNext(PLAYER *pPlayer, int *a2, char bDir)
+{
+    int weapon = pPlayer->atbd;
+    do
+    {
+        if (bDir)
+            weapon = OrderNext[weapon];
+        else
+            weapon = OrderPrev[weapon];
+        if (weaponModes[weapon].at0 && pPlayer->atcb[weapon])
+        {
+            if (weapon == 9)
+            {
+                if (CheckAmmo(pPlayer, weaponModes[weapon].at4, 1))
+                    break;
+            }
+            else
+            {
+                if (sub_4B2C8(pPlayer, weaponModes[weapon].at4, 1))
+                    break;
+            }
+        }
+    } while (weapon != pPlayer->atbd);
+    if (weapon == pPlayer->atbd)
+    {
+        if (!weaponModes[weapon].at0 || !CheckAmmo(pPlayer, weaponModes[weapon].at4, 1))
+            weapon = 1;
+    }
+    if (a2)
+        *a2 = 0;
+    return weapon;
+}
+
+char WeaponFindLoaded(PLAYER *pPlayer, int *a2)
+{
+    char v4 = 1;
+    int v14 = 0;
+    if (weaponModes[pPlayer->atbd].at0 > 1)
+    {
+        for (int i = 0; i < weaponModes[pPlayer->atbd].at0; i++)
+        {
+            if (CheckAmmo(pPlayer, weaponModes[pPlayer->atbd].at4, 1))
+            {
+                v14 = i;
+                v4 = pPlayer->atbd;
+                break;
+            }
+        }
+    }
+    if (v4 == 1)
+    {
+        int vc = 0;
+        for (int i = 0; i < 14; i++)
+        {
+            int weapon = pPlayer->at111[vc][i];
+            if (pPlayer->atcb[weapon])
+            {
+                for (int j = 0; j < weaponModes[weapon].at0; j++)
+                {
+                    if (sub_4B1FC(pPlayer, weapon, weaponModes[weapon].at4, 1))
+                    {
+                        if (a2)
+                            *a2 = j;
+                        return weapon;
+                    }
+                }
+            }
+        }
+    }
+    else if (a2)
+        *a2 = v14;
+    return v4;
+}
+
+char sub_4F0E0(PLAYER *pPlayer)
+{
+    switch (pPlayer->atc3)
+    {
+    case 5:
+        if (!pPlayer->atc.buttonFlags.shoot2)
+            pPlayer->atc3 = 6;
+        return 1;
+    case 6:
+        if (pPlayer->atc.buttonFlags.shoot2)
+        {
+            pPlayer->atc3 = 3;
+            pPlayer->at1b2 = pPlayer->atbf;
+            StartQAV(pPlayer, 13, nClientDropCan, 0);
+        }
+        else if (pPlayer->atc.buttonFlags.shoot)
+        {
+            pPlayer->atc3 = 7;
+            pPlayer->at1b2 = 0;
+            pPlayer->at1b6 = gFrameClock;
+        }
+        return 1;
+    case 7:
+    {
+        pPlayer->at1ba = ClipHigh(divscale16(gFrameClock-pPlayer->at1b6,240), 65536);
+        if (!pPlayer->atc.buttonFlags.shoot)
+        {
+            if (!pPlayer->at1b2)
+                pPlayer->at1b2 = pPlayer->atbf;
+            pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 14, nClientThrowCan, 0);
+        }
+        return 1;
+    }
+    }
+    return 0;
+}
+
+char sub_4F200(PLAYER *pPlayer)
+{
+    switch (pPlayer->atc3)
+    {
+    case 4:
+        if (!pPlayer->atc.buttonFlags.shoot2)
+            pPlayer->atc3 = 5;
+        return 1;
+    case 5:
+        if (pPlayer->atc.buttonFlags.shoot2)
+        {
+            pPlayer->atc3 = 1;
+            pPlayer->at1b2 = pPlayer->atbf;
+            StartQAV(pPlayer, 22, nClientDropBundle, 0);
+        }
+        else if (pPlayer->atc.buttonFlags.shoot)
+        {
+            pPlayer->atc3 = 6;
+            pPlayer->at1b2 = 0;
+            pPlayer->at1b6 = gFrameClock;
+        }
+        return 1;
+    case 6:
+    {
+        pPlayer->at1ba = ClipHigh(divscale16(gFrameClock-pPlayer->at1b6,240), 65536);
+        if (!pPlayer->atc.buttonFlags.shoot)
+        {
+            if (!pPlayer->at1b2)
+                pPlayer->at1b2 = pPlayer->atbf;
+            pPlayer->atc3 = 1;
+            StartQAV(pPlayer, 23, nClientThrowBundle, 0);
+        }
+        return 1;
+    }
+    }
+    return 0;
+}
+
+char sub_4F320(PLAYER *pPlayer)
+{
+    switch (pPlayer->atc3)
+    {
+    case 9:
+        pPlayer->at1ba = ClipHigh(divscale16(gFrameClock-pPlayer->at1b6,240), 65536);
+        pPlayer->atbf = 0;
+        if (!pPlayer->atc.buttonFlags.shoot)
+        {
+            pPlayer->atc3 = 8;
+            StartQAV(pPlayer, 29, nClientThrowProx, 0);
+        }
+        break;
+    }
+    return 0;
+}
+
+char sub_4F3A0(PLAYER *pPlayer)
+{
+    switch (pPlayer->atc3)
+    {
+    case 13:
+        pPlayer->at1ba = ClipHigh(divscale16(gFrameClock-pPlayer->at1b6,240), 65536);
+        if (!pPlayer->atc.buttonFlags.shoot)
+        {
+            pPlayer->atc3 = 11;
+            StartQAV(pPlayer, 39, nClientThrowRemote, 0);
+        }
+        break;
+    }
+    return 0;
+}
+
+char sub_4F414(PLAYER *pPlayer)
+{
+    switch (pPlayer->atc3)
+    {
+    case 4:
+        pPlayer->atc3 = 6;
+        StartQAV(pPlayer, 114, nClientFireLifeLeech, 1);
+        return 1;
+    case 6:
+        if (!pPlayer->atc.buttonFlags.shoot2)
+        {
+            pPlayer->atc3 = 2;
+            StartQAV(pPlayer, 118, -1, 0);
+            return 1;
+        }
+        break;
+    case 8:
+        pPlayer->atc3 = 2;
+        StartQAV(pPlayer, 118, -1, 0);
+        return 1;
+    }
+    return 0;
+}
+
+char sub_4F484(PLAYER *pPlayer)
+{
+    switch (pPlayer->atc3)
+    {
+    case 4:
+        pPlayer->atc3 = 5;
+        if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+            StartQAV(pPlayer, 84, nClientFireTesla, 1);
+        else
+            StartQAV(pPlayer, 77, nClientFireTesla, 1);
+        return 1;
+    case 5:
+        if (!pPlayer->atc.buttonFlags.shoot)
+        {
+            pPlayer->atc3 = 2;
+            if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+                StartQAV(pPlayer, 87, -1, 0);
+            else
+                StartQAV(pPlayer, 80, -1, 0);
+            return 1;
+        }
+        break;
+    case 7:
+        pPlayer->atc3 = 2;
+        if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+            StartQAV(pPlayer, 87, -1, 0);
+        else
+            StartQAV(pPlayer, 80, -1, 0);
+        return 1;
+    }
+    return 0;
+}
+
+void WeaponProcess(PLAYER *pPlayer)
+{
+    pPlayer->at37b = ClipLow(pPlayer->at37b-1, 0);
+    if (pPlayer->pXSprite->health == 0)
+    {
+        pPlayer->at1b1 = 0;
+        sfxKill3DSound(pPlayer->pSprite, 1, -1);
+    }
+    if (pPlayer->at87 && BannedUnderwater(pPlayer->atbd))
+    {
+        if (sub_4B1A4(pPlayer))
+        {
+            if (pPlayer->atbd == 7)
+            {
+                pPlayer->at1b2 = pPlayer->atbf;
+                DropCan(1, pPlayer);
+                pPlayer->atc3 = 3;
+            }
+            else if (pPlayer->atbd == 6)
+            {
+                pPlayer->at1b2 = pPlayer->atbf;
+                DropBundle(1, pPlayer);
+                pPlayer->atc3 = 1;
+            }
+        }
+        WeaponLower(pPlayer);
+        pPlayer->at1ba = 0;
+    }
+    WeaponPlay(pPlayer);
+    UpdateAimVector(pPlayer);
+    pPlayer->atbf -= 4;
+    char bShoot = pPlayer->atc.buttonFlags.shoot;
+    char bShoot2 = pPlayer->atc.buttonFlags.shoot2;
+    if (pPlayer->at1b1 && pPlayer->pXSprite->health > 0)
+    {
+        if (bShoot && CheckAmmo(pPlayer, pPlayer->atc7, 1))
+        {
+            while (pPlayer->atbf <= 0)
+                pPlayer->atbf += weaponQAV[pPlayer->at26]->at10;
+        }
+        else
+        {
+            pPlayer->atbf = 0;
+            pPlayer->at1b1 = 0;
+        }
+        return;
+    }
+    pPlayer->atbf = ClipLow(pPlayer->atbf, 0);
+    switch (pPlayer->atbd)
+    {
+    case 7:
+        if (sub_4F0E0(pPlayer))
+            return;
+        break;
+    case 6:
+        if (sub_4F200(pPlayer))
+            return;
+        break;
+    case 11:
+        if (sub_4F320(pPlayer))
+            return;
+        break;
+    case 12:
+        if (sub_4F3A0(pPlayer))
+            return;
+        break;
+    }
+    if (pPlayer->atbf > 0)
+        return;
+    if (pPlayer->pXSprite->health == 0 || pPlayer->atbd == 0)
+        pPlayer->at26 = -1;
+    switch (pPlayer->atbd)
+    {
+    case 9:
+        if (sub_4F414(pPlayer))
+            return;
+        break;
+    case 8:
+        if (sub_4F484(pPlayer))
+            return;
+        break;
+    }
+    if (pPlayer->atbe)
+    {
+        sfxKill3DSound(pPlayer->pSprite, -1, 441);
+        pPlayer->atc3 = 0;
+        pPlayer->atc.newWeapon = pPlayer->atbe;
+        pPlayer->atbe = 0;
+    }
+    if (pPlayer->atc.keyFlags.nextWeapon)
+    {
+        pPlayer->atc.keyFlags.nextWeapon = 0;
+        pPlayer->atc3 = 0;
+        pPlayer->atbe = 0;
+        int t;
+        char weapon = WeaponFindNext(pPlayer, &t, 1);
+        pPlayer->atd9[weapon] = t;
+        if (pPlayer->atbd)
+        {
+            WeaponLower(pPlayer);
+            pPlayer->atbe = weapon;
+            return;
+        }
+        pPlayer->atc.newWeapon = weapon;
+    }
+    if (pPlayer->atc.keyFlags.prevWeapon)
+    {
+        pPlayer->atc.keyFlags.prevWeapon = 0;
+        pPlayer->atc3 = 0;
+        pPlayer->atbe = 0;
+        int t;
+        char weapon = WeaponFindNext(pPlayer, &t, 0);
+        pPlayer->atd9[weapon] = t;
+        if (pPlayer->atbd)
+        {
+            WeaponLower(pPlayer);
+            pPlayer->atbe = weapon;
+            return;
+        }
+        pPlayer->atc.newWeapon = weapon;
+    }
+    if (pPlayer->atc3 == -1)
+    {
+        pPlayer->atc3 = 0;
+        int t;
+        char weapon = WeaponFindLoaded(pPlayer, &t);
+        pPlayer->atd9[weapon] = t;
+        if (pPlayer->atbd)
+        {
+            WeaponLower(pPlayer);
+            pPlayer->atbe = weapon;
+            return;
+        }
+        pPlayer->atc.newWeapon = weapon;
+    }
+    if (pPlayer->atc.newWeapon)
+    {
+        if (pPlayer->atc.newWeapon == 6)
+        {
+            if (pPlayer->atbd == 6)
+            {
+                if (sub_4B2C8(pPlayer, 10, 1))
+                    pPlayer->atc.newWeapon = 11;
+                else if (sub_4B2C8(pPlayer, 11, 1))
+                    pPlayer->atc.newWeapon = 12;
+            }
+            else if (pPlayer->atbd == 11)
+            {
+                if (sub_4B2C8(pPlayer, 11, 1))
+                    pPlayer->atc.newWeapon = 12;
+                else if (sub_4B2C8(pPlayer, 5, 1) && pPlayer->at87 == 0)
+                    pPlayer->atc.newWeapon = 6;
+            }
+            else if (pPlayer->atbd == 12)
+            {
+                if (sub_4B2C8(pPlayer, 5, 1) && pPlayer->at87 == 0)
+                    pPlayer->atc.newWeapon = 6;
+                else if (sub_4B2C8(pPlayer, 10, 1))
+                    pPlayer->atc.newWeapon = 11;
+            }
+            else
+            {
+                if (sub_4B2C8(pPlayer, 5, 1) && pPlayer->at87 == 0)
+                    pPlayer->atc.newWeapon = 6;
+                else if (sub_4B2C8(pPlayer, 10, 1))
+                    pPlayer->atc.newWeapon = 11;
+                else if (sub_4B2C8(pPlayer, 11, 1))
+                    pPlayer->atc.newWeapon = 12;
+            }
+        }
+        if (pPlayer->pXSprite->health == 0 || pPlayer->atcb[pPlayer->atc.newWeapon] == 0)
+        {
+            pPlayer->atc.newWeapon = 0;
+            return;
+        }
+        if (pPlayer->at87 && BannedUnderwater(pPlayer->atc.newWeapon) && !sub_4B1A4(pPlayer))
+        {
+            pPlayer->atc.newWeapon = 0;
+            return;
+        }
+        int nWeapon = pPlayer->atc.newWeapon;
+        int v4c = weaponModes[nWeapon].at0;
+        if (!pPlayer->atbd)
+        {
+            int nAmmoType = weaponModes[nWeapon].at4;
+            if (v4c > 1)
+            {
+                if (CheckAmmo(pPlayer, nAmmoType, 1) || nAmmoType == 11)
+                    WeaponRaise(pPlayer);
+                pPlayer->atc.newWeapon = 0;
+            }
+            else
+            {
+                if (sub_4B1FC(pPlayer, nWeapon, nAmmoType, 1))
+                    WeaponRaise(pPlayer);
+                else
+                {
+                    pPlayer->atc3 = 0;
+                    int t;
+                    char weapon = WeaponFindLoaded(pPlayer, &t);
+                    pPlayer->atd9[weapon] = t;
+                    if (pPlayer->atbd)
+                    {
+                        WeaponLower(pPlayer);
+                        pPlayer->atbe = weapon;
+                        return;
+                    }
+                    pPlayer->atc.newWeapon = weapon;
+                }
+            }
+            return;
+        }
+        if (nWeapon == pPlayer->atbd && v4c <= 1)
+        {
+            pPlayer->atc.newWeapon = 0;
+            return;
+        }
+        int i = 0;
+        if (nWeapon == pPlayer->atbd)
+            i = 1;
+        for (; i <= v4c; i++)
+        {
+            int v6c = (pPlayer->atd9[nWeapon]+i)%v4c;
+            if (sub_4B1FC(pPlayer, nWeapon, weaponModes[nWeapon].at4, 1))
+            {
+                WeaponLower(pPlayer);
+                pPlayer->atd9[nWeapon] = v6c;
+                return;
+            }
+        }
+        pPlayer->atc.newWeapon = 0;
+        return;
+    }
+    if (pPlayer->atbd && !CheckAmmo(pPlayer, pPlayer->atc7, 1) && pPlayer->atc7 != 11)
+    {
+        pPlayer->atc3 = -1;
+        return;
+    }
+    if (bShoot)
+    {
+        switch (pPlayer->atbd)
+        {
+        case 1:
+            StartQAV(pPlayer, 2, nClientFirePitchfork, 0);
+            return;
+        case 7:
+            switch (pPlayer->atc3)
+            {
+            case 3:
+                pPlayer->atc3 = 4;
+                StartQAV(pPlayer, 10, nClientFireSpray, 1);
+                return;
+            }
+            break;
+        case 6:
+            switch (pPlayer->atc3)
+            {
+            case 3:
+                pPlayer->atc3 = 6;
+                pPlayer->at1b2 = -1;
+                pPlayer->at1b6 = gFrameClock;
+                StartQAV(pPlayer, 21, nClientExplodeBundle, 0);
+                return;
+            }
+            break;
+        case 11:
+            switch (pPlayer->atc3)
+            {
+            case 7:
+                pPlayer->at26 = 27;
+                pPlayer->atc3 = 9;
+                pPlayer->at1b6 = gFrameClock;
+                return;
+            }
+            break;
+        case 12:
+            switch (pPlayer->atc3)
+            {
+            case 10:
+                pPlayer->at26 = 36;
+                pPlayer->atc3 = 13;
+                pPlayer->at1b6 = gFrameClock;
+                return;
+            case 11:
+                pPlayer->atc3 = 12;
+                StartQAV(pPlayer, 40, nClientFireRemote, 0);
+                return;
+            }
+            break;
+        case 3:
+            switch (pPlayer->atc3)
+            {
+            case 7:
+                pPlayer->atc3 = 6;
+                StartQAV(pPlayer, 61, nClientFireShotgun, 0);
+                return;
+            case 3:
+                pPlayer->atc3 = 2;
+                StartQAV(pPlayer, 54, nClientFireShotgun, 0);
+                return;
+            case 2:
+                pPlayer->atc3 = 1;
+                StartQAV(pPlayer, 55, nClientFireShotgun, 0);
+                return;
+            }
+            break;
+        case 4:
+            if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 3, 2))
+                StartQAV(pPlayer, 71, nClientFireTommy, 1);
+            else
+                StartQAV(pPlayer, 66, nClientFireTommy, 1);
+            return;
+        case 2:
+            if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 1, 2))
+                StartQAV(pPlayer, 48, nClientFireFlare, 0);
+            else
+                StartQAV(pPlayer, 43, nClientFireFlare, 0);
+            return;
+        case 10:
+        {
+            static int nChance[] = { 0xa000, 0xc000, 0xe000, 0x10000 };
+            int nRand = wrand()*2;
+            int i;
+            for (i = 0; nChance[i] < nRand; i++)
+            {
+            }
+            pPlayer->voodooTarget = pPlayer->at1d6;
+            if (pPlayer->voodooTarget == -1 || sprite[pPlayer->voodooTarget].statnum != 6)
+                i = 4;
+            StartQAV(pPlayer,103+i, nClientFireVoodoo, 0);
+            return;
+        }
+        case 8:
+            switch (pPlayer->atc3)
+            {
+            case 2:
+                pPlayer->atc3 = 4;
+                if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+                    StartQAV(pPlayer, 84, nClientFireTesla, 0);
+                else
+                    StartQAV(pPlayer, 77, nClientFireTesla, 0);
+                return;
+            case 5:
+                if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+                    StartQAV(pPlayer, 84, nClientFireTesla, 0);
+                else
+                    StartQAV(pPlayer, 77, nClientFireTesla, 0);
+                return;
+            }
+            break;
+        case 5:
+            if (powerupCheck(pPlayer, 17))
+                StartQAV(pPlayer, 122, nClientFireNapalm, 0);
+            else
+                StartQAV(pPlayer, 91, nClientFireNapalm, 0);
+            return;
+        case 9:
+            sfxPlay3DSound(pPlayer->pSprite, 494, 2, 0);
+            StartQAV(pPlayer, 116, nClientFireLifeLeech, 0);
+            return;
+        case 13:
+            StartQAV(pPlayer, 95+Random(4), nClientFireBeast, 0);
+            return;
+        }
+    }
+    if (bShoot2)
+    {
+        switch (pPlayer->atbd)
+        {
+        case 1:
+            StartQAV(pPlayer, 2, nClientFirePitchfork, 0);
+            return;
+        case 7:
+            switch (pPlayer->atc3)
+            {
+            case 3:
+                pPlayer->atc3 = 5;
+                StartQAV(pPlayer, 12, nClientExplodeCan, 0);
+                return;
+            }
+            break;
+        case 6:
+            switch (pPlayer->atc3)
+            {
+            case 3:
+                pPlayer->atc3 = 4;
+                StartQAV(pPlayer, 21, nClientExplodeBundle, 0);
+                return;
+            case 7:
+                pPlayer->atc3 = 8;
+                StartQAV(pPlayer, 28, nClientDropProx, 0);
+                return;
+            case 10:
+                pPlayer->atc3 = 11;
+                StartQAV(pPlayer, 38, nClientDropRemote, 0);
+                return;
+            case 11:
+                if (pPlayer->at181[11] > 0)
+                {
+                    pPlayer->atc3 = 10;
+                    StartQAV(pPlayer, 30, -1, 0);
+                }
+                return;
+            }
+            break;
+        case 11:
+            switch (pPlayer->atc3)
+            {
+            case 7:
+                pPlayer->atc3 = 8;
+                StartQAV(pPlayer, 28, nClientDropProx, 0);
+                return;
+            }
+            break;
+        case 12:
+            switch (pPlayer->atc3)
+            {
+            case 10:
+                pPlayer->atc3 = 11;
+                StartQAV(pPlayer, 38, nClientDropRemote, 0);
+                return;
+            case 11:
+                if (pPlayer->at181[11] > 0)
+                {
+                    pPlayer->atc3 = 10;
+                    StartQAV(pPlayer, 30, -1, 0);
+                }
+                return;
+            }
+            break;
+        case 3:
+            switch (pPlayer->atc3)
+            {
+            case 7:
+                pPlayer->atc3 = 6;
+                StartQAV(pPlayer, 62, nClientFireShotgun, 0);
+                return;
+            case 3:
+                pPlayer->atc3 = 1;
+                StartQAV(pPlayer, 56, nClientFireShotgun, 0);
+                return;
+            case 2:
+                pPlayer->atc3 = 1;
+                StartQAV(pPlayer, 55, nClientFireShotgun, 0);
+                return;
+            }
+            break;
+        case 4:
+            if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 3, 2))
+                StartQAV(pPlayer, 73, nClientAltFireSpread2, 0);
+            else
+                StartQAV(pPlayer, 67, nClientAltFireSpread2, 0);
+            return;
+        case 10:
+            sfxPlay3DSound(pPlayer->pSprite, 461, 2, 0);
+            StartQAV(pPlayer, 110, nClientAltFireVoodoo, 0);
+            return;
+#if 0
+        case 2:
+            if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 1, 2))
+                StartQAV(pPlayer, 48, nClientFireFlare, 0);
+            else
+                StartQAV(pPlayer, 43, nClientFireFlare, 0);
+            return;
+#endif
+        case 8:
+            if (sub_4B2C8(pPlayer, 7, 35))
+            {
+                if (sub_4B2C8(pPlayer, 7, 70) && powerupCheck(pPlayer, 17))
+                    StartQAV(pPlayer, 85, nClientFireTesla, 0);
+                else
+                    StartQAV(pPlayer, 78, nClientFireTesla, 0);
+            }
+            else
+            {
+                if (sub_4B2C8(pPlayer, 7, 10) && powerupCheck(pPlayer, 17))
+                    StartQAV(pPlayer, 84, nClientFireTesla, 0);
+                else
+                    StartQAV(pPlayer, 77, nClientFireTesla, 0);
+            }
+            return;
+        case 5:
+            if (powerupCheck(pPlayer, 17))
+                StartQAV(pPlayer, 122, nClientAltFireNapalm, 0);
+            else
+                StartQAV(pPlayer, 91, nClientAltFireNapalm, 0);
+            return;
+        case 2:
+            if (CheckAmmo(pPlayer, 1, 8))
+            {
+                if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 1, 16))
+                    StartQAV(pPlayer, 48, nClientAltFireFlare, 0);
+                else
+                    StartQAV(pPlayer, 43, nClientAltFireFlare, 0);
+            }
+            else
+            {
+                if (powerupCheck(pPlayer, 17) && sub_4B2C8(pPlayer, 1, 2))
+                    StartQAV(pPlayer, 48, nClientFireFlare, 0);
+                else
+                    StartQAV(pPlayer, 43, nClientFireFlare, 0);
+            }
+            return;
+        case 9:
+            if (gGameOptions.nGameType <= 1 && !sub_4B2C8(pPlayer, 8, 1) && pPlayer->pXSprite->health < (25 << 4))
+            {
+                sfxPlay3DSound(pPlayer->pSprite, 494, 2, 0);
+                StartQAV(pPlayer, 116, nClientFireLifeLeech, 0);
+            }
+            else
+            {
+                StartQAV(pPlayer, 119, -1, 0);
+                AltFireLifeLeech(1, pPlayer);
+                pPlayer->atc3 = -1;
+            }
+            return;
+        }
+    }
+    WeaponUpdateState(pPlayer);
+}
+
+void sub_51340(spritetype *pMissile, int a2)
+{
+    char va4[(kMaxSectors+7)>>3];
+    int x = pMissile->x;
+    int y = pMissile->y;
+    int z = pMissile->z;
+    int nDist = 300;
+    int nSector = pMissile->sectnum;
+    int nOwner = actSpriteOwnerToSpriteId(pMissile);
+    gAffectedSectors[0] = -1;
+    gAffectedXWalls[0] = -1;
+    GetClosestSpriteSectors(nSector, x, y, nDist, gAffectedSectors, va4, gAffectedXWalls);
+    char v4 = 1;
+    int v24 = -1;
+    actHitcodeToData(a2, &gHitInfo, &v24, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
+    if (a2 == 3 && v24 >= 0 && sprite[v24].statnum == 6)
+        v4 = 0;
+    for (int nSprite = headspritestat[6]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        if (nSprite != nOwner || v4)
+        {
+            spritetype *pSprite = &sprite[nSprite];
+            if (pSprite->hitag&32)
+                continue;
+            if (TestBitString(va4, pSprite->sectnum) && CheckProximity(pSprite, x, y, z, nSector, nDist))
+            {
+                int dx = pMissile->x-pSprite->x;
+                int dy = pMissile->y-pSprite->y;
+                int nDamage = ClipLow((nDist-(ksqrt(dx*dx+dy*dy)>>4)+20)>>1, 10);
+                if (nSprite == nOwner)
+                    nDamage /= 2;
+                actDamageSprite(nOwner, pSprite, DAMAGE_TYPE_6, nDamage<<4);
+            }
+        }
+    }
+    for (int nSprite = headspritestat[4]; nSprite >= 0; nSprite = nextspritestat[nSprite])
+    {
+        spritetype *pSprite = &sprite[nSprite];
+        if (pSprite->hitag&32)
+            continue;
+        if (TestBitString(va4, pSprite->sectnum) && CheckProximity(pSprite, x, y, z, nSector, nDist))
+        {
+            XSPRITE *pXSprite = &xsprite[pSprite->extra];
+            if (!pXSprite->locked)
+            {
+                int dx = pMissile->x-pSprite->x;
+                int dy = pMissile->y-pSprite->y;
+                int nDamage = ClipLow(nDist-(ksqrt(dx*dx+dy*dy)>>4)+20, 20);
+                actDamageSprite(nOwner, pSprite, DAMAGE_TYPE_6, nDamage<<4);
+            }
+        }
+    }
+}
+
+class WeaponLoadSave : public LoadSave
+{
+public:
+    virtual void Load();
+    virtual void Save();
+};
+
+void WeaponLoadSave::Load()
+{
+}
+
+void WeaponLoadSave::Save()
+{
+}
+
+static WeaponLoadSave *myLoadSave;
+
+void WeaponLoadSaveConstruct(void)
+{
+    myLoadSave = new WeaponLoadSave();
+}
+
diff --git a/source/blood/src/weapon.h b/source/blood/src/weapon.h
new file mode 100644
index 000000000..b2a0ce01b
--- /dev/null
+++ b/source/blood/src/weapon.h
@@ -0,0 +1,35 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+#pragma once
+#include "common_game.h"
+#include "blood.h"
+#include "db.h"
+#include "player.h"
+
+void WeaponInit(void);
+void WeaponDraw(PLAYER *pPlayer, int a2, int a3, int a4, int a5);
+void WeaponRaise(PLAYER *pPlayer);
+void WeaponLower(PLAYER *pPlayer);
+char WeaponUpgrade(PLAYER *pPlayer, char newWeapon);
+void WeaponProcess(PLAYER *pPlayer);
+void sub_51340(spritetype *pMissile, int a2);
\ No newline at end of file
diff --git a/source/blood/src/winbits.cpp b/source/blood/src/winbits.cpp
new file mode 100644
index 000000000..235c86157
--- /dev/null
+++ b/source/blood/src/winbits.cpp
@@ -0,0 +1,132 @@
+//-------------------------------------------------------------------------
+/*
+Copyright (C) 2010-2019 EDuke32 developers and contributors
+Copyright (C) 2019 Nuke.YKT
+
+This file is part of NBlood.
+
+NBlood is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License version 2
+as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+//-------------------------------------------------------------------------
+
+#ifdef _WIN32
+
+#include "compat.h"
+
+#define NEED_SHELLAPI_H
+#define NEED_WINSOCK2_H
+#define NEED_WS2TCPIP_H
+#include "windows_inc.h"
+
+#include "renderlayer.h"
+
+int32_t G_GetVersionFromWebsite(char *buffer)
+{
+    static int32_t wsainitialized = 0;
+    int32_t i=0, j=0, r=0;
+    struct sockaddr_in dest_addr;
+    struct hostent *h;
+    char const *host = "www.eduke32.com";
+    char const *req = "GET http://www.eduke32.com/VERSION HTTP/1.0\r\n\r\n\r\n";
+    char *tok;
+    char tempbuf[2048],otherbuf[16],ver[16];
+    SOCKET mysock;
+    WSADATA ws;
+
+#ifdef _WIN32
+    if (wsainitialized == 0)
+    {
+        if (WSAStartup(0x101, &ws) == SOCKET_ERROR)
+            return 0;
+
+        wsainitialized = 1;
+    }
+#endif
+
+    if ((h = gethostbyname(host)) == NULL)
+    {
+        initprintf("Couldn't resolve %s!\n", host);
+        return 0;
+    }
+
+    dest_addr.sin_addr.s_addr = ((struct in_addr *)(h->h_addr))->s_addr;
+    dest_addr.sin_family = AF_INET;
+    dest_addr.sin_port = htons(80);
+
+    memset(&(dest_addr.sin_zero), '\0', 8);
+
+    mysock = socket(PF_INET, SOCK_STREAM, 0);
+
+    if (mysock == INVALID_SOCKET)
+    {
+        WSACleanup();
+        return 0;
+    }
+
+    initprintf("Connecting to http://%s\n",host);
+
+    if (connect(mysock, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)) == SOCKET_ERROR)
+        goto done;
+
+    i = send(mysock, req, strlen(req), 0);
+
+    if (i == SOCKET_ERROR)
+        goto done;
+
+    i = recv(mysock, (char *)&tempbuf, sizeof(tempbuf), 0);
+
+    if (i < 0)
+        goto done;
+
+    Bmemcpy(&otherbuf, &tempbuf, sizeof(otherbuf));
+
+    strtok(otherbuf, " ");
+
+    if ((tok = strtok(NULL, " ")) == NULL)
+        goto done;
+
+    if (Batol(tok) == 200)
+    {
+        for (i = 0; (unsigned)i < strlen(tempbuf); i++)  // HACK: all of this needs to die a fiery death; we just skip to the content
+        {
+            // instead of actually parsing any of the http headers
+            if (i > 4)
+                if (tempbuf[i-1] == '\n' && tempbuf[i-2] == '\r' && tempbuf[i-3] == '\n' && tempbuf[i-4] == '\r')
+                {
+                    while (j < 9)
+                    {
+                        ver[j] = tempbuf[i];
+                        i++, j++;
+                    }
+                    ver[j] = '\0';
+                    break;
+                }
+        }
+
+        if (j)
+        {
+            strcpy(buffer, ver);
+            r = 1;
+            goto done;
+        }
+    }
+
+done:
+    closesocket(mysock);
+    WSACleanup();
+
+    return r;
+}
+#endif
diff --git a/source/build/include/clip.h b/source/build/include/clip.h
index 696a43bf2..0fdc288e0 100644
--- a/source/build/include/clip.h
+++ b/source/build/include/clip.h
@@ -46,6 +46,8 @@ extern "C" {
 #define CM_NOROT(Spri) (sprite[Spri].cstat&2)
 #define CM_NOROTS(Sect) (sector[Sect].CM_CSTAT&2)
 
+extern vec2_t hitscangoal;
+
 typedef struct
 {
     int16_t qbeg, qend;  // indices into sectq
diff --git a/source/build/src/clip.cpp b/source/build/src/clip.cpp
index 6f9691c0f..0b469dcff 100644
--- a/source/build/src/clip.cpp
+++ b/source/build/src/clip.cpp
@@ -61,7 +61,7 @@ static usectortype *loadsector;
 static uwalltype *loadwall, *loadwallinv;
 static uspritetype *loadsprite;
 
-static vec2_t const hitscangoal = { (1<<29)-1, (1<<29)-1 };
+vec2_t hitscangoal = { (1<<29)-1, (1<<29)-1 };
 #ifdef USE_OPENGL
 int32_t hitallsprites = 0;
 #endif
diff --git a/source/build/src/tiles.cpp b/source/build/src/tiles.cpp
index 9d4c494fc..f14c19617 100644
--- a/source/build/src/tiles.cpp
+++ b/source/build/src/tiles.cpp
@@ -49,7 +49,6 @@ static buildvfs_kfd artfil;
 ////////// Per-map ART file loading //////////
 
 // Some forward declarations.
-static void tileUpdatePicSiz(int32_t picnum);
 static const char *artGetIndexedFileName(int32_t tilefilei);
 static int32_t artReadIndexedFile(int32_t tilefilei);
 
@@ -267,7 +266,7 @@ void tileDelete(int32_t const tile)
 #endif
 }
 
-static void tileUpdatePicSiz(int32_t picnum)
+void tileUpdatePicSiz(int32_t picnum)
 {
     int j = 15;
 
diff --git a/source/libsmackerdec/COPYING b/source/libsmackerdec/COPYING
new file mode 100644
index 000000000..00b4fedfe
--- /dev/null
+++ b/source/libsmackerdec/COPYING
@@ -0,0 +1,504 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/source/libsmackerdec/include/BitReader.h b/source/libsmackerdec/include/BitReader.h
new file mode 100644
index 000000000..39a1511f3
--- /dev/null
+++ b/source/libsmackerdec/include/BitReader.h
@@ -0,0 +1,54 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef _SmackerBitReader_h_
+#define _SmackerBitReader_h_
+
+#include <stdint.h>
+#include "FileStream.h"
+
+namespace SmackerCommon {
+
+class BitReader
+{
+	public:
+		BitReader(SmackerCommon::FileStream &file, uint32_t size);
+		~BitReader();
+		uint32_t GetBit();
+		uint32_t GetBits(uint32_t n);
+		void SkipBits(uint32_t n);
+
+		uint32_t GetSize();
+		uint32_t GetPosition();
+
+	private:
+		uint32_t totalSize;
+		uint32_t currentOffset;
+		uint32_t bytesRead;
+
+		SmackerCommon::FileStream *file;
+
+		uint8_t *cache;
+
+		void FillCache();
+};
+
+} // close namespace SmackerCommon
+
+#endif
\ No newline at end of file
diff --git a/source/libsmackerdec/include/FileStream.h b/source/libsmackerdec/include/FileStream.h
new file mode 100644
index 000000000..7fe408466
--- /dev/null
+++ b/source/libsmackerdec/include/FileStream.h
@@ -0,0 +1,66 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef _SmackerFileStream_h_
+#define _SmackerFileStream_h_
+
+#include <string>
+#include "cache1d.h"
+#include "compat.h"
+#include <stdint.h>
+
+namespace SmackerCommon {
+
+class FileStream
+{
+	public:
+
+		bool Open(const std::string &fileName);
+		bool Is_Open();
+		void Close();
+
+		int32_t ReadBytes(uint8_t *data, uint32_t nBytes);
+
+		uint32_t ReadUint32LE();
+		uint32_t ReadUint32BE();
+
+		uint16_t ReadUint16LE();
+		uint16_t ReadUint16BE();
+
+		uint8_t ReadByte();
+
+		enum SeekDirection{
+			kSeekCurrent = 0,
+			kSeekStart   = 1,
+			kSeekEnd     = 2
+		};
+
+		bool Seek(int32_t offset, SeekDirection = kSeekStart);
+		bool Skip(int32_t offset);
+
+		int32_t GetPosition();
+		bool Is_Eos();
+
+	private:
+		int file;
+};
+
+} // close namespace SmackerCommon
+
+#endif
diff --git a/source/libsmackerdec/include/HuffmanVLC.h b/source/libsmackerdec/include/HuffmanVLC.h
new file mode 100644
index 000000000..668249317
--- /dev/null
+++ b/source/libsmackerdec/include/HuffmanVLC.h
@@ -0,0 +1,43 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef _SmackerHuffmanVLC_h_
+#define _SmackerHuffmanVLC_h_
+
+#include <stdint.h>
+#include "BitReader.h"
+#include <vector>
+
+namespace SmackerCommon {
+
+struct VLC
+{
+    uint32_t symbol;
+    uint32_t code;
+};
+
+typedef std::vector< std::vector<VLC> > VLCtable;
+
+uint16_t VLC_GetCodeBits(BitReader &bits, VLCtable &table);
+void     VLC_InitTable  (VLCtable &table, uint32_t maxLength, uint32_t size, int *lengths, uint32_t *bits);
+uint32_t VLC_GetSize    (VLCtable &table);
+
+} // close namespace SmackerCommon
+
+#endif
diff --git a/source/libsmackerdec/include/LogError.h b/source/libsmackerdec/include/LogError.h
new file mode 100644
index 000000000..95a572961
--- /dev/null
+++ b/source/libsmackerdec/include/LogError.h
@@ -0,0 +1,31 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef _SmackerLogError_h_
+#define _SmackerLogError_h_
+
+#include <string>
+
+namespace SmackerCommon {
+
+void LogError(const std::string &error);
+
+} // close namespace SmackerCommon
+
+#endif
\ No newline at end of file
diff --git a/source/libsmackerdec/include/SmackerDecoder.h b/source/libsmackerdec/include/SmackerDecoder.h
new file mode 100644
index 000000000..feb719571
--- /dev/null
+++ b/source/libsmackerdec/include/SmackerDecoder.h
@@ -0,0 +1,167 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/* This code is based on smacker.c from the FFmpeg project which can be obtained from http://www.ffmpeg.org/
+ * below is the license from smacker.c
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/*
+ * Smacker decoder
+ * Copyright (c) 2006 Konstantin Shishkov
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _SmackerDecoder_h_
+#define _SmackerDecoder_h_
+
+#include <stdint.h>
+#include "FileStream.h"
+#include "BitReader.h"
+#include <vector>
+
+// exportable interface
+struct SmackerHandle
+{
+	bool isValid;
+	int instanceIndex;
+};
+
+struct SmackerAudioInfo
+{
+	uint32_t sampleRate;
+	uint8_t  nChannels;
+	uint8_t  bitsPerSample;
+
+	uint32_t idealBufferSize;
+};
+
+SmackerHandle     Smacker_Open                 (const char* fileName);
+void              Smacker_Close                (SmackerHandle &handle);
+uint32_t          Smacker_GetNumAudioTracks    (SmackerHandle &handle);
+SmackerAudioInfo  Smacker_GetAudioTrackDetails (SmackerHandle &handle, uint32_t trackIndex);
+uint32_t          Smacker_GetAudioData         (SmackerHandle &handle, uint32_t trackIndex, int16_t *data);
+uint32_t          Smacker_GetNumFrames         (SmackerHandle &handle);
+void              Smacker_GetFrameSize         (SmackerHandle &handle, uint32_t &width, uint32_t &height);
+uint32_t          Smacker_GetCurrentFrameNum   (SmackerHandle &handle);
+uint32_t          Smacker_GetNextFrame         (SmackerHandle &handle);
+float             Smacker_GetFrameRate         (SmackerHandle &handle);
+void              Smacker_GetPalette           (SmackerHandle &handle, uint8_t *palette);
+void              Smacker_GetFrame             (SmackerHandle &handle, uint8_t *frame);
+void              Smacker_GotoFrame            (SmackerHandle &handle, uint32_t frameNum);
+
+const int kMaxAudioTracks = 7;
+
+// forward declare
+struct HuffContext;
+struct DBCtx;
+
+struct SmackerAudioTrack
+{
+	uint32_t sizeInBytes;
+	uint32_t flags;
+	uint32_t sampleRate;
+	uint8_t  nChannels;
+	uint8_t  bitsPerSample;
+//	int	     compressionType;
+
+	uint8_t  *buffer;
+	uint32_t bufferSize;
+
+	uint32_t bytesReadThisFrame;
+};
+
+class SmackerDecoder
+{
+	public:
+		uint32_t frameWidth;
+		uint32_t frameHeight;
+
+		SmackerDecoder();
+		~SmackerDecoder();
+
+		bool Open(const std::string &fileName);
+		void GetPalette(uint8_t *palette);
+		void GetFrame(uint8_t *frame);
+
+		SmackerAudioInfo GetAudioTrackDetails(uint32_t trackIndex);
+		uint32_t GetAudioData(uint32_t trackIndex, int16_t *audioBuffer);
+		uint32_t GetNumFrames();
+		uint32_t GetCurrentFrameNum();
+		float GetFrameRate();
+		void GetNextFrame();
+		void GotoFrame(uint32_t frameNum);
+
+	private:
+		SmackerCommon::FileStream file;
+		char signature[4];
+
+		// video related members
+		uint32_t nFrames;
+		uint32_t fps; // frames per second
+
+		uint8_t palette[768];
+		uint8_t *picture;
+
+		bool isVer4;
+
+		SmackerAudioTrack audioTracks[kMaxAudioTracks];
+
+		uint32_t treeSize;
+		uint32_t mMapSize, MClrSize, fullSize, typeSize;
+
+		std::vector<int> mmap_tbl;
+		std::vector<int> mclr_tbl;
+		std::vector<int> full_tbl;
+		std::vector<int> type_tbl;
+
+		int mmap_last[3], mclr_last[3], full_last[3], type_last[3];
+
+		std::vector<uint32_t> frameSizes;
+		std::vector<uint8_t> frameFlags;
+
+		uint32_t currentFrame;
+		int32_t nextPos;
+
+		bool DecodeHeaderTrees();
+		int DecodeHeaderTree(SmackerCommon::BitReader &bits, std::vector<int> &recodes, int *last, int size);
+		int DecodeTree(SmackerCommon::BitReader &bits, HuffContext *hc, uint32_t prefix, int length);
+		int DecodeBigTree(SmackerCommon::BitReader &bits, HuffContext *hc, DBCtx *ctx);
+		int GetCode(SmackerCommon::BitReader &bits, std::vector<int> &recode, int *last);
+		int ReadPacket();
+		int DecodeFrame(uint32_t frameSize);
+		void GetFrameSize(uint32_t &width, uint32_t &height);
+		int DecodeAudio(uint32_t size, SmackerAudioTrack &track);
+};
+
+#endif
diff --git a/source/libsmackerdec/src/BitReader.cpp b/source/libsmackerdec/src/BitReader.cpp
new file mode 100644
index 000000000..81afa947d
--- /dev/null
+++ b/source/libsmackerdec/src/BitReader.cpp
@@ -0,0 +1,91 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "BitReader.h"
+#include <assert.h>
+
+namespace SmackerCommon {
+
+BitReader::BitReader(SmackerCommon::FileStream &file, uint32_t size)
+{
+	this->file = &file;
+	this->totalSize = size;
+	this->currentOffset = 0;
+	this->bytesRead = 0;
+
+    this->cache = (uint8_t*)Xmalloc(size);
+    file.ReadBytes(this->cache, size);
+}
+
+BitReader::~BitReader()
+{
+    Bfree(this->cache);
+}
+
+void BitReader::FillCache()
+{
+}
+
+uint32_t BitReader::GetSize()
+{
+	return totalSize * 8;
+}
+
+uint32_t BitReader::GetPosition()
+{
+	return currentOffset;
+}
+
+uint32_t BitReader::GetBit()
+{
+	uint32_t ret = (cache[currentOffset>>3]>>(currentOffset&7))&1;
+    currentOffset++;
+	return ret;
+}
+
+uint32_t BitReader::GetBits(uint32_t n)
+{
+	uint32_t ret = 0;
+
+	int bitsTodo = n;
+
+	uint32_t theShift = 0;
+
+	while (bitsTodo)
+	{
+		uint32_t bit = GetBit();
+		bit <<= theShift;
+
+		theShift++;
+
+		ret |= bit;
+
+		bitsTodo--;
+	}
+
+	return ret;
+}
+
+void BitReader::SkipBits(uint32_t n)
+{
+	GetBits(n);
+}
+
+} // close namespace SmackerCommon
+
diff --git a/source/libsmackerdec/src/FileStream.cpp b/source/libsmackerdec/src/FileStream.cpp
new file mode 100644
index 000000000..414c68f4c
--- /dev/null
+++ b/source/libsmackerdec/src/FileStream.cpp
@@ -0,0 +1,131 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "FileStream.h"
+#include <stdlib.h>
+
+namespace SmackerCommon {
+
+bool FileStream::Open(const std::string &fileName)
+{
+    file = kopen4loadfrommod(fileName.c_str(), 0);
+	if (file == -1)
+	{
+		// log error
+		return false;
+	}
+
+	return true;
+}
+
+bool FileStream::Is_Open()
+{
+	return file != -1;
+}
+
+void FileStream::Close()
+{
+    kclose(file);
+    file = -1;
+}
+
+int32_t FileStream::ReadBytes(uint8_t *data, uint32_t nBytes)
+{
+	uint32_t nCount = (uint32_t)kread(file, data, static_cast<int32_t>(nBytes));
+
+	if (nCount != nBytes)
+	{
+		return 0;
+	}
+
+	return (int32_t)nCount;
+}
+
+uint32_t FileStream::ReadUint32LE()
+{
+	uint32_t value;
+	kread(file, &value, 4);
+	return B_LITTLE32(value);
+}
+
+uint32_t FileStream::ReadUint32BE()
+{
+	uint32_t value;
+    kread(file, &value, 4);
+	return B_BIG32(value);
+}
+
+uint16_t FileStream::ReadUint16LE()
+{
+	uint16_t value;
+    kread(file, &value, 2);
+	return B_LITTLE16(value);
+}
+
+uint16_t FileStream::ReadUint16BE()
+{
+	uint16_t value;
+    kread(file, &value, 2);
+	return B_BIG16(value);
+}
+
+uint8_t FileStream::ReadByte()
+{
+	uint8_t value;
+    kread(file, &value, 1);
+	return value;
+}
+
+bool FileStream::Seek(int32_t offset, SeekDirection direction)
+{
+    int32_t nStatus = -1;
+	if (kSeekStart == direction) {
+        nStatus = klseek(file, offset, SEEK_SET);
+	}
+	else if (kSeekCurrent == direction) {
+        nStatus = klseek(file, offset, SEEK_CUR);
+	}
+
+	// TODO - end seek
+	if (nStatus < 0)
+	{
+		// todo
+		return false;
+	}
+
+	return true;
+}
+
+bool FileStream::Skip(int32_t offset)
+{
+	return Seek(offset, kSeekCurrent);
+}
+
+bool FileStream::Is_Eos()
+{
+    // TODO:
+    return false;
+}
+
+int32_t FileStream::GetPosition()
+{
+    return ktell(file);
+}
+
+} // close namespace SmackerCommon
diff --git a/source/libsmackerdec/src/HuffmanVLC.cpp b/source/libsmackerdec/src/HuffmanVLC.cpp
new file mode 100644
index 000000000..3f7fd45f8
--- /dev/null
+++ b/source/libsmackerdec/src/HuffmanVLC.cpp
@@ -0,0 +1,71 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include <HuffmanVLC.h>
+
+namespace SmackerCommon {
+
+uint16_t VLC_GetCodeBits(BitReader &bits, VLCtable &table)
+{
+	uint32_t codeBits = 0;
+
+	// search each length array
+	for (uint32_t i = 0; i < table.size(); i++)
+	{
+		// get and add a new bit to codeBits
+		uint32_t theBit = bits.GetBit() << i;
+		codeBits |= theBit;
+
+		// search for a code match
+		for (uint32_t j = 0; j < table[i].size(); j++)
+		{
+			if (codeBits == table[i][j].code)
+			{
+				return table[i][j].symbol;
+			}
+		}
+	}
+
+	// shouldn't get here..
+	return 0;
+}
+
+void VLC_InitTable(VLCtable &table, uint32_t maxLength, uint32_t size, int *lengths, uint32_t *bits)
+{
+	table.resize(maxLength);
+
+	for (uint32_t i = 0; i < size; i++)
+	{
+		VLC newCode;
+		newCode.symbol = i;
+		newCode.code   = bits[i];
+
+		uint32_t codeLength = lengths[i];
+
+		if (codeLength)
+			table[codeLength - 1].push_back(newCode);
+	}
+}
+
+uint32_t VLC_GetSize(VLCtable &table)
+{
+	return table.size();
+}
+
+} // close namespace SmackerCommon
diff --git a/source/libsmackerdec/src/LogError.cpp b/source/libsmackerdec/src/LogError.cpp
new file mode 100644
index 000000000..82a648a41
--- /dev/null
+++ b/source/libsmackerdec/src/LogError.cpp
@@ -0,0 +1,31 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "LogError.h"
+
+namespace SmackerCommon {
+
+static std::string LastError;
+
+void LogError(const std::string &error)
+{
+	LastError = error;
+}
+
+} // close namespace SmackerCommon
diff --git a/source/libsmackerdec/src/SmackerDecoder.cpp b/source/libsmackerdec/src/SmackerDecoder.cpp
new file mode 100644
index 000000000..e280bcde3
--- /dev/null
+++ b/source/libsmackerdec/src/SmackerDecoder.cpp
@@ -0,0 +1,1157 @@
+/*
+ * libsmackerdec - Smacker video decoder
+ * Copyright (C) 2011 Barry Duncan
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/* This code is heavily based on smacker.c from the FFmpeg project which can be obtained from http://www.ffmpeg.org/
+ * below is the license from smacker.c
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/*
+ * Smacker decoder
+ * Copyright (c) 2006 Konstantin Shishkov
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "SmackerDecoder.h"
+#include "HuffmanVLC.h"
+#include "LogError.h"
+#include <assert.h>
+#include <algorithm>
+#include <string.h>
+#include "compat.h"
+
+std::vector<class SmackerDecoder*> classInstances;
+
+SmackerHandle Smacker_Open(const char* fileName)
+{
+	SmackerHandle newHandle;
+	newHandle.isValid = false;
+	newHandle.instanceIndex = -1;
+
+	SmackerDecoder *newDecoder = new SmackerDecoder();
+	if (!newDecoder->Open(fileName))
+	{
+		delete newDecoder;
+		return newHandle;
+	}
+
+	// add instance to global instance vector
+	classInstances.push_back(newDecoder);
+
+	// get a handle ID
+	newHandle.instanceIndex = classInstances.size() - 1;
+
+	// loaded ok, make handle valid
+	newHandle.isValid = true;
+
+	return newHandle;
+}
+
+void Smacker_Close(SmackerHandle &handle)
+{
+	if (!classInstances.at(handle.instanceIndex))
+	{
+		// invalid handle
+		return;
+	}
+
+	// close bink decoder
+	delete classInstances[handle.instanceIndex];
+	classInstances[handle.instanceIndex] = 0;
+
+	handle.instanceIndex = -1;
+	handle.isValid = false;
+}
+
+uint32_t Smacker_GetNumAudioTracks(SmackerHandle &handle)
+{
+	UNREFERENCED_PARAMETER(handle);
+	// TODO: fixme
+	return 1;
+}
+
+SmackerAudioInfo Smacker_GetAudioTrackDetails(SmackerHandle &handle, uint32_t trackIndex)
+{
+	return classInstances[handle.instanceIndex]->GetAudioTrackDetails(trackIndex);
+}
+
+/* Get a frame's worth of audio data. 
+ * 
+ * 'data' needs to be a pointer to allocated memory that this function will fill.
+ * You can find the size (in bytes) to make this buffer by calling Bink_GetAudioTrackDetails()
+ * and checking the 'idealBufferSize' member in the returned AudioInfo struct
+ */
+uint32_t Smacker_GetAudioData(SmackerHandle &handle, uint32_t trackIndex, int16_t *data)
+{
+	return classInstances[handle.instanceIndex]->GetAudioData(trackIndex, data);
+}
+
+uint32_t Smacker_GetNumFrames(SmackerHandle &handle)
+{
+	return classInstances[handle.instanceIndex]->GetNumFrames();
+}
+
+void Smacker_GetFrameSize(SmackerHandle &handle, uint32_t &width, uint32_t &height)
+{
+	width  = classInstances[handle.instanceIndex]->frameWidth;
+	height = classInstances[handle.instanceIndex]->frameHeight;
+}
+
+uint32_t Smacker_GetCurrentFrameNum(SmackerHandle &handle)
+{
+	return classInstances[handle.instanceIndex]->GetCurrentFrameNum();
+}
+
+uint32_t Smacker_GetNextFrame(SmackerHandle &handle)
+{
+	SmackerDecoder *decoder = classInstances[handle.instanceIndex];
+
+	uint32_t frameIndex = decoder->GetCurrentFrameNum();
+
+	decoder->GetNextFrame();
+
+	return frameIndex;
+}
+
+float Smacker_GetFrameRate(SmackerHandle &handle)
+{
+	return classInstances[handle.instanceIndex]->GetFrameRate();
+}
+
+void Smacker_GetPalette(SmackerHandle &handle, uint8_t *palette)
+{
+	classInstances[handle.instanceIndex]->GetPalette(palette);
+}
+
+void Smacker_GetFrame(SmackerHandle &handle, uint8_t *frame)
+{
+	classInstances[handle.instanceIndex]->GetFrame(frame);
+}
+
+void Smacker_GotoFrame(SmackerHandle &handle, uint32_t frameNum)
+{
+	classInstances[handle.instanceIndex]->GotoFrame(frameNum);
+}
+
+SmackerDecoder::SmackerDecoder()
+{
+	isVer4 = false;
+	currentFrame = 0;
+	picture = 0;
+	nextPos = 0;
+
+	for (int i = 0; i < kMaxAudioTracks; i++)
+	{
+		audioTracks[i].buffer = 0;
+	}
+}
+
+SmackerDecoder::~SmackerDecoder()
+{
+	for (int i = 0; i < kMaxAudioTracks; i++)
+	{
+		delete[] audioTracks[i].buffer;
+	}
+
+	delete[] picture;
+}
+
+// from bswap.h
+static /*av_always_inline av_const*/ uint16_t av_bswap16(uint16_t x)
+{
+    x= (x>>8) | (x<<8);
+    return x;
+}
+
+/* possible runs of blocks */
+static const int block_runs[64] = {
+      1,    2,    3,    4,    5,    6,    7,    8,
+      9,   10,   11,   12,   13,   14,   15,   16,
+     17,   18,   19,   20,   21,   22,   23,   24,
+     25,   26,   27,   28,   29,   30,   31,   32,
+     33,   34,   35,   36,   37,   38,   39,   40,
+     41,   42,   43,   44,   45,   46,   47,   48,
+     49,   50,   51,   52,   53,   54,   55,   56,
+     57,   58,   59,  128,  256,  512, 1024, 2048 };
+
+enum SmkBlockTypes {
+    SMK_BLK_MONO = 0,
+    SMK_BLK_FULL = 1,
+    SMK_BLK_SKIP = 2,
+    SMK_BLK_FILL = 3 };
+
+/* palette used in Smacker */
+static const uint8_t smk_pal[64] = {
+    0x00, 0x04, 0x08, 0x0C, 0x10, 0x14, 0x18, 0x1C,
+    0x20, 0x24, 0x28, 0x2C, 0x30, 0x34, 0x38, 0x3C,
+    0x41, 0x45, 0x49, 0x4D, 0x51, 0x55, 0x59, 0x5D,
+    0x61, 0x65, 0x69, 0x6D, 0x71, 0x75, 0x79, 0x7D,
+    0x82, 0x86, 0x8A, 0x8E, 0x92, 0x96, 0x9A, 0x9E,
+    0xA2, 0xA6, 0xAA, 0xAE, 0xB2, 0xB6, 0xBA, 0xBE,
+    0xC3, 0xC7, 0xCB, 0xCF, 0xD3, 0xD7, 0xDB, 0xDF,
+    0xE3, 0xE7, 0xEB, 0xEF, 0xF3, 0xF7, 0xFB, 0xFF
+};
+
+enum SAudFlags {
+    SMK_AUD_PACKED  = 0x80000000, 
+    SMK_AUD_16BITS  = 0x20000000, 
+    SMK_AUD_STEREO  = 0x10000000, 
+    SMK_AUD_BINKAUD = 0x08000000, 
+    SMK_AUD_USEDCT  = 0x04000000 
+};
+
+const int kSMKpal = 0x01;
+const int kFlagRingFrame = 0x01;
+
+const int kTreeBits = 9;
+const int kSMKnode = 0x80000000;
+
+const char *kSMK2iD = "SMK2";
+const char *kSMK4iD = "SMK4";
+
+
+/**
+ * Context used for code reconstructing
+ */
+typedef struct HuffContext {
+    int length;
+    int maxlength;
+    int current;
+
+	std::vector<uint32_t> bits;
+	std::vector<int> lengths;
+	std::vector<int> values;
+
+} HuffContext;
+
+/* common parameters used for decode_bigtree */
+typedef struct DBCtx {
+	SmackerCommon::VLCtable v1;
+	SmackerCommon::VLCtable v2;
+	std::vector<int> recode1, recode2;
+    int escapes[3];
+    int *last;
+    int lcur;
+} DBCtx;
+
+
+static void last_reset(std::vector<int> &recode, int *last) {
+    recode[last[0]] = recode[last[1]] = recode[last[2]] = 0;
+}
+
+/* get code and update history */
+int SmackerDecoder::GetCode(SmackerCommon::BitReader &bits, std::vector<int> &recode, int *last)
+{
+	int *table = &recode[0];
+
+    int v, b;
+
+    b = bits.GetPosition();
+
+    while (*table & kSMKnode)
+	{
+        if (bits.GetBit())
+            table += (*table) & (~kSMKnode);
+        table++;
+    }
+    v = *table;
+    b = bits.GetPosition() - b;
+
+    if (v != recode[last[0]]) {
+        recode[last[2]] = recode[last[1]];
+        recode[last[1]] = recode[last[0]];
+        recode[last[0]] = v;
+    }
+    return v;
+}
+
+bool SmackerDecoder::Open(const std::string &fileName)
+{
+	// open the file (read only)
+	file.Open(fileName);
+	if (!file.Is_Open())
+	{
+		SmackerCommon::LogError("Can't open file " + fileName);
+		return false;
+	}
+
+	// check the file signature
+	file.ReadBytes((uint8_t*)signature, 4);
+	if (memcmp(signature, kSMK2iD, 4) != 0
+		&& memcmp(signature, kSMK4iD, 4) != 0)
+	{
+		SmackerCommon::LogError("Unknown Smacker signature");
+		return false;
+	}
+
+	if (!memcmp(signature, kSMK4iD, 4)) {
+		isVer4 = true;
+	}
+
+	frameWidth  = file.ReadUint32LE();
+	frameHeight = file.ReadUint32LE();
+	nFrames = file.ReadUint32LE();
+
+	picture = new uint8_t[frameWidth * frameHeight];
+
+	int32_t frameRate = file.ReadUint32LE();
+
+	if (frameRate > 0)
+		fps = 1000 / frameRate;
+	else if (frameRate < 0)
+		fps = 100000 / (-frameRate);
+	else
+		fps = 10;
+
+	uint32_t flags = file.ReadUint32LE();
+
+	if (flags & kFlagRingFrame) {
+		nFrames++;
+	}
+
+	for (int i = 0; i < kMaxAudioTracks; i++) {
+		audioTracks[i].sizeInBytes = file.ReadUint32LE();
+	}
+
+	treeSize = file.ReadUint32LE();
+	mMapSize = file.ReadUint32LE();
+	MClrSize = file.ReadUint32LE();
+	fullSize = file.ReadUint32LE();
+	typeSize = file.ReadUint32LE();
+
+	for (int i = 0; i < kMaxAudioTracks; i++) {
+		audioTracks[i].flags = file.ReadUint32LE();
+	}
+
+	// skip pad
+	file.Skip(4);
+
+	if (nFrames > 0xFFFFFF)
+	{
+		SmackerCommon::LogError("Too many frames!");
+		return false;
+	}
+
+	frameSizes.resize(nFrames);
+	frameFlags.resize(nFrames);
+
+	// read frame info
+	for (uint32_t i = 0; i < nFrames; i++) {
+		frameSizes[i] = file.ReadUint32LE();
+	}
+	for (uint32_t i = 0; i < nFrames; i++) {
+		frameFlags[i] = file.ReadByte();
+	}
+
+	// handle possible audio streams
+	for (int i = 0; i < kMaxAudioTracks; i++)
+	{
+		audioTracks[i].buffer = 0;
+		audioTracks[i].bufferSize = 0;
+		audioTracks[i].bytesReadThisFrame = 0;
+
+		if (audioTracks[i].flags & 0xFFFFFF)
+		{
+/*
+            if (audioTracks[i].flags & SMK_AUD_BINKAUD) {
+                audioTracks[i].compressionType = SMK_AUD_BINKAUD;
+            } else if (audioTracks[i].flags & SMK_AUD_USEDCT) {
+                audioTracks[i].compressionType = SMK_AUD_USEDCT;
+            } else if (audioTracks[i].flags & SMK_AUD_PACKED){
+                ast[i]->codec->codec_id = CODEC_ID_SMACKAUDIO;
+                ast[i]->codec->codec_tag = MKTAG('S', 'M', 'K', 'A');
+            } else {
+                ast[i]->codec->codec_id = CODEC_ID_PCM_U8;
+            }
+*/
+			audioTracks[i].nChannels = (audioTracks[i].flags & SMK_AUD_STEREO) ? 2 : 1;
+			audioTracks[i].sampleRate = audioTracks[i].flags & 0xFFFFFF;
+			audioTracks[i].bitsPerSample = (audioTracks[i].flags & SMK_AUD_16BITS) ? 16 : 8;
+		}
+	}
+
+	memset(palette, 0, 768);
+	
+	DecodeHeaderTrees();
+
+	// set nextPos to where we are now, as next data is frame 1
+	nextPos = file.GetPosition();
+
+	// determine max buffer sizes for audio tracks
+	file.Seek(nextPos, SmackerCommon::FileStream::kSeekStart);
+
+	uint32_t UNUSED(frameSize) = frameSizes[0] & (~3);
+	uint8_t frameFlag  = frameFlags[0];
+
+	// skip over palette
+	if (frameFlag & kSMKpal)
+	{
+		uint32_t size = file.ReadByte();
+		size = size * 4 - 1;
+		file.Skip(size);
+	}
+
+	frameFlag >>= 1;
+
+	for (int i = 0; i < kMaxAudioTracks; i++) 
+	{
+		if (frameFlag & 1) 
+		{
+			// skip size
+			file.Skip(4);
+			
+			uint32_t unpackedSize = file.ReadUint32LE();
+
+			audioTracks[i].bufferSize = unpackedSize;
+			audioTracks[i].buffer = new uint8_t[unpackedSize];
+		}	
+		frameFlag >>= 1;
+	}
+
+	return true;
+}
+
+// static int smacker_decode_tree(GetBitContext *gb, HuffContext *hc, uint32_t prefix, int length)
+int SmackerDecoder::DecodeTree(SmackerCommon::BitReader &bits, HuffContext *hc, uint32_t prefix, int length)
+{
+    if (!bits.GetBit()) // Leaf
+	{
+        if (hc->current >= 256){
+			SmackerCommon::LogError("Tree size exceeded!");
+            return -1;
+        }
+        if (length){
+            hc->bits[hc->current] = prefix;
+            hc->lengths[hc->current] = length;
+        } else {
+            hc->bits[hc->current] = 0;
+            hc->lengths[hc->current] = 0;
+        }
+        hc->values[hc->current] = bits.GetBits(8);
+
+        hc->current++;
+        if (hc->maxlength < length)
+            hc->maxlength = length;
+        return 0;
+    } else { //Node
+        int r;
+        length++;
+        r = DecodeTree(bits, hc, prefix, length);
+        if (r)
+            return r;
+        return DecodeTree(bits, hc, prefix | (1 << (length - 1)), length);
+    }
+}
+
+/**
+ * Decode header tree
+ */
+int SmackerDecoder::DecodeBigTree(SmackerCommon::BitReader &bits, HuffContext *hc, DBCtx *ctx)
+{
+	if (!bits.GetBit()) // Leaf
+	{
+		int val, i1, i2, b1, b2;
+
+		i1 = 0;
+		i2 = 0;
+
+		if (hc->current >= hc->length){
+			SmackerCommon::LogError("Tree size exceeded!");
+            return -1;
+        }
+
+        b1 = bits.GetPosition();
+
+		if (VLC_GetSize(ctx->v1))
+		{
+			i1 = VLC_GetCodeBits(bits, ctx->v1);
+		}
+
+        b1 = bits.GetPosition() - b1;
+        b2 = bits.GetPosition();
+
+		if (VLC_GetSize(ctx->v2))
+		{
+			i2 = VLC_GetCodeBits(bits, ctx->v2);
+		}
+
+        b2 = bits.GetPosition() - b2;
+        if (i1 < 0 || i2 < 0)
+            return -1;
+        val = ctx->recode1[i1] | (ctx->recode2[i2] << 8);
+        if (val == ctx->escapes[0]) {
+            ctx->last[0] = hc->current;
+            val = 0;
+        } else if (val == ctx->escapes[1]) {
+            ctx->last[1] = hc->current;
+            val = 0;
+        } else if (val == ctx->escapes[2]) {
+            ctx->last[2] = hc->current;
+            val = 0;
+        }
+
+        hc->values[hc->current++] = val;
+        return 1;
+    } else { //Node
+        int r = 0, t;
+
+        t = hc->current++;
+        r = DecodeBigTree(bits, hc, ctx);
+        if (r < 0)
+            return r;
+        hc->values[t] = kSMKnode | r;
+        r++;
+        r += DecodeBigTree(bits, hc, ctx);
+        return r;
+    }
+}
+
+/**
+ * Store large tree as Libav's vlc codes
+ */
+int SmackerDecoder::DecodeHeaderTree(SmackerCommon::BitReader &bits, std::vector<int> &recodes, int *last, int size)
+{
+	HuffContext huff;
+	HuffContext tmp1, tmp2;
+	int escapes[3];
+	DBCtx ctx;
+
+	if ((uint32_t)size >= UINT_MAX>>4)
+	{
+		SmackerCommon::LogError("Size too large");
+		return -1;
+	}
+
+	tmp1.length = 256;
+	tmp1.maxlength = 0;
+	tmp1.current = 0;
+
+	tmp1.bits.resize(256);
+	tmp1.lengths.resize(256);
+	tmp1.values.resize(256);
+
+	tmp2.length = 256;
+	tmp2.maxlength = 0;
+	tmp2.current = 0;
+
+	tmp2.bits.resize(256);
+	tmp2.lengths.resize(256);
+	tmp2.values.resize(256);
+
+	// low byte tree
+	if (bits.GetBit()) // 1: Read Tag
+	{
+		DecodeTree(bits, &tmp1, 0, 0);
+
+		bits.SkipBits(1);
+
+		VLC_InitTable(ctx.v1, tmp1.maxlength, tmp1.current, &tmp1.lengths[0], &tmp1.bits[0]);
+	}
+	else
+	{
+		// Skipping low bytes tree
+	}
+
+	// high byte tree
+	if (bits.GetBit())
+	{
+		DecodeTree(bits, &tmp2, 0, 0);
+
+		uint32_t UNUSED(end) = bits.GetPosition();
+
+		bits.SkipBits(1);
+
+		VLC_InitTable(ctx.v2, tmp2.maxlength, tmp2.current, &tmp2.lengths[0], &tmp2.bits[0]);
+	}
+	else
+	{
+		// Skipping high bytes tree
+	}
+
+	escapes[0]  = bits.GetBits(8);
+	escapes[0] |= bits.GetBits(8) << 8;
+	escapes[1]  = bits.GetBits(8);
+	escapes[1] |= bits.GetBits(8) << 8;
+	escapes[2]  = bits.GetBits(8);
+	escapes[2] |= bits.GetBits(8) << 8;
+
+	last[0] = last[1] = last[2] = -1;
+
+	ctx.escapes[0] = escapes[0];
+    ctx.escapes[1] = escapes[1];
+    ctx.escapes[2] = escapes[2];
+
+    ctx.recode1 = tmp1.values;
+    ctx.recode2 = tmp2.values;
+    ctx.last = last;
+
+	huff.length = ((size + 3) >> 2) + 3;
+    huff.maxlength = 0;
+    huff.current = 0;
+	huff.values.resize(huff.length);
+
+	DecodeBigTree(bits, &huff, &ctx);
+
+    bits.SkipBits(1);
+
+    if (ctx.last[0] == -1) ctx.last[0] = huff.current++;
+    if (ctx.last[1] == -1) ctx.last[1] = huff.current++;
+    if (ctx.last[2] == -1) ctx.last[2] = huff.current++;
+
+	recodes = huff.values;
+
+	return 0;
+}
+
+// static int decode_header_trees(SmackVContext *smk) {
+bool SmackerDecoder::DecodeHeaderTrees()
+{
+	SmackerCommon::BitReader bits(file, treeSize);
+
+	if (!bits.GetBit())
+	{
+		// Skipping MMAP tree
+		mmap_tbl.resize(2);
+		mmap_tbl[0] = 0;
+		mmap_last[0] = mmap_last[1] = mmap_last[2] = 1;
+	}
+	else
+	{
+		DecodeHeaderTree(bits, mmap_tbl, mmap_last, mMapSize);
+	}
+
+	if (!bits.GetBit())
+	{
+		// Skipping MCLR tree
+		mclr_tbl.resize(2);
+		mclr_tbl[0] = 0;
+		mclr_last[0] = mclr_last[1] = mclr_last[2] = 1;
+	}
+	else
+	{
+		DecodeHeaderTree(bits, mclr_tbl, mclr_last, MClrSize);
+	}
+
+	if (!bits.GetBit())
+	{
+		// Skipping FULL tree
+		full_tbl.resize(2);
+		full_tbl[0] = 0;
+		full_last[0] = full_last[1] = full_last[2] = 1;
+	}
+	else
+	{
+		DecodeHeaderTree(bits, full_tbl, full_last, fullSize);
+	}
+
+	if (!bits.GetBit())
+	{
+		// Skipping TYPE tree
+		type_tbl.resize(2);
+		type_tbl[0] = 0;
+		type_last[0] = type_last[1] = type_last[2] = 1;
+	}
+	else
+	{
+		DecodeHeaderTree(bits, type_tbl, type_last, typeSize);
+	}
+
+	/* FIXME - we don't seems to read/use EVERY bit we 'load' into the bit reader
+	 * and as my bitreader reads from the file rather than a buffer read from file
+	 * of size 'treeSize', I need to make sure I consume the remaining bits (and thus increment
+	 * the file read position to where the code expects it to be when this function returns (ie 
+	 * 'treeSize' number of bytes must be read
+	 */
+	uint32_t left = bits.GetSize() - bits.GetPosition();
+	bits.SkipBits(left);
+
+	return true;
+}
+
+void SmackerDecoder::GetNextFrame()
+{
+	ReadPacket();
+}
+
+int SmackerDecoder::ReadPacket()
+{
+	// test-remove
+	if (currentFrame >= nFrames)
+		return 1;
+
+	// seek to next frame position
+	file.Seek(nextPos, SmackerCommon::FileStream::kSeekStart);
+
+	uint32_t frameSize = frameSizes[currentFrame] & (~3);
+	uint8_t frameFlag  = frameFlags[currentFrame];
+
+	// handle palette change
+	if (frameFlag & kSMKpal)
+	{
+		int size, sz, t, off, j, pos;
+        uint8_t *pal = palette;
+        uint8_t oldpal[768];
+
+        memcpy(oldpal, pal, 768);
+        size = file.ReadByte();
+        size = size * 4 - 1;
+        frameSize -= size;
+        frameSize--;
+        sz = 0;
+        pos = file.GetPosition() + size;
+
+        while (sz < 256)
+		{
+            t = file.ReadByte();
+            if (t & 0x80){ /* skip palette entries */
+                sz  += (t & 0x7F)  + 1;
+                pal += ((t & 0x7F) + 1) * 3;
+            } else if (t & 0x40){ /* copy with offset */
+                off = file.ReadByte() * 3;
+                j = (t & 0x3F) + 1;
+                while (j-- && sz < 256) {
+                    *pal++ = oldpal[off + 0];
+                    *pal++ = oldpal[off + 1];
+                    *pal++ = oldpal[off + 2];
+                    sz++;
+                    off += 3;
+                }
+            } else { /* new entries */
+                *pal++ = smk_pal[t];
+                *pal++ = smk_pal[file.ReadByte() & 0x3F];
+                *pal++ = smk_pal[file.ReadByte() & 0x3F];
+                sz++;
+            }
+        }
+        
+		file.Seek(pos, SmackerCommon::FileStream::kSeekStart);
+	}
+
+	frameFlag >>= 1;
+
+	// check for and handle audio
+	for (int i = 0; i < kMaxAudioTracks; i++) 
+	{
+		audioTracks[i].bytesReadThisFrame = 0;
+
+		if (frameFlag & 1) 
+		{
+			uint32_t size = file.ReadUint32LE() - 4;
+			frameSize -= size;
+			frameSize -= 4;
+
+			DecodeAudio(size, audioTracks[i]);
+		}
+		frameFlag >>= 1;
+	}
+
+	if (frameSize == 0) {
+		return -1;
+	}
+
+	DecodeFrame(frameSize);
+
+	currentFrame++;
+
+	nextPos = file.GetPosition();
+
+	return 0;
+}
+
+int SmackerDecoder::DecodeFrame(uint32_t frameSize)
+{
+	last_reset(mmap_tbl, mmap_last);
+    last_reset(mclr_tbl, mclr_last);
+    last_reset(full_tbl, full_last);
+    last_reset(type_tbl, type_last);
+
+	int blocks, blk, bw, bh;
+    int i;
+    int stride;
+
+	uint8_t *out = picture; // set to output image
+
+	blk = 0;
+    bw = frameWidth  >> 2;
+    bh = frameHeight >> 2;
+    blocks = bw * bh;
+
+	stride = frameWidth;
+
+	uint32_t UNUSED(fileStart) = file.GetPosition();
+
+	SmackerCommon::BitReader bits(file, frameSize);
+
+	while (blk < blocks)
+	{
+		int type, run, mode;
+        uint16_t pix;
+
+        type = GetCode(bits, type_tbl, type_last);
+        run = block_runs[(type >> 2) & 0x3F];
+        switch (type & 3)
+		{
+        case SMK_BLK_MONO:
+            while (run-- && blk < blocks)
+			{
+                int clr, map;
+                int hi, lo;
+                clr = GetCode(bits, mclr_tbl, mclr_last);
+                map = GetCode(bits, mmap_tbl, mmap_last);
+
+                out = picture + (blk / bw) * (stride * 4) + (blk % bw) * 4;
+
+                hi = clr >> 8;
+                lo = clr & 0xFF;
+                for (i = 0; i < 4; i++) 
+				{
+                    if (map & 1) out[0] = hi; else out[0] = lo;
+                    if (map & 2) out[1] = hi; else out[1] = lo;
+                    if (map & 4) out[2] = hi; else out[2] = lo;
+                    if (map & 8) out[3] = hi; else out[3] = lo;
+                    map >>= 4;
+                    out += stride;
+                }
+                blk++;
+            }
+            break;
+        case SMK_BLK_FULL:
+            mode = 0;
+			if (kSMK4iD == signature) // In case of Smacker v4 we have three modes
+			{
+				if (bits.GetBit()) mode = 1;
+				else if (bits.GetBit()) mode = 2;
+			}
+
+            while (run-- && blk < blocks)
+			{
+                out = picture + (blk / bw) * (stride * 4) + (blk % bw) * 4;
+                switch (mode)
+				{
+                case 0:
+                    for (i = 0; i < 4; i++)
+					{
+                        pix = GetCode(bits, full_tbl, full_last);
+// FIX                        AV_WL16(out+2, pix);
+						out[2] = pix & 0xff;
+						out[3] = pix >> 8;
+
+                        pix = GetCode(bits, full_tbl, full_last);
+// FIX                        AV_WL16(out, pix);
+						out[0] = pix & 0xff;
+						out[1] = pix >> 8;
+                        out += stride;
+                    }
+                    break;
+                case 1:
+                    pix = GetCode(bits, full_tbl, full_last);
+                    out[0] = out[1] = pix & 0xFF;
+                    out[2] = out[3] = pix >> 8;
+                    out += stride;
+                    out[0] = out[1] = pix & 0xFF;
+                    out[2] = out[3] = pix >> 8;
+                    out += stride;
+                    pix = GetCode(bits, full_tbl, full_last);
+                    out[0] = out[1] = pix & 0xFF;
+                    out[2] = out[3] = pix >> 8;
+                    out += stride;
+                    out[0] = out[1] = pix & 0xFF;
+                    out[2] = out[3] = pix >> 8;
+                    out += stride;
+                    break;
+                case 2:
+                    for (i = 0; i < 2; i++)
+					{
+                        uint16_t pix1, pix2;
+                        pix2 = GetCode(bits, full_tbl, full_last);
+                        pix1 = GetCode(bits, full_tbl, full_last);
+
+// FIX                        AV_WL16(out, pix1);
+// FIX                        AV_WL16(out+2, pix2);
+						out[0] = pix1 & 0xff;
+						out[1] = pix1 >> 8;
+						out[2] = pix2 & 0xff;
+						out[3] = pix2 >> 8;
+
+                        out += stride;
+
+// FIX                        AV_WL16(out, pix1);
+// FIX                        AV_WL16(out+2, pix2);
+						out[0] = pix1 & 0xff;
+						out[1] = pix1 >> 8;
+						out[2] = pix2 & 0xff;
+						out[3] = pix2 >> 8;
+
+                        out += stride;
+                    }
+                    break;
+                }
+                blk++;
+            }
+            break;
+        case SMK_BLK_SKIP:
+            while (run-- && blk < blocks)
+                blk++;
+            break;
+        case SMK_BLK_FILL:
+            mode = type >> 8;
+            while (run-- && blk < blocks)
+			{
+                uint32_t col;
+                out = picture + (blk / bw) * (stride * 4) + (blk % bw) * 4;
+                col = mode * 0x01010101;
+                for (i = 0; i < 4; i++) {
+                    *((uint32_t*)out) = col;
+                    out += stride;
+                }
+                blk++;
+            }
+            break;
+        }
+	}
+
+	/* FIXME - we don't seems to read/use EVERY bit we 'load' into the bit reader
+	 * and as my bitreader reads from the file rather than a buffer read from file
+	 * of size 'frameSize', I need to make sure I consume the remaining bits (and thus increment
+	 * the file read position to where the code expects it to be when this function returns (ie 
+	 * 'frameSize' number of bytes must be read
+	 */
+	uint32_t left = bits.GetSize() - bits.GetPosition();
+	bits.SkipBits(left);
+
+	return 0;
+}
+
+/**
+ * Decode Smacker audio data
+ */
+int SmackerDecoder::DecodeAudio(uint32_t size, SmackerAudioTrack &track)
+{
+    HuffContext h[4];
+	SmackerCommon::VLCtable vlc[4];
+    int val;
+    int i, res;
+    int unpackedSize;
+    int sampleBits, stereo;
+    int pred[2] = {0, 0};
+
+	int16_t *samples = reinterpret_cast<int16_t*>(track.buffer);
+    int8_t *samples8 = reinterpret_cast<int8_t*>(track.buffer);
+
+	int buf_size = track.bufferSize;
+
+    if (buf_size <= 4) {
+		SmackerCommon::LogError("packet is too small");
+		return -1;
+    }
+
+	SmackerCommon::BitReader bits(file, size);
+
+    unpackedSize = bits.GetBits(32);
+
+    if (!bits.GetBit()) {
+		// no sound data
+        return 1;
+    }
+
+    stereo     = bits.GetBit();
+    sampleBits = bits.GetBit();
+
+    if (stereo ^ (track.nChannels != 1)) {
+		SmackerCommon::LogError("channels mismatch");
+		return -1;
+    }
+
+    memset(h, 0, sizeof(HuffContext) * 4);
+
+    // Initialize
+    for (i = 0; i < (1 << (sampleBits + stereo)); i++) {
+        h[i].length = 256;
+        h[i].maxlength = 0;
+        h[i].current = 0;
+        h[i].bits.resize(256);
+        h[i].lengths.resize(256);
+        h[i].values.resize(256);
+
+        bits.SkipBits(1);
+		DecodeTree(bits, &h[i], 0, 0);
+        bits.SkipBits(1);
+
+        if (h[i].current > 1) {
+			VLC_InitTable(vlc[i], h[i].maxlength, h[i].current, &h[i].lengths[0], &h[i].bits[0]);
+        }
+    }
+    if (sampleBits) { //decode 16-bit data
+        for (i = stereo; i >= 0; i--)
+            pred[i] = av_bswap16(bits.GetBits(16));
+        for (i = 0; i <= stereo; i++)
+            *samples++ = pred[i];
+        for (; i < unpackedSize / 2; i++) {
+            if (i & stereo) {
+				if (VLC_GetSize(vlc[2]))
+					res = VLC_GetCodeBits(bits, vlc[2]);
+                else
+                    res = 0;
+                val  = h[2].values[res];
+				if (VLC_GetSize(vlc[3]))
+					res = VLC_GetCodeBits(bits, vlc[3]);
+                else
+                    res = 0;
+                val |= h[3].values[res] << 8;
+                pred[1] += (int16_t)val;
+                *samples++ = pred[1];
+            } else {
+				if (VLC_GetSize(vlc[0]))
+					res = VLC_GetCodeBits(bits, vlc[0]);
+                else
+                    res = 0;
+                val  = h[0].values[res];
+				if (VLC_GetSize(vlc[1]))
+					res = VLC_GetCodeBits(bits, vlc[1]);
+                else
+                    res = 0;
+                val |= h[1].values[res] << 8;
+                pred[0] += val;
+                *samples++ = pred[0];
+            }
+        }
+    } 
+	else { //8-bit data
+        for (i = stereo; i >= 0; i--)
+            pred[i] = bits.GetBits(8);
+        for (i = 0; i <= stereo; i++)
+            *samples8++ = pred[i];
+        for (; i < unpackedSize; i++) {
+            if (i & stereo){
+				if (VLC_GetSize(vlc[1]))
+					res = VLC_GetCodeBits(bits, vlc[1]);
+                else
+                    res = 0;
+                pred[1] += (int8_t)h[1].values[res];
+                *samples8++ = pred[1];
+            } else {
+				if (VLC_GetSize(vlc[0]))
+				res = VLC_GetCodeBits(bits, vlc[0]);
+                else
+                    res = 0;
+                pred[0] += (int8_t)h[0].values[res];
+                *samples8++ = pred[0];
+            }
+        }
+    }
+
+	track.bytesReadThisFrame = unpackedSize;
+
+	uint32_t left = bits.GetSize() - bits.GetPosition();
+	bits.SkipBits(left);
+
+	return 0;
+}
+
+void SmackerDecoder::GetPalette(uint8_t *palette)
+{
+	memcpy(palette, this->palette, 768);
+}
+
+void SmackerDecoder::GetFrame(uint8_t *frame)
+{
+	memcpy(frame, this->picture, frameWidth * frameHeight);
+}
+
+void SmackerDecoder::GetFrameSize(uint32_t &width, uint32_t &height)
+{
+	width  = this->frameWidth;
+	height = this->frameHeight;
+}
+
+uint32_t SmackerDecoder::GetNumFrames()
+{
+	return nFrames;
+}
+
+uint32_t SmackerDecoder::GetCurrentFrameNum()
+{
+	return currentFrame;
+}
+
+float SmackerDecoder::GetFrameRate()
+{
+	return (float)fps;
+}
+
+void SmackerDecoder::GotoFrame(uint32_t frameNum)
+{
+	if (frameNum >= nFrames) {
+		SmackerCommon::LogError("Invalid frame number for GotoFrame");
+		return;
+	}
+
+	
+	// TODO
+
+	// seek to the desired frame (just set currentFrame)
+//	currentFrame = frameNum;
+
+	// what else? (memset some stuff?)
+}
+
+
+SmackerAudioInfo SmackerDecoder::GetAudioTrackDetails(uint32_t trackIndex)
+{
+	SmackerAudioInfo info;
+	SmackerAudioTrack *track = &audioTracks[trackIndex];
+
+	info.sampleRate = track->sampleRate;
+	info.nChannels  = track->nChannels;
+	info.bitsPerSample = track->bitsPerSample;
+
+	// audio buffer size in bytes
+	info.idealBufferSize = track->bufferSize;
+
+	return info;
+}
+
+uint32_t SmackerDecoder::GetAudioData(uint32_t trackIndex, int16_t *audioBuffer)
+{
+	if (!audioBuffer) {
+		return 0;
+	}
+
+	SmackerAudioTrack *track = &audioTracks[trackIndex];
+
+	if (track->bytesReadThisFrame) {
+		memcpy(audioBuffer, track->buffer, std::min(track->bufferSize, track->bytesReadThisFrame));
+	}
+
+	return track->bytesReadThisFrame;
+}
diff --git a/source/rr/src/config.cpp b/source/rr/src/config.cpp
index 70029239e..a802521fc 100644
--- a/source/rr/src/config.cpp
+++ b/source/rr/src/config.cpp
@@ -599,8 +599,20 @@ int32_t CONFIG_ReadSetup(void)
 
     if (ud.config.scripthandle < 0)
     {
-            ud.config.scripthandle = SCRIPT_Load(g_setupFileName);
-    }
+		if (buildvfs_exists(g_setupFileName))  // JBF 20031211
+			ud.config.scripthandle = SCRIPT_Load(g_setupFileName);
+#if !defined(EDUKE32_TOUCH_DEVICES) && !defined(EDUKE32_STANDALONE)
+		else if (buildvfs_exists(SETUPFILENAME))
+		{
+			int const i = wm_ynbox("Import Configuration Settings",
+				"The configuration file \"%s\" was not found. "
+				"Import configuration data from \"%s\"?",
+				g_setupFileName, SETUPFILENAME);
+			if (i)
+				ud.config.scripthandle = SCRIPT_Load(SETUPFILENAME);
+		}
+#endif
+	}
 
     pathsearchmode = 0;
 
diff --git a/source/rr/src/duke3d.h b/source/rr/src/duke3d.h
index 0afe11139..b3e3bf6d1 100644
--- a/source/rr/src/duke3d.h
+++ b/source/rr/src/duke3d.h
@@ -39,7 +39,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 #include "cache1d.h"
 #include "pragmas.h"
 #include "baselayer.h"
-#include "file_lib.h"
 #include "keyboard.h"
 #include "fx_man.h"
 
diff --git a/source/rr/src/file_lib.cpp b/source/rr/src/file_lib.cpp
deleted file mode 100644
index a8a253857..000000000
--- a/source/rr/src/file_lib.cpp
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * file_lib.c
- * File functions to emulate MACT
- *
- * by Jonathon Fowler
- *
- * Since we weren't given the source for MACT386.LIB so I've had to do some
- * creative interpolation here.
- *
- */
-//-------------------------------------------------------------------------
-/*
-Duke Nukem Copyright (C) 1996, 2003 3D Realms Entertainment
-
-This file is part of Duke Nukem 3D version 1.5 - Atomic Edition
-
-Duke Nukem 3D is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-*/
-//-------------------------------------------------------------------------
-
-#include "compat.h"
-
-#include "file_lib.h"
-#include "cache1d.h"
-#include "baselayer.h"
-
-#define MaxFiles 20
-static char *FileNames[MaxFiles];
-
-int32_t SafeOpen(const char *filename, int32_t mode, int32_t sharemode)
-{
-    int32_t h;
-
-    h = openfrompath(filename, mode, sharemode);
-    if (h < 0)
-    {
-            initprintf("Error opening %s: %s", filename, strerror(errno));
-            return h;
-    }
-
-    if (h < MaxFiles)
-    {
-        Bfree(FileNames[h]);
-        FileNames[h] = (char*)Xmalloc(strlen(filename)+1);
-        strcpy(FileNames[h], filename);
-    }
-
-    return h;
-}
-
-int32_t SafeOpenRead(const char *filename, int32_t filetype)
-{
-    switch (filetype)
-    {
-    case filetype_binary:
-        return SafeOpen(filename, O_RDONLY|O_BINARY, BS_IREAD);
-    case filetype_text:
-        return SafeOpen(filename, O_RDONLY|O_TEXT, BS_IREAD);
-    default:
-        initprintf("SafeOpenRead: Illegal filetype specified");
-        return -1;
-    }
-}
-
-void SafeClose(int32_t handle)
-{
-    if (handle < 0) return;
-    if (close(handle) < 0)
-    {
-        if (handle < MaxFiles)
-            initprintf("Unable to close file %s", FileNames[handle]);
-        else
-            initprintf("Unable to close file");
-        return;
-    }
-
-    if (handle < MaxFiles && FileNames[handle])
-    {
-        DO_FREE_AND_NULL(FileNames[handle]);
-    }
-}
-
-int32_t SafeFileExists(const char *filename)
-{
-    if (!access(filename, F_OK)) return TRUE;
-    return FALSE;
-}
-
-int32_t SafeFileLength(int32_t handle)
-{
-    if (handle < 0) return -1;
-    return _filelength(handle);
-}
-
-void SafeRead(int32_t handle, void *buffer, int32_t count)
-{
-    int32_t b;
-
-    b = read(handle, buffer, count);
-    if (b != count)
-    {
-        close(handle);
-        if (handle < MaxFiles)
-            initprintf("File read failure %s reading %d bytes from file %s.",
-                  strerror(errno), count, FileNames[handle]);
-        else
-            initprintf("File read failure %s reading %d bytes.",
-                  strerror(errno), count);
-        return;
-    }
-}
-
-
diff --git a/source/rr/src/file_lib.h b/source/rr/src/file_lib.h
deleted file mode 100644
index e99911497..000000000
--- a/source/rr/src/file_lib.h
+++ /dev/null
@@ -1,262 +0,0 @@
-//-------------------------------------------------------------------------
-/*
-Copyright (C) 1996, 2003 - 3D Realms Entertainment
-
-This file is part of Duke Nukem 3D version 1.5 - Atomic Edition
-
-Duke Nukem 3D is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-
-See the GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-
-Original Source: 1996 - Todd Replogle
-Prepared for public release: 03/21/2003 - Charlie Wiederhold, 3D Realms
-*/
-//-------------------------------------------------------------------------
-
-#pragma once
-
-#ifndef file_lib_public_
-#define file_lib_public_
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-enum
-   {
-   filetype_binary,
-   filetype_text
-   };
-
-enum
-   {
-   access_read,
-   access_write,
-   access_append
-   };
-
-//==========================================================================
-//
-// SafeOpenWrite - Opens a file for writing, returns handle
-//
-//==========================================================================
-int32_t SafeOpenWrite ( const char * filename, int32_t filetype );
-
-//==========================================================================
-//
-// SafeOpenRead - Opens a file for reading, returns handle
-//
-//==========================================================================
-int32_t SafeOpenRead ( const char * filename, int32_t filetype );
-
-//==========================================================================
-//
-// SafeOpenAppend - Opens a file for appending, returns handle
-//
-//==========================================================================
-int32_t SafeOpenAppend ( const char * filename, int32_t filetype );
-
-//==========================================================================
-//
-// SafeClose - Close a file denoted by the file handle
-//
-//==========================================================================
-void SafeClose ( int32_t handle );
-
-//==========================================================================
-//
-// SafeFileExists - Checks for existence of file
-//
-//==========================================================================
-int32_t SafeFileExists ( const char * filename );
-
-//==========================================================================
-//
-// SafeFileLength - Get length of a file pointed to by handle
-//
-//==========================================================================
-int32_t SafeFileLength ( int32_t handle );
-
-//==========================================================================
-//
-// SafeRead - reads from a handle
-//
-//            handle - handle of file to read from
-//
-//            buffer - pointer of buffer to read into
-//
-//            count  - number of bytes to read
-//
-//==========================================================================
-void SafeRead (int32_t handle, void *buffer, int32_t count);
-
-//==========================================================================
-//
-// SafeWrite - writes to a handle
-//
-//             handle - handle of file to write to
-//
-//             buffer - pointer of buffer to write from
-//
-//             count  - number of bytes to write
-//
-//==========================================================================
-void SafeWrite (int32_t handle, void *buffer, int32_t count);
-
-//==========================================================================
-//
-// LoadFile - Load a file
-//
-//            filename - name of file
-//
-//            bufferptr - pointer to pointer of buffer to read into
-//
-//            returns number of bytes read
-//
-//==========================================================================
-int32_t LoadFile ( const char * filename, void ** bufferptr );
-
-//==========================================================================
-//
-// SaveFile - Save a file
-//
-//            filename - name of file
-//
-//            bufferptr - pointer to buffer to write from
-//
-//            count - number of bytes to write
-//
-//==========================================================================
-void SaveFile ( const char * filename, void * bufferptr, int32_t count );
-
-//==========================================================================
-//
-// GetPathFromEnvironment - Add a pathname described in an environment
-//                          variable to a standard filename.
-//
-//                          fullname - final string containing entire path
-//
-//                          envname - string naming enivronment variable
-//
-//                          filename - standard filename
-//
-//==========================================================================
-void GetPathFromEnvironment( char *fullname, const char *envname, const char *filename );
-
-//==========================================================================
-//
-// DefaultExtension - Add a default extension to a path
-//
-//                    path - a path
-//
-//                    extension - default extension should include '.'
-//
-//==========================================================================
-void DefaultExtension (char *path, const char *extension);
-
-//==========================================================================
-//
-// DefaultPath - Add the default path to a filename if it doesn't have one
-//
-//               path - filename
-//
-//               extension - default path
-//
-//==========================================================================
-void DefaultPath (char *path, const char *basepath);
-
-//==========================================================================
-//
-// ExtractFileBase - Extract the base filename from a path
-//
-//                   path - the path
-//
-//                   dest - where the file base name will be placed
-//
-//==========================================================================
-void ExtractFileBase (char *path, char *dest);
-
-//==========================================================================
-//
-// GetExtension - Extract the extension from a name
-//                returns true if an extension is found
-//                returns false otherwise
-//
-//==========================================================================
-int32_t GetExtension( char *filename, char *extension );
-
-//==========================================================================
-//
-// SetExtension - Sets the extension from a name.  Assumes that enough
-// 					space is left at the end of the string to hold an extension.
-//
-//==========================================================================
-void SetExtension( char *filename, const char *extension );
-
-#ifdef __MSDOS__
-//******************************************************************************
-//
-// GetPath
-//
-// Purpose
-//    To parse the directory entered by the user to make the directory.
-//
-// Parms
-//    Path - the path to be parsed.
-//
-// Returns
-//    Pointer to next path
-//
-//******************************************************************************
-char * GetPath (char * path, char *dir);
-
-//******************************************************************************
-//
-// ChangeDirectory ()
-//
-// Purpose
-//    To change to a directory.  Checks for drive changes.
-//
-// Parms
-//    path - The path to change to.
-//
-// Returns
-//    TRUE  - If successful.
-//    FALSE - If unsuccessful.
-//
-//******************************************************************************
-int32_t ChangeDirectory (char * path);
-
-//******************************************************************************
-//
-// ChangeDrive ()
-//
-// Purpose
-//    To change drives.
-//
-// Parms
-//    drive - The drive to change to.
-//
-// Returns
-//    TRUE  - If drive change successful.
-//    FALSE - If drive change unsuccessful.
-//
-//******************************************************************************
-int32_t ChangeDrive (char *drive);
-
-#endif
-
-#ifdef __cplusplus
-}
-#endif
-#endif