DEADSOFTWARE

initial commit:
authorFGSFDSFGS <derp.primus@gmail.com>
Tue, 5 Apr 2016 20:59:14 +0000 (23:59 +0300)
committerFGSFDSFGS <derp.primus@gmail.com>
Tue, 5 Apr 2016 20:59:14 +0000 (23:59 +0300)
67 files changed:
.gitattributes [new file with mode: 0644]
.gitignore [new file with mode: 0644]
bin/.gitignore [new file with mode: 0644]
build.bat [new file with mode: 0644]
clean.bat [new file with mode: 0644]
src/engine/e_fixedbuffer.pas [new file with mode: 0644]
src/engine/e_graphics.pas [new file with mode: 0644]
src/engine/e_input.pas [new file with mode: 0644]
src/engine/e_log.pas [new file with mode: 0644]
src/engine/e_sound.pas [new file with mode: 0644]
src/engine/e_textures.pas [new file with mode: 0644]
src/game/CustomRes.rc [new file with mode: 0644]
src/game/CustomRes.res [new file with mode: 0644]
src/game/Doom2DF.dpr [new file with mode: 0644]
src/game/Doom2DF.rc [new file with mode: 0644]
src/game/Doom2DF.res [new file with mode: 0644]
src/game/Icon.ico [new file with mode: 0644]
src/game/MakeRes.bat [new file with mode: 0644]
src/game/g_basic.pas [new file with mode: 0644]
src/game/g_console.pas [new file with mode: 0644]
src/game/g_game.pas [new file with mode: 0644]
src/game/g_gfx.pas [new file with mode: 0644]
src/game/g_gui.pas [new file with mode: 0644]
src/game/g_items.pas [new file with mode: 0644]
src/game/g_language.pas [new file with mode: 0644]
src/game/g_main.pas [new file with mode: 0644]
src/game/g_map.pas [new file with mode: 0644]
src/game/g_menu.pas [new file with mode: 0644]
src/game/g_monsters.pas [new file with mode: 0644]
src/game/g_net.pas [new file with mode: 0644]
src/game/g_nethandler.pas [new file with mode: 0644]
src/game/g_netmaster.pas [new file with mode: 0644]
src/game/g_netmsg.pas [new file with mode: 0644]
src/game/g_options.pas [new file with mode: 0644]
src/game/g_panel.pas [new file with mode: 0644]
src/game/g_phys.pas [new file with mode: 0644]
src/game/g_player.pas [new file with mode: 0644]
src/game/g_playermodel.pas [new file with mode: 0644]
src/game/g_res_downloader.pas [new file with mode: 0644]
src/game/g_saveload.pas [new file with mode: 0644]
src/game/g_sound.pas [new file with mode: 0644]
src/game/g_textures.pas [new file with mode: 0644]
src/game/g_triggers.pas [new file with mode: 0644]
src/game/g_weapons.pas [new file with mode: 0644]
src/game/g_window.pas [new file with mode: 0644]
src/lib/enet/enet.pp [new file with mode: 0644]
src/lib/enet/enetcallbacks.pp [new file with mode: 0644]
src/lib/enet/enetlist.pp [new file with mode: 0644]
src/lib/enet/enetplatform.pp [new file with mode: 0644]
src/lib/enet/enetprotocol.pp [new file with mode: 0644]
src/lib/enet/enettime.pp [new file with mode: 0644]
src/lib/enet/enettypes.pp [new file with mode: 0644]
src/lib/fmod/fmod.inc [new file with mode: 0644]
src/lib/fmod/fmod.pas [new file with mode: 0644]
src/lib/fmod/fmoderrors.pas [new file with mode: 0644]
src/lib/fmod/fmodpresets.pas [new file with mode: 0644]
src/lib/fmod/fmodtypes.pas [new file with mode: 0644]
src/shared/BinEditor.pas [new file with mode: 0644]
src/shared/CONFIG.pas [new file with mode: 0644]
src/shared/CONFIGSIMPLE.pas [new file with mode: 0644]
src/shared/MAPDEF.pas [new file with mode: 0644]
src/shared/MAPREADER.pas [new file with mode: 0644]
src/shared/MAPSTRUCT.pas [new file with mode: 0644]
src/shared/MAPWRITER.pas [new file with mode: 0644]
src/shared/WADEDITOR.pas [new file with mode: 0644]
src/shared/WADSTRUCT.pas [new file with mode: 0644]
tmp/.gitignore [new file with mode: 0644]

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..bdb0cab
--- /dev/null
@@ -0,0 +1,17 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs     diff=csharp
+
+# Standard to msysgit
+*.doc   diff=astextplain
+*.DOC   diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot  diff=astextplain
+*.DOT  diff=astextplain
+*.pdf  diff=astextplain
+*.PDF   diff=astextplain
+*.rtf   diff=astextplain
+*.RTF   diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..2689e70
--- /dev/null
@@ -0,0 +1,23 @@
+# Precompiled and Delphi-specific files
+# without *.res
+*.exe
+*.obj
+*.dcu
+*.cbk
+*.dof
+*.ddp
+*.o
+*.or
+
+# Commonly used temporary files
+~*
+*.~*
+*.tmp
+*.bak
+
+# Windows-specific
+AppPackages/
+$RECYCLE.BIN/
+Thumbs.db
+ehthumbs.db
+Desktop.ini
diff --git a/bin/.gitignore b/bin/.gitignore
new file mode 100644 (file)
index 0000000..c96a04f
--- /dev/null
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/build.bat b/build.bat
new file mode 100644 (file)
index 0000000..4c682e8
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,5 @@
+@echo off
+cd "./src/game"
+fpc -MDELPHI -O2 -FE../../bin -FU../../tmp Doom2DF.dpr
+cd ".."
+pause
\ No newline at end of file
diff --git a/clean.bat b/clean.bat
new file mode 100644 (file)
index 0000000..310c2b0
--- /dev/null
+++ b/clean.bat
@@ -0,0 +1,13 @@
+@echo off
+del /S "tmp\*.res"
+del /S *.~*
+del /S *.dcu
+del /S *.ddp
+del /S *.o
+del /S *.ppu
+del /S *.a
+del /S *.or
+del "src\game\CustomRes.obj"
+del "src\game\*.exe"
+
+pause
\ No newline at end of file
diff --git a/src/engine/e_fixedbuffer.pas b/src/engine/e_fixedbuffer.pas
new file mode 100644 (file)
index 0000000..63570f7
--- /dev/null
@@ -0,0 +1,296 @@
+unit e_fixedbuffer;
+
+interface
+
+uses md5;
+
+const
+  BUF_SIZE = 65536;
+
+type
+  TBuffer = record
+    Data: array [0..BUF_SIZE] of Byte; // îäèí áàéò ñâåðõó íà âñÿêèé ñëó÷àé
+    ReadPos: Cardinal;
+    WritePos: Cardinal;
+    Len: Cardinal;
+  end;
+  pTBuffer = ^TBuffer;
+
+var
+  RawPos: Cardinal = 0;
+
+procedure e_Buffer_Clear(B: pTBuffer);
+
+
+procedure e_Buffer_Write_Generic(B: pTBuffer; var V; N: Cardinal);
+procedure e_Buffer_Read_Generic(B: pTBuffer; var V; N: Cardinal);
+
+
+procedure e_Buffer_Write(B: pTBuffer; V: Char); overload;
+
+procedure e_Buffer_Write(B: pTBuffer; V: Byte); overload;
+procedure e_Buffer_Write(B: pTBuffer; V: Word); overload;
+procedure e_Buffer_Write(B: pTBuffer; V: LongWord); overload;
+
+procedure e_Buffer_Write(B: pTBuffer; V: ShortInt); overload;
+procedure e_Buffer_Write(B: pTBuffer; V: SmallInt); overload;
+procedure e_Buffer_Write(B: pTBuffer; V: LongInt); overload;
+
+procedure e_Buffer_Write(B: pTBuffer; V: string); overload;
+
+procedure e_Buffer_Write(B: pTBuffer; V: TMD5Digest); overload;
+
+
+function  e_Buffer_Read_Char(B: pTBuffer): Char;
+
+function  e_Buffer_Read_Byte(B: pTBuffer): Byte;
+function  e_Buffer_Read_Word(B: pTBuffer): Word;
+function  e_Buffer_Read_LongWord(B: pTBuffer): LongWord;
+
+function  e_Buffer_Read_ShortInt(B: pTBuffer): ShortInt;
+function  e_Buffer_Read_SmallInt(B: pTBuffer): SmallInt;
+function  e_Buffer_Read_LongInt(B: pTBuffer): LongInt;
+
+function  e_Buffer_Read_String(B: pTBuffer): string;
+
+function  e_Buffer_Read_MD5(B: pTBuffer): TMD5Digest;
+
+
+procedure e_Raw_Read_Generic(P: Pointer; var V; N: Cardinal);
+
+function  e_Raw_Read_Char(P: Pointer): Char;
+
+function  e_Raw_Read_Byte(P: Pointer): Byte;
+function  e_Raw_Read_Word(P: Pointer): Word;
+function  e_Raw_Read_LongWord(P: Pointer): LongWord;
+
+function  e_Raw_Read_ShortInt(P: Pointer): ShortInt;
+function  e_Raw_Read_SmallInt(P: Pointer): SmallInt;
+function  e_Raw_Read_LongInt(P: Pointer): LongInt;
+
+function  e_Raw_Read_String(P: Pointer): string;
+
+function  e_Raw_Read_MD5(P: Pointer): TMD5Digest;
+
+procedure e_Raw_Seek(I: Cardinal);
+
+implementation
+
+uses SysUtils, BinEditor;
+
+procedure e_Buffer_Clear(B: pTBuffer);
+begin
+  B^.WritePos := 0;
+  B^.ReadPos := 0;
+  B^.Len := 0;
+end;
+
+
+procedure e_Buffer_Write_Generic(B: pTBuffer; var V; N: Cardinal);
+begin
+  if (B^.WritePos + N >= BUF_SIZE) then Exit;
+  if (B^.WritePos + N > B^.Len) then
+    B^.Len := B^.WritePos + N + 1;
+
+  CopyMemory(Pointer(Cardinal(Addr(B^.Data)) + B^.WritePos),
+             @V, N);
+
+  B^.WritePos := B^.WritePos + N;
+end;
+procedure e_Buffer_Read_Generic(B: pTBuffer; var V; N: Cardinal);
+begin
+  if (B^.ReadPos + N >= BUF_SIZE) then Exit;
+
+  CopyMemory(@V, Pointer(Cardinal(Addr(B^.Data)) + B^.ReadPos), N);
+
+  B^.ReadPos := B^.ReadPos + N;
+end;
+
+
+procedure e_Buffer_Write(B: pTBuffer; V: Char); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 1);
+end;
+
+procedure e_Buffer_Write(B: pTBuffer; V: Byte); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 1);
+end;
+procedure e_Buffer_Write(B: pTBuffer; V: Word); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 2);
+end;
+procedure e_Buffer_Write(B: pTBuffer; V: LongWord); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 4);
+end;
+
+procedure e_Buffer_Write(B: pTBuffer; V: ShortInt); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 1);
+end;
+procedure e_Buffer_Write(B: pTBuffer; V: SmallInt); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 2);
+end;
+procedure e_Buffer_Write(B: pTBuffer; V: LongInt); overload;
+begin
+  e_Buffer_Write_Generic(B, V, 4);
+end;
+
+procedure e_Buffer_Write(B: pTBuffer; V: string); overload;
+var
+  Len: Byte;
+  P: Cardinal;
+begin
+  Len := Length(V);
+  e_Buffer_Write_Generic(B, Len, 1);
+
+  if (Len = 0) then Exit;
+
+  P := B^.WritePos + Len;
+  if (P >= BUF_SIZE) then
+  begin
+    Len := BUF_SIZE - B^.WritePos;
+    P := BUF_SIZE;
+  end;
+
+  if (P > B^.Len) then B^.Len := P;
+
+  CopyMemory(Pointer(Cardinal(Addr(B^.Data)) + B^.WritePos),
+             @V[1], Len);
+
+  B^.WritePos := P;
+end;
+
+procedure e_Buffer_Write(B: pTBuffer; V: TMD5Digest); overload;
+var
+  I: Integer;
+begin
+  for I := 0 to 15 do
+    e_Buffer_Write(B, V[I]);
+end;
+
+
+function e_Buffer_Read_Char(B: pTBuffer): Char;
+begin
+  e_Buffer_Read_Generic(B, Result, 1);
+end;
+
+function e_Buffer_Read_Byte(B: pTBuffer): Byte;
+begin
+  e_Buffer_Read_Generic(B, Result, 1);
+end;
+function e_Buffer_Read_Word(B: pTBuffer): Word;
+begin
+  e_Buffer_Read_Generic(B, Result, 2);
+end;
+function e_Buffer_Read_LongWord(B: pTBuffer): LongWord;
+begin
+  e_Buffer_Read_Generic(B, Result, 4);
+end;
+
+function e_Buffer_Read_ShortInt(B: pTBuffer): ShortInt;
+begin
+  e_Buffer_Read_Generic(B, Result, 1);
+end;
+function e_Buffer_Read_SmallInt(B: pTBuffer): SmallInt;
+begin
+  e_Buffer_Read_Generic(B, Result, 2);
+end;
+function e_Buffer_Read_LongInt(B: pTBuffer): LongInt;
+begin
+  e_Buffer_Read_Generic(B, Result, 4);
+end;
+
+function e_Buffer_Read_String(B: pTBuffer): string;
+var
+  Len: Byte;
+begin
+  Len := e_Buffer_Read_Byte(B);
+  Result := '';
+  if Len = 0 then Exit;
+
+  if B^.ReadPos + Len > B^.Len then
+    Len := B^.Len - B^.ReadPos;
+
+  SetLength(Result, Len);
+  CopyMemory(@Result[1], Pointer(Cardinal(Addr(B^.Data)) + B^.ReadPos), Len);
+
+  B^.ReadPos := B^.ReadPos + Len;
+end;
+
+function e_Buffer_Read_MD5(B: pTBuffer): TMD5Digest;
+var
+  I: Integer;
+begin
+  for I := 0 to 15 do
+    Result[I] := e_Buffer_Read_Byte(B);
+end;
+
+procedure e_Raw_Read_Generic(P: Pointer; var V; N: Cardinal);
+begin
+  CopyMemory(@V, Pointer(Cardinal(P) + RawPos), N);
+
+  RawPos := RawPos + N;
+end;
+
+function e_Raw_Read_Char(P: Pointer): Char;
+begin
+  e_Raw_Read_Generic(P, Result, 1);
+end;
+
+function e_Raw_Read_Byte(P: Pointer): Byte;
+begin
+  e_Raw_Read_Generic(P, Result, 1);
+end;
+function e_Raw_Read_Word(P: Pointer): Word;
+begin
+  e_Raw_Read_Generic(P, Result, 2);
+end;
+function e_Raw_Read_LongWord(P: Pointer): LongWord;
+begin
+  e_Raw_Read_Generic(P, Result, 4);
+end;
+
+function e_Raw_Read_ShortInt(P: Pointer): ShortInt;
+begin
+  e_Raw_Read_Generic(P, Result, 1);
+end;
+function e_Raw_Read_SmallInt(P: Pointer): SmallInt;
+begin
+  e_Raw_Read_Generic(P, Result, 2);
+end;
+function e_Raw_Read_LongInt(P: Pointer): LongInt;
+begin
+  e_Raw_Read_Generic(P, Result, 4);
+end;
+
+function e_Raw_Read_String(P: Pointer): string;
+var
+  Len: Byte;
+begin
+  Len := e_Raw_Read_Byte(P);
+  Result := '';
+  if Len = 0 then Exit;
+
+  SetLength(Result, Len);
+  CopyMemory(@Result[1], Pointer(Cardinal(P) + RawPos), Len);
+
+  RawPos := RawPos + Len;
+end;
+
+function e_Raw_Read_MD5(P: Pointer): TMD5Digest;
+var
+  I: Integer;
+begin
+  for I := 0 to 15 do
+    Result[I] := e_Raw_Read_Byte(P);
+end;
+
+procedure e_Raw_Seek(I: Cardinal);
+begin
+  RawPos := I;
+end;
+
+end.
diff --git a/src/engine/e_graphics.pas b/src/engine/e_graphics.pas
new file mode 100644 (file)
index 0000000..49bdaa6
--- /dev/null
@@ -0,0 +1,1762 @@
+unit e_graphics;
+
+interface
+
+uses
+  SysUtils, Math, e_log, e_textures, SDL, GL, GLExt, MAPDEF;
+
+type
+  TMirrorType=(M_NONE, M_HORIZONTAL, M_VERTICAL);
+  TBlending=(B_NONE, B_BLEND, B_FILTER, B_INVERT);
+
+  TPoint2i = record
+    X, Y: Integer;
+  end;
+
+  TPoint = MAPDEF.TPoint; // TODO: create an utiltypes.pas or something
+                          //       for other types like rect as well
+
+  TPoint2f = record
+    X, Y: Double;
+  end;
+
+  TRect = record
+    Left, Top, Right, Bottom: Integer;
+  end;
+
+  TRectWH = record
+   X, Y: Integer;
+   Width, Height: Word;
+  end;
+
+  TRGB = packed record
+   R, G, B: Byte;
+  end;
+
+  PPoint = ^TPoint;
+  PPoint2f = ^TPoint2f;
+  PRect = ^TRect;
+  PRectWH = ^TRectWH;
+
+
+//------------------------------------------------------------------
+// ïðîòîòèïû ôóíêöèé
+//------------------------------------------------------------------
+procedure e_InitGL();
+procedure e_SetViewPort(X, Y, Width, Height: Word);
+procedure e_ResizeWindow(Width, Height: Integer);
+
+procedure e_Draw(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                 Blending: Boolean; Mirror: TMirrorType = M_NONE);
+procedure e_DrawAdv(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                    Blending: Boolean; Angle: Single; RC: PPoint; Mirror: TMirrorType = M_NONE);
+procedure e_DrawSize(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                     Blending: Boolean; Width, Height: Word; Mirror: TMirrorType = M_NONE);
+procedure e_DrawSizeMirror(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                           Blending: Boolean; Width, Height: Word; Mirror: TMirrorType = M_NONE);
+procedure e_DrawFill(ID: DWORD; X, Y: Integer; XCount, YCount: Word; Alpha: Integer;
+                     AlphaChannel: Boolean; Blending: Boolean);
+procedure e_DrawPoint(Size: Byte; X, Y: Integer; Red, Green, Blue: Byte);
+procedure e_DrawLine(Width: Byte; X1, Y1, X2, Y2: Integer; Red, Green, Blue: Byte; Alpha: Byte = 0);
+procedure e_DrawQuad(X1, Y1, X2, Y2: Integer; Red, Green, Blue: Byte; Alpha: Byte = 0);
+procedure e_DrawFillQuad(X1, Y1, X2, Y2: Integer; Red, Green, Blue, Alpha: Byte;
+                         Blending: TBlending = B_NONE);
+
+function e_CreateTexture(FileName: string; var ID: DWORD): Boolean;
+function e_CreateTextureEx(FileName: string; var ID: DWORD; fX, fY, fWidth, fHeight: Word): Boolean;
+function e_CreateTextureMem(pData: Pointer; var ID: DWORD): Boolean;
+function e_CreateTextureMemEx(pData: Pointer; var ID: DWORD; fX, fY, fWidth, fHeight: Word): Boolean;
+procedure e_GetTextureSize(ID: DWORD; Width, Height: PWord);
+function e_GetTextureSize2(ID: DWORD): TRectWH;
+procedure e_DeleteTexture(ID: DWORD);
+procedure e_RemoveAllTextures();
+
+// CharFont
+function e_CharFont_Create(sp: ShortInt=0): DWORD;
+procedure e_CharFont_AddChar(FontID: DWORD; Texture: Integer; c: Char; w: Byte);
+procedure e_CharFont_Print(FontID: DWORD; X, Y: Integer; Text: string);
+procedure e_CharFont_PrintEx(FontID: DWORD; X, Y: Integer; Text: string;
+                             Color: TRGB; Scale: Single = 1.0);
+procedure e_CharFont_PrintFmt(FontID: DWORD; X, Y: Integer; Text: string);
+procedure e_CharFont_GetSize(FontID: DWORD; Text: string; var w, h: Word);
+procedure e_CharFont_GetSizeFmt(FontID: DWORD; Text: string; var w, h: Word);
+function e_CharFont_GetMaxWidth(FontID: DWORD): Word;
+function e_CharFont_GetMaxHeight(FontID: DWORD): Word;
+procedure e_CharFont_Remove(FontID: DWORD);
+procedure e_CharFont_RemoveAll();
+
+// TextureFont
+procedure e_TextureFontBuild(Tex: DWORD; var FontID: DWORD; XCount, YCount: Word;
+                             Space: ShortInt=0);
+procedure e_TextureFontKill(FontID: DWORD);
+procedure e_TextureFontPrint(X, Y: GLint; Text: string; FontID: DWORD);
+procedure e_TextureFontPrintEx(X, Y: GLint; Text: string; FontID: DWORD; Red, Green,
+                               Blue: Byte; Scale: Single; Shadow: Boolean = False);
+procedure e_TextureFontPrintFmt(X, Y: GLint; Text: string; FontID: DWORD; Shadow: Boolean = False);
+procedure e_TextureFontGetSize(ID: DWORD; var CharWidth, CharHeight: Byte);
+procedure e_RemoveAllTextureFont();
+
+procedure e_ReleaseEngine();
+procedure e_BeginRender();
+procedure e_Clear(Mask: TGLbitfield; Red, Green, Blue: Single); overload;
+procedure e_Clear(); overload;
+procedure e_EndRender();
+
+procedure e_SaveGLContext();
+procedure e_RestoreGLContext();
+
+function e_GetGamma(): Byte;
+procedure e_SetGamma(Gamma: Byte);
+
+procedure e_MakeScreenshot(FileName: string; Width, Height: Word);
+
+function _RGB(Red, Green, Blue: Byte): TRGB;
+function _Point(X, Y: Integer): TPoint2i;
+function _Rect(X, Y: Integer; Width, Height: Word): TRectWH;
+function _TRect(L, T, R, B: LongInt): TRect;
+
+
+var
+  e_Colors: TRGB;
+
+implementation
+
+type
+  TTexture = record
+   ID:     DWORD;
+   Width:  Word;
+   Height: Word;
+   Fmt:    Word;
+  end;
+
+  TTextureFont = record
+   Texture:     DWORD;
+   TextureID:   DWORD;
+   Base:        Uint32;
+   CharWidth:   Byte;
+   CharHeight:  Byte;
+   XC, YC, SPC: Word;
+  end;
+
+  TCharFont = record
+   Chars: array[0..255] of
+    record
+     TextureID: Integer;
+     Width: Byte;
+    end;
+   Space: ShortInt;
+   Height: ShortInt;
+   Live: Boolean;
+  end;
+
+  TSavedTexture = record
+    TexID:  DWORD;
+    OldID:  DWORD;
+    Pixels: Pointer;
+  end;
+  
+var
+  e_Textures: array of TTexture = nil;
+  e_TextureFonts: array of TTextureFont = nil;
+  e_CharFonts: array of TCharFont;
+  e_SavedTextures: array of TSavedTexture;
+
+//------------------------------------------------------------------
+// Èíèöèàëèçèðóåò OpenGL
+//------------------------------------------------------------------
+procedure e_InitGL();
+begin
+  glDisable(GL_DEPTH_TEST);
+  glEnable(GL_SCISSOR_TEST);
+  e_Colors.R := 255;
+  e_Colors.G := 255;
+  e_Colors.B := 255;
+  glClearColor(0, 0, 0, 0);
+end;
+
+procedure e_SetViewPort(X, Y, Width, Height: Word);
+var
+  mat: Array [0..15] of GLDouble;
+
+begin
+  glLoadIdentity();
+  glScissor(X, Y, Width, Height);
+  glViewport(X, Y, Width, Height);
+  //gluOrtho2D(0, Width, Height, 0);
+
+  glMatrixMode(GL_PROJECTION);
+
+  mat[ 0] := 2.0 / Width;
+  mat[ 1] := 0.0;
+  mat[ 2] := 0.0;
+  mat[ 3] := 0.0;
+
+  mat[ 4] := 0.0;
+  mat[ 5] := -2.0 / Height;
+  mat[ 6] := 0.0;
+  mat[ 7] := 0.0;
+
+  mat[ 8] := 0.0;
+  mat[ 9] := 0.0;
+  mat[10] := 1.0;
+  mat[11] := 0.0;
+
+  mat[12] := -1.0;
+  mat[13] := 1.0;
+  mat[14] := 0.0;
+  mat[15] := 1.0;
+
+  glLoadMatrixd(@mat[0]);
+
+  glMatrixMode(GL_MODELVIEW);
+  glLoadIdentity();
+end;
+
+//------------------------------------------------------------------
+// Èùåò ñâîáîäíûé ýëåìåíò â ìàññèâå òåêñòóð
+//------------------------------------------------------------------
+function FindTexture(): DWORD;
+var
+  i: integer;
+begin
+ if e_Textures <> nil then
+ for i := 0 to High(e_Textures) do
+  if e_Textures[i].Width = 0 then
+  begin
+   Result := i;
+   Exit;
+  end;
+
+ if e_Textures = nil then
+ begin
+  SetLength(e_Textures, 32);
+  Result := 0;
+ end
+  else
+ begin
+  Result := High(e_Textures) + 1;
+  SetLength(e_Textures, Length(e_Textures) + 32);
+ end;
+end;
+
+//------------------------------------------------------------------
+// Ñîçäàåò òåêñòóðó
+//------------------------------------------------------------------
+function e_CreateTexture(FileName: String; var ID: DWORD): Boolean;
+var
+  find_id: DWORD;
+  fmt:     Word;
+begin
+ Result := False;
+
+ e_WriteLog('Loading texture from '+FileName, MSG_NOTIFY);
+
+ find_id := FindTexture();
+
+ if not LoadTexture(FileName, e_Textures[find_id].ID, e_Textures[find_id].Width,
+                    e_Textures[find_id].Height, @fmt) then Exit;
+
+ ID := find_id;
+ e_Textures[ID].Fmt := fmt;
+
+ Result := True;
+end;
+
+function e_CreateTextureEx(FileName: String; var ID: DWORD; fX, fY, fWidth, fHeight: Word): Boolean;
+var
+  find_id: DWORD;
+  fmt:     Word;
+begin
+ Result := False;
+
+ find_id := FindTexture();
+
+ if not LoadTextureEx(FileName, e_Textures[find_id].ID, fX, fY, fWidth, fHeight, @fmt) then exit;
+
+ e_Textures[find_id].Width := fWidth;
+ e_Textures[find_id].Height := fHeight;
+ e_Textures[find_id].Fmt := fmt;
+
+ ID := find_id;
+
+ Result := True;
+end;
+
+function e_CreateTextureMem(pData: Pointer; var ID: DWORD): Boolean;
+var
+  find_id: DWORD;
+  fmt:     Word;
+begin
+ Result := False;
+
+ find_id := FindTexture;
+
+ if not LoadTextureMem(pData, e_Textures[find_id].ID, e_Textures[find_id].Width,
+                   e_Textures[find_id].Height, @fmt) then exit;
+
+ id := find_id;
+ e_Textures[id].Fmt := fmt;
+
+ Result := True;
+end;
+
+function e_CreateTextureMemEx(pData: Pointer; var ID: DWORD; fX, fY, fWidth, fHeight: Word): Boolean;
+var
+  find_id: DWORD;
+  fmt:     Word;
+begin
+ Result := False;
+
+ find_id := FindTexture();
+
+ if not LoadTextureMemEx(pData, e_Textures[find_id].ID, fX, fY, fWidth, fHeight, @fmt) then exit;
+
+ e_Textures[find_id].Width := fWidth;
+ e_Textures[find_id].Height := fHeight;
+ e_Textures[find_id].Fmt := fmt;
+
+ ID := find_id;
+
+ Result := True;
+end;
+
+procedure e_GetTextureSize(ID: DWORD; Width, Height: PWord);
+begin
+ if Width <> nil then Width^ := e_Textures[ID].Width;
+ if Height <> nil then Height^ := e_Textures[ID].Height;
+end;
+
+function e_GetTextureSize2(ID: DWORD): TRectWH;
+var
+  data: PChar;
+  x, y: Integer;
+  w, h: Word;
+  a: Boolean;
+  lastline: Integer;
+begin
+ w := e_Textures[ID].Width;
+ h := e_Textures[ID].Height;
+ data := GetMemory(w*h*4);
+ glEnable(GL_TEXTURE_2D);
+ glBindTexture(GL_TEXTURE_2D, e_Textures[ID].ID);
+ glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
+
+ Result.Y := 0;
+ Result.X := 0;
+ Result.Width := w;
+ Result.Height := h;
+
+ for y := h-1 downto 0 do
+ begin
+  lastline := y;
+  a := True;
+
+  for x := 1 to w-4 do
+  begin
+   a := Byte((data+y*w*4+x*4+3)^) <> 0;
+   if a then Break;
+  end;
+
+  if a then
+  begin
+   Result.Y := h-lastline;
+   Break;
+  end;
+ end;
+
+ for y := 0 to h-1 do
+ begin
+  lastline := y;
+  a := True;
+
+  for x := 1 to w-4 do
+  begin
+   a := Byte((data+y*w*4+x*4+3)^) <> 0;
+   if a then Break;
+  end;
+
+  if a then
+  begin
+   Result.Height := h-lastline-Result.Y;
+   Break;
+  end;
+ end;
+
+ for x := 0 to w-1 do
+ begin
+  lastline := x;
+  a := True;
+
+  for y := 1 to h-4 do
+  begin
+   a := Byte((data+y*w*4+x*4+3)^) <> 0;
+   if a then Break;
+  end;
+
+  if a then
+  begin
+   Result.X := lastline+1;
+   Break;
+  end;
+ end;
+
+ for x := w-1 downto 0 do
+ begin
+  lastline := x;
+  a := True;
+
+  for y := 1 to h-4 do
+  begin
+   a := Byte((data+y*w*4+x*4+3)^) <> 0;
+   if a then Break;
+  end;
+
+  if a then
+  begin
+   Result.Width := lastline-Result.X+1;
+   Break;
+  end;
+ end;
+
+ FreeMemory(data);
+end;
+
+procedure e_ResizeWindow(Width, Height: Integer);
+begin
+  if Height = 0 then
+    Height := 1;
+  e_SetViewPort(0, 0, Width, Height);
+end;
+
+procedure e_Draw(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                 Blending: Boolean; Mirror: TMirrorType = M_NONE);
+begin
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  if (Alpha > 0) or (AlphaChannel) or (Blending) then
+    glEnable(GL_BLEND)
+  else
+    glDisable(GL_BLEND);
+
+  if (AlphaChannel) or (Alpha > 0) then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  if Alpha > 0 then
+    glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255-Alpha);
+
+  if Blending then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+  glEnable(GL_TEXTURE_2D);
+  glBindTexture(GL_TEXTURE_2D, e_Textures[ID].ID);
+  glBegin(GL_QUADS);
+
+  if Mirror = M_NONE then
+    begin
+      glTexCoord2i(1,  0); glVertex2i(X + e_Textures[id].Width, Y);
+      glTexCoord2i(0,  0); glVertex2i(X,                        Y);
+      glTexCoord2i(0, -1); glVertex2i(X,                        Y + e_Textures[id].Height);
+      glTexCoord2i(1, -1); glVertex2i(X + e_Textures[id].Width, Y + e_Textures[id].Height);
+    end
+  else
+    if Mirror = M_HORIZONTAL then
+      begin
+        glTexCoord2i(1,  0); glVertex2i(X,                        Y);
+        glTexCoord2i(0,  0); glVertex2i(X + e_Textures[id].Width, Y);
+        glTexCoord2i(0, -1); glVertex2i(X + e_Textures[id].Width, Y + e_Textures[id].Height);
+        glTexCoord2i(1, -1); glVertex2i(X,                        Y + e_Textures[id].Height);
+      end
+    else
+      if Mirror = M_VERTICAL then
+      begin
+        glTexCoord2i(1, -1); glVertex2i(X + e_Textures[id].Width, Y);
+        glTexCoord2i(0, -1); glVertex2i(X,                        Y);
+        glTexCoord2i(0,  0); glVertex2i(X,                        Y + e_Textures[id].Height);
+        glTexCoord2i(1,  0); glVertex2i(X + e_Textures[id].Width, Y + e_Textures[id].Height);
+      end;
+
+  glEnd();
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawSize(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                     Blending: Boolean; Width, Height: Word; Mirror: TMirrorType = M_NONE);
+begin
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  if (Alpha > 0) or (AlphaChannel) or (Blending) then
+    glEnable(GL_BLEND)
+  else
+    glDisable(GL_BLEND);
+
+  if (AlphaChannel) or (Alpha > 0) then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  if Alpha > 0 then
+    glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255-Alpha);
+
+  if Blending then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+  glEnable(GL_TEXTURE_2D);
+  glBindTexture(GL_TEXTURE_2D, e_Textures[ID].ID);
+
+  glBegin(GL_QUADS);
+    glTexCoord2i(0, 1); glVertex2i(X,         Y);
+    glTexCoord2i(1, 1); glVertex2i(X + Width, Y);
+    glTexCoord2i(1, 0); glVertex2i(X + Width, Y + Height);
+    glTexCoord2i(0, 0); glVertex2i(X,         Y + Height);
+  glEnd();
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawSizeMirror(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                           Blending: Boolean; Width, Height: Word; Mirror: TMirrorType = M_NONE);
+begin
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  if (Alpha > 0) or (AlphaChannel) or (Blending) then
+    glEnable(GL_BLEND)
+  else
+    glDisable(GL_BLEND);
+
+  if (AlphaChannel) or (Alpha > 0) then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  if Alpha > 0 then
+    glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255-Alpha);
+
+  if Blending then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+  glEnable(GL_TEXTURE_2D);
+  glBindTexture(GL_TEXTURE_2D, e_Textures[ID].ID);
+  glBegin(GL_QUADS);
+
+  if Mirror = M_NONE then
+    begin
+      glTexCoord2i(1,  0); glVertex2i(X + Width, Y);
+      glTexCoord2i(0,  0); glVertex2i(X,         Y);
+      glTexCoord2i(0, -1); glVertex2i(X,         Y + Height);
+      glTexCoord2i(1, -1); glVertex2i(X + Width, Y + Height);
+    end
+  else
+    if Mirror = M_HORIZONTAL then
+      begin
+        glTexCoord2i(1,  0); glVertex2i(X,         Y);
+        glTexCoord2i(0,  0); glVertex2i(X + Width, Y);
+        glTexCoord2i(0, -1); glVertex2i(X + Width, Y + Height);
+        glTexCoord2i(1, -1); glVertex2i(X,         Y + Height);
+      end
+    else
+      if Mirror = M_VERTICAL then
+      begin
+        glTexCoord2i(1, -1); glVertex2i(X + Width, Y);
+        glTexCoord2i(0, -1); glVertex2i(X,         Y);
+        glTexCoord2i(0,  0); glVertex2i(X,         Y + Height);
+        glTexCoord2i(1,  0); glVertex2i(X + Width, Y + Height);
+      end;
+
+  glEnd();
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawFill(ID: DWORD; X, Y: Integer; XCount, YCount: Word; Alpha: Integer;
+                     AlphaChannel: Boolean; Blending: Boolean);
+var
+  X2, Y2: Integer;
+
+begin
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  if (Alpha > 0) or (AlphaChannel) or (Blending) then
+    glEnable(GL_BLEND)
+  else
+    glDisable(GL_BLEND);
+
+  if (AlphaChannel) or (Alpha > 0) then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  if Alpha > 0 then
+    glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255-Alpha);
+
+  if Blending then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+  if XCount = 0 then
+    XCount := 1;
+
+  if YCount = 0 then
+    YCount := 1;
+
+  glEnable(GL_TEXTURE_2D);
+  glBindTexture(GL_TEXTURE_2D, e_Textures[ID].ID);
+
+  X2 := X + e_Textures[ID].Width * XCount;
+  Y2 := Y + e_Textures[ID].Height * YCount;
+
+  glBegin(GL_QUADS);
+    glTexCoord2i(0,      YCount); glVertex2i(X,  Y);
+    glTexCoord2i(XCount, YCount); glVertex2i(X2, Y);
+    glTexCoord2i(XCount, 0);      glVertex2i(X2, Y2);
+    glTexCoord2i(0,      0);      glVertex2i(X,  Y2);
+  glEnd();
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawAdv(ID: DWORD; X, Y: Integer; Alpha: Byte; AlphaChannel: Boolean;
+                    Blending: Boolean; Angle: Single; RC: PPoint; Mirror: TMirrorType = M_NONE);
+begin
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  if (Alpha > 0) or (AlphaChannel) or (Blending) then
+    glEnable(GL_BLEND)
+  else
+    glDisable(GL_BLEND);
+
+  if (AlphaChannel) or (Alpha > 0) then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  if Alpha > 0 then
+    glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255-Alpha);
+
+  if Blending then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+  if (Angle <> 0) and (RC <> nil) then
+  begin
+    glPushMatrix();
+    glTranslatef(X+RC.X, Y+RC.Y, 0);
+    glRotatef(Angle, 0, 0, 1);
+    glTranslatef(-(X+RC.X), -(Y+RC.Y), 0);
+  end;
+
+  glEnable(GL_TEXTURE_2D);
+  glBindTexture(GL_TEXTURE_2D, e_Textures[id].ID);
+  glBegin(GL_QUADS);                           //0-1        1-1
+                                               //00         10
+  if Mirror = M_NONE then
+    begin
+      glTexCoord2i(1,  0); glVertex2i(X + e_Textures[id].Width, Y);
+      glTexCoord2i(0,  0); glVertex2i(X,                        Y);
+      glTexCoord2i(0, -1); glVertex2i(X,                        Y + e_Textures[id].Height);
+      glTexCoord2i(1, -1); glVertex2i(X + e_Textures[id].Width, Y + e_Textures[id].Height);
+    end
+  else
+    if Mirror = M_HORIZONTAL then
+      begin
+        glTexCoord2i(1,  0); glVertex2i(X,                        Y);
+        glTexCoord2i(0,  0); glVertex2i(X + e_Textures[id].Width, Y);
+        glTexCoord2i(0, -1); glVertex2i(X + e_Textures[id].Width, Y + e_Textures[id].Height);
+        glTexCoord2i(1, -1); glVertex2i(X,                        Y + e_Textures[id].Height);
+      end
+    else
+      if Mirror = M_VERTICAL then
+      begin
+        glTexCoord2i(1, -1); glVertex2i(X + e_Textures[id].Width, Y);
+        glTexCoord2i(0, -1); glVertex2i(X,                        Y);
+        glTexCoord2i(0,  0); glVertex2i(X,                        Y + e_Textures[id].Height);
+        glTexCoord2i(1,  0); glVertex2i(X + e_Textures[id].Width, Y + e_Textures[id].Height);
+      end;
+
+  glEnd();
+
+  if Angle <> 0 then
+    glPopMatrix();
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawPoint(Size: Byte; X, Y: Integer; Red, Green, Blue: Byte);
+begin
+  glDisable(GL_TEXTURE_2D);
+  glColor3ub(Red, Green, Blue);
+  glPointSize(Size);
+
+  if (Size = 2) or (Size = 4) then
+    X := X + 1;
+
+  glBegin(GL_POINTS);
+    glVertex2f(X+0.3, Y+1.0);
+  glEnd();
+
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+end;
+
+procedure e_LineCorrection(var X1, Y1, X2, Y2: Integer);
+begin
+  // Make lines only top-left/bottom-right and top-right/bottom-left
+  if Y2 < Y1 then
+  begin
+    X1 := X1 xor X2;
+    X2 := X1 xor X2;
+    X1 := X1 xor X2;
+
+    Y1 := Y1 xor Y2;
+    Y2 := Y1 xor Y2;
+    Y1 := Y1 xor Y2;
+  end;
+
+  // Pixel-perfect hack
+  if X1 < X2 then
+    Inc(X2)
+  else
+    Inc(X1);
+  Inc(Y2);
+end;
+
+procedure e_DrawQuad(X1, Y1, X2, Y2: Integer; Red, Green, Blue: Byte; Alpha: Byte = 0);
+var
+  nX1, nY1, nX2, nY2: Integer;
+begin
+  // Only top-left/bottom-right quad
+  if X1 > X2 then
+  begin
+    X1 := X1 xor X2;
+    X2 := X1 xor X2;
+    X1 := X1 xor X2;
+  end;
+  if Y1 > Y2 then
+  begin
+    Y1 := Y1 xor Y2;
+    Y2 := Y1 xor Y2;
+    Y1 := Y1 xor Y2;
+  end;
+
+  if Alpha > 0 then
+  begin
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+  end else
+    glDisable(GL_BLEND);
+
+  glDisable(GL_TEXTURE_2D);
+  glColor4ub(Red, Green, Blue, 255-Alpha);
+  glLineWidth(1);
+
+  glBegin(GL_LINES);
+    nX1 := X1; nY1 := Y1;
+    nX2 := X2; nY2 := Y1;
+    e_LineCorrection(nX1, nY1, nX2, nY2); // Pixel-perfect lines
+    glVertex2i(nX1, nY1);
+    glVertex2i(nX2, nY2);
+
+    nX1 := X2; nY1 := Y1;
+    nX2 := X2; nY2 := Y2;
+    e_LineCorrection(nX1, nY1, nX2, nY2);
+    glVertex2i(nX1, nY1);
+    glVertex2i(nX2, nY2);
+
+    nX1 := X2; nY1 := Y2;
+    nX2 := X1; nY2 := Y2;
+    e_LineCorrection(nX1, nY1, nX2, nY2);
+    glVertex2i(nX1, nY1);
+    glVertex2i(nX2, nY2);
+
+    nX1 := X1; nY1 := Y2;
+    nX2 := X1; nY2 := Y1;
+    e_LineCorrection(nX1, nY1, nX2, nY2);
+    glVertex2i(nX1, nY1);
+    glVertex2i(nX2, nY2);
+  glEnd();
+
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawFillQuad(X1, Y1, X2, Y2: Integer; Red, Green, Blue, Alpha: Byte;
+                         Blending: TBlending = B_NONE);
+begin
+  if (Alpha > 0) or (Blending <> B_NONE) then
+    glEnable(GL_BLEND)
+  else
+    glDisable(GL_BLEND);
+
+  if Blending = B_BLEND then
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE)
+  else
+    if Blending = B_FILTER then
+      glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR)
+    else
+      if Blending = B_INVERT then
+        glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO)
+      else
+        if Alpha > 0 then
+          glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+  glDisable(GL_TEXTURE_2D);
+  glColor4ub(Red, Green, Blue, 255-Alpha);
+
+  X2 := X2 + 1;
+  Y2 := Y2 + 1;
+
+  glBegin(GL_QUADS);
+    glVertex2i(X1, Y1);
+    glVertex2i(X2, Y1);
+    glVertex2i(X2, Y2);
+    glVertex2i(X1, Y2);
+  glEnd();
+
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  glDisable(GL_BLEND);
+end;
+
+procedure e_DrawLine(Width: Byte; X1, Y1, X2, Y2: Integer; Red, Green, Blue: Byte; Alpha: Byte = 0);
+begin
+  // Pixel-perfect lines
+  if Width = 1 then
+    e_LineCorrection(X1, Y1, X2, Y2);
+
+  if Alpha > 0 then
+  begin
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+  end else
+    glDisable(GL_BLEND);
+
+  glDisable(GL_TEXTURE_2D);
+  glColor4ub(Red, Green, Blue, 255-Alpha);
+  glLineWidth(Width);
+
+  glBegin(GL_LINES);
+    glVertex2i(X1, Y1);
+    glVertex2i(X2, Y2);
+  glEnd();
+
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  glDisable(GL_BLEND);
+end;
+
+//------------------------------------------------------------------
+// Óäàëÿåò òåêñòóðó èç ìàññèâà
+//------------------------------------------------------------------
+procedure e_DeleteTexture(ID: DWORD);
+begin
+  glDeleteTextures(1, @e_Textures[ID].ID);
+  e_Textures[ID].ID := 0;
+  e_Textures[ID].Width := 0;
+  e_Textures[ID].Height := 0;
+end;
+
+//------------------------------------------------------------------
+// Óäàëÿåò âñå òåêñòóðû
+//------------------------------------------------------------------
+procedure e_RemoveAllTextures();
+var
+  i: integer;
+begin
+ if e_Textures = nil then Exit;
+
+ for i := 0 to High(e_Textures) do
+  if e_Textures[i].Width <> 0 then e_DeleteTexture(i);
+ e_Textures := nil;
+end;
+
+//------------------------------------------------------------------
+// Óäàëÿåò äâèæîê
+//------------------------------------------------------------------
+procedure e_ReleaseEngine();
+begin
+ e_RemoveAllTextures;
+ e_RemoveAllTextureFont;
+end;
+
+procedure e_BeginRender();
+begin
+  glEnable(GL_ALPHA_TEST);
+  glAlphaFunc(GL_GREATER, 0.0);
+end;
+
+procedure e_Clear(Mask: TGLbitfield; Red, Green, Blue: Single); overload;
+begin
+ glClearColor(Red, Green, Blue, 0);
+ glClear(Mask);
+end;
+
+procedure e_Clear(); overload;
+begin
+ glClearColor(0, 0, 0, 0);
+ glClear(GL_COLOR_BUFFER_BIT);
+end;
+
+procedure e_EndRender();
+begin
+  glPopMatrix();
+end;
+
+procedure e_MakeScreenshot(FileName: String; Width, Height: Word);
+begin
+end;
+
+{type
+  aRGB  = Array [0..1] of TRGB;
+  PaRGB = ^aRGB;
+
+  TByteArray = Array [0..1] of Byte;
+  PByteArray = ^TByteArray;
+
+var
+  FILEHEADER: BITMAPFILEHEADER;
+  INFOHEADER: BITMAPINFOHEADER;
+  pixels: PByteArray;
+  tmp:    Byte;
+  i:      Integer;
+  F:      File of Byte;
+
+begin
+  if (Width mod 4) > 0 then
+    Width := Width + 4 - (Width mod 4);
+
+  GetMem(pixels, Width*Height*3);
+  glReadPixels(0, 0, Width, Height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
+
+  for i := 0 to Width * Height - 1 do
+    with PaRGB(pixels)[i] do
+    begin
+      tmp := R;
+      R := B;
+      B := tmp;
+    end;
+
+  with FILEHEADER do
+  begin
+    bfType := $4D42; // "BM"
+    bfSize := Width*Height*3 + SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER);
+    bfReserved1 := 0;
+    bfReserved2 := 0;
+    bfOffBits := SizeOf(BITMAPFILEHEADER) + SizeOf(BITMAPINFOHEADER);
+  end;
+
+  with INFOHEADER do
+  begin
+    biSize := SizeOf(BITMAPINFOHEADER);
+    biWidth := Width;
+    biHeight := Height;
+    biPlanes := 1;
+    biBitCount := 24;
+    biCompression := 0;
+    biSizeImage := Width*Height*3;
+    biXPelsPerMeter := 0;
+    biYPelsPerMeter := 0;
+    biClrUsed := 0;
+    biClrImportant := 0;
+  end;
+
+  AssignFile(F, FileName);
+  Rewrite(F);
+
+  BlockWrite(F, FILEHEADER, SizeOf(FILEHEADER));
+  BlockWrite(F, INFOHEADER, SizeOf(INFOHEADER));
+  BlockWrite(F, pixels[0], Width*Height*3);
+
+  CloseFile(F);
+
+  FreeMem(pixels);
+end;}
+
+function e_GetGamma(): Byte;
+var
+  ramp: array [0..256*3-1] of Word;
+  rgb: array [0..2] of Double;
+  sum: double;
+  count: integer;
+  min: integer;
+  max: integer;
+  A, B: double;
+  i, j: integer;
+begin
+ rgb[0] := 1.0;
+ rgb[1] := 1.0;
+ rgb[2] := 1.0;
+
+ SDL_GetGammaRamp(@ramp[0], @ramp[256], @ramp[512]);
+
+ for i := 0 to 2 do
+ begin
+  sum := 0;
+  count := 0;
+  min := 256 * i;
+  max := min + 256;
+
+  for j := min to max - 1 do
+  if ramp[j] > 0 then
+  begin
+   B := (j mod 256)/256;
+   A := ramp[j]/65536;
+   sum := sum + ln(A)/ln(B);
+   inc(count);
+  end;
+  rgb[i] := sum / count;
+ end;
+
+ Result := 100 - Trunc(((rgb[0] + rgb[1] + rgb[2])/3 - 0.23) * 100/(2.7 - 0.23));
+end;
+
+procedure e_SetGamma(Gamma: Byte);
+var
+  ramp: array [0..256*3-1] of Word;
+  i: integer;
+  r: double;
+  g: double;
+begin
+ g := (100 - Gamma)*(2.7 - 0.23)/100 + 0.23;
+
+ for i := 0 to 255 do
+ begin
+  r := Exp(g * ln(i/256))*65536;
+  if r < 0 then r := 0
+   else if r > 65535 then r := 65535;
+  ramp[i] := trunc(r);
+  ramp[i + 256] := trunc(r);
+  ramp[i + 512] := trunc(r);
+ end;
+
+ SDL_SetGammaRamp(@ramp[0], @ramp[256], @ramp[512]);
+end;
+
+function e_CharFont_Create(sp: ShortInt=0): DWORD;
+var
+  i, id: DWORD;
+begin
+ e_WriteLog('Creating CharFont...', MSG_NOTIFY);
+
+ id := DWORD(-1);
+
+ if e_CharFonts <> nil then
+ for i := 0 to High(e_CharFonts) do
+  if not e_CharFonts[i].Live then
+  begin
+   id := i;
+   Break;
+  end;
+
+ if id = DWORD(-1) then
+ begin
+  SetLength(e_CharFonts, Length(e_CharFonts) + 1);
+  id := High(e_CharFonts);
+ end;
+
+ with e_CharFonts[id] do
+ begin
+  for i := 0 to High(Chars) do
+   with Chars[i] do
+   begin
+    TextureID := -1;
+    Width := 0;
+   end;
+
+  Space := sp;
+  Live := True;
+ end;
+
+ Result := id;
+end;
+
+procedure e_CharFont_AddChar(FontID: DWORD; Texture: Integer; c: Char; w: Byte);
+begin
+ with e_CharFonts[FontID].Chars[Ord(c)] do
+ begin
+  TextureID := Texture;
+  Width := w;
+ end;
+end;
+
+procedure e_CharFont_Print(FontID: DWORD; X, Y: Integer; Text: string);
+var
+  a: Integer;
+begin
+ if Text = '' then Exit;
+ if e_CharFonts = nil then Exit;
+ if Integer(FontID) > High(e_CharFonts) then Exit;
+
+ with e_CharFonts[FontID] do
+ begin
+  for a := 1 to Length(Text) do
+   with Chars[Ord(Text[a])] do
+   if TextureID <> -1 then
+   begin
+    e_Draw(TextureID, X, Y, 0, True, False);
+    X := X+Width+IfThen(a = Length(Text), 0, Space);
+   end;
+ end;
+end;
+
+procedure e_CharFont_PrintEx(FontID: DWORD; X, Y: Integer; Text: string;
+                             Color: TRGB; Scale: Single = 1.0);
+var
+  a: Integer;
+  c: TRGB;
+begin
+ if Text = '' then Exit;
+ if e_CharFonts = nil then Exit;
+ if Integer(FontID) > High(e_CharFonts) then Exit;
+
+ with e_CharFonts[FontID] do
+ begin
+  for a := 1 to Length(Text) do
+   with Chars[Ord(Text[a])] do
+   if TextureID <> -1 then
+   begin
+    if Scale <> 1.0 then
+    begin
+     glPushMatrix;
+     glScalef(Scale, Scale, 0);
+    end;
+
+    c := e_Colors;
+    e_Colors := Color;
+    e_Draw(TextureID, X, Y, 0, True, False);
+    e_Colors := c;
+
+    if Scale <> 1.0 then glPopMatrix;
+
+    X := X+Width+IfThen(a = Length(Text), 0, Space);
+   end;
+ end;
+end;
+
+procedure e_CharFont_PrintFmt(FontID: DWORD; X, Y: Integer; Text: string);
+var
+  a, TX, TY, len: Integer;
+  tc, c: TRGB;
+  w, h: Word;
+begin
+  if Text = '' then Exit;
+  if e_CharFonts = nil then Exit;
+  if Integer(FontID) > High(e_CharFonts) then Exit;
+
+  c.R := 255;
+  c.G := 255;
+  c.B := 255;
+
+  TX := X;
+  TY := Y;
+  len := Length(Text);
+
+  e_CharFont_GetSize(FontID, 'A', w, h);
+
+  with e_CharFonts[FontID] do
+  begin
+    for a := 1 to len do
+    begin
+      case Text[a] of
+        #10: // line feed
+        begin
+          TX := X;
+          TY := TY + h;
+          continue;
+        end;
+        #1: // black
+        begin
+          c.R := 0; c.G := 0; c.B := 0;
+          continue;
+        end;
+        #2: // white
+        begin
+          c.R := 255; c.G := 255; c.B := 255;
+          continue;
+        end;
+        #3: // darker
+        begin
+          c.R := c.R div 2; c.G := c.G div 2; c.B := c.B div 2;
+          continue;
+        end;
+        #4: // lighter
+        begin
+          c.R := Min(c.R * 2, 255); c.G := Min(c.G * 2, 255); c.B := Min(c.B * 2, 255);
+          continue;
+        end;
+        #18: // red
+        begin
+          c.R := 255; c.G := 0; c.B := 0;
+          continue;
+        end;
+        #19: // green
+        begin
+          c.R := 0; c.G := 255; c.B := 0;
+          continue;
+        end;
+        #20: // blue
+        begin
+          c.R := 0; c.G := 0; c.B := 255;
+          continue;
+        end;
+        #21: // yellow
+        begin
+          c.R := 255; c.G := 255; c.B := 0;
+          continue;
+        end;
+      end;
+
+      with Chars[Ord(Text[a])] do
+      if TextureID <> -1 then
+      begin
+        tc := e_Colors;
+        e_Colors := c;
+        e_Draw(TextureID, TX, TY, 0, True, False);
+        e_Colors := tc;
+
+        TX := TX+Width+IfThen(a = Length(Text), 0, Space);
+      end;
+    end;
+  end;
+end;
+
+procedure e_CharFont_GetSize(FontID: DWORD; Text: string; var w, h: Word);
+var
+  a: Integer;
+  h2: Word;
+begin
+ w := 0;
+ h := 0;
+
+ if Text = '' then Exit;
+ if e_CharFonts = nil then Exit;
+ if Integer(FontID) > High(e_CharFonts) then Exit;
+
+ with e_CharFonts[FontID] do
+ begin
+  for a := 1 to Length(Text) do
+   with Chars[Ord(Text[a])] do
+   if TextureID <> -1 then
+   begin
+    w := w+Width+IfThen(a = Length(Text), 0, Space);
+    e_GetTextureSize(TextureID, nil, @h2);
+    if h2 > h then h := h2;
+   end;
+ end;
+end;
+
+procedure e_CharFont_GetSizeFmt(FontID: DWORD; Text: string; var w, h: Word);
+var
+  a, lines, len: Integer;
+  h2, w2: Word;
+begin
+  w2 := 0;
+  w := 0;
+  h := 0;
+
+  if Text = '' then Exit;
+  if e_CharFonts = nil then Exit;
+  if Integer(FontID) > High(e_CharFonts) then Exit;
+
+  lines := 1;
+  len := Length(Text);
+
+  with e_CharFonts[FontID] do
+  begin
+    for a := 1 to len do
+    begin
+      if Text[a] = #10 then
+      begin
+        Inc(lines);
+        if w2 > w then
+        begin
+          w := w2;
+          w2 := 0;
+        end;
+        continue;
+      end
+      else if Text[a] in [#1, #2, #3, #4, #18, #19, #20, #21] then
+        continue;
+
+      with Chars[Ord(Text[a])] do
+      if TextureID <> -1 then
+      begin
+        w2 := w2 + Width + IfThen(a = len, 0, Space);
+        e_GetTextureSize(TextureID, nil, @h2);
+        if h2 > h then h := h2;
+      end;
+    end;
+  end;
+
+  if w2 > w then
+    w := w2;
+  h := h * lines;
+end;
+
+function e_CharFont_GetMaxWidth(FontID: DWORD): Word;
+var
+  a: Integer;
+begin
+ Result := 0;
+
+ if e_CharFonts = nil then Exit;
+ if Integer(FontID) > High(e_CharFonts) then Exit;
+
+ for a := 0 to High(e_CharFonts[FontID].Chars) do
+  Result := Max(Result, e_CharFonts[FontID].Chars[a].Width);
+end;
+
+function e_CharFont_GetMaxHeight(FontID: DWORD): Word;
+var
+  a: Integer;
+  h2: Word;
+begin
+ Result := 0;
+
+ if e_CharFonts = nil then Exit;
+ if Integer(FontID) > High(e_CharFonts) then Exit;
+
+ for a := 0 to High(e_CharFonts[FontID].Chars) do
+ begin
+  if e_CharFonts[FontID].Chars[a].TextureID <> -1 then
+   e_GetTextureSize(e_CharFonts[FontID].Chars[a].TextureID, nil, @h2)
+    else h2 := 0;
+  if h2 > Result then Result := h2;
+ end;
+end;
+
+procedure e_CharFont_Remove(FontID: DWORD);
+var
+  a: Integer;
+begin
+ with e_CharFonts[FontID] do
+  for a := 0 to High(Chars) do
+   if Chars[a].TextureID <> -1 then e_DeleteTexture(Chars[a].TextureID);
+
+ e_CharFonts[FontID].Live := False;
+end;
+
+procedure e_CharFont_RemoveAll();
+var
+  a: Integer;
+begin
+ if e_CharFonts = nil then Exit;
+
+ for a := 0 to High(e_CharFonts) do
+  e_CharFont_Remove(a);
+
+ e_CharFonts := nil;
+end;
+
+procedure e_TextureFontBuild(Tex: DWORD; var FontID: DWORD; XCount, YCount: Word;
+                             Space: ShortInt=0);
+var
+  loop1 : GLuint;
+  cx, cy : real;
+  i, id: DWORD;
+begin
+ e_WriteLog('Creating texture font...', MSG_NOTIFY);
+
+ id := DWORD(-1);
+
+ if e_TextureFonts <> nil then
+ for i := 0 to High(e_TextureFonts) do
+  if e_TextureFonts[i].Base = 0 then
+  begin
+   id := i;
+   Break;
+  end;
+
+ if id = DWORD(-1) then
+ begin
+  SetLength(e_TextureFonts, Length(e_TextureFonts) + 1);
+  id := High(e_TextureFonts);
+ end;
+
+ with e_TextureFonts[id] do
+ begin
+  Base := glGenLists(XCount*YCount);
+  TextureID := e_Textures[Tex].ID;
+  CharWidth := (e_Textures[Tex].Width div XCount)+Space;
+  CharHeight := e_Textures[Tex].Height div YCount;
+  XC := XCount;
+  YC := YCount;
+  Texture := Tex;
+  SPC := Space;
+ end;
+
+ glBindTexture(GL_TEXTURE_2D, e_Textures[Tex].ID);
+ for loop1 := 0 to XCount*YCount-1 do
+ begin
+  cx := (loop1 mod XCount)/XCount;
+  cy := (loop1 div YCount)/YCount;
+
+  glNewList(e_TextureFonts[id].Base+loop1, GL_COMPILE);
+   glBegin(GL_QUADS);
+    glTexCoord2f(cx, 1.0-cy-1/YCount);
+    glVertex2d(0, e_Textures[Tex].Height div YCount);
+
+    glTexCoord2f(cx+1/XCount, 1.0-cy-1/YCount);
+    glVertex2i(e_Textures[Tex].Width div XCount, e_Textures[Tex].Height div YCount);
+
+    glTexCoord2f(cx+1/XCount, 1.0-cy);
+    glVertex2i(e_Textures[Tex].Width div XCount, 0);
+
+    glTexCoord2f(cx, 1.0-cy);
+    glVertex2i(0, 0);
+   glEnd();
+   glTranslated((e_Textures[Tex].Width div XCount)+Space, 0, 0);
+  glEndList();
+ end;
+
+ FontID := id;
+end;
+
+procedure e_TextureFontBuildInPlace(id: DWORD);
+var
+  loop1 : GLuint;
+  cx, cy : real;
+  XCount, YCount, Space: Integer;
+  {i,} Tex: DWORD;
+begin
+ with e_TextureFonts[id] do
+ begin
+  Base := glGenLists(XC*YC);
+  TextureID := e_Textures[Texture].ID;
+  XCount := XC;
+  YCount := YC;
+  Space := SPC;
+  Tex := Texture;
+ end;
+
+ glBindTexture(GL_TEXTURE_2D, e_Textures[Tex].ID);
+ for loop1 := 0 to XCount*YCount-1 do
+ begin
+  cx := (loop1 mod XCount)/XCount;
+  cy := (loop1 div YCount)/YCount;
+
+  glNewList(e_TextureFonts[id].Base+loop1, GL_COMPILE);
+   glBegin(GL_QUADS);
+    glTexCoord2f(cx, 1.0-cy-1/YCount);
+    glVertex2d(0, e_Textures[Tex].Height div YCount);
+
+    glTexCoord2f(cx+1/XCount, 1.0-cy-1/YCount);
+    glVertex2i(e_Textures[Tex].Width div XCount, e_Textures[Tex].Height div YCount);
+
+    glTexCoord2f(cx+1/XCount, 1.0-cy);
+    glVertex2i(e_Textures[Tex].Width div XCount, 0);
+
+    glTexCoord2f(cx, 1.0-cy);
+    glVertex2i(0, 0);
+   glEnd();
+   glTranslated((e_Textures[Tex].Width div XCount)+Space, 0, 0);
+  glEndList();
+ end;
+end;
+
+procedure e_TextureFontKill(FontID: DWORD);
+begin
+  glDeleteLists(e_TextureFonts[FontID].Base, 256);
+  e_TextureFonts[FontID].Base := 0;
+end;
+
+procedure e_TextureFontPrint(X, Y: GLint; Text: string; FontID: DWORD);
+begin
+  if Integer(FontID) > High(e_TextureFonts) then Exit;
+  if Text = '' then Exit;
+
+  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+  glEnable(GL_BLEND);
+
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+
+  glPushMatrix;
+  glBindTexture(GL_TEXTURE_2D, e_TextureFonts[FontID].TextureID);
+  glEnable(GL_TEXTURE_2D);
+  glTranslated(x, y, 0);
+  glListBase(DWORD(Integer(e_TextureFonts[FontID].Base)-32));
+  glCallLists(Length(Text), GL_UNSIGNED_BYTE, PChar(Text));
+  glDisable(GL_TEXTURE_2D);
+  glPopMatrix;
+
+  glDisable(GL_BLEND);
+end;
+
+// god forgive me for this, but i cannot figure out how to do it without lists
+procedure e_TextureFontPrintChar(X, Y: Integer; Ch: Char; FontID: DWORD; Shadow: Boolean = False);
+begin
+  glPushMatrix;
+
+  if Shadow then
+  begin
+   glColor4ub(0, 0, 0, 128);
+   glTranslated(X+1, Y+1, 0);
+   glCallLists(1, GL_UNSIGNED_BYTE, @Ch);
+   glPopMatrix;
+   glPushMatrix;
+  end;
+
+  glColor4ub(e_Colors.R, e_Colors.G, e_Colors.B, 255);
+  glTranslated(X, Y, 0);
+  glCallLists(1, GL_UNSIGNED_BYTE, @Ch);
+
+  glPopMatrix;
+end;
+
+procedure e_TextureFontPrintFmt(X, Y: Integer; Text: string; FontID: DWORD; Shadow: Boolean = False);
+var
+  a, TX, TY, len: Integer;
+  tc, c: TRGB;
+  w: Word;
+begin
+  if Text = '' then Exit;
+  if e_TextureFonts = nil then Exit;
+  if Integer(FontID) > High(e_TextureFonts) then Exit;
+
+  c.R := 255;
+  c.G := 255;
+  c.B := 255;
+
+  TX := X;
+  TY := Y;
+  len := Length(Text);
+
+  w := e_TextureFonts[FontID].CharWidth;
+
+  with e_TextureFonts[FontID] do
+  begin
+    glBindTexture(GL_TEXTURE_2D, e_TextureFonts[FontID].TextureID);
+    glEnable(GL_TEXTURE_2D);
+    glListBase(DWORD(Integer(e_TextureFonts[FontID].Base)-32));
+
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+    glEnable(GL_BLEND);
+
+    for a := 1 to len do
+    begin
+      case Text[a] of
+        {#10: // line feed
+        begin
+          TX := X;
+          TY := TY + h;
+          continue;
+        end;}
+        #1: // black
+        begin
+          c.R := 0; c.G := 0; c.B := 0;
+          continue;
+        end;
+        #2: // white
+        begin
+          c.R := 255; c.G := 255; c.B := 255;
+          continue;
+        end;
+        #3: // darker
+        begin
+          c.R := c.R div 2; c.G := c.G div 2; c.B := c.B  div 2;
+          continue;
+        end;
+        #4: // lighter
+        begin
+          c.R := Min(c.R * 2, 255); c.G := Min(c.G * 2, 255); c.B := Min(c.B * 2, 255);
+          continue;
+        end;
+        #18: // red
+        begin
+          c.R := 255; c.G := 0; c.B := 0;
+          continue;
+        end;
+        #19: // green
+        begin
+          c.R := 0; c.G := 255; c.B := 0;
+          continue;
+        end;
+        #20: // blue
+        begin
+          c.R := 0; c.G := 0; c.B := 255;
+          continue;
+        end;
+        #21: // yellow
+        begin
+          c.R := 255; c.G := 255; c.B := 0;
+          continue;
+        end;
+      end;
+
+      tc := e_Colors;
+      e_Colors := c;
+      e_TextureFontPrintChar(TX, TY, Text[a], FontID, Shadow);
+      e_Colors := tc;
+
+      TX := TX+w;
+    end;
+    glDisable(GL_TEXTURE_2D);
+    glDisable(GL_BLEND);
+  end;
+end;
+
+procedure e_TextureFontPrintEx(X, Y: GLint; Text: string; FontID: DWORD; Red, Green,
+                    Blue: Byte; Scale: Single; Shadow: Boolean = False);
+begin
+  if Text = '' then Exit;
+
+  glPushMatrix;
+  glBindTexture(GL_TEXTURE_2D, e_TextureFonts[FontID].TextureID);
+  glEnable(GL_TEXTURE_2D);
+  glListBase(DWORD(Integer(e_TextureFonts[FontID].Base)-32));
+
+  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+  glEnable(GL_BLEND);
+
+  if Shadow then
+  begin
+   glColor4ub(0, 0, 0, 128);
+   glTranslated(x+1, y+1, 0);
+   glScalef(Scale, Scale, 0);
+   glCallLists(Length(Text), GL_UNSIGNED_BYTE, PChar(Text));
+   glPopMatrix;
+   glPushMatrix;
+  end;
+
+  glColor4ub(Red, Green, Blue, 255);
+  glTranslated(x, y, 0);
+  glScalef(Scale, Scale, 0);
+  glCallLists(Length(Text), GL_UNSIGNED_BYTE, PChar(Text));
+
+  glDisable(GL_TEXTURE_2D);
+  glPopMatrix;
+  glColor3ub(e_Colors.R, e_Colors.G, e_Colors.B);
+  glDisable(GL_BLEND);
+end;
+
+procedure e_TextureFontGetSize(ID: DWORD; var CharWidth, CharHeight: Byte);
+begin
+  if Integer(ID) > High(e_TextureFonts) then
+    Exit;
+  CharWidth := e_TextureFonts[ID].CharWidth;
+  CharHeight := e_TextureFonts[ID].CharHeight;
+end;
+
+procedure e_RemoveAllTextureFont();
+var
+ i: integer;
+begin
+ if e_TextureFonts = nil then Exit;
+
+ for i := 0 to High(e_TextureFonts) do
+  if e_TextureFonts[i].Base <> 0 then
+  begin
+   glDeleteLists(e_TextureFonts[i].Base, 256);
+   e_TextureFonts[i].Base := 0;
+  end;
+
+ e_TextureFonts := nil;
+end;
+
+procedure e_SaveGLContext();
+var
+  PxLen: Cardinal;
+  i: Integer;
+begin
+  e_WriteLog('Backing up GL context:', MSG_NOTIFY);
+
+  glPushAttrib(GL_ALL_ATTRIB_BITS);
+  glPushClientAttrib(GL_CLIENT_ALL_ATTRIB_BITS);
+
+  if e_Textures <> nil then
+  begin
+    e_WriteLog('  Backing up textures...', MSG_NOTIFY);
+    SetLength(e_SavedTextures, Length(e_Textures));
+    for i := Low(e_Textures) to High(e_Textures) do
+    begin
+      e_SavedTextures[i].Pixels := nil;
+      if e_Textures[i].Width > 0 then
+      begin
+        with e_SavedTextures[i] do
+        begin
+          PxLen := 3;
+          if e_Textures[i].Fmt = GL_RGBA then Inc(PxLen);
+          Pixels := GetMem(PxLen * e_Textures[i].Width * e_Textures[i].Height);
+          glBindTexture(GL_TEXTURE_2D, e_Textures[i].ID);
+          glGetTexImage(GL_TEXTURE_2D, 0, e_Textures[i].Fmt, GL_UNSIGNED_BYTE, Pixels);
+          glBindTexture(GL_TEXTURE_2D, 0);
+          OldID := e_Textures[i].ID;
+          TexId := i;
+        end;
+      end;
+    end;
+  end;
+  
+  if e_TextureFonts <> nil then
+  begin
+    e_WriteLog('  Releasing texturefonts...', MSG_NOTIFY);
+    for i := 0 to High(e_TextureFonts) do
+      if e_TextureFonts[i].Base <> 0 then
+      begin
+       glDeleteLists(e_TextureFonts[i].Base, 256);
+       e_TextureFonts[i].Base := 0;
+      end;
+  end;
+end;
+
+procedure e_RestoreGLContext();
+var
+  GLID: GLuint;
+  i: Integer;
+begin
+  e_WriteLog('Restoring GL context:', MSG_NOTIFY);
+
+  glPopClientAttrib();
+  glPopAttrib();
+
+  if e_SavedTextures <> nil then
+  begin
+    e_WriteLog('  Regenerating textures...', MSG_NOTIFY);
+    for i := Low(e_SavedTextures) to High(e_SavedTextures) do
+    begin
+      if e_SavedTextures[i].Pixels <> nil then
+        with e_SavedTextures[i] do
+        begin
+          GLID := CreateTexture(e_Textures[TexID].Width, e_Textures[TexID].Height,
+                                e_Textures[TexID].Fmt, Pixels);
+          e_Textures[TexID].ID := GLID;
+          FreeMem(Pixels);
+        end;
+    end;
+  end;
+
+  if e_TextureFonts <> nil then
+  begin
+    e_WriteLog('  Regenerating texturefonts...', MSG_NOTIFY);
+    for i := Low(e_TextureFonts) to High(e_TextureFonts) do
+      with e_TextureFonts[i] do
+      begin
+        TextureID := e_Textures[Texture].ID;
+        Base := 0;
+        e_TextureFontBuildInPlace(i);
+      end;
+  end;
+  
+  SetLength(e_SavedTextures, 0);
+end;
+
+
+function _RGB(Red, Green, Blue: Byte): TRGB;
+begin
+ Result.R := Red;
+ Result.G := Green;
+ Result.B := Blue;
+end;
+
+function _Point(X, Y: Integer): TPoint2i;
+begin
+ Result.X := X;
+ Result.Y := Y;
+end;
+
+function _Rect(X, Y: Integer; Width, Height: Word): TRectWH;
+begin
+ Result.X := X;
+ Result.Y := Y;
+ Result.Width := Width;
+ Result.Height := Height;
+end;
+
+function _TRect(L, T, R, B: LongInt): TRect;
+begin
+ Result.Top := T;
+ Result.Left := L;
+ Result.Right := R;
+ Result.Bottom := B;
+end;
+
+end.
diff --git a/src/engine/e_input.pas b/src/engine/e_input.pas
new file mode 100644 (file)
index 0000000..e8a03f7
--- /dev/null
@@ -0,0 +1,459 @@
+unit e_input;
+
+interface
+
+uses
+  SysUtils,
+  e_log,
+  SDL;
+
+const
+  e_MaxKbdKeys  = 321;
+  e_MaxJoys     = 4;
+  e_MaxJoyBtns  = 32;
+  e_MaxJoyAxes  = 4;
+  e_MaxJoyHats  = 4;
+  
+  e_MaxJoyKeys = e_MaxJoyBtns + e_MaxJoyAxes*2 + e_MaxJoyHats*4;
+  
+  e_MaxInputKeys = e_MaxKbdKeys + e_MaxJoys*e_MaxJoyKeys - 1;
+  // $$$..$$$ -  321 Keyboard buttons/keys
+  // $$$..$$$ - 4*32 Joystick buttons
+  // $$$..$$$ -  4*4 Joystick axes (- and +)
+  // $$$..$$$ -  4*4 Joystick hats (L U R D)
+  
+  // these are apparently used in g_gui and g_game and elsewhere
+  IK_UNKNOWN = SDLK_UNKNOWN;
+  IK_INVALID = 65535;
+  IK_ESCAPE  = SDLK_ESCAPE;
+  IK_RETURN  = SDLK_RETURN;
+  IK_ENTER   = SDLK_RETURN;
+  IK_UP      = SDLK_UP;
+  IK_DOWN    = SDLK_DOWN;
+  IK_LEFT    = SDLK_LEFT;
+  IK_RIGHT   = SDLK_RIGHT;
+  IK_DELETE  = SDLK_DELETE;
+  IK_HOME    = SDLK_HOME;
+  IK_INSERT  = SDLK_INSERT;
+  IK_SPACE   = SDLK_SPACE;
+  IK_CONTROL = SDLK_LCTRL;
+  IK_SHIFT   = SDLK_LSHIFT;
+  IK_TAB     = SDLK_TAB;
+  IK_PAGEUP  = SDLK_PAGEUP;
+  IK_PAGEDN  = SDLK_PAGEDOWN; 
+  IK_F2      = SDLK_F2;
+  IK_F3      = SDLK_F3;
+  IK_F4      = SDLK_F4;
+  IK_F5      = SDLK_F5;
+  IK_F6      = SDLK_F6;
+  IK_F7      = SDLK_F7;
+  IK_F8      = SDLK_F8;
+  IK_F9      = SDLK_F9;
+  IK_F10     = SDLK_F10;
+  IK_END     = SDLK_END;
+  IK_BACKSPACE = SDLK_BACKSPACE;
+  IK_BACKQUOTE = SDLK_BACKQUOTE;
+  IK_PAUSE   = SDLK_PAUSE;
+  // TODO: think of something better than this shit
+  IK_LASTKEY = 320;
+  
+  AX_MINUS  = 0;
+  AX_PLUS   = 1;
+  HAT_LEFT  = 0;
+  HAT_UP    = 1;
+  HAT_RIGHT = 2;
+  HAT_DOWN  = 3;
+  
+function  e_InitInput(): Boolean;
+procedure e_ReleaseInput();
+procedure e_ClearInputBuffer();
+function  e_PollInput(): Boolean;
+function  e_KeyPressed(Key: Word): Boolean;
+function  e_AnyKeyPressed(): Boolean;
+function  e_GetFirstKeyPressed(): Word;
+function  e_JoystickStateToString(mode: Integer): String;
+function  e_JoyByHandle(handle: Word): Integer;
+function  e_JoyButtonToKey(id: Word; btn: Byte): Word;
+function  e_JoyAxisToKey(id: Word; ax: Byte; dir: Byte): Word;
+function  e_JoyHatToKey(id: Word; hat: Byte; dir: Byte): Word;
+procedure e_SetKeyState(key: Word; state: Integer);
+
+var
+  {e_MouseInfo:          TMouseInfo;}
+  e_EnableInput:        Boolean = False;
+  e_JoysticksAvailable: Byte    = 0;
+  e_JoystickDeadzones:  array [0..e_MaxJoys-1] of Integer = (8192, 8192, 8192, 8192);
+  e_KeyNames:           array [0..e_MaxInputKeys] of String;
+
+implementation
+
+uses Math;
+
+const
+  KBRD_END = e_MaxKbdKeys;
+  JOYK_BEG = KBRD_END;
+  JOYK_END = JOYK_BEG + e_MaxJoyBtns*e_MaxJoys;
+  JOYA_BEG = JOYK_END;
+  JOYA_END = JOYA_BEG + e_MaxJoyAxes*2*e_MaxJoys;
+  JOYH_BEG = JOYA_END;
+  JOYH_END = JOYH_BEG + e_MaxJoyHats*4*e_MaxJoys;
+
+type
+  TJoystick = record
+    ID:      Byte;
+    Handle:  PSDL_Joystick;
+    Axes:    Byte;
+    Buttons: Byte;
+    Hats:    Byte;
+    ButtBuf: array [0..e_MaxJoyBtns] of Boolean;
+    AxisBuf: array [0..e_MaxJoyAxes] of Integer;
+    HatBuf:  array [0..e_MaxJoyHats] of array [HAT_LEFT..HAT_DOWN] of Boolean;
+  end;
+
+var
+  KeyBuffer: array [0..e_MaxKbdKeys] of Boolean;
+  Joysticks: array of TJoystick = nil;        
+
+function OpenJoysticks(): Byte;
+var
+  i, k, c: Integer;
+  joy: PSDL_Joystick;
+begin
+  Result := 0;
+  k := Min(e_MaxJoys, SDL_NumJoysticks());
+  if k = 0 then Exit;
+  c := 0;
+  for i := 0 to k do
+  begin
+    joy := SDL_JoystickOpen(i);
+    if joy <> nil then
+    begin
+      Inc(c);
+      e_WriteLog('Input: Opened SDL joystick ' + IntToStr(i) + ' as joystick ' + IntToStr(c) + ':', MSG_NOTIFY);
+      SetLength(Joysticks, c);
+      with Joysticks[c-1] do
+      begin
+        ID := i;
+        Handle := joy;
+        Axes := Min(e_MaxJoyAxes, SDL_JoystickNumAxes(joy));
+        Buttons := Min(e_MaxJoyBtns, SDL_JoystickNumButtons(joy));
+        Hats := Min(e_MaxJoyHats, SDL_JoystickNumHats(joy));
+        e_WriteLog('       ' + IntToStr(Axes) + ' axes, ' + IntToStr(Buttons) + ' buttons, ' +
+                   IntToStr(Hats) + ' hats.', MSG_NOTIFY);
+      end;
+    end;
+  end;
+  Result := c;
+end;
+
+procedure ReleaseJoysticks();
+var
+  i: Integer;
+begin
+  if (Joysticks = nil) or (e_JoysticksAvailable = 0) then Exit;
+  for i := Low(Joysticks) to High(Joysticks) do
+    with Joysticks[i] do
+      SDL_JoystickClose(Handle);
+  SetLength(Joysticks, 0);
+end;
+  
+function PollKeyboard(): Boolean;
+var
+  Keys: PByte;
+  NKeys: Integer;
+  i: Cardinal;
+begin
+  Result := False;
+  Keys := SDL_GetKeyState(@NKeys);
+  if (Keys = nil) or (NKeys < 1) then
+    Exit;
+  for i := 0 to NKeys do
+    KeyBuffer[i] := ((PByte(Cardinal(Keys) + i)^) <> 0);
+  for i := NKeys to High(KeyBuffer) do
+    KeyBuffer[i] := False;
+end;  
+  
+function PollJoysticks(): Boolean;
+var
+  i, j, d: Word;
+  hat: Byte;
+begin
+  if (Joysticks = nil) or (e_JoysticksAvailable = 0) then Exit;
+  SDL_JoystickUpdate();
+  for j := Low(Joysticks) to High(Joysticks) do
+    with Joysticks[j] do
+    begin
+      for i := 0 to Buttons do
+        ButtBuf[i] := SDL_JoystickGetButton(Handle, i) <> 0;
+      for i := 0 to Axes do
+        AxisBuf[i] := SDL_JoystickGetAxis(Handle, i);
+      for i := 0 to Hats do
+      begin
+        hat := SDL_JoystickGetHat(Handle, i);
+        HatBuf[i, HAT_UP] := LongBool(hat and SDL_HAT_UP);
+        HatBuf[i, HAT_DOWN] := LongBool(hat and SDL_HAT_DOWN);  
+        HatBuf[i, HAT_LEFT] := LongBool(hat and SDL_HAT_LEFT);  
+        HatBuf[i, HAT_RIGHT] := LongBool(hat and SDL_HAT_RIGHT);  
+      end;
+    end;
+  Result := False;
+end;    
+
+procedure GenerateKeyNames();
+var
+  i, j, k: LongWord;
+begin
+  // keyboard key names
+  for i := 0 to IK_LASTKEY do
+  begin
+    e_KeyNames[i] := SDL_GetKeyName(i);
+    if e_KeyNames[i] = 'unknown key' then
+      e_KeyNames[i] := '';
+  end;
+  
+  // joysticks
+  for j := 0 to e_MaxJoys-1 do
+  begin
+    k := JOYK_BEG + j * e_MaxJoyBtns;
+    // buttons
+    for i := 0 to e_MaxJoyBtns-1 do
+      e_KeyNames[k + i] := Format('JOY%d B%d', [j, i]);
+    k := JOYA_BEG + j * e_MaxJoyAxes * 2;
+    // axes
+    for i := 0 to e_MaxJoyAxes-1 do
+    begin
+      e_KeyNames[k + i*2    ] := Format('JOY%d A%d+', [j, i]);
+      e_KeyNames[k + i*2 + 1] := Format('JOY%d A%d-', [j, i]);
+    end;
+    k := JOYH_BEG + j * e_MaxJoyHats * 4;
+    // hats
+    for i := 0 to e_MaxJoyHats-1 do
+    begin
+      e_KeyNames[k + i*4    ] := Format('JOY%d D%dL', [j, i]);
+      e_KeyNames[k + i*4 + 1] := Format('JOY%d D%dU', [j, i]);
+      e_KeyNames[k + i*4 + 2] := Format('JOY%d D%dR', [j, i]);
+      e_KeyNames[k + i*4 + 3] := Format('JOY%d D%dD', [j, i]);
+    end;
+  end;
+end;
+  
+function e_InitInput(): Boolean;
+begin
+  Result := False;
+  
+  e_JoysticksAvailable := OpenJoysticks();
+  e_EnableInput := True;
+  GenerateKeyNames();
+
+  Result := True;
+end;
+
+procedure e_ReleaseInput();
+begin
+  ReleaseJoysticks();
+  e_JoysticksAvailable := 0;
+end;
+                                                         
+procedure e_ClearInputBuffer();
+var
+  i, j, d: Integer;
+begin
+  for i := Low(KeyBuffer) to High(KeyBuffer) do
+    KeyBuffer[i] := False;
+  if (Joysticks = nil) or (e_JoysticksAvailable = 0) then
+  for i := Low(Joysticks) to High(Joysticks) do
+  begin
+    for j := Low(Joysticks[i].ButtBuf) to High(Joysticks[i].ButtBuf) do
+      Joysticks[i].ButtBuf[j] := False;
+    for j := Low(Joysticks[i].AxisBuf) to High(Joysticks[i].AxisBuf) do
+      Joysticks[i].AxisBuf[j] := 0;
+    for j := Low(Joysticks[i].HatBuf) to High(Joysticks[i].HatBuf) do
+      for d := Low(Joysticks[i].HatBuf[j]) to High(Joysticks[i].HatBuf[j]) do
+        Joysticks[i].HatBuf[j, d] := False;
+  end; 
+end;
+
+function e_PollInput(): Boolean;
+var
+  kb, js: Boolean;
+begin
+  kb := PollKeyboard();
+  js := PollJoysticks();
+
+  Result := kb or js;
+end;
+
+function e_KeyPressed(Key: Word): Boolean;
+var
+  joyi, dir: Integer;
+begin
+  Result := False;
+  if (Key = IK_INVALID) or (Key = 0) then Exit;
+  
+  if (Key < KBRD_END) then
+  begin // Keyboard buttons/keys
+    Result := KeyBuffer[Key];
+  end
+  
+  else if (Key >= JOYK_BEG) and (Key < JOYK_END) then
+  begin // Joystick buttons
+    JoyI := (Key - JOYK_BEG) div e_MaxJoyBtns;
+    if JoyI >= e_JoysticksAvailable then
+      Result := False
+    else
+    begin
+      Key := (Key - JOYK_BEG) mod e_MaxJoyBtns;
+      Result := Joysticks[JoyI].ButtBuf[Key];
+    end;
+  end
+  
+  else if (Key >= JOYA_BEG) and (Key < JOYA_END) then
+  begin // Joystick axes
+    JoyI := (Key - JOYA_BEG) div (e_MaxJoyAxes*2);
+    if JoyI >= e_JoysticksAvailable then
+      Result := False
+    else
+    begin
+      Key := (Key - JOYA_BEG) mod (e_MaxJoyAxes*2);
+      dir := Key mod 2;
+      if dir = AX_MINUS then
+        Result := Joysticks[JoyI].AxisBuf[Key div 2] < -e_JoystickDeadzones[JoyI]
+      else 
+        Result := Joysticks[JoyI].AxisBuf[Key div 2] > e_JoystickDeadzones[JoyI]
+    end;    
+  end
+  
+  else if (Key >= JOYH_BEG) and (Key < JOYH_END) then
+  begin // Joystick hats 
+    JoyI := (Key - JOYH_BEG) div (e_MaxJoyHats*4);
+    if JoyI >= e_JoysticksAvailable then
+      Result := False
+    else
+    begin
+      Key := (Key - JOYH_BEG) mod (e_MaxJoyHats*4);
+      dir := Key mod 4;
+      Result := Joysticks[JoyI].HatBuf[Key div 4, dir];
+    end;    
+  end;
+end;
+
+procedure e_SetKeyState(key: Word; state: Integer);
+var
+  JoyI, dir: Integer;
+begin
+  if (Key = IK_INVALID) or (Key = 0) then Exit;
+  
+  if (Key < KBRD_END) then
+  begin // Keyboard buttons/keys
+    keyBuffer[key] := (state <> 0);
+  end
+  
+  else if (Key >= JOYK_BEG) and (Key < JOYK_END) then
+  begin // Joystick buttons
+    JoyI := (Key - JOYK_BEG) div e_MaxJoyBtns;
+    if JoyI >= e_JoysticksAvailable then
+      Exit
+    else
+    begin
+      Key := (Key - JOYK_BEG) mod e_MaxJoyBtns;
+      Joysticks[JoyI].ButtBuf[Key] := (state <> 0);
+    end;
+  end
+  
+  else if (Key >= JOYA_BEG) and (Key < JOYA_END) then
+  begin // Joystick axes      
+    JoyI := (Key - JOYA_BEG) div (e_MaxJoyAxes*2);
+    if JoyI >= e_JoysticksAvailable then
+      Exit
+    else
+    begin
+      Key := (Key - JOYA_BEG) mod (e_MaxJoyAxes*2);
+      Joysticks[JoyI].AxisBuf[Key div 2] := state;
+    end; 
+  end
+  
+  else if (Key >= JOYH_BEG) and (Key < JOYH_END) then
+  begin // Joystick hats        
+    JoyI := (Key - JOYH_BEG) div (e_MaxJoyHats*4);
+    if JoyI >= e_JoysticksAvailable then
+      Exit
+    else
+    begin
+      Key := (Key - JOYH_BEG) mod (e_MaxJoyHats*4);
+      dir := Key mod 4;
+      Joysticks[JoyI].HatBuf[Key div 4, dir] := (state <> 0);
+    end;    
+  end;
+end;
+
+function e_AnyKeyPressed(): Boolean;
+var
+  k: Word;
+begin
+  Result := False;
+
+  for k := 1 to e_MaxInputKeys do
+    if e_KeyPressed(k) then
+    begin
+      Result := True;
+      Break;
+    end;
+end;
+
+function e_GetFirstKeyPressed(): Word;
+var
+  k: Word;
+begin
+  Result := IK_INVALID;
+
+  for k := 1 to e_MaxInputKeys do
+    if e_KeyPressed(k) then
+    begin
+      Result := k;
+      Break;
+    end;
+end;
+
+////////////////////////////////////////////////////////////////////////////////
+
+function e_JoystickStateToString(mode: Integer): String;
+begin
+  Result := '';
+end;
+
+function  e_JoyByHandle(handle: Word): Integer;
+var
+  i: Integer;
+begin
+  Result := -1;
+  if Joysticks = nil then Exit;
+  for i := Low(Joysticks) to High(Joysticks) do
+    if Joysticks[i].ID = handle then
+    begin
+      Result := i;
+      Exit;
+    end;
+end;
+
+function e_JoyButtonToKey(id: Word; btn: Byte): Word;
+begin
+  Result := 0;
+  if id >= Length(Joysticks) then Exit;
+  Result := JOYK_BEG + id*e_MaxJoyBtns + btn;
+end;
+
+function e_JoyAxisToKey(id: Word; ax: Byte; dir: Byte): Word;
+begin
+  Result := 0;
+  if id >= Length(Joysticks) then Exit;
+  Result := JOYA_BEG + id*e_MaxJoyAxes*2 + ax*2 + dir;
+end;
+
+function e_JoyHatToKey(id: Word; hat: Byte; dir: Byte): Word;
+begin
+  Result := 0;
+  if id >= Length(Joysticks) then Exit;
+  Result := JOYH_BEG + id*e_MaxJoyHats*4 + hat*4 + dir;
+end;
+
+end.
diff --git a/src/engine/e_log.pas b/src/engine/e_log.pas
new file mode 100644 (file)
index 0000000..b8a1850
--- /dev/null
@@ -0,0 +1,69 @@
+unit e_log;
+
+interface
+
+uses
+  SysUtils;
+
+type
+  TWriteMode=(WM_NEWFILE,  WM_OLDFILE);
+  TRecordCategory=(MSG_FATALERROR, MSG_WARNING, MSG_NOTIFY);
+
+procedure e_InitLog(fFileName: String; fWriteMode: TWriteMode);
+procedure e_WriteLog(TextLine: String; RecordCategory: TRecordCategory;
+                     WriteTime: Boolean = True);
+function DecodeIPV4(ip: LongWord): string;
+
+implementation
+
+var
+  FirstRecord: Boolean;
+  FileName: String;
+
+{ TLog }
+
+function DecodeIPV4(ip: LongWord): string;
+begin
+  Result := Format('%d.%d.%d.%d', [ip and $FF, (ip shr 8) and $FF, (ip shr 16) and $FF, (ip shr 24)]);
+end;
+
+procedure e_WriteLog(TextLine: String; RecordCategory: TRecordCategory;
+                     WriteTime: Boolean = True);
+var
+  LogFile: TextFile;
+  Prefix: ShortString;
+begin
+  if FileName = '' then Exit;
+
+  Assign(LogFile, FileName);
+  if FileExists(FileName) then
+    Append(LogFile)
+  else
+    Rewrite(LogFile);
+  if FirstRecord then
+  begin
+    Writeln(LogFile, '--- Log started at '+TimeToStr(Time)+' ---');
+    FirstRecord := False;
+  end;
+  case RecordCategory of
+    MSG_FATALERROR: Prefix := '!!!';
+    MSG_WARNING:    Prefix := '!  ';
+    MSG_NOTIFY:     Prefix := '***';
+  end;
+  if WriteTime then
+    Writeln(LogFile, '['+TimeToStr(Time)+'] '+Prefix+' '+TextLine)
+  else
+    Writeln(LogFile, Prefix+' '+TextLine);
+  Close(LogFile);
+end;
+
+procedure e_InitLog(fFileName: String; fWriteMode: TWriteMode);
+begin
+ FileName := fFileName;
+ if fWriteMode = WM_NEWFILE then
+  if FileExists(FileName) then
+   DeleteFile(FileName);
+ FirstRecord := True;
+end;
+
+end.
diff --git a/src/engine/e_sound.pas b/src/engine/e_sound.pas
new file mode 100644 (file)
index 0000000..05bcb10
--- /dev/null
@@ -0,0 +1,1013 @@
+unit e_sound;
+
+interface
+
+uses
+  fmod,
+  fmodtypes,
+  fmoderrors,
+  e_log,
+  SysUtils;
+
+type
+  TSoundRec = record
+    Data: Pointer;
+    Sound: FMOD_SOUND;
+    Loop: Boolean;
+    nRefs: Integer;
+  end;
+
+  TBasicSound = class (TObject)
+  private
+    FChannel: FMOD_CHANNEL;
+
+  protected
+    FID: DWORD;
+    FLoop: Boolean;
+    FPosition: DWORD;
+    FPriority: Integer;
+
+    function RawPlay(Pan: Single; Volume: Single; aPos: DWORD): Boolean;
+
+  public
+    constructor Create();
+    destructor Destroy(); override;
+    procedure SetID(ID: DWORD);
+    procedure FreeSound();
+    function IsPlaying(): Boolean;
+    procedure Stop();
+    function IsPaused(): Boolean;
+    procedure Pause(Enable: Boolean);
+    function GetVolume(): Single;
+    procedure SetVolume(Volume: Single);
+    function GetPan(): Single;
+    procedure SetPan(Pan: Single);
+    function IsMuted(): Boolean;
+    procedure Mute(Enable: Boolean);
+    function GetPosition(): DWORD;
+    procedure SetPosition(aPos: DWORD);
+    procedure SetPriority(priority: Integer);
+  end;
+
+const
+  NO_SOUND_ID = DWORD(-1);
+
+function e_InitSoundSystem(Freq: Integer; forceNoSound: Boolean): Boolean;
+
+function e_LoadSound(FileName: string; var ID: DWORD; bLoop: Boolean): Boolean;
+function e_LoadSoundMem(pData: Pointer; Length: Integer; var ID: DWORD; bLoop: Boolean): Boolean;
+
+function e_PlaySound(ID: DWORD): Boolean;
+function e_PlaySoundPan(ID: DWORD; Pan: Single): Boolean;
+function e_PlaySoundVolume(ID: DWORD; Volume: Single): Boolean;
+function e_PlaySoundPanVolume(ID: DWORD; Pan, Volume: Single): Boolean;
+
+procedure e_ModifyChannelsVolumes(SoundMod: Single; setMode: Boolean);
+procedure e_MuteChannels(Enable: Boolean);
+procedure e_StopChannels();
+
+procedure e_DeleteSound(ID: DWORD);
+procedure e_RemoveAllSounds();
+procedure e_ReleaseSoundSystem();
+procedure e_SoundUpdate();
+
+var
+  e_SoundsArray: array of TSoundRec = nil;
+
+implementation
+
+uses
+  g_window, g_options, BinEditor;
+
+const
+  N_CHANNELS = 512;
+
+var
+  F_System: FMOD_SYSTEM = nil;
+  SoundMuted: Boolean = False;
+
+
+function Channel_Callback(channel: FMOD_CHANNEL; callbacktype: FMOD_CHANNEL_CALLBACKTYPE;
+                          commanddata1: Pointer; commanddata2: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+var
+  res: FMOD_RESULT;
+  sound: FMOD_SOUND;
+  ud: Pointer;
+  id: DWORD;
+
+begin
+  res := FMOD_OK;
+
+  if callbacktype = FMOD_CHANNEL_CALLBACKTYPE_END then
+  begin
+    res := FMOD_Channel_GetCurrentSound(channel, sound);
+    if res = FMOD_OK then
+    begin
+      res := FMOD_Sound_GetUserData(sound, ud);
+      if res = FMOD_OK then
+      begin
+        id := DWORD(ud^);
+        if id < DWORD(Length(e_SoundsArray)) then
+          if e_SoundsArray[id].nRefs > 0 then
+            Dec(e_SoundsArray[id].nRefs);
+      end;
+    end;
+  end;
+
+  Result := res;
+end;
+
+function TryInitWithOutput(Output: FMOD_OUTPUTTYPE; OutputName: String): FMOD_RESULT;
+begin
+  e_WriteLog('Trying with ' + OutputName + '...', MSG_WARNING);
+  Result := FMOD_System_SetOutput(F_System, Output);
+  if Result <> FMOD_OK then
+  begin
+    e_WriteLog('Error setting FMOD output to ' + OutputName + '!', MSG_WARNING);
+    e_WriteLog(FMOD_ErrorString(Result), MSG_WARNING);
+    Exit;
+  end;
+  Result := FMOD_System_Init(F_System, N_CHANNELS, FMOD_INIT_NORMAL, nil);
+  if Result <> FMOD_OK then
+  begin
+    e_WriteLog('Error initializing FMOD system!', MSG_WARNING);
+    e_WriteLog(FMOD_ErrorString(Result), MSG_WARNING);
+    Exit;
+  end;
+end;
+
+function e_InitSoundSystem(Freq: Integer; forceNoSound: Boolean): Boolean;
+var
+  res: FMOD_RESULT;
+  ver: Cardinal;
+  output: FMOD_OUTPUTTYPE;
+  drv: Integer;
+
+begin
+  Result := False;
+
+  res := FMOD_System_Create(F_System);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error creating FMOD system:', MSG_FATALERROR);
+    e_WriteLog(FMOD_ErrorString(res), MSG_FATALERROR);
+    Exit;
+  end;
+
+  res := FMOD_System_GetVersion(F_System, ver);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error getting FMOD version:', MSG_FATALERROR);
+    e_WriteLog(FMOD_ErrorString(res), MSG_FATALERROR);
+    Exit;
+  end;
+
+  if ver < FMOD_VERSION then
+  begin
+    e_WriteLog('FMOD library version is too old! Need '+IntToStr(FMOD_VERSION), MSG_FATALERROR);
+    Exit;
+  end;
+
+  res := FMOD_System_SetSoftwareFormat(F_System, Freq,
+           FMOD_SOUND_FORMAT_PCM16, 0, 0, FMOD_DSP_RESAMPLER_LINEAR);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error setting FMOD software format!', MSG_FATALERROR);
+    e_WriteLog(FMOD_ErrorString(res), MSG_FATALERROR);
+    Exit;
+  end;
+
+  res := FMOD_System_Init(F_System, N_CHANNELS, FMOD_INIT_NORMAL, nil);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error initializing FMOD system!', MSG_WARNING);
+    e_WriteLog(FMOD_ErrorString(res), MSG_WARNING);
+
+    {$IFDEF LINUX}
+    res := TryInitWithOutput(FMOD_OUTPUTTYPE_ALSA, 'OUTPUTTYPE_ALSA');
+    if res <> FMOD_OK then
+      res := TryInitWithOutput(FMOD_OUTPUTTYPE_OSS, 'OUTPUTTYPE_OSS');
+    {$ENDIF}
+    if not forceNoSound then Exit;
+    if res <> FMOD_OK then
+      res := TryInitWithOutput(FMOD_OUTPUTTYPE_NOSOUND, 'OUTPUTTYPE_NOSOUND');
+    if res <> FMOD_OK then
+    begin
+      e_WriteLog('FMOD: Giving up, can''t init any output.', MSG_FATALERROR);
+      Exit;
+    end;
+  end;
+
+  res := FMOD_System_GetOutput(F_System, output);
+  if res <> FMOD_OK then
+    e_WriteLog('Error getting FMOD output!', MSG_WARNING)
+  else
+    case output of
+      FMOD_OUTPUTTYPE_NOSOUND: e_WriteLog('FMOD Output Method: NOSOUND', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_NOSOUND_NRT: e_WriteLog('FMOD Output Method: NOSOUND_NRT', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_DSOUND: e_WriteLog('FMOD Output Method: DSOUND', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_WINMM: e_WriteLog('FMOD Output Method: WINMM', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_OPENAL: e_WriteLog('FMOD Output Method: OPENAL', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_WASAPI: e_WriteLog('FMOD Output Method: WASAPI', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_ASIO: e_WriteLog('FMOD Output Method: ASIO', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_OSS:  e_WriteLog('FMOD Output Method: OSS', MSG_NOTIFY);
+      FMOD_OUTPUTTYPE_ALSA: e_Writelog('FMOD Output Method: ALSA', MSG_NOTIFY);
+      else e_WriteLog('FMOD Output Method: Unknown', MSG_NOTIFY);
+    end;
+
+  res := FMOD_System_GetDriver(F_System, drv);
+  if res <> FMOD_OK then
+    e_WriteLog('Error getting FMOD driver!', MSG_WARNING)
+  else
+    e_WriteLog('FMOD driver id: '+IntToStr(drv), MSG_NOTIFY);
+
+  Result := True;
+end;
+
+function FindESound(): DWORD;
+var
+  i: Integer;
+
+begin
+  if e_SoundsArray <> nil then
+    for i := 0 to High(e_SoundsArray) do
+      if e_SoundsArray[i].Sound = nil then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if e_SoundsArray = nil then
+    begin
+      SetLength(e_SoundsArray, 16);
+      Result := 0;
+    end
+  else
+    begin
+      Result := High(e_SoundsArray) + 1;
+      SetLength(e_SoundsArray, Length(e_SoundsArray) + 16);
+    end;
+end;
+
+function e_LoadSound(FileName: String; var ID: DWORD; bLoop: Boolean): Boolean;
+var
+  find_id: DWORD;
+  res: FMOD_RESULT;
+  bt: Cardinal;
+  ud: Pointer;
+
+begin
+  Result := False;
+
+  e_WriteLog('Loading sound '+FileName+'...', MSG_NOTIFY);
+
+  find_id := FindESound();
+
+  if bLoop then
+    bt := FMOD_LOOP_NORMAL
+  else
+    bt := FMOD_LOOP_OFF;
+
+  if not bLoop then
+    res := FMOD_System_CreateSound(F_System, PAnsiChar(FileName),
+             bt + FMOD_2D + FMOD_HARDWARE,
+             nil, e_SoundsArray[find_id].Sound)
+  else
+    res := FMOD_System_CreateStream(F_System, PAnsiChar(FileName),
+             bt + FMOD_2D + FMOD_HARDWARE,
+             nil, e_SoundsArray[find_id].Sound);
+  if res <> FMOD_OK then
+  begin
+    e_SoundsArray[find_id].Sound := nil;
+    Exit;
+  end;
+
+  GetMem(ud, SizeOf(DWORD));
+  DWORD(ud^) := find_id;
+  res := FMOD_Sound_SetUserData(e_SoundsArray[find_id].Sound, ud);
+  if res <> FMOD_OK then
+  begin
+    e_SoundsArray[find_id].Sound := nil;
+    Exit;
+  end;
+
+  e_SoundsArray[find_id].Data := nil;
+  e_SoundsArray[find_id].Loop := bLoop;
+  e_SoundsArray[find_id].nRefs := 0;
+
+  ID := find_id;
+
+  Result := True;
+end;
+
+function e_LoadSoundMem(pData: Pointer; Length: Integer; var ID: DWORD; bLoop: Boolean): Boolean;
+var
+  find_id: DWORD;
+  res: FMOD_RESULT;
+  sz: Integer;
+  bt: Cardinal;
+  soundExInfo: FMOD_CREATESOUNDEXINFO;
+  ud: Pointer;
+
+begin
+  Result := False;
+
+  find_id := FindESound();
+
+  sz := SizeOf(FMOD_CREATESOUNDEXINFO);
+  FillMemory(@soundExInfo, sz, 0);
+  soundExInfo.cbsize := sz;
+  soundExInfo.length := Length;
+
+  if bLoop then
+    bt := FMOD_LOOP_NORMAL
+  else
+    bt := FMOD_LOOP_OFF;
+
+  if not bLoop then
+    res := FMOD_System_CreateSound(F_System, pData,
+             bt + FMOD_2D + FMOD_HARDWARE + FMOD_OPENMEMORY,
+             @soundExInfo, e_SoundsArray[find_id].Sound)
+  else
+    res := FMOD_System_CreateStream(F_System, pData,
+             bt + FMOD_2D + FMOD_HARDWARE + FMOD_OPENMEMORY,
+             @soundExInfo, e_SoundsArray[find_id].Sound);
+  if res <> FMOD_OK then
+  begin
+    e_SoundsArray[find_id].Sound := nil;
+    Exit;
+  end;
+
+  GetMem(ud, SizeOf(DWORD));
+  DWORD(ud^) := find_id;
+  res := FMOD_Sound_SetUserData(e_SoundsArray[find_id].Sound, ud);
+  if res <> FMOD_OK then
+  begin
+    e_SoundsArray[find_id].Sound := nil;
+    Exit;
+  end;
+
+  e_SoundsArray[find_id].Data := pData;
+  e_SoundsArray[find_id].Loop := bLoop;
+  e_SoundsArray[find_id].nRefs := 0;
+
+  ID := find_id;
+
+  Result := True;
+end;
+
+function e_PlaySound(ID: DWORD): Boolean;
+var
+  res: FMOD_RESULT;
+  Chan: FMOD_CHANNEL;
+
+begin
+  if e_SoundsArray[ID].nRefs >= gMaxSimSounds then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+  Result := False;
+
+  res := FMOD_System_PlaySound(F_System, FMOD_CHANNEL_FREE,
+           e_SoundsArray[ID].Sound, False, Chan);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  res := FMOD_Channel_SetCallback(Chan, Channel_Callback);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  if SoundMuted then
+  begin
+    res := FMOD_Channel_SetMute(Chan, True);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+
+  Inc(e_SoundsArray[ID].nRefs);
+  Result := True;
+end;
+
+function e_PlaySoundPan(ID: DWORD; Pan: Single): Boolean;
+var
+  res: FMOD_RESULT;
+  Chan: FMOD_CHANNEL;
+
+begin
+  if e_SoundsArray[ID].nRefs >= gMaxSimSounds then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+  Result := False;
+
+  res := FMOD_System_PlaySound(F_System, FMOD_CHANNEL_FREE,
+           e_SoundsArray[ID].Sound, False, Chan);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  res := FMOD_Channel_SetPan(Chan, Pan);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  res := FMOD_Channel_SetCallback(Chan, Channel_Callback);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  if SoundMuted then
+  begin
+    res := FMOD_Channel_SetMute(Chan, True);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+
+  Inc(e_SoundsArray[ID].nRefs);
+  Result := True;
+end;
+
+function e_PlaySoundVolume(ID: DWORD; Volume: Single): Boolean;
+var
+  res: FMOD_RESULT;
+  Chan: FMOD_CHANNEL;
+
+begin
+  if e_SoundsArray[ID].nRefs >= gMaxSimSounds then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+  Result := False;
+
+  res := FMOD_System_PlaySound(F_System, FMOD_CHANNEL_FREE,
+           e_SoundsArray[ID].Sound, False, Chan);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  res := FMOD_Channel_SetVolume(Chan, Volume);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  res := FMOD_Channel_SetCallback(Chan, Channel_Callback);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  if SoundMuted then
+  begin
+    res := FMOD_Channel_SetMute(Chan, True);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+
+  Inc(e_SoundsArray[ID].nRefs);
+  Result := True;
+end;
+
+function e_PlaySoundPanVolume(ID: DWORD; Pan, Volume: Single): Boolean;
+var
+  res: FMOD_RESULT;
+  Chan: FMOD_CHANNEL;
+
+begin
+  if e_SoundsArray[ID].nRefs >= gMaxSimSounds then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+  Result := False;
+
+  res := FMOD_System_PlaySound(F_System, FMOD_CHANNEL_FREE,
+           e_SoundsArray[ID].Sound, False, Chan);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  res := FMOD_Channel_SetPan(Chan, Pan);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  res := FMOD_Channel_SetVolume(Chan, Volume);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  res := FMOD_Channel_SetCallback(Chan, Channel_Callback);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  if SoundMuted then
+  begin
+    res := FMOD_Channel_SetMute(Chan, True);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+
+  Inc(e_SoundsArray[ID].nRefs);
+  Result := True;
+end;
+
+procedure e_DeleteSound(ID: DWORD);
+var
+  res: FMOD_RESULT;
+  ud: Pointer;
+
+begin
+  if e_SoundsArray[ID].Sound = nil then
+    Exit;
+
+  if e_SoundsArray[ID].Data <> nil then
+    FreeMem(e_SoundsArray[ID].Data);
+
+  res := FMOD_Sound_GetUserData(e_SoundsArray[ID].Sound, ud);
+  if res = FMOD_OK then
+  begin
+    FreeMem(ud);
+  end;
+
+  res := FMOD_Sound_Release(e_SoundsArray[ID].Sound);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error releasing sound:', MSG_WARNING);
+    e_WriteLog(FMOD_ErrorString(res), MSG_WARNING);
+  end;
+
+  e_SoundsArray[ID].Sound := nil;
+  e_SoundsArray[ID].Data := nil;
+end;
+
+procedure e_ModifyChannelsVolumes(SoundMod: Single; setMode: Boolean);
+var
+  res: FMOD_RESULT;
+  i: Integer;
+  Chan: FMOD_CHANNEL;
+  vol: Single;
+
+begin
+  for i := 0 to N_CHANNELS-1 do
+  begin
+    Chan := nil;
+    res := FMOD_System_GetChannel(F_System, i, Chan);
+
+    if (res = FMOD_OK) and (Chan <> nil) then
+    begin
+      res := FMOD_Channel_GetVolume(Chan, vol);
+
+      if res = FMOD_OK then
+      begin
+        if setMode then
+          vol := SoundMod
+        else
+          vol := vol * SoundMod;
+
+        res := FMOD_Channel_SetVolume(Chan, vol);
+
+        if res <> FMOD_OK then
+        begin
+        end;
+      end;
+    end;
+  end;
+end;
+
+procedure e_MuteChannels(Enable: Boolean);
+var
+  res: FMOD_RESULT;
+  i: Integer;
+  Chan: FMOD_CHANNEL;
+
+begin
+  if Enable = SoundMuted then
+    Exit;
+
+  SoundMuted := Enable;
+
+  for i := 0 to N_CHANNELS-1 do
+  begin
+    Chan := nil;
+    res := FMOD_System_GetChannel(F_System, i, Chan);
+
+    if (res = FMOD_OK) and (Chan <> nil) then
+    begin
+      res := FMOD_Channel_SetMute(Chan, Enable);
+
+      if res <> FMOD_OK then
+      begin
+      end;
+    end;
+  end;
+end;
+
+procedure e_StopChannels();
+var
+  res: FMOD_RESULT;
+  i: Integer;
+  Chan: FMOD_CHANNEL;
+
+begin
+  for i := 0 to N_CHANNELS-1 do
+  begin
+    Chan := nil;
+    res := FMOD_System_GetChannel(F_System, i, Chan);
+
+    if (res = FMOD_OK) and (Chan <> nil) then
+    begin
+      res := FMOD_Channel_Stop(Chan);
+
+      if res <> FMOD_OK then
+      begin
+      end;
+    end;
+  end;
+end;
+
+procedure e_RemoveAllSounds();
+var
+  i: Integer;
+
+begin
+  for i := 0 to High(e_SoundsArray) do
+    if e_SoundsArray[i].Sound <> nil then
+      e_DeleteSound(i);
+
+  SetLength(e_SoundsArray, 0);
+  e_SoundsArray := nil;
+end;
+
+procedure e_ReleaseSoundSystem();
+var
+  res: FMOD_RESULT;
+
+begin
+  e_RemoveAllSounds();
+
+  res := FMOD_System_Close(F_System);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error closing FMOD system!', MSG_FATALERROR);
+    e_WriteLog(FMOD_ErrorString(res), MSG_FATALERROR);
+    Exit;
+  end;
+
+  res := FMOD_System_Release(F_System);
+  if res <> FMOD_OK then
+  begin
+    e_WriteLog('Error releasing FMOD system!', MSG_FATALERROR);
+    e_WriteLog(FMOD_ErrorString(res), MSG_FATALERROR);
+  end;
+end;
+
+procedure e_SoundUpdate();
+begin
+  FMOD_System_Update(F_System);
+end;
+
+{ TBasicSound: }
+
+constructor TBasicSound.Create();
+begin
+  FID := NO_SOUND_ID;
+  FLoop := False;
+  FChannel := nil;
+  FPosition := 0;
+  FPriority := 128;
+end;
+
+destructor TBasicSound.Destroy();
+begin
+  FreeSound();
+  inherited;
+end;
+
+procedure TBasicSound.FreeSound();
+begin
+  if FID = NO_SOUND_ID then
+    Exit;
+
+  Stop();
+  FID := NO_SOUND_ID;
+  FLoop := False;
+  FPosition := 0;
+end;
+
+function TBasicSound.RawPlay(Pan: Single; Volume: Single; aPos: DWORD): Boolean;
+var
+  res: FMOD_RESULT;
+
+begin
+  if e_SoundsArray[FID].nRefs >= gMaxSimSounds then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+  Result := False;
+
+  if FID = NO_SOUND_ID then
+    Exit;
+
+  res := FMOD_System_PlaySound(F_System, FMOD_CHANNEL_FREE,
+           e_SoundsArray[FID].Sound, False, FChannel);
+  if res <> FMOD_OK then
+  begin
+    FChannel := nil;
+    Exit;
+  end;
+
+  res := FMOD_Channel_SetPosition(FChannel, aPos, FMOD_TIMEUNIT_MS);
+  if res <> FMOD_OK then
+    begin
+      FPosition := 0;
+    end
+  else
+    FPosition := aPos;
+
+  res := FMOD_Channel_SetPan(FChannel, Pan);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  res := FMOD_Channel_SetVolume(FChannel, Volume);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  res := FMOD_Channel_SetCallback(FChannel, Channel_Callback);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  if SoundMuted then
+  begin
+    res := FMOD_Channel_SetMute(FChannel, True);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+
+  Inc(e_SoundsArray[FID].nRefs);
+  Result := True;
+end;
+
+procedure TBasicSound.SetID(ID: DWORD);
+begin
+  FreeSound();
+  FID := ID;
+  FLoop := e_SoundsArray[ID].Loop;
+end;
+
+function TBasicSound.IsPlaying(): Boolean;
+var
+  res: FMOD_RESULT;
+  b: LongBool;
+
+begin
+  Result := False;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_IsPlaying(FChannel, b);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  Result := b;
+end;
+
+procedure TBasicSound.Stop();
+var
+  res: FMOD_RESULT;
+
+begin
+  if FChannel = nil then
+    Exit;
+
+  GetPosition();
+
+  res := FMOD_Channel_Stop(FChannel);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  FChannel := nil;
+end;
+
+function TBasicSound.IsPaused(): Boolean;
+var
+  res: FMOD_RESULT;
+  b: LongBool;
+
+begin
+  Result := False;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_GetPaused(FChannel, b);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  Result := b;
+end;
+
+procedure TBasicSound.Pause(Enable: Boolean);
+var
+  res: FMOD_RESULT;
+
+begin
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_SetPaused(FChannel, Enable);
+  if res <> FMOD_OK then
+  begin
+  end;
+
+  if Enable then
+  begin
+    res := FMOD_Channel_GetPosition(FChannel, FPosition, FMOD_TIMEUNIT_MS);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+end;
+
+function TBasicSound.GetVolume(): Single;
+var
+  res: FMOD_RESULT;
+  vol: Single;
+
+begin
+  Result := 0.0;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_GetVolume(FChannel, vol);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  Result := vol;
+end;
+
+procedure TBasicSound.SetVolume(Volume: Single);
+var
+  res: FMOD_RESULT;
+
+begin
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_SetVolume(FChannel, Volume);
+  if res <> FMOD_OK then
+  begin
+  end;
+end;
+
+function TBasicSound.GetPan(): Single;
+var
+  res: FMOD_RESULT;
+  pan: Single;
+
+begin
+  Result := 0.0;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_GetPan(FChannel, pan);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  Result := pan;
+end;
+
+procedure TBasicSound.SetPan(Pan: Single);
+var
+  res: FMOD_RESULT;
+
+begin
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_SetPan(FChannel, Pan);
+  if res <> FMOD_OK then
+  begin
+  end;
+end;
+
+function TBasicSound.IsMuted(): Boolean;
+var
+  res: FMOD_RESULT;
+  b: LongBool;
+
+begin
+  Result := False;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_GetMute(FChannel, b);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  Result := b;
+end;
+
+procedure TBasicSound.Mute(Enable: Boolean);
+var
+  res: FMOD_RESULT;
+
+begin
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_SetMute(FChannel, Enable);
+  if res <> FMOD_OK then
+  begin
+  end;
+end;
+
+function TBasicSound.GetPosition(): DWORD;
+var
+  res: FMOD_RESULT;
+
+begin
+  Result := 0;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_GetPosition(FChannel, FPosition, FMOD_TIMEUNIT_MS);
+  if res <> FMOD_OK then
+  begin
+    Exit;
+  end;
+
+  Result := FPosition;
+end;
+
+procedure TBasicSound.SetPosition(aPos: DWORD);
+var
+  res: FMOD_RESULT;
+
+begin
+  FPosition := aPos;
+
+  if FChannel = nil then
+    Exit;
+
+  res := FMOD_Channel_SetPosition(FChannel, FPosition, FMOD_TIMEUNIT_MS);
+  if res <> FMOD_OK then
+  begin
+  end;
+end;
+
+procedure TBasicSound.SetPriority(priority: Integer);
+var
+  res: FMOD_RESULT;
+
+begin
+  if (FChannel <> nil) and (FPriority <> priority) and
+     (priority >= 0) and (priority <= 256) then
+  begin
+    FPriority := priority;
+    res := FMOD_Channel_SetPriority(FChannel, priority);
+    if res <> FMOD_OK then
+    begin
+    end;
+  end;
+end;
+
+end.
diff --git a/src/engine/e_textures.pas b/src/engine/e_textures.pas
new file mode 100644 (file)
index 0000000..504b3fb
--- /dev/null
@@ -0,0 +1,453 @@
+unit e_textures;
+
+{ This unit provides interface to load 24-bit and 32-bit uncompressed images
+  from Truevision Targa (TGA) graphic files, and create OpenGL textures
+  from it's data. }
+
+interface
+
+uses
+  GL, GLExt, SysUtils, e_log;
+
+var
+  fUseMipmaps: Boolean = False;
+  TEXTUREFILTER: Integer = GL_NEAREST;
+
+function CreateTexture( Width, Height, Format: Word; pData: Pointer ): Integer;  
+  
+// Standard set of images loading functions
+function LoadTexture( Filename: String; var Texture: GLuint;
+                      var pWidth, pHeight: Word; Fmt: PWord = nil ): Boolean;
+
+function LoadTextureEx( Filename: String; var Texture: GLuint;
+                        fX, fY, fWidth, fHeight: Word; Fmt: PWord = nil ): Boolean;
+
+function LoadTextureMem( pData: Pointer; var Texture: GLuint;
+                         var pWidth, pHeight: Word; Fmt: PWord = nil ): Boolean;
+
+function LoadTextureMemEx( pData: Pointer; var Texture: GLuint;
+                           fX, fY, fWidth, fHeight: Word; Fmt: PWord = nil ): Boolean;
+
+implementation
+
+uses BinEditor;
+
+type
+  TTGAHeader = packed record
+    FileType:     Byte;
+    ColorMapType: Byte;
+    ImageType:    Byte;
+    ColorMapSpec: array[0..4] of Byte;
+    OrigX:        array[0..1] of Byte;
+    OrigY:        array[0..1] of Byte;
+    Width:        array[0..1] of Byte;
+    Height:       array[0..1] of Byte;
+    BPP:          Byte;
+    ImageInfo:    Byte;
+  end;
+
+// This is auxiliary function that creates OpenGL texture from raw image data
+function CreateTexture( Width, Height, Format: Word; pData: Pointer ): Integer;
+var
+  Texture: GLuint;
+begin
+  glGenTextures( 1, @Texture );
+  glBindTexture( GL_TEXTURE_2D, Texture );
+
+    {Texture blends with object background}
+  glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE );
+    {Texture does NOT blend with object background}
+ // glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
+
+  {
+    Select a filtering type.
+    BiLinear filtering produces very good results with little performance impact
+
+    GL_NEAREST               - Basic texture (grainy looking texture)
+    GL_LINEAR                - BiLinear filtering
+    GL_LINEAR_MIPMAP_NEAREST - Basic mipmapped texture
+    GL_LINEAR_MIPMAP_LINEAR  - BiLinear Mipmapped texture
+  }
+
+  // for GL_TEXTURE_MAG_FILTER only first two can be used
+  glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, TEXTUREFILTER );
+  // for GL_TEXTURE_MIN_FILTER all of the above can be used
+  glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, TEXTUREFILTER );
+
+  if Format = GL_RGBA then
+  begin
+    glTexImage2D( GL_TEXTURE_2D, 0, 4, Width, Height,
+                  0, GL_RGBA, GL_UNSIGNED_BYTE, pData );
+  end else
+  begin
+    glTexImage2D( GL_TEXTURE_2D, 0, 3, Width, Height,
+                  0, GL_RGB, GL_UNSIGNED_BYTE, pData );
+  end;
+  
+  glBindTexture(GL_TEXTURE_2D, 0);
+
+  Result := Texture;
+end;
+
+function LoadTextureMem( pData: Pointer; var Texture: GLuint;
+                         var pWidth, pHeight: Word; Fmt: PWord = nil ): Boolean;
+var
+  TGAHeader:     TTGAHeader;
+  image:         Pointer;
+  Width, Height: Integer;
+  ImageSize:     Integer;
+  i:             Integer;
+  Front:         ^Byte;
+  Back:          ^Byte;
+  Temp:          Byte;
+  BPP:           Byte;
+  TFmt:           Word;
+
+begin
+  Result := False;
+  pWidth := 0;
+  pHeight := 0;
+
+  CopyMemory( @TGAHeader, pData, SizeOf(TGAHeader) );
+
+  if ( TGAHeader.ImageType <> 2 ) then
+  begin
+    e_WriteLog( 'Error loading texture: Bad ImageType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.ColorMapType <> 0 ) then
+  begin
+    e_WriteLog( 'Error loading texture: Bad ColorMapType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.BPP < 24 ) then
+  begin
+    e_WriteLog( 'Error loading texture: BPP less than 24', MSG_WARNING );
+    Exit;
+  end;
+
+  Width  := TGAHeader.Width[0]  + TGAHeader.Width[1]  * 256;
+  Height := TGAHeader.Height[0] + TGAHeader.Height[1] * 256;
+  BPP := TGAHeader.BPP;
+
+  ImageSize := Width * Height * (BPP div 8);
+
+  GetMem( Image, ImageSize );
+  CopyMemory( Image, PByte(pData) + SizeOf(TGAHeader), ImageSize );
+
+  for i := 0 to Width * Height - 1 do
+  begin
+    Front := PByte(Image) + i*(BPP div 8);
+    Back  := PByte(Image) + i*(BPP div 8) + 2;
+    Temp   := Front^;
+    Front^ := Back^;
+    Back^  := Temp;
+  end;
+
+  if ( BPP = 24 ) then
+    TFmt := GL_RGB
+  else
+    TFmt := GL_RGBA;
+    
+  Texture := CreateTexture( Width, Height, TFmt, Image );
+
+  FreeMem( Image );
+  
+  if Fmt <> nil then Fmt^ := TFmt;
+
+  pWidth := Width;
+  pHeight := Height;
+
+  Result := True;
+end;
+
+function LoadTextureMemEx( pData: Pointer; var Texture: GLuint;
+                           fX, fY, fWidth, fHeight: Word; Fmt: PWord = nil ): Boolean;
+var
+  TGAHeader:     TTGAHeader;
+  image, image2: Pointer;
+  Width, Height: Integer;
+  ImageSize:     Integer;
+  i, a, b:       Integer;
+  Front:         ^Byte;
+  Back:          ^Byte;
+  Temp:          Byte;
+  BPP:           Byte;
+  Base:          PByte;
+  TFmt:          Word;
+
+begin
+  Result := False;
+
+  CopyMemory( @TGAHeader, pData, SizeOf(TGAHeader) );
+
+  if ( TGAHeader.ImageType <> 2 ) then
+  begin
+    e_WriteLog( 'Error loading texture: Bad ImageType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.ColorMapType <> 0 ) then
+  begin
+    e_WriteLog( 'Error loading texture: Bad ColorMapType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.BPP < 24 ) then
+  begin
+    e_WriteLog( 'Error loading texture: BPP less than 24', MSG_WARNING );
+    Exit;
+  end;
+
+  Width  := TGAHeader.Width[0]  + TGAHeader.Width[1]  * 256;
+  Height := TGAHeader.Height[0] + TGAHeader.Height[1] * 256;
+  BPP := TGAHeader.BPP;
+
+  if fX > Width then Exit;
+  if fY > Height then Exit;
+  if fX+fWidth > Width then Exit;
+  if fY+fHeight > Height then Exit;
+
+  ImageSize := Width * Height * (BPP div 8);
+  GetMem( Image2, ImageSize );
+  CopyMemory( Image2, PByte(pData) + SizeOf(TGAHeader), ImageSize );
+
+  a := BPP div 8;
+
+  for i := 0 to Width * Height - 1 do
+  begin
+    Front := PByte(Image2) + i * a;
+    Back  := PByte(Image2) + i * a + 2;
+    Temp   := Front^;
+    Front^ := Back^;
+    Back^  := Temp;
+  end;
+
+  fY := Height - (fY + fHeight);
+
+  ImageSize := fHeight * fWidth * (BPP div 8);
+  GetMem( Image, ImageSize );
+
+  Base := PByte( Image2 ) + fY * Width * (BPP div 8) + fX * (BPP div 8);
+  a := fWidth * (BPP div 8);
+  b := Width * (BPP div 8);
+
+  for i := 0 to fHeight-1 do
+    CopyMemory( PByte(image) + a*i, Base + b*i, a );
+
+  if ( BPP = 24 ) then
+    TFmt := GL_RGB
+  else
+    TFmt := GL_RGBA;
+    
+  Texture := CreateTexture( fWidth, fHeight, TFmt, Image );
+
+  FreeMem( Image );
+  FreeMem( Image2 );
+  
+  if Fmt <> nil then Fmt^ := TFmt;
+
+  Result := True;
+end;
+
+function LoadTexture( Filename: String; var Texture: GLuint;
+                      var pWidth, pHeight: Word; Fmt: PWord = nil ): Boolean;
+var
+  TGAHeader:     TTGAHeader;
+  TGAFile:       File;
+  bytesRead:     Integer;
+  image:         Pointer;
+  Width, Height: Integer;
+  ImageSize:     Integer;
+  i:             Integer;
+  Front:         ^Byte;
+  Back:          ^Byte;
+  Temp:          Byte;
+  BPP:           Byte;
+  TFmt:          Word;
+
+begin
+  Result := False;
+  pWidth := 0;
+  pHeight := 0;
+
+  if not FileExists(Filename) then
+  begin
+    e_WriteLog('Texture ' + Filename + ' not found', MSG_WARNING);
+    Exit;
+  end;
+
+  AssignFile( TGAFile, Filename );
+  Reset( TGAFile, 1 );
+  BlockRead( TGAFile, TGAHeader, SizeOf(TGAHeader) );
+
+  if ( TGAHeader.ImageType <> 2 ) then
+  begin
+    CloseFile( TGAFile );
+    e_WriteLog( 'Error loading texture: Bad ImageType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.ColorMapType <> 0 ) then
+  begin
+    CloseFile( TGAFile );
+    e_WriteLog( 'Error loading texture: Bad ColorMapType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.BPP < 24 ) then
+  begin
+    CloseFile( TGAFile );
+    e_WriteLog( 'Error loading texture: BPP less than 24', MSG_WARNING );
+    Exit;
+  end;
+
+  Width  := TGAHeader.Width[0]  + TGAHeader.Width[1]  * 256;
+  Height := TGAHeader.Height[0] + TGAHeader.Height[1] * 256;
+  BPP := TGAHeader.BPP;
+
+  ImageSize := Width * Height * (BPP div 8);
+
+  GetMem( Image, ImageSize );
+
+  BlockRead( TGAFile, image^, ImageSize, bytesRead );
+  if ( bytesRead <> ImageSize ) then
+  begin
+    CloseFile( TGAFile );
+    Exit;
+  end;
+
+  CloseFile( TGAFile );
+
+  for i := 0 to Width * Height - 1 do
+  begin
+    Front := PByte(Image) + i * (BPP div 8);
+    Back  := PByte(Image) + i * (BPP div 8) + 2;
+    Temp   := Front^;
+    Front^ := Back^;
+    Back^  := Temp;
+  end;
+
+  if ( BPP = 24 ) then
+    TFmt := GL_RGB
+  else
+    TFmt := GL_RGBA;
+    
+  Texture := CreateTexture( Width, Height, TFmt, Image );
+
+  FreeMem( Image );
+  
+  if Fmt <> nil then Fmt^ := TFmt;
+
+  pWidth := Width;
+  pHeight := Height;
+
+  Result := True;
+end;
+
+function LoadTextureEx( Filename: String; var Texture: GLuint;
+                        fX, fY, fWidth, fHeight: Word; Fmt: PWord = nil ): Boolean;
+var
+  TGAHeader:     TTGAHeader;
+  TGAFile:       File;
+  image, image2: Pointer;
+  Width, Height: Integer;
+  ImageSize:     Integer;
+  i:             Integer;
+  Front:         ^Byte;
+  Back:          ^Byte;
+  Temp:          Byte;
+  BPP:           Byte;
+  Base:          PByte;
+  TFmt:          Word;
+  
+begin
+  Result := False;
+
+  if not FileExists(Filename) then
+  begin
+    e_WriteLog( 'Texture ' + Filename + ' not found', MSG_WARNING );
+    Exit;
+  end;
+
+  AssignFile( TGAFile, Filename );
+  Reset( TGAFile, 1 );
+  BlockRead( TGAFile, TGAHeader, SizeOf(TGAHeader) );
+
+  if ( TGAHeader.ImageType <> 2 ) then
+  begin
+    CloseFile( TGAFile );
+    e_WriteLog( 'Error loading texture: Bad ImageType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.ColorMapType <> 0 ) then
+  begin
+    CloseFile( TGAFile );
+    e_WriteLog( 'Error loading texture: Bad ColorMapType', MSG_WARNING );
+    Exit;
+  end;
+
+  if ( TGAHeader.BPP < 24 ) then
+  begin
+    CloseFile( TGAFile );
+    e_WriteLog( 'Error loading texture: BPP less than 24', MSG_WARNING );
+    Exit;
+  end;
+
+  Width  := TGAHeader.Width[0]  + TGAHeader.Width[1]  * 256;
+  Height := TGAHeader.Height[0] + TGAHeader.Height[1] * 256;
+  BPP := TGAHeader.BPP;
+
+  if fX > Width then Exit;
+  if fY > Height then Exit;
+  if fX+fWidth > Width then Exit;
+  if fY+fHeight > Height then Exit;
+
+  ImageSize := Width * Height * (BPP div 8);
+  GetMem( Image2, ImageSize );
+  BlockRead( TGAFile, Image2^, ImageSize );
+
+  CloseFile( TGAFile );
+
+  for i := 0 to Width * Height - 1 do
+  begin
+    Front := PByte(Image2) + i * (BPP div 8);
+    Back  := PByte(Image2) + i * (BPP div 8) + 2;
+    Temp   := Front^;
+    Front^ := Back^;
+    Back^  := Temp;
+  end;
+
+  fY := Height - (fY + fHeight);
+
+  ImageSize := fHeight * fWidth * (BPP div 8);
+  GetMem( Image, ImageSize );
+
+  Base := PByte(Image2) + fY * Width * (BPP div 8) + fX * (BPP div 8);
+
+  for i := 0 to fHeight-1 do
+  begin
+    CopyMemory( PByte(image) + fWidth * (BPP div 8) * i,
+                Base + Width * (BPP div 8) * i, fWidth * (BPP div 8) );
+  end;
+
+  if ( BPP = 24 ) then
+    TFmt := GL_RGB
+  else
+    TFmt := GL_RGBA;
+
+  Texture := CreateTexture( fWidth, fHeight, TFmt, Image );
+
+  FreeMem( Image );
+  FreeMem( Image2 );
+
+  if Fmt <> nil then Fmt^ := TFmt;
+
+  Result := True;
+end;
+
+end.
+
diff --git a/src/game/CustomRes.rc b/src/game/CustomRes.rc
new file mode 100644 (file)
index 0000000..ddff531
--- /dev/null
@@ -0,0 +1 @@
+dficon ICON    "Icon.ico"
diff --git a/src/game/CustomRes.res b/src/game/CustomRes.res
new file mode 100644 (file)
index 0000000..f5fbac3
Binary files /dev/null and b/src/game/CustomRes.res differ
diff --git a/src/game/Doom2DF.dpr b/src/game/Doom2DF.dpr
new file mode 100644 (file)
index 0000000..e2dacc1
--- /dev/null
@@ -0,0 +1,74 @@
+program Doom2DF;
+{$APPTYPE GUI}
+{$HINTS OFF}
+
+uses
+  GL,
+  GLExt,
+  ENet in '../lib/enet/enet.pp',
+  ENetTypes in '../lib/enet/enettypes.pp',
+  ENetList in '../lib/enet/enetlist.pp',
+  ENetTime in '../lib/enet/enettime.pp',
+  ENetProtocol in '../lib/enet/enetprotocol.pp',
+  ENetCallbacks in '../lib/enet/enetcallbacks.pp',
+  ENetPlatform in '../lib/enet/enetplatform.pp',
+  e_graphics in '../engine/e_graphics.pas',
+  e_input in '../engine/e_input.pas',
+  e_log in '../engine/e_log.pas',
+  e_sound in '../engine/e_sound.pas',
+  e_textures in '../engine/e_textures.pas',
+  e_fixedbuffer in '../engine/e_fixedbuffer.pas',
+  WADEDITOR in '../shared/WADEDITOR.pas',
+  WADSTRUCT in '../shared/WADSTRUCT.pas',
+  MAPSTRUCT in '../shared/MAPSTRUCT.pas',
+  MAPREADER in '../shared/MAPREADER.pas',
+  MAPDEF in '../shared/MAPDEF.pas',
+  CONFIG in '../shared/CONFIG.pas',
+  g_basic in 'g_basic.pas',
+  g_console in 'g_console.pas',
+  g_net in 'g_net.pas',
+  g_netmsg in 'g_netmsg.pas',
+  g_nethandler in 'g_nethandler.pas',
+  g_netmaster in 'g_netmaster.pas',
+  g_res_downloader in 'g_res_downloader.pas',
+  g_game in 'g_game.pas',
+  g_gfx in 'g_gfx.pas',
+  g_gui in 'g_gui.pas',
+  g_items in 'g_items.pas',
+  g_main in 'g_main.pas',
+  g_map in 'g_map.pas',
+  g_menu in 'g_menu.pas',
+  g_monsters in 'g_monsters.pas',
+  g_options in 'g_options.pas',
+  g_phys in 'g_phys.pas',
+  g_player in 'g_player.pas',
+  g_playermodel in 'g_playermodel.pas',
+  g_saveload in 'g_saveload.pas',
+  g_sound in 'g_sound.pas',
+  g_textures in 'g_textures.pas',
+  g_triggers in 'g_triggers.pas',
+  g_weapons in 'g_weapons.pas',
+  g_window in 'g_window.pas',
+  sysutils,
+  fmod in '../lib/FMOD/fmod.pas',
+  fmoderrors in '../lib/FMOD/fmoderrors.pas',
+  fmodpresets in '../lib/FMOD/fmodpresets.pas',
+  fmodtypes in '../lib/FMOD/fmodtypes.pas',
+  BinEditor in '../shared/BinEditor.pas',
+  g_panel in 'g_panel.pas',
+  g_language in 'g_language.pas';
+
+{$R *.res}
+{$R CustomRes.res}
+
+begin
+  try
+    Main();
+    e_WriteLog('Shutdown with no errors.', MSG_NOTIFY);
+  except
+    on E: Exception do
+      e_WriteLog(Format(_lc[I_SYSTEM_ERROR_MSG], [E.Message]), MSG_FATALERROR);
+    else
+      e_WriteLog(Format(_lc[I_SYSTEM_ERROR_UNKNOWN], [LongWord(ExceptAddr())]), MSG_FATALERROR);
+  end;
+end.
diff --git a/src/game/Doom2DF.rc b/src/game/Doom2DF.rc
new file mode 100644 (file)
index 0000000..0473fb6
--- /dev/null
@@ -0,0 +1,24 @@
+APPVERINFO VERSIONINFO
+FILEVERSION 0,6,6,7
+PRODUCTVERSION 0,6,6,7
+FILETYPE VFT_APP
+{
+ BLOCK "StringFileInfo"
+ {
+  BLOCK "040904E4"
+  {
+   VALUE "CompanyName",      "www.doom2d.org\0"
+   VALUE "FileDescription",  "Doom 2D: Forever\0"
+   VALUE "FileVersion",      "0.6.6.7\0"
+   VALUE "InternalName",     "Doom 2D: Forever\0"
+   VALUE "LegalCopyright",   "All rights reserved.\0"
+   VALUE "OriginalFilename", "Doom2DF.exe\0"
+   VALUE "ProductName",      "Doom 2D: Forever\0"
+   VALUE "ProductVersion",   "0.6.6.7\0"
+  }
+ }
+ BLOCK "VarFileInfo"
+ {
+  VALUE "Translation", LANG_RUSSIAN, 1251
+ }
+}
diff --git a/src/game/Doom2DF.res b/src/game/Doom2DF.res
new file mode 100644 (file)
index 0000000..497f06d
Binary files /dev/null and b/src/game/Doom2DF.res differ
diff --git a/src/game/Icon.ico b/src/game/Icon.ico
new file mode 100644 (file)
index 0000000..9b80cb6
Binary files /dev/null and b/src/game/Icon.ico differ
diff --git a/src/game/MakeRes.bat b/src/game/MakeRes.bat
new file mode 100644 (file)
index 0000000..0a910e4
--- /dev/null
@@ -0,0 +1,3 @@
+gorc.exe CustomRes.rc
+"C:\Program Files (x86)\Borland\Delphi7\Bin\brcc32.exe" Doom2DF.rc
+pause
\ No newline at end of file
diff --git a/src/game/g_basic.pas b/src/game/g_basic.pas
new file mode 100644 (file)
index 0000000..e1ce980
--- /dev/null
@@ -0,0 +1,1173 @@
+unit g_basic;
+
+interface
+
+uses
+  WADEDITOR, g_phys;
+
+const
+  GAME_VERSION  = '0.667';
+  UID_GAME    = 1;
+  UID_PLAYER  = 2;
+  UID_MONSTER = 3;
+  UID_ITEM    = 10;
+  UID_MAX_GAME    = $10;
+  UID_MAX_PLAYER  = $7FFF;
+  UID_MAX_MONSTER = $FFFF;
+
+type
+  TDirection = (D_LEFT, D_RIGHT);
+  WArray = array of Word;
+  DWArray = array of DWORD;
+  String20 = String[20];
+
+function g_CreateUID(UIDType: Byte): Word;
+function g_GetUIDType(UID: Word): Byte;
+function g_Collide(X1, Y1: Integer; Width1, Height1: Word;
+                   X2, Y2: Integer; Width2, Height2: Word): Boolean;
+function g_CollideLine(x1, y1, x2, y2, rX, rY: Integer; rWidth, rHeight: Word): Boolean;
+function g_CollidePoint(X, Y, X2, Y2: Integer; Width, Height: Word): Boolean;
+function g_CollideLevel(X, Y: Integer; Width, Height: Word): Boolean;
+function g_CollideAround(X1, Y1: Integer; Width1, Height1: Word;
+                         X2, Y2: Integer; Width2, Height2: Word): Boolean;
+function g_CollidePlayer(X, Y: Integer; Width, Height: Word): Boolean;
+function g_CollideMonster(X, Y: Integer; Width, Height: Word): Boolean;
+function g_CollideItem(X, Y: Integer; Width, Height: Word): Boolean;
+function g_PatchLength(X1, Y1, X2, Y2: Integer): Word;
+function g_TraceVector(X1, Y1, X2, Y2: Integer): Boolean;
+function g_GetAcidHit(X, Y: Integer; Width, Height: Word): Byte;
+function g_Look(a, b: PObj; d: TDirection): Boolean;
+procedure IncMax(var A: Integer; B, Max: Integer); overload;
+procedure IncMax(var A: Single; B, Max: Single); overload;
+procedure IncMax(var A: Integer; Max: Integer); overload;
+procedure IncMax(var A: Single; Max: Single); overload;
+procedure IncMax(var A: Word; B, Max: Word); overload;
+procedure IncMax(var A: Word; Max: Word); overload;
+procedure IncMax(var A: SmallInt; B, Max: SmallInt); overload;
+procedure IncMax(var A: SmallInt; Max: SmallInt); overload;
+procedure DecMin(var A: Integer; B, Min: Integer); overload;
+procedure DecMin(var A: Single; B, Min: Single); overload;
+procedure DecMin(var A: Integer; Min: Integer); overload;
+procedure DecMin(var A: Single; Min: Single); overload;
+procedure DecMin(var A: Word; B, Min: Word); overload;
+procedure DecMin(var A: Word; Min: Word); overload;
+procedure DecMin(var A: Byte; B, Min: Byte); overload;
+procedure DecMin(var A: Byte; Min: Byte); overload;
+function Sign(A: Integer): ShortInt; overload;
+function Sign(A: Single): ShortInt; overload;
+function PointToRect(X, Y, X1, Y1: Integer; Width, Height: Word): Integer;
+function GetAngle(baseX, baseY, pointX, PointY: Integer): SmallInt;
+function GetAngle2(vx, vy: Integer): SmallInt;
+function GetLines(Text: string; FontID: DWORD; MaxWidth: Word): SArray;
+procedure Sort(var a: SArray);
+function Sscanf(const s: string; const fmt: string;
+                const Pointers: array of Pointer): Integer;
+function InDWArray(a: DWORD; arr: DWArray): Boolean;
+function InWArray(a: Word; arr: WArray): Boolean;
+function InSArray(a: string; arr: SArray): Boolean;
+function GetPos(UID: Word; o: PObj): Boolean;
+function parse(s: string): SArray;
+function parse2(s: string; delim: Char): SArray;
+function g_GetFileTime(fileName: String): Integer;
+function g_SetFileTime(fileName: String; time: Integer): Boolean;
+procedure SortSArray(var S: SArray);
+function b_Text_Format(S: string): string;
+function b_Text_Unformat(S: string): string;
+
+implementation
+
+uses
+  Math, g_map, g_gfx, g_player, SysUtils, MAPDEF,
+  StrUtils, e_graphics, g_monsters, g_items;
+
+function g_PatchLength(X1, Y1, X2, Y2: Integer): Word;
+begin
+  Result := Min(Round(Hypot(Abs(X2-X1), Abs(Y2-Y1))), 65535);
+end;
+
+function g_CollideLevel(X, Y: Integer; Width, Height: Word): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if gWalls = nil then
+    Exit;
+
+  for a := 0 to High(gWalls) do
+    if gWalls[a].Enabled and
+       not ( ((Y + Height <= gWalls[a].Y) or
+              (Y          >= gWalls[a].Y + gWalls[a].Height)) or
+             ((X + Width  <= gWalls[a].X) or
+              (X          >= gWalls[a].X + gWalls[a].Width)) ) then
+    begin
+      Result := True;
+      Exit;
+    end;
+end;
+
+function g_CollidePlayer(X, Y: Integer; Width, Height: Word): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if gPlayers = nil then Exit;
+
+  for a := 0 to High(gPlayers) do
+    if (gPlayers[a] <> nil) and gPlayers[a].Live then
+      if gPlayers[a].Collide(X, Y, Width, Height) then
+      begin
+        Result := True;
+        Exit;
+      end;
+end;
+
+function g_CollideMonster(X, Y: Integer; Width, Height: Word): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if gMonsters = nil then Exit;
+
+  for a := 0 to High(gMonsters) do
+    if (gMonsters[a] <> nil) and gMonsters[a].Live then
+      if g_Obj_Collide(X, Y, Width, Height, @gMonsters[a].Obj) then
+      begin
+        Result := True;
+        Exit;
+      end;
+end;
+
+function g_CollideItem(X, Y: Integer; Width, Height: Word): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if gItems = nil then
+    Exit;
+
+  for a := 0 to High(gItems) do
+    if gItems[a].Live then
+      if g_Obj_Collide(X, Y, Width, Height, @gItems[a].Obj) then
+        begin
+          Result := True;
+          Exit;
+        end;
+end;
+
+function g_TraceVector(X1, Y1, X2, Y2: Integer): Boolean;
+var
+  i: Integer;
+  dx, dy: Integer;
+  Xerr, Yerr, d: LongWord;
+  incX, incY: Integer;
+  x, y: Integer;
+begin
+  Result := False;
+
+  Assert(gCollideMap <> nil, 'g_TraceVector: gCollideMap = nil');
+
+  Xerr := 0;
+  Yerr := 0;
+  dx := X2-X1;
+  dy := Y2-Y1;
+
+  if dx > 0 then incX := 1 else if dx < 0 then incX := -1 else incX := 0;
+  if dy > 0 then incY := 1 else if dy < 0 then incY := -1 else incY := 0;
+
+  dx := abs(dx);
+  dy := abs(dy);
+
+  if dx > dy then d := dx else d := dy;
+
+  x := X1;
+  y := Y1;
+
+  for i := 1 to d do
+  begin
+    Inc(Xerr, dx);
+    Inc(Yerr, dy);
+    if Xerr>d then
+    begin
+      Dec(Xerr, d);
+      Inc(x, incX);
+    end;
+    if Yerr > d then
+    begin
+      Dec(Yerr, d);
+      Inc(y, incY);
+    end;
+
+    if (y > gMapInfo.Height-1) or
+    (y < 0) or (x > gMapInfo.Width-1) or (x < 0) then
+      Exit;
+    if ByteBool(gCollideMap[y, x] and MARK_BLOCKED) then
+      Exit;
+  end;
+
+  Result := True;
+end;
+
+function g_CreateUID(UIDType: Byte): Word;
+var
+  ok: Boolean;
+  i: Integer;
+begin
+  Result := $0;
+
+  case UIDType of
+    UID_PLAYER:
+    begin
+      repeat
+        Result := UID_MAX_GAME+$1+Random(UID_MAX_PLAYER-UID_MAX_GAME+$1);
+
+        ok := True;
+        if gPlayers <> nil then
+          for i := 0 to High(gPlayers) do
+            if gPlayers[i] <> nil then
+              if Result = gPlayers[i].UID then
+              begin
+                ok := False;
+                Break;
+              end;
+      until ok;
+    end;
+
+    UID_MONSTER:
+    begin
+      repeat
+        Result := UID_MAX_PLAYER+$1+Random(UID_MAX_MONSTER-UID_MAX_GAME-UID_MAX_PLAYER+$1);
+
+        ok := True;
+        if gMonsters <> nil then
+          for i := 0 to High(gMonsters) do
+            if gMonsters[i] <> nil then
+              if Result = gMonsters[i].UID then
+              begin
+                ok := False;
+                Break;
+              end;
+      until ok;
+    end;
+  end;
+end;
+
+function g_GetUIDType(UID: Word): Byte;
+begin
+  if UID <= UID_MAX_GAME then
+    Result := UID_GAME
+  else
+    if UID <= UID_MAX_PLAYER then
+      Result := UID_PLAYER
+    else
+      Result := UID_MONSTER;
+end;
+
+function g_Collide(X1, Y1: Integer; Width1, Height1: Word;
+                   X2, Y2: Integer; Width2, Height2: Word): Boolean;
+begin
+  Result := not ( ((Y1 + Height1 <= Y2) or
+                   (Y2 + Height2 <= Y1)) or
+                  ((X1 + Width1  <= X2) or
+                   (X2 + Width2  <= X1)) );
+end;
+
+function g_CollideAround(X1, Y1: Integer; Width1, Height1: Word;
+                         X2, Y2: Integer; Width2, Height2: Word): Boolean;
+begin
+  Result := g_Collide(X1, Y1, Width1, Height1, X2, Y2, Width2, Height2) or
+            g_Collide(X1+1, Y1, Width1, Height1, X2, Y2, Width2, Height2) or
+            g_Collide(X1-1, Y1, Width1, Height1, X2, Y2, Width2, Height2) or
+            g_Collide(X1, Y1+1, Width1, Height1, X2, Y2, Width2, Height2) or
+            g_Collide(X1, Y1-1, Width1, Height1, X2, Y2, Width2, Height2);
+end;
+
+function c(X1, Y1, Width1, Height1, X2, Y2, Width2, Height2: Integer): Boolean;
+begin
+  Result := not (((Y1 + Height1 <= Y2) or
+                  (Y1           >= Y2 + Height2)) or
+                  ((X1 + Width1 <= X2) or
+                   (X1          >= X2 + Width2)));
+end;
+
+function g_Collide2(X1, Y1, X2, Y2, X3, Y3, X4, Y4: Integer): Boolean;
+begin
+  //Result :=  not (((Y2 <= Y3) or (Y1  >= Y4)) or ((X2 <= X3) or (X1  >= X4)));
+  Result := c(X1, Y1, X2-X1, Y2-Y1, X3, Y3, X4-X3, Y4-Y3);
+end;
+
+function g_CollidePoint(X, Y, X2, Y2: Integer; Width, Height: Word): Boolean;
+begin
+  X := X-X2;
+  Y := Y-Y2;
+  Result := (x >= 0) and (x <= Width) and
+            (y >= 0) and (y <= Height);
+end;
+
+procedure IncMax(var A: Integer; B, Max: Integer);
+begin
+  if A+B > Max then A := Max else A := A+B;
+end;
+
+procedure IncMax(var A: Single; B, Max: Single);
+begin
+  if A+B > Max then A := Max else A := A+B;
+end;
+
+procedure DecMin(var A: Integer; B, Min: Integer);
+begin
+  if A-B < Min then A := Min else A := A-B;
+end;
+
+procedure DecMin(var A: Word; B, Min: Word);
+begin
+  if A-B < Min then A := Min else A := A-B;
+end;
+
+procedure DecMin(var A: Single; B, Min: Single);
+begin
+  if A-B < Min then A := Min else A := A-B;
+end;
+
+procedure IncMax(var A: Integer; Max: Integer);
+begin
+  if A+1 > Max then A := Max else A := A+1;
+end;
+
+procedure IncMax(var A: Single; Max: Single);
+begin
+  if A+1 > Max then A := Max else A := A+1;
+end;
+
+procedure IncMax(var A: Word; B, Max: Word);
+begin
+  if A+B > Max then A := Max else A := A+B;
+end;
+
+procedure IncMax(var A: Word; Max: Word);
+begin
+  if A+1 > Max then A := Max else A := A+1;
+end;
+
+procedure IncMax(var A: SmallInt; B, Max: SmallInt);
+begin
+  if A+B > Max then A := Max else A := A+B;
+end;
+
+procedure IncMax(var A: SmallInt; Max: SmallInt);
+begin
+  if A+1 > Max then A := Max else A := A+1;
+end;
+
+procedure DecMin(var A: Integer; Min: Integer);
+begin
+  if A-1 < Min then A := Min else A := A-1;
+end;
+
+procedure DecMin(var A: Single; Min: Single);
+begin
+  if A-1 < Min then A := Min else A := A-1;
+end;
+
+procedure DecMin(var A: Word; Min: Word);
+begin
+  if A-1 < Min then A := Min else A := A-1;
+end;
+
+procedure DecMin(var A: Byte; B, Min: Byte);
+begin
+  if A-B < Min then A := Min else A := A-B;
+end;
+
+procedure DecMin(var A: Byte; Min: Byte); overload;
+begin
+  if A-1 < Min then A := Min else A := A-1;
+end;
+
+function Sign(A: Integer): ShortInt;
+begin
+  if A < 0 then Result := -1
+    else if A > 0 then Result := 1
+      else Result := 0;
+end;
+
+function Sign(A: Single): ShortInt;
+const
+  Eps = 1.0E-5;
+begin
+  if Abs(A) < Eps then Result := 0
+    else if A < 0 then Result := -1
+      else Result := 1;
+end;
+
+function PointToRect(X, Y, X1, Y1: Integer; Width, Height: Word): Integer;
+begin
+  X := X-X1;            // A(0;0) --- B(W;0)
+  Y := Y-Y1;            // |          |
+                        // D(0;H) --- C(W;H)
+  if X < 0 then
+    begin // Ñëåâà
+      if Y < 0 then // Ñëåâà ñâåðõó: ðàññòîÿíèå äî A
+        Result := Round(Hypot(X, Y))
+      else
+        if Y > Height then // Ñëåâà ñíèçó: ðàññòîÿíèå äî D
+          Result := Round(Hypot(X, Y-Height))
+        else // Ñëåâà ïîñåðåäèíå: ðàññòîÿíèå äî AD
+          Result := -X;
+    end
+  else
+    if X > Width then
+      begin // Ñïðàâà
+        X := X-Width;
+        if y < 0 then // Ñïðàâà ñâåðõó: ðàññòîÿíèå äî B
+          Result := Round(Hypot(X, Y))
+        else
+          if Y > Height then // Ñïðàâà ñíèçó: ðàññòîÿíèå äî C
+            Result := Round(Hypot(X, Y-Height))
+          else // Ñïðàâà ïîñåðåäèíå: ðàññòîÿíèå äî BC
+            Result := X;
+      end
+    else // Ïîñåðåäèíå
+      begin
+        if Y < 0 then // Ïîñåðåäèíå ñâåðõó: ðàññòîÿíèå äî AB
+          Result := -Y
+        else
+          if Y > Height then // Ïîñåðåäèíå ñíèçó: ðàññòîÿíèå äî DC
+            Result := Y-Height
+          else // Âíóòðè ïðÿìîóãîëüíèêà: ðàññòîÿíèå 0
+            Result := 0;
+      end;
+end;
+
+function g_GetAcidHit(X, Y: Integer; Width, Height: Word): Byte;
+const
+  tab: array[0..3] of Byte = (0, 5, 10, 20);
+var
+  a: Byte;
+begin
+  a := 0;
+
+  if g_Map_CollidePanel(X, Y, Width, Height, PANEL_ACID1, False) then a := a or 1;
+  if g_Map_CollidePanel(X, Y, Width, Height, PANEL_ACID2, False) then a := a or 2;
+
+  Result := tab[a];
+end;
+
+function g_Look(a, b: PObj; d: TDirection): Boolean;
+begin
+  if ((b^.X > a^.X) and (d = D_LEFT)) or
+     ((b^.X < a^.X) and (d = D_RIGHT)) then
+  begin
+    Result := False;
+    Exit;
+  end;
+
+  Result := g_TraceVector(a^.X+a^.Rect.X+(a^.Rect.Width div 2),
+                          a^.Y+a^.Rect.Y+(a^.Rect.Height div 2),
+                          b^.X+b^.Rect.X+(b^.Rect.Width div 2),
+                          b^.Y+b^.Rect.Y+(b^.Rect.Height div 2));
+end;
+
+function GetAngle(baseX, baseY, pointX, PointY: Integer): SmallInt;
+var
+  c: Single;
+  a, b: Integer;
+begin
+  a := abs(pointX-baseX);
+  b := abs(pointY-baseY);
+
+  if a = 0 then c := 90
+    else c := RadToDeg(ArcTan(b/a));
+
+  if pointY < baseY then c := -c;
+  if pointX > baseX then c := 180-c;
+
+  Result := Round(c);
+end;
+
+function GetAngle2(vx, vy: Integer): SmallInt;
+var
+  c: Single;
+  a, b: Integer;
+begin
+  a := abs(vx);
+  b := abs(vy);
+
+  if a = 0 then 
+    c := 0
+  else 
+    c := RadToDeg(ArcTan(b/a));
+
+  if vy < 0 then 
+    c := -c;
+  if vx > 0 then 
+    c := 180 - c;
+
+  c := c + 180;
+
+  Result := Round(c);
+end;
+
+{function g_CollideLine(x1, y1, x2, y2, rX, rY: Integer; rWidth, rHeight: Word): Boolean;
+const
+  table: array[0..8, 0..8] of Byte =
+         ((0, 0, 3, 3, 1, 2, 2, 0, 1),
+          (0, 0, 0, 0, 4, 7, 2, 0, 1),
+          (3, 0, 0, 0, 4, 4, 1, 3, 1),
+          (3, 0, 0, 0, 0, 0, 5, 6, 1),
+          (1, 4, 4, 0, 0, 0, 5, 5, 1),
+          (2, 7, 4, 0, 0, 0, 0, 0, 1),
+          (2, 2, 1, 5, 5, 0, 0, 0, 1),
+          (0, 0, 3, 6, 5, 0, 0, 0, 1),
+          (1, 1, 1, 1, 1, 1, 1, 1, 1));
+
+function GetClass(x, y: Integer): Byte;
+begin
+ if y < rY then
+ begin
+  if x < rX then Result := 7
+   else if x < rX+rWidth then Result := 0
+    else Result := 1;
+ end
+  else if y < rY+rHeight then
+ begin
+  if x < rX then Result := 6
+   else if x < rX+rWidth then Result := 8
+    else Result := 2;
+ end
+  else
+ begin
+  if x < rX then Result := 5
+   else if x < rX+rWidth then Result := 4
+    else Result := 3;
+ end;
+end;
+
+begin
+ case table[GetClass(x1, y1), GetClass(x2, y2)] of
+  0: Result := False;
+  1: Result := True;
+  2: Result := Abs((rY-y1))/Abs((rX-x1)) <= Abs((y2-y1))/Abs((x2-x1));
+  3: Result := Abs((rY-y1))/Abs((rX+rWidth-x1)) <= Abs((y2-y1))/Abs((x2-x1));
+  4: Result := Abs((rY+rHeight-y1))/Abs((rX+rWidth-x1)) >= Abs((y2-y1))/Abs((x2-x1));
+  5: Result := Abs((rY+rHeight-y1))/Abs((rX-x1)) >= Abs((y2-y1))/Abs((x2-x1));
+  6: Result := (Abs((rY-y1))/Abs((rX+rWidth-x1)) <= Abs((y2-y1))/Abs((x2-x1))) and
+               (Abs((rY+rHeight-y1))/Abs((rX-x1)) >= Abs((y2-y1))/Abs((x2-x1)));
+  7: Result := (Abs((rY+rHeight-y1))/Abs((rX+rWidth-x1)) >= Abs((y2-y1))/Abs((x2-x1))) and
+               (Abs((rY-y1))/Abs((rX-x1)) <= Abs((y2-y1))/Abs((x2-x1)));
+  else Result := False;
+ end;
+end;}
+
+function g_CollideLine(x1, y1, x2, y2, rX, rY: Integer; rWidth, rHeight: Word): Boolean;
+var
+  i: Integer;
+  dx, dy: Integer;
+  Xerr, Yerr: Integer;
+  incX, incY: Integer;
+  x, y, d: Integer;
+begin
+  Result := True;
+
+  Xerr := 0;
+  Yerr := 0;
+  dx := X2-X1;
+  dy := Y2-Y1;
+
+  if dx > 0 then incX := 1 else if dx < 0 then incX := -1 else incX := 0;
+  if dy > 0 then incY := 1 else if dy < 0 then incY := -1 else incY := 0;
+
+  dx := abs(dx);
+  dy := abs(dy);
+
+  if dx > dy then d := dx else d := dy;
+
+  x := X1;
+  y := Y1;
+
+  for i := 1 to d+1 do
+  begin
+    Inc(Xerr, dx);
+    Inc(Yerr, dy);
+    if Xerr > d then
+    begin
+      Dec(Xerr, d);
+      Inc(x, incX);
+    end;
+    if Yerr > d then
+    begin
+      Dec(Yerr, d);
+      Inc(y, incY);
+    end;
+
+    if (x >= rX) and (x <= (rX + rWidth - 1)) and
+       (y >= rY) and (y <= (rY + rHeight - 1)) then Exit;
+  end;
+
+  Result := False;
+end;
+
+function GetStr(var Str: string): string;
+var
+  a: Integer;
+begin
+  for a := 1 to Length(Str) do
+    if (a = Length(Str)) or (Str[a+1] = ' ') then
+    begin
+      Result := Copy(Str, 1, a);
+      Delete(Str, 1, a+1);
+      Str := Trim(Str);
+      Exit;
+    end;
+end;
+
+{function GetLines(Text: string; MaxChars: Word): SArray;
+var
+  a: Integer;
+  b: array of string;
+  str: string;
+begin
+ Text := Trim(Text);
+
+ while Pos('  ', Text) <> 0 do Text := AnsiReplaceStr(Text, '  ', ' ');
+
+ while Text <> '' do
+ begin
+  SetLength(b, Length(b)+1);
+  b[High(b)] := GetStr(Text);
+ end;
+
+ a := 0;
+ while True do
+ begin
+  if a > High(b) then Break;
+
+  str := b[a];
+  a := a+1;
+
+  if Length(str) >= MaxChars then
+  begin
+   while str <> '' do
+   begin
+    SetLength(Result, Length(Result)+1);
+    Result[High(Result)] := Copy(str, 1, MaxChars);
+    Delete(str, 1, MaxChars);
+   end;
+
+   Continue;
+  end;
+
+  while (a <= High(b)) and (Length(str+' '+b[a]) <= MaxChars) do
+  begin
+   str := str+' '+b[a];
+   a := a+1;
+  end;
+
+  SetLength(Result, Length(Result)+1);
+  Result[High(Result)] := str;
+ end;
+end;}
+
+function GetLines(Text: string; FontID: DWORD; MaxWidth: Word): SArray;
+
+function TextLen(Text: string): Word;
+var
+  h: Word;
+begin
+  e_CharFont_GetSize(FontID, Text, Result, h);
+end;
+
+var
+  a, c: Integer;
+  b: array of string;
+  str: string;
+begin
+  SetLength(Result, 0);
+  SetLength(b, 0);
+
+  Text := Trim(Text);
+
+// Óäàëÿåì ìíîæåñòâåííûå ïðîáåëû:
+  while Pos('  ', Text) <> 0 do
+    Text := AnsiReplaceStr(Text, '  ', ' ');
+
+  while Text <> '' do
+  begin
+    SetLength(b, Length(b)+1);
+    b[High(b)] := GetStr(Text);
+  end;
+
+  a := 0;
+  while True do
+  begin
+    if a > High(b) then
+      Break;
+
+    str := b[a];
+    a := a+1;
+
+    if TextLen(str) > MaxWidth then
+    begin // Òåêóùàÿ ñòðîêà ñëèøêîì äëèííàÿ => ðàçáèâàåì
+      while str <> '' do
+      begin
+        SetLength(Result, Length(Result)+1);
+
+        c := 0;
+        while (c < Length(str)) and
+              (TextLen(Copy(str, 1, c+1)) < MaxWidth) do
+          c := c+1;
+
+        Result[High(Result)] := Copy(str, 1, c);
+        Delete(str, 1, c);
+      end;
+    end
+    else // Ñòðîêà íîðìàëüíîé äëèíû => ñîåäèíÿåì ñî ñëåäóþùèìè
+    begin
+      while (a <= High(b)) and
+            (TextLen(str+' '+b[a]) < MaxWidth) do
+      begin
+        str := str+' '+b[a];
+        a := a + 1;
+      end;
+
+      SetLength(Result, Length(Result)+1);
+      Result[High(Result)] := str;
+    end;
+  end;
+end;
+
+procedure Sort(var a: SArray);
+var
+  i, j: Integer;
+  s: string;
+begin
+  if a = nil then Exit;
+
+  for i := High(a) downto Low(a) do
+    for j := Low(a) to High(a)-1 do
+      if LowerCase(a[j]) > LowerCase(a[j+1]) then
+      begin
+        s := a[j];
+        a[j] := a[j+1];
+        a[j+1] := s;
+      end;
+end;
+
+function Sscanf(const s: String; const fmt: String;
+                const Pointers: array of Pointer): Integer;
+var
+  i, j, n, m: Integer;
+  s1: ShortString;
+  L: LongInt;
+  X: Extended;
+
+  function GetInt(): Integer;
+  begin
+    s1 := '';
+    while (n <= Length(s)) and (s[n] = ' ') do
+      Inc(n);
+
+    while (n <= Length(s)) and (s[n] in ['0'..'9', '+', '-']) do
+    begin
+      s1 := s1 + s[n];
+      Inc(n);
+    end;
+
+    Result := Length(s1);
+  end;
+
+  function GetFloat(): Integer;
+  begin
+    s1 := '';
+    while (n <= Length(s)) and (s[n] = ' ') do
+      Inc(n);
+
+    while (n <= Length(s)) and //jd >= rather than >
+          (s[n] in ['0'..'9', '+', '-', '.', 'e', 'E']) do
+    begin
+      s1 := s1 + s[n];
+      Inc(n);
+    end;
+
+    Result := Length(s1);
+  end;
+
+  function GetString(): Integer;
+  begin
+    s1 := '';
+    while (n <= Length(s)) and (s[n] = ' ') do
+      Inc(n);
+
+    while (n <= Length(s)) and (s[n] <> ' ') do
+    begin
+      s1 := s1 + s[n];
+      Inc(n);
+    end;
+
+    Result := Length(s1);
+  end;
+
+  function ScanStr(c: Char): Boolean;
+  begin
+    while (n <= Length(s)) and (s[n] <> c) do
+      Inc(n);
+    Inc(n);
+
+    Result := (n <= Length(s));
+  end;
+
+  function GetFmt(): Integer;
+  begin
+    Result := -1;
+
+    while (True) do
+    begin
+      while (fmt[m] = ' ') and (m < Length(fmt)) do
+        Inc(m);
+      if (m >= Length(fmt)) then 
+        Break;
+
+      if (fmt[m] = '%') then
+      begin
+        Inc(m);
+        case fmt[m] of
+          'd': Result := vtInteger;
+          'f': Result := vtExtended;
+          's': Result := vtString;
+        end;
+        Inc(m);
+        Break;
+      end;
+
+      if (not ScanStr(fmt[m])) then
+        Break;
+      Inc(m);
+    end;
+  end;
+
+begin
+  n := 1;
+  m := 1;
+  Result := 0;
+
+  for i := 0 to High(Pointers) do
+  begin
+    j := GetFmt();
+
+    case j of
+      vtInteger :
+      begin
+        if GetInt() > 0 then
+          begin
+            L := StrToIntDef(s1, 0);
+            Move(L, Pointers[i]^, SizeOf(LongInt));
+            Inc(Result);
+          end
+        else
+          Break;
+      end;
+
+      vtExtended :
+      begin
+        if GetFloat() > 0 then
+          begin
+            X := StrToFloatDef(s1, 0.0);
+            Move(X, Pointers[i]^, SizeOf(Extended));
+            Inc(Result);
+          end
+        else
+          Break;
+      end;
+
+      vtString :
+      begin
+        if GetString() > 0 then
+          begin
+            Move(s1, Pointers[i]^, Length(s1)+1);
+            Inc(Result);
+          end
+        else
+          Break;
+      end;
+      
+      else {case}
+        Break;
+    end; {case}
+  end;
+end;
+
+function InDWArray(a: DWORD; arr: DWArray): Boolean;
+var
+  b: Integer;
+begin
+  Result := False;
+
+  if arr = nil then Exit;
+
+  for b := 0 to High(arr) do
+    if arr[b] = a then
+    begin
+      Result := True;
+      Exit;
+    end;
+end;
+
+function InWArray(a: Word; arr: WArray): Boolean;
+var
+  b: Integer;
+begin
+  Result := False;
+
+  if arr = nil then Exit;
+
+  for b := 0 to High(arr) do
+    if arr[b] = a then
+    begin
+      Result := True;
+      Exit;
+    end;
+end;
+
+function InSArray(a: string; arr: SArray): Boolean;
+var
+  b: Integer;
+begin
+  Result := False;
+
+  if arr = nil then Exit;
+
+  a := AnsiLowerCase(a);
+
+  for b := 0 to High(arr) do
+    if AnsiLowerCase(arr[b]) = a then
+    begin
+      Result := True;
+      Exit;
+    end;
+end;
+
+function GetPos(UID: Word; o: PObj): Boolean;
+var
+  p: TPlayer;
+  m: TMonster;
+begin
+  Result := False;
+
+  case g_GetUIDType(UID) of
+    UID_PLAYER:
+    begin
+      p := g_Player_Get(UID);
+      if p = nil then Exit;
+      if not p.Live then Exit;
+
+      o^ := p.Obj;
+    end;
+  
+    UID_MONSTER:
+    begin
+      m := g_Monsters_Get(UID);
+      if m = nil then Exit;
+      if not m.Live then Exit;
+
+      o^ := m.Obj;
+    end;
+    else Exit;
+  end;
+
+  Result := True;
+end;
+
+function parse(s: String): SArray;
+var
+  a: Integer;
+begin
+  Result := nil;
+  if s = '' then
+    Exit;
+
+  while s <> '' do
+  begin
+    for a := 1 to Length(s) do
+      if (s[a] = ',') or (a = Length(s)) then
+      begin
+        SetLength(Result, Length(Result)+1);
+
+        if s[a] = ',' then
+          Result[High(Result)] := Copy(s, 1, a-1)
+        else // Êîíåö ñòðîêè
+          Result[High(Result)] := s;
+
+        Delete(s, 1, a);
+        Break;
+      end;
+  end;
+end;
+
+function parse2(s: string; delim: Char): SArray;
+var
+  a: Integer;
+begin
+  Result := nil;
+  if s = '' then Exit;
+
+  while s <> '' do
+  begin
+    for a := 1 to Length(s) do
+      if (s[a] = delim) or (a = Length(s)) then
+      begin
+        SetLength(Result, Length(Result)+1);
+
+        if s[a] = delim then Result[High(Result)] := Copy(s, 1, a-1)
+        else Result[High(Result)] := s;
+
+        Delete(s, 1, a);
+        Break;
+      end;
+  end;
+end;
+
+function g_GetFileTime(fileName: String): Integer;
+var
+  F: File;
+begin
+  if not FileExists(fileName) then
+  begin
+    Result := -1;
+    Exit;
+  end;
+
+  AssignFile(F, fileName);
+  Reset(F);
+  Result := FileGetDate(TFileRec(F).Handle);
+  CloseFile(F);
+end;
+
+function g_SetFileTime(fileName: String; time: Integer): Boolean;
+var
+  F: File;
+begin
+  if (not FileExists(fileName)) or (time < 0) then
+  begin
+    Result := False;
+    Exit;
+  end;
+
+  AssignFile(F, fileName);
+  Reset(F);
+  Result := (FileSetDate(TFileRec(F).Handle, time) = 0);
+  CloseFile(F);
+end;
+
+procedure SortSArray(var S: SArray);
+var
+  b: Boolean;
+  i: Integer;
+  sw: ShortString;
+begin
+  repeat
+    b := False;
+    for i := Low(S) to High(S) - 1 do
+      if S[i] > S[i + 1] then begin
+        sw := S[i];
+        S[i] := S[i + 1];
+        S[i + 1] := sw;
+        b := True;
+      end;
+  until not b;
+end;
+
+function b_Text_Format(S: string): string;
+var
+  Spec, Rst: Boolean;
+  I: Integer;
+begin
+  Result := '';
+  Spec := False;
+  Rst := False;
+  for I := 1 to Length(S) do
+  begin
+    if (not Spec) and (S[I] = '\') and (I + 1 <= Length(S)) then
+    begin
+      Spec := True;
+      Rst := True;
+      continue;
+    end;
+    if Spec then
+    begin
+      case S[I] of
+        'n': // line feed
+          Result := Result + #10;
+        '0': // black
+          Result := Result + #1;
+        '1': // white
+          Result := Result + #2;
+        'd': // darker
+          Result := Result + #3;
+        'l': // lighter
+          Result := Result + #4;
+        'r': // red
+          Result := Result + #18;
+        'g': // green
+          Result := Result + #19;
+        'b': // blue
+          Result := Result + #20;
+        'y': // yellow
+          Result := Result + #21;
+        '\': // escape
+          Result := Result + '\';
+        else
+          Result := Result + '\' + S[I];
+      end;
+      Spec := False;
+    end else
+      Result := Result + S[I];
+  end;
+  // reset to white at end
+  if Rst then Result := Result + #2;
+end;
+
+function b_Text_Unformat(S: string): string;
+var
+  Spec: Boolean;
+  I: Integer;
+begin
+  Result := '';
+  Spec := False;
+  for I := 1 to Length(S) do
+  begin
+    if S[I] in [#1, #2, #3, #4, #10, #18, #19, #20, #21] then
+    begin
+      Spec := False;
+      continue;
+    end;
+    if (not Spec) and (S[I] = '\') and (I + 1 <= Length(S)) then
+    begin
+      Spec := True;
+      continue;
+    end;
+    if Spec then
+    begin
+      case S[I] of
+        'n': ;
+        '0': ;
+        '1': ;
+        'd': ;
+        'l': ;
+        'r': ;
+        'g': ;
+        'b': ;
+        'y': ;
+        '\': Result := Result + '\';
+        else
+          Result := Result + '\' + S[I];
+      end;
+      Spec := False;
+    end else
+      Result := Result + S[I];
+  end;
+end;
+
+end.
diff --git a/src/game/g_console.pas b/src/game/g_console.pas
new file mode 100644 (file)
index 0000000..3c16037
--- /dev/null
@@ -0,0 +1,903 @@
+unit g_console;
+
+interface
+
+procedure g_Console_Init();
+procedure g_Console_Update();
+procedure g_Console_Draw();
+procedure g_Console_Switch();
+procedure g_Console_Char(C: Char);
+procedure g_Console_Control(K: Word);
+procedure g_Console_Process(L: String; Quiet: Boolean = False);
+procedure g_Console_Add(L: String; Show: Boolean = False);
+procedure g_Console_Clear();
+function  g_Console_CommandBlacklisted(C: String): Boolean;
+
+procedure g_Console_Chat_Switch(Team: Boolean = False);
+
+var
+  gConsoleShow: Boolean; // True - êîíñîëü îòêðûòà èëè îòêðûâàåòñÿ
+  gChatShow: Boolean;
+  gChatTeam: Boolean = False;
+  gAllowConsoleMessages: Boolean = True;
+  gChatEnter: Boolean = True;
+  gJustChatted: Boolean = False; // ÷òîáû àäìèí â èíòåðå ÷àòÿñü íå ïðîìàòûâàë ñòàòèñòèêó
+
+implementation
+
+uses
+  g_textures, g_main, e_graphics, e_input, g_game,
+  SysUtils, g_basic, g_options, WADEDITOR, Math,
+  g_menu, g_language, g_net, g_netmsg;
+
+type
+  TCmdProc = procedure (P: SArray);
+
+  TCommand = record
+    Cmd: String;
+    Proc: TCmdProc;
+  end;
+
+  TAlias = record
+    Name: String;
+    Commands: SArray;
+  end;
+
+const
+  Step = 32;
+  Alpha = 25;
+  MsgTime = 144;
+  MaxScriptRecursion = 16;
+
+  DEBUG_STRING = 'DEBUG MODE';
+
+var
+  ID: DWORD;
+  RecursionDepth: Word = 0;
+  RecursionLimitHit: Boolean = False;
+  Cons_Y: SmallInt;
+  Cons_Shown: Boolean; // Ðèñîâàòü ëè êîíñîëü?
+  Line: String;
+  CPos: Word;
+  ConsoleHistory: SArray;
+  CommandHistory: SArray;
+  Whitelist: SArray;
+  Commands: Array of TCommand;
+  Aliases: Array of TAlias;
+  CmdIndex: Word;
+  Offset: Word;
+  MsgArray: Array [0..4] of record
+                              Msg: String;
+                              Time: Word;
+                            end;
+
+function GetStrACmd(var Str: String): String;
+var
+  a: Integer;
+begin
+  for a := 1 to Length(Str) do
+    if (a = Length(Str)) or (Str[a+1] = ';') then
+    begin
+      Result := Copy(Str, 1, a);
+      Delete(Str, 1, a+1);
+      Str := Trim(Str);
+      Exit;
+    end;
+end;
+
+function ParseAlias(Str: String): SArray;
+begin
+  Result := nil;
+
+  Str := Trim(Str);
+
+  if Str = '' then
+    Exit;
+
+  while Str <> '' do
+  begin
+    SetLength(Result, Length(Result)+1);
+    Result[High(Result)] := GetStrACmd(Str);
+  end;
+end;
+
+procedure ConsoleCommands(P: SArray);
+var
+  Cmd, s: String;
+  a, b: Integer;
+  F: TextFile;
+begin
+  Cmd := LowerCase(P[0]);
+
+  if Cmd = 'clear' then
+  begin
+    ConsoleHistory := nil;
+
+    for a := 0 to High(MsgArray) do
+      with MsgArray[a] do
+      begin
+        Msg := '';
+        Time := 0;
+      end;
+  end;
+
+  if Cmd = 'clearhistory' then
+    CommandHistory := nil;
+
+  if Cmd = 'showhistory' then
+    if CommandHistory <> nil then
+    begin
+      g_Console_Add('');
+      for a := 0 to High(CommandHistory) do
+        g_Console_Add('  '+CommandHistory[a]);
+    end;
+
+  if Cmd = 'commands' then
+  begin
+    g_Console_Add('');
+    g_Console_Add('Commands list:');
+    for a := High(Commands) downto 0 do
+      g_Console_Add('  '+Commands[a].Cmd);
+  end;
+
+  if Cmd = 'time' then
+    g_Console_Add(TimeToStr(Now), True);
+
+  if Cmd = 'date' then
+    g_Console_Add(DateToStr(Now), True);
+
+  if Cmd = 'echo' then
+    if Length(P) > 1 then
+      begin
+        if P[1] = 'ololo' then
+          gCheats := True
+        else
+        begin
+          s := '';
+          for a := 1 to High(P) do
+            s := s + P[a] + ' ';
+          g_Console_Add(b_Text_Format(s), True);
+        end;
+      end
+    else
+      g_Console_Add('');
+
+  if Cmd = 'dump' then
+  begin
+    if ConsoleHistory <> nil then
+    begin
+      if Length(P) > 1 then
+        s := P[1]
+      else
+        s := GameDir+'/console.txt';
+
+      {$I-}
+      AssignFile(F, s);
+      Rewrite(F);
+      if IOResult <> 0 then
+      begin
+        g_Console_Add(Format(_lc[I_CONSOLE_ERROR_WRITE], [s]));
+        CloseFile(F);
+        Exit;
+      end;
+
+      for a := 0 to High(ConsoleHistory) do
+        WriteLn(F, ConsoleHistory[a]);
+
+      CloseFile(F);
+      g_Console_Add(Format(_lc[I_CONSOLE_DUMPED], [s]));
+      {$I+}
+    end;
+  end;
+
+  if Cmd = 'exec' then
+  begin
+    // exec <filename>
+    if Length(P) > 1 then
+    begin
+      s := GameDir+'/'+P[1];
+
+      {$I-}
+      AssignFile(F, s);
+      Reset(F);
+      if IOResult <> 0 then
+      begin
+        g_Console_Add(Format(_lc[I_CONSOLE_ERROR_READ], [s]));
+        CloseFile(F);
+        Exit;
+      end;
+      g_Console_Add(Format(_lc[I_CONSOLE_EXEC], [s]));
+
+      while not EOF(F) do
+      begin
+        ReadLn(F, s);
+        if IOResult <> 0 then
+        begin
+          g_Console_Add(Format(_lc[I_CONSOLE_ERROR_READ], [s]));
+          CloseFile(F);
+          Exit;
+        end;
+        if Pos('#', s) <> 1 then // script comment
+        begin
+          // prevents endless loops
+          Inc(RecursionDepth);
+          RecursionLimitHit := (RecursionDepth > MaxScriptRecursion) or RecursionLimitHit;
+          if not RecursionLimitHit then
+            g_Console_Process(s, True);
+          Dec(RecursionDepth);
+        end;
+      end;
+      if (RecursionDepth = 0) and RecursionLimitHit then
+      begin
+        g_Console_Add(Format(_lc[I_CONSOLE_ERROR_CALL], [s]));
+        RecursionLimitHit := False;
+      end;
+
+      CloseFile(F);
+      {$I+}
+    end
+    else
+      g_Console_Add('exec <script file>');
+  end;
+
+  if Cmd = 'alias' then
+  begin
+    // alias [alias_name] [commands]
+    if Length(P) > 1 then
+    begin
+      for a := 0 to High(Aliases) do
+        if Aliases[a].Name = P[1] then
+        begin
+          if Length(P) > 2 then
+            Aliases[a].Commands := ParseAlias(P[2])
+          else
+            for b := 0 to High(Aliases[a].Commands) do
+              g_Console_Add(Aliases[a].Commands[b]);
+          Exit;
+        end;
+      SetLength(Aliases, Length(Aliases)+1);
+      a := High(Aliases);
+      Aliases[a].Name := P[1];
+      if Length(P) > 2 then
+        Aliases[a].Commands := ParseAlias(P[2])
+      else
+        for b := 0 to High(Aliases[a].Commands) do
+          g_Console_Add(Aliases[a].Commands[b]);
+    end else
+      for a := 0 to High(Aliases) do
+        if Aliases[a].Commands <> nil then
+          g_Console_Add(Aliases[a].Name);
+  end;
+
+  if Cmd = 'call' then
+  begin
+    // call <alias_name>
+    if Length(P) > 1 then
+    begin
+      if Aliases = nil then
+        Exit;
+      for a := 0 to High(Aliases) do
+        if Aliases[a].Name = P[1] then
+        begin
+          if Aliases[a].Commands <> nil then
+          begin
+            // with this system proper endless loop detection seems either impossible
+            // or very dirty to implement, so let's have this instead
+            // prevents endless loops
+            for b := 0 to High(Aliases[a].Commands) do
+            begin
+              Inc(RecursionDepth);
+              RecursionLimitHit := (RecursionDepth > MaxScriptRecursion) or RecursionLimitHit;
+              if not RecursionLimitHit then
+                g_Console_Process(Aliases[a].Commands[b], True);
+              Dec(RecursionDepth);
+            end;
+            if (RecursionDepth = 0) and RecursionLimitHit then
+            begin
+              g_Console_Add(Format(_lc[I_CONSOLE_ERROR_CALL], [s]));
+              RecursionLimitHit := False;
+            end;
+          end;
+          Exit;
+        end;
+    end
+    else
+      g_Console_Add('call <alias name>');
+  end;
+end;
+
+procedure WhitelistCommand(Cmd: string);
+var
+  a: Integer;
+begin
+  SetLength(Whitelist, Length(Whitelist)+1);
+  a := High(Whitelist);
+  Whitelist[a] := Cmd;
+end;
+
+procedure AddCommand(Cmd: String; Proc: TCmdProc);
+var
+  a: Integer;
+begin
+  SetLength(Commands, Length(Commands)+1);
+  a := High(Commands);
+  Commands[a].Cmd := Cmd;
+  Commands[a].Proc := Proc;
+end;
+
+procedure g_Console_Init();
+var
+  a: Integer;
+begin
+  g_Texture_CreateWAD(ID, GameWAD+':TEXTURES\CONSOLE');
+  Cons_Y := -(gScreenHeight div 2);
+  gConsoleShow := False;
+  gChatShow := False;
+  Cons_Shown := False;
+  CPos := 1;
+
+  for a := 0 to High(MsgArray) do
+    with MsgArray[a] do
+    begin
+      Msg := '';
+      Time := 0;
+    end;
+
+  AddCommand('clear', ConsoleCommands);
+  AddCommand('clearhistory', ConsoleCommands);
+  AddCommand('showhistory', ConsoleCommands);
+  AddCommand('commands', ConsoleCommands);
+  AddCommand('time', ConsoleCommands);
+  AddCommand('date', ConsoleCommands);
+  AddCommand('echo', ConsoleCommands);
+  AddCommand('dump', ConsoleCommands);
+  AddCommand('exec', ConsoleCommands);
+  AddCommand('alias', ConsoleCommands);
+  AddCommand('call', ConsoleCommands);
+
+  AddCommand('d_window', DebugCommands);
+  AddCommand('d_sounds', DebugCommands);
+  AddCommand('d_frames', DebugCommands);
+  AddCommand('d_winmsg', DebugCommands);
+  AddCommand('d_monoff', DebugCommands);
+  AddCommand('d_botoff', DebugCommands);
+  AddCommand('d_monster', DebugCommands);
+  AddCommand('d_health', DebugCommands);
+  AddCommand('d_player', DebugCommands);
+  AddCommand('d_joy', DebugCommands);
+
+  AddCommand('p1_name', GameCVars);
+  AddCommand('p2_name', GameCVars);
+  AddCommand('p1_color', GameCVars);
+  AddCommand('p2_color', GameCVars);
+  AddCommand('r_showfps', GameCVars);
+  AddCommand('r_showtime', GameCVars);
+  AddCommand('r_showscore', GameCVars);
+  AddCommand('r_showlives', GameCVars);
+  AddCommand('r_showstat', GameCVars);
+  AddCommand('r_showkillmsg', GameCVars);
+  AddCommand('r_showspect', GameCVars);
+  AddCommand('r_showping', GameCVars);
+  AddCommand('g_gamemode', GameCVars);
+  AddCommand('g_friendlyfire', GameCVars);
+  AddCommand('g_weaponstay', GameCVars);
+  AddCommand('g_allow_exit', GameCVars);
+  AddCommand('g_allow_monsters', GameCVars);
+  AddCommand('g_bot_vsmonsters', GameCVars);
+  AddCommand('g_bot_vsplayers', GameCVars);
+  AddCommand('g_scorelimit', GameCVars);
+  AddCommand('g_timelimit', GameCVars);
+  AddCommand('g_maxlives', GameCVars);
+  AddCommand('g_warmuptime', GameCVars);
+  AddCommand('net_interp', GameCVars);
+  AddCommand('net_forceplayerupdate', GameCVars);
+  AddCommand('net_predictself', GameCVars);
+  AddCommand('sv_name', GameCVars);
+  AddCommand('sv_passwd', GameCVars);
+  AddCommand('sv_maxplrs', GameCVars);
+  AddCommand('sv_public', GameCVars);
+  AddCommand('sv_intertime', GameCVars);
+
+  AddCommand('quit', GameCommands);
+  AddCommand('exit', GameCommands);
+  AddCommand('pause', GameCommands);
+  AddCommand('endgame', GameCommands);
+  AddCommand('restart', GameCommands);
+  AddCommand('addbot', GameCommands);
+  AddCommand('bot_add', GameCommands);
+  AddCommand('bot_addlist', GameCommands);
+  AddCommand('bot_addred', GameCommands);
+  AddCommand('bot_addblue', GameCommands);
+  AddCommand('bot_removeall', GameCommands);
+  AddCommand('chat', GameCommands);
+  AddCommand('teamchat', GameCommands);
+  AddCommand('game', GameCommands);
+  AddCommand('host', GameCommands);
+  AddCommand('map', GameCommands);
+  AddCommand('nextmap', GameCommands);
+  AddCommand('endmap', GameCommands);
+  AddCommand('goodbye', GameCommands);
+  AddCommand('suicide', GameCommands);
+  AddCommand('spectate', GameCommands);
+  AddCommand('ready', GameCommands);
+  AddCommand('kick', GameCommands);
+  AddCommand('kick_id', GameCommands);
+  AddCommand('ban', GameCommands);
+  AddCommand('permban', GameCommands);
+  AddCommand('ban_id', GameCommands);
+  AddCommand('permban_id', GameCommands);
+  AddCommand('unban', GameCommands);
+  AddCommand('connect', GameCommands);
+  AddCommand('disconnect', GameCommands);
+  AddCommand('reconnect', GameCommands);
+  AddCommand('say', GameCommands);
+  AddCommand('tell', GameCommands);
+  AddCommand('overtime', GameCommands);
+  AddCommand('rcon_password', GameCommands);
+  AddCommand('rcon', GameCommands);
+  AddCommand('callvote', GameCommands);
+  AddCommand('vote', GameCommands);
+  AddCommand('clientlist', GameCommands);
+  AddCommand('event', GameCommands);
+
+  WhitelistCommand('say');
+  WhitelistCommand('tell');
+  WhitelistCommand('overtime');
+  WhitelistCommand('ready');
+  WhitelistCommand('map');
+  WhitelistCommand('nextmap');
+  WhitelistCommand('endmap');
+  WhitelistCommand('restart');
+  WhitelistCommand('kick');
+  WhitelistCommand('ban');
+
+  WhitelistCommand('addbot');
+  WhitelistCommand('bot_add');
+  WhitelistCommand('bot_addred');
+  WhitelistCommand('bot_addblue');
+  WhitelistCommand('bot_removeall');
+
+  WhitelistCommand('g_gamemode');
+  WhitelistCommand('g_friendlyfire');
+  WhitelistCommand('g_weaponstay');
+  WhitelistCommand('g_allow_exit');
+  WhitelistCommand('g_allow_monsters');
+  WhitelistCommand('g_scorelimit');
+  WhitelistCommand('g_timelimit');
+
+  g_Console_Add(Format(_lc[I_CONSOLE_WELCOME], [GAME_VERSION]));
+  g_Console_Add('');
+end;
+
+procedure g_Console_Update();
+var
+  a, b: Integer;
+begin
+  if Cons_Shown then
+  begin
+  // Â ïðîöåññå îòêðûòèÿ:
+    if gConsoleShow and (Cons_Y < 0) then
+    begin
+      Cons_Y := Cons_Y+Step;
+    end;
+
+  // Â ïðîöåññå çàêðûòèÿ:
+    if (not gConsoleShow) and
+       (Cons_Y > -(gScreenHeight div 2)) then
+      Cons_Y := Cons_Y-Step;
+
+  // Îêîí÷àòåëüíî îòêðûëàñü:
+    if Cons_Y > 0 then
+      Cons_Y := 0;
+
+  // Îêîí÷àòåëüíî çàêðûëàñü:
+    if Cons_Y <= (-(gScreenHeight div 2)) then
+    begin
+      Cons_Y := -(gScreenHeight div 2);
+      Cons_Shown := False;
+    end;
+  end;
+
+  a := 0;
+  while a <= High(MsgArray) do
+  begin
+    if MsgArray[a].Time > 0 then
+    begin
+      if MsgArray[a].Time = 1 then
+        begin
+          if a < High(MsgArray) then
+          begin
+            for b := a to High(MsgArray)-1 do
+              MsgArray[b] := MsgArray[b+1];
+
+            MsgArray[High(MsgArray)].Time := 0;
+
+            a := a - 1;
+          end;
+        end
+      else
+        Dec(MsgArray[a].Time);
+    end;
+
+    a := a + 1;
+  end;
+end;
+
+procedure g_Console_Draw();
+var
+  CWidth, CHeight: Byte;
+  mfW, mfH: Word;
+  a, b, c, d: Integer;
+begin
+  e_TextureFontGetSize(gStdFont, CWidth, CHeight);
+
+  for a := 0 to High(MsgArray) do
+    if MsgArray[a].Time > 0 then
+      e_TextureFontPrintFmt(0, CHeight*a, MsgArray[a].Msg,
+        gStdFont, True);
+
+  if not Cons_Shown then
+  begin
+    if gChatShow then
+    begin
+      if gChatTeam then
+      begin
+        e_TextureFontPrintEx(0, gScreenHeight - CHeight - 1, 'say team> ' + Line,
+          gStdFont, 255, 255, 255, 1, True);
+        e_TextureFontPrintEx((CPos + 9)*CWidth, gScreenHeight - CHeight - 1, '_',
+          gStdFont, 255, 255, 255, 1, True);
+      end
+      else
+      begin
+        e_TextureFontPrintEx(0, gScreenHeight - CHeight - 1, 'say> ' + Line,
+          gStdFont, 255, 255, 255, 1, True);
+        e_TextureFontPrintEx((CPos + 4)*CWidth, gScreenHeight - CHeight - 1, '_',
+          gStdFont, 255, 255, 255, 1, True);
+      end;
+    end;
+    Exit;
+  end;
+
+  if gDebugMode then
+  begin
+    e_CharFont_GetSize(gMenuFont, DEBUG_STRING, mfW, mfH);
+    a := (gScreenWidth - 2*mfW) div 2;
+    b := Cons_Y + ((gScreenHeight div 2) - 2*mfH) div 2;
+    e_CharFont_PrintEx(gMenuFont, a div 2, b div 2, DEBUG_STRING,
+                       _RGB(128, 0, 0), 2.0);
+  end;
+
+  e_DrawSize(ID, 0, Cons_Y, Alpha, False, False, gScreenWidth, gScreenHeight div 2);
+  e_TextureFontPrint(0, Cons_Y+(gScreenHeight div 2)-CHeight-4, '> '+Line, gStdFont);
+
+  if ConsoleHistory <> nil then
+  begin
+    b := 0;
+    if CHeight > 0 then
+      if Length(ConsoleHistory) > ((gScreenHeight div 2) div CHeight)-1 then
+        b := Length(ConsoleHistory)-((gScreenHeight div 2) div CHeight)+1;
+
+    b := Max(b-Offset, 0);
+    d := Max(High(ConsoleHistory)-Offset, 0);
+
+    c := 2;
+    for a := d downto b do
+    begin
+      e_TextureFontPrintFmt(0, (gScreenHeight div 2)-4-c*CHeight-Abs(Cons_Y), ConsoleHistory[a],
+                           gStdFont, True);
+      c := c + 1;
+    end;
+  end;
+
+  e_TextureFontPrint((CPos+1)*CWidth, Cons_Y+(gScreenHeight div 2)-21, '_', gStdFont);
+end;
+
+procedure g_Console_Switch();
+begin
+  if gChatShow then Exit;
+  gConsoleShow := not gConsoleShow;
+  Cons_Shown := True;
+end;
+
+procedure g_Console_Chat_Switch(Team: Boolean = False);
+begin
+  if gConsoleShow then Exit;
+  if not g_Game_IsNet then Exit;
+  gChatShow := not gChatShow;
+  gChatTeam := Team;
+  if gChatShow then
+    gChatEnter := False;
+  Line := '';
+  CPos := 1;
+end;
+
+procedure g_Console_Char(C: Char);
+begin
+  if gChatShow and (not gChatEnter) then
+    Exit;
+  Insert(C, Line, CPos);
+  CPos := CPos + 1;
+end;
+
+procedure Complete();
+var
+  i: Integer;
+  t: Array of String;
+begin
+  if Line = '' then
+    Exit;
+
+  t := nil;
+
+  for i := 0 to High(Commands) do
+    if LowerCase(Line) = LowerCase(Copy(Commands[i].Cmd, 0, Length(Line))) then
+    begin
+      SetLength(t, Length(t) + 1);
+      t[Length(t)-1] := Commands[i].Cmd;
+    end;
+
+  if t = nil then
+    Exit;
+
+  if Length(t) = 1 then
+    begin
+      Line := t[0]+' ';
+      CPos := Length(Line)+1;
+    end
+  else
+    begin
+      g_Console_Add('');
+      for i := 0 to High(t) do
+        g_Console_Add('  '+t[i]);
+    end;
+end;
+
+procedure g_Console_Control(K: Word);
+begin
+  case K of
+    IK_BACKSPACE:
+      if (Length(Line) > 0) and (CPos > 1) then
+      begin
+        Delete(Line, CPos-1, 1);
+        CPos := CPos-1;
+      end;
+    IK_DELETE:
+      if (Length(Line) > 0) and (CPos <= Length(Line)) then
+        Delete(Line, CPos, 1);
+    IK_LEFT:
+      if CPos > 1 then
+        CPos := CPos - 1;
+    IK_RIGHT:
+      if CPos <= Length(Line) then
+        CPos := CPos + 1;
+    IK_RETURN:
+    begin
+      if Cons_Shown then
+        g_Console_Process(Line)
+      else
+        if gChatShow then
+        begin
+          if (Length(Line) > 0) and g_Game_IsNet then
+          begin
+            if gChatTeam then
+            begin
+              if g_Game_IsClient then
+                MC_SEND_Chat(b_Text_Format(Line), NET_CHAT_TEAM)
+              else
+                MH_SEND_Chat('[' + gPlayer1Settings.Name + ']: ' + b_Text_Format(Line),
+                  NET_CHAT_TEAM, gPlayer1Settings.Team);
+            end
+            else
+            begin
+              if g_Game_IsClient then
+                MC_SEND_Chat(b_Text_Format(Line), NET_CHAT_PLAYER)
+              else
+                MH_SEND_Chat('[' + gPlayer1Settings.Name + ']: ' + b_Text_Format(Line),
+                NET_CHAT_PLAYER);
+            end;
+          end;
+
+          Line := '';
+          CPos := 1;
+          gChatShow := False;
+          gJustChatted := True;
+        end;
+    end;
+    IK_TAB:
+      if not gChatShow then
+        Complete();
+    IK_DOWN:
+      if not gChatShow then
+        if (CommandHistory <> nil) and
+           (CmdIndex < Length(CommandHistory)) then
+        begin
+          if CmdIndex < Length(CommandHistory)-1 then
+            CmdIndex := CmdIndex + 1;
+          Line := CommandHistory[CmdIndex];
+          CPos := Length(Line) + 1;
+        end;
+    IK_UP:
+      if not gChatShow then
+        if (CommandHistory <> nil) and
+           (CmdIndex <= Length(CommandHistory)) then
+        begin
+          if CmdIndex > 0 then
+            CmdIndex := CmdIndex - 1;
+          Line := CommandHistory[CmdIndex];
+          Cpos := Length(Line) + 1;
+        end;
+    IK_PAGEUP: // PgUp
+      if not gChatShow then
+        IncMax(OffSet, Length(ConsoleHistory)-1);
+    IK_PAGEDN: // PgDown
+      if not gChatShow then
+        DecMin(OffSet, 0);
+    IK_HOME:
+      CPos := 1;
+    IK_END:
+      CPos := Length(Line) + 1;
+  end;
+end;
+
+function GetStr(var Str: String): String;
+var
+  a, b: Integer;
+begin
+  if Str[1] = '"' then
+  begin
+    for b := 1 to Length(Str) do
+      if (b = Length(Str)) or (Str[b+1] = '"') then
+      begin
+        Result := Copy(Str, 2, b-1);
+        Delete(Str, 1, b+1);
+        Str := Trim(Str);
+        Exit;
+      end;
+  end;
+
+  for a := 1 to Length(Str) do
+    if (a = Length(Str)) or (Str[a+1] = ' ') then
+    begin
+      Result := Copy(Str, 1, a);
+      Delete(Str, 1, a+1);
+      Str := Trim(Str);
+      Exit;
+    end;
+end;
+
+function ParseString(Str: String): SArray;
+begin
+  Result := nil;
+
+  Str := Trim(Str);
+
+  if Str = '' then
+    Exit;
+
+  while Str <> '' do
+  begin
+    SetLength(Result, Length(Result)+1);
+    Result[High(Result)] := GetStr(Str);
+  end;
+end;
+
+procedure g_Console_Add(L: String; Show: Boolean = False);
+var
+  a: Integer;
+begin
+  // Âûâîä ñòðîê ñ ïåðåíîñàìè ïî î÷åðåäè
+  while Pos(#10, L) > 0 do
+  begin
+    g_Console_Add(Copy(L, 1, Pos(#10, L) - 1), Show);
+    Delete(L, 1, Pos(#10, L));
+  end;
+
+  SetLength(ConsoleHistory, Length(ConsoleHistory)+1);
+  ConsoleHistory[High(ConsoleHistory)] := L;
+
+  Show := Show and gAllowConsoleMessages;
+
+  if Show and gShowMessages then
+  begin
+    for a := 0 to High(MsgArray) do
+      with MsgArray[a] do
+        if Time = 0 then
+        begin
+          Msg := L;
+          Time := MsgTime;
+          Exit;
+        end;
+
+    for a := 0 to High(MsgArray)-1 do
+      MsgArray[a] := MsgArray[a+1];
+
+    with MsgArray[High(MsgArray)] do
+    begin
+      Msg := L;
+      Time := MsgTime;
+    end;
+  end;
+end;
+
+procedure g_Console_Clear();
+begin
+  ConsoleHistory := nil;
+  Offset := 0;
+end;
+
+procedure AddToHistory(L: String);
+var
+  len: Integer;
+begin
+  len := Length(CommandHistory);
+
+  if (len = 0) or
+     (LowerCase(CommandHistory[len-1]) <> LowerCase(L)) then
+  begin
+    SetLength(CommandHistory, len+1);
+    CommandHistory[len] := L;
+  end;
+
+  CmdIndex := Length(CommandHistory);
+end;
+
+function g_Console_CommandBlacklisted(C: String): Boolean;
+var
+  Arr: SArray;
+  i: Integer;
+begin
+  Result := True;
+
+  Arr := nil;
+
+  if Trim(C) = '' then
+    Exit;
+
+  Arr := ParseString(C);
+  if Arr = nil then
+    Exit;
+
+  for i := 0 to High(Whitelist) do
+    if Whitelist[i] = LowerCase(Arr[0]) then
+      Result := False;
+end;
+
+procedure g_Console_Process(L: String; Quiet: Boolean = False);
+var
+  Arr: SArray;
+  i: Integer;
+begin
+  Arr := nil;
+
+  if Trim(L) = '' then
+    Exit;
+
+  if not Quiet then
+  begin
+    g_Console_Add('> '+L);
+    Line := '';
+    CPos := 1;
+  end;
+
+  Arr := ParseString(L);
+  if Arr = nil then
+    Exit;
+
+  if Commands = nil then
+    Exit;
+
+  if not Quiet then
+    AddToHistory(L);
+
+  for i := 0 to High(Commands) do
+    if Commands[i].Cmd = LowerCase(Arr[0]) then
+      if @Commands[i].Proc <> nil then
+      begin
+        Commands[i].Proc(Arr);
+        Exit;
+      end;
+
+  g_Console_Add(Format(_lc[I_CONSOLE_UNKNOWN], [Arr[0]]));
+end;
+
+end.
diff --git a/src/game/g_game.pas b/src/game/g_game.pas
new file mode 100644 (file)
index 0000000..9adfa22
--- /dev/null
@@ -0,0 +1,6357 @@
+unit g_game;
+
+interface
+
+uses
+  g_basic, g_player, e_graphics, Classes, g_res_downloader,
+  SysUtils, g_sound, g_gui, MAPSTRUCT, WADEDITOR, md5;
+
+type
+  TGameSettings = record
+    GameType: Byte;
+    GameMode: Byte;
+    TimeLimit: Word;
+    GoalLimit: Word;
+    WarmupTime: Word;
+    MaxLives: Byte;
+    Options: LongWord;
+    WAD: String;
+  end;
+
+  TGameEvent = record
+    Name: String;
+    Command: String;
+  end;
+
+  TDelayedEvent = record
+    Pending: Boolean;
+    Time: LongWord;
+    DEType: Byte;
+    DENum: Integer;
+    DEStr: String;
+  end;
+
+  TPlayerSettings = record
+    Name: String;
+    Model: String;
+    Color: TRGB;
+    Team: Byte;
+  end;
+
+  TMegaWADInfo = record
+    Name: String;
+    Description: String;
+    Author: String;
+    Pic: String;
+  end;
+
+  THearPoint = record
+    Active: Boolean;
+    Coords: TPoint;
+  end;
+
+function  g_Game_IsNet(): Boolean;
+function  g_Game_IsServer(): Boolean;
+function  g_Game_IsClient(): Boolean;
+procedure g_Game_Init();
+procedure g_Game_Free();
+procedure g_Game_LoadData();
+procedure g_Game_FreeData();
+procedure g_Game_Update();
+procedure g_Game_Draw();
+procedure g_Game_Quit();
+procedure g_Game_SetupScreenSize();
+procedure g_Game_ChangeResolution(newWidth, newHeight: Word; nowFull, nowMax: Boolean);
+function  g_Game_ModeToText(Mode: Byte): string;
+function  g_Game_TextToMode(Mode: string): Byte;
+procedure g_Game_ExecuteEvent(Name: String);
+function  g_Game_DelayEvent(DEType: Byte; Time: LongWord; Num: Integer = 0; Str: String = ''): Integer;
+procedure g_Game_AddPlayer(Team: Byte = TEAM_NONE);
+procedure g_Game_RemovePlayer();
+procedure g_Game_Spectate();
+procedure g_Game_SpectateCenterView();
+procedure g_Game_StartSingle(Map: String; TwoPlayers: Boolean; nPlayers: Byte);
+procedure g_Game_StartCustom(Map: String; GameMode: Byte; TimeLimit, GoalLimit: Word; MaxLives: Byte; Options: LongWord; nPlayers: Byte);
+procedure g_Game_StartServer(Map: String; GameMode: Byte; TimeLimit, GoalLimit: Word; MaxLives: Byte; Options: LongWord; nPlayers: Byte; IPAddr: LongWord; Port: Word);
+procedure g_Game_StartClient(Addr: String; Port: Word; PW: String);
+procedure g_Game_Restart();
+procedure g_Game_RestartLevel();
+procedure g_Game_RestartRound(NoMapRestart: Boolean = False);
+procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest);
+procedure g_Game_SaveOptions();
+function  g_Game_StartMap(Map: String; Force: Boolean = False): Boolean;
+procedure g_Game_ChangeMap(MapPath: String);
+procedure g_Game_ExitLevel(Map: Char16);
+function  g_Game_GetFirstMap(WAD: String): String;
+function  g_Game_GetNextMap(): String;
+procedure g_Game_NextLevel();
+procedure g_Game_Pause(Enable: Boolean);
+procedure g_Game_InGameMenu(Show: Boolean);
+function  g_Game_IsWatchedPlayer(UID: Word): Boolean;
+function  g_Game_IsWatchedTeam(Team: Byte): Boolean;
+procedure g_Game_Message(Msg: String; Time: Word);
+procedure g_Game_LoadMapList(FileName: String);
+procedure g_Game_PauseAllSounds(Enable: Boolean);
+procedure g_Game_StopAllSounds(all: Boolean);
+procedure g_Game_UpdateTriggerSounds();
+function  g_Game_GetMegaWADInfo(WAD: String): TMegaWADInfo;
+procedure g_Game_Announce_GoodShot(SpawnerUID: Word);
+procedure g_Game_Announce_KillCombo(Param: Integer);
+procedure g_Game_StartVote(Command, Initiator: string);
+procedure g_Game_CheckVote;
+procedure g_TakeScreenShot();
+procedure g_FatalError(Text: String);
+procedure g_SimpleError(Text: String);
+function  g_Game_IsTestMap(): Boolean;
+procedure g_Game_DeleteTestMap();
+procedure GameCVars(P: SArray);
+procedure GameCommands(P: SArray);
+procedure DebugCommands(P: SArray);
+procedure g_Game_Process_Params;
+procedure g_Game_SetLoadingText(Text: String; Max: Integer; reWrite: Boolean);
+procedure g_Game_StepLoading();
+procedure g_Game_ClearLoading();
+procedure g_Game_SetDebugMode();
+procedure DrawLoadingStat();
+
+{ procedure SetWinPause(Enable: Boolean); }
+
+const
+  GAME_TICK = 28;
+
+  LOADING_SHOW_STEP = 100;
+  LOADING_INTERLINE = 20;
+
+  GT_NONE   = 0;
+  GT_SINGLE = 1;
+  GT_CUSTOM = 2;
+  GT_SERVER = 3;
+  GT_CLIENT = 4;
+
+  GM_NONE = 0;
+  GM_DM   = 1;
+  GM_TDM  = 2;
+  GM_CTF  = 3;
+  GM_COOP = 4;
+  GM_SINGLE = 5;
+
+  MESSAGE_DIKEY = WM_USER + 1;
+
+  EXIT_QUIT            = 1;
+  EXIT_SIMPLE          = 2;
+  EXIT_RESTART         = 3;
+  EXIT_ENDLEVELSINGLE  = 4;
+  EXIT_ENDLEVELCUSTOM  = 5;
+
+  GAME_OPTION_RESERVED     = 1;
+  GAME_OPTION_TEAMDAMAGE   = 2;
+  GAME_OPTION_ALLOWEXIT    = 4;
+  GAME_OPTION_WEAPONSTAY   = 8;
+  GAME_OPTION_MONSTERS     = 16;
+  GAME_OPTION_BOTVSPLAYER  = 32;
+  GAME_OPTION_BOTVSMONSTER = 64;
+
+  STATE_NONE        = 0;
+  STATE_MENU        = 1;
+  STATE_FOLD        = 2;
+  STATE_INTERCUSTOM = 3;
+  STATE_INTERSINGLE = 4;
+  STATE_INTERTEXT   = 5;
+  STATE_INTERPIC    = 6;
+  STATE_ENDPIC      = 7;
+  STATE_SLIST       = 8;
+
+  LMS_RESPAWN_NONE   = 0;
+  LMS_RESPAWN_WARMUP = 1;
+  LMS_RESPAWN_FINAL  = 2;
+
+  SPECT_NONE    = 0;
+  SPECT_STATS   = 1;
+  SPECT_MAPVIEW = 2;
+  SPECT_PLAYERS = 3;
+
+  DE_GLOBEVENT = 0;
+  DE_BFGHIT    = 1;
+  DE_KILLCOMBO = 2;
+
+  ANNOUNCE_NONE   = 0;
+  ANNOUNCE_ME     = 1;
+  ANNOUNCE_MEPLUS = 2;
+  ANNOUNCE_ALL    = 3;
+
+  CONFIG_FILENAME = 'Doom2DF.cfg';
+  LOG_FILENAME = 'Doom2DF.log';
+
+  TEST_MAP_NAME = '$$$_TEST_$$$';
+
+  STD_PLAYER_MODEL = 'Doomer';
+
+var
+  gStdFont: DWORD;
+  gGameSettings: TGameSettings;
+  gPlayer1Settings: TPlayerSettings;
+  gPlayer2Settings: TPlayerSettings;
+  gGameOn: Boolean;
+  gPlayerScreenSize: TPoint;
+  gPlayer1ScreenCoord: TPoint;
+  gPlayer2ScreenCoord: TPoint;
+  gPlayer1: TPlayer = nil;
+  gPlayer2: TPlayer = nil;
+  gPlayerDrawn: TPlayer = nil;
+  gTime: LongWord;
+  gSwitchGameMode: Byte = GM_DM;
+  gHearPoint1, gHearPoint2: THearPoint;
+  gSoundEffectsDF: Boolean = False;
+  gSoundTriggerTime: Word = 0;
+  gAnnouncer: Byte = ANNOUNCE_NONE;
+  goodsnd: array[0..3] of TPlayableSound;
+  killsnd: array[0..3] of TPlayableSound;
+  gDefInterTime: ShortInt = -1;
+  gInterEndTime: LongWord = 0;
+  gInterTime: LongWord = 0;
+  gServInterTime: Byte = 0;
+  gGameStartTime: LongWord = 0;
+  gTotalMonsters: Integer = 0;
+  gPause: Boolean;
+  gShowTime: Boolean = True;
+  gShowFPS: Boolean = False;
+  gShowGoals: Boolean = True;
+  gShowStat: Boolean = True;
+  gShowKillMsg: Boolean = True;
+  gShowLives: Boolean = True;
+  gShowPing: Boolean = False;
+  gShowMap: Boolean = False;
+  gExit: Byte = 0;
+  gState: Byte = STATE_NONE;
+  sX, sY: Integer;
+  sWidth, sHeight: Word;
+  gSpectMode: Byte = SPECT_NONE;
+  gSpectHUD: Boolean = True;
+  gSpectKeyPress: Boolean = False;
+  gSpectX: Integer = 0;
+  gSpectY: Integer = 0;
+  gSpectStep: Byte = 8;
+  gSpectViewTwo: Boolean = False;
+  gSpectPID1: Integer = -1;
+  gSpectPID2: Integer = -1;
+  gMusic: TMusic = nil;
+  gLoadGameMode: Boolean;
+  gCheats: Boolean = False;
+  gMapOnce: Boolean = False;
+  gMapToDelete: String;
+  gTempDelete: Boolean = False;
+  gLastMap: Boolean = False;
+  gWinPosX, gWinPosY: Integer;
+  gWinSizeX, gWinSizeY: Integer;
+  gWinFrameX, gWinFrameY, gWinCaption: Integer;
+  gWinActive: Boolean = False;
+  gResolutionChange: Boolean = False;
+  gRC_Width, gRC_Height: Word;
+  gRC_FullScreen, gRC_Maximized: Boolean;
+  gLanguageChange: Boolean = False;
+  gDebugMode: Boolean = False;
+  g_debug_Sounds: Boolean = False;
+  g_debug_Frames: Boolean = False;
+  g_debug_WinMsgs: Boolean = False;
+  g_debug_MonsterOff: Boolean = False;
+  g_debug_BotAIOff: Byte = 0;
+  g_debug_HealthBar: Boolean = False;
+  g_Debug_Player: Boolean = False;
+  gCoopMonstersKilled: Word = 0;
+  gCoopSecretsFound: Word = 0;
+  gCoopTotalMonstersKilled: Word = 0;
+  gCoopTotalSecretsFound: Word = 0;
+  gCoopTotalMonsters: Word = 0;
+  gCoopTotalSecrets: Word = 0;
+  gStatsOff: Boolean = False;
+  gStatsPressed: Boolean = False;
+  gExitByTrigger: Boolean = False;
+  gNextMap: String = '';
+  gLMSRespawn: Byte = LMS_RESPAWN_NONE;
+  gLMSRespawnTime: Cardinal = 0;
+  gLMSSoftSpawn: Boolean = False;
+  gMissionFailed: Boolean = False;
+  gVoteInProgress: Boolean = False;
+  gVotePassed: Boolean = False;
+  gVoteCommand: string = '';
+  gVoteTimer: Cardinal = 0;
+  gVoteCmdTimer: Cardinal = 0;
+  gVoteCount: Integer = 0;
+  gVoteTimeout: Cardinal = 30;
+  gVoted: Boolean = False;
+  gVotesEnabled: Boolean = True;
+  gEvents: Array of TGameEvent;
+  gDelayedEvents: Array of TDelayedEvent;
+
+  P1MoveButton: Byte = 0;
+  P2MoveButton: Byte = 0;
+
+implementation
+
+uses
+  g_textures, g_main, g_window, g_menu,
+  e_input, e_log, g_console, g_items, g_map,
+  g_playermodel, g_gfx, g_options, g_weapons, Math,
+  g_triggers, MAPDEF, g_monsters, e_sound, CONFIG,
+  BinEditor, g_language, g_net, SDL,
+  ENet, e_fixedbuffer, g_netmsg, g_netmaster, GL, GLExt;
+
+type
+  TEndCustomGameStat = record
+    PlayerStat: TPlayerStatArray;
+    TeamStat: TTeamStat;
+    GameTime: LongWord;
+    GameMode: Byte;
+    Map, MapName: String;
+  end;
+
+  TEndSingleGameStat = record
+    PlayerStat: Array [0..1] of record
+      Kills: Integer;
+      Secrets: Integer;
+    end;
+    GameTime: LongWord;
+    TwoPlayers: Boolean;
+    TotalSecrets: Integer;
+  end;
+
+  TLoadingStat = record
+    CurValue: Integer;
+    MaxValue: Integer;
+    ShowCount: Integer;
+    Msgs: Array of String;
+    NextMsg: Word;
+  end;
+
+  TParamStrValue = record
+    Name: String;
+    Value: String;
+  end;
+
+  TParamStrValues = Array of TParamStrValue;
+
+const
+  INTER_ACTION_TEXT = 1;
+  INTER_ACTION_PIC = 2;
+  INTER_ACTION_MUSIC = 3;
+
+var
+  FPS, UPS: Word;
+  FPSCounter, UPSCounter: Word;
+  FPSTime, UPSTime: LongWord;
+  DataLoaded: Boolean = False;
+  LastScreenShot: Int64;
+  IsDrawStat: Boolean = False;
+  CustomStat: TEndCustomGameStat;
+  SingleStat: TEndSingleGameStat;
+  LoadingStat: TLoadingStat;
+  EndingGameCounter: Byte = 0;
+  MessageText: String;
+  MessageTime: Word;
+  MapList: SArray = nil;
+  MapIndex: Integer = -1;
+  MegaWAD: record
+    info: TMegaWADInfo;
+    endpic: String;
+    endmus: String;
+    res: record
+      text: Array of ShortString;
+      anim: Array of ShortString;
+      pic: Array of ShortString;
+      mus: Array of ShortString;
+    end;
+    triggers: Array of record
+      event: ShortString;
+      actions: Array of record
+        action, p1, p2: Integer;
+      end;
+    end;
+    cur_trigger: Integer;
+    cur_action: Integer;
+  end;
+  //InterPic: String;
+  InterText: record
+    lines: SArray;
+    img: String;
+    cur_line: Integer;
+    cur_char: Integer;
+    counter: Integer;
+    endtext: Boolean;
+  end;
+
+function Compare(a, b: TPlayerStat): Integer;
+begin
+  if a.Spectator then Result := 1
+    else if b.Spectator then Result := -1
+      else if a.Frags < b.Frags then Result := 1
+        else if a.Frags > b.Frags then Result := -1
+          else if a.Deaths < b.Deaths then Result := -1
+            else if a.Deaths > b.Deaths then Result := 1
+              else if a.Kills < b.Kills then Result := -1
+                else Result := 1;
+end;
+
+procedure SortGameStat(var stat: TPlayerStatArray);
+var
+  I, J: Integer;
+  T: TPlayerStat;
+begin
+  if stat = nil then Exit;
+
+  for I := High(stat) downto Low(stat) do
+    for J := Low(stat) to High(stat) - 1 do
+      if Compare(stat[J], stat[J + 1]) = 1  then
+      begin
+        T := stat[J];
+        stat[J] := stat[J + 1];
+        stat[J + 1] := T;
+      end;
+end;
+
+function g_Game_ModeToText(Mode: Byte): string;
+begin
+  Result := '';
+  case Mode of
+    GM_DM:   Result := _lc[I_MENU_GAME_TYPE_DM];
+    GM_TDM:  Result := _lc[I_MENU_GAME_TYPE_TDM];
+    GM_CTF:  Result := _lc[I_MENU_GAME_TYPE_CTF];
+    GM_COOP: Result := _lc[I_MENU_GAME_TYPE_COOP];
+    GM_SINGLE: Result := _lc[I_MENU_GAME_TYPE_SINGLE];
+  end;
+end;
+
+function g_Game_TextToMode(Mode: string): Byte;
+begin
+  Result := GM_NONE;
+  Mode := UpperCase(Mode);
+  if Mode = _lc[I_MENU_GAME_TYPE_DM] then
+  begin
+    Result := GM_DM;
+    Exit;
+  end;
+  if Mode = _lc[I_MENU_GAME_TYPE_TDM] then
+  begin
+    Result := GM_TDM;
+    Exit;
+  end;
+  if Mode = _lc[I_MENU_GAME_TYPE_CTF] then
+  begin
+    Result := GM_CTF;
+    Exit;
+  end;
+  if Mode = _lc[I_MENU_GAME_TYPE_COOP] then
+  begin
+    Result := GM_COOP;
+    Exit;
+  end;
+  if Mode = _lc[I_MENU_GAME_TYPE_SINGLE] then
+  begin
+    Result := GM_SINGLE;
+    Exit;
+  end;
+end;
+
+function g_Game_IsNet(): Boolean;
+begin
+  Result := (gGameSettings.GameType in [GT_SERVER, GT_CLIENT]);
+end;
+
+function g_Game_IsServer(): Boolean;
+begin
+  Result := (gGameSettings.GameType in [GT_SINGLE, GT_CUSTOM, GT_SERVER]);
+end;
+
+function g_Game_IsClient(): Boolean;
+begin
+  Result := (gGameSettings.GameType = GT_CLIENT);
+end;
+
+function g_Game_GetMegaWADInfo(WAD: String): TMegaWADInfo;
+var
+  w: TWADEditor_1;
+  cfg: TConfig;
+  p: Pointer;
+  len: Integer;
+begin
+  Result.name := ExtractFileName(WAD);
+  Result.description := '';
+  Result.author := '';
+
+  w := TWADEditor_1.Create();
+  w.ReadFile(WAD);
+
+  if not w.GetResource('', 'INTERSCRIPT', p, len) then
+  begin
+    w.Free();
+    Exit;
+  end;
+
+  cfg := TConfig.CreateMem(p, len);
+  Result.name := cfg.ReadStr('megawad', 'name', ExtractFileName(WAD));
+  Result.description := cfg.ReadStr('megawad', 'description', '');
+  Result.author := cfg.ReadStr('megawad', 'author', '');
+  Result.pic := cfg.ReadStr('megawad', 'pic', '');
+  cfg.Free();
+
+  FreeMem(p);
+end;
+
+procedure g_Game_FreeWAD();
+var
+  a: Integer;
+begin
+  for a := 0 to High(MegaWAD.res.pic) do
+    if MegaWAD.res.pic[a] <> '' then
+      g_Texture_Delete(MegaWAD.res.pic[a]);
+
+  for a := 0 to High(MegaWAD.res.mus) do
+    if MegaWAD.res.mus[a] <> '' then
+      g_Sound_Delete(MegaWAD.res.mus[a]);
+
+  MegaWAD.res.pic := nil;
+  MegaWAD.res.text := nil;
+  MegaWAD.res.anim := nil;
+  MegaWAD.res.mus := nil;
+  MegaWAD.triggers := nil;
+
+  g_Texture_Delete('TEXTURE_endpic');
+  g_Sound_Delete('MUSIC_endmus');
+
+  ZeroMemory(@MegaWAD, SizeOf(MegaWAD));
+  gGameSettings.WAD := '';
+end;
+
+procedure g_Game_LoadWAD(WAD: string);
+var
+  w: TWADEditor_1;
+  cfg: TConfig;
+  p: Pointer;
+  {b, }len: Integer;
+  s: string;
+begin
+  g_Game_FreeWAD();
+  gGameSettings.WAD := WAD;
+  if not (gGameSettings.GameMode in [GM_COOP, GM_SINGLE]) then
+    Exit;
+
+  MegaWAD.info := g_Game_GetMegaWADInfo(MapsDir + WAD);
+
+  w := TWADEditor_1.Create();
+  w.ReadFile(MapsDir + WAD);
+
+  if not w.GetResource('', 'INTERSCRIPT', p, len) then
+  begin
+    w.Free();
+    Exit;
+  end;
+
+  cfg := TConfig.CreateMem(p, len);
+
+ {b := 1;
+ while True do
+ begin
+  s := cfg.ReadStr('pic', 'pic'+IntToStr(b), '');
+  if s = '' then Break;
+  b := b+1;
+
+  SetLength(MegaWAD.res.pic, Length(MegaWAD.res.pic)+1);
+  MegaWAD.res.pic[High(MegaWAD.res.pic)] := s;
+
+  g_Texture_CreateWADEx(s, s);
+ end;
+
+ b := 1;
+ while True do
+ begin
+  s := cfg.ReadStr('mus', 'mus'+IntToStr(b), '');
+  if s = '' then Break;
+  b := b+1;
+
+  SetLength(MegaWAD.res.mus, Length(MegaWAD.res.mus)+1);
+  MegaWAD.res.mus[High(MegaWAD.res.mus)] := s;
+
+  g_Music_CreateWADEx(s, s);
+ end;}
+
+  MegaWAD.endpic := cfg.ReadStr('megawad', 'endpic', '');
+  if MegaWAD.endpic <> '' then
+  begin
+    g_ProcessResourceStr(MegaWAD.endpic, @s, nil, nil);
+    if s = '' then s := MapsDir+WAD else s := GameDir+'/wads/';
+    g_Texture_CreateWADEx('TEXTURE_endpic', s+MegaWAD.endpic);
+  end;
+  MegaWAD.endmus := cfg.ReadStr('megawad', 'endmus', 'Standart.wad:D2DMUS\ÊÎÍÅÖ');
+  if MegaWAD.endmus <> '' then
+  begin
+    g_ProcessResourceStr(MegaWAD.endmus, @s, nil, nil);
+    if s = '' then s := MapsDir+WAD else s := GameDir+'/wads/';
+    g_Sound_CreateWADEx('MUSIC_endmus', s+MegaWAD.endmus, True);
+  end;
+
+  cfg.Free();
+  FreeMem(p);
+  w.Free();
+end;
+
+{procedure start_trigger(t: string);
+begin
+end;
+
+function next_trigger(): Boolean;
+begin
+end;}
+
+procedure DisableCheats();
+begin
+  MAX_RUNVEL := 8;
+  VEL_JUMP := 10;
+  gFly := False;
+
+  if gPlayer1 <> nil then gPlayer1.GodMode := False;
+  if gPlayer2 <> nil then gPlayer2.GodMode := False;
+  if gPlayer1 <> nil then gPlayer1.NoTarget := False;
+  if gPlayer2 <> nil then gPlayer2.NoTarget := False;
+end;
+
+procedure g_Game_ExecuteEvent(Name: String);
+var
+  a: Integer;
+begin
+  if Name = '' then
+    Exit;
+  if gEvents = nil then
+    Exit;
+  for a := 0 to High(gEvents) do
+    if gEvents[a].Name = Name then
+    begin
+      if gEvents[a].Command <> '' then
+        g_Console_Process(gEvents[a].Command, True);
+      break;
+    end;
+end;
+
+function g_Game_DelayEvent(DEType: Byte; Time: LongWord; Num: Integer = 0; Str: String = ''): Integer;
+var
+  a, n: Integer;
+begin
+  n := -1;
+  if gDelayedEvents <> nil then
+    for a := 0 to High(gDelayedEvents) do
+      if not gDelayedEvents[a].Pending then
+      begin
+        n := a;
+        break;
+      end;
+  if n = -1 then
+  begin
+    SetLength(gDelayedEvents, Length(gDelayedEvents) + 1);
+    n := High(gDelayedEvents);
+  end;
+  gDelayedEvents[n].Pending := True;
+  gDelayedEvents[n].DEType := DEType;
+  gDelayedEvents[n].DENum := Num;
+  gDelayedEvents[n].DEStr := Str;
+  if DEType = DE_GLOBEVENT then
+    gDelayedEvents[n].Time := (GetTimer() div 1000) + Time
+  else
+    gDelayedEvents[n].Time := gTime + Time;
+  Result := n;
+end;
+
+procedure EndGame();
+var
+  a: Integer;
+  FileName, SectionName, ResName: string;
+begin
+  if g_Game_IsNet and g_Game_IsServer then
+    MH_SEND_GameEvent(NET_EV_MAPEND, Byte(gMissionFailed));
+
+// Ñòîï èãðà:
+  gPause := False;
+  gGameOn := False;
+
+  g_Game_StopAllSounds(False);
+
+  MessageTime := 0;
+  MessageText := '';
+
+  EndingGameCounter := 0;
+  g_ActiveWindow := nil;
+
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+
+  case gExit of
+    EXIT_SIMPLE: // Âûõîä ÷åðåç ìåíþ èëè êîíåö òåñòà
+      begin
+        g_Game_Free();
+
+        if gMapOnce  then
+          begin // Ýòî áûë òåñò
+            g_Game_Quit();
+          end
+        else
+          begin // Âûõîä â ãëàâíîå ìåíþ
+            gMusic.SetByName('MUSIC_MENU');
+            gMusic.Play();
+            if gState <> STATE_SLIST then
+            begin
+              g_GUI_ShowWindow('MainMenu');
+              gState := STATE_MENU;
+            end else
+            begin
+              // Îáíîâëÿåì ñïèñîê ñåðâåðîâ
+              slReturnPressed := True;
+              if g_Net_Slist_Fetch(slCurrent) then
+              begin
+                if slCurrent = nil then
+                  slWaitStr := _lc[I_NET_SLIST_NOSERVERS];
+              end
+              else
+                slWaitStr := _lc[I_NET_SLIST_ERROR];
+            end;
+
+            g_Game_ExecuteEvent('ongameend');
+          end;
+      end;
+
+    EXIT_RESTART: // Íà÷àòü óðîâåíü ñíà÷àëà
+      begin
+        if not g_Game_IsClient then g_Game_Restart();
+      end;
+
+    EXIT_ENDLEVELCUSTOM: // Çàêîí÷èëñÿ óðîâåíü â Ñâîåé èãðå
+      begin
+      // Ñòàòèñòèêà Ñâîåé èãðû:
+        g_ProcessResourceStr(gMapInfo.Map, FileName, SectionName, ResName);
+
+        CustomStat.GameTime := gTime;
+        CustomStat.Map := ExtractFileName(FileName)+':'+ResName;
+        CustomStat.MapName := gMapInfo.Name;
+        CustomStat.GameMode := gGameSettings.GameMode;
+        if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+          CustomStat.TeamStat := gTeamStat;
+
+        CustomStat.PlayerStat := nil;
+
+      // Ñòàòèñòèêà èãðîêîâ:
+        if gPlayers <> nil then
+        begin
+          for a := 0 to High(gPlayers) do
+            if gPlayers[a] <> nil then
+            begin
+              SetLength(CustomStat.PlayerStat, Length(CustomStat.PlayerStat)+1);
+              with CustomStat.PlayerStat[High(CustomStat.PlayerStat)] do
+              begin
+                Name := gPlayers[a].Name;
+                Frags := gPlayers[a].Frags;
+                Deaths := gPlayers[a].Death;
+                Kills := gPlayers[a].Kills;
+                Team := gPlayers[a].Team;
+                Color := gPlayers[a].Model.Color;
+                Spectator := gPlayers[a].FSpectator;
+              end;
+            end;
+
+          SortGameStat(CustomStat.PlayerStat);
+        end;
+
+        g_Game_ExecuteEvent('onmapend');
+
+      // Çàòóõàþùèé ýêðàí:
+        EndingGameCounter := 255;
+        gState := STATE_FOLD;
+        gInterTime := 0;
+        if gDefInterTime < 0 then
+          gInterEndTime := IfThen((gGameSettings.GameType = GT_SERVER) and (gPlayer1 = nil), 15000, 25000)
+        else
+          gInterEndTime := gDefInterTime * 1000;
+      end;
+
+    EXIT_ENDLEVELSINGLE: // Çàêîí÷èëñÿ óðîâåíü â Îäèíî÷íîé èãðå
+      begin
+      // Ñòàòèñòèêà Îäèíî÷íîé èãðû:
+        SingleStat.GameTime := gTime;
+        SingleStat.TwoPlayers := gPlayer2 <> nil;
+        SingleStat.TotalSecrets := gSecretsCount;
+      // Ñòàòèñòèêà ïåðâîãî èãðîêà:
+        SingleStat.PlayerStat[0].Kills := gPlayer1.MonsterKills;
+        SingleStat.PlayerStat[0].Secrets := gPlayer1.Secrets;
+      // Ñòàòèñòèêà âòîðîãî èãðîêà (åñëè åñòü):
+        if SingleStat.TwoPlayers then
+        begin
+          SingleStat.PlayerStat[1].Kills := gPlayer2.MonsterKills;
+          SingleStat.PlayerStat[1].Secrets := gPlayer2.Secrets;
+        end;
+
+        g_Game_ExecuteEvent('onmapend');
+
+      // Åñòü åùå êàðòû:
+        if gNextMap <> '' then
+          begin
+            gMusic.SetByName('MUSIC_INTERMUS');
+            gMusic.Play();
+            gState := STATE_INTERSINGLE;
+
+            g_Game_ExecuteEvent('oninter');
+          end
+        else // Áîëüøå íåò êàðò
+          begin
+          // Çàòóõàþùèé ýêðàí:
+            EndingGameCounter := 255;
+            gState := STATE_FOLD;
+          end;
+      end;
+  end;
+
+// Îêîí÷àíèå îáðàáîòàíî:
+  if gExit <> EXIT_QUIT then
+    gExit := 0;
+end;
+
+procedure DrawStat();
+var
+  pc, x, y, w, h: Integer;
+  w1, w2, w3, w4: Integer;
+  a, aa: Integer;
+  cw, ch, r, g, b, rr, gg, bb: Byte;
+  s1, s2, s3: String;
+  _y: Integer;
+  stat: TPlayerStatArray;
+  wad, map: string;
+  mapstr: string;
+begin
+  pc := g_Player_GetCount;
+  e_TextureFontGetSize(gStdFont, cw, ch);
+
+  w := gScreenWidth-(gScreenWidth div 5);
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+    h := 32+ch*(11+pc)
+  else
+    h := 40+ch*5+(ch+8)*pc;
+  x := (gScreenWidth div 2)-(w div 2);
+  y := (gScreenHeight div 2)-(h div 2);
+
+  e_DrawFillQuad(x, y, x+w-1, y+h-1, 64, 64, 64, 32);
+  e_DrawQuad(x, y, x+w-1, y+h-1, 255, 127, 0);
+
+  g_ProcessResourceStr(gMapInfo.Map, @wad, nil, @map);
+  wad := ExtractFileName(wad);
+  mapstr := wad + ':\' + map + ' - ' + gMapInfo.Name;
+
+  case gGameSettings.GameMode of
+    GM_DM:
+    begin
+      if gGameSettings.MaxLives = 0 then
+        s1 := _lc[I_GAME_DM]
+      else
+        s1 := _lc[I_GAME_LMS];
+      s2 := Format(_lc[I_GAME_FRAG_LIMIT], [gGameSettings.GoalLimit]);
+      s3 := Format(_lc[I_GAME_TIME_LIMIT], [gGameSettings.TimeLimit div 3600, (gGameSettings.TimeLimit div 60) mod 60, gGameSettings.TimeLimit mod 60]);
+    end;
+
+    GM_TDM:
+    begin
+      if gGameSettings.MaxLives = 0 then
+        s1 := _lc[I_GAME_TDM]
+      else
+        s1 := _lc[I_GAME_TLMS];
+      s2 := Format(_lc[I_GAME_FRAG_LIMIT], [gGameSettings.GoalLimit]);
+      s3 := Format(_lc[I_GAME_TIME_LIMIT], [gGameSettings.TimeLimit div 3600, (gGameSettings.TimeLimit div 60) mod 60, gGameSettings.TimeLimit mod 60]);
+    end;
+
+    GM_CTF:
+    begin
+      s1 := _lc[I_GAME_CTF];
+      s2 := Format(_lc[I_GAME_SCORE_LIMIT], [gGameSettings.GoalLimit]);
+      s3 := Format(_lc[I_GAME_TIME_LIMIT], [gGameSettings.TimeLimit div 3600, (gGameSettings.TimeLimit div 60) mod 60, gGameSettings.TimeLimit mod 60]);
+    end;
+
+    GM_COOP:
+    begin
+      if gGameSettings.MaxLives = 0 then
+        s1 := _lc[I_GAME_COOP]
+      else
+        s1 := _lc[I_GAME_SURV];
+      s2 := _lc[I_GAME_MONSTERS] + ' ' + IntToStr(gCoopMonstersKilled) + '/' + IntToStr(gTotalMonsters);
+      s3 := _lc[I_GAME_SECRETS] + ' ' + IntToStr(gCoopSecretsFound) + '/' + IntToStr(gSecretsCount);
+    end;
+
+    else
+    begin
+      s1 := '';
+      s2 := '';
+    end;
+  end;
+
+  _y := y+8;
+  e_TextureFontPrintEx(x+(w div 2)-(Length(s1)*cw div 2), _y, s1, gStdFont, 255, 255, 255, 1);
+  _y := _y+ch+8;
+  e_TextureFontPrintEx(x+(w div 2)-(Length(mapstr)*cw div 2), _y, mapstr, gStdFont, 200, 200, 200, 1);
+  _y := _y+ch+8;
+  e_TextureFontPrintEx(x+16, _y, s2, gStdFont, 200, 200, 200, 1);
+
+  e_TextureFontPrintEx(x+w-16-(Length(s3))*cw, _y, s3,
+                       gStdFont, 200, 200, 200, 1);
+
+  if NetMode = NET_SERVER then
+    e_TextureFontPrintEx(x+8, y + 8, _lc[I_NET_SERVER], gStdFont, 255, 255, 255, 1)
+  else
+    if NetMode = NET_CLIENT then
+      e_TextureFontPrintEx(x+8, y + 8,
+        NetClientIP + ':' + IntToStr(NetClientPort), gStdFont, 255, 255, 255, 1);
+
+  if pc = 0 then
+    Exit;
+  stat := g_Player_GetStats();
+  SortGameStat(stat);
+
+  w2 := (w-16) div 6 + 48; // øèðèíà 2 ñòîëáöà
+  w3 := (w-16) div 6; // øèðèíà 3 è 4 ñòîëáöîâ
+  w4 := w3;
+  w1 := w-16-w2-w3-w4; // îñòàâøååñÿ ïðîñòðàíñòâî - äëÿ öâåòà è èìåíè èãðîêà
+
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+  begin
+    _y := _y+ch+ch;
+
+    for a := TEAM_RED to TEAM_BLUE do
+    begin
+      if a = TEAM_RED then
+      begin
+        s1 := _lc[I_GAME_TEAM_RED];
+        r := 255;
+        g := 0;
+        b := 0;
+      end
+      else
+      begin
+        s1 := _lc[I_GAME_TEAM_BLUE];
+        r := 0;
+        g := 0;
+        b := 255;
+      end;
+
+      e_TextureFontPrintEx(x+16, _y, s1, gStdFont, r, g, b, 1);
+      e_TextureFontPrintEx(x+w1+16, _y, IntToStr(gTeamStat[a].Goals),
+                           gStdFont, r, g, b, 1);
+
+      _y := _y+ch+(ch div 4);
+      e_DrawLine(1, x+16, _y, x+w-16, _y, r, g, b);
+      _y := _y+(ch div 4);
+
+      for aa := 0 to High(stat) do
+        if stat[aa].Team = a then
+          with stat[aa] do
+          begin
+            if Spectator then
+            begin
+              rr := r div 2;
+              gg := g div 2;
+              bb := b div 2;
+            end
+            else
+            begin
+              rr := r;
+              gg := g;
+              bb := b;
+            end;
+            // Èìÿ
+            e_TextureFontPrintEx(x+16, _y, Name, gStdFont, rr, gg, bb, 1);
+            // Ïèíã/ïîòåðè
+            e_TextureFontPrintEx(x+w1+16, _y, Format(_lc[I_GAME_PING_MS], [Ping, Loss]), gStdFont, rr, gg, bb, 1);
+            // Ôðàãè
+            e_TextureFontPrintEx(x+w1+w2+16, _y, IntToStr(Frags), gStdFont, rr, gg, bb, 1);
+            // Ñìåðòè
+            e_TextureFontPrintEx(x+w1+w2+w3+16, _y, IntToStr(Deaths), gStdFont, rr, gg, bb, 1);
+            _y := _y+ch;
+          end;
+
+          _y := _y+ch;
+    end;
+  end
+  else if gGameSettings.GameMode in [GM_DM, GM_COOP] then
+  begin
+    _y := _y+ch+ch;
+    e_TextureFontPrintEx(x+16, _y, _lc[I_GAME_PLAYER_NAME], gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+16+w1, _y, _lc[I_GAME_PING], gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+16+w1+w2, _y, _lc[I_GAME_FRAGS], gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+16+w1+w2+w3, _y, _lc[I_GAME_DEATHS], gStdFont, 255, 127, 0, 1);
+
+    _y := _y+ch+8;
+    for aa := 0 to High(stat) do
+      with stat[aa] do
+      begin
+        if Spectator then
+        begin
+          r := 127;
+          g := 64;
+        end
+        else
+        begin
+          r := 255;
+          g := 127;
+        end;
+        // Öâåò èãðîêà
+        e_DrawFillQuad(x+16, _y+4, x+32-1, _y+16+4-1, Color.R, Color.G, Color.B, 0);
+        e_DrawQuad(x+16, _y+4, x+32-1, _y+16+4-1, 192, 192, 192);
+        // Èìÿ
+        e_TextureFontPrintEx(x+16+16+8, _y+4, Name, gStdFont, r, g, 0, 1);
+        // Ïèíã/ïîòåðè
+        e_TextureFontPrintEx(x+w1+16, _y+4, Format(_lc[I_GAME_PING_MS], [Ping, Loss]), gStdFont, r, g, 0, 1);
+        // Ôðàãè
+        e_TextureFontPrintEx(x+w1+w2+16, _y+4, IntToStr(Frags), gStdFont, r, g, 0, 1);
+        // Ñìåðòè
+        e_TextureFontPrintEx(x+w1+w2+w3+16, _y+4, IntToStr(Deaths), gStdFont, r, g, 0, 1);
+        _y := _y+ch+8;
+      end;
+  end
+end;
+
+procedure g_Game_Init();
+var
+  SR: TSearchRec;
+begin
+  gExit := 0;
+  gMapToDelete := '';
+  gTempDelete := False;
+
+  g_Texture_CreateWADEx('MENU_BACKGROUND', GameWAD+':TEXTURES\TITLE');
+  g_Texture_CreateWADEx('INTER', GameWAD+':TEXTURES\INTER');
+  g_Texture_CreateWADEx('ENDGAME_EN', GameWAD+':TEXTURES\ENDGAME_EN');
+  g_Texture_CreateWADEx('ENDGAME_RU', GameWAD+':TEXTURES\ENDGAME_RU');
+
+  LoadStdFont('STDTXT', 'STDFONT', gStdFont);
+  LoadFont('MENUTXT', 'MENUFONT', gMenuFont);
+  LoadFont('SMALLTXT', 'SMALLFONT', gMenuSmallFont);
+
+  g_Game_ClearLoading();
+  g_Game_SetLoadingText(Format('Doom 2D: Forever %s', [GAME_VERSION]), 0, False);
+  g_Game_SetLoadingText('', 0, False);
+
+  g_Game_SetLoadingText(_lc[I_LOAD_CONSOLE], 0, False);
+  g_Console_Init();
+
+  g_Game_SetLoadingText(_lc[I_LOAD_MODELS], 0, False);
+  g_PlayerModel_LoadData();
+
+  if FindFirst(ModelsDir+'*.wad', faAnyFile, SR) = 0 then
+    repeat
+      if not g_PlayerModel_Load(ModelsDir+SR.Name) then
+        e_WriteLog(Format('Error loading model %s', [SR.Name]), MSG_WARNING);
+    until FindNext(SR) <> 0;
+  FindClose(SR);
+
+  gGameOn := False;
+  gPause := False;
+  gTime := 0;
+  LastScreenShot := 0;
+
+  {e_MouseInfo.Accel := 1.0;}
+
+  g_Game_SetLoadingText(_lc[I_LOAD_GAME_DATA], 0, False);
+  g_Game_LoadData();
+
+  g_Game_SetLoadingText(_lc[I_LOAD_MUSIC], 0, False);
+  g_Sound_CreateWADEx('MUSIC_INTERMUS', GameWAD+':MUSIC\INTERMUS', True);
+  g_Sound_CreateWADEx('MUSIC_MENU', GameWAD+':MUSIC\MENU', True);
+  g_Sound_CreateWADEx('MUSIC_ROUNDMUS', GameWAD+':MUSIC\ROUNDMUS');
+  g_Sound_CreateWADEx('MUSIC_STDENDMUS', GameWAD+':MUSIC\ENDMUS', True);
+
+  g_Game_SetLoadingText(_lc[I_LOAD_MENUS], 0, False);
+  g_Menu_Init();
+
+  gMusic := TMusic.Create();
+  gMusic.SetByName('MUSIC_MENU');
+  gMusic.Play();
+
+  gGameSettings.WarmupTime := 30;
+
+  gState := STATE_MENU;
+
+  SetLength(gEvents, 6);
+  gEvents[0].Name := 'ongamestart';
+  gEvents[1].Name := 'ongameend';
+  gEvents[2].Name := 'onmapstart';
+  gEvents[3].Name := 'onmapend';
+  gEvents[4].Name := 'oninter';
+  gEvents[5].Name := 'onwadend';
+end;
+
+procedure g_Game_Free();
+begin
+  if NetMode = NET_CLIENT then g_Net_Disconnect();
+  if NetMode = NET_SERVER then g_Net_Host_Die();
+
+  g_Map_Free();
+  g_Player_Free();
+  g_Player_RemoveAllCorpses();
+
+  gGameSettings.GameType := GT_NONE;
+  if gGameSettings.GameMode = GM_SINGLE then
+    gGameSettings.GameMode := GM_DM;
+  gSwitchGameMode := gGameSettings.GameMode;
+
+  gChatShow := False;
+  gExitByTrigger := False;
+end;
+
+function IsActivePlayer(p: TPlayer): Boolean;
+begin
+  Result := False;
+  if p = nil then
+    Exit;
+  Result := (not p.FDummy) and (not p.FSpectator);
+end;
+
+function GetActivePlayer_ByID(ID: Integer): TPlayer;
+var
+  a: Integer;
+begin
+  Result := nil;
+  if ID < 0 then
+    Exit;
+  if gPlayers = nil then
+    Exit;
+  for a := Low(gPlayers) to High(gPlayers) do
+    if IsActivePlayer(gPlayers[a]) then
+    begin
+      if gPlayers[a].UID <> ID then
+        continue;
+      Result := gPlayers[a];
+      break;
+    end;
+end;
+
+function GetActivePlayerID_Next(Skip: Integer = -1): Integer;
+var
+  a, idx: Integer;
+  ids: Array of Word;
+begin
+  Result := -1;
+  if gPlayers = nil then
+    Exit;
+  SetLength(ids, 0);
+  idx := -1;
+  for a := Low(gPlayers) to High(gPlayers) do
+    if IsActivePlayer(gPlayers[a]) then
+    begin
+      SetLength(ids, Length(ids) + 1);
+      ids[High(ids)] := gPlayers[a].UID;
+      if gPlayers[a].UID = Skip then
+        idx := High(ids);
+    end;
+  if Length(ids) = 0 then
+    Exit;
+  if idx = -1 then
+    Result := ids[0]
+  else
+    Result := ids[(idx + 1) mod Length(ids)];
+end;
+
+function GetActivePlayerID_Prev(Skip: Integer = -1): Integer;
+var
+  a, idx: Integer;
+  ids: Array of Word;
+begin
+  Result := -1;
+  if gPlayers = nil then
+    Exit;
+  SetLength(ids, 0);
+  idx := -1;
+  for a := Low(gPlayers) to High(gPlayers) do
+    if IsActivePlayer(gPlayers[a]) then
+    begin
+      SetLength(ids, Length(ids) + 1);
+      ids[High(ids)] := gPlayers[a].UID;
+      if gPlayers[a].UID = Skip then
+        idx := High(ids);
+    end;
+  if Length(ids) = 0 then
+    Exit;
+  if idx = -1 then
+    Result := ids[Length(ids) - 1]
+  else
+    Result := ids[(Length(ids) - 1 + idx) mod Length(ids)];
+end;
+
+procedure g_Game_Update();
+var
+  Msg: g_gui.TMessage;
+  Time: Int64;
+  a: Byte;
+  w: Word;
+  i, b: Integer;
+begin
+// Ïîðà âûêëþ÷àòü èãðó:
+  if gExit = EXIT_QUIT then
+    Exit;
+// Èãðà çàêîí÷èëàñü - îáðàáàòûâàåì:
+  if gExit <> 0 then
+  begin
+    EndGame();
+    if gExit = EXIT_QUIT then
+      Exit;
+  end;
+
+// ×èòàåì êëàâèàòóðó è äæîéñòèê, åñëè îêíî àêòèâíî:
+  e_PollInput();
+
+// Îáíîâëÿåì êîíñîëü (äâèæåíèå è ñîîáùåíèÿ):
+  g_Console_Update();
+
+  if (NetMode = NET_NONE) and (g_Game_IsNet) and (gGameOn or (gState in [STATE_FOLD, STATE_INTERCUSTOM])) then
+  begin
+    gExit := EXIT_SIMPLE;
+    EndGame();
+    Exit;
+  end;
+
+  case gState of
+    STATE_INTERSINGLE, // Ñòàòèñòêà ïîñëå ïðîõîæäåíèÿ óðîâíÿ â Îäèíî÷íîé èãðå
+    STATE_INTERCUSTOM, // Ñòàòèñòêà ïîñëå ïðîõîæäåíèÿ óðîâíÿ â Ñâîåé èãðå
+    STATE_INTERTEXT, // Òåêñò ìåæäó óðîâíÿìè
+    STATE_INTERPIC: // Êàðòèíêà ìåæäó óðîâíÿìè
+      begin
+        if g_Game_IsNet and g_Game_IsServer then
+        begin
+          gInterTime := gInterTime + GAME_TICK;
+          a := Min((gInterEndTime - gInterTime) div 1000 + 1, 255);
+          if a <> gServInterTime then
+          begin
+            gServInterTime := a;
+            MH_SEND_TimeSync(gServInterTime);
+          end;
+        end;
+
+        if (not g_Game_IsClient) and
+        (
+          (
+            (e_KeyPressed(IK_RETURN) or e_KeyPressed(IK_SPACE))
+            and (not gJustChatted) and (not gConsoleShow) and (not gChatShow)
+            and (g_ActiveWindow = nil)
+          )
+          or (g_Game_IsNet and (gInterTime > gInterEndTime))
+        )
+        then
+        begin // Íàæàëè <Enter>/<Ïðîáåë> èëè ïðîøëî äîñòàòî÷íî âðåìåíè:
+          g_Game_StopAllSounds(True);
+
+          if gMapOnce then // Ýòî áûë òåñò
+            gExit := EXIT_SIMPLE
+          else
+            if gNextMap <> '' then // Ïåðåõîäèì íà ñëåäóþùóþ êàðòó
+              g_Game_ChangeMap(gNextMap)
+            else // Ñëåäóþùåé êàðòû íåò
+            begin
+              if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER] then
+              begin
+              // Âûõîä â ãëàâíîå ìåíþ:
+                g_Game_Free;
+                g_GUI_ShowWindow('MainMenu');
+                gMusic.SetByName('MUSIC_MENU');
+                gMusic.Play();
+                gState := STATE_MENU;
+              end else
+              begin
+              // Ôèíàëüíàÿ êàðòèíêà:
+                g_Game_ExecuteEvent('onwadend');
+                g_Game_Free();
+                if not gMusic.SetByName('MUSIC_endmus') then
+                  gMusic.SetByName('MUSIC_STDENDMUS');
+                gMusic.Play();
+                gState := STATE_ENDPIC;
+              end;
+              g_Game_ExecuteEvent('ongameend');
+            end;
+
+          Exit;
+        end;
+
+        if gState = STATE_INTERTEXT then
+          if InterText.counter > 0 then
+            InterText.counter := InterText.counter - 1;
+      end;
+
+    STATE_FOLD: // Çàòóõàíèå ýêðàíà
+      begin
+        if EndingGameCounter = 0 then
+          begin
+          // Çàêîí÷èëñÿ óðîâåíü â Ñâîåé èãðå:
+            if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
+              begin
+                if gLastMap and (gGameSettings.GameMode = GM_COOP) then
+                begin
+                  g_Game_ExecuteEvent('onwadend');
+                  if not gMusic.SetByName('MUSIC_endmus') then
+                    gMusic.SetByName('MUSIC_STDENDMUS');
+                end
+                else
+                  gMusic.SetByName('MUSIC_ROUNDMUS');
+
+                gMusic.Play();
+                gState := STATE_INTERCUSTOM;
+              end
+            else // Çàêîí÷èëàñü ïîñëåäíÿÿ êàðòà â Îäèíî÷íîé èãðå
+              begin
+                gMusic.SetByName('MUSIC_INTERMUS');
+                gMusic.Play();
+                gState := STATE_INTERSINGLE;
+              end;
+            g_Game_ExecuteEvent('oninter');
+          end
+        else
+          DecMin(EndingGameCounter, 6, 0);
+      end;
+
+    STATE_ENDPIC: // Êàðòèíêà îêîí÷àíèÿ ìåãàÂàäà
+      begin
+        if gMapOnce then // Ýòî áûë òåñò
+        begin
+          gExit := EXIT_SIMPLE;
+          Exit;
+        end;
+      end;
+
+    STATE_SLIST:
+        g_Serverlist_Control(slCurrent);
+  end;
+
+  if g_Game_IsNet then
+    if not gConsoleShow then
+      if not gChatShow then
+      begin
+        if g_ActiveWindow = nil then
+        begin
+          if e_KeyPressed(gGameControls.GameControls.Chat) then
+            g_Console_Chat_Switch(False)
+          else if (e_KeyPressed(gGameControls.GameControls.TeamChat)) and
+                  (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+            g_Console_Chat_Switch(True);
+        end;
+      end else
+        if not gChatEnter then
+          if (not e_KeyPressed(gGameControls.GameControls.Chat))
+             and (not e_KeyPressed(gGameControls.GameControls.TeamChat)) then
+            gChatEnter := True;
+
+// Ñòàòèñòèêà ïî Tab:
+  if gGameOn then
+    IsDrawStat := (not gConsoleShow) and (not gChatShow) and
+                  (gGameSettings.GameType <> GT_SINGLE) and
+                  e_KeyPressed(gGameControls.GameControls.Stat);
+
+// Èãðà èäåò:
+  if gGameOn and not gPause and (gState <> STATE_FOLD) then
+  begin
+  // Âðåìÿ += 28 ìèëëèñåêóíä:
+    gTime := gTime + GAME_TICK;
+
+  // Ñîîáùåíèå ïîñåðåäèíå ýêðàíà:
+    if MessageTime = 0 then
+      MessageText := '';
+    if MessageTime > 0 then
+      MessageTime := MessageTime - 1;
+
+    if (g_Game_IsServer) then
+    begin
+    // Áûë çàäàí ëèìèò âðåìåíè:
+      if (gGameSettings.TimeLimit > 0) then
+        if (gTime - gGameStartTime) div 1000 >= gGameSettings.TimeLimit then
+        begin // Îí ïðîøåë => êîíåö óðîâíÿ
+          g_Game_NextLevel();
+          Exit;
+        end;
+
+    // Íàäî ðåñïàâíèòü èãðîêîâ â LMS:
+      if (gLMSRespawn > LMS_RESPAWN_NONE) and (gLMSRespawnTime < gTime) then
+        g_Game_RestartRound(gLMSSoftSpawn);
+
+    // Ïðîâåðèì ðåçóëüòàò ãîëîñîâàíèÿ, åñëè âðåìÿ ïðîøëî
+      if gVoteInProgress and (gVoteTimer < gTime) then
+        g_Game_CheckVote
+      else if gVotePassed and (gVoteCmdTimer < gTime) then
+      begin
+        g_Console_Process(gVoteCommand);
+        gVoteCommand := '';
+        gVotePassed := False;
+      end;
+
+    // Çàìåðÿåì âðåìÿ çàõâàòà ôëàãîâ
+      if gFlags[FLAG_RED].State = FLAG_STATE_CAPTURED then
+        gFlags[FLAG_RED].CaptureTime := gFlags[FLAG_RED].CaptureTime + GAME_TICK;
+      if gFlags[FLAG_BLUE].State = FLAG_STATE_CAPTURED then
+        gFlags[FLAG_BLUE].CaptureTime := gFlags[FLAG_BLUE].CaptureTime + GAME_TICK;
+
+    // Áûë çàäàí ëèìèò ïîáåä:
+      if (gGameSettings.GoalLimit > 0) then
+      begin
+        b := 0;
+
+        if gGameSettings.GameMode = GM_DM then
+          begin // Â DM èùåì èãðîêà ñ max ôðàãàìè
+            for i := 0 to High(gPlayers) do
+              if gPlayers[i] <> nil then
+                if gPlayers[i].Frags > b then
+                  b := gPlayers[i].Frags;
+          end
+        else
+          if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+          begin // Â CTF/TDM âûáèðàåì êîìàíäó ñ íàèáîëüøèì ñ÷åòîì
+            b := Max(gTeamStat[TEAM_RED].Goals, gTeamStat[TEAM_BLUE].Goals);
+          end;
+
+      // Ëèìèò ïîáåä íàáðàí => êîíåö óðîâíÿ:
+        if b >= gGameSettings.GoalLimit then
+        begin
+          g_Game_NextLevel();
+          Exit;
+        end;
+      end;
+
+    // Îáðàáàòûâàåì êëàâèøè èãðîêîâ:
+      if gPlayer1 <> nil then gPlayer1.ReleaseKeys();
+      if gPlayer2 <> nil then gPlayer2.ReleaseKeys();
+      if (not gConsoleShow) and (not gChatShow) and (g_ActiveWindow = nil) then
+      begin
+      // Ïåðâûé èãðîê:
+        if gPlayer1 <> nil then
+          with gGameControls.P1Control do
+          begin
+            if e_KeyPressed(KeyLeft) and (not e_KeyPressed(KeyRight)) then
+              P1MoveButton := 1 // Íàæàòà òîëüêî "Âëåâî"
+            else
+              if (not e_KeyPressed(KeyLeft)) and e_KeyPressed(KeyRight) then
+                P1MoveButton := 2 // Íàæàòà òîëüêî "Âïðàâî"
+              else
+                if (not e_KeyPressed(KeyLeft)) and (not e_KeyPressed(KeyRight)) then
+                  P1MoveButton := 0; // Íå íàæàòû íè "Âëåâî", íè "Âïðàâî"
+
+          // Ñåé÷àñ èëè ðàíüøå áûëè íàæàòû "Âëåâî"/"Âïðàâî" => ïåðåäàåì èãðîêó:
+            if P1MoveButton = 1 then
+              gPlayer1.PressKey(KEY_LEFT)
+            else
+            if P1MoveButton = 2 then
+              gPlayer1.PressKey(KEY_RIGHT);
+
+          // Ðàíüøå áûëà íàæàòà "Âïðàâî", à ñåé÷àñ "Âëåâî" => áåæèì âïðàâî, ñìîòðèì âëåâî:
+            if (P1MoveButton = 2) and e_KeyPressed(KeyLeft) then
+              gPlayer1.SetDirection(D_LEFT)
+            else
+            // Ðàíüøå áûëà íàæàòà "Âëåâî", à ñåé÷àñ "Âïðàâî" => áåæèì âëåâî, ñìîòðèì âïðàâî:
+              if (P1MoveButton = 1) and e_KeyPressed(KeyRight) then
+                gPlayer1.SetDirection(D_RIGHT)
+              else
+              // ×òî-òî áûëî íàæàòî è íå èçìåíèëîñü => êóäà áåæèì, òóäà è ñìîòðèì:
+                if P1MoveButton <> 0 then
+                  gPlayer1.SetDirection(TDirection(P1MoveButton-1));
+
+          // Îñòàëüíûå êëàâèøè:
+            if e_KeyPressed(KeyJump) then gPlayer1.PressKey(KEY_JUMP);
+            if e_KeyPressed(KeyUp) then gPlayer1.PressKey(KEY_UP);
+            if e_KeyPressed(KeyDown) then gPlayer1.PressKey(KEY_DOWN);
+            if e_KeyPressed(KeyFire) then gPlayer1.PressKey(KEY_FIRE);
+            if e_KeyPressed(KeyNextWeapon) then gPlayer1.PressKey(KEY_NEXTWEAPON);
+            if e_KeyPressed(KeyPrevWeapon) then gPlayer1.PressKey(KEY_PREVWEAPON);
+            if e_KeyPressed(KeyOpen) then gPlayer1.PressKey(KEY_OPEN);
+          end;
+      // Âòîðîé èãðîê:
+        if gPlayer2 <> nil then
+          with gGameControls.P2Control do
+          begin
+            if e_KeyPressed(KeyLeft) and (not e_KeyPressed(KeyRight)) then
+              P2MoveButton := 1 // Íàæàòà òîëüêî "Âëåâî"
+            else
+              if (not e_KeyPressed(KeyLeft)) and e_KeyPressed(KeyRight) then
+                P2MoveButton := 2 // Íàæàòà òîëüêî "Âïðàâî"
+              else
+                if (not e_KeyPressed(KeyLeft)) and (not e_KeyPressed(KeyRight)) then
+                  P2MoveButton := 0; // Íå íàæàòû íè "Âëåâî", íè "Âïðàâî"
+
+          // Ñåé÷àñ èëè ðàíüøå áûëè íàæàòû "Âëåâî"/"Âïðàâî" => ïåðåäàåì èãðîêó:
+            if P2MoveButton = 1 then
+              gPlayer2.PressKey(KEY_LEFT, 1000)
+            else
+              if P2MoveButton = 2 then
+                gPlayer2.PressKey(KEY_RIGHT, 1000);
+
+          // Ðàíüøå áûëà íàæàòà "Âïðàâî", à ñåé÷àñ "Âëåâî" => áåæèì âïðàâî, ñìîòðèì âëåâî:
+            if (P2MoveButton = 2) and e_KeyPressed(KeyLeft) then
+              gPlayer2.SetDirection(D_LEFT)
+            else
+            // Ðàíüøå áûëà íàæàòà "Âëåâî", à ñåé÷àñ "Âïðàâî" => áåæèì âëåâî, ñìîòðèì âïðàâî:
+              if (P2MoveButton = 1) and e_KeyPressed(KeyRight) then
+                gPlayer2.SetDirection(D_RIGHT)
+              else
+              // ×òî-òî áûëî íàæàòî è íå èçìåíèëîñü => êóäà áåæèì, òóäà è ñìîòðèì:
+                if P2MoveButton <> 0 then
+                  gPlayer2.SetDirection(TDirection(P2MoveButton-1));
+
+          // Îñòàëüíûå êëàâèøè:
+            if e_KeyPressed(KeyJump) then gPlayer2.PressKey(KEY_JUMP, 1000);
+            if e_KeyPressed(KeyUp) then gPlayer2.PressKey(KEY_UP, 1000);
+            if e_KeyPressed(KeyDown) then gPlayer2.PressKey(KEY_DOWN, 1000);
+            if e_KeyPressed(KeyFire) then gPlayer2.PressKey(KEY_FIRE);
+            if e_KeyPressed(KeyNextWeapon) then gPlayer2.PressKey(KEY_NEXTWEAPON);
+            if e_KeyPressed(KeyPrevWeapon) then gPlayer2.PressKey(KEY_PREVWEAPON);
+            if e_KeyPressed(KeyOpen) then gPlayer2.PressKey(KEY_OPEN);
+          end;
+      end  // if not console
+      else
+        if g_Game_IsNet and (gPlayer1 <> nil) then
+          gPlayer1.PressKey(KEY_CHAT, 10000);
+
+    end; // if server
+
+  // Íàáëþäàòåëü
+    if (gPlayer1 = nil) and (gPlayer2 = nil) and
+       (not gConsoleShow) and (not gChatShow) and (g_ActiveWindow = nil) then
+    begin
+      if not gSpectKeyPress then
+      begin
+        if e_KeyPressed(gGameControls.P1Control.KeyJump) then
+        begin
+          // switch spect mode
+          case gSpectMode of
+            SPECT_NONE: ; // not spectator
+            SPECT_STATS,
+            SPECT_MAPVIEW: Inc(gSpectMode);
+            SPECT_PLAYERS: gSpectMode := SPECT_STATS; // reset to 1
+          end;
+          gSpectKeyPress := True;
+        end;
+        if gSpectMode = SPECT_MAPVIEW then
+        begin
+          if e_KeyPressed(gGameControls.P1Control.KeyLeft) then
+            gSpectX := Max(gSpectX - gSpectStep, 0);
+          if e_KeyPressed(gGameControls.P1Control.KeyRight) then
+            gSpectX := Min(gSpectX + gSpectStep, gMapInfo.Width - gScreenWidth);
+          if e_KeyPressed(gGameControls.P1Control.KeyUp) then
+            gSpectY := Max(gSpectY - gSpectStep, 0);
+          if e_KeyPressed(gGameControls.P1Control.KeyDown) then
+            gSpectY := Min(gSpectY + gSpectStep, gMapInfo.Height - gScreenHeight);
+          if e_KeyPressed(gGameControls.P1Control.KeyPrevWeapon) then
+          begin
+            // decrease step
+            if gSpectStep > 4 then gSpectStep := gSpectStep shr 1;
+            gSpectKeyPress := True;
+          end;
+          if e_KeyPressed(gGameControls.P1Control.KeyNextWeapon) then
+          begin
+            // increase step
+            if gSpectStep < 64 then gSpectStep := gSpectStep shl 1;
+            gSpectKeyPress := True;
+          end;
+        end;
+        if gSpectMode = SPECT_PLAYERS then
+        begin
+          if e_KeyPressed(gGameControls.P1Control.KeyUp) then
+          begin
+            // add second view
+            gSpectViewTwo := True;
+            gSpectKeyPress := True;
+          end;
+          if e_KeyPressed(gGameControls.P1Control.KeyDown) then
+          begin
+            // remove second view
+            gSpectViewTwo := False;
+            gSpectKeyPress := True;
+          end;
+          if e_KeyPressed(gGameControls.P1Control.KeyLeft) then
+          begin
+            // prev player (view 1)
+            gSpectPID1 := GetActivePlayerID_Prev(gSpectPID1);
+            gSpectKeyPress := True;
+          end;
+          if e_KeyPressed(gGameControls.P1Control.KeyRight) then
+          begin
+            // next player (view 1)
+            gSpectPID1 := GetActivePlayerID_Next(gSpectPID1);
+            gSpectKeyPress := True;
+          end;
+          if e_KeyPressed(gGameControls.P1Control.KeyPrevWeapon) then
+          begin
+            // prev player (view 2)
+            gSpectPID2 := GetActivePlayerID_Prev(gSpectPID2);
+            gSpectKeyPress := True;
+          end;
+          if e_KeyPressed(gGameControls.P1Control.KeyNextWeapon) then
+          begin
+            // next player (view 2)
+            gSpectPID2 := GetActivePlayerID_Next(gSpectPID2);
+            gSpectKeyPress := True;
+          end;
+        end;
+      end
+      else
+        if (not e_KeyPressed(gGameControls.P1Control.KeyJump)) and
+           (not e_KeyPressed(gGameControls.P1Control.KeyLeft)) and
+           (not e_KeyPressed(gGameControls.P1Control.KeyRight)) and
+           (not e_KeyPressed(gGameControls.P1Control.KeyUp)) and
+           (not e_KeyPressed(gGameControls.P1Control.KeyDown)) and
+           (not e_KeyPressed(gGameControls.P1Control.KeyPrevWeapon)) and
+           (not e_KeyPressed(gGameControls.P1Control.KeyNextWeapon)) then
+          gSpectKeyPress := False;
+    end;
+
+  // Îáíîâëÿåì âñå îñòàëüíîå:
+    g_Map_Update();
+    g_Items_Update();
+    g_Triggers_Update();
+    g_Weapon_Update();
+    g_Monsters_Update();
+    g_GFX_Update();
+    g_Player_UpdateAll();
+    g_Player_UpdatePhysicalObjects();
+    if gGameSettings.GameType = GT_SERVER then
+      if Length(gMonstersSpawned) > 0 then
+      begin
+        for I := 0 to High(gMonstersSpawned) do
+          MH_SEND_MonsterSpawn(gMonstersSpawned[I]);
+        SetLength(gMonstersSpawned, 0);
+      end;
+
+    if (gSoundTriggerTime > 8) then
+    begin
+      g_Game_UpdateTriggerSounds();
+      gSoundTriggerTime := 0;
+    end
+    else
+      Inc(gSoundTriggerTime);
+
+    if (NetMode = NET_SERVER) then
+    begin
+      Inc(NetTimeToUpdate);
+      Inc(NetTimeToReliable);
+      if NetTimeToReliable >= NetRelupdRate then
+      begin
+        for I := 0 to High(gPlayers) do
+          if gPlayers[I] <> nil then
+            MH_SEND_PlayerPos(True, gPlayers[I].UID);
+
+        if gMonsters <> nil then
+          for I := 0 to High(gMonsters) do
+            if gMonsters[I] <> nil then
+            begin
+              if (gMonsters[I].MonsterType = MONSTER_BARREL) then
+              begin
+                if (gMonsters[I].GameVelX <> 0) or (gMonsters[I].GameVelY <> 0) then
+                  MH_SEND_MonsterPos(gMonsters[I].UID);
+              end
+              else
+                if (gMonsters[I].MonsterState <> MONSTATE_SLEEP) then
+                  if (gMonsters[I].MonsterState <> MONSTATE_DEAD) or
+                     (gMonsters[I].GameVelX <> 0) or
+                     (gMonsters[I].GameVelY <> 0) then
+                  MH_SEND_MonsterPos(gMonsters[I].UID);
+            end;
+
+        NetTimeToReliable := 0;
+        NetTimeToUpdate := NetUpdateRate;
+      end
+      else if NetTimeToUpdate >= NetUpdateRate then
+      begin
+        if gPlayers <> nil then
+          for I := 0 to High(gPlayers) do
+            if gPlayers[I] <> nil then
+              MH_SEND_PlayerPos(False, gPlayers[I].UID);
+
+        if gMonsters <> nil then
+          for I := 0 to High(gMonsters) do
+            if gMonsters[I] <> nil then
+            begin
+              if (gMonsters[I].MonsterType = MONSTER_BARREL) then
+              begin
+                if (gMonsters[I].GameVelX <> 0) or (gMonsters[I].GameVelY <> 0) then
+                  MH_SEND_MonsterPos(gMonsters[I].UID);
+              end
+              else
+                if (gMonsters[I].MonsterState <> MONSTATE_SLEEP) then
+                  if (gMonsters[I].MonsterState <> MONSTATE_DEAD) or
+                     (gMonsters[I].GameVelX <> 0) or
+                     (gMonsters[I].GameVelY <> 0) then
+                  MH_SEND_MonsterPos(gMonsters[I].UID);
+            end;
+
+        NetTimeToUpdate := 0;
+      end;
+
+      if NetUseMaster then
+        if gTime >= NetTimeToMaster then
+        begin
+          if (NetMHost = nil) or (NetMPeer = nil) then
+            if not g_Net_Slist_Connect then
+              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR]);
+
+          g_Net_Slist_Update;
+          NetTimeToMaster := gTime + NetMasterRate;
+        end;
+    end
+    else
+      if NetMode = NET_CLIENT then
+        MC_SEND_PlayerPos();
+  end; // if gameOn ...
+
+// Àêòèâíî îêíî èíòåðôåéñà - ïåðåäàåì êëàâèøè åìó:
+  if g_ActiveWindow <> nil then
+  begin
+    w := e_GetFirstKeyPressed();
+
+    if (w <> IK_INVALID) then
+      begin
+        Msg.Msg := MESSAGE_DIKEY;
+        Msg.wParam := w;
+        g_ActiveWindow.OnMessage(Msg);
+      end;
+
+  // Åñëè îíî îò ýòîãî íå çàêðûëîñü, òî îáíîâëÿåì:
+    if g_ActiveWindow <> nil then
+      g_ActiveWindow.Update();
+
+  // Íóæíî ñìåíèòü ðàçðåøåíèå:
+    if gResolutionChange then
+    begin
+      e_WriteLog('Changing resolution', MSG_NOTIFY);
+      g_Game_ChangeResolution(gRC_Width, gRC_Height, gRC_FullScreen, gRC_Maximized);
+      gResolutionChange := False;
+    end;
+
+  // Íóæíî ñìåíèòü ÿçûê:
+    if gLanguageChange then
+    begin
+      //e_WriteLog('Read language file', MSG_NOTIFY);
+      //g_Language_Load(DataDir + gLanguage + '.txt');
+      g_Language_Set(gLanguage);
+      g_Menu_Reset();
+      gLanguageChange := False;
+    end;
+  end;
+
+// Äåëàåì ñêðèíøîò (íå ÷àùå 200 ìèëëèñåêóíä):
+  if e_KeyPressed(gGameControls.GameControls.TakeScreenshot) then
+    if (GetTimer()-LastScreenShot) > 200000 then
+    begin
+      g_TakeScreenShot();
+      LastScreenShot := GetTimer();
+    end;
+
+// Ãîðÿ÷àÿ êëàâèøà äëÿ âûçîâà ìåíþ âûõîäà èç èãðû (F10):
+  if e_KeyPressed(IK_F10) and
+     gGameOn and
+     (not gConsoleShow) and
+     (g_ActiveWindow = nil) then
+  begin
+    KeyPress(IK_F10);
+  end;
+
+  Time := GetTimer() div 1000;
+
+// Îáðàáîòêà îòëîæåííûõ ñîáûòèé:
+  if gDelayedEvents <> nil then
+    for a := 0 to High(gDelayedEvents) do
+      if gDelayedEvents[a].Pending and
+      (
+        ((gDelayedEvents[a].DEType = DE_GLOBEVENT) and (gDelayedEvents[a].Time <= Time)) or
+        ((gDelayedEvents[a].DEType > DE_GLOBEVENT) and (gDelayedEvents[a].Time <= gTime))
+      ) then
+      begin
+        case gDelayedEvents[a].DEType of
+          DE_GLOBEVENT:
+            g_Game_ExecuteEvent(gDelayedEvents[a].DEStr);
+          DE_BFGHIT:
+            if gGameOn then
+              g_Game_Announce_GoodShot(gDelayedEvents[a].DENum);
+          DE_KILLCOMBO:
+            if gGameOn then
+            begin
+              g_Game_Announce_KillCombo(gDelayedEvents[a].DENum);
+              if g_Game_IsNet and g_Game_IsServer then
+                MH_SEND_GameEvent(NET_EV_KILLCOMBO, gDelayedEvents[a].DENum);
+            end;
+        end;
+        gDelayedEvents[a].Pending := False;
+      end;
+
+// Êàæäóþ ñåêóíäó îáíîâëÿåì ñ÷åò÷èê îáíîâëåíèé:
+  UPSCounter := UPSCounter + 1;
+  if Time - UPSTime >= 1000 then
+  begin
+    UPS := UPSCounter;
+    UPSCounter := 0;
+    UPSTime := Time;
+  end;
+end;
+
+procedure g_Game_LoadData();
+begin
+  if DataLoaded then Exit;
+
+  e_WriteLog('Loading game data...', MSG_NOTIFY);
+
+  g_Texture_CreateWADEx('NOTEXTURE', GameWAD+':TEXTURES\NOTEXTURE');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_HUD', GameWAD+':TEXTURES\HUD');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_HUDAIR', GameWAD+':TEXTURES\AIRBAR');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_HUDJET', GameWAD+':TEXTURES\JETBAR');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_HUDBG', GameWAD+':TEXTURES\HUDBG');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_ARMORHUD', GameWAD+':TEXTURES\ARMORHUD');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG', GameWAD+':TEXTURES\FLAGHUD_RB');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG_S', GameWAD+':TEXTURES\FLAGHUD_RS');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_REDFLAG_D', GameWAD+':TEXTURES\FLAGHUD_RD');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG', GameWAD+':TEXTURES\FLAGHUD_BB');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG_S', GameWAD+':TEXTURES\FLAGHUD_BS');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_BLUEFLAG_D', GameWAD+':TEXTURES\FLAGHUD_BD');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_TALKBUBBLE', GameWAD+':TEXTURES\TALKBUBBLE');
+  g_Texture_CreateWADEx('TEXTURE_PLAYER_INVULPENTA', GameWAD+':TEXTURES\PENTA');
+  g_Frames_CreateWAD(nil, 'FRAMES_TELEPORT', GameWAD+':TEXTURES\TELEPORT', 64, 64, 10, False);
+  g_Sound_CreateWADEx('SOUND_GAME_TELEPORT', GameWAD+':SOUNDS\TELEPORT');
+  g_Sound_CreateWADEx('SOUND_GAME_NOTELEPORT', GameWAD+':SOUNDS\NOTELEPORT');
+  g_Sound_CreateWADEx('SOUND_GAME_DOOROPEN', GameWAD+':SOUNDS\DOOROPEN');
+  g_Sound_CreateWADEx('SOUND_GAME_DOORCLOSE', GameWAD+':SOUNDS\DOORCLOSE');
+  g_Sound_CreateWADEx('SOUND_GAME_BULK1', GameWAD+':SOUNDS\BULK1');
+  g_Sound_CreateWADEx('SOUND_GAME_BULK2', GameWAD+':SOUNDS\BULK2');
+  g_Sound_CreateWADEx('SOUND_GAME_BUBBLE1', GameWAD+':SOUNDS\BUBBLE1');
+  g_Sound_CreateWADEx('SOUND_GAME_BUBBLE2', GameWAD+':SOUNDS\BUBBLE2');
+  g_Sound_CreateWADEx('SOUND_GAME_SWITCH1', GameWAD+':SOUNDS\SWITCH1');
+  g_Sound_CreateWADEx('SOUND_GAME_SWITCH0', GameWAD+':SOUNDS\SWITCH0');
+  g_Sound_CreateWADEx('SOUND_GAME_RADIO', GameWAD+':SOUNDS\RADIO');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_GOOD1', GameWAD+':SOUNDS\GOOD1');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_GOOD2', GameWAD+':SOUNDS\GOOD2');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_GOOD3', GameWAD+':SOUNDS\GOOD3');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_GOOD4', GameWAD+':SOUNDS\GOOD4');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILL2X', GameWAD+':SOUNDS\KILL2X');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILL3X', GameWAD+':SOUNDS\KILL3X');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILL4X', GameWAD+':SOUNDS\KILL4X');
+  g_Sound_CreateWADEx('SOUND_ANNOUNCER_KILLMX', GameWAD+':SOUNDS\KILLMX');
+
+  goodsnd[0] := TPlayableSound.Create();
+  goodsnd[1] := TPlayableSound.Create();
+  goodsnd[2] := TPlayableSound.Create();
+  goodsnd[3] := TPlayableSound.Create();
+
+  goodsnd[0].SetByName('SOUND_ANNOUNCER_GOOD1');
+  goodsnd[1].SetByName('SOUND_ANNOUNCER_GOOD2');
+  goodsnd[2].SetByName('SOUND_ANNOUNCER_GOOD3');
+  goodsnd[3].SetByName('SOUND_ANNOUNCER_GOOD4');
+
+  killsnd[0] := TPlayableSound.Create();
+  killsnd[1] := TPlayableSound.Create();
+  killsnd[2] := TPlayableSound.Create();
+  killsnd[3] := TPlayableSound.Create();
+
+  killsnd[0].SetByName('SOUND_ANNOUNCER_KILL2X');
+  killsnd[1].SetByName('SOUND_ANNOUNCER_KILL3X');
+  killsnd[2].SetByName('SOUND_ANNOUNCER_KILL4X');
+  killsnd[3].SetByName('SOUND_ANNOUNCER_KILLMX');
+
+  g_Game_SetLoadingText(_lc[I_LOAD_ITEMS_DATA], 0, False);
+  g_Items_LoadData();
+
+  g_Game_SetLoadingText(_lc[I_LOAD_WEAPONS_DATA], 0, False);
+  g_Weapon_LoadData();
+
+  g_Monsters_LoadData();
+
+  DataLoaded := True;
+end;
+
+procedure g_Game_FreeData();
+begin
+  if not DataLoaded then Exit;
+
+  g_Items_FreeData();
+  g_Weapon_FreeData();
+  g_Monsters_FreeData();
+
+  e_WriteLog('Releasing game data...', MSG_NOTIFY);
+
+  g_Texture_Delete('NOTEXTURE');
+  g_Texture_Delete('TEXTURE_PLAYER_HUD');
+  g_Texture_Delete('TEXTURE_PLAYER_HUDBG');
+  g_Texture_Delete('TEXTURE_PLAYER_ARMORHUD');
+  g_Texture_Delete('TEXTURE_PLAYER_REDFLAG');
+  g_Texture_Delete('TEXTURE_PLAYER_REDFLAG_S');
+  g_Texture_Delete('TEXTURE_PLAYER_REDFLAG_D');
+  g_Texture_Delete('TEXTURE_PLAYER_BLUEFLAG');
+  g_Texture_Delete('TEXTURE_PLAYER_BLUEFLAG_S');
+  g_Texture_Delete('TEXTURE_PLAYER_BLUEFLAG_D');
+  g_Texture_Delete('TEXTURE_PLAYER_TALKBUBBLE');
+  g_Texture_Delete('TEXTURE_PLAYER_INVULPENTA');
+  g_Frames_DeleteByName('FRAMES_TELEPORT');
+  g_Sound_Delete('SOUND_GAME_TELEPORT');
+  g_Sound_Delete('SOUND_GAME_NOTELEPORT');
+  g_Sound_Delete('SOUND_GAME_DOOROPEN');
+  g_Sound_Delete('SOUND_GAME_DOORCLOSE');
+  g_Sound_Delete('SOUND_GAME_BULK1');
+  g_Sound_Delete('SOUND_GAME_BULK2');
+  g_Sound_Delete('SOUND_GAME_BUBBLE1');
+  g_Sound_Delete('SOUND_GAME_BUBBLE2');
+  g_Sound_Delete('SOUND_GAME_SWITCH1');
+  g_Sound_Delete('SOUND_GAME_SWITCH0');
+
+  goodsnd[0].Free();
+  goodsnd[1].Free();
+  goodsnd[2].Free();
+  goodsnd[3].Free();
+
+  g_Sound_Delete('SOUND_ANNOUNCER_GOOD1');
+  g_Sound_Delete('SOUND_ANNOUNCER_GOOD2');
+  g_Sound_Delete('SOUND_ANNOUNCER_GOOD3');
+  g_Sound_Delete('SOUND_ANNOUNCER_GOOD4');
+
+  killsnd[0].Free();
+  killsnd[1].Free();
+  killsnd[2].Free();
+  killsnd[3].Free();
+
+  g_Sound_Delete('SOUND_ANNOUNCER_KILL2X');
+  g_Sound_Delete('SOUND_ANNOUNCER_KILL3X');
+  g_Sound_Delete('SOUND_ANNOUNCER_KILL4X');
+  g_Sound_Delete('SOUND_ANNOUNCER_KILLMX');
+
+  DataLoaded := False;
+end;
+
+procedure DrawCustomStat();
+var
+  pc, x, y, w, _y,
+  w1, w2, w3,
+  t, p, m: Integer;
+  ww1, hh1: Word;
+  ww2, hh2, r, g, b, rr, gg, bb: Byte;
+  s1, s2, topstr: String;
+begin
+  e_TextureFontGetSize(gStdFont, ww2, hh2);
+
+  e_PollInput();
+  if e_KeyPressed(IK_TAB) then
+  begin
+    if not gStatsPressed then
+    begin
+      gStatsOff := not gStatsOff;
+      gStatsPressed := True;
+    end;
+  end
+  else
+    gStatsPressed := False;
+
+  if gStatsOff then
+  begin
+    s1 := _lc[I_MENU_INTER_NOTICE_TAB];
+    w := (Length(s1) * ww2) div 2;
+    x := gScreenWidth div 2 - w;
+    y := 8;
+    e_TextureFontPrint(x, y, s1, gStdFont);
+    Exit;
+  end;
+
+  if (gGameSettings.GameMode = GM_COOP) then
+  begin
+    if gMissionFailed then
+      topstr := _lc[I_MENU_INTER_MISSION_FAIL]
+    else
+      topstr := _lc[I_MENU_INTER_LEVEL_COMPLETE];
+  end
+  else
+    topstr := _lc[I_MENU_INTER_ROUND_OVER];
+
+  e_CharFont_GetSize(gMenuFont, topstr, ww1, hh1);
+  e_CharFont_Print(gMenuFont, (gScreenWidth div 2)-(ww1 div 2), 16, topstr);
+
+  if g_Game_IsNet then
+  begin
+    topstr := Format(_lc[I_MENU_INTER_NOTICE_TIME], [gServInterTime]);
+    if not gChatShow then
+      e_TextureFontPrintEx((gScreenWidth div 2)-(Length(topstr)*ww2 div 2),
+                           gScreenHeight-(hh2+4)*2, topstr, gStdFont, 255, 255, 255, 1);
+  end;
+
+  if g_Game_IsClient then
+    topstr := _lc[I_MENU_INTER_NOTICE_MAP]
+  else
+    topstr := _lc[I_MENU_INTER_NOTICE_SPACE];
+  if not gChatShow then
+    e_TextureFontPrintEx((gScreenWidth div 2)-(Length(topstr)*ww2 div 2),
+                         gScreenHeight-(hh2+4), topstr, gStdFont, 255, 255, 255, 1);
+
+  x := 32;
+  y := 16+hh1+16;
+
+  w := gScreenWidth-x*2;
+
+  w2 := (w-16) div 6;
+  w3 := w2;
+  w1 := w-16-w2-w3;
+
+  e_DrawFillQuad(x, y, gScreenWidth-x-1, gScreenHeight-y-1, 64, 64, 64, 32);
+  e_DrawQuad(x, y, gScreenWidth-x-1, gScreenHeight-y-1, 255, 127, 0);
+
+  m := Max(Length(_lc[I_MENU_MAP])+1, Length(_lc[I_GAME_GAME_TIME])+1)*ww2;
+
+  case CustomStat.GameMode of
+    GM_DM:
+    begin
+      if gGameSettings.MaxLives = 0 then
+        s1 := _lc[I_GAME_DM]
+      else
+        s1 := _lc[I_GAME_LMS];
+    end;
+    GM_TDM:
+    begin
+      if gGameSettings.MaxLives = 0 then
+        s1 := _lc[I_GAME_TDM]
+      else
+        s1 := _lc[I_GAME_TLMS];
+    end;
+    GM_CTF: s1 := _lc[I_GAME_CTF];
+    GM_COOP:
+    begin
+      if gGameSettings.MaxLives = 0 then
+        s1 := _lc[I_GAME_COOP]
+      else
+        s1 := _lc[I_GAME_SURV];
+    end;
+    else s1 := '';
+  end;
+
+  _y := y+16;
+  e_TextureFontPrintEx(x+(w div 2)-(Length(s1)*ww2 div 2), _y, s1, gStdFont, 255, 255, 255, 1);
+  _y := _y+8;
+
+  _y := _y+16;
+  e_TextureFontPrintEx(x+8, _y, _lc[I_MENU_MAP], gStdFont, 255, 127, 0, 1);
+  e_TextureFontPrint(x+8+m, _y, Format('%s - %s', [CustomStat.Map, CustomStat.MapName]), gStdFont);
+
+  _y := _y+16;
+  e_TextureFontPrintEx(x+8, _y, _lc[I_GAME_GAME_TIME], gStdFont, 255, 127, 0, 1);
+  e_TextureFontPrint(x+8+m, _y, Format('%d:%.2d:%.2d', [CustomStat.GameTime div 1000 div 3600,
+                                                       (CustomStat.GameTime div 1000 div 60) mod 60,
+                                                        CustomStat.GameTime div 1000 mod 60]), gStdFont);
+
+  pc := Length(CustomStat.PlayerStat);
+  if pc = 0 then Exit;
+
+  if CustomStat.GameMode = GM_COOP then
+  begin
+    m := Max(Length(_lc[I_GAME_MONSTERS])+1, Length(_lc[I_GAME_SECRETS])+1)*ww2;
+    _y := _y+32;
+    s2 := _lc[I_GAME_MONSTERS];
+    e_TextureFontPrintEx(x+8, _y, s2, gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+8+m, _y, IntToStr(gCoopMonstersKilled) + '/' + IntToStr(gTotalMonsters), gStdFont, 255, 255, 255, 1);
+    _y := _y+16;
+    s2 := _lc[I_GAME_SECRETS];
+    e_TextureFontPrintEx(x+8, _y, s2, gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+8+m, _y, IntToStr(gCoopSecretsFound) + '/' + IntToStr(gSecretsCount), gStdFont, 255, 255, 255, 1);
+    if gLastMap then
+    begin
+      m := Max(Length(_lc[I_GAME_MONSTERS_TOTAL])+1, Length(_lc[I_GAME_SECRETS_TOTAL])+1)*ww2;
+      _y := _y-16;
+      s2 := _lc[I_GAME_MONSTERS_TOTAL];
+      e_TextureFontPrintEx(x+250, _y, s2, gStdFont, 255, 127, 0, 1);
+      e_TextureFontPrintEx(x+250+m, _y, IntToStr(gCoopTotalMonstersKilled) + '/' + IntToStr(gCoopTotalMonsters), gStdFont, 255, 255, 255, 1);
+      _y := _y+16;
+      s2 := _lc[I_GAME_SECRETS_TOTAL];
+      e_TextureFontPrintEx(x+250, _y, s2, gStdFont, 255, 127, 0, 1);
+      e_TextureFontPrintEx(x+250+m, _y, IntToStr(gCoopTotalSecretsFound) + '/' + IntToStr(gCoopTotalSecrets), gStdFont, 255, 255,  255, 1);
+    end;
+  end;
+
+  if CustomStat.GameMode in [GM_TDM, GM_CTF] then
+  begin
+    _y := _y+16+16;
+
+    with CustomStat do
+      if TeamStat[TEAM_RED].Goals > TeamStat[TEAM_BLUE].Goals then s1 := _lc[I_GAME_WIN_RED]
+        else if TeamStat[TEAM_BLUE].Goals > TeamStat[TEAM_RED].Goals then s1 := _lc[I_GAME_WIN_BLUE]
+          else s1 := _lc[I_GAME_WIN_DRAW];
+
+    e_TextureFontPrintEx(x+8+(w div 2)-(Length(s1)*ww2 div 2), _y, s1, gStdFont, 255, 255, 255, 1);
+    _y := _y+40;
+
+    for t := TEAM_RED to TEAM_BLUE do
+    begin
+      if t = TEAM_RED then
+      begin
+        e_TextureFontPrintEx(x+8, _y, _lc[I_GAME_TEAM_RED],
+                             gStdFont, 255, 0, 0, 1);
+        e_TextureFontPrintEx(x+w1+8, _y, IntToStr(CustomStat.TeamStat[TEAM_RED].Goals),
+                             gStdFont, 255, 0, 0, 1);
+        r := 255;
+        g := 0;
+        b := 0;
+      end
+      else
+      begin
+        e_TextureFontPrintEx(x+8, _y, _lc[I_GAME_TEAM_BLUE],
+                             gStdFont, 0, 0, 255, 1);
+        e_TextureFontPrintEx(x+w1+8, _y, IntToStr(CustomStat.TeamStat[TEAM_BLUE].Goals),
+                             gStdFont, 0, 0, 255, 1);
+        r := 0;
+        g := 0;
+        b := 255;
+      end;
+
+      e_DrawLine(1, x+8, _y+20, x-8+w, _y+20, r, g, b);
+      _y := _y+24;
+
+      for p := 0 to High(CustomStat.PlayerStat) do
+        if CustomStat.PlayerStat[p].Team = t then
+          with CustomStat.PlayerStat[p] do
+          begin
+            if Spectator then
+            begin
+              rr := r div 2;
+              gg := g div 2;
+              bb := b div 2;
+            end
+            else
+            begin
+              rr := r;
+              gg := g;
+              bb := b;
+            end;
+            e_TextureFontPrintEx(x+8, _y, Name, gStdFont, rr, gg, bb, 1);
+            e_TextureFontPrintEx(x+w1+8, _y, IntToStr(Frags), gStdFont, rr, gg, bb, 1);
+            e_TextureFontPrintEx(x+w1+w2+8, _y, IntToStr(Deaths), gStdFont, rr, gg, bb, 1);
+            _y := _y+24;
+          end;
+
+      _y := _y+16+16;
+    end;
+  end
+  else if CustomStat.GameMode in [GM_DM, GM_COOP] then
+  begin
+    _y := _y+40;
+    e_TextureFontPrintEx(x+8, _y, _lc[I_GAME_PLAYER_NAME], gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+8+w1, _y, _lc[I_GAME_FRAGS], gStdFont, 255, 127, 0, 1);
+    e_TextureFontPrintEx(x+8+w1+w2, _y, _lc[I_GAME_DEATHS], gStdFont, 255, 127, 0, 1);
+
+    _y := _y+24;
+    for p := 0 to High(CustomStat.PlayerStat) do
+      with CustomStat.PlayerStat[p] do
+      begin
+        e_DrawFillQuad(x+8, _y+4, x+24-1, _y+16+4-1, Color.R, Color.G, Color.B, 0);
+
+        if Spectator then
+          r := 127
+        else
+          r := 255;
+
+        e_TextureFontPrintEx(x+8+16+8, _y+4, Name, gStdFont, r, r, r, 1, True);
+        e_TextureFontPrintEx(x+w1+8+16+8, _y+4, IntToStr(Frags), gStdFont, r, r, r, 1, True);
+        e_TextureFontPrintEx(x+w1+w2+8+16+8, _y+4, IntToStr(Deaths), gStdFont, r, r, r, 1, True);
+        _y := _y+24;
+      end;
+  end;
+end;
+
+procedure DrawSingleStat();
+var
+  tm, key_x, val_x, y: Integer;
+  w1, w2, h: Word;
+  s1, s2: String;
+
+  procedure player_stat(n: Integer);
+  var
+    kpm: Real;
+
+  begin
+  // "Kills: # / #":
+    s1 := Format(' %d ', [SingleStat.PlayerStat[n].Kills]);
+    s2 := Format(' %d', [gTotalMonsters]);
+
+    e_CharFont_Print(gMenuFont, key_x, y, _lc[I_MENU_INTER_KILLS]);
+    e_CharFont_PrintEx(gMenuFont, val_x, y, s1, _RGB(255, 0, 0));
+    e_CharFont_GetSize(gMenuFont, s1, w1, h);
+    e_CharFont_Print(gMenuFont, val_x+w1, y, '/');
+    s1 := s1 + '/';
+    e_CharFont_GetSize(gMenuFont, s1, w1, h);
+    e_CharFont_PrintEx(gMenuFont, val_x+w1, y, s2, _RGB(255, 0, 0));
+
+  // "Kills-per-minute: ##.#":
+    s1 := _lc[I_MENU_INTER_KPM];
+    if tm > 0 then
+      kpm := (SingleStat.PlayerStat[n].Kills / tm) * 60
+    else
+      kpm := SingleStat.PlayerStat[n].Kills;
+    s2 := Format(' %.1f', [kpm]);
+
+    e_CharFont_Print(gMenuFont, key_x, y+32, s1);
+    e_CharFont_PrintEx(gMenuFont, val_x, y+32, s2, _RGB(255, 0, 0));
+
+  // "Secrets found: # / #":
+    s1 := Format(' %d ', [SingleStat.PlayerStat[n].Secrets]);
+    s2 := Format(' %d', [SingleStat.TotalSecrets]);
+
+    e_CharFont_Print(gMenuFont, key_x, y+64, _lc[I_MENU_INTER_SECRETS]);
+    e_CharFont_PrintEx(gMenuFont, val_x, y+64, s1, _RGB(255, 0, 0));
+    e_CharFont_GetSize(gMenuFont, s1, w1, h);
+    e_CharFont_Print(gMenuFont, val_x+w1, y+64, '/');
+    s1 := s1 + '/';
+    e_CharFont_GetSize(gMenuFont, s1, w1, h);
+    e_CharFont_PrintEx(gMenuFont, val_x+w1, y+64, s2, _RGB(255, 0, 0));
+  end;
+
+begin
+// "Level Complete":
+  e_CharFont_GetSize(gMenuFont, _lc[I_MENU_INTER_LEVEL_COMPLETE], w1, h);
+  e_CharFont_Print(gMenuFont, (gScreenWidth-w1) div 2, 32, _lc[I_MENU_INTER_LEVEL_COMPLETE]);
+
+// Îïðåäåëÿåì êîîðäèíàòû âûðàâíèâàíèÿ ïî ñàìîé äëèííîé ñòðîêå:
+  s1 := _lc[I_MENU_INTER_KPM];
+  e_CharFont_GetSize(gMenuFont, s1, w1, h);
+  Inc(w1, 16);
+  s1 := ' 9999.9';
+  e_CharFont_GetSize(gMenuFont, s1, w2, h);
+
+  key_x := (gScreenWidth-w1-w2) div 2;
+  val_x := key_x + w1;
+
+// "Time: #:##:##":
+  tm := SingleStat.GameTime div 1000;
+  s1 := _lc[I_MENU_INTER_TIME];
+  s2 := Format(' %d:%.2d:%.2d', [tm div (60*60), (tm mod (60*60)) div 60, tm mod 60]);
+
+  e_CharFont_Print(gMenuFont, key_x, 80, s1);
+  e_CharFont_PrintEx(gMenuFont, val_x, 80, s2, _RGB(255, 0, 0));
+
+  if SingleStat.TwoPlayers then
+    begin
+    // "Player 1":
+      s1 := _lc[I_MENU_PLAYER_1];
+      e_CharFont_GetSize(gMenuFont, s1, w1, h);
+      e_CharFont_Print(gMenuFont, (gScreenWidth-w1) div 2, 128, s1);
+
+    // Ñòàòèñòèêà ïåðâîãî èãðîêà:
+      y := 176;
+      player_stat(0);
+
+    // "Player 2":
+      s1 := _lc[I_MENU_PLAYER_2];
+      e_CharFont_GetSize(gMenuFont, s1, w1, h);
+      e_CharFont_Print(gMenuFont, (gScreenWidth-w1) div 2, 288, s1);
+
+    // Ñòàòèñòèêà âòîðîãî èãðîêà:
+      y := 336;
+      player_stat(1);
+    end
+  else
+    begin
+    // Ñòàòèñòèêà ïåðâîãî èãðîêà:
+      y := 128;
+      player_stat(0);
+    end;
+end;
+
+procedure DrawLoadingStat();
+var
+  ww, hh: Word;
+  xx, yy, i: Integer;
+  s: String;
+begin
+  if Length(LoadingStat.Msgs) = 0 then
+    Exit;
+
+  e_CharFont_GetSize(gMenuFont, _lc[I_MENU_LOADING], ww, hh);
+  yy := (gScreenHeight div 3);
+  e_CharFont_Print(gMenuFont, (gScreenWidth div 2)-(ww div 2), yy-2*hh, _lc[I_MENU_LOADING]);
+  xx := (gScreenWidth div 3);
+
+  with LoadingStat do
+    for i := 0 to NextMsg-1 do
+      begin
+        if (i = (NextMsg-1)) and (MaxValue > 0) then
+          s := Format('%s:  %d/%d', [Msgs[i], CurValue, MaxValue])
+        else
+          s := Msgs[i];
+
+        e_CharFont_PrintEx(gMenuSmallFont, xx, yy, s, _RGB(255, 0, 0));
+        yy := yy + LOADING_INTERLINE;
+      end;
+end;
+
+procedure DrawMinimap(p: TPlayer; RenderRect: TRect);
+var
+  a, aX, aY, aX2, aY2, Scale, ScaleSz: Integer;
+begin
+  if (gMapInfo.Width > RenderRect.Right - RenderRect.Left) or
+     (gMapInfo.Height > RenderRect.Bottom - RenderRect.Top) then
+  begin
+    Scale := 1;
+  // Ñêîëüêî ïèêñåëîâ êàðòû â 1 ïèêñåëå ìèíè-êàðòû:
+    ScaleSz := 16 div Scale;
+  // Ðàçìåðû ìèíè-êàðòû:
+    aX := max(gMapInfo.Width div ScaleSz, 1);
+    aY := max(gMapInfo.Height div ScaleSz, 1);
+  // Ðàìêà êàðòû:
+    e_DrawFillQuad(0, 0, aX-1, aY-1, 0, 0, 0, 0);
+
+    if gWalls <> nil then
+    begin
+    // Ðèñóåì ñòåíû:
+      for a := 0 to High(gWalls) do
+        with gWalls[a] do
+          if PanelType <> 0 then
+          begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := X div ScaleSz;
+            aY := Y div ScaleSz;
+          // Ðàçìåðû:
+            aX2 := max(Width div ScaleSz, 1);
+            aY2 := max(Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            case PanelType of
+              PANEL_WALL:      e_DrawFillQuad(aX, aY, aX2, aY2, 208, 208, 208, 0);
+              PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+                if Enabled then e_DrawFillQuad(aX, aY, aX2, aY2, 160, 160, 160, 0);
+            end;
+          end;
+    end;
+    if gSteps <> nil then
+    begin
+    // Ðèñóåì ñòóïåíè:
+      for a := 0 to High(gSteps) do
+        with gSteps[a] do
+          if PanelType <> 0 then
+          begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := X div ScaleSz;
+            aY := Y div ScaleSz;
+          // Ðàçìåðû:
+            aX2 := max(Width div ScaleSz, 1);
+            aY2 := max(Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            e_DrawFillQuad(aX, aY, aX2, aY2, 128, 128, 128, 0);
+          end;
+    end;
+    if gLifts <> nil then
+    begin
+    // Ðèñóåì ëèôòû:
+      for a := 0 to High(gLifts) do
+        with gLifts[a] do
+          if PanelType <> 0 then
+          begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := X div ScaleSz;
+            aY := Y div ScaleSz;
+          // Ðàçìåðû:
+            aX2 := max(Width div ScaleSz, 1);
+            aY2 := max(Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            case LiftType of
+              0: e_DrawFillQuad(aX, aY, aX2, aY2, 116,  72,  36, 0);
+              1: e_DrawFillQuad(aX, aY, aX2, aY2, 116, 124,  96, 0);
+              2: e_DrawFillQuad(aX, aY, aX2, aY2, 200,  80,   4, 0);
+              3: e_DrawFillQuad(aX, aY, aX2, aY2, 252, 140,  56, 0);
+            end;
+          end;
+    end;
+    if gWater <> nil then
+    begin
+    // Ðèñóåì âîäó:
+      for a := 0 to High(gWater) do
+        with gWater[a] do
+          if PanelType <> 0 then
+          begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := X div ScaleSz;
+            aY := Y div ScaleSz;
+          // Ðàçìåðû:
+            aX2 := max(Width div ScaleSz, 1);
+            aY2 := max(Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            e_DrawFillQuad(aX, aY, aX2, aY2, 0, 0, 192, 0);
+          end;
+    end;
+    if gAcid1 <> nil then
+    begin
+    // Ðèñóåì êèñëîòó 1:
+      for a := 0 to High(gAcid1) do
+        with gAcid1[a] do
+          if PanelType <> 0 then
+          begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := X div ScaleSz;
+            aY := Y div ScaleSz;
+          // Ðàçìåðû:
+            aX2 := max(Width div ScaleSz, 1);
+            aY2 := max(Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            e_DrawFillQuad(aX, aY, aX2, aY2, 0, 176, 0, 0);
+          end;
+    end;
+    if gAcid2 <> nil then
+    begin
+    // Ðèñóåì êèñëîòó 2:
+      for a := 0 to High(gAcid2) do
+        with gAcid2[a] do
+          if PanelType <> 0 then
+          begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := X div ScaleSz;
+            aY := Y div ScaleSz;
+          // Ðàçìåðû:
+            aX2 := max(Width div ScaleSz, 1);
+            aY2 := max(Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            e_DrawFillQuad(aX, aY, aX2, aY2, 176, 0, 0, 0);
+          end;
+    end;
+    if gPlayers <> nil then
+    begin
+    // Ðèñóåì èãðîêîâ:
+      for a := 0 to High(gPlayers) do
+        if gPlayers[a] <> nil then with gPlayers[a] do
+          if Live then begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := Obj.X div ScaleSz + 1;
+            aY := Obj.Y div ScaleSz + 1;
+          // Ðàçìåðû:
+            aX2 := max(Obj.Rect.Width div ScaleSz, 1);
+            aY2 := max(Obj.Rect.Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            if gPlayers[a] = p then
+              e_DrawFillQuad(aX, aY, aX2, aY2, 0, 255, 0, 0)
+            else
+              case Team of
+                TEAM_RED:  e_DrawFillQuad(aX, aY, aX2, aY2, 255,   0,   0, 0);
+                TEAM_BLUE: e_DrawFillQuad(aX, aY, aX2, aY2, 0,     0, 255, 0);
+                else       e_DrawFillQuad(aX, aY, aX2, aY2, 255, 128,   0, 0);
+              end;
+          end;
+    end;
+    if gMonsters <> nil then
+    begin
+    // Ðèñóåì ìîíñòðîâ:
+      for a := 0 to High(gMonsters) do
+        if gMonsters[a] <> nil then with gMonsters[a] do
+          if Live then begin
+          // Ëåâûé âåðõíèé óãîë:
+            aX := Obj.X div ScaleSz + 1;
+            aY := Obj.Y div ScaleSz + 1;
+          // Ðàçìåðû:
+            aX2 := max(Obj.Rect.Width div ScaleSz, 1);
+            aY2 := max(Obj.Rect.Height div ScaleSz, 1);
+          // Ïðàâûé íèæíèé óãîë:
+            aX2 := aX + aX2 - 1;
+            aY2 := aY + aY2 - 1;
+
+            e_DrawFillQuad(aX, aY, aX2, aY2, 255, 255, 0, 0);
+          end;
+    end;
+  end;
+end;
+
+procedure DrawMapView(x, y, w, h: Integer);
+var
+  bx, by: Integer;
+begin
+  glPushMatrix();
+
+  bx := Round(x/(gMapInfo.Width - w)*(gBackSize.X - w));
+  by := Round(y/(gMapInfo.Height - h)*(gBackSize.Y - h));
+  g_Map_DrawBack(-bx, -by);
+
+  sX := x;
+  sY := y;
+  sWidth := w;
+  sHeight := h;
+
+  glTranslatef(-x, -y, 0);
+
+  g_Map_DrawPanels(PANEL_BACK);
+  g_Map_DrawPanels(PANEL_STEP);
+  g_Items_Draw();
+  g_Weapon_Draw();
+  g_Player_DrawShells();
+  g_Player_DrawAll();
+  g_Player_DrawCorpses();
+  g_Map_DrawPanels(PANEL_WALL);
+  g_Monsters_Draw();
+  g_Map_DrawPanels(PANEL_CLOSEDOOR);
+  g_GFX_Draw();
+  g_Map_DrawFlags();
+  g_Map_DrawPanels(PANEL_ACID1);
+  g_Map_DrawPanels(PANEL_ACID2);
+  g_Map_DrawPanels(PANEL_WATER);
+  g_Map_DrawPanels(PANEL_FORE);
+  if g_debug_HealthBar then
+  begin
+    g_Monsters_DrawHealth();
+    g_Player_DrawHealth();
+  end;
+
+  glPopMatrix();
+end;
+
+procedure DrawPlayer(p: TPlayer);
+var
+  px, py, a, b, c, d: Integer;
+  //R: TRect;
+begin
+  if (p = nil) or (p.FDummy) then
+  begin
+    glPushMatrix();
+    g_Map_DrawBack(0, 0);
+    glPopMatrix();
+    Exit;
+  end;
+
+  gPlayerDrawn := p;
+
+  glPushMatrix();
+
+  px := p.GameX + PLAYER_RECT_CX;
+  py := p.GameY + PLAYER_RECT_CY;
+
+  if px > (gPlayerScreenSize.X div 2) then
+    a := -px + (gPlayerScreenSize.X div 2)
+  else
+    a := 0;
+  if py > (gPlayerScreenSize.Y div 2) then
+    b := -py + (gPlayerScreenSize.Y div 2)
+  else
+    b := 0;
+  if px > (gMapInfo.Width - (gPlayerScreenSize.X div 2)) then
+    a := -gMapInfo.Width + gPlayerScreenSize.X;
+  if py > (gMapInfo.Height - (gPlayerScreenSize.Y div 2)) then
+    b := -gMapInfo.Height + gPlayerScreenSize.Y;
+  if gMapInfo.Width <= gPlayerScreenSize.X then
+    a := 0;
+  if gMapInfo.Height <= gPlayerScreenSize.Y then
+    b := 0;
+
+  if p.IncCam <> 0 then
+  begin
+    if py > (gMapInfo.Height - (gPlayerScreenSize.Y div 2)) then
+    begin
+      if p.IncCam > 120-(py-(gMapInfo.Height-(gPlayerScreenSize.Y div 2))) then
+        p.IncCam := 120-(py-(gMapInfo.Height-(gPlayerScreenSize.Y div 2)));
+    end;
+
+    if py < (gPlayerScreenSize.Y div 2) then
+    begin
+      if p.IncCam < -120+((gPlayerScreenSize.Y div 2)-py) then
+        p.IncCam := -120+((gPlayerScreenSize.Y div 2)-py);
+    end;
+
+    if p.IncCam < 0 then
+      while (py+(gPlayerScreenSize.Y div 2)-p.IncCam > gMapInfo.Height) and
+            (p.IncCam < 0) do
+        p.IncCam := p.IncCam + 1;
+
+    if p.IncCam > 0 then
+      while (py-(gPlayerScreenSize.Y div 2)-p.IncCam < 0) and
+            (p.IncCam > 0) do
+        p.IncCam := p.IncCam - 1;
+  end;
+
+  if (px< gPlayerScreenSize.X div 2) or
+     (gMapInfo.Width-gPlayerScreenSize.X <= 256) then
+    c := 0
+  else
+    if (px > gMapInfo.Width-(gPlayerScreenSize.X div 2)) then
+      c := gBackSize.X - gPlayerScreenSize.X
+    else
+      c := Round((px-(gPlayerScreenSize.X div 2))/(gMapInfo.Width-gPlayerScreenSize.X)*(gBackSize.X-gPlayerScreenSize.X));
+
+  if (py-p.IncCam <= gPlayerScreenSize.Y div 2) or
+     (gMapInfo.Height-gPlayerScreenSize.Y <= 256) then
+    d := 0
+  else
+    if (py-p.IncCam >= gMapInfo.Height-(gPlayerScreenSize.Y div 2)) then
+      d := gBackSize.Y - gPlayerScreenSize.Y
+    else
+      d := Round((py-p.IncCam-(gPlayerScreenSize.Y div 2))/(gMapInfo.Height-gPlayerScreenSize.Y)*(gBackSize.Y-gPlayerScreenSize.Y));
+
+  g_Map_DrawBack(-c, -d);
+
+  sX := -a;
+  sY := -(b+p.IncCam);
+  sWidth := gPlayerScreenSize.X;
+  sHeight := gPlayerScreenSize.Y;
+
+  glTranslatef(a, b+p.IncCam, 0);
+
+  g_Map_DrawPanels(PANEL_BACK);
+  g_Map_DrawPanels(PANEL_STEP);
+  g_Items_Draw();
+  g_Weapon_Draw();
+  g_Player_DrawShells();
+  g_Player_DrawAll();
+  g_Player_DrawCorpses();
+  g_Map_DrawPanels(PANEL_WALL);
+  g_Monsters_Draw();
+  g_Map_DrawPanels(PANEL_CLOSEDOOR);
+  g_GFX_Draw();
+  g_Map_DrawFlags();
+  g_Map_DrawPanels(PANEL_ACID1);
+  g_Map_DrawPanels(PANEL_ACID2);
+  g_Map_DrawPanels(PANEL_WATER);
+  g_Map_DrawPanels(PANEL_FORE);
+  if g_debug_HealthBar then
+  begin
+    g_Monsters_DrawHealth();
+    g_Player_DrawHealth();
+  end;
+
+  if p.FSpectator then
+    e_TextureFontPrintEx(p.GameX + PLAYER_RECT_CX - 4,
+                         p.GameY + PLAYER_RECT_CY - 4,
+                         'X', gStdFont, 255, 255, 255, 1, True);
+  {
+  for a := 0 to High(gCollideMap) do
+    for b := 0 to High(gCollideMap[a]) do
+    begin
+      d := 0;
+      if ByteBool(gCollideMap[a, b] and MARK_WALL) then
+        d := d + 1;
+      if ByteBool(gCollideMap[a, b] and MARK_DOOR) then
+        d := d + 2;
+
+      case d of
+        1: e_DrawPoint(1, b, a, 200, 200, 200);
+        2: e_DrawPoint(1, b, a, 64, 64, 255);
+        3: e_DrawPoint(1, b, a, 255, 0, 255);
+      end;
+    end;
+  }
+
+  glPopMatrix();
+
+  p.DrawPain();
+  p.DrawPickup();
+  p.DrawRulez();
+  //if gShowMap then
+    //DrawMinimap(p, _TRect(0, 0, 128, 128));
+  if g_Debug_Player then
+    g_Player_DrawDebug(p);
+  p.DrawGUI();
+end;
+
+procedure g_Game_Draw();
+var
+  ID: DWORD;
+  w, h: Word;
+  ww, hh: Byte;
+  Time: Int64;
+  back: string;
+  plView1, plView2: TPlayer;
+  Split: Boolean;
+begin
+  if gExit = EXIT_QUIT then Exit;
+
+  Time := GetTimer() div 1000;
+  FPSCounter := FPSCounter+1;
+  if Time - FPSTime >= 1000 then
+  begin
+    FPS := FPSCounter;
+    FPSCounter := 0;
+    FPSTime := Time;
+  end;
+
+  if gGameOn or (gState = STATE_FOLD) then
+  begin
+    if (gPlayer1 <> nil) and (gPlayer2 <> nil) then
+    begin
+      gSpectMode := SPECT_NONE;
+      if not gRevertPlayers then
+      begin
+        plView1 := gPlayer1;
+        plView2 := gPlayer2;
+      end
+      else
+      begin
+        plView1 := gPlayer2;
+        plView2 := gPlayer1;
+      end;
+    end
+    else
+      if (gPlayer1 <> nil) or (gPlayer2 <> nil) then
+      begin
+        gSpectMode := SPECT_NONE;
+        if gPlayer2 = nil then
+          plView1 := gPlayer1
+        else
+          plView1 := gPlayer2;
+        plView2 := nil;
+      end
+      else
+      begin
+        plView1 := nil;
+        plView2 := nil;
+      end;
+
+    if (plView1 = nil) and (plView2 = nil) and (gSpectMode = SPECT_NONE) then
+      gSpectMode := SPECT_STATS;
+
+    if gSpectMode = SPECT_PLAYERS then
+      if gPlayers <> nil then
+      begin
+        plView1 := GetActivePlayer_ByID(gSpectPID1);
+        if plView1 = nil then
+        begin
+          gSpectPID1 := GetActivePlayerID_Next();
+          plView1 := GetActivePlayer_ByID(gSpectPID1);
+        end;
+        if gSpectViewTwo then
+        begin
+          plView2 := GetActivePlayer_ByID(gSpectPID2);
+          if plView2 = nil then
+          begin
+            gSpectPID2 := GetActivePlayerID_Next();
+            plView2 := GetActivePlayer_ByID(gSpectPID2);
+          end;
+        end;
+      end;
+
+    if gSpectMode = SPECT_MAPVIEW then
+    begin
+    // Ðåæèì ïðîñìîòðà êàðòû
+      Split := False;
+      e_SetViewPort(0, 0, gScreenWidth, gScreenHeight);
+      DrawMapView(gSpectX, gSpectY, gScreenWidth, gScreenHeight);
+      gHearPoint1.Active := True;
+      gHearPoint1.Coords.X := gScreenWidth div 2 + gSpectX;
+      gHearPoint1.Coords.Y := gScreenHeight div 2 + gSpectY;
+      gHearPoint2.Active := False;
+    end
+    else
+    begin
+      Split := (plView1 <> nil) and (plView2 <> nil);
+
+    // Òî÷êè ñëóõà èãðîêîâ
+      if plView1 <> nil then
+      begin
+        gHearPoint1.Active := True;
+        gHearPoint1.Coords.X := plView1.GameX;
+        gHearPoint1.Coords.Y := plView1.GameY;
+      end else
+        gHearPoint1.Active := False;
+      if plView2 <> nil then
+      begin
+        gHearPoint2.Active := True;
+        gHearPoint2.Coords.X := plView2.GameX;
+        gHearPoint2.Coords.Y := plView2.GameY;
+      end else
+        gHearPoint2.Active := False;
+
+    // Ðàçìåð ýêðàíîâ èãðîêîâ:
+      gPlayerScreenSize.X := gScreenWidth-196;
+      if Split then
+      begin
+        gPlayerScreenSize.Y := gScreenHeight div 2;
+        if gScreenHeight mod 2 = 0 then
+          Dec(gPlayerScreenSize.Y);
+      end
+      else
+        gPlayerScreenSize.Y := gScreenHeight;
+
+      if Split then
+        if gScreenHeight mod 2 = 0 then
+          e_SetViewPort(0, gPlayerScreenSize.Y+2, gPlayerScreenSize.X+196, gPlayerScreenSize.Y)
+        else
+          e_SetViewPort(0, gPlayerScreenSize.Y+1, gPlayerScreenSize.X+196, gPlayerScreenSize.Y);
+
+      DrawPlayer(plView1);
+      gPlayer1ScreenCoord.X := sX;
+      gPlayer1ScreenCoord.Y := sY;
+
+      if Split then
+      begin
+        e_SetViewPort(0, 0, gPlayerScreenSize.X+196, gPlayerScreenSize.Y);
+
+        DrawPlayer(plView2);
+        gPlayer2ScreenCoord.X := sX;
+        gPlayer2ScreenCoord.Y := sY;
+      end;
+
+      e_SetViewPort(0, 0, gScreenWidth, gScreenHeight);
+
+      if Split then
+        e_DrawLine(2, 0, gScreenHeight div 2, gScreenWidth, gScreenHeight div 2, 0, 0, 0);
+    end;
+
+    if MessageText <> '' then
+    begin
+      w := 0;
+      h := 0;
+      e_CharFont_GetSizeFmt(gMenuFont, MessageText, w, h);
+      if Split then
+        e_CharFont_PrintFmt(gMenuFont, (gScreenWidth div 2)-(w div 2),
+                        (gScreenHeight div 2)-(h div 2), MessageText)
+      else
+        e_CharFont_PrintFmt(gMenuFont, (gScreenWidth div 2)-(w div 2),
+                  Round(gScreenHeight / 2.75)-(h div 2), MessageText);
+    end;
+
+    if IsDrawStat or (gSpectMode = 1) then DrawStat();
+
+    if gSpectHUD and (not gChatShow) and (gSpectMode <> SPECT_NONE) then
+    begin
+    // Draw spectator GUI
+      ww := 0;
+      hh := 0;
+      e_TextureFontGetSize(gStdFont, ww, hh);
+      case gSpectMode of
+        SPECT_STATS:
+          e_TextureFontPrintEx(0, gScreenHeight - (hh+2)*2, 'MODE: Stats', gStdFont, 255, 255, 255, 1);
+        SPECT_MAPVIEW:
+          e_TextureFontPrintEx(0, gScreenHeight - (hh+2)*2, 'MODE: Observe Map', gStdFont, 255, 255, 255, 1);
+        SPECT_PLAYERS:
+          e_TextureFontPrintEx(0, gScreenHeight - (hh+2)*2, 'MODE: Watch Players', gStdFont, 255, 255, 255, 1);
+      end;
+      e_TextureFontPrintEx(2*ww, gScreenHeight - (hh+2), '< jump >', gStdFont, 255, 255, 255, 1);
+      if gSpectMode = SPECT_MAPVIEW then
+      begin
+        e_TextureFontPrintEx(22*ww, gScreenHeight - (hh+2)*2, '[-]', gStdFont, 255, 255, 255, 1);
+        e_TextureFontPrintEx(26*ww, gScreenHeight - (hh+2)*2, 'Step ' + IntToStr(gSpectStep), gStdFont, 255, 255, 255, 1);
+        e_TextureFontPrintEx(34*ww, gScreenHeight - (hh+2)*2, '[+]', gStdFont, 255, 255, 255, 1);
+        e_TextureFontPrintEx(18*ww, gScreenHeight - (hh+2), '<prev weap>', gStdFont, 255, 255, 255, 1);
+        e_TextureFontPrintEx(30*ww, gScreenHeight - (hh+2), '<next weap>', gStdFont, 255, 255, 255, 1);
+      end;
+      if gSpectMode = SPECT_PLAYERS then
+      begin
+        e_TextureFontPrintEx(22*ww, gScreenHeight - (hh+2)*2, 'Player 1', gStdFont, 255, 255, 255, 1);
+        e_TextureFontPrintEx(20*ww, gScreenHeight - (hh+2), '<left/right>', gStdFont, 255, 255, 255, 1);
+        if gSpectViewTwo then
+        begin
+          e_TextureFontPrintEx(37*ww, gScreenHeight - (hh+2)*2, 'Player 2', gStdFont, 255, 255, 255, 1);
+          e_TextureFontPrintEx(34*ww, gScreenHeight - (hh+2), '<prev w/next w>', gStdFont, 255, 255, 255, 1);
+          e_TextureFontPrintEx(52*ww, gScreenHeight - (hh+2)*2, '2x View', gStdFont, 255, 255, 255, 1);
+          e_TextureFontPrintEx(51*ww, gScreenHeight - (hh+2), '<up/down>', gStdFont, 255, 255, 255, 1);
+        end
+        else
+        begin
+          e_TextureFontPrintEx(35*ww, gScreenHeight - (hh+2)*2, '2x View', gStdFont, 255, 255, 255, 1);
+          e_TextureFontPrintEx(34*ww, gScreenHeight - (hh+2), '<up/down>', gStdFont, 255, 255, 255, 1);
+        end;
+      end;
+    end;
+  end;
+
+  if gPause and gGameOn and (g_ActiveWindow = nil) then
+  begin
+    e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+
+    e_CharFont_GetSize(gMenuFont, _lc[I_MENU_PAUSE], w, h);
+    e_CharFont_Print(gMenuFont, (gScreenWidth div 2)-(w div 2),
+                    (gScreenHeight div 2)-(h div 2), _lc[I_MENU_PAUSE]);
+  end;
+
+  if not gGameOn then
+  begin
+    if (gState = STATE_MENU) then
+    begin
+      if  ((g_ActiveWindow = nil) or (g_ActiveWindow.BackTexture = '')) then
+      begin
+        if g_Texture_Get('MENU_BACKGROUND', ID) then
+          e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
+        else e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+      end;
+      if g_ActiveWindow <> nil then
+        e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+    end;
+
+    if gState = STATE_FOLD then
+      e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 0, 0, 0, EndingGameCounter);
+
+    if gState = STATE_INTERCUSTOM then
+    begin
+      if gLastMap and (gGameSettings.GameMode = GM_COOP) then
+      begin
+        back := 'TEXTURE_endpic';
+        if not g_Texture_Get(back, ID) then
+          back := _lc[I_TEXTURE_ENDPIC];
+      end
+      else
+        back := 'INTER';
+
+      if g_Texture_Get(back, ID) then
+        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
+      else
+        e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+
+      DrawCustomStat();
+
+      if g_ActiveWindow <> nil then
+        e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+    end;
+
+    if gState = STATE_INTERSINGLE then
+    begin
+      if EndingGameCounter > 0 then
+        e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 0, 0, 0, EndingGameCounter)
+      else
+      begin
+        back := 'INTER';
+
+        if g_Texture_Get(back, ID) then
+          e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
+        else
+          e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+
+        DrawSingleStat();
+
+        if g_ActiveWindow <> nil then
+          e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+      end;
+    end;
+
+    if gState = STATE_ENDPIC then
+    begin
+      ID := DWORD(-1);
+      if not g_Texture_Get('TEXTURE_endpic', ID) then
+        g_Texture_Get(_lc[I_TEXTURE_ENDPIC], ID);
+
+      if ID <> DWORD(-1) then
+        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
+      else
+        e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+
+      if g_ActiveWindow <> nil then
+        e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+    end;
+
+    if gState = STATE_SLIST then
+    begin
+      if g_Texture_Get('MENU_BACKGROUND', ID) then
+      begin
+        e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight);
+        e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+      end;
+      g_Serverlist_Draw(slCurrent);
+    end;
+  end;
+
+  if g_ActiveWindow <> nil then
+  begin
+    if gGameOn then
+      e_DrawFillQuad(0, 0, gScreenWidth-1, gScreenHeight-1, 48, 48, 48, 180);
+    g_ActiveWindow.Draw();
+  end;
+
+  g_Console_Draw();
+
+  if g_debug_Sounds and gGameOn then
+  begin
+    for w := 0 to High(e_SoundsArray) do
+      for h := 0 to e_SoundsArray[w].nRefs do
+        e_DrawPoint(1, w+100, h+100, 255, 0, 0);
+  end;
+
+  if gShowFPS then
+  begin
+    e_TextureFontPrint(0, 0, Format('FPS: %d', [FPS]), gStdFont);
+    e_TextureFontPrint(0, 16, Format('UPS: %d', [UPS]), gStdFont);
+  end;
+
+  if gGameOn and gShowTime and (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT]) then
+  e_TextureFontPrint(gScreenWidth-72, 0,
+                     Format('%d:%.2d:%.2d', [gTime div 1000 div 3600, (gTime div 1000 div 60) mod 60, gTime div 1000 mod 60]),
+                     gStdFont);
+end;
+
+procedure g_Game_Quit();
+begin
+  g_Game_StopAllSounds(True);
+  gMusic.Free();
+  g_Game_SaveOptions();
+  g_Game_FreeData();
+  g_PlayerModel_FreeData();
+  g_Texture_DeleteAll();
+  g_Frames_DeleteAll();
+  g_Menu_Free();
+  
+  if NetInitDone then g_Net_Free;
+
+// Íàäî óäàëèòü êàðòó ïîñëå òåñòà:
+  if gMapToDelete <> '' then
+    g_Game_DeleteTestMap();
+
+  gExit := EXIT_QUIT;
+  PushExitEvent();
+end;
+
+procedure g_FatalError(Text: String);
+begin
+  g_Console_Add(Format(_lc[I_FATAL_ERROR], [Text]), True);
+  e_WriteLog(Format(_lc[I_FATAL_ERROR], [Text]), MSG_WARNING);
+
+  gExit := EXIT_SIMPLE;
+end;
+
+procedure g_SimpleError(Text: String);
+begin
+  g_Console_Add(Format(_lc[I_SIMPLE_ERROR], [Text]), True);
+  e_WriteLog(Format(_lc[I_SIMPLE_ERROR], [Text]), MSG_WARNING);
+end;
+
+procedure g_Game_SetupScreenSize();
+var
+  d: Single;
+begin
+// Ðàçìåð ýêðàíîâ èãðîêîâ:
+  gPlayerScreenSize.X := gScreenWidth-196;
+  if (gPlayer1 <> nil) and (gPlayer2 <> nil) then
+    gPlayerScreenSize.Y := gScreenHeight div 2
+  else
+    gPlayerScreenSize.Y := gScreenHeight;
+
+// Ðàçìåð çàäíåãî ïëàíà:
+  if BackID <> DWORD(-1) then
+  begin
+    d := SKY_STRETCH;
+
+    if (gScreenWidth*d > gMapInfo.Width) or
+       (gScreenHeight*d > gMapInfo.Height) then
+      d := 1.0;
+
+    gBackSize.X := Round(gScreenWidth*d);
+    gBackSize.Y := Round(gScreenHeight*d);
+  end;
+end;
+
+procedure g_Game_ChangeResolution(newWidth, newHeight: Word; nowFull, nowMax: Boolean);
+begin
+  g_Window_SetSize(newWidth, newHeight, nowFull);
+end;
+
+procedure g_Game_AddPlayer(Team: Byte = TEAM_NONE);
+begin
+  if ((not gGameOn) and (gState <> STATE_INTERCUSTOM))
+  or (not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT])) then
+    Exit;
+  if gPlayer1 = nil then
+  begin
+    if g_Game_IsClient then
+    begin
+      if NetPlrUID1 > -1 then
+      begin
+        MC_SEND_CheatRequest(NET_CHEAT_SPECTATE);
+        gPlayer1 := g_Player_Get(NetPlrUID1);
+      end;
+      Exit;
+    end;
+
+    if not (Team in [TEAM_RED, TEAM_BLUE]) then
+      Team := gPlayer1Settings.Team;
+
+    // Ñîçäàíèå ïåðâîãî èãðîêà:
+    gPlayer1 := g_Player_Get(g_Player_Create(gPlayer1Settings.Model,
+                                             gPlayer1Settings.Color,
+                                             Team, False));
+    if gPlayer1 = nil then
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [1]))
+    else
+    begin
+      gPlayer1.Name := gPlayer1Settings.Name;
+      g_Console_Add(Format(_lc[I_PLAYER_JOIN], [gPlayer1.Name]), True);
+      if g_Game_IsServer and g_Game_IsNet then
+        MH_SEND_PlayerCreate(gPlayer1.UID);
+      gPlayer1.Respawn(False, True);
+
+      if g_Game_IsNet and NetUseMaster then
+        g_Net_Slist_Update;
+    end;
+
+    Exit;
+  end;
+  if gPlayer2 = nil then
+  begin
+    if g_Game_IsClient then
+    begin
+      if NetPlrUID2 > -1 then
+        gPlayer2 := g_Player_Get(NetPlrUID2);
+      Exit;
+    end;
+
+    if not (Team in [TEAM_RED, TEAM_BLUE]) then
+      Team := gPlayer2Settings.Team;
+
+    // Ñîçäàíèå âòîðîãî èãðîêà:
+    gPlayer2 := g_Player_Get(g_Player_Create(gPlayer2Settings.Model,
+                                             gPlayer2Settings.Color,
+                                             Team, False));
+    if gPlayer2 = nil then
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [2]))
+    else
+    begin
+      gPlayer2.Name := gPlayer2Settings.Name;
+      g_Console_Add(Format(_lc[I_PLAYER_JOIN], [gPlayer2.Name]), True);
+      if g_Game_IsServer and g_Game_IsNet then
+        MH_SEND_PlayerCreate(gPlayer2.UID);
+      gPlayer2.Respawn(False, True);
+
+      if g_Game_IsNet and NetUseMaster then
+        g_Net_Slist_Update;
+    end;
+
+    Exit;
+  end;
+end;
+
+procedure g_Game_RemovePlayer();
+var
+  Pl: TPlayer;
+begin
+  if ((not gGameOn) and (gState <> STATE_INTERCUSTOM))
+  or (not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT])) then
+    Exit;
+  Pl := gPlayer2;
+  if Pl <> nil then
+  begin
+    if g_Game_IsServer then
+    begin
+      Pl.Lives := 0;
+      Pl.Kill(K_SIMPLEKILL, 0, HIT_DISCON);
+      g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [Pl.Name]), True);
+      g_Player_Remove(Pl.UID);
+
+      if g_Game_IsNet and NetUseMaster then
+        g_Net_Slist_Update;
+    end else
+      gPlayer2 := nil;
+    Exit;
+  end;
+  Pl := gPlayer1;
+  if Pl <> nil then
+  begin
+    if g_Game_IsServer then
+    begin
+      Pl.Lives := 0;
+      Pl.Kill(K_SIMPLEKILL, 0, HIT_DISCON);
+      g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [Pl.Name]), True);
+      g_Player_Remove(Pl.UID);
+
+      if g_Game_IsNet and NetUseMaster then
+        g_Net_Slist_Update;
+    end else
+    begin
+      gPlayer1 := nil;
+      MC_SEND_CheatRequest(NET_CHEAT_SPECTATE);
+    end;
+    Exit;
+  end;
+end;
+
+procedure g_Game_Spectate();
+begin
+  g_Game_RemovePlayer();
+  if gPlayer1 <> nil then
+    g_Game_RemovePlayer();
+end;
+
+procedure g_Game_SpectateCenterView();
+begin
+  gSpectX := Max(gMapInfo.Width div 2 - gScreenWidth div 2, 0);
+  gSpectY := Max(gMapInfo.Height div 2 - gScreenHeight div 2, 0);
+end;
+
+procedure g_Game_StartSingle(Map: String; TwoPlayers: Boolean; nPlayers: Byte);
+var
+  i, nPl: Integer;
+begin
+  g_Game_Free();
+
+  e_WriteLog('Starting singleplayer game...', MSG_NOTIFY);
+
+  g_Game_ClearLoading();
+
+// Íàñòðîéêè èãðû:
+  FillByte(gGameSettings, SizeOf(TGameSettings), 0);
+  gAimLine := False;
+  gShowMap := False;
+  gGameSettings.GameType := GT_SINGLE;
+  gGameSettings.MaxLives := 0;
+  gGameSettings.Options := gGameSettings.Options + GAME_OPTION_ALLOWEXIT;
+  gGameSettings.Options := gGameSettings.Options + GAME_OPTION_MONSTERS;
+  gGameSettings.Options := gGameSettings.Options + GAME_OPTION_BOTVSMONSTER;
+  gSwitchGameMode := GM_SINGLE;
+
+  g_Game_ExecuteEvent('ongamestart');
+
+// Óñòàíîâêà ðàçìåðîâ îêîí èãðîêîâ:
+  g_Game_SetupScreenSize();
+
+// Ñîçäàíèå ïåðâîãî èãðîêà:
+  gPlayer1 := g_Player_Get(g_Player_Create(gPlayer1Settings.Model,
+                                           gPlayer1Settings.Color,
+                                           gPlayer1Settings.Team, False));
+  if gPlayer1 = nil then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [1]));
+    Exit;
+  end;
+
+  gPlayer1.Name := gPlayer1Settings.Name;
+  nPl := 1;
+
+// Ñîçäàíèå âòîðîãî èãðîêà, åñëè åñòü:
+  if TwoPlayers then
+  begin
+    gPlayer2 := g_Player_Get(g_Player_Create(gPlayer2Settings.Model,
+                                             gPlayer2Settings.Color,
+                                             gPlayer2Settings.Team, False));
+    if gPlayer2 = nil then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [2]));
+      Exit;
+    end;
+
+    gPlayer2.Name := gPlayer2Settings.Name;
+    Inc(nPl);
+  end;
+
+// Çàãðóçêà è çàïóñê êàðòû:
+  if not g_Game_StartMap(MAP, True) then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [gGameSettings.WAD + ':\' + MAP]));
+    Exit;
+  end;
+
+// Íàñòðîéêè èãðîêîâ è áîòîâ:
+  g_Player_Init();
+
+// Ñîçäàåì áîòîâ:
+  for i := nPl+1 to nPlayers do
+    g_Player_Create(STD_PLAYER_MODEL, _RGB(0, 0, 0), 0, True);
+end;
+
+procedure g_Game_StartCustom(Map: String; GameMode: Byte;
+                             TimeLimit, GoalLimit: Word;
+                             MaxLives: Byte;
+                             Options: LongWord; nPlayers: Byte);
+var
+  i, nPl: Integer;
+begin
+  g_Game_Free();
+
+  e_WriteLog('Starting custom game...', MSG_NOTIFY);
+
+  g_Game_ClearLoading();
+
+// Íàñòðîéêè èãðû:
+  gGameSettings.GameType := GT_CUSTOM;
+  gGameSettings.GameMode := GameMode;
+  gSwitchGameMode := GameMode;
+  gGameSettings.TimeLimit := TimeLimit;
+  gGameSettings.GoalLimit := GoalLimit;
+  gGameSettings.MaxLives := IfThen(GameMode = GM_CTF, 0, MaxLives);
+  gGameSettings.Options := Options;
+
+  gCoopTotalMonstersKilled := 0;
+  gCoopTotalSecretsFound := 0;
+  gCoopTotalMonsters := 0;
+  gCoopTotalSecrets := 0;
+  gAimLine := False;
+  gShowMap := False;
+
+  g_Game_ExecuteEvent('ongamestart');
+
+// Óñòàíîâêà ðàçìåðîâ îêîí èãðîêîâ:
+  g_Game_SetupScreenSize();
+
+// Ðåæèì íàáëþäàòåëÿ:
+  if nPlayers = 0 then
+  begin
+    gPlayer1 := nil;
+    gPlayer2 := nil;
+  end;
+
+  nPl := 0;
+  if nPlayers >= 1 then
+  begin
+  // Ñîçäàíèå ïåðâîãî èãðîêà:
+    gPlayer1 := g_Player_Get(g_Player_Create(gPlayer1Settings.Model,
+                                             gPlayer1Settings.Color,
+                                             gPlayer1Settings.Team, False));
+    if gPlayer1 = nil then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [1]));
+      Exit;
+    end;
+
+    gPlayer1.Name := gPlayer1Settings.Name;
+    Inc(nPl);
+  end;
+
+  if nPlayers >= 2 then
+  begin
+  // Ñîçäàíèå âòîðîãî èãðîêà:
+    gPlayer2 := g_Player_Get(g_Player_Create(gPlayer2Settings.Model,
+                                             gPlayer2Settings.Color,
+                                             gPlayer2Settings.Team, False));
+    if gPlayer2 = nil then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [2]));
+      Exit;
+    end;
+
+    gPlayer2.Name := gPlayer2Settings.Name;
+    Inc(nPl);
+  end;
+
+// Çàãðóçêà è çàïóñê êàðòû:
+  if not g_Game_StartMap(Map, True) then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [Map]));
+    Exit;
+  end;
+
+// Íåò òî÷åê ïîÿâëåíèÿ:
+  if (g_Map_GetPointCount(RESPAWNPOINT_PLAYER1) +
+      g_Map_GetPointCount(RESPAWNPOINT_PLAYER2) +
+      g_Map_GetPointCount(RESPAWNPOINT_DM) +
+      g_Map_GetPointCount(RESPAWNPOINT_RED)+
+      g_Map_GetPointCount(RESPAWNPOINT_BLUE)) < 1 then
+  begin
+    g_FatalError(_lc[I_GAME_ERROR_GET_SPAWN]);
+    Exit;
+  end;
+
+// Íàñòðîéêè èãðîêîâ è áîòîâ:
+  g_Player_Init();
+
+// Ñîçäàåì áîòîâ:
+  for i := nPl+1 to nPlayers do
+    g_Player_Create(STD_PLAYER_MODEL, _RGB(0, 0, 0), 0, True);
+end;
+
+procedure g_Game_StartServer(Map: String; GameMode: Byte;
+                             TimeLimit, GoalLimit: Word; MaxLives: Byte;
+                             Options: LongWord; nPlayers: Byte;
+                             IPAddr: LongWord; Port: Word);
+begin
+  g_Game_Free();
+
+  e_WriteLog('Starting net game (server)...', MSG_NOTIFY);
+
+  g_Game_ClearLoading();
+
+// Íàñòðîéêè èãðû:
+  gGameSettings.GameType := GT_SERVER;
+  gGameSettings.GameMode := GameMode;
+  gSwitchGameMode := GameMode;
+  gGameSettings.TimeLimit := TimeLimit;
+  gGameSettings.GoalLimit := GoalLimit;
+  gGameSettings.MaxLives := IfThen(GameMode = GM_CTF, 0, MaxLives);
+  gGameSettings.Options := Options;
+
+  gCoopTotalMonstersKilled := 0;
+  gCoopTotalSecretsFound := 0;
+  gCoopTotalMonsters := 0;
+  gCoopTotalSecrets := 0;
+  gAimLine := False;
+  gShowMap := False;
+
+  g_Game_ExecuteEvent('ongamestart');
+
+// Óñòàíîâêà ðàçìåðîâ îêíà èãðîêà
+  g_Game_SetupScreenSize();
+
+// Ðåæèì íàáëþäàòåëÿ:
+  if nPlayers = 0 then
+  begin
+    gPlayer1 := nil;
+    gPlayer2 := nil;
+  end;
+
+  if nPlayers >= 1 then
+  begin
+  // Ñîçäàíèå ïåðâîãî èãðîêà:
+    gPlayer1 := g_Player_Get(g_Player_Create(gPlayer1Settings.Model,
+                                             gPlayer1Settings.Color,
+                                             gPlayer1Settings.Team, False));
+    if gPlayer1 = nil then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [1]));
+      Exit;
+    end;
+
+    gPlayer1.Name := gPlayer1Settings.Name;
+  end;
+
+  if nPlayers >= 2 then
+  begin
+  // Ñîçäàíèå âòîðîãî èãðîêà:
+    gPlayer2 := g_Player_Get(g_Player_Create(gPlayer2Settings.Model,
+                                             gPlayer2Settings.Color,
+                                             gPlayer2Settings.Team, False));
+    if gPlayer2 = nil then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [2]));
+      Exit;
+    end;
+
+    gPlayer2.Name := gPlayer2Settings.Name;
+  end;
+
+// Ñòàðòóåì ñåðâåð
+  if not g_Net_Host(IPAddr, Port, NetMaxClients) then
+  begin
+    g_FatalError(_lc[I_NET_MSG] + _lc[I_NET_ERR_HOST]);
+    Exit;
+  end;
+
+  g_Net_Slist_Set(NetSlistIP, NetSlistPort);
+
+// Çàãðóçêà è çàïóñê êàðòû:
+  if not g_Game_StartMap(Map, True) then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [Map]));
+    Exit;
+  end;
+
+// Íåò òî÷åê ïîÿâëåíèÿ:
+  if (g_Map_GetPointCount(RESPAWNPOINT_PLAYER1) +
+      g_Map_GetPointCount(RESPAWNPOINT_PLAYER2) +
+      g_Map_GetPointCount(RESPAWNPOINT_DM) +
+      g_Map_GetPointCount(RESPAWNPOINT_RED)+
+      g_Map_GetPointCount(RESPAWNPOINT_BLUE)) < 1 then
+  begin
+    g_FatalError(_lc[I_GAME_ERROR_GET_SPAWN]);
+    Exit;
+  end;
+
+// Íàñòðîéêè èãðîêîâ è áîòîâ:
+  g_Player_Init();
+
+  NetState := NET_STATE_GAME;
+end;
+
+procedure g_Game_StartClient(Addr: String; Port: Word; PW: String);
+var
+  Map: String;
+  WadName: string;
+  Ptr: Pointer;
+  T: Cardinal;
+  MID: Byte;
+  State: Byte;
+  OuterLoop: Boolean;
+  newResPath: string;
+begin
+  g_Game_Free();
+
+  State := 0;
+  e_WriteLog('Starting net game (client)...', MSG_NOTIFY);
+  e_WriteLog('NET: Trying to connect to ' + Addr + ':' + IntToStr(Port) + '...', MSG_NOTIFY);
+
+  g_Game_ClearLoading();
+
+// Íàñòðîéêè èãðû:
+  gGameSettings.GameType := GT_CLIENT;
+
+  gCoopTotalMonstersKilled := 0;
+  gCoopTotalSecretsFound := 0;
+  gCoopTotalMonsters := 0;
+  gCoopTotalSecrets := 0;
+  gAimLine := False;
+  gShowMap := False;
+
+  g_Game_ExecuteEvent('ongamestart');
+
+// Óñòàíîâêà ðàçìåðîâ îêîí èãðîêîâ:
+  g_Game_SetupScreenSize();
+
+  NetState := NET_STATE_AUTH;
+
+  g_Game_SetLoadingText(_lc[I_LOAD_CONNECT], 0, False);
+// Ñòàðòóåì êëèåíò
+  if not g_Net_Connect(Addr, Port) then
+  begin
+    g_FatalError(_lc[I_NET_MSG] + _lc[I_NET_ERR_CONN]);
+    NetState := NET_STATE_NONE;
+    Exit;
+  end;
+
+  g_Game_SetLoadingText(_lc[I_LOAD_SEND_INFO], 0, False);
+  MC_SEND_Info(PW);
+  g_Game_SetLoadingText(_lc[I_LOAD_WAIT_INFO], 0, False);
+
+  OuterLoop := True;
+  while OuterLoop do
+  begin
+    while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+    begin
+      if (NetEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
+      begin
+        Ptr := NetEvent.packet^.data;
+        e_Raw_Seek(0);
+
+        MID := e_Raw_Read_Byte(Ptr);
+
+        if (MID = NET_MSG_INFO) and (State = 0) then
+        begin
+          NetMyID := e_Raw_Read_Byte(Ptr);
+          NetPlrUID1 := e_Raw_Read_Word(Ptr);
+
+          WadName := e_Raw_Read_String(Ptr);
+          Map := e_Raw_Read_String(Ptr);
+
+          gWADHash := e_Raw_Read_MD5(Ptr);
+
+          gGameSettings.GameMode := e_Raw_Read_Byte(Ptr);
+          gSwitchGameMode := gGameSettings.GameMode;
+          gGameSettings.GoalLimit := e_Raw_Read_Word(Ptr);
+          gGameSettings.TimeLimit := e_Raw_Read_Word(Ptr);
+          gGameSettings.MaxLives := e_Raw_Read_Byte(Ptr);
+          gGameSettings.Options := e_Raw_Read_LongWord(Ptr);
+          T := e_Raw_Read_LongWord(Ptr);
+
+          newResPath := g_Res_SearchSameWAD(MapsDir, WadName, gWADHash);
+          if newResPath = '' then
+          begin
+            g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
+            newResPath := g_Res_DownloadWAD(WadName);
+            if newResPath = '' then
+            begin
+              g_FatalError(_lc[I_NET_ERR_HASH]);
+              enet_packet_destroy(NetEvent.packet);
+              NetState := NET_STATE_NONE;
+              Exit;
+            end;
+          end;
+          newResPath := ExtractRelativePath(MapsDir, newResPath);
+
+          gPlayer1 := g_Player_Get(g_Player_Create(gPlayer1Settings.Model,
+                                                   gPlayer1Settings.Color,
+                                                   gPlayer1Settings.Team, False));
+
+          if gPlayer1 = nil then
+          begin
+            g_FatalError(Format(_lc[I_GAME_ERROR_PLAYER_CREATE], [1]));
+
+            enet_packet_destroy(NetEvent.packet);
+            NetState := NET_STATE_NONE;
+            Exit;
+          end;
+
+          gPlayer1.Name := gPlayer1Settings.Name;
+          gPlayer1.UID := NetPlrUID1;
+          gPlayer1.Reset(True);
+
+          if not g_Game_StartMap(newResPath + ':\' + Map, True) then
+          begin
+            g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [WadName + ':\' + Map]));
+
+            enet_packet_destroy(NetEvent.packet);
+            NetState := NET_STATE_NONE;
+            Exit;
+          end;
+
+          gTime := T;
+
+          State := 1;
+          OuterLoop := False;
+          enet_packet_destroy(NetEvent.packet);
+          break;
+        end
+        else
+          enet_packet_destroy(NetEvent.packet);
+      end
+      else
+        if (NetEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
+        begin
+          State := 0;
+          if (NetEvent.data <= NET_DISC_MAX) then
+            g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' +
+            _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + NetEvent.data)], True);
+          OuterLoop := False;
+          Break;
+        end;
+    end;
+
+    ProcessLoading();
+
+    e_PollInput();
+    
+    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
+    begin
+      State := 0;
+      break;
+    end;
+  end;
+
+  if State <> 1 then
+  begin
+    g_FatalError(_lc[I_NET_MSG] + _lc[I_NET_ERR_CONN]);
+    NetState := NET_STATE_NONE;
+    Exit;
+  end;
+
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+
+  g_Player_Init();
+  NetState := NET_STATE_GAME;
+  MC_SEND_FullStateRequest;
+  e_WriteLog('NET: Connection successful.', MSG_NOTIFY);
+end;
+
+procedure g_Game_SaveOptions();
+begin
+  g_Options_Write_Video(GameDir+'/'+CONFIG_FILENAME);
+end;
+
+procedure g_Game_ChangeMap(MapPath: String);
+var
+  Force: Boolean;
+begin
+  g_Game_ClearLoading();
+
+  Force := gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF];
+  // Åñëè óðîâåíü çàâåðøèëñÿ ïî òðèããåðó Âûõîä, íå î÷èùàòü èíâåíòàðü
+  if gExitByTrigger then
+  begin
+    Force := False;
+    gExitByTrigger := False;
+  end;
+  if not g_Game_StartMap(MapPath, Force) then
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [MapPath]));
+end;
+
+procedure g_Game_Restart();
+var
+  Map: string;
+begin
+  if g_Game_IsClient then
+    Exit;
+  g_ProcessResourceStr(gMapInfo.Map, nil, nil, @Map);
+
+  MessageTime := 0;
+  gGameOn := False;
+  g_Game_ClearLoading();
+  g_Game_StartMap(Map, True);
+end;
+
+function g_Game_StartMap(Map: String; Force: Boolean = False): Boolean;
+var
+  NewWAD, ResName: String;
+  I: Integer;
+begin
+  g_Map_Free();
+  g_Player_RemoveAllCorpses();
+
+  if (not g_Game_IsClient) and
+     (gSwitchGameMode <> gGameSettings.GameMode) and
+     (gGameSettings.GameMode <> GM_SINGLE) then
+  begin
+    if gSwitchGameMode = GM_CTF then
+      gGameSettings.MaxLives := 0;
+    gGameSettings.GameMode := gSwitchGameMode;
+    Force := True;
+  end else
+    gSwitchGameMode := gGameSettings.GameMode;
+
+  g_Player_ResetTeams();
+
+  if Pos(':\', Map) > 0 then
+  begin
+    g_ProcessResourceStr(Map, @NewWAD, nil, @ResName);
+    if g_Game_IsServer then
+    begin
+      gWADHash := MD5File(MapsDir + NewWAD);
+      g_Game_LoadWAD(NewWAD);
+    end else
+      // hash recieved in MC_RECV_GameEvent -> NET_EV_MAPSTART 
+      g_Game_ClientWAD(NewWAD, gWADHash);
+  end else
+    ResName := Map;
+
+  Result := g_Map_Load(MapsDir + gGameSettings.WAD + ':\' + ResName);
+  if Result then
+    begin
+      g_Player_ResetAll(Force or gLastMap, gGameSettings.GameType = GT_SINGLE);
+
+      gState := STATE_NONE;
+      g_ActiveWindow := nil;
+      gGameOn := True;
+
+      DisableCheats();
+      ResetTimer();
+
+      if gGameSettings.GameMode = GM_CTF then
+      begin
+        g_Map_ResetFlag(FLAG_RED);
+        g_Map_ResetFlag(FLAG_BLUE);
+        // CTF, à ôëàãîâ íåò:
+        if not g_Map_HaveFlagPoints() then
+          g_SimpleError(_lc[I_GAME_ERROR_CTF]);
+      end;
+    end
+  else
+    begin
+      gState := STATE_MENU;
+      gGameOn := False;
+    end;
+
+  gExit := 0;
+  gPause := False;
+  gTime := 0;
+  NetTimeToUpdate := 1;
+  NetTimeToReliable := 0;
+  NetTimeToMaster := NetMasterRate;
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+  gMissionFailed := False;
+  gNextMap := '';
+
+  gCoopMonstersKilled := 0;
+  gCoopSecretsFound := 0;
+
+  gVoteInProgress := False;
+  gVotePassed := False;
+  gVoteCount := 0;
+  gVoted := False;
+
+  gStatsOff := False;
+
+  if not gGameOn then Exit;
+
+  g_Game_SpectateCenterView();
+
+  if (gGameSettings.MaxLives > 0) and (gGameSettings.WarmupTime > 0) then
+  begin
+    gLMSRespawn := LMS_RESPAWN_WARMUP;
+    gLMSRespawnTime := gTime + gGameSettings.WarmupTime*1000;
+    gLMSSoftSpawn := True;
+    if NetMode = NET_SERVER then
+      MH_SEND_GameEvent(NET_EV_LMS_WARMUP, (gLMSRespawnTime - gTime) div 1000)
+    else
+      g_Console_Add(Format(_lc[I_MSG_WARMUP_START], [(gLMSRespawnTime - gTime) div 1000]), True);
+  end;
+
+  if NetMode = NET_SERVER then
+  begin
+    MH_SEND_GameEvent(NET_EV_MAPSTART, gGameSettings.GameMode, Map);
+
+  // Ìàñòåðñåðâåð
+    if NetUseMaster then
+    begin
+      if (NetMHost = nil) or (NetMPeer = nil) then
+        if not g_Net_Slist_Connect then
+          g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR]);
+
+      g_Net_Slist_Update;
+    end;
+
+    if NetClients <> nil then
+      for I := 0 to High(NetClients) do
+        if NetClients[I].Used then
+        begin
+          NetClients[I].Voted := False;
+          if NetClients[I].RequestedFullUpdate then
+          begin
+            MH_SEND_Everything((NetClients[I].State = NET_STATE_AUTH), I);
+            NetClients[I].RequestedFullUpdate := False;
+          end;
+        end;
+
+    g_Net_UnbanNonPermHosts();
+  end;
+
+  if gLastMap then
+  begin
+    gCoopTotalMonstersKilled := 0;
+    gCoopTotalSecretsFound := 0;
+    gCoopTotalMonsters := 0;
+    gCoopTotalSecrets := 0;
+    gLastMap := False;
+  end;
+  
+  g_Game_ExecuteEvent('onmapstart');
+end;
+
+procedure SetFirstLevel();
+begin
+  gNextMap := '';
+
+  MapList := g_Map_GetMapsList(MapsDir + gGameSettings.WAD);
+  if MapList = nil then
+    Exit;
+
+  SortSArray(MapList);
+  gNextMap := MapList[Low(MapList)];
+
+  MapList := nil;
+end;
+
+procedure g_Game_ExitLevel(Map: Char16);
+begin
+  gNextMap := Map;
+
+  gCoopTotalMonstersKilled := gCoopTotalMonstersKilled + gCoopMonstersKilled;
+  gCoopTotalSecretsFound := gCoopTotalSecretsFound + gCoopSecretsFound;
+  gCoopTotalMonsters := gCoopTotalMonsters + gTotalMonsters;
+  gCoopTotalSecrets := gCoopTotalSecrets + gSecretsCount;
+
+// Âûøëè â âûõîä â Îäèíî÷íîé èãðå:
+  if gGameSettings.GameType = GT_SINGLE then
+    gExit := EXIT_ENDLEVELSINGLE
+  else // Âûøëè â âûõîä â Ñâîåé èãðå
+  begin
+    gExit := EXIT_ENDLEVELCUSTOM;
+    if gGameSettings.GameMode = GM_COOP then
+      g_Player_RememberAll;
+
+    if not g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + gNextMap) then
+    begin
+      gLastMap := True;
+      if gGameSettings.GameMode = GM_COOP then
+        gStatsOff := True;
+
+      gStatsPressed := True;
+      gNextMap := 'MAP01';
+
+      if not g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + gNextMap) then
+        g_Game_NextLevel;
+
+      if g_Game_IsNet then
+      begin
+        MH_SEND_GameStats();
+        MH_SEND_CoopStats();
+      end;
+    end;
+  end;
+end;
+
+procedure g_Game_RestartLevel();
+var
+  Map: string;
+begin
+  if gGameSettings.GameMode = GM_SINGLE then
+  begin
+    g_Game_Restart();
+    Exit;
+  end;
+  gExit := EXIT_ENDLEVELCUSTOM;
+  g_ProcessResourceStr(gMapInfo.Map, nil, nil, @Map);
+  gNextMap := Map;
+end;
+
+procedure g_Game_ClientWAD(NewWAD: String; WHash: TMD5Digest);
+var
+  gWAD: String;
+begin
+  if LowerCase(NewWAD) = LowerCase(gGameSettings.WAD) then
+    Exit;
+  if not g_Game_IsClient then
+    Exit;
+  gWAD := g_Res_SearchSameWAD(MapsDir, ExtractFileName(NewWAD), WHash);
+  if gWAD = '' then
+  begin
+    g_Game_SetLoadingText(_lc[I_LOAD_DL_RES], 0, False);
+    gWAD := g_Res_DownloadWAD(ExtractFileName(NewWAD));
+    if gWAD = '' then
+    begin
+      g_Game_Free();
+      g_FatalError(Format(_lc[I_GAME_ERROR_MAP_WAD], [ExtractFileName(NewWAD)]));
+      Exit;
+    end;
+  end;
+  NewWAD := ExtractRelativePath(MapsDir, gWAD);
+  g_Game_LoadWAD(NewWAD);
+end;
+
+procedure g_Game_RestartRound(NoMapRestart: Boolean = False);
+var
+  i, n, nb, nr: Integer;
+begin
+  if not g_Game_IsServer then Exit;
+  if gLMSRespawn = LMS_RESPAWN_NONE then Exit;
+  gLMSRespawn := LMS_RESPAWN_NONE;
+  gLMSRespawnTime := 0;
+  MessageTime := 0;
+
+  if (gGameSettings.GameMode = GM_COOP) and not NoMapRestart then
+  begin
+    gMissionFailed := True;
+    g_Game_RestartLevel;
+    Exit;
+  end;
+
+  n := 0; nb := 0; nr := 0;
+  for i := Low(gPlayers) to High(gPlayers) do
+    if (gPlayers[i] <> nil) and
+       ((not gPlayers[i].FSpectator) or gPlayers[i].FWantsInGame or
+        (gPlayers[i] is TBot)) then
+      begin
+        Inc(n);
+        if gPlayers[i].Team = TEAM_RED then Inc(nr)
+        else if gPlayers[i].Team = TEAM_BLUE then Inc(nb)
+      end;
+
+  if (n < 2) or ((gGameSettings.GameMode = GM_TDM) and ((nr = 0) or (nb = 0))) then
+  begin
+    // wait a second until the fuckers finally decide to join
+    gLMSRespawn := LMS_RESPAWN_WARMUP;
+    gLMSRespawnTime := gTime + 1000;
+    gLMSSoftSpawn := NoMapRestart;
+    Exit;
+  end;
+
+  g_Player_RemoveAllCorpses;
+  g_Game_Message(_lc[I_MESSAGE_LMS_START], 144);
+  if g_Game_IsNet then
+    MH_SEND_GameEvent(NET_EV_LMS_START);
+
+  for i := Low(gPlayers) to High(gPlayers) do
+  begin
+    if gPlayers[i] = nil then continue;
+    if gPlayers[i] is TBot then gPlayers[i].FWantsInGame := True;
+    // don't touch normal spectators
+    if gPlayers[i].FSpectator and not gPlayers[i].FWantsInGame then
+    begin
+      gPlayers[i].FNoRespawn := True;
+      gPlayers[i].Lives := 0;
+      if g_Game_IsNet then
+        MH_SEND_PlayerStats(gPlayers[I].UID);
+      continue;
+    end;
+    gPlayers[i].FNoRespawn := False;
+    gPlayers[i].Lives := gGameSettings.MaxLives;
+    gPlayers[i].Respawn(False, True);
+    if gGameSettings.GameMode = GM_COOP then
+    begin
+      gPlayers[i].Frags := 0;
+      gPlayers[i].RecallState;
+    end;
+    if (gPlayer1 = nil) and (gLMSPID1 > 0) then
+      gPlayer1 := g_Player_Get(gLMSPID1);
+    if (gPlayer2 = nil) and (gLMSPID2 > 0) then
+      gPlayer2 := g_Player_Get(gLMSPID2);
+  end;
+
+  for i := Low(gItems) to High(gItems) do
+  begin
+    if gItems[i].Respawnable then
+    begin
+      gItems[i].QuietRespawn := True;
+      gItems[i].RespawnTime := 0;
+    end
+    else
+    begin
+      g_Items_Remove(i);
+      if g_Game_IsNet then MH_SEND_ItemDestroy(True, i);
+    end;
+  end;
+
+  for i := Low(gMonsters) to High(gMonsters) do
+  begin
+    if (gMonsters[i] <> nil) and not gMonsters[i].FNoRespawn then
+      gMonsters[i].Respawn;
+  end;
+
+  gLMSSoftSpawn := False;
+end;
+
+function g_Game_GetFirstMap(WAD: String): String;
+begin
+  Result := '';
+
+  MapList := g_Map_GetMapsList(WAD);
+  if MapList = nil then
+    Exit;
+
+  SortSArray(MapList);
+  Result := MapList[Low(MapList)];
+
+  if not g_Map_Exist(WAD + ':\' + Result) then
+    Result := '';
+
+  MapList := nil;
+end;
+
+function g_Game_GetNextMap(): String;
+var
+  I: Integer;
+  Map: string;
+begin
+  Result := '';
+
+  MapList := g_Map_GetMapsList(MapsDir + gGameSettings.WAD);
+  if MapList = nil then
+    Exit;
+
+  g_ProcessResourceStr(gMapInfo.Map, nil, nil, @Map);
+
+  SortSArray(MapList);
+  MapIndex := -255;
+  for I := Low(MapList) to High(MapList) do
+    if Map = MapList[I] then
+    begin
+      MapIndex := I;
+      Break;
+    end;
+
+  if MapIndex <> -255 then
+  begin
+    if MapIndex = High(MapList) then
+     Result := MapList[Low(MapList)]
+    else
+      Result := MapList[MapIndex + 1];
+
+    if not g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + Result) then Result := Map;
+  end;
+
+  MapList := nil;
+end;
+
+procedure g_Game_NextLevel();
+begin
+  if gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF, GM_COOP] then
+    gExit := EXIT_ENDLEVELCUSTOM
+  else
+  begin
+    gExit := EXIT_ENDLEVELSINGLE;
+    Exit;
+  end;
+
+  if gNextMap <> '' then Exit;
+  gNextMap := g_Game_GetNextMap();
+end;
+
+function g_Game_IsTestMap(): Boolean;
+var
+  FName, Sect, Res: String;
+begin
+  g_ProcessResourceStr(gMapInfo.Map, FName, Sect, Res);
+  Result := UpperCase(Res) = TEST_MAP_NAME;
+end;
+
+procedure g_Game_DeleteTestMap();
+var
+  WAD: TWADEditor_1;
+  MapName: Char16;
+  MapList: SArray;
+  a, time: Integer;
+  WadName: string;
+begin
+  a := Pos('.wad:\', gMapToDelete);
+  if a = 0 then
+    Exit;
+
+// Âûäåëÿåì èìÿ wad-ôàéëà è èìÿ êàðòû:
+  WadName := Copy(gMapToDelete, 1, a + 3);
+  Delete(gMapToDelete, 1, a + 5);
+  gMapToDelete := UpperCase(gMapToDelete);
+  MapName := '';
+  CopyMemory(@MapName[0], @gMapToDelete[1], Min(16, Length(gMapToDelete)));
+
+// Èìÿ êàðòû íå ñòàíäàðòíîå òåñòîâîå:
+  if MapName <> TEST_MAP_NAME then
+    Exit;
+
+  if not gTempDelete then
+  begin
+    time := g_GetFileTime(WadName);
+    WAD := TWADEditor_1.Create();
+
+  // ×èòàåì Wad-ôàéë:
+    if not WAD.ReadFile(WadName) then
+    begin // Íåò òàêîãî WAD-ôàéëà
+      WAD.Free();
+      Exit;
+    end;
+
+  // Ñîñòàâëÿåì ñïèñîê êàðò è èùåì íóæíóþ:
+    WAD.CreateImage();
+    MapList := WAD.GetResourcesList('');
+
+    if MapList <> nil then
+      for a := 0 to High(MapList) do
+        if MapList[a] = MapName then
+        begin
+        // Óäàëÿåì è ñîõðàíÿåì:
+          WAD.RemoveResource('', MapName);
+          WAD.SaveTo(WadName);
+          Break;
+        end;
+
+    WAD.Free();
+    g_SetFileTime(WadName, time);
+  end else
+    DeleteFile(WadName);
+end;
+
+procedure GameCVars(P: SArray);
+var
+  a, b: Integer;
+  stat: TPlayerStatArray;
+  cmd, s: string;
+  config: TConfig;
+begin
+  stat := nil;
+  cmd := LowerCase(P[0]);
+  if cmd = 'r_showfps' then
+  begin
+    if (Length(P) > 1) and
+       ((P[1] = '1') or (P[1] = '0')) then
+      gShowFPS := (P[1][1] = '1');
+
+    if gShowFPS then
+      g_Console_Add(_lc[I_MSG_SHOW_FPS_ON])
+    else
+      g_Console_Add(_lc[I_MSG_SHOW_FPS_OFF]);
+  end
+  else if (cmd = 'g_friendlyfire') and not g_Game_IsClient then
+  begin
+    with gGameSettings do
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+      begin
+        if (P[1][1] = '1') then
+          Options := Options or GAME_OPTION_TEAMDAMAGE
+        else
+          Options := Options and (not GAME_OPTION_TEAMDAMAGE);
+      end;
+
+      if (LongBool(Options and GAME_OPTION_TEAMDAMAGE)) then
+        g_Console_Add(_lc[I_MSG_FRIENDLY_FIRE_ON])
+      else
+        g_Console_Add(_lc[I_MSG_FRIENDLY_FIRE_OFF]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if (cmd = 'g_weaponstay') and not g_Game_IsClient then
+  begin
+    with gGameSettings do
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+      begin
+        if (P[1][1] = '1') then
+          Options := Options or GAME_OPTION_WEAPONSTAY
+        else
+          Options := Options and (not GAME_OPTION_WEAPONSTAY);
+      end;
+
+      if (LongBool(Options and GAME_OPTION_WEAPONSTAY)) then
+        g_Console_Add(_lc[I_MSG_WEAPONSTAY_ON])
+      else
+        g_Console_Add(_lc[I_MSG_WEAPONSTAY_OFF]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if cmd = 'g_gamemode' then
+  begin
+    a := g_Game_TextToMode(P[1]);
+    if a = GM_SINGLE then a := GM_COOP;
+    if (Length(P) > 1) and (a <> GM_NONE) and (not g_Game_IsClient) then
+    begin
+      gSwitchGameMode := a;
+      if (gGameOn and (gGameSettings.GameMode = GM_SINGLE)) or
+         (gState = STATE_INTERSINGLE) then
+        gSwitchGameMode := GM_SINGLE;
+      if not gGameOn then
+        gGameSettings.GameMode := gSwitchGameMode;
+    end;
+    if gSwitchGameMode = gGameSettings.GameMode then
+      g_Console_Add(Format(_lc[I_MSG_GAMEMODE_CURRENT],
+                          [g_Game_ModeToText(gGameSettings.GameMode)]))
+    else
+      g_Console_Add(Format(_lc[I_MSG_GAMEMODE_CHANGE],
+                          [g_Game_ModeToText(gGameSettings.GameMode),
+                           g_Game_ModeToText(gSwitchGameMode)]));
+  end
+  else if (cmd = 'g_allow_exit') and not g_Game_IsClient then
+  begin
+    with gGameSettings do
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+      begin
+        if (P[1][1] = '1') then
+          Options := Options or GAME_OPTION_ALLOWEXIT
+        else
+          Options := Options and (not GAME_OPTION_ALLOWEXIT);
+      end;
+        
+      if (LongBool(Options and GAME_OPTION_ALLOWEXIT)) then
+        g_Console_Add(_lc[I_MSG_ALLOWEXIT_ON])
+      else
+        g_Console_Add(_lc[I_MSG_ALLOWEXIT_OFF]);
+      g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if (cmd = 'g_allow_monsters') and not g_Game_IsClient then
+  begin
+    with gGameSettings do
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+      begin
+        if (P[1][1] = '1') then
+          Options := Options or GAME_OPTION_MONSTERS
+        else
+          Options := Options and (not GAME_OPTION_MONSTERS);
+      end;
+
+      if (LongBool(Options and GAME_OPTION_MONSTERS)) then
+        g_Console_Add(_lc[I_MSG_ALLOWMON_ON])
+      else
+        g_Console_Add(_lc[I_MSG_ALLOWMON_OFF]);
+      g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if (cmd = 'g_bot_vsplayers') and not g_Game_IsClient then
+  begin
+    with gGameSettings do
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+      begin
+        if (P[1][1] = '1') then
+          Options := Options or GAME_OPTION_BOTVSPLAYER
+        else
+          Options := Options and (not GAME_OPTION_BOTVSPLAYER);
+      end;
+        
+      if (LongBool(Options and GAME_OPTION_BOTVSPLAYER)) then
+        g_Console_Add(_lc[I_MSG_BOTSVSPLAYERS_ON])
+      else
+        g_Console_Add(_lc[I_MSG_BOTSVSPLAYERS_OFF]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if (cmd = 'g_bot_vsmonsters') and not g_Game_IsClient then
+  begin
+    with gGameSettings do
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+      begin
+        if (P[1][1] = '1') then
+          Options := Options or GAME_OPTION_BOTVSMONSTER
+        else
+          Options := Options and (not GAME_OPTION_BOTVSMONSTER);
+      end;
+
+      if (LongBool(Options and GAME_OPTION_BOTVSMONSTER)) then
+        g_Console_Add(_lc[I_MSG_BOTSVSMONSTERS_ON])
+      else
+        g_Console_Add(_lc[I_MSG_BOTSVSMONSTERS_OFF]);
+
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end
+  else if (cmd = 'g_warmuptime') and not g_Game_IsClient then
+  begin
+    if Length(P) > 1 then
+    begin
+      if StrToIntDef(P[1], gGameSettings.WarmupTime) = 0 then
+        gGameSettings.WarmupTime := 30
+      else
+        gGameSettings.WarmupTime := StrToIntDef(P[1], gGameSettings.WarmupTime);
+    end;
+
+    g_Console_Add(Format(_lc[I_MSG_WARMUP],
+                 [gGameSettings.WarmupTime]));
+    g_Console_Add(_lc[I_MSG_ONMAPCHANGE]);
+  end
+  else if cmd = 'net_interp' then
+  begin
+    if (Length(P) > 1) then
+      NetInterpLevel := StrToIntDef(P[1], NetInterpLevel);
+
+    g_Console_Add('net_interp = ' + IntToStr(NetInterpLevel));
+    config := TConfig.CreateFile(GameDir+'/'+CONFIG_FILENAME);
+    config.WriteInt('Client', 'InterpolationSteps', NetInterpLevel);
+    config.SaveFile(GameDir+'/'+CONFIG_FILENAME);
+    config.Free();
+  end
+  else if cmd = 'net_forceplayerupdate' then
+  begin
+    if (Length(P) > 1) and
+       ((P[1] = '1') or (P[1] = '0')) then
+      NetForcePlayerUpdate := (P[1][1] = '1');
+
+    if NetForcePlayerUpdate then
+      g_Console_Add('net_forceplayerupdate = 1')
+    else
+      g_Console_Add('net_forceplayerupdate = 0');
+    config := TConfig.CreateFile(GameDir+'/'+CONFIG_FILENAME);
+    config.WriteBool('Client', 'ForcePlayerUpdate', NetForcePlayerUpdate);
+    config.SaveFile(GameDir+'/'+CONFIG_FILENAME);
+    config.Free();
+  end
+  else if cmd = 'net_predictself' then
+  begin
+    if (Length(P) > 1) and
+       ((P[1] = '1') or (P[1] = '0')) then
+      NetPredictSelf := (P[1][1] = '1');
+
+    if NetPredictSelf then
+      g_Console_Add('net_predictself = 1')
+    else
+      g_Console_Add('net_predictself = 0');
+    config := TConfig.CreateFile(GameDir+'/'+CONFIG_FILENAME);
+    config.WriteBool('Client', 'PredictSelf', NetPredictSelf);
+    config.SaveFile(GameDir+'/'+CONFIG_FILENAME);
+    config.Free();
+  end
+  else if cmd = 'sv_name' then
+  begin
+    if (Length(P) > 1) and (Length(P[1]) > 0) then
+    begin
+      NetServerName := P[1];
+      if Length(NetServerName) > 64 then
+        SetLength(NetServerName, 64);
+      if g_Game_IsServer and g_Game_IsNet and NetUseMaster then
+        g_Net_Slist_Update;
+    end;
+
+    g_Console_Add(cmd + ' = "' + NetServerName + '"');
+  end
+  else if cmd = 'sv_passwd' then
+  begin
+    if (Length(P) > 1) and (Length(P[1]) > 0) then
+    begin
+      NetPassword := P[1];
+      if Length(NetPassword) > 24 then
+        SetLength(NetPassword, 24);
+      if g_Game_IsServer and g_Game_IsNet and NetUseMaster then
+        g_Net_Slist_Update;
+    end;
+
+    g_Console_Add(cmd + ' = "' + AnsiLowerCase(NetPassword) + '"');
+  end
+  else if cmd = 'sv_maxplrs' then
+  begin
+    if (Length(P) > 1) then
+    begin
+      NetMaxClients := Min(Max(StrToIntDef(P[1], NetMaxClients), 1), NET_MAXCLIENTS);
+      if g_Game_IsServer and g_Game_IsNet then
+      begin
+        b := 0;
+        for a := 0 to High(NetClients) do
+          if NetClients[a].Used then
+          begin
+            Inc(b);
+            if b > NetMaxClients then
+            begin
+              s := g_Player_Get(NetClients[a].Player).Name;
+              enet_peer_disconnect(NetClients[a].Peer, NET_DISC_FULL);
+              g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
+              MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
+            end;
+          end;
+        if NetUseMaster then
+          g_Net_Slist_Update;
+      end;
+    end;
+
+    g_Console_Add(cmd + ' = ' + IntToStr(NetMaxClients));
+  end
+  else if cmd = 'sv_public' then
+  begin
+    if (Length(P) > 1) then
+    begin
+      NetUseMaster := StrToIntDef(P[1], Byte(NetUseMaster)) > 0;
+      if g_Game_IsServer and g_Game_IsNet then
+        if NetUseMaster then
+        begin
+          if NetMPeer = nil then
+            if not g_Net_Slist_Connect() then
+              g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR]);
+          g_Net_Slist_Update();
+        end
+        else
+          if NetMPeer <> nil then
+            g_Net_Slist_Disconnect();
+    end;
+
+    g_Console_Add(cmd + ' = ' + IntToStr(Byte(NetUseMaster)));
+  end
+  else if cmd = 'sv_intertime' then
+  begin
+    if (Length(P) > 1) then
+      gDefInterTime := Min(Max(StrToIntDef(P[1], gDefInterTime), -1), 120);
+
+    g_Console_Add(cmd + ' = ' + IntToStr(gDefInterTime));
+  end
+  else if cmd = 'p1_name' then
+  begin
+    if (Length(P) > 1) and gGameOn then
+    begin
+      if g_Game_IsClient then
+      begin
+        gPlayer1Settings.Name := b_Text_Unformat(P[1]);
+        MC_SEND_PlayerSettings;
+      end
+      else
+        if gPlayer1 <> nil then
+        begin
+          gPlayer1.Name := b_Text_Unformat(P[1]);
+          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+        end
+        else
+          gPlayer1Settings.Name := b_Text_Unformat(P[1]);
+    end;
+  end
+  else if cmd = 'p2_name' then
+  begin
+    if (Length(P) > 1) and gGameOn then
+    begin
+      if g_Game_IsClient then
+      begin
+        gPlayer2Settings.Name := b_Text_Unformat(P[1]);
+        MC_SEND_PlayerSettings;
+      end
+      else
+        if gPlayer2 <> nil then
+        begin
+          gPlayer2.Name := b_Text_Unformat(P[1]);
+          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
+        end
+        else
+          gPlayer2Settings.Name := b_Text_Unformat(P[1]);
+    end;
+  end
+  else if cmd = 'p1_color' then
+  begin
+    if Length(P) > 3 then
+      if g_Game_IsClient then
+      begin
+        gPlayer1Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
+                                       EnsureRange(StrToIntDef(P[2], 0), 0, 255),
+                                       EnsureRange(StrToIntDef(P[3], 0), 0, 255));
+        MC_SEND_PlayerSettings;
+      end
+      else
+        if gPlayer1 <> nil then
+        begin
+          gPlayer1.Model.SetColor(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
+                                  EnsureRange(StrToIntDef(P[2], 0), 0, 255),
+                                  EnsureRange(StrToIntDef(P[3], 0), 0, 255));
+          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+        end
+        else
+          gPlayer1Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
+                                         EnsureRange(StrToIntDef(P[2], 0), 0, 255),
+                                         EnsureRange(StrToIntDef(P[3], 0), 0, 255));
+  end
+  else if (cmd = 'p2_color') and not g_Game_IsNet then
+  begin
+    if Length(P) > 3 then
+      if g_Game_IsClient then
+      begin
+        gPlayer2Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
+                                       EnsureRange(StrToIntDef(P[2], 0), 0, 255),
+                                       EnsureRange(StrToIntDef(P[3], 0), 0, 255));
+        MC_SEND_PlayerSettings;
+      end
+      else
+        if gPlayer2 <> nil then
+        begin
+          gPlayer2.Model.SetColor(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
+                                  EnsureRange(StrToIntDef(P[2], 0), 0, 255),
+                                  EnsureRange(StrToIntDef(P[3], 0), 0, 255));
+          if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer2.UID);
+        end
+        else
+          gPlayer2Settings.Color := _RGB(EnsureRange(StrToIntDef(P[1], 0), 0, 255),
+                                         EnsureRange(StrToIntDef(P[2], 0), 0, 255),
+                                         EnsureRange(StrToIntDef(P[3], 0), 0, 255));
+  end
+  else if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
+  begin
+    if cmd = 'r_showtime' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gShowTime := (P[1][1] = '1');
+
+      if gShowTime then
+        g_Console_Add(_lc[I_MSG_TIME_ON])
+      else
+        g_Console_Add(_lc[I_MSG_TIME_OFF]);
+    end
+    else if cmd = 'r_showscore' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gShowGoals := (P[1][1] = '1');
+
+      if gShowGoals then
+        g_Console_Add(_lc[I_MSG_SCORE_ON])
+      else
+        g_Console_Add(_lc[I_MSG_SCORE_OFF]);
+    end
+    else if cmd = 'r_showstat' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gShowStat := (P[1][1] = '1');
+
+      if gShowStat then
+        g_Console_Add(_lc[I_MSG_STATS_ON])
+      else
+        g_Console_Add(_lc[I_MSG_STATS_OFF]);
+    end
+    else if cmd = 'r_showkillmsg' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gShowKillMsg := (P[1][1] = '1');
+
+      if gShowKillMsg then
+        g_Console_Add(_lc[I_MSG_KILL_MSGS_ON])
+      else
+        g_Console_Add(_lc[I_MSG_KILL_MSGS_OFF]);
+    end
+    else if cmd = 'r_showlives' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gShowLives := (P[1][1] = '1');
+
+      if gShowLives then
+        g_Console_Add(_lc[I_MSG_LIVES_ON])
+      else
+        g_Console_Add(_lc[I_MSG_LIVES_OFF]);
+    end
+    else if cmd = 'r_showspect' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gSpectHUD := (P[1][1] = '1');
+
+      if gSpectHUD then
+        g_Console_Add(_lc[I_MSG_SPECT_HUD_ON])
+      else
+        g_Console_Add(_lc[I_MSG_SPECT_HUD_OFF]);
+    end
+    else if cmd = 'r_showping' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        gShowPing := (P[1][1] = '1');
+
+      if gShowPing then
+        g_Console_Add(_lc[I_MSG_PING_ON])
+      else
+        g_Console_Add(_lc[I_MSG_PING_OFF]);
+    end
+    else if (cmd = 'g_scorelimit') and not g_Game_IsClient then
+    begin
+      if Length(P) > 1 then
+      begin
+        if StrToIntDef(P[1], gGameSettings.GoalLimit) = 0 then
+          gGameSettings.GoalLimit := 0
+        else
+          begin
+            b := 0;
+
+            if gGameSettings.GameMode = GM_DM then
+              begin // DM
+                stat := g_Player_GetStats();
+                if stat <> nil then
+                  for a := 0 to High(stat) do
+                    if stat[a].Frags > b then
+                      b := stat[a].Frags;
+              end
+            else // TDM/CTF
+              b := Max(gTeamStat[TEAM_RED].Goals, gTeamStat[TEAM_BLUE].Goals);
+
+            gGameSettings.GoalLimit := Max(StrToIntDef(P[1], gGameSettings.GoalLimit), b);
+          end;
+
+        if g_Game_IsNet then MH_SEND_GameSettings;
+      end;
+
+      g_Console_Add(Format(_lc[I_MSG_SCORE_LIMIT], [gGameSettings.GoalLimit]));
+    end
+    else if (cmd = 'g_timelimit') and not g_Game_IsClient then
+    begin
+      if (Length(P) > 1) and (StrToIntDef(P[1], -1) >= 0) then
+        gGameSettings.TimeLimit := StrToIntDef(P[1], -1);
+
+      g_Console_Add(Format(_lc[I_MSG_TIME_LIMIT],
+                           [gGameSettings.TimeLimit div 3600,
+                           (gGameSettings.TimeLimit div 60) mod 60,
+                            gGameSettings.TimeLimit mod 60]));
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end
+    else if (cmd = 'g_maxlives') and not g_Game_IsClient then
+    begin
+      if Length(P) > 1 then
+      begin
+        if StrToIntDef(P[1], gGameSettings.MaxLives) = 0 then
+          gGameSettings.MaxLives := 0
+        else
+        begin
+          b := 0;
+          stat := g_Player_GetStats();
+          if stat <> nil then
+            for a := 0 to High(stat) do
+              if stat[a].Lives > b then
+                b := stat[a].Lives;
+          gGameSettings.MaxLives :=
+            Max(StrToIntDef(P[1], gGameSettings.MaxLives), b);
+        end;
+      end;
+
+      g_Console_Add(Format(_lc[I_MSG_LIVES],
+                           [gGameSettings.MaxLives]));
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end;
+  end;
+end;
+
+procedure DebugCommands(P: SArray);
+var
+  a, b: Integer;
+  cmd: string;
+  //pt: TPoint;
+begin
+// Êîìàíäû îòëàäî÷íîãî ðåæèìà:
+  if gDebugMode then
+  begin
+    cmd := LowerCase(P[0]);
+    if cmd = 'd_window' then
+    begin
+      g_Console_Add(Format('gWinPosX = %d, gWinPosY %d', [gWinPosX, gWinPosY]));
+      g_Console_Add(Format('gWinRealPosX = %d, gWinRealPosY %d', [gWinRealPosX, gWinRealPosY]));
+      g_Console_Add(Format('gScreenWidth = %d, gScreenHeight = %d', [gScreenWidth, gScreenHeight]));
+      g_Console_Add(Format('gWinSizeX = %d, gWinSizeY = %d', [gWinSizeX, gWinSizeY]));
+      g_Console_Add(Format('Frame X = %d, Y = %d, Caption Y = %d', [gWinFrameX, gWinFrameY, gWinCaption]));
+    end
+    else if cmd = 'd_sounds' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        g_Debug_Sounds := (P[1][1] = '1');
+
+      g_Console_Add(Format('d_sounds is %d', [Byte(g_Debug_Sounds)]));
+    end
+    else if cmd = 'd_frames' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        g_Debug_Frames := (P[1][1] = '1');
+
+      g_Console_Add(Format('d_frames is %d', [Byte(g_Debug_Frames)]));
+    end
+    else if cmd = 'd_winmsg' then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        g_Debug_WinMsgs := (P[1][1] = '1');
+
+      g_Console_Add(Format('d_winmsg is %d', [Byte(g_Debug_WinMsgs)]));
+    end
+    else if (cmd = 'd_monoff') and not g_Game_IsNet then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        g_Debug_MonsterOff := (P[1][1] = '1');
+
+      g_Console_Add(Format('d_monoff is %d', [Byte(g_debug_MonsterOff)]));
+    end
+    else if (cmd = 'd_botoff') and not g_Game_IsNet then
+    begin
+      if Length(P) > 1 then
+        case P[1][1] of
+          '0': g_debug_BotAIOff := 0;
+          '1': g_debug_BotAIOff := 1;
+          '2': g_debug_BotAIOff := 2;
+          '3': g_debug_BotAIOff := 3;
+        end;
+
+      g_Console_Add(Format('d_botoff is %d', [g_debug_BotAIOff]));
+    end
+    else if cmd = 'd_monster' then
+    begin
+      if gGameOn and (gPlayer1 <> nil) and (gPlayer1.Live) and (not g_Game_IsNet) then
+        if Length(P) < 2 then
+        begin
+          g_Console_Add(cmd + ' [ID | Name] [behaviour]');
+          g_Console_Add('ID | Name');
+          for b := MONSTER_DEMON to MONSTER_MAN do
+            g_Console_Add(Format('%2d | %s', [b, g_Monsters_GetNameByID(b)]));
+        end else
+        begin
+          a := StrToIntDef(P[1], 0);
+          if (a < MONSTER_DEMON) or (a > MONSTER_MAN) then
+            a := g_Monsters_GetIDByName(P[1]);
+
+          if (a < MONSTER_DEMON) or (a > MONSTER_MAN) then
+            g_Console_Add(Format(_lc[I_MSG_NO_MONSTER], [P[1]]))
+          else
+            begin
+              with gPlayer1.Obj do
+                b := g_Monsters_Create(a,
+                     X + Rect.X + (Rect.Width div 2),
+                     Y + Rect.Y + Rect.Height,
+                     gPlayer1.Direction, True);
+              if (Length(P) > 2) and (b >= 0) then
+                gMonsters[b].MonsterBehaviour := Min(Max(StrToIntDef(P[2], BH_NORMAL), BH_NORMAL), BH_GOOD);
+            end;
+        end;
+    end
+    else if (cmd = 'd_health') then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        g_debug_HealthBar := (P[1][1] = '1');
+
+      g_Console_Add(Format('d_health is %d', [Byte(g_debug_HealthBar)]));
+    end
+    else if (cmd = 'd_player') then
+    begin
+      if (Length(P) > 1) and
+         ((P[1] = '1') or (P[1] = '0')) then
+        g_debug_Player := (P[1][1] = '1');
+
+      g_Console_Add(Format(cmd + ' is %d', [Byte(g_Debug_Player)]));
+    end
+    else if (cmd = 'd_joy') then
+    begin
+      for a := 1 to 8 do
+        g_Console_Add(e_JoystickStateToString(a));
+    end;
+  end
+    else
+      g_Console_Add(_lc[I_MSG_NOT_DEBUG]);
+end;
+
+procedure GameCommands(P: SArray);
+var
+  a, b: Integer;
+  s, pw: String;
+  chstr: string;
+  cmd: string;
+  pl: pTNetClient;
+  plr: TPlayer;
+  prt: Word;
+  nm: Boolean;
+  listen: LongWord;
+begin
+// Îáùèå êîìàíäû:
+  cmd := LowerCase(P[0]);
+  chstr := '';
+  if (cmd = 'quit') or
+     (cmd = 'exit') then
+  begin
+    g_Game_Free();
+    g_Game_Quit();
+    Exit;
+  end
+  else if cmd = 'pause' then
+  begin
+    if (g_ActiveWindow = nil) then
+      g_Game_Pause(not gPause);
+  end
+  else if cmd = 'endgame' then
+    gExit := EXIT_SIMPLE
+  else if cmd = 'restart' then
+  begin
+    if gGameOn or (gState in [STATE_INTERSINGLE, STATE_INTERCUSTOM]) then
+    begin
+      if g_Game_IsClient then
+      begin
+        g_Console_Add(_lc[I_MSG_SERVERONLY]);
+        Exit;
+      end;
+      g_Game_Restart();
+    end else
+      g_Console_Add(_lc[I_MSG_NOT_GAME]);
+  end
+  else if cmd = 'kick' then
+  begin
+    if g_Game_IsServer then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('kick <name>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('kick <name>');
+        Exit;
+      end;
+
+      if g_Game_IsNet then
+        pl := g_Net_Client_ByName(P[1]);
+      if (pl <> nil) then
+      begin
+        s := g_Net_ClientName_ByID(pl^.ID);
+        enet_peer_disconnect(pl^.Peer, NET_DISC_KICK);
+        g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
+        MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
+        if NetUseMaster then
+          g_Net_Slist_Update;
+      end else if gPlayers <> nil then
+        for a := Low(gPlayers) to High(gPlayers) do
+          if gPlayers[a] <> nil then
+            if Copy(LowerCase(gPlayers[a].Name), 1, Length(P[1])) = LowerCase(P[1]) then
+            begin
+              // Íå îòêëþ÷àòü îñíîâíûõ èãðîêîâ â ñèíãëå
+              if not(gPlayers[a] is TBot) and (gGameSettings.GameType = GT_SINGLE) then
+                continue;
+              gPlayers[a].Lives := 0;
+              gPlayers[a].Kill(K_SIMPLEKILL, 0, HIT_DISCON);
+              g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [gPlayers[a].Name]), True);
+              g_Player_Remove(gPlayers[a].UID);
+              if NetUseMaster then
+                g_Net_Slist_Update;
+              // Åñëè íå ïåðåìåøàòü, ïðè äîáàâëåíèè íîâûõ áîòîâ ïîÿâÿòñÿ ñòàðûå
+              g_Bot_MixNames();
+            end;
+    end else
+      g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+  end
+  else if cmd = 'kick_id' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('kick_id <client ID>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('kick_id <client ID>');
+        Exit;
+      end;
+
+      a := StrToIntDef(P[1], 0);
+      if (NetClients <> nil) and (a <= High(NetClients)) then
+      begin
+        if NetClients[a].Used and (NetClients[a].Peer <> nil) then
+        begin
+          s := g_Net_ClientName_ByID(NetClients[a].ID);
+          enet_peer_disconnect(NetClients[a].Peer, NET_DISC_KICK);
+          g_Console_Add(Format(_lc[I_PLAYER_KICK], [s]));
+          MH_SEND_GameEvent(NET_EV_PLAYER_KICK, 0, s);
+          if NetUseMaster then
+            g_Net_Slist_Update;
+        end;
+      end;
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'ban' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('ban <name>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('ban <name>');
+        Exit;
+      end;
+
+      pl := g_Net_Client_ByName(P[1]);
+      if (pl <> nil) then
+      begin
+        s := g_Net_ClientName_ByID(pl^.ID);
+        g_Net_BanHost(pl^.Peer^.address.host, False);
+        enet_peer_disconnect(pl^.Peer, NET_DISC_TEMPBAN);
+        g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
+        MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
+        if NetUseMaster then
+          g_Net_Slist_Update;
+      end else
+        g_Console_Add(Format(_lc[I_NET_ERR_NAME404], [P[1]]));
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'ban_id' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('ban_id <client ID>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('ban_id <client ID>');
+        Exit;
+      end;
+
+      a := StrToIntDef(P[1], 0);
+      if (NetClients <> nil) and (a <= High(NetClients)) then
+        if NetClients[a].Used and (NetClients[a].Peer <> nil) then
+        begin
+          s := g_Net_ClientName_ByID(NetClients[a].ID);
+          g_Net_BanHost(NetClients[a].Peer^.address.host, False);
+          enet_peer_disconnect(NetClients[a].Peer, NET_DISC_TEMPBAN);
+          g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
+          MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
+          if NetUseMaster then
+            g_Net_Slist_Update;
+        end;
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'permban' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('permban <name>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('permban <name>');
+        Exit;
+      end;
+
+      pl := g_Net_Client_ByName(P[1]);
+      if (pl <> nil) then
+      begin
+        s := g_Net_ClientName_ByID(pl^.ID);
+        g_Net_BanHost(pl^.Peer^.address.host);
+        enet_peer_disconnect(pl^.Peer, NET_DISC_BAN);
+        g_Net_SaveBanList();
+        g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
+        MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
+        if NetUseMaster then
+          g_Net_Slist_Update;
+      end else
+        g_Console_Add(Format(_lc[I_NET_ERR_NAME404], [P[1]]));
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'permban_id' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('permban_id <client ID>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('permban_id <client ID>');
+        Exit;
+      end;
+
+      a := StrToIntDef(P[1], 0);
+      if (NetClients <> nil) and (a <= High(NetClients)) then
+        if NetClients[a].Used and (NetClients[a].Peer <> nil) then
+        begin
+          s := g_Net_ClientName_ByID(NetClients[a].ID);
+          g_Net_BanHost(NetClients[a].Peer^.address.host);
+          enet_peer_disconnect(NetClients[a].Peer, NET_DISC_BAN);
+          g_Net_SaveBanList();
+          g_Console_Add(Format(_lc[I_PLAYER_BAN], [s]));
+          MH_SEND_GameEvent(NET_EV_PLAYER_BAN, 0, s);
+          if NetUseMaster then
+            g_Net_Slist_Update;
+        end;
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'unban' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('unban <IP Address>');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('unban <IP Address>');
+        Exit;
+      end;
+
+      if g_Net_UnbanHost(P[1]) then
+      begin
+        g_Console_Add(Format(_lc[I_MSG_UNBAN_OK], [P[1]]));
+        g_Net_SaveBanList();
+      end else
+        g_Console_Add(Format(_lc[I_MSG_UNBAN_FAIL], [P[1]]));
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'clientlist' then
+  begin
+    if g_Game_IsServer and g_Game_IsNet then
+    begin
+      b := 0;
+      if NetClients <> nil then
+        for a := Low(NetClients) to High(NetClients) do
+          if NetClients[a].Used and (NetClients[a].Peer <> nil) then
+          begin
+            plr := g_Player_Get(NetClients[a].Player);
+            if plr = nil then continue;
+            Inc(b);
+            g_Console_Add(Format('#%2d: %-15s | %s', [a,
+                          IpToStr(NetClients[a].Peer^.address.host), plr.Name]));
+          end;
+      if b = 0 then
+        g_Console_Add(_lc[I_MSG_NOCLIENTS]);
+    end else
+      g_Console_Add(_lc[I_MSG_SERVERONLY]);
+  end
+  else if cmd = 'connect' then
+  begin
+    if (NetMode = NET_NONE) then
+    begin
+      if Length(P) < 2 then
+      begin
+        g_Console_Add('connect <IP> [port] [password]');
+        Exit;
+      end;
+      if P[1] = '' then
+      begin
+        g_Console_Add('connect <IP> [port] [password]');
+        Exit;
+      end;
+
+      if Length(P) > 2 then
+        prt := StrToIntDef(P[2], 25666)
+      else
+        prt := 25666;
+
+      if Length(P) > 3 then
+        pw := P[3]
+      else
+        pw := '';
+
+      g_Game_StartClient(P[1], prt, pw);
+    end;
+  end
+  else if cmd = 'disconnect' then
+  begin
+    if (NetMode = NET_CLIENT) then
+      g_Net_Disconnect();
+  end
+  else if cmd = 'reconnect' then
+  begin
+    if (NetMode = NET_SERVER) then
+      Exit;
+
+    if (NetMode = NET_CLIENT) then
+    begin
+      g_Net_Disconnect();
+      gExit := EXIT_SIMPLE;
+      EndGame;
+    end;
+
+    //TODO: Use last successful password to reconnect, instead of ''
+    g_Game_StartClient(NetClientIP, NetClientPort, '');
+  end
+  else if (cmd = 'addbot') or
+     (cmd = 'bot_add') then
+  begin
+    if Length(P) > 1 then
+      g_Bot_Add(TEAM_NONE, StrToIntDef(P[1], 2))
+    else
+      g_Bot_Add(TEAM_NONE, 2);
+  end
+  else if cmd = 'bot_addlist' then
+  begin
+    if Length(P) > 1 then
+      if Length(P) = 2 then
+        g_Bot_AddList(TEAM_NONE, P[1], StrToIntDef(P[1], -1))
+      else
+        g_Bot_AddList(IfThen(P[2] = 'red', TEAM_RED, TEAM_BLUE), P[1], StrToIntDef(P[1], -1));
+  end
+  else if cmd = 'bot_removeall' then
+    g_Bot_RemoveAll()
+  else if cmd = 'chat' then
+  begin
+    if g_Game_IsNet then
+    begin
+      if Length(P) > 1 then
+      begin
+        for a := 1 to High(P) do
+          chstr := chstr + P[a] + ' ';
+
+        if Length(chstr) > 200 then SetLength(chstr, 200);
+
+        if Length(chstr) < 1 then
+        begin
+          g_Console_Add('chat <text>');
+          Exit;
+        end;
+
+        chstr := b_Text_Format(chstr);
+        if g_Game_IsClient then
+          MC_SEND_Chat(chstr, NET_CHAT_PLAYER)
+        else
+          MH_SEND_Chat(gPlayer1Settings.Name + ': ' + chstr, NET_CHAT_PLAYER);
+      end
+      else
+        g_Console_Add('chat <text>');
+    end else
+      g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+  end
+  else if cmd = 'teamchat' then
+  begin
+    if g_Game_IsNet and (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+    begin
+      if Length(P) > 1 then
+      begin
+        for a := 1 to High(P) do
+          chstr := chstr + P[a] + ' ';
+
+        if Length(chstr) > 200 then SetLength(chstr, 200);
+
+        if Length(chstr) < 1 then
+        begin
+          g_Console_Add('teamchat <text>');
+          Exit;
+        end;
+
+        chstr := b_Text_Format(chstr);
+        if g_Game_IsClient then
+          MC_SEND_Chat(chstr, NET_CHAT_TEAM)
+        else
+          MH_SEND_Chat(gPlayer1Settings.Name + ': ' + chstr, NET_CHAT_TEAM,
+            gPlayer1Settings.Team);
+      end
+      else
+        g_Console_Add('teamchat <text>');
+    end else
+      g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+  end
+  else if cmd = 'game' then
+  begin
+    if gGameSettings.GameType <> GT_NONE then
+    begin
+      g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+      Exit;
+    end;
+    if Length(P) = 1 then
+    begin
+      g_Console_Add(cmd + ' <WAD> [MAP] [# players]');
+      Exit;
+    end;
+    // Èãðà åù¸ íå çàïóùåíà, ñíà÷àëà íàì íàäî çàãðóçèòü êàêîé-òî WAD
+    if Pos('.wad', LowerCase(P[1])) = 0 then
+      P[1] := P[1] + '.wad';
+
+    if FileExists(MapsDir + P[1]) then
+    begin
+      // Åñëè êàðòà íå óêàçàíà, áåð¸ì ïåðâóþ êàðòó â ôàéëå
+      if Length(P) < 3 then
+      begin
+        SetLength(P, 3);
+        P[2] := g_Game_GetFirstMap(MapsDir + P[1]);
+      end;
+
+      s := P[1] + ':\' + UpperCase(P[2]);
+
+      if g_Map_Exist(MapsDir + s) then
+      begin
+        // Çàïóñêàåì ñâîþ èãðó
+        g_Game_Free();
+        with gGameSettings do
+        begin
+          GameMode := g_Game_TextToMode(gcGameMode);
+          if gSwitchGameMode <> GM_NONE then
+            GameMode := gSwitchGameMode;
+          if GameMode = GM_NONE then GameMode := GM_DM;
+          if GameMode = GM_SINGLE then GameMode := GM_COOP;
+          b := 1;
+          if Length(P) >= 4 then
+            b := StrToIntDef(P[3], 1);
+          g_Game_StartCustom(s, GameMode, TimeLimit,
+                             GoalLimit, MaxLives, Options, b);
+        end;
+      end
+      else
+        if P[2] = '' then
+          g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[1]]))
+        else
+          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [UpperCase(P[2])]));
+    end else
+      g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
+  end
+  else if cmd = 'host' then
+  begin
+    if gGameSettings.GameType <> GT_NONE then
+    begin
+      g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+      Exit;
+    end;
+    if Length(P) < 4 then
+    begin
+      g_Console_Add(cmd + ' <listen IP> <port> <WAD> [MAP] [# players]');
+      Exit;
+    end;
+    if not StrToIp(P[1], listen) then
+      Exit;
+    prt := StrToIntDef(P[2], 25666);
+
+    if Pos('.wad', LowerCase(P[3])) = 0 then
+      P[3] := P[3] + '.wad';
+
+    if FileExists(MapsDir + P[3]) then
+    begin
+      // Åñëè êàðòà íå óêàçàíà, áåð¸ì ïåðâóþ êàðòó â ôàéëå
+      if Length(P) < 5 then
+      begin
+        SetLength(P, 5);
+        P[4] := g_Game_GetFirstMap(MapsDir + P[1]);
+      end;
+
+      s := P[3] + ':\' + UpperCase(P[4]);
+
+      if g_Map_Exist(MapsDir + s) then
+      begin
+        // Çàïóñêàåì ñâîþ èãðó
+        g_Game_Free();
+        with gGameSettings do
+        begin
+          GameMode := g_Game_TextToMode(gcGameMode);
+          if gSwitchGameMode <> GM_NONE then
+            GameMode := gSwitchGameMode;
+          if GameMode = GM_NONE then GameMode := GM_DM;
+          if GameMode = GM_SINGLE then GameMode := GM_COOP;
+          b := 0;
+          if Length(P) >= 6 then
+            b := StrToIntDef(P[5], 0);
+          g_Game_StartServer(s, GameMode, TimeLimit,
+                             GoalLimit, MaxLives, Options, b, listen, prt);
+        end;
+      end
+      else
+        if P[4] = '' then
+          g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[3]]))
+        else
+          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [UpperCase(P[4])]));
+    end else
+      g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[3]]));
+  end
+  else if cmd = 'map' then
+  begin
+    if Length(P) = 1 then
+    begin
+      if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
+      begin
+        g_Console_Add(cmd + ' <MAP>');
+        g_Console_Add(cmd + ' <WAD> [MAP]');
+      end else
+        g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+    end else
+      if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
+      begin
+        // Èä¸ò ñâîÿ èãðà èëè ñåðâåð
+        if Length(P) < 3 then
+        begin
+          // Ïåðâûé ïàðàìåòð - ëèáî êàðòà, ëèáî èìÿ WAD ôàéëà
+          s := UpperCase(P[1]);
+          if g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + s) then
+          begin // Êàðòà íàøëàñü
+            gExitByTrigger := False;
+            if gGameOn then
+            begin // Èä¸ò èãðà - çàâåðøàåì óðîâåíü
+              gNextMap := s;
+              gExit := EXIT_ENDLEVELCUSTOM;
+            end
+            else // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
+              g_Game_ChangeMap(s);
+          end else
+          begin
+            g_Console_Add(Format(_lc[I_MSG_NO_MAP], [s]));
+            // Òàêîé êàðòû íåò, èùåì WAD ôàéë
+            if Pos('.wad', LowerCase(P[1])) = 0 then
+              P[1] := P[1] + '.wad';
+
+            if FileExists(MapsDir + P[1]) then
+            begin
+              // Ïàðàìåòðà êàðòû íåò, ïîýòîìó ñòàâèì ïåðâóþ èç ôàéëà
+              SetLength(P, 3);
+              P[2] := g_Game_GetFirstMap(MapsDir + P[1]);
+
+              s := P[1] + ':\' + P[2];
+
+              if g_Map_Exist(MapsDir + s) then
+              begin
+                gExitByTrigger := False;
+                if gGameOn then
+                begin // Èä¸ò èãðà - çàâåðøàåì óðîâåíü
+                  gNextMap := s;
+                  gExit := EXIT_ENDLEVELCUSTOM;
+                end
+                else // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
+                  g_Game_ChangeMap(s);
+              end else
+                if P[2] = '' then
+                  g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[1]]))
+                else
+                  g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
+            end else
+              g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
+          end;
+        end else
+        begin
+          // Óêàçàíî äâà ïàðàìåòðà, çíà÷èò ïåðâûé - WAD ôàéë, à âòîðîé - êàðòà
+          if Pos('.wad', LowerCase(P[1])) = 0 then
+            P[1] := P[1] + '.wad';
+
+          if FileExists(MapsDir + P[1]) then
+          begin
+            // Íàøëè WAD ôàéë
+            P[2] := UpperCase(P[2]);
+            s := P[1] + ':\' + P[2];
+
+            if g_Map_Exist(MapsDir + s) then
+            begin // Íàøëè êàðòó
+              gExitByTrigger := False;
+              if gGameOn then
+              begin // Èä¸ò èãðà - çàâåðøàåì óðîâåíü
+                gNextMap := s;
+                gExit := EXIT_ENDLEVELCUSTOM;
+              end
+              else // Èíòåðìèññèÿ - ñðàçó çàãðóæàåì êàðòó
+                g_Game_ChangeMap(s);
+            end else
+              g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
+          end else
+            g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
+        end;
+      end else
+        g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+  end
+  else if cmd = 'nextmap' then
+  begin
+    if not(gGameOn or (gState = STATE_INTERCUSTOM)) then
+      g_Console_Add(_lc[I_MSG_NOT_GAME])
+    else begin
+      nm := True;
+      if Length(P) = 1 then
+      begin
+        if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
+        begin
+          g_Console_Add(cmd + ' <MAP>');
+          g_Console_Add(cmd + ' <WAD> [MAP]');
+        end else begin
+          nm := False;
+          g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+        end;
+      end else
+      begin
+        nm := False;
+        if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
+        begin
+          if Length(P) < 3 then
+          begin
+            // Ïåðâûé ïàðàìåòð - ëèáî êàðòà, ëèáî èìÿ WAD ôàéëà
+            s := UpperCase(P[1]);
+            if g_Map_Exist(MapsDir + gGameSettings.WAD + ':\' + s) then
+            begin // Êàðòà íàøëàñü
+              gExitByTrigger := False;
+              gNextMap := s;
+              nm := True;
+            end else
+            begin
+              g_Console_Add(Format(_lc[I_MSG_NO_MAP], [s]));
+              // Òàêîé êàðòû íåò, èùåì WAD ôàéë
+              if Pos('.wad', LowerCase(P[1])) = 0 then
+                P[1] := P[1] + '.wad';
+
+              if FileExists(MapsDir + P[1]) then
+              begin
+                // Ïàðàìåòðà êàðòû íåò, ïîýòîìó ñòàâèì ïåðâóþ èç ôàéëà
+                SetLength(P, 3);
+                P[2] := g_Game_GetFirstMap(MapsDir + P[1]);
+
+                s := P[1] + ':\' + P[2];
+
+                if g_Map_Exist(MapsDir + s) then
+                begin // Óñòàíàâëèâàåì êàðòó
+                  gExitByTrigger := False;
+                  gNextMap := s;
+                  nm := True;
+                end else
+                  if P[2] = '' then
+                    g_Console_Add(Format(_lc[I_MSG_NO_MAPS], [P[1]]))
+                  else
+                    g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
+              end else
+                g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
+            end;
+          end else
+          begin
+            // Óêàçàíî äâà ïàðàìåòðà, çíà÷èò ïåðâûé - WAD ôàéë, à âòîðîé - êàðòà
+            if Pos('.wad', LowerCase(P[1])) = 0 then
+              P[1] := P[1] + '.wad';
+
+            if FileExists(MapsDir + P[1]) then
+            begin
+              // Íàøëè WAD ôàéë
+              P[2] := UpperCase(P[2]);
+              s := P[1] + ':\' + P[2];
+
+              if g_Map_Exist(MapsDir + s) then
+              begin // Íàøëè êàðòó
+                gExitByTrigger := False;
+                gNextMap := s;
+                nm := True;
+              end else
+                g_Console_Add(Format(_lc[I_MSG_NO_MAP], [P[2]]));
+            end else
+              g_Console_Add(Format(_lc[I_MSG_NO_WAD], [P[1]]));
+          end;
+        end else
+          g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+      end;
+      if nm then
+        if gNextMap = '' then
+          g_Console_Add(_lc[I_MSG_NEXTMAP_UNSET])
+        else
+          g_Console_Add(Format(_lc[I_MSG_NEXTMAP_SET], [gNextMap]));
+    end;
+  end
+  else if (cmd = 'endmap') or (cmd = 'goodbye') then
+  begin
+    if not gGameOn then
+      g_Console_Add(_lc[I_MSG_NOT_GAME])
+    else
+      if g_Game_IsServer and (gGameSettings.GameType <> GT_SINGLE) then
+      begin
+        gExitByTrigger := False;
+        // Ñëåäóþùàÿ êàðòà íå çàäàíà, ïðîáóåì íàéòè òðèããåð Âûõîä
+        if (gNextMap = '') and (gTriggers <> nil) then
+          for a := 0 to High(gTriggers) do
+            if gTriggers[a].TriggerType = TRIGGER_EXIT then
+            begin
+              gExitByTrigger := True;
+              gNextMap := gTriggers[a].Data.MapName;
+              Break;
+            end;
+        // Èùåì ñëåäóþùóþ êàðòó â WAD ôàéëå
+        if gNextMap = '' then
+          gNextMap := g_Game_GetNextMap();
+        // Ïðîâåðÿåì, íå çàäàí ëè WAD ôàéë ðåñóðñíîé ñòðîêîé
+        if Pos(':\', gNextMap) = 0 then
+          s := gGameSettings.WAD + ':\' + gNextMap
+        else
+          s := gNextMap;
+        // Åñëè êàðòà íàéäåíà, âûõîäèì ñ óðîâíÿ
+        if g_Map_Exist(MapsDir + s) then
+          gExit := EXIT_ENDLEVELCUSTOM
+        else
+          g_Console_Add(Format(_lc[I_MSG_NO_MAP], [gNextMap]));
+      end else
+        g_Console_Add(_lc[I_MSG_GM_UNAVAIL]);
+  end
+  else if (cmd = 'event') then
+  begin
+    if (Length(P) <= 1) then
+    begin
+      for a := 0 to High(gEvents) do
+        if gEvents[a].Command = '' then
+          g_Console_Add(gEvents[a].Name + ' <none>')
+        else
+          g_Console_Add(gEvents[a].Name + ' "' + gEvents[a].Command + '"');
+      Exit;
+    end;
+    if (Length(P) = 2) then
+    begin
+      for a := 0 to High(gEvents) do
+        if gEvents[a].Name = P[1] then
+          if gEvents[a].Command = '' then
+            g_Console_Add(gEvents[a].Name + ' <none>')
+          else
+            g_Console_Add(gEvents[a].Name + ' "' + gEvents[a].Command + '"');
+      Exit;
+    end;
+    for a := 0 to High(gEvents) do
+      if gEvents[a].Name = P[1] then
+      begin
+        gEvents[a].Command := '';
+        for b := 2 to High(P) do
+          if Pos(' ', P[b]) = 0 then
+            gEvents[a].Command := gEvents[a].Command + ' ' + P[b]
+          else
+            gEvents[a].Command := gEvents[a].Command + ' "' + P[b] + '"';
+        gEvents[a].Command := Trim(gEvents[a].Command);
+        Exit;
+      end;
+  end
+// Êîìàíäû Ñâîåé èãðû:
+  else if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
+  begin
+    if cmd = 'bot_addred' then
+    begin
+      if Length(P) > 1 then
+        g_Bot_Add(TEAM_RED, StrToIntDef(P[1], 2))
+      else
+        g_Bot_Add(TEAM_RED, 2);
+    end
+    else if cmd = 'bot_addblue' then
+    begin
+      if Length(P) > 1 then
+        g_Bot_Add(TEAM_BLUE, StrToIntDef(P[1], 2))
+      else
+        g_Bot_Add(TEAM_BLUE, 2);
+    end
+    else if cmd = 'suicide' then
+    begin
+      if gGameOn then
+      begin
+        if g_Game_IsClient then
+          MC_SEND_CheatRequest(NET_CHEAT_SUICIDE)
+        else
+        begin
+          if gPlayer1 <> nil then
+            gPlayer1.Damage(SUICIDE_DAMAGE, gPlayer1.UID, 0, 0, HIT_SELF);
+          if gPlayer2 <> nil then
+            gPlayer2.Damage(SUICIDE_DAMAGE, gPlayer2.UID, 0, 0, HIT_SELF);
+        end;
+      end;
+    end
+    else if cmd = 'spectate' then
+    begin
+      if not gGameOn then
+        Exit;
+      g_Game_Spectate();
+    end
+    else if cmd = 'say' then
+    begin
+      if g_Game_IsServer and g_Game_IsNet then
+      begin
+        if Length(P) > 1 then
+        begin
+          chstr := '';
+          for a := 1 to High(P) do
+            chstr := chstr + P[a] + ' ';
+
+          if Length(chstr) > 200 then SetLength(chstr, 200);
+
+          if Length(chstr) < 1 then
+          begin
+            g_Console_Add('say <text>');
+            Exit;
+          end;
+
+          chstr := b_Text_Format(chstr);
+          MH_SEND_Chat(chstr, NET_CHAT_PLAYER);
+        end
+        else g_Console_Add('say <text>');
+      end else
+        g_Console_Add(_lc[I_MSG_SERVERONLY]);
+    end
+    else if cmd = 'tell' then
+    begin
+      if g_Game_IsServer and g_Game_IsNet then
+      begin
+        if (Length(P) > 2) and (P[1] <> '') then
+        begin
+          chstr := '';
+          for a := 2 to High(P) do
+            chstr := chstr + P[a] + ' ';
+
+          if Length(chstr) > 200 then SetLength(chstr, 200);
+
+          if Length(chstr) < 1 then
+          begin
+            g_Console_Add('tell <playername> <text>');
+            Exit;
+          end;
+
+          pl := g_Net_Client_ByName(P[1]);
+          if pl <> nil then
+            MH_SEND_Chat(b_Text_Format(chstr), NET_CHAT_PLAYER, pl^.ID)
+          else
+            g_Console_Add(Format(_lc[I_NET_ERR_NAME404], [P[1]]));
+        end
+        else g_Console_Add('tell <playername> <text>');
+      end else
+        g_Console_Add(_lc[I_MSG_SERVERONLY]);
+    end
+    else if (cmd = 'overtime') and not g_Game_IsClient then
+    begin
+      if (Length(P) = 1) or (StrToIntDef(P[1], -1) <= 0) then
+        Exit;
+      // Äîïîëíèòåëüíîå âðåìÿ:
+      gGameSettings.TimeLimit := (gTime - gGameStartTime) div 1000 + Word(StrToIntDef(P[1], 0));
+
+      g_Console_Add(Format(_lc[I_MSG_TIME_LIMIT],
+                           [gGameSettings.TimeLimit div 3600,
+                           (gGameSettings.TimeLimit div 60) mod 60,
+                            gGameSettings.TimeLimit mod 60]));
+      if g_Game_IsNet then MH_SEND_GameSettings;
+    end
+    else if (cmd = 'rcon_password') and g_Game_IsClient then
+    begin
+      if (Length(P) <= 1) then
+        g_Console_Add('rcon_password <password>')
+      else
+        MC_SEND_RCONPassword(P[1]);
+    end
+    else if cmd = 'rcon' then
+    begin
+      if g_Game_IsClient then
+      begin
+        if Length(P) > 1 then
+        begin
+          chstr := '';
+          for a := 1 to High(P) do
+            chstr := chstr + P[a] + ' ';
+
+          if Length(chstr) > 200 then SetLength(chstr, 200);
+
+          if Length(chstr) < 1 then
+          begin
+            g_Console_Add('rcon <command>');
+            Exit;
+          end;
+
+          MC_SEND_RCONCommand(chstr);
+        end
+        else g_Console_Add('rcon <command>');
+      end;
+    end
+    else if cmd = 'ready' then
+    begin
+      if g_Game_IsServer and (gLMSRespawn = LMS_RESPAWN_WARMUP) then
+        gLMSRespawnTime := gTime + 100;
+    end
+    else if (cmd = 'callvote') and g_Game_IsNet then
+    begin
+      if Length(P) > 1 then
+      begin
+        chstr := '';
+        for a := 1 to High(P) do begin
+          if a > 1 then chstr := chstr + ' ';
+          chstr := chstr + P[a];
+        end;
+
+        if Length(chstr) > 200 then SetLength(chstr, 200);
+
+        if Length(chstr) < 1 then
+        begin
+          g_Console_Add('callvote <command>');
+          Exit;
+        end;
+
+        if g_Game_IsClient then
+          MC_SEND_Vote(True, chstr)
+        else
+          g_Game_StartVote(chstr, gPlayer1Settings.Name);
+        g_Console_Process('vote', True);
+      end
+      else
+        g_Console_Add('callvote <command>');
+    end
+    else if (cmd = 'vote') and g_Game_IsNet then
+    begin
+      if g_Game_IsClient then
+        MC_SEND_Vote(False)
+      else if gVoteInProgress then
+      begin
+        if (gPlayer1 <> nil) or (gPlayer2 <> nil) then
+          a := Floor((NetClientCount+1)/2.0) + 1
+        else
+          a := Floor(NetClientCount/2.0) + 1;
+        if gVoted then
+        begin
+          Dec(gVoteCount);
+          gVoted := False;
+          g_Console_Add(Format(_lc[I_MESSAGE_VOTE_REVOKED], [gPlayer1Settings.Name, gVoteCount, a]), True);
+          MH_SEND_VoteEvent(NET_VE_REVOKE, gPlayer1Settings.Name, 'a', gVoteCount, a);
+        end
+        else
+        begin
+          Inc(gVoteCount);
+          gVoted := True;
+          g_Console_Add(Format(_lc[I_MESSAGE_VOTE_VOTE], [gPlayer1Settings.Name, gVoteCount, a]), True);
+          MH_SEND_VoteEvent(NET_VE_VOTE, gPlayer1Settings.Name, 'a', gVoteCount, a);
+          g_Game_CheckVote;
+        end;
+      end;
+    end
+  end;
+end;
+
+procedure g_TakeScreenShot();
+var
+  a: Word;
+  FileName: String;
+begin
+  for a := 1 to High(Word) do
+  begin
+    FileName := Format(GameDir+'/Screenshots/Screenshot%.3d.bmp', [a]);
+    if not FileExists(FileName) then
+    begin
+      e_MakeScreenshot(FileName, gScreenWidth, gScreenHeight);
+      g_Console_Add(Format(_lc[I_CONSOLE_SCREENSHOT], [ExtractFileName(FileName)]));
+      Break;
+    end;
+  end;
+end;
+
+procedure g_Game_InGameMenu(Show: Boolean);
+begin
+  if (g_ActiveWindow = nil) and Show then
+    begin
+      if gGameSettings.GameType = GT_SINGLE then
+        g_GUI_ShowWindow('GameSingleMenu')
+      else
+      begin
+        if g_Game_IsClient then
+          g_GUI_ShowWindow('GameClientMenu')
+        else
+          if g_Game_IsNet then
+            g_GUI_ShowWindow('GameServerMenu')
+          else
+            g_GUI_ShowWindow('GameCustomMenu');
+      end;
+      g_Sound_PlayEx('MENU_OPEN');
+
+    // Ïàóçà ïðè ìåíþ òîëüêî â îäèíî÷íîé èãðå:
+      if (not g_Game_IsNet) then
+        g_Game_Pause(True);
+    end
+  else
+    if (g_ActiveWindow <> nil) and (not Show) then
+    begin
+    // Ïàóçà ïðè ìåíþ òîëüêî â îäèíî÷íîé èãðå:
+      if (not g_Game_IsNet) then
+        g_Game_Pause(False);
+    end;
+end;
+
+procedure g_Game_Pause(Enable: Boolean);
+begin
+  if not gGameOn then
+    Exit;
+
+  if gPause = Enable then
+    Exit;
+
+  if not (gGameSettings.GameType in [GT_SINGLE, GT_CUSTOM]) then
+    Exit;
+
+  gPause := Enable;
+  g_Game_PauseAllSounds(Enable);
+end;
+
+procedure g_Game_PauseAllSounds(Enable: Boolean);
+var
+  i: Integer;
+begin
+// Òðèããåðû:
+  if gTriggers <> nil then
+    for i := 0 to High(gTriggers) do
+      with gTriggers[i] do
+        if (TriggerType = TRIGGER_SOUND) and
+           (Sound <> nil) and
+           Sound.IsPlaying() then
+        begin
+          Sound.Pause(Enable);
+        end;
+
+// Çâóêè èãðîêîâ:
+  if gPlayers <> nil then
+    for i := 0 to High(gPlayers) do
+      if gPlayers[i] <> nil then
+        gPlayers[i].PauseSounds(Enable);
+
+// Ìóçûêà:
+  if gMusic <> nil then
+    gMusic.Pause(Enable);
+end;
+
+procedure g_Game_StopAllSounds(all: Boolean);
+var
+  i: Integer;
+begin
+  if gTriggers <> nil then
+    for i := 0 to High(gTriggers) do
+      with gTriggers[i] do
+        if (TriggerType = TRIGGER_SOUND) and
+           (Sound <> nil) then
+          Sound.Stop();
+
+  if gMusic <> nil then
+    gMusic.Stop();
+
+  if all then
+    e_StopChannels();
+end;
+
+procedure g_Game_UpdateTriggerSounds();
+var
+  i: Integer;
+begin
+  if gTriggers <> nil then
+    for i := 0 to High(gTriggers) do
+      with gTriggers[i] do
+        if (TriggerType = TRIGGER_SOUND) and
+           (Sound <> nil) and
+           (Data.Local) and
+           Sound.IsPlaying() then
+        begin
+          if ((gPlayer1 <> nil) and g_CollidePoint(gPlayer1.GameX, gPlayer1.GameY, X, Y, Width, Height)) or
+             ((gPlayer2 <> nil) and g_CollidePoint(gPlayer2.GameX, gPlayer2.GameY, X, Y, Width, Height)) then
+          begin
+            Sound.SetPan(0.5 - Data.Pan/255.0);
+            Sound.SetVolume(Data.Volume/255.0);
+          end
+          else
+            Sound.SetCoords(X+(Width div 2), Y+(Height div 2), Data.Volume/255.0);
+        end;
+end;
+
+function g_Game_IsWatchedPlayer(UID: Word): Boolean;
+begin
+  Result := False;
+  if (gPlayer1 <> nil) and (gPlayer1.UID = UID) then
+  begin
+    Result := True;
+    Exit;
+  end;
+  if (gPlayer2 <> nil) and (gPlayer2.UID = UID) then
+  begin
+    Result := True;
+    Exit;
+  end;
+  if gSpectMode <> SPECT_PLAYERS then
+    Exit;
+  if gSpectPID1 = UID then
+  begin
+    Result := True;
+    Exit;
+  end;
+  if gSpectViewTwo and (gSpectPID2 = UID) then
+  begin
+    Result := True;
+    Exit;
+  end;
+end;
+
+function g_Game_IsWatchedTeam(Team: Byte): Boolean;
+var
+  Pl: TPlayer;
+begin
+  Result := False;
+  if (gPlayer1 <> nil) and (gPlayer1.Team = Team) then
+  begin
+    Result := True;
+    Exit;
+  end;
+  if (gPlayer2 <> nil) and (gPlayer2.Team = Team) then
+  begin
+    Result := True;
+    Exit;
+  end;
+  if gSpectMode <> SPECT_PLAYERS then
+    Exit;
+  Pl := g_Player_Get(gSpectPID1);
+  if (Pl <> nil) and (Pl.Team = Team) then
+  begin
+    Result := True;
+    Exit;
+  end;
+  if gSpectViewTwo then
+  begin
+    Pl := g_Player_Get(gSpectPID2);
+    if (Pl <> nil) and (Pl.Team = Team) then
+    begin
+      Result := True;
+      Exit;
+    end;
+  end;
+end;
+
+procedure g_Game_Message(Msg: string; Time: Word);
+begin
+  MessageText := b_Text_Format(Msg);
+  MessageTime := Time;
+end;
+
+procedure g_Game_Announce_GoodShot(SpawnerUID: Word);
+var
+  a: Integer;
+begin
+  case gAnnouncer of
+    ANNOUNCE_NONE:
+      Exit;
+    ANNOUNCE_ME,
+    ANNOUNCE_MEPLUS:
+      if not g_Game_IsWatchedPlayer(SpawnerUID) then
+        Exit;
+  end;
+  for a := 0 to 3 do
+    if goodsnd[a].IsPlaying() then
+      Exit;
+
+  goodsnd[Random(4)].Play();
+end;
+
+procedure g_Game_Announce_KillCombo(Param: Integer);
+var
+  UID: Word;
+  c, n: Byte;
+  Pl: TPlayer;
+  Name: String;
+begin
+  UID := Param and $FFFF;
+  c := Param shr 16;
+  if c < 2 then
+    Exit;
+
+  Pl := g_Player_Get(UID);
+  if Pl = nil then
+    Name := '?'
+  else
+    Name := Pl.Name;
+
+  case c of
+    2: begin
+      n := 0;
+      g_Console_Add(Format(_lc[I_PLAYER_KILL_2X], [Name]), True);
+    end;
+    3: begin
+      n := 1;
+      g_Console_Add(Format(_lc[I_PLAYER_KILL_3X], [Name]), True);
+    end;
+    4: begin
+      n := 2;
+      g_Console_Add(Format(_lc[I_PLAYER_KILL_4X], [Name]), True);
+    end;
+    else begin
+      n := 3;
+      g_Console_Add(Format(_lc[I_PLAYER_KILL_MX], [Name]), True);
+    end;
+  end;
+
+  case gAnnouncer of
+    ANNOUNCE_NONE:
+      Exit;
+    ANNOUNCE_ME:
+      if not g_Game_IsWatchedPlayer(UID) then
+        Exit;
+    ANNOUNCE_MEPLUS:
+      if (not g_Game_IsWatchedPlayer(UID)) and (c < 4) then
+        Exit;
+  end;
+
+  if killsnd[n].IsPlaying() then
+    killsnd[n].Stop();
+  killsnd[n].Play();
+end;
+
+procedure g_Game_StartVote(Command, Initiator: string);
+var
+  Need: Integer;
+begin
+  if not gVotesEnabled then Exit;
+  if gGameSettings.GameType <> GT_SERVER then Exit;
+  if gVoteInProgress or gVotePassed then
+  begin
+    g_Console_Add(Format(_lc[I_MESSAGE_VOTE_INPROGRESS], [gVoteCommand]), True);
+    MH_SEND_VoteEvent(NET_VE_INPROGRESS, gVoteCommand);
+    Exit;
+  end;
+  gVoteInProgress := True;
+  gVotePassed := False;
+  gVoteTimer := gTime + gVoteTimeout * 1000;
+  gVoteCount := 0;
+  gVoted := False;
+  gVoteCommand := Command;
+
+  if (gPlayer1 <> nil) or (gPlayer2 <> nil) then
+    Need := Floor((NetClientCount+1)/2.0)+1
+  else
+    Need := Floor(NetClientCount/2.0)+1;
+  g_Console_Add(Format(_lc[I_MESSAGE_VOTE_STARTED], [Initiator, Command, Need]), True);
+  MH_SEND_VoteEvent(NET_VE_STARTED, Initiator, Command, Need);
+end;
+
+procedure g_Game_CheckVote;
+var
+  I, Need: Integer;
+begin
+  if gGameSettings.GameType <> GT_SERVER then Exit;
+  if not gVoteInProgress then Exit;
+
+  if (gTime >= gVoteTimer) then
+  begin
+    if (gPlayer1 <> nil) or (gPlayer2 <> nil) then
+      Need := Floor((NetClientCount+1)/2.0) + 1
+    else
+      Need := Floor(NetClientCount/2.0) + 1;
+    if gVoteCount >= Need then
+    begin
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_PASSED], [gVoteCommand]), True);
+      MH_SEND_VoteEvent(NET_VE_PASSED, gVoteCommand);
+      gVotePassed := True;
+      gVoteCmdTimer := gTime + 5000;
+    end
+    else
+    begin
+      g_Console_Add(_lc[I_MESSAGE_VOTE_FAILED], True);
+      MH_SEND_VoteEvent(NET_VE_FAILED);
+    end;
+    if NetClients <> nil then
+      for I := Low(NetClients) to High(NetClients) do
+        if NetClients[i].Used then
+          NetClients[i].Voted := False;
+    gVoteInProgress := False;
+    gVoted := False;
+    gVoteCount := 0;
+  end
+  else
+  begin
+    if (gPlayer1 <> nil) or (gPlayer2 <> nil) then
+      Need := Floor((NetClientCount+1)/2.0) + 1
+    else
+      Need := Floor(NetClientCount/2.0) + 1;
+    if gVoteCount >= Need then
+    begin
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_PASSED], [gVoteCommand]), True);
+      MH_SEND_VoteEvent(NET_VE_PASSED, gVoteCommand);
+      gVoteInProgress := False;
+      gVotePassed := True;
+      gVoteCmdTimer := gTime + 5000;
+      gVoted := False;
+      gVoteCount := 0;
+      if NetClients <> nil then
+        for I := Low(NetClients) to High(NetClients) do
+          if NetClients[i].Used then
+            NetClients[i].Voted := False;
+    end;
+  end;
+end;
+
+procedure g_Game_LoadMapList(FileName: string);
+var
+  ListFile: TextFile;
+  s: string;
+begin
+  MapList := nil;
+  MapIndex := -1;
+
+  if not FileExists(FileName) then Exit; 
+
+  AssignFile(ListFile, FileName);
+  Reset(ListFile);
+  while not EOF(ListFile) do
+  begin
+    ReadLn(ListFile, s);
+
+    s := Trim(s);
+    if s = '' then Continue;
+
+    SetLength(MapList, Length(MapList)+1);
+    MapList[High(MapList)] := s;
+  end;
+  CloseFile(ListFile);
+end;
+
+procedure g_Game_SetDebugMode();
+begin
+  gDebugMode := True;
+// ×èòû (äàæå â ñâîåé èãðå):
+  gCheats := True;
+end;
+
+procedure g_Game_SetLoadingText(Text: String; Max: Integer; reWrite: Boolean);
+var
+  i: Word;
+begin
+  if Length(LoadingStat.Msgs) = 0 then
+    Exit;
+
+  with LoadingStat do
+  begin
+    if not reWrite then
+    begin // Ïåðåõîäèì íà ñëåäóþùóþ ñòðîêó èëè ñêðîëëèðóåì:
+      if NextMsg = Length(Msgs) then
+        begin // scroll
+          for i := 0 to High(Msgs)-1 do
+            Msgs[i] := Msgs[i+1];
+        end
+      else
+        Inc(NextMsg);
+    end else
+      if NextMsg = 0 then
+        Inc(NextMsg);
+
+    Msgs[NextMsg-1] := Text;
+    CurValue := 0;
+    MaxValue := Max;
+    ShowCount := 0;
+  end;
+
+  g_ActiveWindow := nil;
+
+  ProcessLoading;
+end;
+
+procedure g_Game_StepLoading();
+begin
+  with LoadingStat do
+  begin
+    Inc(CurValue);
+    Inc(ShowCount);
+    if (ShowCount > LOADING_SHOW_STEP) then
+    begin
+      ShowCount := 0;
+      ProcessLoading;
+    end;
+  end;
+end;
+
+procedure g_Game_ClearLoading();
+var
+  len: Word;
+begin
+  with LoadingStat do
+  begin
+    CurValue := 0;
+    MaxValue := 0;
+    ShowCount := 0;
+    len := ((gScreenHeight div 3)*2 - 50) div LOADING_INTERLINE;
+    if len < 1 then len := 1;
+    SetLength(Msgs, len);
+    for len := Low(Msgs) to High(Msgs) do
+      Msgs[len] := '';
+    NextMsg := 0;
+  end;
+end;
+
+procedure Parse_Params(var pars: TParamStrValues);
+var
+  i: Integer;
+  s: String;
+begin
+  SetLength(pars, 0);
+  i := 1;
+  while i <= ParamCount do
+  begin
+    s := ParamStr(i);
+    if (s[1] = '-') and (Length(s) > 1) then
+    begin
+      if (s[2] = '-') and (Length(s) > 2) then
+        begin // Îäèíî÷íûé ïàðàìåòð
+          SetLength(pars, Length(pars) + 1);
+          with pars[High(pars)] do
+          begin
+            Name := LowerCase(s);
+            Value := '+';
+          end;
+        end
+      else
+        if (i < ParamCount) then
+        begin // Ïàðàìåòð ñî çíà÷åíèåì
+          Inc(i);
+          SetLength(pars, Length(pars) + 1);
+          with pars[High(pars)] do
+          begin
+            Name := LowerCase(s);
+            Value := LowerCase(ParamStr(i));
+          end;
+        end;
+    end;
+
+    Inc(i);
+  end;
+end;
+
+function Find_Param_Value(var pars: TParamStrValues; aName: String): String;
+var
+  i: Integer;
+begin
+  Result := '';
+  for i := 0 to High(pars) do
+    if pars[i].Name = aName then
+    begin
+      Result := pars[i].Value;
+      Break;
+    end;
+end;
+
+procedure g_Game_Process_Params();
+var
+  pars: TParamStrValues;
+  map: String;
+  GMode, n: Byte;
+  LimT, LimS: Integer;
+  Opt: LongWord;
+  Lives: Integer;
+  s: String;
+  Port: Integer;
+  ip: String;
+  F: TextFile;
+begin
+  Parse_Params(pars);
+
+// Debug mode:
+  s := Find_Param_Value(pars, '--debug');
+  if (s <> '') then
+    g_Game_SetDebugMode();
+
+// Connect when game loads
+  ip := Find_Param_Value(pars, '-connect');
+
+  if ip <> '' then
+  begin
+    s := Find_Param_Value(pars, '-port');
+    if (s = '') or not TryStrToInt(s, Port) then
+      Port := 25666;
+
+    s := Find_Param_Value(pars, '-pw');
+
+    g_Game_StartClient(ip, Port, s);
+    Exit;
+  end;
+
+// Start map when game loads:
+  map := LowerCase(Find_Param_Value(pars, '-map'));
+  if (map <> '') and (Pos('.wad:\', map) > 0) then
+  begin
+  // Game mode:
+    s := Find_Param_Value(pars, '-gm');
+    GMode := g_Game_TextToMode(s);
+    if GMode = GM_NONE then GMode := GM_DM;
+    if GMode = GM_SINGLE then GMode := GM_COOP;
+
+  // Time limit:
+    s := Find_Param_Value(pars, '-limt');
+    if (s = '') or (not TryStrToInt(s, LimT)) then
+      LimT := 0;
+    if LimT < 0 then
+      LimT := 0;
+
+  // Goal limit:
+    s := Find_Param_Value(pars, '-lims');
+    if (s = '') or (not TryStrToInt(s, LimS)) then
+      LimS := 0;
+    if LimS < 0 then
+      LimS := 0;
+
+  // Lives limit:
+    s := Find_Param_Value(pars, '-lives');
+    if (s = '') or (not TryStrToInt(s, Lives)) then
+      Lives := 0;
+    if Lives < 0 then
+      Lives := 0;
+
+  // Options:
+    s := Find_Param_Value(pars, '-opt');
+    if (s = '') then
+      Opt := GAME_OPTION_ALLOWEXIT or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER
+    else
+      Opt := StrToIntDef(s, 0);
+    if Opt = 0 then
+      Opt := GAME_OPTION_ALLOWEXIT or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER;
+
+  // Close after map:
+    s := Find_Param_Value(pars, '--close');
+    if (s <> '') then
+      gMapOnce := True;
+
+  // Delete test map after play:
+    s := Find_Param_Value(pars, '--testdelete');
+    if (s <> '') then
+      gMapToDelete := MapsDir + map;
+
+  // Delete temporary WAD after play:
+    s := Find_Param_Value(pars, '--tempdelete');
+    if (s <> '') then
+    begin
+      gMapToDelete := MapsDir + map;
+      gTempDelete := True;
+    end;
+
+  // Number of players:
+    s := Find_Param_Value(pars, '-pl');
+    if (s = '') then
+      n := 1
+    else
+      n := StrToIntDef(s, 1);
+
+  // Start:
+    s := Find_Param_Value(pars, '-port');
+    if (s = '') or not TryStrToInt(s, Port) then
+      g_Game_StartCustom(map, GMode, LimT, LimS, Lives, Opt, n)
+    else
+      g_Game_StartServer(map, GMode, LimT, LimS, Lives, Opt, n, 0, Port);
+  end;
+
+// Execute script when game loads:
+  s := Find_Param_Value(pars, '-exec');
+  if s <> '' then
+  begin
+    if Pos(':\', s) = 0 then
+      s := GameDir + '/' + s;
+
+    {$I-}
+    AssignFile(F, s);
+    Reset(F);
+    if IOResult <> 0 then
+    begin
+      e_WriteLog(Format(_lc[I_SIMPLE_ERROR], ['Failed to read file: ' + s]), MSG_WARNING);
+      g_Console_Add(Format(_lc[I_CONSOLE_ERROR_READ], [s]));
+      CloseFile(F);
+      Exit;
+    end;
+    e_WriteLog('Executing script: ' + s, MSG_NOTIFY);
+    g_Console_Add(Format(_lc[I_CONSOLE_EXEC], [s]));
+
+    while not EOF(F) do
+    begin
+      ReadLn(F, s);
+      if IOResult <> 0 then
+      begin
+        e_WriteLog(Format(_lc[I_SIMPLE_ERROR], ['Failed to read file: ' + s]), MSG_WARNING);
+        g_Console_Add(Format(_lc[I_CONSOLE_ERROR_READ], [s]));
+        CloseFile(F);
+        Exit;
+      end;
+      if Pos('#', s) <> 1 then // script comment
+        g_Console_Process(s, True);
+    end;
+
+    CloseFile(F);
+    {$I+}
+  end;
+
+  SetLength(pars, 0);
+end;
+
+end.
diff --git a/src/game/g_gfx.pas b/src/game/g_gfx.pas
new file mode 100644 (file)
index 0000000..82a45e3
--- /dev/null
@@ -0,0 +1,1286 @@
+unit g_gfx;
+
+interface
+
+uses
+  g_textures;
+
+const
+  BLOOD_NORMAL = 0;
+  BLOOD_SPARKS = 1;
+  ONCEANIM_NONE  = 0;
+  ONCEANIM_SMOKE = 1;
+  MARK_FREE     = 0;
+  MARK_WALL     = 1;
+  MARK_WATER    = 2;
+  MARK_ACID     = 4;
+  MARK_LIFTDOWN = 8;
+  MARK_LIFTUP   = 16;
+  MARK_DOOR     = 32;
+  MARK_LIFTLEFT  = 64;
+  MARK_LIFTRIGHT = 128;
+  MARK_BLOCKED  = MARK_WALL + MARK_DOOR;
+  MARK_LIQUID   = MARK_WATER + MARK_ACID;
+  MARK_LIFT     = MARK_LIFTDOWN + MARK_LIFTUP + MARK_LIFTLEFT + MARK_LIFTRIGHT;
+
+procedure g_GFX_Init();
+procedure g_GFX_Free();
+
+procedure g_GFX_Blood(fX, fY: Integer; Count: Word; vx, vy: Integer;
+                      DevX, DevY: Word; CR, CG, CB: Byte; Kind: Byte = BLOOD_NORMAL);
+procedure g_GFX_Spark(fX, fY: Integer; Count: Word; Angle: SmallInt; DevX, DevY: Byte);
+procedure g_GFX_Water(fX, fY: Integer; Count: Word; fVelX, fVelY: Single; DevX, DevY, Color: Byte);
+procedure g_GFX_SimpleWater(fX, fY: Integer; Count: Word; fVelX, fVelY: Single; DefColor, CR, CG, CB: Byte);
+procedure g_GFX_Bubbles(fX, fY: Integer; Count: Word; DevX, DevY: Byte);
+procedure g_GFX_SetMax(Count: Integer);
+function  g_GFX_GetMax(): Integer;
+
+procedure g_GFX_OnceAnim(X, Y: Integer; Anim: TAnimation; AnimType: Byte = 0);
+
+procedure g_Mark(x, y, Width, Height: Integer; t: Byte; st: Boolean);
+
+procedure g_GFX_Update();
+procedure g_GFX_Draw();
+
+var
+  gCollideMap: Array of Array of Byte;
+
+implementation
+
+uses
+  g_map, g_basic, Math, e_graphics, GL, GLExt,
+  g_options, g_console, SysUtils, g_triggers, MAPDEF,
+  g_game, g_language, g_net;
+
+type
+  TParticle = record
+    X, Y:               Integer;
+    VelX, VelY:         Single;
+    AccelX, AccelY:     Single;
+    Red, Green, Blue:   Byte;
+    Alpha:              Byte;
+    Time, LiveTime:     Word;
+    State:              Byte;
+    ParticleType:       Byte;
+    offsetX, offsetY:   ShortInt;
+  end;
+
+  TOnceAnim = record
+    AnimType:   Byte;
+    X, Y:       Integer;
+    Animation:  TAnimation;
+  end;
+
+const
+  PARTICLE_BLOOD   = 0;
+  PARTICLE_SPARK   = 1;
+  PARTICLE_BUBBLES = 2;
+  PARTICLE_WATER   = 3;
+  STATE_FREE   = 0;
+  STATE_NORMAL = 1;
+  STATE_STICK  = 2;
+
+var
+  Particles: Array of TParticle;
+  OnceAnims: Array of TOnceAnim;
+  MaxParticles: Integer;
+  CurrentParticle: Integer;
+
+procedure g_Mark(x, y, Width, Height: Integer; t: Byte; st: Boolean);
+var
+  yy, y2, xx, x2: Integer;
+begin
+  if x < 0 then
+  begin
+    Width := Width + x;
+    x := 0;
+  end;
+
+  if Width < 0 then
+    Exit;
+  if y < 0 then
+  begin
+    Height := Height + y;
+    y := 0;
+  end;
+
+  if Height < 0 then
+    Exit;
+
+  if x > gMapInfo.Width then
+    Exit;
+  if y > gMapInfo.Height then
+    Exit;
+
+  y2 := y + Height - 1;
+  if y2 > gMapInfo.Height then
+    y2 := gMapInfo.Height;
+
+  x2 := x + Width - 1;
+  if x2 > gMapInfo.Width then
+    x2 := gMapInfo.Width;
+
+  if st then
+    begin // Óñòàíîâèòü ïðèçíàê
+      for yy := y to y2 do
+        for xx := x to x2 do
+          gCollideMap[yy][xx] := gCollideMap[yy][xx] or t;
+    end
+  else
+    begin // Óáðàòü ïðèçíàê
+      t := not t;
+      for yy := y to y2 do
+        for xx := x to x2 do
+          gCollideMap[yy][xx] := gCollideMap[yy][xx] and t;
+    end;
+end;
+
+procedure CreateCollideMap();
+var
+  a: Integer;
+begin
+  g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 1/6', 0, False);
+  SetLength(gCollideMap, gMapInfo.Height+1);
+  for a := 0 to High(gCollideMap) do
+    SetLength(gCollideMap[a], gMapInfo.Width+1);
+
+  if gWater <> nil then
+  begin
+    g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 2/6', 0, True);
+    for a := 0 to High(gWater) do
+      with gWater[a] do
+        g_Mark(X, Y, Width, Height, MARK_WATER, True);
+  end;
+
+  if gAcid1 <> nil then
+  begin
+    g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 3/6', 0, True);
+    for a := 0 to High(gAcid1) do
+      with gAcid1[a] do
+        g_Mark(X, Y, Width, Height, MARK_ACID, True);
+  end;
+
+  if gAcid2 <> nil then
+  begin
+    g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 4/6', 0, True);
+    for a := 0 to High(gAcid2) do
+      with gAcid2[a] do
+        g_Mark(X, Y, Width, Height, MARK_ACID, True);
+  end;
+
+  if gLifts <> nil then
+  begin
+    g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 5/6', 0, True);
+    for a := 0 to High(gLifts) do
+      with gLifts[a] do
+      begin
+        g_Mark(X, Y, Width, Height, MARK_LIFT, False);
+
+        if LiftType = 0 then
+          g_Mark(X, Y, Width, Height, MARK_LIFTUP, True)
+        else if LiftType = 1 then
+          g_Mark(X, Y, Width, Height, MARK_LIFTDOWN, True)
+        else if LiftType = 2 then
+          g_Mark(X, Y, Width, Height, MARK_LIFTLEFT, True)
+        else if LiftType = 3 then
+          g_Mark(X, Y, Width, Height, MARK_LIFTRIGHT, True)
+      end;
+  end;
+
+  if gWalls <> nil then
+  begin
+    g_Game_SetLoadingText(_lc[I_LOAD_COLLIDE_MAP]+' 6/6', 0, True);
+    for a := 0 to High(gWalls) do
+    begin
+      if gWalls[a].Door then
+        begin
+        // Çàêðûòàÿ äâåðü:
+          if gWalls[a].Enabled then
+            with gWalls[a] do
+              g_Mark(X, Y, Width, Height, MARK_DOOR, True)
+          else // Îòêðûòàÿ äâåðü:
+            if gWalls[a].Enabled then
+              with gWalls[a] do
+                g_Mark(X, Y, Width, Height, MARK_DOOR, False);
+        end
+      else // Ñòåíà
+        with gWalls[a] do
+          g_Mark(X, Y, Width, Height, MARK_WALL, True);
+    end;
+  end;
+end;
+
+procedure g_GFX_Init();
+begin
+  CreateCollideMap();
+end;
+
+procedure g_GFX_Free();
+var
+  a: Integer;
+begin
+  Particles := nil;
+  SetLength(Particles, MaxParticles);
+  CurrentParticle := 0;
+
+  if OnceAnims <> nil then
+  begin
+    for a := 0 to High(OnceAnims) do
+      OnceAnims[a].Animation.Free();
+
+    OnceAnims := nil;
+  end;
+
+  gCollideMap := nil;
+end;
+
+procedure CorrectOffsets(id: Integer);
+begin
+  with Particles[id] do
+  begin
+    if (X >= 0) and (Y > 0) and
+    (Y < Length(gCollideMap)) and (X < Length(gCollideMap[0])) and
+    (ByteBool(gCollideMap[Y-1, X] and MARK_BLOCKED)) then
+      offsetY := 1 // Ñòåíà ñâåðõó
+    else
+      offsetY := 0;
+
+    if (X > 0) and (Y >= 0) and
+    (Y < Length(gCollideMap)) and (X < Length(gCollideMap[0])) and
+    (ByteBool(gCollideMap[Y, X-1] and MARK_BLOCKED)) then
+      offsetX := 1 // Ñòåíà ñëåâà
+    else
+      offsetX := 0;
+  end;
+end;
+
+procedure g_GFX_SparkVel(fX, fY: Integer; Count: Word; VX, VY: Integer; DevX, DevY: Byte);
+var
+  a: Integer;
+  DevX1, DevX2,
+  DevY1, DevY2: Byte;
+  l: Integer;
+begin
+  l := Length(Particles);
+  if l = 0 then
+    Exit;
+  if Count > l then
+    Count := l;
+
+  DevX1 := DevX div 2;
+  DevX2 := DevX + 1;
+  DevY1 := DevY div 2;
+  DevY2 := DevY + 1;
+
+  for a := 1 to Count do
+  begin
+    with Particles[CurrentParticle] do
+    begin
+      X := fX-DevX1+Random(DevX2);
+      Y := fY-DevY1+Random(DevY2);
+
+      VelX := VX + (Random-Random)*3;
+      VelY := VY + (Random-Random)*3;
+
+      if VelY > -4 then
+        if VelY-4 < -4 then
+          VelY := -4
+        else
+          VelY := VelY-4;
+
+      AccelX := -Sign(VelX)*Random/100;
+      AccelY := 0.8;
+
+      Red := 255;
+      Green := 100+Random(155);
+      Blue := 64;
+      Alpha := 255;
+
+      State := STATE_NORMAL;
+      Time := 0;
+      LiveTime := 30+Random(60);
+      ParticleType := PARTICLE_SPARK;
+
+      CorrectOffsets(CurrentParticle);
+    end;
+
+    if CurrentParticle+2 > MaxParticles then
+      CurrentParticle := 0
+    else
+      CurrentParticle := CurrentParticle+1;
+  end;
+end;
+
+procedure g_GFX_Blood(fX, fY: Integer; Count: Word; vx, vy: Integer;
+  DevX, DevY: Word; CR, CG, CB: Byte; Kind: Byte = BLOOD_NORMAL);
+var
+  a: Integer;
+  DevX1, DevX2,
+  DevY1, DevY2: Word;
+  l: Integer;
+  CRnd: Byte;
+  CC: SmallInt;
+begin
+  if Kind = BLOOD_SPARKS then
+  begin
+    g_GFX_SparkVel(fX, fY, 2 + Random(2), -VX div 2, -VY div 2, DevX, DevY);
+    Exit;
+  end;
+  l := Length(Particles);
+  if l = 0 then
+    Exit;
+  if Count > l then
+    Count := l;
+
+  DevX1 := DevX div 2;
+  DevX2 := DevX + 1;
+  DevY1 := DevY div 2;
+  DevY2 := DevY + 1;
+
+  for a := 1 to Count do
+  begin
+    with Particles[CurrentParticle] do
+    begin
+      X := fX - DevX1 + Random(DevX2);
+      Y := fY - DevY1 + Random(DevY2);
+
+      if (X < 0) or (X > gMapInfo.Width-1) or
+         (Y < 0) or (Y > gMapInfo.Height-1) or
+         ByteBool(gCollideMap[Y, X] and MARK_WALL) then
+        Continue;
+
+      VelX := vx + (Random-Random)*3;
+      VelY := vy + (Random-Random)*3;
+
+      if VelY > -4 then
+        if VelY-4 < -4 then
+          VelY := -4
+        else
+          VelY := VelY-4;
+
+      AccelX := -Sign(VelX)*Random/100;
+      AccelY := 0.8;
+
+      CRnd := 20*Random(6);
+      if CR > 0 then
+      begin
+        CC := CR + CRnd - 50;
+        if CC < 0   then CC := 0;
+        if CC > 255 then CC := 255;
+        Red := CC;
+      end else
+        Red := 0;
+      if CG > 0 then
+      begin
+        CC := CG + CRnd - 50;
+        if CC < 0   then CC := 0;
+        if CC > 255 then CC := 255;
+        Green := CC;
+      end else
+        Green := 0;
+      if CB > 0 then
+      begin
+        CC := CB + CRnd - 50;
+        if CC < 0   then CC := 0;
+        if CC > 255 then CC := 255;
+        Blue := CC;
+      end else
+        Blue := 0;
+
+      Alpha := 255;
+
+      State := STATE_NORMAL;
+      Time := 0;
+      LiveTime := 120+Random(40);
+      ParticleType := PARTICLE_BLOOD;
+
+      CorrectOffsets(CurrentParticle);
+    end;
+
+    if CurrentParticle >= MaxParticles-1 then
+      CurrentParticle := 0
+    else
+      CurrentParticle := CurrentParticle+1;
+  end;
+end;
+
+procedure g_GFX_Spark(fX, fY: Integer; Count: Word; Angle: SmallInt; DevX, DevY: Byte);
+var
+  a: Integer;
+  b: Single;
+  DevX1, DevX2,
+  DevY1, DevY2: Byte;
+  BaseVelX, BaseVelY: Single;
+  l: Integer;
+begin
+  l := Length(Particles);
+  if l = 0 then
+    Exit;
+  if Count > l then
+    Count := l;
+
+  Angle := 360 - Angle;
+
+  DevX1 := DevX div 2;
+  DevX2 := DevX + 1;
+  DevY1 := DevY div 2;
+  DevY2 := DevY + 1;
+
+  b := DegToRad(Angle);
+  BaseVelX := cos(b);
+  BaseVelY := 1.6*sin(b);
+  if Abs(BaseVelX) < 0.01 then
+    BaseVelX := 0.0;
+  if Abs(BaseVelY) < 0.01 then
+    BaseVelY := 0.0;
+  for a := 1 to Count do
+  begin
+    with Particles[CurrentParticle] do
+    begin
+      X := fX-DevX1+Random(DevX2);
+      Y := fY-DevY1+Random(DevY2);
+
+      VelX := BaseVelX*Random;
+      VelY := BaseVelY-Random;
+      AccelX := VelX/3.0;
+      AccelY := VelY/5.0;
+
+      Red := 255;
+      Green := 100+Random(155);
+      Blue := 64;
+      Alpha := 255;
+
+      State := STATE_NORMAL;
+      Time := 0;
+      LiveTime := 30+Random(60);
+      ParticleType := PARTICLE_SPARK;
+
+      CorrectOffsets(CurrentParticle);
+    end;
+
+    if CurrentParticle+2 > MaxParticles then
+      CurrentParticle := 0
+    else
+      CurrentParticle := CurrentParticle+1;
+  end;
+end;
+
+procedure g_GFX_Water(fX, fY: Integer; Count: Word; fVelX, fVelY: Single; DevX, DevY, Color: Byte);
+var
+  a: Integer;
+  DevX1, DevX2,
+  DevY1, DevY2: Byte;
+  l: Integer;
+begin
+  l := Length(Particles);
+  if l = 0 then
+    Exit;
+  if Count > l then
+    Count := l;
+
+  if Abs(fVelX) < 3.0 then
+    fVelX := 3.0 - 6.0*Random;
+
+  DevX1 := DevX div 2;
+  DevX2 := DevX + 1;
+  DevY1 := DevY div 2;
+  DevY2 := DevY + 1;
+
+  for a := 1 to Count do
+  begin
+    with Particles[CurrentParticle] do
+    begin
+      X := fX-DevX1+Random(DevX2);
+      Y := fY-DevY1+Random(DevY2);
+
+      if Abs(fVelX) < 0.5 then
+        VelX := 1.0 - 2.0*Random
+      else
+        VelX := fVelX*Random;
+      if Random(10) < 7 then
+        VelX := -VelX;
+      VelY := fVelY*Random;
+      AccelX := 0.0;
+      AccelY := 0.8;
+
+      case Color of
+        1: // Êðàñíûé
+          begin
+            Red := 155 + Random(9)*10;
+            Green := Trunc(150*Random);
+            Blue := Green;
+          end;
+        2: // Çåëåíûé
+          begin
+            Red := Trunc(150*Random);
+            Green := 175 + Random(9)*10;
+            Blue := Red;
+          end;
+        3: // Ñèíèé
+          begin
+            Red := Trunc(200*Random);
+            Green := Red;
+            Blue := 175 + Random(9)*10;
+          end;
+        else // Ñåðûé
+          begin
+            Red := 90 + Random(12)*10;
+            Green := Red;
+            Blue := Red;
+          end;
+      end;
+
+      Alpha := 255;
+
+      State := STATE_NORMAL;
+      Time := 0;
+      LiveTime := 60+Random(60);
+      ParticleType := PARTICLE_WATER;
+
+      CorrectOffsets(CurrentParticle);
+    end;
+
+    if CurrentParticle+2 > MaxParticles then
+      CurrentParticle := 0
+    else
+      CurrentParticle := CurrentParticle+1;
+  end;
+end;
+
+procedure g_GFX_SimpleWater(fX, fY: Integer; Count: Word; fVelX, fVelY: Single; DefColor, CR, CG, CB: Byte);
+var
+  a: Integer;
+  l: Integer;
+begin
+  l := Length(Particles);
+  if l = 0 then
+    Exit;
+  if Count > l then
+    Count := l;
+
+  for a := 1 to Count do
+  begin
+    with Particles[CurrentParticle] do
+    begin
+      X := fX;
+      Y := fY;
+
+      VelX := fVelX;
+      VelY := fVelY;
+      AccelX := 0.0;
+      AccelY := 0.8;
+
+      case DefColor of
+        1: // Êðàñíûé
+          begin
+            Red := 155 + Random(9)*10;
+            Green := Trunc(150*Random);
+            Blue := Green;
+          end;
+        2: // Çåëåíûé
+          begin
+            Red := Trunc(150*Random);
+            Green := 175 + Random(9)*10;
+            Blue := Red;
+          end;
+        3: // Ñèíèé
+          begin
+            Red := Trunc(200*Random);
+            Green := Red;
+            Blue := 175 + Random(9)*10;
+          end;
+        4: // Ñâîé öâåò, ñâåòëåå
+          begin
+            Red := 20 + Random(19)*10;
+            Green := Red;
+            Blue := Red;
+            Red := Min(Red + CR, 255);
+            Green := Min(Green + CG, 255);
+            Blue := Min(Blue + CB, 255);
+          end;
+        5: // Ñâîé öâåò, òåìíåå
+          begin
+            Red := 20 + Random(19)*10;
+            Green := Red;
+            Blue := Red;
+            Red := Max(CR - Red, 0);
+            Green := Max(CG - Green, 0);
+            Blue := Max(CB - Blue, 0);
+          end;
+        else // Ñåðûé
+          begin
+            Red := 90 + Random(12)*10;
+            Green := Red;
+            Blue := Red;
+          end;
+      end;
+
+      Alpha := 255;
+
+      State := STATE_NORMAL;
+      Time := 0;
+      LiveTime := 60+Random(60);
+      ParticleType := PARTICLE_WATER;
+
+      CorrectOffsets(CurrentParticle);
+    end;
+
+    if CurrentParticle+2 > MaxParticles then
+      CurrentParticle := 0
+    else
+      CurrentParticle := CurrentParticle+1;
+  end;
+end;
+
+procedure g_GFX_Bubbles(fX, fY: Integer; Count: Word; DevX, DevY: Byte);
+var
+  a: Integer;
+  DevX1, DevX2,
+  DevY1, DevY2: Byte;
+  l: Integer;
+begin
+  l := Length(Particles);
+  if l = 0 then
+    Exit;
+  if Count > l then
+    Count := l;
+
+  DevX1 := DevX div 2;
+  DevX2 := DevX + 1;
+  DevY1 := DevY div 2;
+  DevY2 := DevY + 1;
+
+  for a := 1 to Count do
+  begin
+    with Particles[CurrentParticle] do
+    begin
+      X := fX-DevX1+Random(DevX2);
+      Y := fY-DevY1+Random(DevY2);
+
+      if (X >= gMapInfo.Width) or (X <= 0) or
+         (Y >= gMapInfo.Height) or (Y <= 0) then
+        Continue;
+
+      if not ByteBool(gCollideMap[Y, X] and MARK_LIQUID) then
+        Continue;
+
+      VelX := 0;
+      VelY := -1-Random;
+      AccelX := 0;
+      AccelY := VelY/10;
+
+      Red := 255;
+      Green := 255;
+      Blue := 255;
+      Alpha := 255;
+      
+      State := STATE_NORMAL;
+      Time := 0;
+      LiveTime := 65535;
+      ParticleType := PARTICLE_BUBBLES;
+      
+      CorrectOffsets(CurrentParticle);
+    end;
+
+    if CurrentParticle+2 > MaxParticles then
+      CurrentParticle := 0
+    else
+      CurrentParticle := CurrentParticle+1;
+  end;
+end;
+
+procedure g_GFX_SetMax(Count: Integer);
+begin
+  if Count > 50000 then
+    Count := 50000;
+
+  SetLength(Particles, Count);
+  MaxParticles := Count;
+  if CurrentParticle >= Count then
+    CurrentParticle := 0;
+end;
+
+function g_GFX_GetMax(): Integer;
+begin
+  Result := MaxParticles;
+end;
+
+function FindOnceAnim: DWORD;
+var
+  i: Integer;
+begin
+  if OnceAnims <> nil then
+    for i := 0 to High(OnceAnims) do
+      if OnceAnims[i].Animation = nil then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if OnceAnims = nil then
+    begin
+      SetLength(OnceAnims, 16);
+      Result := 0;
+    end
+  else
+    begin
+      Result := High(OnceAnims) + 1;
+      SetLength(OnceAnims, Length(OnceAnims) + 16);
+    end;
+end;
+
+procedure g_GFX_OnceAnim(X, Y: Integer; Anim: TAnimation; AnimType: Byte = 0);
+var
+  find_id: DWORD;
+begin
+  if Anim = nil then
+    Exit;
+
+  find_id := FindOnceAnim();
+
+  OnceAnims[find_id].AnimType := AnimType;
+  OnceAnims[find_id].Animation := TAnimation.Create(Anim.FramesID, Anim.Loop, Anim.Speed);
+  OnceAnims[find_id].Animation.Blending := Anim.Blending;
+  OnceAnims[find_id].Animation.Alpha := Anim.Alpha;
+  OnceAnims[find_id].X := X;
+  OnceAnims[find_id].Y := Y;
+end;
+
+procedure g_GFX_Update();
+var
+  a: Integer;
+  w, h: Integer;
+  dX, dY: SmallInt;
+  b, len: Integer;
+  s: ShortInt;
+  c: Byte;
+begin
+  if Particles <> nil then
+  begin
+    w := gMapInfo.Width;
+    h := gMapInfo.Height;
+
+    len := High(Particles);
+
+    for a := 0 to len do
+      if Particles[a].State <> 0 then
+        with Particles[a] do
+        begin
+          if Time = LiveTime then
+            State := STATE_FREE;
+          if (X+1 >= w) or (Y+1 >= h) or (X <= 0) or (Y <= 0) then
+            State := STATE_FREE;
+          if State = STATE_FREE then
+            Continue;
+
+          case ParticleType of
+            PARTICLE_BLOOD:
+            begin
+              if gAdvBlood then
+                begin
+                  if (State = STATE_STICK) then
+                    if (not ByteBool(gCollideMap[Y-1, X] and MARK_BLOCKED)) and
+                       (not ByteBool(gCollideMap[Y+1, X] and MARK_BLOCKED)) and
+                       (not ByteBool(gCollideMap[Y, X-1] and MARK_BLOCKED)) and
+                       (not ByteBool(gCollideMap[Y, X+1] and MARK_BLOCKED)) then
+                      begin // Îòëèïëà - êàïàåò
+                        VelY := 0.5;
+                        AccelY := 0.15;
+                        State := STATE_NORMAL;
+                      end
+                    else
+                      if Random(200) = 100 then
+                      begin // Ïðèëåïëåíà - íî âîçìîæíî ñòåêàåò
+                        VelY := 0.5;
+                        AccelY := 0.15;
+                        Continue;
+                      end;
+
+                  if not ByteBool(gCollideMap[Y, X] and MARK_BLOCKED) then
+                  begin
+                    if ByteBool(gCollideMap[Y, X] and MARK_LIFTUP) then
+                    begin // Ëèôò ââåðõ
+                      if VelY > -4-Random(3) then
+                        VelY := VelY - 0.8;
+                      if Abs(VelX) > 0.1 then
+                        VelX := VelX - VelX/10.0;
+                      VelX := VelX + (Random-Random)*0.2;
+                      AccelY := 0.15;
+                    end;
+                    if ByteBool(gCollideMap[Y, X] and MARK_LIFTLEFT) then
+                    begin // Ïîòîê âëåâî
+                      if VelX > -8-Random(3) then
+                        VelX := VelX - 0.8;
+                      AccelY := 0.15;
+                    end;
+                    if ByteBool(gCollideMap[Y, X] and MARK_LIFTRIGHT) then
+                    begin // Ïîòîê âïðàâî
+                      if VelX < 8+Random(3) then
+                        VelX := VelX + 0.8;
+                      AccelY := 0.15;
+                    end;
+                  end;
+
+                  dX := Round(VelX);
+                  dY := Round(VelY);
+
+                  if (Abs(VelX) < 0.1) and (Abs(VelY) < 0.1) then
+                    if (State <> STATE_STICK) and
+                       (not ByteBool(gCollideMap[Y-1, X] and MARK_BLOCKED)) and
+                       (not ByteBool(gCollideMap[Y, X] and MARK_BLOCKED)) and
+                       (not ByteBool(gCollideMap[Y+1, X] and MARK_BLOCKED)) then
+                    begin // Âèñèò â âîçäóõå - êàïàåò
+                      VelY := 0.8;
+                      AccelY := 0.5;
+                      State := STATE_NORMAL;
+                    end;
+
+                  if dX <> 0 then
+                  begin
+                    if dX > 0 then
+                      s := 1
+                    else
+                      s := -1;
+
+                    dX := Abs(dX);
+
+                    for b := 1 to dX do
+                    begin
+                      if (X+s >= w) or (X+s <= 0) then
+                      begin
+                        State := STATE_FREE;
+                        Break;
+                      end;
+
+                      c := gCollideMap[Y, X+s];
+
+                      if ByteBool(c and MARK_BLOCKED) then
+                      begin // Ñòåíà/äâåðü
+                        VelX := 0;
+                        VelY := 0;
+                        AccelX := 0;
+                        AccelY := 0;
+                        State := STATE_STICK;
+                        Break;
+                      end;
+
+                      X := X+s;
+                    end;
+                  end;
+
+                  if dY <> 0 then
+                  begin
+                    if dY > 0 then
+                      s := 1
+                    else
+                      s := -1;
+
+                    dY := Abs(dY);
+
+                    for b := 1 to dY do
+                    begin
+                      if (Y+s >= h) or (Y+s <= 0) then
+                      begin
+                        State := STATE_FREE;
+                        Break;
+                      end;
+
+                      c := gCollideMap[Y+s, X];
+
+                      if ByteBool(c and MARK_BLOCKED) then
+                      begin // Ñòåíà/äâåðü
+                        VelX := 0;
+                        VelY := 0;
+                        AccelX := 0;
+                        AccelY := 0;
+                        if (s > 0) and (State <> STATE_STICK) then
+                          State := STATE_NORMAL
+                        else
+                          State := STATE_STICK;
+                        Break;
+                      end;
+
+                      Y := Y+s;
+                    end;
+                  end;
+                end // if gAdvBlood
+              else
+                begin
+                  dX := Round(VelX);
+                  dY := Round(VelY);
+
+                  if (X+dX >= w) or (Y+dY >= h) or
+                     (X+dX <= 0) or (Y+dY <= 0) or
+                     ByteBool(gCollideMap[Y+dY, X+dX] and MARK_BLOCKED) then
+                    begin // Ñòåíà/äâåðü/ãðàíèöà
+                      State := STATE_FREE;
+                      VelX := 0;
+                      VelY := 0;
+                    end
+                  else
+                    begin
+                      Y := Y + dY;
+                      X := X + dX;
+                    end;
+                end;
+
+              VelX := VelX + AccelX;
+              VelY := VelY + AccelY;
+
+            // Êðîâü ðàñòâîðÿåòñÿ â æèäêîñòè:
+              if ByteBool(gCollideMap[Y, X] and MARK_LIQUID) then
+              begin
+                Inc(Time);
+
+                Alpha := 255 - Trunc((255.0 * Time) / LiveTime);
+              end;
+            end;
+
+            PARTICLE_SPARK:
+            begin
+              dX := Round(VelX);
+              dY := Round(VelY);
+
+              if (Abs(VelX) < 0.1) and (Abs(VelY) < 0.1) and
+                 (not ByteBool(gCollideMap[Y-1, X] and MARK_BLOCKED)) and
+                 (not ByteBool(gCollideMap[Y, X] and MARK_BLOCKED)) and
+                 (not ByteBool(gCollideMap[Y+1, X] and MARK_BLOCKED)) then
+              begin // Âèñèò â âîçäóõå
+                VelY := 0.8;
+                AccelY := 0.5;
+              end;
+
+              if dX <> 0 then
+              begin
+                if dX > 0 then
+                  s := 1
+                else
+                  s := -1;
+
+                dX := Abs(dX);
+
+                for b := 1 to dX do
+                begin
+                  if (X+s >= w) or (X+s <= 0) then
+                  begin
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  c := gCollideMap[Y, X+s];
+
+                  if ByteBool(c and MARK_BLOCKED) then
+                    begin // Ñòåíà/äâåðü - ïàäàåò âåðòèêàëüíî
+                      VelX := 0;
+                      AccelX := 0;
+                      Break;
+                    end
+                  else // Ïóñòî:
+                    if c = MARK_FREE then
+                      X := X + s
+                    else // Îñòàëüíîå:
+                      begin
+                        State := STATE_FREE;
+                        Break;
+                      end;
+                end;
+              end;
+
+              if dY <> 0 then
+              begin
+                if dY > 0 then
+                  s := 1
+                else
+                  s := -1;
+
+                dY := Abs(dY);
+
+                for b := 1 to dY do
+                begin
+                  if (Y+s >= h) or (Y+s <= 0) then
+                  begin
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  c := gCollideMap[Y+s, X];
+
+                  if ByteBool(c and MARK_BLOCKED) then
+                    begin // Ñòåíà/äâåðü - ïàäàåò âåðòèêàëüíî
+                      if s < 0 then
+                        begin
+                          VelY := -VelY;
+                          AccelY := Abs(AccelY);
+                        end
+                      else // Èëè íå ïàäàåò
+                        begin
+                          VelX := 0;
+                          AccelX := 0;
+                          VelY := 0;
+                          AccelY := 0.8;
+                        end;
+
+                      Break;
+                    end
+                  else // Ïóñòî:
+                    if c = MARK_FREE then
+                      Y := Y + s
+                    else // Îñàëüíîå:
+                      begin
+                        State := STATE_FREE;
+                        Break;
+                      end;
+                end;
+              end;
+
+              if VelX <> 0.0 then
+                VelX := VelX + AccelX;
+              if VelY <> 0.0 then
+              begin
+                if AccelY < 10 then
+                  AccelY := AccelY + 0.08;
+                VelY := VelY + AccelY;
+              end;
+
+              Time := Time + 1;
+            end;
+
+            PARTICLE_WATER:
+            begin
+              if (State = STATE_STICK) and (Random(30) = 15) then
+              begin // Ñòåêàåò/îòëèïàåò
+                VelY := 0.5;
+                AccelY := 0.15;
+                if (not ByteBool(gCollideMap[Y, X-1] and MARK_BLOCKED)) and
+                   (not ByteBool(gCollideMap[Y, X+1] and MARK_BLOCKED)) then
+                  State := STATE_NORMAL;
+                Continue;
+              end;
+
+              if not ByteBool(gCollideMap[Y, X] and MARK_BLOCKED) then
+              begin
+                if ByteBool(gCollideMap[Y, X] and MARK_LIFTUP) then
+                begin // Ëèôò ââåðõ
+                  if VelY > -4-Random(3) then
+                    VelY := VelY - 0.8;
+                  if Abs(VelX) > 0.1 then
+                    VelX := VelX - VelX/10.0;
+                  VelX := VelX + (Random-Random)*0.2;
+                  AccelY := 0.15;
+                end;
+                if ByteBool(gCollideMap[Y, X] and MARK_LIFTLEFT) then
+                begin // Ïîòîê âëåâî
+                  if VelX > -8-Random(3) then
+                    VelX := VelX - 0.8;
+                  AccelY := 0.15;
+                end;
+                if ByteBool(gCollideMap[Y, X] and MARK_LIFTRIGHT) then
+                begin // Ïîòîê âïðàâî
+                  if VelX < 8+Random(3) then
+                    VelX := VelX + 0.8;
+                  AccelY := 0.15;
+                end;
+              end;
+
+              dX := Round(VelX);
+              dY := Round(VelY);
+
+              if (Abs(VelX) < 0.1) and (Abs(VelY) < 0.1) then
+                if (State <> STATE_STICK) and
+                   (not ByteBool(gCollideMap[Y-1, X] and MARK_BLOCKED)) and
+                   (not ByteBool(gCollideMap[Y, X] and MARK_BLOCKED)) and
+                   (not ByteBool(gCollideMap[Y+1, X] and MARK_BLOCKED)) then
+                begin // Âèñèò â âîçäóõå - êàïàåò
+                  VelY := 0.8;
+                  AccelY := 0.5;
+                  State := STATE_NORMAL;
+                end;
+
+              if dX <> 0 then
+              begin
+                if dX > 0 then
+                  s := 1
+                else
+                  s := -1;
+
+                for b := 1 to Abs(dX) do
+                begin
+                  if (X+s >= w) or (X+s <= 0) then
+                  begin // Ñáîêó ãðàíèöà
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  c := gCollideMap[Y, X+s];
+
+                  if ByteBool(c and MARK_LIQUID) and (dY > 0) then
+                  begin // Ñáîêó æèäêîñòü, à ÷àñòèöà óæå ïàäàåò
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  if ByteBool(c and MARK_BLOCKED) then
+                  begin // Ñòåíà/äâåðü
+                    VelX := 0;
+                    VelY := 0;
+                    AccelX := 0;
+                    AccelY := 0;
+                    State := STATE_STICK;
+                    Break;
+                  end;
+
+                  X := X+s;
+                end;
+              end;
+
+              if dY <> 0 then
+              begin
+                if dY > 0 then
+                  s := 1
+                else
+                  s := -1;
+
+                for b := 1 to Abs(dY) do
+                begin
+                  if (Y+s >= h) or (Y+s <= 0) then
+                  begin // Ñíèçó/ñâåðõó ãðàíèöà
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  c := gCollideMap[Y+s, X];
+
+                  if ByteBool(c and MARK_LIQUID) and (dY > 0) then
+                  begin // Ñíèçó æèäêîñòü, à ÷àñòèöà óæå ïàäàåò
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  if ByteBool(c and MARK_BLOCKED) then
+                  begin // Ñòåíà/äâåðü
+                    VelX := 0;
+                    VelY := 0;
+                    AccelX := 0;
+                    AccelY := 0;
+                    if (s > 0) and (State <> STATE_STICK) then
+                      State := STATE_NORMAL
+                    else
+                      State := STATE_STICK;
+                    Break;
+                  end;
+
+                  Y := Y+s;
+                end;
+              end;
+
+              VelX := VelX + AccelX;
+              VelY := VelY + AccelY;
+
+              Time := Time + 1;
+            end;
+
+            PARTICLE_BUBBLES:
+            begin
+              dY := Round(VelY);
+
+              if dY <> 0 then
+              begin
+                if dY > 0 then
+                  s := 1
+                else
+                  s := -1;
+
+                for b := 1 to Abs(dY) do
+                begin
+                  if (Y+s >= h) or (Y+s <= 0) then
+                  begin
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  if not ByteBool(gCollideMap[Y+s, X] and MARK_LIQUID) then
+                  begin // Óæå íå æèäêîñòü
+                    State := STATE_FREE;
+                    Break;
+                  end;
+
+                  Y := Y+s;
+                end;
+              end;
+
+              if VelY > -4 then
+                VelY := VelY + AccelY;
+
+              Time := Time + 1;
+            end;
+          end; // case
+
+          CorrectOffsets(a);
+        end;
+  end; // Particles <> nil
+
+  if OnceAnims <> nil then
+  begin
+    for a := 0 to High(OnceAnims) do
+      if OnceAnims[a].Animation <> nil then
+      begin
+        case OnceAnims[a].AnimType of
+          ONCEANIM_SMOKE:
+            begin
+              if Random(3) = 0 then
+                OnceAnims[a].X := OnceAnims[a].X-1+Random(3);
+              if Random(2) = 0 then
+                OnceAnims[a].Y := OnceAnims[a].Y-Random(2);
+            end;
+        end;
+
+        if OnceAnims[a].Animation.Played then
+          begin
+            OnceAnims[a].Animation.Free();
+            OnceAnims[a].Animation := nil;
+          end
+        else
+          OnceAnims[a].Animation.Update();
+      end;
+  end;
+end;
+
+procedure g_GFX_Draw();
+var
+  a, len: Integer;
+begin
+  if Particles <> nil then
+  begin
+    glDisable(GL_TEXTURE_2D);
+    glPointSize(2);
+    
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+    glBegin(GL_POINTS);
+
+    len := High(Particles);
+
+    for a := 0 to len do
+      with Particles[a] do
+        if (State <> STATE_FREE) and (X >= sX) and (Y >= sY) and
+           (X <= sX+sWidth) and (sY <= sY+sHeight) then
+        begin
+          glColor4ub(Red, Green, Blue, Alpha);
+          glVertex2i(X + offsetX, Y + offsetY);
+        end;
+
+    glEnd();
+
+    glDisable(GL_BLEND);
+  end;
+
+  if OnceAnims <> nil then
+    for a := 0 to High(OnceAnims) do
+      if OnceAnims[a].Animation <> nil then
+        with OnceAnims[a] do
+          Animation.Draw(X, Y, M_NONE);
+end;
+
+end.
diff --git a/src/game/g_gui.pas b/src/game/g_gui.pas
new file mode 100644 (file)
index 0000000..34436a6
--- /dev/null
@@ -0,0 +1,3061 @@
+unit g_gui;
+
+interface
+
+uses
+  e_graphics, e_input, g_playermodel, g_basic, MAPSTRUCT, WADEDITOR;
+
+const
+  MAINMENU_HEADER_COLOR: TRGB = (R:255; G:255; B:255);
+  MAINMENU_ITEMS_COLOR: TRGB = (R:255; G:255; B:255);
+  MAINMENU_UNACTIVEITEMS_COLOR: TRGB = (R:192; G:192; B:192);
+  MAINMENU_CLICKSOUND = 'MENU_SELECT';
+  MAINMENU_CHANGESOUND = 'MENU_CHANGE';
+  MAINMENU_SPACE = 4;
+  MAINMENU_MARKER1 = 'MAINMENU_MARKER1';
+  MAINMENU_MARKER2 = 'MAINMENU_MARKER2';
+  MAINMENU_MARKERDELAY = 24;
+  WINDOW_CLOSESOUND = 'MENU_CLOSE';
+  MENU_HEADERCOLOR: TRGB = (R:255; G:255; B:255);
+  MENU_ITEMSTEXT_COLOR: TRGB = (R:255; G:255; B:255);
+  MENU_UNACTIVEITEMS_COLOR: TRGB = (R:128; G:128; B:128);
+  MENU_ITEMSCTRL_COLOR: TRGB = (R:255; G:0; B:0);
+  MENU_VSPACE = 2;
+  MENU_HSPACE = 32;
+  MENU_CLICKSOUND = 'MENU_SELECT';
+  MENU_CHANGESOUND = 'MENU_CHANGE';
+  MENU_MARKERDELAY = 24;
+  SCROLL_LEFT = 'SCROLL_LEFT';
+  SCROLL_RIGHT = 'SCROLL_RIGHT';
+  SCROLL_MIDDLE = 'SCROLL_MIDDLE';
+  SCROLL_MARKER = 'SCROLL_MARKER';
+  SCROLL_ADDSOUND = 'SCROLL_ADD';
+  SCROLL_SUBSOUND = 'SCROLL_SUB';
+  EDIT_LEFT = 'EDIT_LEFT';
+  EDIT_RIGHT = 'EDIT_RIGHT';
+  EDIT_MIDDLE = 'EDIT_MIDDLE';
+  EDIT_CURSORCOLOR: TRGB = (R:200; G:0; B:0);
+  EDIT_CURSORLEN = 10;
+  KEYREAD_QUERY = '<...>';
+  KEYREAD_CLEAR = '???';
+  KEYREAD_TIMEOUT = 24;
+  MAPPREVIEW_WIDTH = 8;
+  MAPPREVIEW_HEIGHT = 8;
+  BOX1 = 'BOX1';
+  BOX2 = 'BOX2';
+  BOX3 = 'BOX3';
+  BOX4 = 'BOX4';
+  BOX5 = 'BOX5';
+  BOX6 = 'BOX6';
+  BOX7 = 'BOX7';
+  BOX8 = 'BOX8';
+  BOX9 = 'BOX9';
+  BSCROLL_UPA = 'BSCROLL_UP_A';
+  BSCROLL_UPU = 'BSCROLL_UP_U';
+  BSCROLL_DOWNA = 'BSCROLL_DOWN_A';
+  BSCROLL_DOWNU = 'BSCROLL_DOWN_U';
+  BSCROLL_MIDDLE = 'BSCROLL_MIDDLE';
+  WM_KEYDOWN = 101;
+  WM_CHAR    = 102;
+  WM_USER    = 110;
+
+type
+  TMessage = record
+    Msg: DWORD;
+    wParam: LongInt;
+    lParam: LongInt;
+  end;
+  TFontType = (FONT_TEXTURE, FONT_CHAR);
+
+  TFont = class(TObject)
+  private
+    ID: DWORD;
+    FScale: Single;
+    FFontType: TFontType;
+  public
+    constructor Create(FontID: DWORD; FontType: TFontType);
+    destructor Destroy; override;
+    procedure Draw(X, Y: Integer; Text: string; R, G, B: Byte);
+    procedure GetTextSize(Text: string; var w, h: Word);
+    property Scale: Single read FScale write FScale;
+  end;
+
+  TGUIControl = class;
+
+  TOnKeyDownEvent = procedure(Key: Byte);
+  TOnCloseEvent = procedure;
+  TOnShowEvent = procedure;
+  TOnClickEvent = procedure;
+  TOnChangeEvent = procedure(Sender: TGUIControl);
+  TOnEnterEvent = procedure(Sender: TGUIControl);
+
+  TGUIWindow = class;
+
+  TGUIControl = class
+  private
+    FX, FY: Integer;
+    FEnabled: Boolean;
+    FWindow : TGUIWindow;
+    FName: string;
+  public
+    constructor Create;
+    procedure OnMessage(var Msg: TMessage); virtual;
+    procedure Update; virtual;
+    procedure Draw; virtual;    
+    property X: Integer read FX write FX;
+    property Y: Integer read FY write FY;
+    property Enabled: Boolean read FEnabled write FEnabled;
+    property Name: string read FName write FName;
+  end;
+
+  TGUIWindow = class
+  private
+    FActiveControl: TGUIControl;
+    FDefControl: string;
+    FPrevWindow: TGUIWindow;
+    FName: string;
+    FBackTexture: string;
+    FMainWindow: Boolean;
+    FOnKeyDown: TOnKeyDownEvent;
+    FOnCloseEvent: TOnCloseEvent;
+    FOnShowEvent: TOnShowEvent;
+  public
+    Childs: array of TGUIControl;
+    constructor Create(Name: string);
+    destructor Destroy; override;
+    function AddChild(Child: TGUIControl): TGUIControl;
+    procedure OnMessage(var Msg: TMessage);
+    procedure Update;
+    procedure Draw;
+    procedure SetActive(Control: TGUIControl);
+    function GetControl(Name: string): TGUIControl;
+    property OnKeyDown: TOnKeyDownEvent read FOnKeyDown write FOnKeyDown;
+    property OnClose: TOnCloseEvent read FOnCloseEvent write FOnCloseEvent;
+    property OnShow: TOnShowEvent read FOnShowEvent write FOnShowEvent;
+    property Name: string read FName;
+    property DefControl: string read FDefControl write FDefControl;
+    property BackTexture: string read FBackTexture write FBackTexture;
+    property MainWindow: Boolean read FMainWindow write FMainWindow;
+  end;
+
+  TGUITextButton = class(TGUIControl)
+  private
+    FText: string;
+    FColor: TRGB;
+    FFont: TFont;
+    FSound: string;
+    FShowWindow: string;
+  public
+    Proc: procedure;
+    constructor Create(Proc: Pointer; FontID: DWORD; Text: string);
+    destructor Destroy(); override;
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Update(); override;
+    procedure Draw(); override;
+    function GetWidth(): Integer;
+    function GetHeight(): Integer;
+    procedure Click(Silent: Boolean = False);
+    property Caption: string read FText write FText;
+    property Color: TRGB read FColor write FColor;
+    property Font: TFont read FFont write FFont;
+    property ShowWindow: string read FShowWindow write FShowWindow;
+  end;
+
+  TGUILabel = class(TGUIControl)
+  private
+    FText: string;
+    FColor: TRGB;
+    FFont: TFont;
+    FFixedLen: Word;
+    FOnClickEvent: TOnClickEvent;
+  public
+    constructor Create(Text: string; FontID: DWORD);
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Draw; override;
+    function GetWidth: Integer;
+    function GetHeight: Integer;
+    property OnClick: TOnClickEvent read FOnClickEvent write FOnClickEvent;
+    property FixedLength: Word read FFixedLen write FFixedLen;
+    property Text: string read FText write FText;
+    property Color: TRGB read FColor write FColor;
+    property Font: TFont read FFont write FFont;
+  end;
+
+  TGUIScroll = class(TGUIControl)
+  private
+    FValue: Integer;
+    FMax: Word;
+    FLeftID: DWORD;
+    FRightID: DWORD;
+    FMiddleID: DWORD;
+    FMarkerID: DWORD;
+    FOnChangeEvent: TOnChangeEvent;
+    procedure FSetValue(a: Integer);
+  public
+    constructor Create();
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Update; override;
+    procedure Draw; override;
+    function GetWidth(): Word;
+    property OnChange: TOnChangeEvent read FOnChangeEvent write FOnChangeEvent;
+    property Max: Word read FMax write FMax;
+    property Value: Integer read FValue write FSetValue;
+ end;
+
+  TGUISwitch = class(TGUIControl)
+  private
+    FFont: TFont;
+    FItems: array of string;
+    FIndex: Integer;
+    FColor: TRGB;
+    FOnChangeEvent: TOnChangeEvent;
+  public
+    constructor Create(FontID: DWORD);
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure AddItem(Item: string);
+    procedure Update; override;
+    procedure Draw; override;
+    function GetWidth(): Word;
+    function GetText: string;
+    property ItemIndex: Integer read FIndex write FIndex;
+    property Color: TRGB read FColor write FColor;
+    property Font: TFont read FFont write FFont;
+    property OnChange: TOnChangeEvent read FOnChangeEvent write FOnChangeEvent;
+  end;
+
+  TGUIEdit = class(TGUIControl)
+  private
+    FFont: TFont;
+    FCaretPos: Integer;
+    FMaxLength: Word;
+    FWidth: Word;
+    FText: string;
+    FColor: TRGB;
+    FOnlyDigits: Boolean;
+    FLeftID: DWORD;
+    FRightID: DWORD;
+    FMiddleID: DWORD;
+    FOnChangeEvent: TOnChangeEvent;
+    FOnEnterEvent: TOnEnterEvent;
+    procedure SetText(Text: string);
+  public
+    constructor Create(FontID: DWORD);
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Update; override;
+    procedure Draw; override;
+    function GetWidth(): Word;
+    property OnChange: TOnChangeEvent read FOnChangeEvent write FOnChangeEvent;
+    property OnEnter: TOnEnterEvent read FOnEnterEvent write FOnEnterEvent;
+    property Width: Word read FWidth write FWidth;
+    property MaxLength: Word read FMaxLength write FMaxLength;
+    property OnlyDigits: Boolean read FOnlyDigits write FOnlyDigits;
+    property Text: string read FText write SetText;
+    property Color: TRGB read FColor write FColor;
+    property Font: TFont read FFont write FFont;
+  end;
+
+  TGUIKeyRead = class(TGUIControl)
+  private
+    FFont: TFont;
+    FColor: TRGB;
+    FKey: Word;
+    FIsQuery: Boolean;
+  public
+    constructor Create(FontID: DWORD);
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Draw; override;
+    function GetWidth(): Word;
+    property Key: Word read FKey write FKey;
+    property Color: TRGB read FColor write FColor;
+    property Font: TFont read FFont write FFont;
+  end;
+
+  TGUIModelView = class(TGUIControl)
+  private
+    FModel: TPlayerModel;
+    a: Boolean;
+  public
+    constructor Create;
+    destructor Destroy; override;
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure SetModel(ModelName: string);
+    procedure SetColor(Red, Green, Blue: Byte);
+    procedure NextAnim();
+    procedure NextWeapon();
+    procedure Update; override;
+    procedure Draw; override;
+    property  Model: TPlayerModel read FModel;
+  end;
+
+  TPreviewPanel = record
+    X1, Y1, X2, Y2: Integer;
+    PanelType: Word;
+  end;
+
+  TGUIMapPreview = class(TGUIControl)
+  private
+    FMapData: array of TPreviewPanel;
+    FMapSize: TPoint;
+    FScale: Single;
+  public
+    constructor Create();
+    destructor Destroy(); override;
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure SetMap(Res: string);
+    procedure ClearMap();
+    procedure Update(); override;
+    procedure Draw(); override;
+    function GetScaleStr: String;
+  end;
+
+  TGUIImage = class(TGUIControl)
+  private
+    FImageRes: string;
+    FDefaultRes: string;
+  public
+    constructor Create();
+    destructor Destroy(); override;
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure SetImage(Res: string);
+    procedure ClearImage();
+    procedure Update(); override;
+    procedure Draw(); override;
+    property DefaultRes: string read FDefaultRes write FDefaultRes;
+  end;
+
+  TGUIListBox = class(TGUIControl)
+  private
+    FItems: SArray;
+    FActiveColor: TRGB;
+    FUnActiveColor: TRGB;
+    FFont: TFont;
+    FStartLine: Integer;
+    FIndex: Integer;
+    FWidth: Word;
+    FHeight: Word;
+    FSort: Boolean;
+    FDrawBack: Boolean;
+    FDrawScroll: Boolean;
+    FOnChangeEvent: TOnChangeEvent;
+
+    procedure FSetItems(Items: SArray);
+    procedure FSetIndex(aIndex: Integer);
+
+  public
+    constructor Create(FontID: DWORD; Width, Height: Word);
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Draw(); override;
+    procedure AddItem(Item: String);
+    procedure SelectItem(Item: String);
+    procedure Clear();
+    function  GetWidth(): Word;
+    function  GetHeight(): Word;
+    function  SelectedItem(): String;
+
+    property OnChange: TOnChangeEvent read FOnChangeEvent write FOnChangeEvent;
+    property Sort: Boolean read FSort write FSort;
+    property ItemIndex: Integer read FIndex write FSetIndex;
+    property Items: SArray read FItems write FSetItems;
+    property DrawBack: Boolean read FDrawBack write FDrawBack;
+    property DrawScrollBar: Boolean read FDrawScroll write FDrawScroll;
+    property ActiveColor: TRGB read FActiveColor write FActiveColor;
+    property UnActiveColor: TRGB read FUnActiveColor write FUnActiveColor;
+    property Font: TFont read FFont write FFont;
+  end;
+
+  TGUIFileListBox = class (TGUIListBox)
+  private
+    FBasePath: String;
+    FPath: String;
+    FFileMask: String;
+    FDirs: Boolean;
+
+    procedure OpenDir(path: String);
+
+  public
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure SetBase(path: String);
+    function  SelectedItem(): String;
+    procedure UpdateFileList();
+
+    property Dirs: Boolean read FDirs write FDirs;
+    property FileMask: String read FFileMask write FFileMask;
+    property Path: String read FPath;
+  end;
+
+  TGUIMemo = class(TGUIControl)
+  private
+    FLines: SArray;
+    FFont: TFont;
+    FStartLine: Integer;
+    FWidth: Word;
+    FHeight: Word;
+    FColor: TRGB;
+    FDrawBack: Boolean;
+    FDrawScroll: Boolean;
+  public
+    constructor Create(FontID: DWORD; Width, Height: Word);
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure Draw; override;
+    procedure Clear;
+    function GetWidth(): Word;
+    function GetHeight(): Word;
+    procedure SetText(Text: string);
+    property DrawBack: Boolean read FDrawBack write FDrawBack;
+    property DrawScrollBar: Boolean read FDrawScroll write FDrawScroll;
+    property Color: TRGB read FColor write FColor;
+    property Font: TFont read FFont write FFont;
+  end;
+
+  TGUIMainMenu = class(TGUIControl)
+  private
+    FButtons: array of TGUITextButton;
+    FHeader: TGUILabel;
+    FIndex: Integer;
+    FFontID: DWORD;
+    FCounter: Byte;
+    FMarkerID1: DWORD;
+    FMarkerID2: DWORD;
+  public
+    constructor Create(FontID: DWORD; Header: string);
+    destructor Destroy; override;
+    procedure OnMessage(var Msg: TMessage); override;
+    function AddButton(fProc: Pointer; Caption: string; ShowWindow: string = ''): TGUITextButton;
+    function GetButton(Name: string): TGUITextButton;
+    procedure EnableButton(Name: string; e: Boolean);
+    procedure AddSpace();
+    procedure Update; override;
+    procedure Draw; override;
+  end;
+
+  TControlType = class of TGUIControl;
+
+  PMenuItem = ^TMenuItem;
+  TMenuItem = record
+    Text: TGUILabel;
+    ControlType: TControlType;
+    Control: TGUIControl;
+  end;
+
+  TGUIMenu = class(TGUIControl)
+  private
+    FItems: array of TMenuItem;
+    FHeader: TGUILabel;
+    FIndex: Integer;
+    FFontID: DWORD;
+    FCounter: Byte;
+    FAlign: Boolean;
+    FLeft: Integer;
+    function NewItem(): Integer;
+  public
+    constructor Create(HeaderFont, ItemsFont: DWORD; Header: string);
+    destructor Destroy; override;
+    procedure OnMessage(var Msg: TMessage); override;
+    procedure AddSpace();
+    procedure AddLine(fText: string);
+    procedure AddText(fText: string; MaxWidth: Word);
+    function AddLabel(fText: string): TGUILabel;
+    function AddButton(Proc: Pointer; fText: string; _ShowWindow: string = ''): TGUITextButton;
+    function AddScroll(fText: string): TGUIScroll;
+    function AddSwitch(fText: string): TGUISwitch;
+    function AddEdit(fText: string): TGUIEdit;
+    function AddKeyRead(fText: string): TGUIKeyRead;
+    function AddList(fText: string; Width, Height: Word): TGUIListBox;
+    function AddFileList(fText: string; Width, Height: Word): TGUIFileListBox;
+    function AddMemo(fText: string; Width, Height: Word): TGUIMemo;
+    procedure ReAlign();
+    function GetControl(Name: string): TGUIControl;
+    function GetControlsText(Name: string): TGUILabel;
+    procedure Draw; override;
+    procedure Update; override;
+    procedure UpdateIndex();
+    property Align: Boolean read FAlign write FAlign;
+    property Left: Integer read FLeft write FLeft;
+  end;
+
+var
+  g_GUIWindows: array of TGUIWindow;
+  g_ActiveWindow: TGUIWindow = nil;
+
+procedure g_GUI_Init();
+function  g_GUI_AddWindow(Window: TGUIWindow): TGUIWindow;
+function  g_GUI_GetWindow(Name: string): TGUIWindow;
+procedure g_GUI_ShowWindow(Name: string);
+procedure g_GUI_HideWindow(PlaySound: Boolean = True);
+function  g_GUI_Destroy(): Boolean;
+procedure g_GUI_SaveMenuPos();
+procedure g_GUI_LoadMenuPos();
+
+implementation
+
+uses
+  GL, GLExt, g_textures, g_sound, SysUtils,
+  g_game, Math, StrUtils, g_player, g_options, MAPREADER,
+  g_map, MAPDEF, g_weapons;
+
+var
+  Box: Array [0..8] of DWORD;
+  Saved_Windows: SArray;
+
+procedure g_GUI_Init();
+begin
+  g_Texture_Get(BOX1, Box[0]);
+  g_Texture_Get(BOX2, Box[1]);
+  g_Texture_Get(BOX3, Box[2]);
+  g_Texture_Get(BOX4, Box[3]);
+  g_Texture_Get(BOX5, Box[4]);
+  g_Texture_Get(BOX6, Box[5]);
+  g_Texture_Get(BOX7, Box[6]);
+  g_Texture_Get(BOX8, Box[7]);
+  g_Texture_Get(BOX9, Box[8]);
+end;
+
+function g_GUI_Destroy(): Boolean;
+var
+  i: Integer;
+begin
+  Result := (Length(g_GUIWindows) > 0);
+
+  for i := 0 to High(g_GUIWindows) do
+    g_GUIWindows[i].Free();
+
+  g_GUIWindows := nil;
+  g_ActiveWindow := nil;
+end;
+
+function g_GUI_AddWindow(Window: TGUIWindow): TGUIWindow;
+begin
+  SetLength(g_GUIWindows, Length(g_GUIWindows)+1);
+  g_GUIWindows[High(g_GUIWindows)] := Window;
+
+  Result := Window;
+end;
+
+function g_GUI_GetWindow(Name: string): TGUIWindow;
+var
+  i: Integer;
+begin
+  Result := nil;
+
+  if g_GUIWindows <> nil then
+    for i := 0 to High(g_GUIWindows) do
+      if g_GUIWindows[i].FName = Name then
+      begin
+        Result := g_GUIWindows[i];
+        Break;
+      end;
+
+  Assert(Result <> nil, 'GUI_Window "'+Name+'" not found');
+end;
+
+procedure g_GUI_ShowWindow(Name: string);
+var
+  i: Integer;
+begin
+  if g_GUIWindows = nil then
+    Exit;
+
+  for i := 0 to High(g_GUIWindows) do
+    if g_GUIWindows[i].FName = Name then
+    begin
+      g_GUIWindows[i].FPrevWindow := g_ActiveWindow;
+      g_ActiveWindow := g_GUIWindows[i];
+
+      if g_ActiveWindow.MainWindow then
+        g_ActiveWindow.FPrevWindow := nil;
+
+      if g_ActiveWindow.FDefControl <> '' then
+        g_ActiveWindow.SetActive(g_ActiveWindow.GetControl(g_ActiveWindow.FDefControl))
+      else
+        g_ActiveWindow.SetActive(nil);
+
+      if @g_ActiveWindow.FOnShowEvent <> nil then
+        g_ActiveWindow.FOnShowEvent();
+
+      Break;
+    end;
+end;
+
+procedure g_GUI_HideWindow(PlaySound: Boolean = True);
+begin
+  if g_ActiveWindow <> nil then
+  begin
+    if @g_ActiveWindow.OnClose <> nil then
+      g_ActiveWindow.OnClose();
+    g_ActiveWindow := g_ActiveWindow.FPrevWindow;
+    if PlaySound then
+      g_Sound_PlayEx(WINDOW_CLOSESOUND);
+  end;
+end;
+
+procedure g_GUI_SaveMenuPos();
+var
+  len: Integer;
+  win: TGUIWindow;
+begin
+  SetLength(Saved_Windows, 0);
+  win := g_ActiveWindow;
+
+  while win <> nil do
+  begin
+    len := Length(Saved_Windows);
+    SetLength(Saved_Windows, len + 1);
+
+    Saved_Windows[len] := win.Name;
+
+    if win.MainWindow then
+      win := nil
+    else
+      win := win.FPrevWindow;
+  end;
+end;
+
+procedure g_GUI_LoadMenuPos();
+var
+  i, j, k, len: Integer;
+  ok: Boolean;
+begin
+  g_ActiveWindow := nil;
+  len := Length(Saved_Windows);
+
+  if len = 0 then
+    Exit;
+
+// Îêíî ñ ãëàâíûì ìåíþ:
+  g_GUI_ShowWindow(Saved_Windows[len-1]);
+
+// Íå ïåðåêëþ÷èëîñü (èëè íåêóäà äàëüøå):
+  if (len = 1) or (g_ActiveWindow = nil) then
+    Exit;
+
+// Èùåì êíîïêè â îñòàëüíûõ îêíàõ:
+  for k := len-1 downto 1 do
+  begin
+    ok := False;
+
+    for i := 0 to Length(g_ActiveWindow.Childs)-1 do
+    begin
+      if g_ActiveWindow.Childs[i] is TGUIMainMenu then
+        begin // GUI_MainMenu
+          with TGUIMainMenu(g_ActiveWindow.Childs[i]) do
+            for j := 0 to Length(FButtons)-1 do
+              if FButtons[j].ShowWindow = Saved_Windows[k-1] then
+              begin
+                FButtons[j].Click(True);
+                ok := True;
+                Break;
+              end;
+        end
+      else // GUI_Menu
+        if g_ActiveWindow.Childs[i] is TGUIMenu then
+          with TGUIMenu(g_ActiveWindow.Childs[i]) do
+            for j := 0 to Length(FItems)-1 do
+              if FItems[j].ControlType = TGUITextButton then
+                if TGUITextButton(FItems[j].Control).ShowWindow = Saved_Windows[k-1] then
+                begin
+                  TGUITextButton(FItems[j].Control).Click(True);
+                  ok := True;
+                  Break;
+                end;
+
+      if ok then
+        Break;
+    end;
+
+  // Íå ïåðåêëþ÷èëîñü:
+    if (not ok) or
+       (g_ActiveWindow.Name = Saved_Windows[k]) then
+      Break;
+  end;
+end;
+
+procedure DrawBox(X, Y: Integer; Width, Height: Word);
+begin
+  e_Draw(Box[0], X, Y, 0, False, False);
+  e_DrawFill(Box[1], X+4, Y, Width*4, 1, 0, False, False);
+  e_Draw(Box[2], X+4+Width*16, Y, 0, False, False);
+  e_DrawFill(Box[3], X, Y+4, 1, Height*4, 0, False, False);
+  e_DrawFill(Box[4], X+4, Y+4, Width, Height, 0, False, False);
+  e_DrawFill(Box[5], X+4+Width*16, Y+4, 1, Height*4, 0, False, False);
+  e_Draw(Box[6], X, Y+4+Height*16, 0, False, False);
+  e_DrawFill(Box[7], X+4, Y+4+Height*16, Width*4, 1, 0, False, False);
+  e_Draw(Box[8], X+4+Width*16, Y+4+Height*16, 0, False, False);
+end;
+
+procedure DrawScroll(X, Y: Integer; Height: Word; Up, Down: Boolean);
+var
+  ID: DWORD;
+begin
+  if Height < 3 then Exit;
+
+  if Up then
+    g_Texture_Get(BSCROLL_UPA, ID)
+  else
+    g_Texture_Get(BSCROLL_UPU, ID);
+  e_Draw(ID, X, Y, 0, False, False);
+
+  if Down then
+    g_Texture_Get(BSCROLL_DOWNA, ID)
+  else
+    g_Texture_Get(BSCROLL_DOWNU, ID);
+  e_Draw(ID, X, Y+(Height-1)*16, 0, False, False);
+
+  g_Texture_Get(BSCROLL_MIDDLE, ID);
+  e_DrawFill(ID, X, Y+16, 1, Height-2, 0, False, False);
+end;
+
+{ TGUIWindow }
+
+constructor TGUIWindow.Create(Name: string);
+begin
+  Childs := nil;
+  FActiveControl := nil;
+  FName := Name;
+  FOnKeyDown := nil;
+  FOnCloseEvent := nil;
+  FOnShowEvent := nil;
+end;
+
+destructor TGUIWindow.Destroy;
+var
+  i: Integer;
+begin
+  if Childs = nil then
+    Exit;
+
+  for i := 0 to High(Childs) do
+    Childs[i].Free();
+end;
+
+function TGUIWindow.AddChild(Child: TGUIControl): TGUIControl;
+begin
+  Child.FWindow := Self;
+
+  SetLength(Childs, Length(Childs) + 1);
+  Childs[High(Childs)] := Child;
+
+  Result := Child;
+end;
+
+procedure TGUIWindow.Update;
+var
+  i: Integer;
+begin
+  for i := 0 to High(Childs) do
+    if Childs[i] <> nil then Childs[i].Update;
+end;
+
+procedure TGUIWindow.Draw;
+var
+  i: Integer;
+  ID: DWORD;
+begin
+  if FBackTexture <> '' then
+    if g_Texture_Get(FBackTexture, ID) then
+      e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
+    else
+      e_Clear(GL_COLOR_BUFFER_BIT, 0.5, 0.5, 0.5);
+
+  for i := 0 to High(Childs) do
+    if Childs[i] <> nil then Childs[i].Draw;
+end;
+
+procedure TGUIWindow.OnMessage(var Msg: TMessage);
+begin
+  if FActiveControl <> nil then FActiveControl.OnMessage(Msg);
+  if @FOnKeyDown <> nil then FOnKeyDown(Msg.wParam);
+
+  if Msg.Msg = WM_KEYDOWN then
+    if Msg.wParam = IK_ESCAPE then
+    begin
+      g_GUI_HideWindow;
+      Exit;
+    end;
+end;
+
+procedure TGUIWindow.SetActive(Control: TGUIControl);
+begin
+  FActiveControl := Control;
+end;
+
+function TGUIWindow.GetControl(Name: String): TGUIControl;
+var
+  i: Integer;
+begin
+  Result := nil;
+
+  if Childs <> nil then
+    for i := 0 to High(Childs) do
+      if Childs[i] <> nil then
+        if LowerCase(Childs[i].FName) = LowerCase(Name) then
+        begin
+          Result := Childs[i];
+          Break;
+        end;
+
+  Assert(Result <> nil, 'Window Control "'+Name+'" not Found!');
+end;
+
+{ TGUIControl }
+
+constructor TGUIControl.Create();
+begin
+  FX := 0;
+  FY := 0;
+
+  FEnabled := True;
+end;
+
+procedure TGUIControl.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then
+    Exit;
+end;
+
+procedure TGUIControl.Update();
+begin
+
+end;
+
+procedure TGUIControl.Draw();
+begin
+
+end;
+
+{ TGUITextButton }
+
+procedure TGUITextButton.Click(Silent: Boolean = False);
+begin
+  if (FSound <> '') and (not Silent) then
+    g_Sound_PlayEx(FSound);
+
+  if @Proc <> nil then
+    Proc();
+  if FShowWindow <> '' then
+    g_GUI_ShowWindow(FShowWindow);
+end;
+
+constructor TGUITextButton.Create(Proc: Pointer; FontID: DWORD; Text: string);
+begin
+  inherited Create();
+
+  Self.Proc := Proc;
+
+  FFont := TFont.Create(FontID, FONT_CHAR);
+
+  FText := Text;
+end;
+
+destructor TGUITextButton.Destroy;
+begin
+
+ inherited;
+end;
+
+procedure TGUITextButton.Draw;
+begin
+  FFont.Draw(FX, FY, FText, FColor.R, FColor.G, FColor.B)
+end;
+
+function TGUITextButton.GetHeight: Integer;
+var
+  w, h: Word;
+begin
+  FFont.GetTextSize(FText, w, h);
+  Result := h;
+end;
+
+function TGUITextButton.GetWidth: Integer;
+var
+  w, h: Word;
+begin
+  FFont.GetTextSize(FText, w, h);
+  Result := w;
+end;
+
+procedure TGUITextButton.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  case Msg.Msg of
+    WM_KEYDOWN:
+      case Msg.wParam of
+        IK_RETURN: Click();
+      end;
+  end;
+end;
+
+procedure TGUITextButton.Update;
+begin
+  inherited;
+end;
+
+{ TFont }
+
+constructor TFont.Create(FontID: DWORD; FontType: TFontType);
+begin
+  ID := FontID;
+
+  FScale := 1;
+  FFontType := FontType;
+end;
+
+destructor TFont.Destroy;
+begin
+
+  inherited;
+end;
+
+procedure TFont.Draw(X, Y: Integer; Text: string; R, G, B: Byte);
+begin
+  if FFontType = FONT_CHAR then e_CharFont_PrintEx(ID, X, Y, Text, _RGB(R, G, B), FScale)
+  else e_TextureFontPrintEx(X, Y, Text, ID, R, G, B, FScale); 
+end;
+
+procedure TFont.GetTextSize(Text: string; var w, h: Word);
+var
+  cw, ch: Byte;
+begin
+  if FFontType = FONT_CHAR then e_CharFont_GetSize(ID, Text, w, h)
+  else
+  begin
+    e_TextureFontGetSize(ID, cw, ch);
+    w := cw*Length(Text);
+    h := ch;
+  end;
+
+  w := Round(w*FScale);
+  h := Round(h*FScale);
+end;
+
+{ TGUIMainMenu }
+
+function TGUIMainMenu.AddButton(fProc: Pointer; Caption: string; ShowWindow: string = ''): TGUITextButton;
+var
+  a, _x: Integer;
+  h, hh: Word;
+begin
+  FIndex := 0;
+
+  SetLength(FButtons, Length(FButtons)+1);
+  FButtons[High(FButtons)] := TGUITextButton.Create(fProc, FFontID, Caption);
+  FButtons[High(FButtons)].ShowWindow := ShowWindow;
+  with FButtons[High(FButtons)] do
+  begin
+    if (fProc <> nil) or (ShowWindow <> '') then FColor := MAINMENU_ITEMS_COLOR
+    else FColor := MAINMENU_UNACTIVEITEMS_COLOR;
+    FSound := MAINMENU_CLICKSOUND;
+  end;
+
+  _x := gScreenWidth div 2;
+
+  for a := 0 to High(FButtons) do
+    if FButtons[a] <> nil then
+      _x := Min(_x, (gScreenWidth div 2)-(FButtons[a].GetWidth div 2));
+
+  hh := FHeader.GetHeight;
+
+  h := hh*(2+Length(FButtons))+MAINMENU_SPACE*(Length(FButtons)-1);
+  h := (gScreenHeight div 2)-(h div 2);
+
+  with FHeader do
+  begin
+    FX := _x;
+    FY := h;
+  end;
+
+  Inc(h, hh*2);
+
+  for a := 0 to High(FButtons) do
+  begin
+    if FButtons[a] <> nil then
+    with FButtons[a] do
+    begin
+      FX := _x;
+      FY := h;
+    end;
+
+    Inc(h, hh+MAINMENU_SPACE);
+  end;
+
+  Result := FButtons[High(FButtons)];
+end;
+
+procedure TGUIMainMenu.AddSpace;
+begin
+  SetLength(FButtons, Length(FButtons)+1);
+  FButtons[High(FButtons)] := nil;
+end;
+
+constructor TGUIMainMenu.Create(FontID: DWORD; Header: string);
+begin
+  inherited Create();
+
+  FIndex := -1;
+  FFontID := FontID;
+  FCounter := MAINMENU_MARKERDELAY;
+
+  g_Texture_Get(MAINMENU_MARKER1, FMarkerID1);
+  g_Texture_Get(MAINMENU_MARKER2, FMarkerID2); 
+
+  FHeader := TGUILabel.Create(Header, FFontID);
+  with FHeader do
+  begin
+    FColor := MAINMENU_HEADER_COLOR;
+    FX := (gScreenWidth div 2)-(GetWidth div 2);
+    FY := (gScreenHeight div 2)-(GetHeight div 2);
+  end;
+end;
+
+destructor TGUIMainMenu.Destroy;
+var
+  a: Integer;
+begin
+  if FButtons <> nil then
+    for a := 0 to High(FButtons) do
+      FButtons[a].Free();
+
+  FHeader.Free();
+
+  inherited;
+end;
+
+procedure TGUIMainMenu.Draw;
+var
+  a: Integer;
+begin
+  inherited;
+
+  FHeader.Draw;
+
+  if FButtons <> nil then
+  begin
+    for a := 0 to High(FButtons) do
+      if FButtons[a] <> nil then FButtons[a].Draw;
+
+    if FIndex <> -1 then
+      e_Draw(FMarkerID1, FButtons[FIndex].FX-48, FButtons[FIndex].FY, 0, True, False); 
+  end;
+end;
+
+procedure TGUIMainMenu.EnableButton(Name: string; e: Boolean);
+var
+  a: Integer;
+begin
+  if FButtons = nil then Exit;
+
+  for a := 0 to High(FButtons) do
+    if (FButtons[a] <> nil) and (FButtons[a].Name = Name) then
+    begin
+      if e then FButtons[a].FColor := MAINMENU_ITEMS_COLOR
+      else FButtons[a].FColor := MAINMENU_UNACTIVEITEMS_COLOR;
+      FButtons[a].Enabled := e;
+      Break;
+    end;
+end;
+
+function TGUIMainMenu.GetButton(Name: string): TGUITextButton;
+var
+  a: Integer;
+begin
+  Result := nil;
+
+  if FButtons = nil then Exit;
+
+  for a := 0 to High(FButtons) do
+    if (FButtons[a] <> nil) and (FButtons[a].Name = Name) then
+    begin
+      Result := FButtons[a];
+      Break;
+    end;
+end;
+
+procedure TGUIMainMenu.OnMessage(var Msg: TMessage);
+var
+  ok: Boolean;
+  a: Integer;
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  if FButtons = nil then Exit;
+
+  ok := False;
+  for a := 0 to High(FButtons) do
+    if FButtons[a] <> nil then
+    begin
+      ok := True;
+      Break;
+    end;
+
+  if not ok then Exit;
+
+  case Msg.Msg of
+    WM_KEYDOWN:
+      case Msg.wParam of
+        IK_UP:
+        begin
+          repeat
+            Dec(FIndex);
+            if FIndex < 0 then FIndex := High(FButtons);
+          until FButtons[FIndex] <> nil;
+
+          g_Sound_PlayEx(MENU_CHANGESOUND);
+        end;
+        IK_DOWN:
+        begin
+          repeat
+            Inc(FIndex);
+            if FIndex > High(FButtons) then FIndex := 0;
+          until FButtons[FIndex] <> nil;
+
+          g_Sound_PlayEx(MENU_CHANGESOUND);
+        end;
+        IK_RETURN: if (FIndex <> -1) and FButtons[FIndex].FEnabled then FButtons[FIndex].Click;
+      end;
+  end;
+end;
+
+procedure TGUIMainMenu.Update;
+var
+  t: DWORD;
+begin
+  inherited;
+
+  if FCounter = 0 then
+  begin
+    t := FMarkerID1;
+    FMarkerID1 := FMarkerID2;
+    FMarkerID2 := t;
+
+    FCounter := MAINMENU_MARKERDELAY;
+  end else Dec(FCounter);
+end;
+
+{ TGUILabel }
+
+constructor TGUILabel.Create(Text: string; FontID: DWORD);
+begin
+  inherited Create();
+
+  FFont := TFont.Create(FontID, FONT_CHAR); 
+
+  FText := Text;
+  FFixedLen := 0;
+  FOnClickEvent := nil;
+end;
+
+procedure TGUILabel.Draw;
+begin
+  FFont.Draw(FX, FY, FText, FColor.R, FColor.G, FColor.B);
+end;
+
+function TGUILabel.GetHeight: Integer;
+var
+  w, h: Word;
+begin
+  FFont.GetTextSize(FText, w, h);
+  Result := h;
+end;
+
+function TGUILabel.GetWidth: Integer;
+var
+  w, h: Word;
+begin
+  if FFixedLen = 0 then
+    FFont.GetTextSize(FText, w, h)
+  else
+    w := e_CharFont_GetMaxWidth(FFont.ID)*FFixedLen; 
+  Result := w; 
+end;
+
+procedure TGUILabel.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  case Msg.Msg of
+    WM_KEYDOWN:
+      case Msg.wParam of
+        IK_RETURN: if @FOnClickEvent <> nil then FOnClickEvent();
+      end;
+  end;
+end;
+
+{ TGUIMenu }
+
+function TGUIMenu.AddButton(Proc: Pointer; fText: string; _ShowWindow: string = ''): TGUITextButton;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUITextButton.Create(Proc, FFontID, fText);
+    with Control as TGUITextButton  do
+    begin
+      ShowWindow := _ShowWindow;
+      FColor := MENU_ITEMSCTRL_COLOR;
+    end;
+
+    Text := nil;
+    ControlType := TGUITextButton;
+
+    Result := (Control as TGUITextButton);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+procedure TGUIMenu.AddLine(fText: string);
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Text := TGUILabel.Create(fText, FFontID);
+    with Text do
+    begin
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    Control := nil;
+  end;
+
+  ReAlign();
+end;
+
+procedure TGUIMenu.AddText(fText: string; MaxWidth: Word);
+var
+  a, i: Integer;
+  l: SArray;
+begin
+  l := GetLines(fText, FFontID, MaxWidth);
+
+  if l = nil then Exit;
+
+  for a := 0 to High(l) do
+  begin
+    i := NewItem();
+    with FItems[i] do
+    begin
+      Text := TGUILabel.Create(l[a], FFontID);
+      with Text do
+      begin
+        FColor := MENU_ITEMSTEXT_COLOR;
+      end;
+
+      Control := nil;
+    end;
+  end;
+
+  ReAlign();
+end;
+
+procedure TGUIMenu.AddSpace;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Text := nil;
+    Control := nil;
+  end;
+
+  ReAlign();
+end;
+
+constructor TGUIMenu.Create(HeaderFont, ItemsFont: DWORD; Header: string);
+begin
+  inherited Create();
+
+  FItems := nil;
+  FIndex := -1;
+  FFontID := ItemsFont;
+  FCounter := MENU_MARKERDELAY;
+  FAlign := True;
+
+  FHeader := TGUILabel.Create(Header, HeaderFont);
+  with FHeader do
+  begin
+    FX := (gScreenWidth div 2)-(GetWidth div 2);
+    FY := 0;
+    FColor := MAINMENU_HEADER_COLOR;
+  end;
+end;
+
+destructor TGUIMenu.Destroy;
+var
+  a: Integer;
+begin
+  if FItems <> nil then
+    for a := 0 to High(FItems) do
+      with FItems[a] do
+      begin
+        Text.Free();
+        Control.Free();
+      end;
+
+  FItems := nil;
+
+  FHeader.Free();
+
+  inherited;
+end;
+
+procedure TGUIMenu.Draw;
+var
+  a, x, y: Integer;
+begin
+  inherited;
+
+  if FHeader <> nil then FHeader.Draw;
+
+  if FItems <> nil then
+    for a := 0 to High(FItems) do
+    begin
+      if FItems[a].Text <> nil then FItems[a].Text.Draw;
+      if FItems[a].Control <> nil then FItems[a].Control.Draw;
+    end;
+
+  if (FIndex <> -1) and (FCounter > MENU_MARKERDELAY div 2) then
+  begin
+    x := 0;
+    y := 0;
+
+    if FItems[FIndex].Text <> nil then
+    begin
+      x := FItems[FIndex].Text.FX;
+      y := FItems[FIndex].Text.FY;
+    end
+    else if FItems[FIndex].Control <> nil then
+    begin
+      x := FItems[FIndex].Control.FX;
+      y := FItems[FIndex].Control.FY;
+    end;
+
+    x := x-e_CharFont_GetMaxWidth(FFontID); 
+
+    e_CharFont_PrintEx(FFontID, x, y, #16, _RGB(255, 0, 0)); 
+  end;
+end;
+
+function TGUIMenu.GetControl(Name: String): TGUIControl;
+var
+  a: Integer;
+begin
+  Result := nil;
+
+  if FItems <> nil then
+    for a := 0 to High(FItems) do
+      if FItems[a].Control <> nil then
+        if LowerCase(FItems[a].Control.Name) = LowerCase(Name) then
+        begin
+          Result := FItems[a].Control;
+          Break;
+        end;
+
+  Assert(Result <> nil, 'GUI control "'+Name+'" not found!');
+end;
+
+function TGUIMenu.GetControlsText(Name: String): TGUILabel;
+var
+  a: Integer;
+begin
+  Result := nil;
+
+  if FItems <> nil then
+    for a := 0 to High(FItems) do
+      if FItems[a].Control <> nil then
+        if LowerCase(FItems[a].Control.Name) = LowerCase(Name) then
+        begin
+          Result := FItems[a].Text;
+          Break;
+        end;
+
+  Assert(Result <> nil, 'GUI control''s text "'+Name+'" not found!');
+end;
+
+function TGUIMenu.NewItem: Integer;
+begin
+  SetLength(FItems, Length(FItems)+1);
+  Result := High(FItems);
+end;
+
+procedure TGUIMenu.OnMessage(var Msg: TMessage);
+var
+  ok: Boolean;
+  a, c: Integer;
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  if FItems = nil then Exit;
+
+  ok := False;
+  for a := 0 to High(FItems) do
+    if FItems[a].Control <> nil then
+    begin
+      ok := True;
+      Break;
+    end;
+
+  if not ok then Exit;
+
+  case Msg.Msg of
+    WM_KEYDOWN:
+    begin
+      case Msg.wParam of
+        IK_UP:
+        begin
+          c := 0;
+          repeat
+            c := c+1;
+            if c > Length(FItems) then
+            begin
+              FIndex := -1;
+              Break;
+            end;
+
+            Dec(FIndex);
+            if FIndex < 0 then FIndex := High(FItems);
+          until (FItems[FIndex].Control <> nil) and
+                (FItems[FIndex].Control.Enabled);
+
+          FCounter := 0;
+
+          g_Sound_PlayEx(MENU_CHANGESOUND);
+        end;
+
+        IK_DOWN:
+        begin
+          c := 0;
+          repeat
+            c := c+1;
+            if c > Length(FItems) then
+            begin
+              FIndex := -1;
+              Break;
+            end;
+
+            Inc(FIndex);
+            if FIndex > High(FItems) then FIndex := 0;
+          until (FItems[FIndex].Control <> nil) and
+                (FItems[FIndex].Control.Enabled);
+
+          FCounter := 0;
+
+          g_Sound_PlayEx(MENU_CHANGESOUND);
+        end;
+
+        IK_LEFT, IK_RIGHT:
+        begin
+          if FIndex <> -1 then
+            if FItems[FIndex].Control <> nil then
+              FItems[FIndex].Control.OnMessage(Msg);
+        end;
+        IK_RETURN:
+        begin
+          if FIndex <> -1 then
+            if FItems[FIndex].Control <> nil then
+              FItems[FIndex].Control.OnMessage(Msg);
+
+          g_Sound_PlayEx(MENU_CLICKSOUND);
+        end;
+      end;
+    end;
+  end;
+end;
+
+procedure TGUIMenu.ReAlign();
+var
+  a, tx, cx, w, h: Integer;
+begin
+  if FItems = nil then Exit;
+
+  if not FAlign then tx := FLeft else
+  begin
+    tx := gScreenWidth;
+    for a := 0 to High(FItems) do
+    begin
+      w := 0;
+      if FItems[a].Text <> nil then w := FItems[a].Text.GetWidth;
+      if FItems[a].Control <> nil then
+      begin
+        w := w+MENU_HSPACE;
+
+        if FItems[a].ControlType = TGUILabel then
+          w := w+(FItems[a].Control as TGUILabel).GetWidth
+        else if FItems[a].ControlType = TGUITextButton then
+          w := w+(FItems[a].Control as TGUITextButton).GetWidth
+        else if FItems[a].ControlType = TGUIScroll then
+          w := w+(FItems[a].Control as TGUIScroll).GetWidth
+        else if FItems[a].ControlType = TGUISwitch then
+          w := w+(FItems[a].Control as TGUISwitch).GetWidth
+        else if FItems[a].ControlType = TGUIEdit then
+          w := w+(FItems[a].Control as TGUIEdit).GetWidth
+        else if FItems[a].ControlType = TGUIKeyRead then
+          w := w+(FItems[a].Control as TGUIKeyRead).GetWidth
+        else if (FItems[a].ControlType = TGUIListBox) then
+          w := w+(FItems[a].Control as TGUIListBox).GetWidth
+        else if (FItems[a].ControlType = TGUIFileListBox) then
+          w := w+(FItems[a].Control as TGUIFileListBox).GetWidth
+        else if FItems[a].ControlType = TGUIMemo then
+          w := w+(FItems[a].Control as TGUIMemo).GetWidth;
+      end;
+
+      tx := Min(tx, (gScreenWidth div 2)-(w div 2));
+    end;
+  end;
+
+  cx := 0;
+  for a := 0 to High(FItems) do
+    with FItems[a] do
+    begin
+      if (Text <> nil) and (Control = nil) then Continue;
+
+      w := 0;
+      if Text <> nil then w := tx+Text.GetWidth;
+
+      if w > cx then cx := w;
+    end;
+
+  cx := cx+MENU_HSPACE;
+
+  h := FHeader.GetHeight*2+MENU_VSPACE*(Length(FItems)-1);
+
+  for a := 0 to High(FItems) do
+    with FItems[a] do
+    begin
+      if (ControlType = TGUIListBox) or (ControlType = TGUIFileListBox) then
+        h := h+(FItems[a].Control as TGUIListBox).GetHeight()
+      else
+        h := h+e_CharFont_GetMaxHeight(FFontID);
+    end;
+
+  h := (gScreenHeight div 2)-(h div 2);
+
+  with FHeader do
+  begin
+    FX := (gScreenWidth div 2)-(GetWidth div 2);
+    FY := h;
+
+    Inc(h, GetHeight*2);
+  end;
+
+  for a := 0 to High(FItems) do
+    with FItems[a] do
+    begin
+      if Text <> nil then
+        with Text do
+        begin
+          FX := tx;
+          FY := h;
+        end;
+
+        if Control <> nil then
+          with Control do
+            if Text <> nil then
+            begin
+              FX := cx;
+              FY := h;
+            end
+            else
+            begin
+              FX := tx;
+              FY := h;
+            end;
+
+        if (ControlType = TGUIListBox) or (ControlType = TGUIFileListBox) then
+          Inc(h, (Control as TGUIListBox).GetHeight+MENU_VSPACE)
+        else if ControlType = TGUIMemo then
+          Inc(h, (Control as TGUIMemo).GetHeight+MENU_VSPACE)
+        else
+          Inc(h, e_CharFont_GetMaxHeight(FFontID)+MENU_VSPACE);
+    end;
+end;
+
+function TGUIMenu.AddScroll(fText: string): TGUIScroll;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUIScroll.Create();
+
+    Text := TGUILabel.Create(fText, FFontID);
+    with Text do
+    begin
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUIScroll;
+
+    Result := (Control as TGUIScroll);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+function TGUIMenu.AddSwitch(fText: string): TGUISwitch;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUISwitch.Create(FFontID);
+   (Control as TGUISwitch).FColor := MENU_ITEMSCTRL_COLOR;
+
+    Text := TGUILabel.Create(fText, FFontID);
+    with Text do
+    begin
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUISwitch;
+
+    Result := (Control as TGUISwitch);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+function TGUIMenu.AddEdit(fText: string): TGUIEdit;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUIEdit.Create(FFontID);
+    with Control as TGUIEdit do
+    begin
+      FWindow := Self.FWindow;
+      FColor := MENU_ITEMSCTRL_COLOR;
+    end;
+
+    if fText = '' then Text := nil else
+    begin
+      Text := TGUILabel.Create(fText, FFontID);
+      Text.FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUIEdit;
+
+    Result := (Control as TGUIEdit);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+procedure TGUIMenu.Update;
+var
+  a: Integer;
+begin
+  inherited;
+
+  if FCounter = 0 then FCounter := MENU_MARKERDELAY else Dec(FCounter);
+
+  if FItems <> nil then
+    for a := 0 to High(FItems) do
+      if FItems[a].Control <> nil then
+        (FItems[a].Control as FItems[a].ControlType).Update;
+end;
+
+function TGUIMenu.AddKeyRead(fText: string): TGUIKeyRead;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUIKeyRead.Create(FFontID);
+    with Control as TGUIKeyRead do
+    begin
+      FWindow := Self.FWindow;
+      FColor := MENU_ITEMSCTRL_COLOR;
+    end;
+
+    Text := TGUILabel.Create(fText, FFontID);
+    with Text do
+    begin
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUIKeyRead;
+
+    Result := (Control as TGUIKeyRead);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+function TGUIMenu.AddList(fText: string; Width, Height: Word): TGUIListBox;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUIListBox.Create(FFontID, Width, Height);
+    with Control as TGUIListBox do
+    begin
+      FWindow := Self.FWindow;
+      FActiveColor := MENU_ITEMSCTRL_COLOR;
+      FUnActiveColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    Text := TGUILabel.Create(fText, FFontID);
+    with Text do
+    begin
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUIListBox;
+
+    Result := (Control as TGUIListBox);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+function TGUIMenu.AddFileList(fText: string; Width, Height: Word): TGUIFileListBox;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUIFileListBox.Create(FFontID, Width, Height);
+    with Control as TGUIFileListBox do
+    begin
+      FWindow := Self.FWindow;
+      FActiveColor := MENU_ITEMSCTRL_COLOR;
+      FUnActiveColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    if fText = '' then Text := nil else
+    begin
+      Text := TGUILabel.Create(fText, FFontID);
+      Text.FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUIFileListBox;
+
+    Result := (Control as TGUIFileListBox);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+function TGUIMenu.AddLabel(fText: string): TGUILabel;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUILabel.Create('', FFontID);
+    with Control as TGUILabel do
+    begin
+      FWindow := Self.FWindow;
+      FColor := MENU_ITEMSCTRL_COLOR;
+    end;
+
+    Text := TGUILabel.Create(fText, FFontID);
+    with Text do
+    begin
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUILabel;
+
+    Result := (Control as TGUILabel);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+function TGUIMenu.AddMemo(fText: string; Width, Height: Word): TGUIMemo;
+var
+  i: Integer;
+begin
+  i := NewItem();
+  with FItems[i] do
+  begin
+    Control := TGUIMemo.Create(FFontID, Width, Height);
+    with Control as TGUIMemo do
+    begin
+      FWindow := Self.FWindow;
+      FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    if fText = '' then Text := nil else
+    begin
+      Text := TGUILabel.Create(fText, FFontID);
+      Text.FColor := MENU_ITEMSTEXT_COLOR;
+    end;
+
+    ControlType := TGUIMemo;
+
+    Result := (Control as TGUIMemo);
+  end;
+
+  if FIndex = -1 then FIndex := i;
+
+  ReAlign();
+end;
+
+procedure TGUIMenu.UpdateIndex();
+var
+  res: Boolean;
+begin
+  res := True;
+
+  while res do
+  begin
+    if (FIndex < 0) or (FIndex > High(FItems)) then
+      begin
+        FIndex := -1;
+        res := False;
+      end
+    else
+      if FItems[FIndex].Control.Enabled then
+        res := False
+      else
+        Inc(FIndex);
+  end;
+end;
+
+{ TGUIScroll }
+
+constructor TGUIScroll.Create;
+begin
+  inherited Create();
+
+  FMax := 0;
+  FOnChangeEvent := nil;
+
+  g_Texture_Get(SCROLL_LEFT, FLeftID);
+  g_Texture_Get(SCROLL_RIGHT, FRightID);
+  g_Texture_Get(SCROLL_MIDDLE, FMiddleID);
+  g_Texture_Get(SCROLL_MARKER, FMarkerID);
+end;
+
+procedure TGUIScroll.Draw;
+var
+  a: Integer;
+begin
+  inherited;
+
+  e_Draw(FLeftID, FX, FY, 0, True, False);
+  e_Draw(FRightID, FX+8+(FMax+1)*8, FY, 0, True, False);
+
+  for a := 0 to FMax do
+    e_Draw(FMiddleID, FX+8+a*8, FY, 0, True, False);
+
+  e_Draw(FMarkerID, FX+8+FValue*8, FY, 0, True, False);
+end;
+
+procedure TGUIScroll.FSetValue(a: Integer);
+begin
+  if a > FMax then FValue := FMax else FValue := a;
+end;
+
+function TGUIScroll.GetWidth: Word;
+begin
+  Result := 16+(FMax+1)*8;
+end;
+
+procedure TGUIScroll.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  case Msg.Msg of
+    WM_KEYDOWN:
+    begin
+      case Msg.wParam of
+        IK_LEFT:
+          if FValue > 0 then
+          begin
+            Dec(FValue);
+            g_Sound_PlayEx(SCROLL_SUBSOUND);
+            if @FOnChangeEvent <> nil then FOnChangeEvent(Self);
+          end;
+        IK_RIGHT:
+          if FValue < FMax then
+          begin
+            Inc(FValue);
+            g_Sound_PlayEx(SCROLL_ADDSOUND);
+            if @FOnChangeEvent <> nil then FOnChangeEvent(Self);
+          end;
+      end;
+    end;
+  end;
+end;
+
+procedure TGUIScroll.Update;
+begin
+  inherited;
+
+end;
+
+{ TGUISwitch }
+
+procedure TGUISwitch.AddItem(Item: string);
+begin
+  SetLength(FItems, Length(FItems)+1);
+  FItems[High(FItems)] := Item;
+
+  if FIndex = -1 then FIndex := 0;
+end;
+
+constructor TGUISwitch.Create(FontID: DWORD);
+begin
+  inherited Create();
+
+  FIndex := -1;
+
+  FFont := TFont.Create(FontID, FONT_CHAR);
+end;
+
+procedure TGUISwitch.Draw;
+begin
+  inherited;
+
+  FFont.Draw(FX, FY, FItems[FIndex], FColor.R, FColor.G, FColor.B);
+end;
+
+function TGUISwitch.GetText: string;
+begin
+  if FIndex <> -1 then Result := FItems[FIndex]
+  else Result := '';
+end;
+
+function TGUISwitch.GetWidth: Word;
+var
+  a: Integer;
+  w, h: Word;
+begin
+  Result := 0;
+
+  if FItems = nil then Exit;
+
+  for a := 0 to High(FItems) do
+  begin
+    FFont.GetTextSize(FItems[a], w, h);
+    if w > Result then Result := w;
+  end;
+end;
+
+procedure TGUISwitch.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  if FItems = nil then Exit;
+
+  case Msg.Msg of
+    WM_KEYDOWN:
+      case Msg.wParam of
+        IK_RETURN, IK_RIGHT:
+        begin
+          if FIndex < High(FItems) then
+            Inc(FIndex)
+          else
+            FIndex := 0;
+
+          if @FOnChangeEvent <> nil then
+            FOnChangeEvent(Self);
+        end;
+
+    IK_LEFT:
+      begin
+        if FIndex > 0 then
+          Dec(FIndex)
+        else
+          FIndex := High(FItems);
+
+        if @FOnChangeEvent <> nil then
+          FOnChangeEvent(Self);
+      end;
+    end;
+  end;
+end;
+
+procedure TGUISwitch.Update;
+begin
+  inherited;
+
+end;
+
+{ TGUIEdit }
+
+constructor TGUIEdit.Create(FontID: DWORD);
+begin
+  inherited Create();
+
+  FFont := TFont.Create(FontID, FONT_CHAR);
+
+  FMaxLength := 0;
+  FWidth := 0;
+
+  g_Texture_Get(EDIT_LEFT, FLeftID);
+  g_Texture_Get(EDIT_RIGHT, FRightID);
+  g_Texture_Get(EDIT_MIDDLE, FMiddleID);
+end;
+
+procedure TGUIEdit.Draw;
+var
+  c, w, h: Word;
+begin
+  inherited;
+
+  e_Draw(FLeftID, FX, FY, 0, True, False);
+  e_Draw(FRightID, FX+8+FWidth*16, FY, 0, True, False);
+
+  for c := 0 to FWidth-1 do
+    e_Draw(FMiddleID, FX+8+c*16, FY, 0, True, False);
+
+  FFont.Draw(FX+8, FY, FText, FColor.R, FColor.G, FColor.B);
+
+  if FWindow.FActiveControl = Self then
+  begin
+    FFont.GetTextSize(Copy(FText, 1, FCaretPos), w, h);
+    h := e_CharFont_GetMaxHeight(FFont.ID);
+    e_DrawLine(2, FX+8+w, FY+h-3, FX+8+w+EDIT_CURSORLEN, FY+h-3,
+               EDIT_CURSORCOLOR.R, EDIT_CURSORCOLOR.G, EDIT_CURSORCOLOR.B);
+  end;
+end;
+
+function TGUIEdit.GetWidth: Word;
+begin
+  Result := 16+FWidth*16;
+end;
+
+procedure TGUIEdit.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  with Msg do
+    case Msg of
+      WM_CHAR:
+        if FOnlyDigits then
+        begin
+          if (wParam in [48..57]) and (Chr(wParam) <> '`') then
+            if Length(Text) < FMaxLength then
+            begin
+              Insert(Chr(wParam), FText, FCaretPos + 1);
+              Inc(FCaretPos);
+            end;
+        end
+        else
+        begin
+          if (wParam in [32..255]) and (Chr(wParam) <> '`') then
+            if Length(Text) < FMaxLength then
+            begin
+              Insert(Chr(wParam), FText, FCaretPos + 1);
+              Inc(FCaretPos);
+            end;
+        end;
+      WM_KEYDOWN:
+        case wParam of
+          IK_BACKSPACE:
+          begin
+            Delete(FText, FCaretPos, 1);
+            if FCaretPos > 0 then Dec(FCaretPos);
+          end;
+          IK_DELETE: Delete(FText, FCaretPos + 1, 1);
+          IK_END: FCaretPos := Length(FText);
+          IK_HOME: FCaretPos := 0;
+          IK_LEFT: if FCaretPos > 0 then Dec(FCaretPos);
+          IK_RIGHT: if FCaretPos < Length(FText) then Inc(FCaretPos);
+          IK_RETURN:
+            with FWindow do
+            begin
+              if FActiveControl <> Self then
+              begin
+                SetActive(Self);
+                if @FOnEnterEvent <> nil then FOnEnterEvent(Self);
+              end
+              else
+              begin
+                if FDefControl <> '' then SetActive(GetControl(FDefControl))
+                else SetActive(nil);
+                if @FOnChangeEvent <> nil then FOnChangeEvent(Self);
+              end;
+            end;
+        end;
+    end;
+end;
+
+procedure TGUIEdit.SetText(Text: string);
+begin
+  if Length(Text) > FMaxLength then SetLength(Text, FMaxLength);
+  FText := Text;
+  FCaretPos := Length(FText);
+end;
+
+procedure TGUIEdit.Update;
+begin
+  inherited;
+end;
+
+{ TGUIKeyRead }
+
+constructor TGUIKeyRead.Create(FontID: DWORD);
+begin
+  inherited Create();
+
+  FFont := TFont.Create(FontID, FONT_CHAR);
+end;
+
+procedure TGUIKeyRead.Draw;
+begin
+  inherited;
+
+  FFont.Draw(FX, FY, IfThen(FIsQuery, KEYREAD_QUERY, IfThen(FKey <> 0, e_KeyNames[FKey], KEYREAD_CLEAR)),
+             FColor.R, FColor.G, FColor.B);
+end;
+
+function TGUIKeyRead.GetWidth: Word;
+var
+  a: Byte;
+  w, h: Word;
+begin
+  Result := 0;
+  for a := 0 to 255 do
+  begin
+    FFont.GetTextSize(e_KeyNames[a], w, h);
+    Result := Max(Result, w);
+  end;
+
+  FFont.GetTextSize(KEYREAD_QUERY, w, h);
+  if w > Result then Result := w;
+
+  FFont.GetTextSize(KEYREAD_CLEAR, w, h);
+  if w > Result then Result := w;
+end;
+
+procedure TGUIKeyRead.OnMessage(var Msg: TMessage);
+begin
+  inherited;
+
+  if not FEnabled then
+    Exit;
+
+  with Msg do
+    case Msg of
+      WM_KEYDOWN:
+        case wParam of
+          IK_ESCAPE:
+            begin
+              if FIsQuery then
+                with FWindow do
+                  if FDefControl <> '' then
+                    SetActive(GetControl(FDefControl))
+                  else
+                    SetActive(nil);
+
+              FIsQuery := False;
+            end;
+          IK_RETURN:
+            begin
+              if not FIsQuery then
+                begin
+                  with FWindow do
+                    if FActiveControl <> Self then
+                      SetActive(Self);
+
+                  FIsQuery := True;
+                end
+              else
+                begin
+                  FKey := IK_ENTER; // <Enter>
+                  FIsQuery := False;
+                  
+                  with FWindow do
+                    if FDefControl <> '' then
+                      SetActive(GetControl(FDefControl))
+                    else
+                      SetActive(nil);
+                end;   
+            end;
+        end;
+
+      MESSAGE_DIKEY:
+        if FIsQuery and (wParam <> IK_ENTER) then // Not <Enter
+        begin
+          if e_KeyNames[wParam] <> '' then
+            FKey := wParam;
+          FIsQuery := False;
+
+          with FWindow do
+            if FDefControl <> '' then
+              SetActive(GetControl(FDefControl))
+            else
+              SetActive(nil);
+        end;
+    end;
+end;
+
+{ TGUIModelView }
+
+constructor TGUIModelView.Create;
+begin
+  inherited Create();
+
+  FModel := nil;
+end;
+
+destructor TGUIModelView.Destroy;
+begin
+  FModel.Free();
+
+  inherited;
+end;
+
+procedure TGUIModelView.Draw;
+begin
+  inherited;
+
+  DrawBox(FX, FY, 4, 4);
+
+  if FModel <> nil then FModel.Draw(FX+4, FY+4); 
+end;
+
+procedure TGUIModelView.NextAnim();
+begin
+  if FModel = nil then
+    Exit;
+
+  if FModel.Animation < A_PAIN then
+    FModel.ChangeAnimation(FModel.Animation+1, True)
+  else
+    FModel.ChangeAnimation(A_STAND, True);
+end;
+
+procedure TGUIModelView.NextWeapon();
+begin
+  if FModel = nil then
+    Exit;
+
+  if FModel.Weapon < WEAPON_SUPERPULEMET then
+    FModel.SetWeapon(FModel.Weapon+1)
+  else
+    FModel.SetWeapon(WEAPON_KASTET);
+end;
+
+procedure TGUIModelView.OnMessage(var Msg: TMessage);
+begin
+  inherited;
+
+end;
+
+procedure TGUIModelView.SetColor(Red, Green, Blue: Byte);
+begin
+  if FModel <> nil then FModel.SetColor(Red, Green, Blue); 
+end;
+
+procedure TGUIModelView.SetModel(ModelName: string);
+begin
+  FModel.Free();
+
+  FModel := g_PlayerModel_Get(ModelName);
+end;
+
+procedure TGUIModelView.Update;
+begin
+  inherited;
+
+  a := not a;
+  if a then Exit;
+  if FModel <> nil then FModel.Update;
+end;
+
+{ TGUIMapPreview }
+
+constructor TGUIMapPreview.Create();
+begin
+  inherited Create();
+  ClearMap;
+end;
+
+destructor TGUIMapPreview.Destroy();
+begin
+  ClearMap;
+  inherited;
+end;
+
+procedure TGUIMapPreview.Draw();
+var
+  a: Integer;
+  r, g, b: Byte;
+begin
+  inherited;
+
+  DrawBox(FX, FY, MAPPREVIEW_WIDTH, MAPPREVIEW_HEIGHT);
+
+  if (FMapSize.X <= 0) or (FMapSize.Y <= 0) then
+    Exit;
+
+  e_DrawFillQuad(FX+4, FY+4,
+    FX+4 + Trunc(FMapSize.X / FScale) - 1,
+    FY+4 + Trunc(FMapSize.Y / FScale) - 1,
+    32, 32, 32, 0);     
+
+  if FMapData <> nil then
+    for a := 0 to High(FMapData) do
+      with FMapData[a] do
+      begin
+        if X1 > MAPPREVIEW_WIDTH*16 then Continue;
+        if Y1 > MAPPREVIEW_HEIGHT*16 then Continue;
+
+        if X2 < 0 then Continue;
+        if Y2 < 0 then Continue;
+     
+        if X2 > MAPPREVIEW_WIDTH*16 then X2 := MAPPREVIEW_WIDTH*16;
+        if Y2 > MAPPREVIEW_HEIGHT*16 then Y2 := MAPPREVIEW_HEIGHT*16;
+
+        if X1 < 0 then X1 := 0;
+        if Y1 < 0 then Y1 := 0;
+
+        case PanelType of
+          PANEL_WALL:
+            begin
+              r := 255;
+              g := 255;
+              b := 255;
+            end;
+          PANEL_CLOSEDOOR:
+            begin
+              r := 255;
+              g := 255;
+              b := 0;
+            end;
+          PANEL_WATER:
+            begin
+              r := 0;
+              g := 0;
+              b := 192;
+            end;
+          PANEL_ACID1:
+            begin
+              r := 0;
+              g := 176;
+              b := 0;
+            end;
+          PANEL_ACID2:
+            begin
+              r := 176;
+              g := 0;
+              b := 0;
+            end;
+          else
+            begin
+              r := 128;
+              g := 128;
+              b := 128;
+            end;
+        end;
+
+        if ((X2-X1) > 0) and ((Y2-Y1) > 0) then
+          e_DrawFillQuad(FX+4 + X1, FY+4 + Y1,
+            FX+4 + X2 - 1, FY+4 + Y2 - 1, r, g, b, 0);
+      end;
+end;
+
+procedure TGUIMapPreview.OnMessage(var Msg: TMessage);
+begin
+  inherited;
+
+end;
+
+procedure TGUIMapPreview.SetMap(Res: string);
+var
+  WAD: TWADEditor_1;
+  MapReader: TMapReader_1;
+  panels: TPanelsRec1Array;
+  header: TMapHeaderRec_1;
+  a: Integer;
+  FileName, SectionName, ResName: string;
+  Data: Pointer;
+  Len: Integer;
+  rX, rY: Single;
+begin
+  g_ProcessResourceStr(Res, FileName, SectionName, ResName);
+
+  WAD := TWADEditor_1.Create();
+  if not WAD.ReadFile(FileName) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  if not WAD.GetResource('', ResName, Data, Len) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  WAD.Free();
+
+  MapReader := TMapReader_1.Create();
+
+  if not MapReader.LoadMap(Data) then
+  begin
+    FreeMem(Data);
+    MapReader.Free();
+    FMapSize.X := 0;
+    FMapSize.Y := 0;
+    FScale := 0.0;
+    FMapData := nil;
+    Exit;
+  end;
+
+  FreeMem(Data);
+
+  panels := MapReader.GetPanels();
+  header := MapReader.GetMapHeader();
+
+  FMapSize.X := header.Width div 16;
+  FMapSize.Y := header.Height div 16;
+
+  rX := Ceil(header.Width / (MAPPREVIEW_WIDTH*256.0));
+  rY := Ceil(header.Height / (MAPPREVIEW_HEIGHT*256.0));
+  FScale := max(rX, rY);
+
+  FMapData := nil;
+
+  if panels <> nil then
+    for a := 0 to High(panels) do
+      if WordBool(panels[a].PanelType and (PANEL_WALL or PANEL_CLOSEDOOR or
+                                           PANEL_STEP or PANEL_WATER or
+                                           PANEL_ACID1 or PANEL_ACID2)) then
+      begin
+        SetLength(FMapData, Length(FMapData)+1);
+        with FMapData[High(FMapData)] do
+        begin
+          X1 := panels[a].X div 16;
+          Y1 := panels[a].Y div 16;
+
+          X2 := (panels[a].X + panels[a].Width) div 16;
+          Y2 := (panels[a].Y + panels[a].Height) div 16;
+
+          X1 := Trunc(X1/FScale + 0.5);
+          Y1 := Trunc(Y1/FScale + 0.5);
+          X2 := Trunc(X2/FScale + 0.5);
+          Y2 := Trunc(Y2/FScale + 0.5);
+
+          if (X1 <> X2) or (Y1 <> Y2) then
+          begin
+            if X1 = X2 then
+              X2 := X2 + 1;
+            if Y1 = Y2 then
+              Y2 := Y2 + 1;
+          end;
+
+          PanelType := panels[a].PanelType;
+      end;
+   end;
+
+  panels := nil;
+
+  MapReader.Free();
+end;
+
+procedure TGUIMapPreview.ClearMap();
+begin
+  SetLength(FMapData, 0);
+  FMapData := nil;
+  FMapSize.X := 0;
+  FMapSize.Y := 0;
+  FScale := 0.0;
+end;
+
+procedure TGUIMapPreview.Update();
+begin
+  inherited;
+
+end;
+
+function TGUIMapPreview.GetScaleStr(): String;
+begin
+  if FScale > 0.0 then
+    begin
+      Result := FloatToStrF(FScale*16.0, ffFixed, 3, 3);
+      while (Result[Length(Result)] = '0') do
+        Delete(Result, Length(Result), 1);
+      if (Result[Length(Result)] = ',') or (Result[Length(Result)] = '.') then
+        Delete(Result, Length(Result), 1);
+      Result := '1 : ' + Result;
+    end
+  else
+    Result := '';
+end;
+
+{ TGUIListBox }
+
+procedure TGUIListBox.AddItem(Item: string);
+begin
+  SetLength(FItems, Length(FItems)+1);
+  FItems[High(FItems)] := Item;
+
+  if FSort then g_Basic.Sort(FItems);
+end;
+
+procedure TGUIListBox.Clear();
+begin
+  FItems := nil;
+
+  FStartLine := 0;
+  FIndex := -1;
+end;
+
+constructor TGUIListBox.Create(FontID: DWORD; Width, Height: Word);
+begin
+  inherited Create();
+
+  FFont := TFont.Create(FontID, FONT_CHAR);
+
+  FWidth := Width;
+  FHeight := Height;
+  FIndex := -1;
+  FOnChangeEvent := nil;
+  FDrawBack := True;
+  FDrawScroll := True;
+end;
+
+procedure TGUIListBox.Draw;
+var
+  w2, h2: Word;
+  a: Integer;
+  s: string;
+begin
+  inherited;
+
+  if FDrawBack then DrawBox(FX, FY, FWidth+1, FHeight);
+  if FDrawScroll then
+    DrawScroll(FX+4+FWidth*16, FY+4, FHeight, (FStartLine > 0) and (FItems <> nil),
+              (FStartLine+FHeight-1 < High(FItems)) and (FItems <> nil));
+
+  if FItems <> nil then
+    for a := FStartLine to Min(High(FItems), FStartLine+FHeight-1) do
+    begin
+      s := Items[a];
+
+      FFont.GetTextSize(s, w2, h2);
+      while (Length(s) > 0) and (w2 > FWidth*16) do
+      begin
+        SetLength(s, Length(s)-1);
+        FFont.GetTextSize(s, w2, h2);
+      end;
+
+      if a = FIndex then
+        FFont.Draw(FX+4, FY+4+(a-FStartLine)*16, s, FActiveColor.R, FActiveColor.G, FActiveColor.B)
+      else
+        FFont.Draw(FX+4, FY+4+(a-FStartLine)*16, s, FUnActiveColor.R, FUnActiveColor.G, FUnActiveColor.B);
+    end;
+end;
+
+function TGUIListBox.GetHeight: Word;
+begin
+  Result := 8+FHeight*16; 
+end;
+
+function TGUIListBox.GetWidth: Word;
+begin
+  Result := 8+(FWidth+1)*16; 
+end;
+
+procedure TGUIListBox.OnMessage(var Msg: TMessage);
+var
+  a: Integer;
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  if FItems = nil then Exit;
+
+  with Msg do
+    case Msg of
+      WM_KEYDOWN:
+        case wParam of
+          IK_HOME:
+          begin
+            FIndex := 0;
+            FStartLine := 0;
+          end;
+          IK_END:
+          begin
+            FIndex := High(FItems);
+            FStartLine := Max(High(FItems)-FHeight+1, 0);
+          end;
+          IK_UP, IK_LEFT:
+            if FIndex > 0 then
+            begin
+              Dec(FIndex);
+              if FIndex < FStartLine then Dec(FStartLine);
+              if @FOnChangeEvent <> nil then FOnChangeEvent(Self);
+            end;
+          IK_DOWN, IK_RIGHT:
+            if FIndex < High(FItems) then
+            begin
+              Inc(FIndex);
+              if FIndex > FStartLine+FHeight-1 then Inc(FStartLine);
+              if @FOnChangeEvent <> nil then FOnChangeEvent(Self);
+            end;
+          IK_RETURN:
+            with FWindow do
+            begin
+              if FActiveControl <> Self then SetActive(Self)
+              else
+                if FDefControl <> '' then SetActive(GetControl(FDefControl))
+                else SetActive(nil);
+            end;
+        end;
+      WM_CHAR:
+        for a := 0 to High(FItems) do
+          if (Length(FItems[a]) > 0) and (LowerCase(FItems[a][1]) = LowerCase(Chr(wParam))) then
+          begin
+            FIndex := a;
+            FStartLine := Min(Max(FIndex-1, 0), Length(FItems)-FHeight);
+            if @FOnChangeEvent <> nil then FOnChangeEvent(Self);
+            Break;
+          end;
+    end;
+end;
+
+function TGUIListBox.SelectedItem(): String;
+begin
+  Result := '';
+
+  if (FIndex < 0) or (FItems = nil) or
+     (FIndex > High(FItems)) then
+    Exit;
+
+  Result := FItems[FIndex];
+end;
+
+procedure TGUIListBox.FSetItems(Items: SArray);
+begin
+  if FItems <> nil then
+    FItems := nil;
+   
+  FItems := Items;
+
+  FStartLine := 0;
+  FIndex := -1;
+
+  if FSort then g_Basic.Sort(FItems);
+end;
+
+procedure TGUIListBox.SelectItem(Item: String);
+var
+  a: Integer;
+begin
+  if FItems = nil then
+    Exit;
+
+  FIndex := 0;
+  Item := LowerCase(Item);
+
+  for a := 0 to High(FItems) do
+    if LowerCase(FItems[a]) = Item then
+    begin
+      FIndex := a;
+      Break;
+    end;
+
+  if FIndex < FHeight then
+    FStartLine := 0
+  else
+    FStartLine := Min(FIndex, Length(FItems)-FHeight);
+end;
+
+procedure TGUIListBox.FSetIndex(aIndex: Integer);
+begin
+  if FItems = nil then
+    Exit;
+
+  if (aIndex < 0) or (aIndex > High(FItems)) then
+    Exit;
+
+  FIndex := aIndex;
+
+  if FIndex <= FHeight then
+    FStartLine := 0
+  else
+    FStartLine := Min(FIndex, Length(FItems)-FHeight);
+end;
+
+{ TGUIFileListBox }
+
+procedure TGUIFileListBox.OnMessage(var Msg: TMessage);
+var
+  a: Integer;
+begin
+  if not FEnabled then
+    Exit;
+
+  if FItems = nil then
+    Exit;
+
+  with Msg do
+    case Msg of
+      WM_KEYDOWN:
+        case wParam of
+          IK_HOME:
+            begin
+              FIndex := 0;
+              FStartLine := 0;
+              if @FOnChangeEvent <> nil then
+                FOnChangeEvent(Self);
+            end;
+
+          IK_END:
+            begin
+              FIndex := High(FItems);
+              FStartLine := Max(High(FItems)-FHeight+1, 0);
+              if @FOnChangeEvent <> nil then
+                FOnChangeEvent(Self);
+            end;
+
+          IK_PAGEUP:
+            begin
+              if FIndex > FHeight then
+                FIndex := FIndex-FHeight
+              else
+                FIndex := 0;
+
+              if FStartLine > FHeight then
+                FStartLine := FStartLine-FHeight
+              else
+                FStartLine := 0;
+            end;
+
+          IK_PAGEDN:
+            begin
+              if FIndex < High(FItems)-FHeight then
+                FIndex := FIndex+FHeight
+              else
+                FIndex := High(FItems);
+
+              if FStartLine < High(FItems)-FHeight then
+                FStartLine := FStartLine+FHeight
+              else
+                FStartLine := High(FItems)-FHeight+1;
+            end;
+
+          IK_UP, IK_LEFT:
+            if FIndex > 0 then
+            begin
+              Dec(FIndex);
+              if FIndex < FStartLine then
+                Dec(FStartLine);
+              if @FOnChangeEvent <> nil then
+                FOnChangeEvent(Self);
+            end;
+
+          IK_DOWN, IK_RIGHT:
+            if FIndex < High(FItems) then
+            begin
+              Inc(FIndex);
+              if FIndex > FStartLine+FHeight-1 then
+                Inc(FStartLine);
+              if @FOnChangeEvent <> nil then
+                FOnChangeEvent(Self);
+            end;
+
+          IK_RETURN:
+            with FWindow do
+            begin
+              if FActiveControl <> Self then
+                SetActive(Self)
+              else
+                begin
+                  if FItems[FIndex][1] = #29 then // Ïàïêà
+                  begin
+                    OpenDir(FPath+Copy(FItems[FIndex], 2, 255));
+                    FIndex := 0;
+                    Exit;
+                  end;
+
+                  if FDefControl <> '' then
+                    SetActive(GetControl(FDefControl))
+                  else
+                    SetActive(nil);
+                end;
+            end;
+        end;
+
+      WM_CHAR:
+        for a := 0 to High(FItems) do
+          if ( (Length(FItems[a]) > 0) and
+               (LowerCase(FItems[a][1]) = LowerCase(Chr(wParam))) ) or
+             ( (Length(FItems[a]) > 1) and
+               (FItems[a][1] = #29) and // Ïàïêà
+               (LowerCase(FItems[a][2]) = LowerCase(Chr(wParam))) ) then
+          begin
+            FIndex := a;
+            FStartLine := Min(Max(FIndex-1, 0), Length(FItems)-FHeight);
+            if @FOnChangeEvent <> nil then
+              FOnChangeEvent(Self);
+            Break;
+          end;
+    end;
+end;
+
+procedure TGUIFileListBox.OpenDir(path: String);
+var
+  SR: TSearchRec;
+  i: Integer;
+begin
+  Clear();
+
+  path := IncludeTrailingPathDelimiter(path);
+  path := ExpandFileName(path);
+
+// Êàòàëîãè:
+  if FDirs then
+  begin
+    if FindFirst(path+'*', faDirectory, SR) = 0 then
+    repeat
+      if not LongBool(SR.Attr and faDirectory) then
+        Continue;
+      if (SR.Name = '.') or
+         ((SR.Name = '..') and (path = ExpandFileName(FBasePath))) then
+        Continue;
+
+      AddItem(#1 + SR.Name);
+    until FindNext(SR) <> 0;
+
+    FindClose(SR);
+  end;
+
+// Ôàéëû:
+  if FindFirst(path+FFileMask, faAnyFile, SR) = 0 then
+    repeat
+      AddItem(SR.Name);
+    until FindNext(SR) <> 0;
+
+  FindClose(SR);
+
+  for i := 0 to High(FItems) do
+    if FItems[i][1] = #1 then
+      FItems[i][1] := #29;
+
+  FPath := path;
+end;
+
+procedure TGUIFileListBox.SetBase(path: String);
+begin
+  FBasePath := path;
+  OpenDir(FBasePath);
+end;
+
+function TGUIFileListBox.SelectedItem(): String;
+begin
+  Result := '';
+
+  if (FIndex = -1) or (FItems = nil) or
+     (FIndex > High(FItems)) or
+     (FItems[FIndex][1] = '/') or
+     (FItems[FIndex][1] = '\') then
+    Exit;
+
+  Result := FPath + FItems[FIndex];
+end;
+
+procedure TGUIFileListBox.UpdateFileList();
+var
+  fn: String;
+begin
+  if (FIndex = -1) or (FItems = nil) or
+     (FIndex > High(FItems)) or
+     (FItems[FIndex][1] = '/') or 
+     (FItems[FIndex][1] = '\') then
+    fn := ''
+  else
+    fn := FItems[FIndex];
+
+  OpenDir(FPath);
+
+  if fn <> '' then
+    SelectItem(fn);
+end;
+
+{ TGUIMemo }
+
+procedure TGUIMemo.Clear;
+begin
+  FLines := nil;
+  FStartLine := 0;
+end;
+
+constructor TGUIMemo.Create(FontID: DWORD; Width, Height: Word);
+begin
+  inherited Create();
+
+  FFont := TFont.Create(FontID, FONT_CHAR);
+
+  FWidth := Width;
+  FHeight := Height;
+  FDrawBack := True;
+  FDrawScroll := True;
+end;
+
+procedure TGUIMemo.Draw;
+var
+  a: Integer;
+begin
+  inherited;
+
+  if FDrawBack then DrawBox(FX, FY, FWidth+1, FHeight);
+  if FDrawScroll then
+    DrawScroll(FX+4+FWidth*16, FY+4, FHeight, (FStartLine > 0) and (FLines <> nil),
+              (FStartLine+FHeight-1 < High(FLines)) and (FLines <> nil));
+
+  if FLines <> nil then
+    for a := FStartLine to Min(High(FLines), FStartLine+FHeight-1) do
+      FFont.Draw(FX+4, FY+4+(a-FStartLine)*16, FLines[a], FColor.R, FColor.G, FColor.B);
+end;
+
+function TGUIMemo.GetHeight: Word;
+begin
+  Result := 8+FHeight*16;
+end;
+
+function TGUIMemo.GetWidth: Word;
+begin
+  Result := 8+(FWidth+1)*16;
+end;
+
+procedure TGUIMemo.OnMessage(var Msg: TMessage);
+begin
+  if not FEnabled then Exit;
+
+  inherited;
+
+  if FLines = nil then Exit;
+
+  with Msg do
+    case Msg of
+      WM_KEYDOWN:
+        case wParam of
+          IK_UP, IK_LEFT:
+            if FStartLine > 0 then
+              Dec(FStartLine);
+          IK_DOWN, IK_RIGHT:
+            if FStartLine < Length(FLines)-FHeight then
+              Inc(FStartLine);
+          IK_RETURN:
+            with FWindow do
+            begin
+              if FActiveControl <> Self then
+              begin
+                SetActive(Self);
+                {FStartLine := 0;}
+              end
+              else
+              if FDefControl <> '' then SetActive(GetControl(FDefControl))
+                else SetActive(nil);
+            end;
+        end;
+    end;
+end;
+
+procedure TGUIMemo.SetText(Text: string);
+begin
+  FStartLine := 0;
+  FLines := GetLines(Text, FFont.ID, FWidth*16);
+end;
+
+{ TGUIimage }
+
+procedure TGUIimage.ClearImage();
+begin
+  if FImageRes = '' then Exit;
+
+  g_Texture_Delete(FImageRes);
+  FImageRes := '';
+end;
+
+constructor TGUIimage.Create();
+begin
+  inherited Create();
+
+  FImageRes := '';
+end;
+
+destructor TGUIimage.Destroy();
+begin
+  inherited;
+end;
+
+procedure TGUIimage.Draw();
+var
+  ID: DWORD;
+begin
+  inherited;
+
+  if FImageRes = '' then
+  begin   
+    if g_Texture_Get(FDefaultRes, ID) then e_Draw(ID, FX, FY, 0, True, False);
+  end
+  else
+    if g_Texture_Get(FImageRes, ID) then e_Draw(ID, FX, FY, 0, True, False);
+end;
+
+procedure TGUIimage.OnMessage(var Msg: TMessage);
+begin
+  inherited;
+end;
+
+procedure TGUIimage.SetImage(Res: string);
+begin
+  ClearImage();
+
+  if g_Texture_CreateWADEx(Res, Res) then FImageRes := Res;
+end;
+
+procedure TGUIimage.Update();
+begin
+  inherited;
+end;
+
+end.
diff --git a/src/game/g_items.pas b/src/game/g_items.pas
new file mode 100644 (file)
index 0000000..e912c42
--- /dev/null
@@ -0,0 +1,656 @@
+unit g_items;
+
+interface
+
+uses
+  g_textures, g_phys, g_saveload, BinEditor;
+
+Type
+  TItem = record
+    ItemType:      Byte;
+    Respawnable:   Boolean;
+    InitX, InitY:  Integer;
+    RespawnTime:   Word;
+    Live:          Boolean;
+    Fall:          Boolean;
+    QuietRespawn:  Boolean;
+    SpawnTrigger:  Integer;
+    Obj:           TObj;
+    Animation:     TAnimation;
+  end;
+
+procedure g_Items_LoadData();
+procedure g_Items_FreeData();
+procedure g_Items_Init();
+procedure g_Items_Free();
+function g_Items_Create(X, Y: Integer; ItemType: Byte;
+           Fall, Respawnable: Boolean; AdjCoord: Boolean = False; ForcedID: Integer = -1): DWORD;
+procedure g_Items_Update();
+procedure g_Items_Draw();
+procedure g_Items_Pick(ID: DWORD);
+procedure g_Items_Remove(ID: DWORD);
+procedure g_Items_SaveState(var Mem: TBinMemoryWriter);
+procedure g_Items_LoadState(var Mem: TBinMemoryReader);
+
+var
+  gItems: Array of TItem = nil;
+  gItemsTexturesID: Array [1..35] of DWORD;
+  gMaxDist: Integer = 1;
+  ITEM_RESPAWNTIME: Integer = 60 * 36;
+
+implementation
+
+uses
+  g_basic, e_graphics, g_sound, g_main, g_gfx, g_map,
+  Math, g_game, g_triggers, g_console, SysUtils, g_player, g_net, g_netmsg,
+  MAPDEF, e_log;
+
+const
+  ITEM_SIGNATURE = $4D455449; // 'ITEM'
+
+  ITEMSIZE: Array [ITEM_MEDKIT_SMALL..ITEM_MAX] of Array [0..1] of Byte =
+    (((14), (15)), // MEDKIT_SMALL
+     ((28), (19)), // MEDKIT_LARGE
+     ((28), (19)), // MEDKIT_BLACK
+     ((31), (16)), // ARMOR_GREEN
+     ((31), (16)), // ARMOR_BLUE
+     ((25), (25)), // SPHERE_BLUE
+     ((25), (25)), // SPHERE_WHITE
+     ((24), (47)), // SUIT
+     ((14), (27)), // OXYGEN
+     ((25), (25)), // INVUL
+     ((62), (24)), // WEAPON_SAW
+     ((63), (12)), // WEAPON_SHOTGUN1
+     ((54), (13)), // WEAPON_SHOTGUN2
+     ((54), (16)), // WEAPON_CHAINGUN
+     ((62), (16)), // WEAPON_ROCKETLAUNCHER
+     ((54), (16)), // WEAPON_PLASMA
+     ((61), (36)), // WEAPON_BFG
+     ((54), (16)), // WEAPON_SUPERPULEMET
+     (( 9), (11)), // AMMO_BULLETS
+     ((28), (16)), // AMMO_BULLETS_BOX
+     ((15), ( 7)), // AMMO_SHELLS
+     ((32), (12)), // AMMO_SHELLS_BOX
+     ((12), (27)), // AMMO_ROCKET
+     ((54), (21)), // AMMO_ROCKET_BOX
+     ((15), (12)), // AMMO_CELL
+     ((32), (21)), // AMMO_CELL_BIG
+     ((22), (29)), // AMMO_BACKPACK
+     ((16), (16)), // KEY_RED
+     ((16), (16)), // KEY_GREEN
+     ((16), (16)), // KEY_BLUE
+     (( 1), ( 1)), // WEAPON_KASTET
+     ((43), (16)), // WEAPON_PISTOL
+     ((14), (18)), // BOTTLE
+     ((16), (15)), // HELMET
+     ((32), (24)), // JETPACK
+     ((25), (25))); // INVIS
+
+procedure InitTextures();
+begin
+  g_Texture_Get('ITEM_MEDKIT_SMALL',     gItemsTexturesID[ITEM_MEDKIT_SMALL]);
+  g_Texture_Get('ITEM_MEDKIT_LARGE',     gItemsTexturesID[ITEM_MEDKIT_LARGE]);
+  g_Texture_Get('ITEM_MEDKIT_BLACK',     gItemsTexturesID[ITEM_MEDKIT_BLACK]);
+  g_Texture_Get('ITEM_SUIT',             gItemsTexturesID[ITEM_SUIT]);
+  g_Texture_Get('ITEM_OXYGEN',           gItemsTexturesID[ITEM_OXYGEN]);
+  g_Texture_Get('ITEM_WEAPON_SAW',       gItemsTexturesID[ITEM_WEAPON_SAW]);
+  g_Texture_Get('ITEM_WEAPON_SHOTGUN1',  gItemsTexturesID[ITEM_WEAPON_SHOTGUN1]);
+  g_Texture_Get('ITEM_WEAPON_SHOTGUN2',  gItemsTexturesID[ITEM_WEAPON_SHOTGUN2]);
+  g_Texture_Get('ITEM_WEAPON_CHAINGUN',  gItemsTexturesID[ITEM_WEAPON_CHAINGUN]);
+  g_Texture_Get('ITEM_WEAPON_ROCKETLAUNCHER', gItemsTexturesID[ITEM_WEAPON_ROCKETLAUNCHER]);
+  g_Texture_Get('ITEM_WEAPON_PLASMA',    gItemsTexturesID[ITEM_WEAPON_PLASMA]);
+  g_Texture_Get('ITEM_WEAPON_BFG',       gItemsTexturesID[ITEM_WEAPON_BFG]);
+  g_Texture_Get('ITEM_WEAPON_SUPERPULEMET', gItemsTexturesID[ITEM_WEAPON_SUPERPULEMET]);
+  g_Texture_Get('ITEM_AMMO_BULLETS',     gItemsTexturesID[ITEM_AMMO_BULLETS]);
+  g_Texture_Get('ITEM_AMMO_BULLETS_BOX', gItemsTexturesID[ITEM_AMMO_BULLETS_BOX]);
+  g_Texture_Get('ITEM_AMMO_SHELLS',      gItemsTexturesID[ITEM_AMMO_SHELLS]);
+  g_Texture_Get('ITEM_AMMO_SHELLS_BOX',  gItemsTexturesID[ITEM_AMMO_SHELLS_BOX]);
+  g_Texture_Get('ITEM_AMMO_ROCKET',      gItemsTexturesID[ITEM_AMMO_ROCKET]);
+  g_Texture_Get('ITEM_AMMO_ROCKET_BOX',  gItemsTexturesID[ITEM_AMMO_ROCKET_BOX]);
+  g_Texture_Get('ITEM_AMMO_CELL',        gItemsTexturesID[ITEM_AMMO_CELL]);
+  g_Texture_Get('ITEM_AMMO_CELL_BIG',    gItemsTexturesID[ITEM_AMMO_CELL_BIG]);
+  g_Texture_Get('ITEM_AMMO_BACKPACK',    gItemsTexturesID[ITEM_AMMO_BACKPACK]);
+  g_Texture_Get('ITEM_KEY_RED',          gItemsTexturesID[ITEM_KEY_RED]);
+  g_Texture_Get('ITEM_KEY_GREEN',        gItemsTexturesID[ITEM_KEY_GREEN]);
+  g_Texture_Get('ITEM_KEY_BLUE',         gItemsTexturesID[ITEM_KEY_BLUE]);
+  g_Texture_Get('ITEM_WEAPON_KASTET',    gItemsTexturesID[ITEM_WEAPON_KASTET]);
+  g_Texture_Get('ITEM_WEAPON_PISTOL',    gItemsTexturesID[ITEM_WEAPON_PISTOL]);
+  g_Texture_Get('ITEM_JETPACK',          gItemsTexturesID[ITEM_JETPACK]);
+end;
+
+procedure g_Items_LoadData();
+begin
+  e_WriteLog('Loading items data...', MSG_NOTIFY);
+
+  g_Sound_CreateWADEx('SOUND_ITEM_RESPAWNITEM', GameWAD+':SOUNDS\RESPAWNITEM');
+  g_Sound_CreateWADEx('SOUND_ITEM_GETRULEZ', GameWAD+':SOUNDS\GETRULEZ');
+  g_Sound_CreateWADEx('SOUND_ITEM_GETWEAPON', GameWAD+':SOUNDS\GETWEAPON');
+  g_Sound_CreateWADEx('SOUND_ITEM_GETITEM', GameWAD+':SOUNDS\GETITEM');
+
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_BLUESPHERE', GameWAD+':TEXTURES\SBLUE', 32, 32, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_WHITESPHERE', GameWAD+':TEXTURES\SWHITE', 32, 32, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_ARMORGREEN', GameWAD+':TEXTURES\ARMORGREEN', 32, 16, 3, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_ARMORBLUE', GameWAD+':TEXTURES\ARMORBLUE', 32, 16, 3, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_INVUL', GameWAD+':TEXTURES\INVUL', 32, 32, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_INVIS', GameWAD+':TEXTURES\INVIS', 32, 32, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_RESPAWN', GameWAD+':TEXTURES\ITEMRESPAWN', 32, 32, 5, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_BOTTLE', GameWAD+':TEXTURES\BOTTLE', 16, 32, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_ITEM_HELMET', GameWAD+':TEXTURES\HELMET', 16, 16, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_FLAG_RED', GameWAD+':TEXTURES\FLAGRED', 64, 64, 5, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_FLAG_BLUE', GameWAD+':TEXTURES\FLAGBLUE', 64, 64, 5, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_FLAG_DOM', GameWAD+':TEXTURES\FLAGDOM', 64, 64, 5, False);
+  g_Texture_CreateWADEx('ITEM_MEDKIT_SMALL', GameWAD+':TEXTURES\MED1');
+  g_Texture_CreateWADEx('ITEM_MEDKIT_LARGE', GameWAD+':TEXTURES\MED2');
+  g_Texture_CreateWADEx('ITEM_WEAPON_SAW', GameWAD+':TEXTURES\SAW');
+  g_Texture_CreateWADEx('ITEM_WEAPON_PISTOL', GameWAD+':TEXTURES\PISTOL');
+  g_Texture_CreateWADEx('ITEM_WEAPON_KASTET', GameWAD+':TEXTURES\KASTET');
+  g_Texture_CreateWADEx('ITEM_WEAPON_SHOTGUN1', GameWAD+':TEXTURES\SHOTGUN1');
+  g_Texture_CreateWADEx('ITEM_WEAPON_SHOTGUN2', GameWAD+':TEXTURES\SHOTGUN2');
+  g_Texture_CreateWADEx('ITEM_WEAPON_CHAINGUN', GameWAD+':TEXTURES\MGUN');
+  g_Texture_CreateWADEx('ITEM_WEAPON_ROCKETLAUNCHER', GameWAD+':TEXTURES\RLAUNCHER');
+  g_Texture_CreateWADEx('ITEM_WEAPON_PLASMA', GameWAD+':TEXTURES\PGUN');
+  g_Texture_CreateWADEx('ITEM_WEAPON_BFG', GameWAD+':TEXTURES\BFG');
+  g_Texture_CreateWADEx('ITEM_WEAPON_SUPERPULEMET', GameWAD+':TEXTURES\SPULEMET');
+  g_Texture_CreateWADEx('ITEM_AMMO_BULLETS', GameWAD+':TEXTURES\CLIP');
+  g_Texture_CreateWADEx('ITEM_AMMO_BULLETS_BOX', GameWAD+':TEXTURES\AMMO');
+  g_Texture_CreateWADEx('ITEM_AMMO_SHELLS', GameWAD+':TEXTURES\SHELL1');
+  g_Texture_CreateWADEx('ITEM_AMMO_SHELLS_BOX', GameWAD+':TEXTURES\SHELL2');
+  g_Texture_CreateWADEx('ITEM_AMMO_ROCKET', GameWAD+':TEXTURES\ROCKET');
+  g_Texture_CreateWADEx('ITEM_AMMO_ROCKET_BOX', GameWAD+':TEXTURES\ROCKETS');
+  g_Texture_CreateWADEx('ITEM_AMMO_CELL', GameWAD+':TEXTURES\CELL');
+  g_Texture_CreateWADEx('ITEM_AMMO_CELL_BIG', GameWAD+':TEXTURES\CELL2');
+  g_Texture_CreateWADEx('ITEM_AMMO_BACKPACK', GameWAD+':TEXTURES\BPACK');
+  g_Texture_CreateWADEx('ITEM_KEY_RED', GameWAD+':TEXTURES\KEYR');
+  g_Texture_CreateWADEx('ITEM_KEY_GREEN', GameWAD+':TEXTURES\KEYG');
+  g_Texture_CreateWADEx('ITEM_KEY_BLUE', GameWAD+':TEXTURES\KEYB');
+  g_Texture_CreateWADEx('ITEM_OXYGEN', GameWAD+':TEXTURES\OXYGEN');
+  g_Texture_CreateWADEx('ITEM_SUIT', GameWAD+':TEXTURES\SUIT');
+  g_Texture_CreateWADEx('ITEM_WEAPON_KASTET', GameWAD+':TEXTURES\KASTET');
+  g_Texture_CreateWADEx('ITEM_MEDKIT_BLACK', GameWAD+':TEXTURES\BMED');
+  g_Texture_CreateWADEx('ITEM_JETPACK', GameWAD+':TEXTURES\JETPACK');
+
+  InitTextures();
+end;
+
+procedure g_Items_FreeData();
+begin
+  e_WriteLog('Releasing items data...', MSG_NOTIFY);
+
+  g_Sound_Delete('SOUND_ITEM_RESPAWNITEM');
+  g_Sound_Delete('SOUND_ITEM_GETRULEZ');
+  g_Sound_Delete('SOUND_ITEM_GETWEAPON');
+  g_Sound_Delete('SOUND_ITEM_GETITEM');
+
+  g_Frames_DeleteByName('FRAMES_ITEM_BLUESPHERE');
+  g_Frames_DeleteByName('FRAMES_ITEM_WHITESPHERE');
+  g_Frames_DeleteByName('FRAMES_ITEM_ARMORGREEN');
+  g_Frames_DeleteByName('FRAMES_ITEM_ARMORBLUE');
+  g_Frames_DeleteByName('FRAMES_ITEM_INVUL');
+  g_Frames_DeleteByName('FRAMES_ITEM_INVIS');
+  g_Frames_DeleteByName('FRAMES_ITEM_RESPAWN');
+  g_Frames_DeleteByName('FRAMES_ITEM_BOTTLE');
+  g_Frames_DeleteByName('FRAMES_ITEM_HELMET');
+  g_Frames_DeleteByName('FRAMES_FLAG_RED');
+  g_Frames_DeleteByName('FRAMES_FLAG_BLUE');
+  g_Frames_DeleteByName('FRAMES_FLAG_DOM');
+  g_Texture_Delete('ITEM_MEDKIT_SMALL');
+  g_Texture_Delete('ITEM_MEDKIT_LARGE');
+  g_Texture_Delete('ITEM_WEAPON_SAW');
+  g_Texture_Delete('ITEM_WEAPON_PISTOL');
+  g_Texture_Delete('ITEM_WEAPON_KASTET');
+  g_Texture_Delete('ITEM_WEAPON_SHOTGUN1');
+  g_Texture_Delete('ITEM_WEAPON_SHOTGUN2');
+  g_Texture_Delete('ITEM_WEAPON_CHAINGUN');
+  g_Texture_Delete('ITEM_WEAPON_ROCKETLAUNCHER');
+  g_Texture_Delete('ITEM_WEAPON_PLASMA');
+  g_Texture_Delete('ITEM_WEAPON_BFG');
+  g_Texture_Delete('ITEM_WEAPON_SUPERPULEMET');
+  g_Texture_Delete('ITEM_AMMO_BULLETS');
+  g_Texture_Delete('ITEM_AMMO_BULLETS_BOX');
+  g_Texture_Delete('ITEM_AMMO_SHELLS');
+  g_Texture_Delete('ITEM_AMMO_SHELLS_BOX');
+  g_Texture_Delete('ITEM_AMMO_ROCKET');
+  g_Texture_Delete('ITEM_AMMO_ROCKET_BOX');
+  g_Texture_Delete('ITEM_AMMO_CELL');
+  g_Texture_Delete('ITEM_AMMO_CELL_BIG');
+  g_Texture_Delete('ITEM_AMMO_BACKPACK');
+  g_Texture_Delete('ITEM_KEY_RED');
+  g_Texture_Delete('ITEM_KEY_GREEN');
+  g_Texture_Delete('ITEM_KEY_BLUE');
+  g_Texture_Delete('ITEM_OXYGEN');
+  g_Texture_Delete('ITEM_SUIT');
+  g_Texture_Delete('ITEM_WEAPON_KASTET');
+  g_Texture_Delete('ITEM_MEDKIT_BLACK');
+  g_Texture_Delete('ITEM_JETPACK');
+end;
+
+function FindItem(): DWORD;
+var
+  i: Integer;
+begin
+  if gItems <> nil then
+    for i := 0 to High(gItems) do
+      if gItems[i].ItemType = ITEM_NONE then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if gItems = nil then
+    begin
+      SetLength(gItems, 32);
+      Result := 0;
+    end
+  else
+    begin
+      Result := High(gItems) + 1;
+      SetLength(gItems, Length(gItems) + 32);
+    end;
+end;
+
+procedure g_Items_Init();
+var
+  a, b: Integer;
+begin
+  if gMapInfo.Height > gPlayerScreenSize.Y then
+    a := gMapInfo.Height - gPlayerScreenSize.Y
+  else
+    a := gMapInfo.Height;
+
+  if gMapInfo.Width > gPlayerScreenSize.X then
+    b := gMapInfo.Width - gPlayerScreenSize.X
+  else
+    b := gMapInfo.Width;
+
+  gMaxDist := Trunc(Hypot(a, b));
+end;
+
+procedure g_Items_Free();
+var
+  i: Integer;
+begin
+  if gItems <> nil then
+  begin
+    for i := 0 to High(gItems) do
+      gItems[i].Animation.Free();
+    gItems := nil;
+  end;
+end;
+
+function g_Items_Create(X, Y: Integer; ItemType: Byte;
+           Fall, Respawnable: Boolean; AdjCoord: Boolean = False; ForcedID: Integer = -1): DWORD;
+var
+  find_id: DWORD;
+  ID: DWORD;
+begin
+  if ForcedID < 0 then
+    find_id := FindItem()
+  else
+  begin
+    find_id := ForcedID;
+    if Integer(find_id) > High(gItems) then
+      SetLength(gItems, find_id + 32);
+  end;
+
+  gItems[find_id].ItemType := ItemType;
+  gItems[find_id].Respawnable := Respawnable;
+  if g_Game_IsServer and (ITEM_RESPAWNTIME = 0) then
+    gItems[find_id].Respawnable := False;
+  gItems[find_id].InitX := X;
+  gItems[find_id].InitY := Y;
+  gItems[find_id].RespawnTime := 0;
+  gItems[find_id].Fall := Fall;
+  gItems[find_id].Live := True;
+  gItems[find_id].QuietRespawn := False;
+
+  g_Obj_Init(@gItems[find_id].Obj);
+  gItems[find_id].Obj.X := X;
+  gItems[find_id].Obj.Y := Y;
+  gItems[find_id].Obj.Rect.Width := ITEMSIZE[ItemType][0];
+  gItems[find_id].Obj.Rect.Height := ITEMSIZE[ItemType][1];
+
+  gItems[find_id].Animation := nil;
+  gItems[find_id].SpawnTrigger := -1;
+
+// Êîîðäèíàòû îòíîñèòåëüíî öåíòðà íèæíåãî ðåáðà:
+  if AdjCoord then
+    with gItems[find_id] do
+    begin
+      Obj.X := X - (Obj.Rect.Width div 2);
+      Obj.Y := Y - Obj.Rect.Height;
+      InitX := Obj.X;
+      InitY := Obj.Y;
+    end;
+
+// Óñòàíîâêà àíèìàöèè:
+  with gItems[find_id] do
+  begin
+    case ItemType of
+      ITEM_ARMOR_GREEN:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_ARMORGREEN') then
+          Animation := TAnimation.Create(ID, True, 20);
+      ITEM_ARMOR_BLUE:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_ARMORBLUE') then
+          Animation := TAnimation.Create(ID, True, 20);
+      ITEM_SPHERE_BLUE:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_BLUESPHERE') then
+          Animation := TAnimation.Create(ID, True, 15);
+      ITEM_SPHERE_WHITE:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_WHITESPHERE') then
+          Animation := TAnimation.Create(ID, True, 20);
+      ITEM_INVUL:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_INVUL') then
+          Animation := TAnimation.Create(ID, True, 20);
+      ITEM_INVIS:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_INVIS') then
+          Animation := TAnimation.Create(ID, True, 20);
+      ITEM_BOTTLE:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_BOTTLE') then
+          Animation := TAnimation.Create(ID, True, 20);
+      ITEM_HELMET:
+        if g_Frames_Get(ID, 'FRAMES_ITEM_HELMET') then
+          Animation := TAnimation.Create(ID, True, 20);
+    end;
+  end;
+
+  Result := find_id;
+end;
+
+procedure g_Items_Update();
+var
+  i, j, k: Integer;
+  ID: DWORD;
+  Anim: TAnimation;
+  m: Word;
+  r, nxt: Boolean;
+begin
+  if gItems <> nil then
+    for i := 0 to High(gItems) do
+      if gItems[i].ItemType <> ITEM_NONE then
+        with gItems[i] do
+        begin
+          nxt := False;
+          
+          if Live then
+          begin
+            if Fall then
+            begin
+              m := g_Obj_Move(@Obj, True, True);
+
+            // Ñîïðîòèâëåíèå âîçäóõà:
+              if gTime mod (GAME_TICK*2) = 0 then
+                Obj.Vel.X := z_dec(Obj.Vel.X, 1);
+            // Åñëè âûïàë çà êàðòó:
+              if WordBool(m and MOVE_FALLOUT) then
+              begin
+                if SpawnTrigger = -1 then
+                  g_Items_Pick(i)
+                else begin
+                  g_Items_Remove(i);
+                  if g_Game_IsServer and g_Game_IsNet then MH_SEND_ItemDestroy(True, i);
+                end;
+                Continue;
+              end;
+            end;
+
+          // Åñëè èãðîêè ïîáëèçîñòè:
+            if gPlayers <> nil then
+            begin
+              j := Random(Length(gPlayers)) - 1;
+
+              for k := 0 to High(gPlayers) do
+              begin
+                Inc(j);
+                if j > High(gPlayers) then
+                  j := 0;
+
+                if (gPlayers[j] <> nil) and gPlayers[j].Live and
+                   g_Obj_Collide(@gPlayers[j].Obj, @Obj) then
+                begin
+                  if g_Game_IsClient then Continue;
+
+                  if not gPlayers[j].PickItem(ItemType, Respawnable, r) then
+                    Continue;
+
+                  if g_Game_IsNet then MH_SEND_PlayerStats(gPlayers[j].UID);
+
+{
+  Doom 2D: Original:
+  1. I_NONE,I_CLIP,I_SHEL,I_ROCKET,I_CELL,I_AMMO,I_SBOX,I_RBOX,I_CELP,I_BPACK,I_CSAW,I_SGUN,I_SGUN2,I_MGUN,I_LAUN,I_PLAS,I_BFG,I_GUN2
+  +2. I_MEGA,I_INVL,I_SUPER
+  3. I_STIM,I_MEDI,I_ARM1,I_ARM2,I_AQUA,I_KEYR,I_KEYG,I_KEYB,I_SUIT,I_RTORCH,I_GTORCH,I_BTORCH,I_GOR1,I_FCAN
+}
+
+                  if gSoundEffectsDF then
+                  begin
+                    if ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL,
+                                    ITEM_INVIS, ITEM_MEDKIT_BLACK, ITEM_JETPACK] then
+                      g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ',
+                        gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
+                    else
+                      if ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
+                                      ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
+                                      ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERPULEMET, ITEM_AMMO_BACKPACK] then
+                        g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON',
+                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
+                      else
+                        g_Sound_PlayExAt('SOUND_ITEM_GETITEM',
+                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y);
+                  end
+                  else
+                  begin
+                    if ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_SUIT,
+                                    ITEM_MEDKIT_BLACK, ITEM_INVUL, ITEM_INVIS, ITEM_JETPACK] then
+                      g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ',
+                        gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
+                    else
+                      if ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
+                                      ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
+                                      ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERPULEMET] then
+                        g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON',
+                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y)
+                      else
+                        g_Sound_PlayExAt('SOUND_ITEM_GETITEM',
+                          gPlayers[j].Obj.X, gPlayers[j].Obj.Y);
+                  end;
+
+                // Íàäî óáðàòü ñ êàðòû, åñëè ýòî íå êëþ÷, êîòîðûì íóæíî ïîäåëèòüñÿ ñ äðóãèì èãðîêîì:
+                  if r then
+                  begin
+                    if not Respawnable then
+                      g_Items_Remove(i)
+                    else
+                      g_Items_Pick(i);
+
+                    if g_Game_IsNet then MH_SEND_ItemDestroy(False, i);
+                    nxt := True;
+                    Break;
+                  end;
+                end;
+              end;
+            end;
+
+            if nxt then
+              Continue;
+          end;
+
+          if Respawnable and g_Game_IsServer then
+          begin
+            DecMin(RespawnTime, 0);
+            if (RespawnTime = 0) and (not Live) then
+            begin
+              if not QuietRespawn then
+                g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', InitX, InitY);
+
+              if g_Frames_Get(ID, 'FRAMES_ITEM_RESPAWN') then
+              begin
+                Anim := TAnimation.Create(ID, False, 4);
+                g_GFX_OnceAnim(InitX+(Obj.Rect.Width div 2)-16, InitY+(Obj.Rect.Height div 2)-16, Anim);
+                Anim.Free();
+              end;
+
+              Obj.X := InitX;
+              Obj.Y := InitY;
+              Obj.Vel.X := 0;
+              Obj.Vel.Y := 0;
+              Obj.Accel.X := 0;
+              Obj.Accel.Y := 0;
+
+              Live := True;
+
+              if g_Game_IsNet then MH_SEND_ItemSpawn(QuietRespawn, i);
+              QuietRespawn := False;
+            end;
+          end;
+
+          if Animation <> nil then
+            Animation.Update();
+        end;
+end;
+
+procedure g_Items_Draw();
+var
+  i: Integer;
+begin
+  if gItems <> nil then
+    for i := 0 to High(gItems) do
+      if gItems[i].Live then
+        with gItems[i] do
+          if g_Collide(Obj.X, Obj.Y, Obj.Rect.Width, Obj.Rect.Height,
+                       sX, sY, sWidth, sHeight) then
+          begin
+            if Animation = nil then
+              e_Draw(gItemsTexturesID[ItemType], Obj.X, Obj.Y, 0, True, False)
+            else
+              Animation.Draw(Obj.X, Obj.Y, M_NONE);
+
+            if g_debug_Frames then
+            begin
+              e_DrawQuad(Obj.X+Obj.Rect.X,
+                         Obj.Y+Obj.Rect.Y,
+                         Obj.X+Obj.Rect.X+Obj.Rect.Width-1,
+                         Obj.Y+Obj.Rect.Y+Obj.Rect.Height-1,
+                         0, 255, 0);
+            end;
+          end;
+end;
+
+procedure g_Items_Pick(ID: DWORD);
+begin
+  gItems[ID].Live := False;
+  gItems[ID].RespawnTime := ITEM_RESPAWNTIME;
+end;
+
+procedure g_Items_Remove(ID: DWORD);
+begin
+  gItems[ID].ItemType := ITEM_NONE;
+
+  if gItems[ID].Animation <> nil then
+  begin
+    gItems[ID].Animation.Free();
+    gItems[ID].Animation := nil;
+  end;
+
+  gItems[ID].Live := False;
+
+  if gItems[ID].SpawnTrigger > -1 then
+  begin
+    g_Triggers_DecreaseSpawner(gItems[ID].SpawnTrigger);
+    gItems[ID].SpawnTrigger := -1;
+  end;
+end;
+
+procedure g_Items_SaveState(var Mem: TBinMemoryWriter);
+var
+  count, i: Integer;
+  sig: DWORD;
+begin
+// Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ ïðåäìåòîâ:
+  count := 0;
+  if gItems <> nil then
+    for i := 0 to High(gItems) do
+      if gItems[i].ItemType <> ITEM_NONE then
+        count := count + 1;
+
+  Mem := TBinMemoryWriter.Create((count+1) * 60);
+
+// Êîëè÷åñòâî ïðåäìåòîâ:
+  Mem.WriteInt(count);
+
+  if count = 0 then
+    Exit;
+
+  for i := 0 to High(gItems) do
+    if gItems[i].ItemType <> ITEM_NONE then
+    begin
+    // Ñèãíàòóðà ïðåäìåòà:
+      sig := ITEM_SIGNATURE; // 'ITEM'
+      Mem.WriteDWORD(sig);
+    // Òèï ïðåäìåòà:
+      Mem.WriteByte(gItems[i].ItemType);
+    // Åñòü ëè ðåñïàóí:
+      Mem.WriteBoolean(gItems[i].Respawnable);
+    // Êîîðäèíàòû ðåñïóíà:
+      Mem.WriteInt(gItems[i].InitX);
+      Mem.WriteInt(gItems[i].InitY);
+    // Âðåìÿ äî ðåñïàóíà:
+      Mem.WriteWord(gItems[i].RespawnTime);
+    // Ñóùåñòâóåò ëè ýòîò ïðåäìåò:
+      Mem.WriteBoolean(gItems[i].Live);
+    // Ìîæåò ëè îí ïàäàòü:
+      Mem.WriteBoolean(gItems[i].Fall);
+    // Èíäåêñ òðèããåðà, ñîçäàâøåãî ïðåäìåò:
+      Mem.WriteInt(gItems[i].SpawnTrigger);
+    // Îáúåêò ïðåäìåòà:
+      Obj_SaveState(@gItems[i].Obj, Mem);
+    end;
+end;
+
+procedure g_Items_LoadState(var Mem: TBinMemoryReader);
+var
+  count, i, a: Integer;
+  sig: DWORD;
+  b: Byte;
+begin
+  if Mem = nil then
+    Exit;
+
+  g_Items_Free();
+
+// Êîëè÷åñòâî ïðåäìåòîâ:
+  Mem.ReadInt(count);
+
+  if count = 0 then
+    Exit;
+
+  for a := 0 to count-1 do
+  begin
+  // Ñèãíàòóðà ïðåäìåòà:
+    Mem.ReadDWORD(sig);
+    if sig <> ITEM_SIGNATURE then // 'ITEM'
+    begin
+      raise EBinSizeError.Create('g_Items_LoadState: Wrong Item Signature');
+    end;
+  // Òèï ïðåäìåòà:
+    Mem.ReadByte(b);
+  // Ñîçäàåì ïðåäìåò:
+    i := g_Items_Create(0, 0, b, False, False);
+  // Åñòü ëè ðåñïàóí:
+    Mem.ReadBoolean(gItems[i].Respawnable);
+  // Êîîðäèíàòû ðåñïóíà:
+    Mem.ReadInt(gItems[i].InitX);
+    Mem.ReadInt(gItems[i].InitY);
+  // Âðåìÿ äî ðåñïàóíà:
+    Mem.ReadWord(gItems[i].RespawnTime);
+  // Ñóùåñòâóåò ëè ýòîò ïðåäìåò:
+    Mem.ReadBoolean(gItems[i].Live);
+  // Ìîæåò ëè îí ïàäàòü:
+    Mem.ReadBoolean(gItems[i].Fall);
+  // Èíäåêñ òðèããåðà, ñîçäàâøåãî ïðåäìåò:
+    Mem.ReadInt(gItems[i].SpawnTrigger);
+  // Îáúåêò ïðåäìåòà:
+    Obj_LoadState(@gItems[i].Obj, Mem);
+  end;
+end;
+
+end.
diff --git a/src/game/g_language.pas b/src/game/g_language.pas
new file mode 100644 (file)
index 0000000..97dd2b3
--- /dev/null
@@ -0,0 +1,1815 @@
+unit g_language;
+
+interface
+
+uses
+  MAPDEF;
+
+type
+  TStrings_Locale = (
+    I_CONSOLE_EXEC,
+    I_CONSOLE_DUMPED,
+    I_CONSOLE_ERROR_CALL,
+    I_CONSOLE_ERROR_READ,
+    I_CONSOLE_ERROR_WRITE,
+    I_CONSOLE_SCREENSHOT,
+    I_CONSOLE_UNKNOWN,
+    I_CONSOLE_WELCOME,
+
+    I_GAME_ERROR_GET_SPAWN,
+    I_GAME_ERROR_CTF,
+    I_GAME_ERROR_MAP_WAD,
+    I_GAME_ERROR_MAP_RES,
+    I_GAME_ERROR_MAP_LOAD,
+    I_GAME_ERROR_MAP_SELECT,
+    I_GAME_ERROR_PLAYER_CREATE,
+    I_GAME_ERROR_TEXTURE_ANIM,
+    I_GAME_ERROR_TEXTURE_SIMPLE,
+    I_GAME_ERROR_MODEL_FALLBACK,
+    I_GAME_ERROR_MODEL,
+    I_GAME_ERROR_SKY,
+    I_GAME_ERROR_MUSIC,
+    I_GAME_ERROR_SAVE,
+    I_GAME_ERROR_LOAD,
+    I_GAME_ERROR_SOUND,
+    I_GAME_ERROR_FRAMES,
+    I_GAME_ERROR_TR_SOUND,
+    I_GAME_ERROR_SWITCH_TEXTURE,
+
+    I_GAME_PLAYER_NAME,               
+    I_GAME_GAME_TIME,
+    I_GAME_PING,
+    I_GAME_PING_MS,
+    I_GAME_PING_HUD,
+    I_GAME_FRAGS,
+    I_GAME_DEATHS,
+    I_GAME_DM,                        
+    I_GAME_CTF,
+    I_GAME_TDM,
+    I_GAME_COOP,
+    I_GAME_LMS,
+    I_GAME_TLMS,
+    I_GAME_SURV,
+    I_GAME_FRAG_LIMIT,
+    I_GAME_SCORE_LIMIT,
+    I_GAME_TIME_LIMIT,                
+    I_GAME_TEAM_SCORE_RED,
+    I_GAME_TEAM_SCORE_BLUE,
+    I_GAME_TEAM_RED,                  
+    I_GAME_TEAM_BLUE,                 
+    I_GAME_WIN_RED,
+    I_GAME_WIN_BLUE,                  
+    I_GAME_WIN_DRAW,
+    I_GAME_MONSTERS,
+    I_GAME_SECRETS,
+    I_GAME_MONSTERS_TOTAL,
+    I_GAME_SECRETS_TOTAL,
+
+    I_GAME_CHEAT_GODMODE,
+    I_GAME_CHEAT_WEAPONS,
+    I_GAME_CHEAT_HEALTH,
+    I_GAME_CHEAT_DEATH,
+    I_GAME_CHEAT_DOORS,
+    I_GAME_CHEAT_NEXTMAP,
+    I_GAME_CHEAT_CHANGEMAP,
+    I_GAME_CHEAT_FLY,
+    I_GAME_CHEAT_JUMPS,
+    I_GAME_CHEAT_SPEED,
+    I_GAME_CHEAT_SUIT,
+    I_GAME_CHEAT_AIR,
+    I_GAME_CHEAT_BERSERK,
+    I_GAME_CHEAT_JETPACK,
+    I_GAME_CHEAT_NOCLIP,
+    I_GAME_CHEAT_NOTARGET,
+    I_GAME_CHEAT_NORELOAD,
+    I_GAME_CHEAT_AIMLINE,
+    I_GAME_CHEAT_AUTOMAP,
+
+    I_MENU_START_GAME,
+    I_MENU_MAIN_MENU,
+    I_MENU_NEW_GAME,                  
+    I_MENU_MULTIPLAYER,               
+    I_MENU_OPTIONS,
+    I_MENU_AUTHORS,                   
+    I_MENU_EXIT,                      
+    I_MENU_1_PLAYER,                  
+    I_MENU_2_PLAYERS,
+    I_MENU_CUSTOM_GAME,
+    I_MENU_CAMPAIGN,
+    I_MENU_START_SERVER,
+    I_MENU_START_CLIENT,
+    I_MENU_CLIENT_CONNECT,
+    I_MENU_SELECT_MAP,
+    I_MENU_VIDEO_OPTIONS,             
+    I_MENU_SOUND_OPTIONS,             
+    I_MENU_SAVED_OPTIONS,             
+    I_MENU_DEFAULT_OPTIONS,
+    I_MENU_GAME_OPTIONS,              
+    I_MENU_CONTROLS_OPTIONS,          
+    I_MENU_PLAYER_OPTIONS,
+    I_MENU_LANGUAGE_OPTIONS,
+    I_MENU_CHANGE_PLAYERS,
+    I_MENU_LOAD_GAME,
+    I_MENU_SAVE_GAME,
+    I_MENU_END_GAME,                  
+    I_MENU_RESTART,                   
+    I_MENU_SET_GAME,
+
+    I_MENU_JOIN_RED,
+    I_MENU_JOIN_BLUE,
+    I_MENU_JOIN_GAME,
+    I_MENU_ADD_PLAYER_2,
+    I_MENU_REM_PLAYER_2,
+    I_MENU_SPECTATE,
+
+    I_MENU_STATISTICS,
+    I_MENU_MAP,                       
+    I_MENU_GAME_TYPE,                 
+    I_MENU_GAME_TYPE_DM,              
+    I_MENU_GAME_TYPE_CTF,
+    I_MENU_GAME_TYPE_TDM,             
+    I_MENU_GAME_TYPE_COOP,
+    I_MENU_GAME_TYPE_SINGLE,
+    I_MENU_TIME_LIMIT,
+    I_MENU_GOAL_LIMIT,
+    I_MENU_MAX_LIVES,               
+    I_MENU_TEAM_DAMAGE,               
+    I_MENU_ENABLE_EXITS,
+    I_MENU_WEAPONS_STAY,
+    I_MENU_ENABLE_MONSTERS,           
+    I_MENU_BOTS_VS,                   
+    I_MENU_BOTS_VS_PLAYERS,           
+    I_MENU_BOTS_VS_MONSTERS,          
+    I_MENU_BOTS_VS_ALL,               
+
+    I_MENU_MAP_WAD,
+    I_MENU_MAP_RESOURCE,              
+    I_MENU_MAP_NAME,                  
+    I_MENU_MAP_AUTHOR,                
+    I_MENU_MAP_DESCRIPTION,
+    I_MENU_MAP_SIZE,
+    I_MENU_PLAYERS,
+    I_MENU_PLAYERS_ONE,
+    I_MENU_PLAYERS_TWO,
+    I_MENU_SERVER_PLAYERS,
+
+    I_MENU_INTER_MISSION_FAIL,
+    I_MENU_INTER_ROUND_OVER,
+    I_MENU_INTER_LEVEL_COMPLETE,
+    I_MENU_INTER_TIME,
+    I_MENU_INTER_KILLS,
+    I_MENU_INTER_KPM,
+    I_MENU_INTER_SECRETS,
+    I_MENU_INTER_NOTICE_TAB,
+    I_MENU_INTER_NOTICE_MAP,
+    I_MENU_INTER_NOTICE_SPACE,
+    I_MENU_INTER_NOTICE_TIME,
+    I_MENU_LOADING,
+    I_MENU_PLAYER_1,
+    I_MENU_PLAYER_2,
+
+    I_MENU_CONTROL_JOYSTICKS,
+    I_MENU_CONTROL_DEADZONE,
+    
+    I_MENU_CONTROL_GLOBAL,            
+    I_MENU_CONTROL_SCREENSHOT,        
+    I_MENU_CONTROL_STAT,
+    I_MENU_CONTROL_CHAT,
+    I_MENU_CONTROL_TEAMCHAT,           
+    I_MENU_CONTROL_LEFT,              
+    I_MENU_CONTROL_RIGHT,
+    I_MENU_CONTROL_UP,                
+    I_MENU_CONTROL_DOWN,
+    I_MENU_CONTROL_JUMP,              
+    I_MENU_CONTROL_FIRE,
+    I_MENU_CONTROL_USE,               
+    I_MENU_CONTROL_NEXT_WEAPON,
+    I_MENU_CONTROL_PREV_WEAPON,
+
+    I_MENU_COUNT_NONE,
+    I_MENU_COUNT_SMALL,               
+    I_MENU_COUNT_NORMAL,              
+    I_MENU_COUNT_BIG,                 
+    I_MENU_COUNT_VERYBIG,             
+
+    I_MENU_GAME_BLOOD_COUNT,
+    I_MENU_GAME_MAX_GIBS,
+    I_MENU_GAME_MAX_CORPSES,          
+    I_MENU_GAME_GIBS_COUNT,
+    I_MENU_GAME_MAX_SHELLS,
+    I_MENU_GAME_BLOOD_TYPE,
+    I_MENU_GAME_BLOOD_TYPE_SIMPLE,    
+    I_MENU_GAME_BLOOD_TYPE_ADV,       
+    I_MENU_GAME_CORPSE_TYPE,          
+    I_MENU_GAME_CORPSE_TYPE_SIMPLE,   
+    I_MENU_GAME_CORPSE_TYPE_ADV,
+    I_MENU_GAME_GIBS_TYPE,
+    I_MENU_GAME_GIBS_TYPE_SIMPLE,
+    I_MENU_GAME_GIBS_TYPE_ADV,
+    I_MENU_GAME_PARTICLES_COUNT,
+    I_MENU_GAME_SCREEN_FLASH,
+    I_MENU_GAME_BACKGROUND,
+    I_MENU_GAME_MESSAGES,
+    I_MENU_GAME_REVERT_PLAYERS,
+    I_MENU_GAME_CHAT_BUBBLE,
+    I_MENU_GAME_CHAT_TYPE_NONE,
+    I_MENU_GAME_CHAT_TYPE_SIMPLE,
+    I_MENU_GAME_CHAT_TYPE_ADV,
+    I_MENU_GAME_CHAT_TYPE_COLOR,
+    I_MENU_GAME_CHAT_TYPE_TEXTURE,
+
+    I_MENU_VIDEO_RESOLUTION,
+    I_MENU_VIDEO_BPP,
+    I_MENU_VIDEO_VSYNC,               
+    I_MENU_VIDEO_FILTER_SKY,          
+    I_MENU_VIDEO_NEED_RESTART,
+
+    I_MENU_RESOLUTION_SELECT,
+    I_MENU_RESOLUTION_CURRENT,
+    I_MENU_RESOLUTION_LIST,
+    I_MENU_RESOLUTION_FULLSCREEN,
+    I_MENU_RESOLUTION_APPLY,
+
+    I_MENU_SOUND_MUSIC_LEVEL,         
+    I_MENU_SOUND_SOUND_LEVEL,
+    I_MENU_SOUND_MAX_SIM_SOUNDS,      
+    I_MENU_SOUND_INACTIVE_SOUNDS,     
+    I_MENU_SOUND_INACTIVE_SOUNDS_ON,  
+    I_MENU_SOUND_INACTIVE_SOUNDS_OFF,
+    I_MENU_SOUND_ANNOUNCE,
+    I_MENU_SOUND_COMPAT,
+    I_MENU_ANNOUNCE_NONE,
+    I_MENU_ANNOUNCE_ME,
+    I_MENU_ANNOUNCE_MEPLUS,
+    I_MENU_ANNOUNCE_ALL,
+    I_MENU_COMPAT_DF,
+    I_MENU_COMPAT_DOOM2,
+
+    I_MENU_PLAYER_NAME,
+    I_MENU_PLAYER_TEAM,               
+    I_MENU_PLAYER_TEAM_RED,           
+    I_MENU_PLAYER_TEAM_BLUE,
+    I_MENU_PLAYER_MODEL,              
+    I_MENU_PLAYER_RED,                
+    I_MENU_PLAYER_GREEN,              
+    I_MENU_PLAYER_BLUE,
+               
+    I_MENU_MODEL_INFO,                
+    I_MENU_MODEL_ANIMATION,
+    I_MENU_MODEL_CHANGE_WEAPON,       
+    I_MENU_MODEL_ROTATE,              
+    I_MENU_MODEL_NAME,
+    I_MENU_MODEL_AUTHOR,
+    I_MENU_MODEL_COMMENT,             
+    I_MENU_MODEL_OPTIONS,             
+    I_MENU_MODEL_WEAPON,
+
+    I_MENU_LANGUAGE_RUSSIAN,
+    I_MENU_LANGUAGE_ENGLISH,
+
+    I_MENU_PAUSE,                     
+    I_MENU_YES,                       
+    I_MENU_NO,                        
+    I_MENU_OK,                        
+    I_MENU_FINISH,
+
+    I_MENU_END_GAME_PROMT,            
+    I_MENU_RESTART_GAME_PROMT,        
+    I_MENU_EXIT_PROMT,
+    I_MENU_SET_DEFAULT_PROMT,         
+    I_MENU_LOAD_SAVED_PROMT,
+    I_MENU_ENTERPASSWORD,
+
+    I_NET_SERVER,
+    I_NET_CLIENT,
+    I_NET_ADDRESS,
+    I_NET_PORT,
+    I_NET_SERVER_NAME,
+    I_NET_SERVER_PASSWORD,
+    I_NET_MAX_CLIENTS,
+    I_NET_USE_MASTER,
+
+    I_NET_MSG,
+    I_NET_MSG_ERROR,
+    I_NET_MSG_FERROR,
+
+    I_NET_MSG_HOST,
+    I_NET_MSG_HOST_DISCALL,
+    I_NET_MSG_HOST_DIE,
+    I_NET_MSG_HOST_CONN,
+    I_NET_MSG_HOST_REJECT,
+    I_NET_MSG_HOST_ADD,
+    I_NET_MSG_HOST_DISC,
+    I_NET_MSG_CLIENT_CONN,
+    I_NET_MSG_CLIENT_DONE,
+    I_NET_MSG_CLIENT_DISC,
+    I_NET_MSG_KICK,
+
+    I_NET_MAP_DL,
+    I_NET_WAD_DL,
+
+    I_NET_ERR_ENET,
+    I_NET_ERR_HOST,
+    I_NET_ERR_INGAME,
+    I_NET_ERR_CLIENT,
+    I_NET_ERR_CONN,
+    I_NET_ERR_TIMEOUT,
+    I_NET_ERR_HASH,
+    I_NET_ERR_NAME404,
+
+    I_NET_DISC_NONE,
+    I_NET_DISC_PROTOCOL,
+    I_NET_DISC_VERSION,
+    I_NET_DISC_FULL,
+    I_NET_DISC_KICK,
+    I_NET_DISC_DOWN,
+    I_NET_DISC_PASSWORD,
+    I_NET_DISC_TEMPBAN,
+    I_NET_DISC_BAN,
+
+    I_NET_SLIST,
+    I_NET_SLIST_NOSERVERS,
+    I_NET_SLIST_SERVERS,
+    I_NET_SLIST_HELP,
+    I_NET_SLIST_WAIT,
+    I_NET_SLIST_PING_MS,
+    I_NET_SLIST_NO_ACCESS,
+
+    I_NET_SLIST_FETCH,
+    I_NET_SLIST_RETRIEVED,
+    I_NET_SLIST_CONN,
+    I_NET_SLIST_DISC,
+    I_NET_SLIST_LOST,
+    I_NET_SLIST_ERROR,
+
+    I_NET_RCON_PWD_VALID,
+    I_NET_RCON_PWD_INVALID,
+    I_NET_RCON_NOAUTH,
+
+    I_PLAYER_DIED,
+    I_PLAYER_KILL,
+    I_PLAYER_KILL_EXTRAHARD_1,
+    I_PLAYER_KILL_EXTRAHARD_2,
+    I_PLAYER_KILL_ACID,
+    I_PLAYER_KILL_TRAP,
+    I_PLAYER_KILL_FALL,
+    I_PLAYER_KILL_SELF,
+    I_PLAYER_KILL_WATER,
+
+    I_PLAYER_KILL_2X,
+    I_PLAYER_KILL_3X,
+    I_PLAYER_KILL_4X,
+    I_PLAYER_KILL_MX,
+
+    I_PLAYER_JOIN,
+    I_PLAYER_LEAVE,
+    I_PLAYER_KICK,
+    I_PLAYER_BAN,
+    I_PLAYER_NAME,
+    I_PLAYER_CHTEAM_RED,
+    I_PLAYER_CHTEAM_BLUE,
+
+    I_PLAYER_SPECT,
+    I_PLAYER_SPECT1,
+    I_PLAYER_SPECT1S,
+    I_PLAYER_SPECT2,
+    I_PLAYER_SPECT3,
+    I_PLAYER_SPECT4,
+
+    I_PLAYER_FLAG_GET,
+    I_PLAYER_FLAG_RETURN,             
+    I_PLAYER_FLAG_CAPTURE,            
+    I_PLAYER_FLAG_DROP,
+    I_PLAYER_FLAG_RED,                
+    I_PLAYER_FLAG_BLUE,
+
+    I_PLAYER_SCORE_RED,
+    I_PLAYER_SCORE_BLUE,
+    I_PLAYER_SCORE_TO_RED,
+    I_PLAYER_SCORE_TO_BLUE,
+    I_PLAYER_SCORE_ADD_OWN,
+    I_PLAYER_SCORE_SUB_OWN,
+    I_PLAYER_SCORE_ADD_ENEMY,
+    I_PLAYER_SCORE_SUB_ENEMY,
+    I_PLAYER_SCORE_WIN_OWN,
+    I_PLAYER_SCORE_WIN_ENEMY,
+    I_PLAYER_SCORE_ADD_TEAM,
+    I_PLAYER_SCORE_SUB_TEAM,
+
+    I_MESSAGE_FLAG_GET,
+    I_MESSAGE_FLAG_RETURN,
+    I_MESSAGE_FLAG_CAPTURE,
+    I_MESSAGE_FLAG_DROP,
+
+    I_MESSAGE_LMS_LOSE,
+    I_MESSAGE_LMS_WIN,
+    I_MESSAGE_LMS_START,
+    I_MESSAGE_TLMS_WIN,
+    I_MESSAGE_LMS_SURVIVOR,
+
+    I_MESSAGE_SCORE_ADD,
+    I_MESSAGE_SCORE_SUB,
+
+    I_MESSAGE_VOTE_INPROGRESS,
+    I_MESSAGE_VOTE_STARTED,
+    I_MESSAGE_VOTE_PASSED,
+    I_MESSAGE_VOTE_FAILED,
+    I_MESSAGE_VOTE_VOTE,
+    I_MESSAGE_VOTE_REVOKED,
+
+    I_KEY_UP,
+    I_KEY_DOWN,                       
+    I_KEY_LEFT,                       
+    I_KEY_RIGHT,
+
+    I_MONSTER_DEMON,
+    I_MONSTER_IMP,
+    I_MONSTER_ZOMBIE,
+    I_MONSTER_SERGEANT,
+    I_MONSTER_CYBER,
+    I_MONSTER_CGUN,
+    I_MONSTER_BARON,
+    I_MONSTER_KNIGHT,
+    I_MONSTER_CACODEMON,
+    I_MONSTER_SOUL,
+    I_MONSTER_PAIN,
+    I_MONSTER_MASTERMIND,
+    I_MONSTER_SPIDER,
+    I_MONSTER_MANCUBUS,
+    I_MONSTER_REVENANT,
+    I_MONSTER_ARCHVILE,
+    I_MONSTER_FISH,
+    I_MONSTER_BARREL,
+    I_MONSTER_ROBOT,
+    I_MONSTER_PRIKOLIST,
+
+    I_LOAD_MUSIC,
+    I_LOAD_MODELS,                    
+    I_LOAD_MENUS,                     
+    I_LOAD_CONSOLE,                   
+    I_LOAD_ITEMS_DATA,                
+    I_LOAD_WEAPONS_DATA,
+    I_LOAD_GAME_DATA,                 
+    I_LOAD_COLLIDE_MAP,               
+    I_LOAD_DOOR_MAP,
+    I_LOAD_LIFT_MAP,
+    I_LOAD_WATER_MAP,                 
+    I_LOAD_WAD_FILE,                  
+    I_LOAD_MAP,
+    I_LOAD_TEXTURES,                  
+    I_LOAD_TRIGGERS,                  
+    I_LOAD_PANELS,                    
+    I_LOAD_TRIGGERS_TABLE,
+    I_LOAD_LINK_TRIGGERS,             
+    I_LOAD_CREATE_TRIGGERS,           
+    I_LOAD_ITEMS,                     
+    I_LOAD_CREATE_ITEMS,              
+    I_LOAD_AREAS,
+    I_LOAD_CREATE_AREAS,              
+    I_LOAD_MONSTERS,                  
+    I_LOAD_CREATE_MONSTERS,           
+    I_LOAD_MAP_HEADER,                
+    I_LOAD_SKY,
+    I_LOAD_MONSTER_TEXTURES,
+    I_LOAD_MONSTER_SOUNDS,
+    I_LOAD_SAVE_FILE,                 
+    I_LOAD_MAP_STATE,                 
+    I_LOAD_ITEMS_STATE,               
+    I_LOAD_TRIGGERS_STATE,
+    I_LOAD_WEAPONS_STATE,             
+    I_LOAD_MONSTERS_STATE,
+    I_LOAD_CONNECT,
+    I_LOAD_SEND_INFO,
+    I_LOAD_WAIT_INFO,
+    I_LOAD_DL_RES,           
+
+    I_CREDITS_CAP_1,
+    I_CREDITS_CAP_2,
+    I_CREDITS_A_1,
+    I_CREDITS_A_1_1,
+    I_CREDITS_A_2,
+    I_CREDITS_A_2_1,
+    I_CREDITS_A_3,
+    I_CREDITS_A_3_1,
+    I_CREDITS_A_4,
+    I_CREDITS_A_4_1,
+    I_CREDITS_CAP_3,                      
+    I_CREDITS_CLO_1,
+    I_CREDITS_CLO_2,
+    I_CREDITS_CLO_3,
+    I_CREDITS_CLO_4,
+
+    I_MSG_SHOW_FPS_ON,
+    I_MSG_SHOW_FPS_OFF,
+    I_MSG_GAMEMODE_CURRENT,
+    I_MSG_GAMEMODE_CHANGE,
+    I_MSG_FRIENDLY_FIRE_ON,
+    I_MSG_FRIENDLY_FIRE_OFF,
+    I_MSG_WEAPONSTAY_ON,
+    I_MSG_WEAPONSTAY_OFF,
+    I_MSG_ALLOWEXIT_ON,
+    I_MSG_ALLOWEXIT_OFF,
+    I_MSG_ALLOWMON_ON,
+    I_MSG_ALLOWMON_OFF,
+    I_MSG_BOTSVSPLAYERS_ON,
+    I_MSG_BOTSVSPLAYERS_OFF,
+    I_MSG_BOTSVSMONSTERS_ON,
+    I_MSG_BOTSVSMONSTERS_OFF,
+    I_MSG_TIME_ON,
+    I_MSG_TIME_OFF,
+    I_MSG_SCORE_ON,
+    I_MSG_SCORE_OFF,
+    I_MSG_STATS_ON,
+    I_MSG_STATS_OFF,
+    I_MSG_KILL_MSGS_ON,
+    I_MSG_KILL_MSGS_OFF,
+    I_MSG_LIVES_ON,
+    I_MSG_LIVES_OFF,
+    I_MSG_SPECT_HUD_ON,
+    I_MSG_SPECT_HUD_OFF,
+    I_MSG_PING_ON,
+    I_MSG_PING_OFF,
+    I_MSG_NO_WAD,
+    I_MSG_NO_MAPS,
+    I_MSG_NO_MAP,
+    I_MSG_NO_MONSTER,
+    I_MSG_SCORE_LIMIT,
+    I_MSG_TIME_LIMIT,
+    I_MSG_LIVES,
+    I_MSG_WARMUP,
+    I_MSG_WARMUP_START,
+    I_MSG_NEXTMAP_SET,
+    I_MSG_NEXTMAP_UNSET,
+    I_MSG_ONMAPCHANGE,
+    I_MSG_NOT_GAME,
+    I_MSG_NOT_DEBUG,
+    I_MSG_GM_UNAVAIL,
+    I_MSG_SERVERONLY,
+    I_MSG_NOCLIENTS,
+    I_MSG_UNBAN_OK,
+    I_MSG_UNBAN_FAIL,
+
+    I_TEXTURE_ENDPIC,
+
+    I_VERSION,                    
+
+    I_FATAL_ERROR,
+    I_SIMPLE_ERROR,
+    I_SYSTEM_ERROR_UNKNOWN,
+    I_SYSTEM_ERROR_MSG,
+
+    I_LAST );
+
+const
+  LANGUAGE_RUSSIAN = 'Russian';
+  LANGUAGE_ENGLISH = 'English';
+  LANGUAGE_RUSSIAN_N = 3;
+  LANGUAGE_ENGLISH_N = 2;
+
+var
+  _lc: Array [TStrings_Locale] of String;
+  KilledByMonster: Array [MONSTER_DEMON..MONSTER_MAN] of String;
+  CheatEng: Array [I_GAME_CHEAT_GODMODE..I_GAME_CHEAT_AUTOMAP] of String;
+  CheatRus: Array [I_GAME_CHEAT_GODMODE..I_GAME_CHEAT_AUTOMAP] of String;
+
+procedure g_Language_Load(fileName: String);
+procedure g_Language_Set(lang: String);
+procedure g_Language_Dump(fileName: String);
+
+implementation
+
+uses
+  SysUtils, g_gui, g_basic, e_log, e_input;
+
+const
+  g_lang_default: Array [TStrings_Locale] of Array [1..3] of String = (
+    ('CONSOLE EXEC',                   'Executing script: "%s"',
+                                       'Âûïîëíÿåòñÿ ñêðèïò: "%s"'),
+    ('CONSOLE DUMPED',                 'Saved to "%s"',
+                                       'Ñîõðàíåíî â "%s"'),
+    ('CONSOLE ERROR CALL',             'Console loop detected. Execution terminated.',
+                                       'Îáíàðóæåí áåñêîíå÷íûé öèêë. Ñêðèïò îñòàíîâëåí.'),
+    ('CONSOLE ERROR READ',             'Error reading file "%s"',
+                                       'Îøèáêà ïðè ÷òåíèè ôàéëà "%s"'),
+    ('CONSOLE ERROR WRITE',            'Error writing file "%s"',
+                                       'Îøèáêà ïðè çàïèñè â ôàéë "%s"'),
+    ('CONSOLE SCREENSHOT',             'Screenshot saved to "%s"',
+                                       'Ñêðèíøîò ñîõðàí¸í â "%s"'),
+    ('CONSOLE UNKNOWN',                'Unknown command "%s"',
+                                       'Íåèçâåñòíàÿ êîìàíäà "%s"'),
+    ('CONSOLE WELCOME',                'Welcome to Doom 2D: Forever %s',
+                                       'Äîáðî ïîæàëîâàòü â Doom 2D: Forever %s'),
+
+    ('GAME ERROR GET SPAWN',           'Can''t find a spawn point!',
+                                       'Íå óäàëîñü ïîëó÷èòü òî÷êó âîçðîæäåíèÿ!'),
+    ('GAME ERROR CTF',                 'There are no flags on this map!',
+                                       'Íà êàðòå íåò ôëàãîâ!'),
+    ('GAME ERROR MAP WAD',             'Can''t read map WAD "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü WAD êàðòû: "%s"'),
+    ('GAME ERROR MAP RES',             'Can''t load map resource "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü ðåñóðñ êàðòû èç WAD: "%s"'),
+    ('GAME ERROR MAP LOAD',            'Can''t load map "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü êàðòó "%s"'),
+    ('GAME ERROR MAP SELECT',          'Map reading error!',
+                                       'Êàðòà íå ÷èòàåòñÿ!'),
+    ('GAME ERROR PLAYER CREATE',       'Can''t create player #%d',
+                                       'Íå óäàëîñü ñîçäàòü Èãðîêà #%d'),
+    ('GAME ERROR TEXTURE ANIM',        'Can''t create animated texture "%s"',
+                                       'Íå ïîëó÷èëîñü ñîçäàòü àíèìèðîâàííóþ òåêñòóðó "%s"'),
+    ('GAME ERROR TEXTURE SIMPLE',      'Can''t create ordinary Texture "%s"',
+                                       'Íå ïîëó÷èëîñü ñîçäàòü îáû÷íóþ òåêñòóðó "%s"'),
+    ('GAME ERROR MODEL FALLBACK',      'Model "%s" not found, using default model instead',
+                                       'Ìîäåëü "%s" íå íàéäåíà, èñïîëüçóåòñÿ ìîäåëü ïî óìîë÷àíèþ'),
+    ('GAME ERROR MODEL',               'Model "%s" not found',
+                                       'Ìîäåëü "%s" íå íàéäåíà'),
+    ('GAME ERROR SKY',                 'Can''t load sky "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü íåáî "%s"'),
+    ('GAME ERROR MUSIC',               'Can''t load music "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü ìóçûêó "%s"'),
+    ('GAME ERROR SAVE',                'Saving state error!',
+                                       'Îøèáêà âî âðåìÿ ñîõðàíåíèÿ!'),
+    ('GAME ERROR LOAD',                'Loading state error!',
+                                       'Îøèáêà âî âðåìÿ çàãðóçêè!'),
+    ('GAME ERROR SOUND',               'Can''t load sound "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü çâóê "%s"'),
+    ('GAME ERROR FRAMES',              'Can''t load animation''s frame list "%s"',
+                                       'Íå óäàëîñü çàãðóçèòü ñïèñîê êàäðîâ àíèìàöèè: "%s"'),
+    ('GAME ERROR TR SOUND',            'Can''t load sound "%s:%s" for trigger',
+                                       'Íå óäàëîñü çàãðóçèòü çâóê "%s:%s" äëÿ òðèããåðà'),
+    ('GAME ERROR SWITCH TEXTURE',      'Texture switching error: no animation',
+                                       'Îøèáêà ïðè ïåðåêëþ÷åíèè òåêñòóðû: íåò àíèìàöèè'),
+
+    ('GAME PLAYER NAME',               'Player name',
+                                       'Èãðîê'),
+    ('GAME GAME TIME',                 'Game time:',
+                                       'Âðåìÿ èãðû:'),
+    ('GAME PING',                      'Ping (Loss)',
+                                       'Ïèíã (Ïîòåðè)'),
+    ('GAME PING MS',                   '%d ms (%d%%)',
+                                       '%d ìñ (%d%%)'),
+    ('GAME PING HUD',                  'Ping: ',
+                                       'Ïèíã: '),
+    ('GAME FRAGS',                     'Frags',
+                                       'Ôðàãîâ'),
+    ('GAME DEATHS',                    'Deaths',
+                                       'Ñìåðòåé'),
+    ('GAME DM',                        'Deathmatch',
+                                       'ÌßÑÎÏÎÂÀË'),
+    ('GAME CTF',                       'Capture the Flag',
+                                       'ÇÀÕÂÀÒ ÔËÀÃÀ'),
+    ('GAME TDM',                       'Team Deathmatch',
+                                       'ÊÎÌÀÍÄÍÛÉ ÌßÑÎÏÎÂÀË'),
+    ('GAME COOP',                      'Cooperative',
+                                       'ÊÎÎÏÅÐÀÒÈÂ'),
+    ('GAME LMS',                       'Last Man Standing',
+                                       'ÏÎÑËÅÄÍÈÉ ÃÅÐÎÉ'),
+    ('GAME TLMS',                      'Team LMS',
+                                       'ÑÒÅÍÊÀ ÍÀ ÑÒÅÍÊÓ'),
+    ('GAME SURV',                      'Survivor',
+                                       'ÂÛÆÈÂÀÍÈÅ'),
+    ('GAME FRAG LIMIT',                'Frag Limit: %d',
+                                       'ËÈÌÈÒ ÔÐÀÃÎÂ: %d'),
+    ('GAME SCORE LIMIT',               'Score Limit: %d',
+                                       'ËÈÌÈÒ Î×ÊÎÂ: %d'),
+    ('GAME TIME LIMIT',                'Time Limit: %d:%.2d:%.2d',
+                                       'ËÈÌÈÒ ÂÐÅÌÅÍÈ: %d:%.2d:%.2d'),
+    ('GAME TEAM SCORE RED',            'Red Team (%d)',
+                                       'ÊÐÀÑÍÀß ÊÎÌÀÍÄÀ (%d)'),
+    ('GAME TEAM SCORE BLUE',           'Blue Team (%d)',
+                                       'ÑÈÍßß ÊÎÌÀÍÄÀ (%d)'),
+    ('GAME TEAM RED',                  'Red',
+                                       'ÊÐÀÑÍÀß'),
+    ('GAME TEAM BLUE',                 'Blue',
+                                       'ÑÈÍßß'),
+    ('GAME WIN RED',                   'Red Team Wins!',
+                                       'ÏÎÁÅÄÈËÀ ÊÐÀÑÍÀß ÊÎÌÀÍÄÀ!'),
+    ('GAME WIN BLUE',                  'Blue Team Wins!',
+                                       'ÏÎÁÅÄÈËÀ ÑÈÍßß ÊÎÌÀÍÄÀ!'),
+    ('GAME WIN DRAW',                  'Stalemate!',
+                                       'ÍÈ×Üß!'),
+    ('GAME MONSTERS',                  'Monsters:',
+                                       'Ìîíñòðîâ óáèòî:'),
+    ('GAME SECRETS',                   'Secrets:',
+                                       'Ñåêðåòîâ íàéäåíî:'),
+    ('GAME MONSTERS TOTAL',            'Total monsters killed:',
+                                       'Âñåãî ìîíñòðîâ óáèòî:'),
+    ('GAME SECRETS TOTAL',             'Total secrets found:',
+                                       'Âñåãî ñåêðåòîâ íàéäåíî:'),
+
+    ('GAME CHEAT GODMODE',             'MACLEOD',
+                                       'ÃÎÐÅÖ'),
+    ('GAME CHEAT WEAPONS',             'RAMBO',
+                                       'RAMBO'),
+    ('GAME CHEAT HEALTH',              'TANK',
+                                       'TANK'),
+    ('GAME CHEAT DEATH',               'IDDQD',
+                                       'IDDQD'),
+    ('GAME CHEAT DOORS',               'SESAME',
+                                       'ÑÈÌÑÈÌ'),
+    ('GAME CHEAT NEXTMAP',             'GOODBYE',
+                                       'GOODBYE'),
+    ('GAME CHEAT CHANGEMAP',           'GOTO',
+                                       'ÏÎØÅËÍÀ'),
+    ('GAME CHEAT FLY',                 'WHITEEAGLE',
+                                       'ÁÅËÛÉÎÐÅË'),
+    ('GAME CHEAT JUMPS',               'BULLFROG',
+                                       'BULLFROG'),
+    ('GAME CHEAT SPEED',               'FORMULA1',
+                                       'FORMULA1'),
+    ('GAME CHEAT SUIT',                'CONDOM',
+                                       'CONDOM'),
+    ('GAME CHEAT AIR',                 'AQUAMAN',
+                                       '×ÅÐÍÎÌÎÐ'),
+    ('GAME CHEAT BERSERK',             'PURELOVE',
+                                       'PURELOVE'),
+    ('GAME CHEAT JETPACK',             'LUCYINTHESKY',
+                                       'ßÑÂÎÁÎÄÅÍ'),
+    ('GAME CHEAT NOCLIP',              'CASPER',
+                                       'CASPER'),
+    ('GAME CHEAT NOTARGET',            'JAMESBOND',
+                                       'ØÒÈÐËÈÖ'),
+    ('GAME CHEAT NORELOAD',            'INFERNO',
+                                       'INFERNO'),
+    ('GAME CHEAT AIMLINE',             'SNIPER',
+                                       'ÑÍÀÉÏÅÐ'),
+    ('GAME CHEAT AUTOMAP',             'ID2DT',
+                                       'ID2DT'),
+
+    ('MENU START GAME',                'Start Game',
+                                       'ÍÀ×ÀÒÜ ÈÃÐÓ'),
+    ('MENU MAIN MENU',                 'Menu',
+                                       'ÌÅÍÞ'),
+    ('MENU NEW GAME',                  'New Game',
+                                       'ÍÎÂÀß ÈÃÐÀ'),
+    ('MENU MULTIPLAYER',               'Multiplayer',
+                                       'ÌÓËÜÒÈÏËÅÅÐ'),
+    ('MENU OPTIONS',                   'Options',
+                                       'ÍÀÑÒÐÎÉÊÈ'),
+    ('MENU AUTHORS',                   'Credits',
+                                       'ÀÂÒÎÐÛ'),
+    ('MENU EXIT',                      'Exit',
+                                       'ÂÛÕÎÄ'),
+    ('MENU 1 PLAYER',                  'Single player',
+                                       'ÎÄÈÍ ÈÃÐÎÊ'),
+    ('MENU 2 PLAYERS',                 'Two players',
+                                       'ÄÂÀ ÈÃÐÎÊÀ'),
+    ('MENU CUSTOM GAME',               'Custom game',
+                                       'ÑÂÎß ÈÃÐÀ'),
+    ('MENU CAMPAIGN',                  'Campaign select',
+                                       'ÂÛÁÎРÊÀÌÏÀÍÈÈ'),
+    ('MENU START SERVER',              'Create a server',
+                                       'ÑÎÇÄÀÒÜ ÑÅÐÂÅÐ'),
+    ('MENU START CLIENT',              'Join to game',
+                                       'ÂÑÒÓÏÈÒܠ ÈÃÐÓ'),
+    ('MENU CLIENT CONNECT',            'Connect',
+                                       'Ïðèñîåäèíèòüñÿ'),
+    ('MENU SELECT MAP',                'Map',
+                                       'ÊÀÐÒÀ'),
+    ('MENU VIDEO OPTIONS',             'Video',
+                                       'ÂÈÄÅÎ'),
+    ('MENU SOUND OPTIONS',             'Sound',
+                                       'ÇÂÓÊ'),
+    ('MENU SAVED OPTIONS',             'Saved options',
+                                       'ÑÎÕÐÀͨÍÍÛÅ'),
+    ('MENU DEFAULT OPTIONS',           'Default options',
+                                       'ÑÒÀÍÄÀÐÒÍÛÅ'),
+    ('MENU GAME OPTIONS',              'Gameplay',
+                                       'ÈÃÐÀ'),
+    ('MENU CONTROLS OPTIONS',          'Controls',
+                                       'ÓÏÐÀÂËÅÍÈÅ'),
+    ('MENU PLAYER OPTIONS',            'Players',
+                                       'ÈÃÐÎÊÈ'),
+    ('MENU LANGUAGE OPTIONS',          'Language',
+                                       'ßÇÛÊ'),
+    ('MENU CHANGE PLAYERS',            'Change players',
+                                       'ÑÌÅÍÀ ÑÎÑÒÀÂÀ'),
+    ('MENU LOAD GAME',                 'Load game',
+                                       'ÑÒÀÐÀß ÈÃÐÀ'),
+    ('MENU SAVE GAME',                 'Save game',
+                                       'ÑÎÕÐÀÍÈÒÜ ÈÃÐÓ'),
+    ('MENU END GAME',                  'End game',
+                                       'ÇÀÊÎÍ×ÈÒÜ ÈÃÐÓ'),
+    ('MENU RESTART',                   'Restart game',
+                                       'ÍÀ×ÀÒÜ ÇÀÍÎÂÎ'),
+    ('MENU SET GAME',                  'Setup game',
+                                       'ÐÀÇÍÎÅ'),
+
+    ('MENU JOIN RED',                  'Join Red',
+                                       'ÂÎÉÒÈ ÇÀ ÊÐÀÑÍÛÕ'),
+    ('MENU JOIN BLUE',                 'Join Blue',
+                                       'ÂÎÉÒÈ ÇÀ ÑÈÍÈÕ'),
+    ('MENU JOIN GAME',                 'Join Game',
+                                       'ÂÎÉÒȠ ÈÃÐÓ'),
+    ('MENU ADD PLAYER 2',              'Add Player 2',
+                                       'ÄÎÁÀÂÈÒÜ 2 ÈÃÐÎÊÀ'),
+    ('MENU REM PLAYER 2',              'Remove Player 2',
+                                       'ÓÁÐÀÒÜ 2 ÈÃÐÎÊÀ'),
+    ('MENU SPECTATE',                  'Spectate',
+                                       'ÍÀÁËÞÄÀÒÜ'),
+
+    ('MENU STATISTICS',                'Statistics',
+                                       'ÑÒÀÒÈÑÒÈÊÀ ÈÃÐÛ'),
+    ('MENU MAP',                       'Map:',
+                                       'Êàðòà:'),
+    ('MENU GAME TYPE',                 'Game type:',
+                                       'Ðåæèì èãðû:'),
+    ('MENU GAME TYPE DM',              'DM',
+                                       'DM'),
+    ('MENU GAME TYPE CTF',             'CTF',
+                                       'CTF'),
+    ('MENU GAME TYPE TDM',             'TDM',
+                                       'TDM'),
+    ('MENU GAME TYPE COOP',            'COOP',
+                                       'COOP'),
+    ('MENU GAME TYPE SINGLE',          'SINGLE',
+                                       'SINGLE'),
+    ('MENU TIME LIMIT',                'Time Limit:',
+                                       'Ëèìèò âðåìåíè:'),
+    ('MENU GOAL LIMIT',                'Score Limit:',
+                                       'Ëèìèò î÷êîâ:'),
+    ('MENU MAX LIVES',                 'Lives:',
+                                       'Æèçíè:'),
+    ('MENU TEAM DAMAGE',               'Friendly Fire:',
+                                       'Óðîí ñâîèõ:'),
+    ('MENU ENABLE EXITS',              'Enable Exit:',
+                                       'Âêëþ÷èòü âûõîä:'),
+    ('MENU WEAPONS STAY',              'Weapons stay:',
+                                       'Îðóæèå îñòà¸òñÿ:'),
+    ('MENU ENABLE MONSTERS',           'Enable monsters:',
+                                       'Ìîíñòðû:'),
+    ('MENU BOTS VS',                   'Bots fight with:',
+                                       'Áîòû ïðîòèâ:'),
+    ('MENU BOTS VS PLAYERS',           'Players',
+                                       'Èãðîêîâ'),
+    ('MENU BOTS VS MONSTERS',          'Monsters',
+                                       'Ìîíñòðîâ'),
+    ('MENU BOTS VS ALL',               'Everybody',
+                                       'Âñåõ'),
+
+    ('MENU MAP WAD',                   'Select WAD:',
+                                       'Âûáîð WAD''à:'),
+    ('MENU MAP RESOURCE',              'Select Map:',
+                                       'Âûáîð êàðòû:'),
+    ('MENU MAP NAME',                  'Name:',
+                                       'Íàçâàíèå:'),
+    ('MENU MAP AUTHOR',                'Author:',
+                                       'Àâòîð:'),
+    ('MENU MAP DESCRIPTION',           'Description:',
+                                       'Îïèñàíèå:'),
+    ('MENU MAP SIZE',                  'Size:',
+                                       'Ðàçìåð:'),
+    ('MENU PLAYERS',                   'Players:',
+                                       '×èñëî èãðîêîâ:'),
+    ('MENU PLAYERS ONE',               'One',
+                                       'Îäèí'),
+    ('MENU PLAYERS TWO',               'Two',
+                                       'Äâà'),
+    ('MENU SERVER PLAYERS',            'Local Players:',
+                                       'Ëîêàëüíûå èãðîêè:'),
+
+    ('MENU INTER MISSION FAIL',        'Mission Failed',
+                                       'ÌÈÑÑÈß ÏÐÎÂÀËÅÍÀ'),
+    ('MENU INTER ROUND OVER',          'Round Over',
+                                       'ÐÀÓÍÄ ÎÊÎÍ×ÅÍ'),
+    ('MENU INTER LEVEL COMPLETE',      'Level Completed',
+                                       'ÓÐÎÂÅÍÜ ÏÐÎÉÄÅÍ'),
+    ('MENU INTER TIME',                'Time:',
+                                       'ÂÐÅÌß:'),
+    ('MENU INTER KILLS',               'Kills:',
+                                       'ÓÁÈË:'),
+    ('MENU INTER KPM',                 'Kills-per-minute:',
+                                       'ÓÁÈÉÑÒ  ÌÈÍÓÒÓ:'),
+    ('MENU INTER SECRETS',             'Secrets found:',
+                                       'ÍÀØÅË ÑÅÊÐÅÒÎÂ:'),
+    ('MENU INTER NOTICE TAB',          'Press [TAB] to view stats',
+                                       'Íàæìèòå [TAB] äëÿ ïðîñìîòðà ñòàòèñòèêè'),
+    ('MENU INTER NOTICE MAP',          'Please wait until map change',
+                                       'Ïîäîæäèòå, ïîêà ñìåíèòñÿ êàðòà íà ñåðâåðå'),
+    ('MENU INTER NOTICE SPACE',        'Press [SPACE] to continue',
+                                       'Íàæìèòå [ÏÐÎÁÅË], ÷òîáû ïðîäîëæèòü'),
+    ('MENU INTER NOTICE TIME',         '%d seconds till the next round',
+                                       'Îñòàëîñü %d ñåêóíä äî ñëåäóþùåãî ðàóíäà'),
+    ('MENU LOADING',                   'Loading...',
+                                       'Çàãðóçêà...'),
+    ('MENU PLAYER 1',                  'Player 1',
+                                       'Ïåðâûé èãðîê'),
+    ('MENU PLAYER 2',                  'Player 2',
+                                       'Âòîðîé èãðîê'),
+
+    ('MENU CONTROL JOYSTICKS',         'Joystick Settings',
+                                       'Íàñòðîéêè äæîéñòèêîâ'),
+    ('MENU CONTROL DEADZONE',          'Joystick %d Deadzone',
+                                       'Ìåðòâàÿ çîíà äæîéñòèêà %d'),
+                                       
+    ('MENU CONTROL GLOBAL',            'Global Controls',
+                                       'ÎÁÙÅÅ ÓÏÐÀÂËÅÍÈÅ'),
+    ('MENU CONTROL SCREENSHOT',        'Screenshot:',
+                                       'Ñêðèíøîò:'),
+    ('MENU CONTROL STAT',              'Statistics:',
+                                       'Ñòàòèñòèêà:'),
+    ('MENU CONTROL CHAT',              'Chat:',
+                                       '×àò:'),
+    ('MENU CONTROL TEAMCHAT',          'Team chat:',
+                                       'Êîìàíäíûé ÷àò:'),
+    ('MENU CONTROL LEFT',              'Left:',
+                                       'Âëåâî:'),
+    ('MENU CONTROL RIGHT',             'Right:',
+                                       'Âïðàâî:'),
+    ('MENU CONTROL UP',                'Up:',
+                                       'Ââåðõ:'),
+    ('MENU CONTROL DOWN',              'Down:',
+                                       'Âíèç:'),
+    ('MENU CONTROL JUMP',              'Jump:',
+                                       'Ïðûæîê:'),
+    ('MENU CONTROL FIRE',              'Fire:',
+                                       'Îãîíü:'),
+    ('MENU CONTROL USE',               'Use:',
+                                       'Èñïîëüçîâàòü:'),
+    ('MENU CONTROL NEXT WEAPON',       'Next weapon:',
+                                       'Ñëåä. îðóæèå:'),
+    ('MENU CONTROL PREV WEAPON',       'Prev weapon:',
+                                       'Ïðåä. îðóæèå:'),
+
+    ('MENU COUNT NONE',                'None',
+                                       'Íåò'),
+    ('MENU COUNT SMALL',               'Little',
+                                       'Ìàëî'),
+    ('MENU COUNT NORMAL',              'Normal',
+                                       'Ñðåäíå'),
+    ('MENU COUNT BIG',                 'Lots',
+                                       'Ìíîãî'),
+    ('MENU COUNT VERYBIG',             'Massacre',
+                                       'Î÷åíü ìíîãî'),
+
+    ('MENU GAME BLOOD COUNT',          'Blood:',
+                                       'Êîëè÷åñòâî êðîâè:'),
+    ('MENU GAME MAX GIBS',             'Gib limit:',
+                                       'Ëèìèò êóñêîâ:'),
+    ('MENU GAME MAX CORPSES',          'Corpse limit:',
+                                       'Ëèìèò òðóïîâ:'),
+    ('MENU GAME GIBS COUNT',           'Gibs count:',
+                                       'Êóñêîâ çà ðàç:'),
+    ('MENU GAME MAX SHELLS',           'Shells limit:',
+                                       'Ëèìèò ãèëüç:'),
+    ('MENU GAME BLOOD TYPE',           'Blood type:',
+                                       'Òèï êðîâè:'),
+    ('MENU GAME BLOOD TYPE SIMPLE',    'Simple',
+                                       'Ïðîñòàÿ'),
+    ('MENU GAME BLOOD TYPE ADV',       'Dripping',
+                                       'Ïðîäâèíóòàÿ'),
+    ('MENU GAME CORPSE TYPE',          'Corpse type:',
+                                       'Òèï òðóïîâ:'),
+    ('MENU GAME CORPSE TYPE SIMPLE',   'Simple',
+                                       'Ïðîñòûå'),
+    ('MENU GAME CORPSE TYPE ADV',      'Interactive',
+                                       'Ïðîäâèíóòûå'),
+    ('MENU GAME GIBS TYPE',            'Gibs type:',
+                                       'Òèï êóñêîâ:'),
+    ('MENU GAME GIBS TYPE SIMPLE',     'Simple',
+                                       'Ïðîñòûå'),
+    ('MENU GAME GIBS TYPE ADV',        'Interactive',
+                                       'Ïðîäâèíóòûå'),
+    ('MENU GAME PARTICLES COUNT',      'Particle limit:',
+                                       'Êîëè÷åñòâî ÷àñòèö:'),
+    ('MENU GAME SCREEN FLASH',         'Screen flash:',
+                                       'Âñïûøêè ýêðàíà:'),
+    ('MENU GAME BACKGROUND',           'Draw background:',
+                                       'Ðèñîâàòü ôîí:'),
+    ('MENU GAME MESSAGES',             'Show messages:',
+                                       'Âûâîäèòü ñîîáùåíèÿ:'),
+    ('MENU GAME REVERT PLAYERS',       'Revert players:',
+                                       'Ïåðåâåðíóòü ýêðàíû:'),
+    ('MENU GAME CHAT BUBBLE',          'Chat bubbles:',
+                                       '×àò çíà÷êè:'),
+    ('MENU GAME CHAT TYPE NONE',       'None',
+                                       'Íåò'),
+    ('MENU GAME CHAT TYPE SIMPLE',     'Simple',
+                                       'Ïðîñòûå'),
+    ('MENU GAME CHAT TYPE ADV',        'Advanced',
+                                       'Ïðîäâèíóòûå'),
+    ('MENU GAME CHAT TYPE COLOR',      'Colored',
+                                       'Öâåòíûå'),
+    ('MENU GAME CHAT TYPE TEXTURE',    'Textured',
+                                       'Òåêñòóðíûå'),
+
+    ('MENU VIDEO RESOLUTION',          'Set video mode',
+                                       'Óñòàíîâêà âèäåîðåæèìà'),
+    ('MENU VIDEO BPP',                 'Color depth:',
+                                       'Ãëóáèíà öâåòà:'),
+    ('MENU VIDEO VSYNC',               'Vertical sync',
+                                       'Âåðò. ñèíõðîíèçàöèÿ:'),
+    ('MENU VIDEO FILTER SKY',          'Anisotropic sky',
+                                       'Ôèëüòðàöèÿ íåáà:'),
+    ('MENU VIDEO NEED RESTART',        'Video settings will be changed after game restart.',
+                                       'Äàííûå íàñòðîéêè âèäåî âñòóïÿò â ñèëó ïîñëå ïåðåçàïóñêà èãðû.'),
+
+    ('MENU RESOLUTION SELECT',         'SET VIDEO MODE',
+                                       'ÓÑÒÀÍÎÂÊÀ ÂÈÄÅÎÐÅÆÈÌÀ'),
+    ('MENU RESOLUTION CURRENT',        'Current:',
+                                       'Òåêóùèé:'),
+    ('MENU RESOLUTION LIST',           'New:',
+                                       'Íîâûé:'),
+    ('MENU RESOLUTION FULLSCREEN',     'Fullscreen:',
+                                       'Ïîëíûé ýêðàí:'),
+    ('MENU RESOLUTION APPLY',          'Apply',
+                                       'Ïðèìåíèòü'),
+
+    ('MENU SOUND MUSIC LEVEL',         'Music volume:',
+                                       'Ãðîìêîñòü ìóçûêè:'),
+    ('MENU SOUND SOUND LEVEL',         'Sound volume:',
+                                       'Ãðîìêîñòü çâóêà:'),
+    ('MENU SOUND MAX SIM SOUNDS',      'One sound count:',
+                                       'Êîë-âî îäíîãî çâóêà:'),
+    ('MENU SOUND INACTIVE SOUNDS',     'Window inactive:',
+                                       'Îêíî íåàêòèâíî:'),
+    ('MENU SOUND INACTIVE SOUNDS ON',  'Play sounds',
+                                       'Çâóêè åñòü'),
+    ('MENU SOUND INACTIVE SOUNDS OFF', 'Mute sounds',
+                                       'Çâóêîâ íåò'),
+    ('MENU SOUND ANNOUNCE',            'Announcements:',
+                                       'Îáúÿâëåíèÿ:'),
+    ('MENU SOUND COMPAT',              'Sound FX set:',
+                                       'Íàáîð çâóêîâ:'),
+    ('MENU ANNOUNCE NONE',             'None',
+                                       'Íåò'),
+    ('MENU ANNOUNCE ME',               'Only my',
+                                       'Òîëüêî ìîè'),
+    ('MENU ANNOUNCE MEPLUS',           'My + multikills',
+                                       'Ìîè + ìóëüòèêèëëû'),
+    ('MENU ANNOUNCE ALL',              'Everybody',
+                                       'Âñå'),
+    ('MENU COMPAT DF',                 'Doom 2D',
+                                       'Doom 2D'),
+    ('MENU COMPAT DOOM2',              'Doom II',
+                                       'Doom II'),
+
+    ('MENU PLAYER NAME',               'Name:',
+                                       'Èìÿ:'),
+    ('MENU PLAYER TEAM',               'Team:',
+                                       'Êîìàíäà:'),
+    ('MENU PLAYER TEAM RED',           'Red',
+                                       'Êðàñíàÿ'),
+    ('MENU PLAYER TEAM BLUE',          'Blue',
+                                       'Ñèíÿÿ'),
+    ('MENU PLAYER MODEL',              'Model:',
+                                       'Ìîäåëü:'),
+    ('MENU PLAYER RED',                'Red:',
+                                       'Êðàñíûé:'),
+    ('MENU PLAYER GREEN',              'Green:',
+                                       'Çåëåíûé:'),
+    ('MENU PLAYER BLUE',               'Blue:',
+                                       'Ñèíèé:'),
+
+    ('MENU MODEL INFO',                'Model info',
+                                       'Èíôîðìàöèÿ î ìîäåëè'),
+    ('MENU MODEL ANIMATION',           'Change anim',
+                                       'Ñìåíèòü àíèìàöèþ'),
+    ('MENU MODEL CHANGE WEAPON',       'Change weapon',
+                                       'Ñìåíèòü îðóæèå'),
+    ('MENU MODEL ROTATE',              'Reflect model',
+                                       'Ïîâåðíóòü ìîäåëü'),
+    ('MENU MODEL NAME',                'Model name:',
+                                       'Èìÿ:'),
+    ('MENU MODEL AUTHOR',              'Author:',
+                                       'Àâòîð:'),
+    ('MENU MODEL COMMENT',             'Description:',
+                                       'Êîììåíòàðèé:'),
+    ('MENU MODEL OPTIONS',             'Model Options:',
+                                       'Îïöèè ìîäåëè:'),
+    ('MENU MODEL WEAPON',              'Weapon:',
+                                       'Îðóæèå:'),
+
+    ('MENU LANGUAGE RUSSIAN',          'Ðóññêèé',
+                                       'Ðóññêèé'),
+    ('MENU LANGUAGE ENGLISH',          'English',
+                                       'English'),
+
+    ('MENU PAUSE',                     'Pause',
+                                       'ÏÀÓÇÀ'),
+    ('MENU YES',                       'Yes',
+                                       'Äà'),
+    ('MENU NO',                        'No',
+                                       'Íåò'),
+    ('MENU OK',                        'OK',
+                                       'OK'),
+    ('MENU FINISH',                    'Done',
+                                       'Ãîòîâî'),
+
+    ('MENU END GAME PROMT',            'Are you sure to end the game?',
+                                       'Âû äåéñòâèòåëüíî õîòèòå çàêîí÷èòü èãðó?'),
+    ('MENU RESTART GAME PROMT',        'Are you sure to restart level?',
+                                       'Âû äåéñòâèòåëüíî õîòèòå íà÷àòü óðîâåíü çàíîâî?'),
+    ('MENU EXIT PROMT',                'Chickening out already?',
+                                       'Âû äåéñòâèòåëüíî õîòèòå âûéòè èç Doom 2D: Forever?'),
+    ('MENU SET DEFAULT PROMT',         'Load default settings?',
+                                       'Èçìåíèòü âñå íàñòðîéêè íà ñòàíäàðòíûå?'),
+    ('MENU LOAD SAVED PROMT',          'Load saved settings?',
+                                       'Âåðíóòü âñå íàñòðîéêè íà ñîõðàí¸ííûå?'),
+    ('MENU ENTER PASSWORD',            'This server requires a password to join.',
+                                       'Ýòîò ñåðâåð òðåáóåò ïàðîëü.'),
+
+    ('NET SERVER',                     'Server',
+                                       'Ñåðâåð'),
+    ('NET CLIENT',                     'Client',
+                                       'Êëèåíò'),
+    ('NET ADDRESS',                    'Address:',
+                                       'Àäðåñ:'),
+    ('NET PORT',                       'Port:',
+                                       'Ïîðò:'),
+    ('NET SERVER NAME',                'Server name:',
+                                       'Íàçâàíèå:'),
+    ('NET SERVER PASSWORD',            'Password:',
+                                       'Ïàðîëü:'),
+    ('NET MAX CLIENTS',                'Player limit:',
+                                       'Ëèìèò èãðîêîâ:'),
+    ('NET USE MASTER',                 'Public server:',
+                                       'Ïóáëè÷íûé ñåðâåð:'),
+
+    ('NET MSG',                        'NET: ',
+                                       'NET: '),
+    ('NET MSG ERROR',                  'NET: ERROR: ',
+                                       'NET: ÎØÈÁÊÀ: '),
+    ('NET MSG FERROR',                 'NET: FATAL ERROR: ',
+                                       'NET: ÊÐÈÒÈ×ÅÑÊÀß ÎØÈÁÊÀ: '),
+
+    ('NET MSG HOST',                   'Starting server on port %d...',
+                                       'Âêëþ÷àåì ñåðâåð íà ïîðòó %d...'),
+    ('NET MSG HOST DISCALL',           'Disconnecting clients...',
+                                       'Îòêëþ÷àåì èãðîêîâ...'),
+    ('NET MSG HOST DIE',               'Shutting down server...',
+                                       'Âûêëþ÷àåì ñåðâåð...'),
+    ('NET MSG HOST CONN',              'Something is trying to connect from %s:%d.',
+                                       'Ïîïûòêà ñîåäèíåíèÿ ñ àäðåñà %s:%d.'),
+    ('NET MSG HOST REJECT',            'Connection rejected. Reason: ',
+                                       ' ñîåäèíåíèè îòêàçàíî. Ïðè÷èíà: '),
+    ('NET MSG HOST ADD',               'Client #%d added. Awaiting info...',
+                                       'Äîáàâëåí êëèåíò ¹%d. Æäåì îòâåòà...'),
+    ('NET MSG HOST DISC',              'Client #%d disconnected.',
+                                       'Êëèåíò ¹%d îòêëþ÷åí.'),
+    ('NET MSG CLIENT CONN',            'Trying to connect to %s:%d...',
+                                       'Ïûòàåìñÿ ñîåäèíèòüñÿ ñ %s:%d...'),
+    ('NET MSG CLIENT DONE',            'Connection succeeded!',
+                                       'Ñîåäèíåíèå âûïîëíåíî.'),
+    ('NET MSG CLIENT DISC',            'Disconnected.',
+                                       'Îòêëþ÷åíèå âûïîëíåíî.'),
+    ('NET MSG KICK',                   'You were dropped from the game! Reason: ',
+                                       'Âàñ âûáðîñèëî ñ ñåðâåðà! Ïðè÷èíà: '),
+
+    ('NET MAP DL',                     'Map %s not found. Downloading from server...',
+                                       'Êàðòà %s íå íàéäåíà. Ñêà÷èâàåì ñ ñåðâåðà...'),
+    ('NET WAD DL',                     'WAD %s not found. Downloading from server...',
+                                       'WAD %s íå íàéäåí. Ñêà÷èâàåì ñ ñåðâåðà...'),
+
+    ('NET ERR ENET',                   'Failed to init ENet!',
+                                       'Íå ïîëó÷èëîñü ïîäêëþ÷èòü ENet!'),
+    ('NET ERR HOST',                   'Could not start server on port %d!',
+                                       'Íå ïîëó÷èëîñü âêëþ÷èòü ñåðâåð íà ïîðòó %d!'),
+    ('NET ERR INGAME',                 'You are already in a net game!',
+                                       'Âû óæå â ñåòåâîé èãðå!'),
+    ('NET ERR CLIENT',                 'Could not create client!',
+                                       'Íå ïîëó÷èëîñü ñîçäàòü êëèåíò!'),
+    ('NET ERR CONN',                   'Connection failed!',
+                                       'Íå âûøëî ñîåäèíèòüñÿ!'),
+    ('NET ERR TIMEOUT',                'Connection timed out!',
+                                       'Ñåðâåð íå îòâå÷àåò!'),
+    ('NET ERR HASH',                   'Your WAD is different from server''s!',
+                                       'Âàø WAD îòëè÷àåòñÿ îò ñåðâåðíîãî!'),
+    ('NET ERR NAME404',                'Player or client %s not found!',
+                                       'Èãðîê èëè êëèåíò %s íå íàéäåí!'),
+
+    ('NET DISC NONE',                  'Server is down.',
+                                       'Ñåðâåð íå îòâå÷àåò.'),
+    ('NET DISC PROTOCOL',              'Net protocol version mismatch.',
+                                       'Âåðñèè ñåòåâûõ ïðîòîêîëîâ íå ñîâïàäàþò.'),
+    ('NET DISC VERSION',               'Game version mismatch.',
+                                       'Âåðñèÿ èãðû íå ñîâïàäàåò ñ âåðñèåé ñåðâåðà.'),
+    ('NET DISC FULL',                  'Server is full.',
+                                       'Ñåðâåð ïîëîí.'),
+    ('NET DISC KICK',                  'Kicked by server admin.',
+                                       'Âû áûëè îòêëþ÷åíû àäìèíèñòðàòîðîì.'),
+    ('NET DISC DOWN',                  'Server is shutting down.',
+                                       'Ñåðâåð âûêëþ÷àåòñÿ.'),
+    ('NET DISC PASSWORD',              'Invalid password.',
+                                       'Íåâåðíûé ïàðîëü.'),
+    ('NET DISC TEMPBAN',               'You are banned until the round end.',
+                                       'Âû çàáàíåíû äî êîíöà ðàóíäà.'),
+    ('NET DISC BAN',                   'You are banned. Contact the server administrator.',
+                                       'Âû çàáàíåíû. Ñâÿæèòåñü ñ àäìèíèñòðàòîðîì ñåðâåðà.'),
+
+    ('NET SLIST',                      'Server list',
+                                       'Ñïèñîê ñåðâåðîâ'),
+    ('NET SLIST NOSERVERS',            'No active servers.',
+                                       'Íåò àêòèâíûõ ñåðâåðîâ.'),
+    ('NET SLIST SERVERS',              ' server(s)',
+                                       ' ñåðâåð(à, îâ)'),
+    ('NET SLIST HELP',                 '[ENTER] - join   [SPACE] - refresh   [ESC] - quit',
+                                       '[ENTER] - ïðèñîåäèíèòüñÿ   [SPACE] - îáíîâèòü   [ESC] - ìåíþ'),
+    ('NET SLIST WAIT',                 'Please wait...',
+                                       'Ïîäîæäèòå...'),
+    ('NET SLIST PING MS',              ' ms',
+                                       ' ìñ'),
+    ('NET SLIST NO ACCESS',            'N/A',
+                                       'Í/Ä'),
+
+    ('NET SLIST FETCH',                'Fetching server list...',
+                                       'Ïîëó÷àåì ñïèñîê ñåðâåðîâ...'),
+    ('NET SLIST RETRIEVED',            '%d servers retrieved.',
+                                       'Ïîëó÷åíî ñåðâåðîâ: %d'),
+    ('NET SLIST CONN',                 'Connected to masterserver.',
+                                       'Ïîäêëþ÷èëèñü ê ìàñòåðñåðâåðó.'),
+    ('NET SLIST DISC',                 'Disconnected from masterserver.',
+                                       'Îòêëþ÷èëèñü îò ìàñòåðñåðâåðà.'),
+    ('NET SLIST LOST',                 'Lost connection with masterserver.',
+                                       'Ñîåäèíåíèå ñ ìàñòåðñåðâåðîì ïîòåðÿíî.'),
+    ('NET SLIST ERROR',                'Could not connect to masterserver.',
+                                       'Íå óäàëîñü ñîåäèíèòüñÿ ñ ìàñòåðñåðâåðîì.'),
+
+    ('NET RCON VALID PWD',             'RCON password accepted. You can use RCON now.',
+                                       'Ïàðîëü ïðèíÿò. Òåïåðü âû ìîæåòå èñïîëüçîâàòü RCON.'),
+    ('NET RCON INVALID PWD',           'RCON password is invalid.',
+                                       'Íåâåðíûé RCON ïàðîëü.'),
+    ('NET RCON NOAUTH',                'You are not authorized to use RCON.',
+                                       'Âû íå èìååòå ïðàâà èñïîëüçîâàòü RCON.'),
+
+    ('PLAYER DIED',                    '*** %s died',
+                                       '*** %s ïîãèá'),
+    ('PLAYER KILL',                    '*** %s was killed by %s',
+                                       '*** %s áûë óáèò %s'),
+    ('PLAYER KILL EXTRAHARD 1',        '*** %s was fragged by %s',
+                                       '*** %s áûë ðàçîðâàí íà êóñêè %s'),
+    ('PLAYER KILL EXTRAHARD 2',        '*** %s was murdered by %s',
+                                       '*** %s áûë çâåðñêè óáèò %s'),
+    ('PLAYER KILL ACID',               '*** %s dissolved in acid',
+                                       '*** %s óòîíóë â êèñëîòå'),
+    ('PLAYER KILL TRAP',               '*** %s got caught in a trap',
+                                       '*** %s íàïîðîëñÿ íà ëîâóøêó'),
+    ('PLAYER KILL FALL',               '*** %s fell too far',
+                                       '*** %s óëåòåë'),
+    ('PLAYER KILL SELF',               '*** %s killed himself',
+                                       '*** %s óáèë ñåáÿ'),
+    ('PLAYER KILL WATER',              '*** %s drowned',
+                                       '*** %s óòîíóë'),
+
+    ('PLAYER KILL 2X',                 '%s has a double kill!',
+                                       '%s ñîâåðøèë äâîéíîå óáèéñòâî!'),
+    ('PLAYER KILL 3X',                 '%s has a triple kill!',
+                                       '%s ñîâåðøèë òðîéíîå óáèéñòâî!'),
+    ('PLAYER KILL 4X',                 '%s has a multi kill!',
+                                       '%s ñîâåðøèë ìíîãî óáèéñòâ ïîäðÿä!'),
+    ('PLAYER KILL MX',                 '%s has a m-m-m-monster kill!!!',
+                                       '%s ì-ì-ì-ìîíñòð óáèéöà!!!'),
+
+    ('PLAYER JOIN',                    '%s joined the game.',
+                                       '%s ïðèñîåäèíèëñÿ ê èãðå.'),
+    ('PLAYER LEAVE',                   '%s left the game.',
+                                       '%s ïîêèíóë èãðó.'),
+    ('PLAYER KICK',                    '%s was kicked by admin.',
+                                       '%s áûë îòêëþ÷åí àäìèíèñòðàòîðîì.'),
+    ('PLAYER BAN',                     '%s was banned by admin.',
+                                       '%s áûë çàáàíåí àäìèíèñòðàòîðîì.'),
+    ('PLAYER NAME',                    '%s is now known as %s.',
+                                       '%s òåïåðü èçâåñòåí êàê %s.'),
+    ('PLAYER CHTEAM RED',              '%s is now on Red.',
+                                       '%s ïåðåøåë â Êðàñíóþ êîìàíäó.'),
+    ('PLAYER CHTEAM BLUE',             '%s is now on Blue.',
+                                       '%s ïåðåøåë â Ñèíþþ êîìàíäó.'),
+
+    ('PLAYER SPECT',                   'Spectating',
+                                       'Íàáëþäåíèå'),
+    ('PLAYER SPECT1',                  '[USE] - spawn',
+                                       '[ÎÒÊÐÛÒÜ] - îæèòü'),
+    ('PLAYER SPECT1S',                 '(next round)',
+                                       '(â ñëåäóþùåì ðàóíäå)'),
+    ('PLAYER SPECT2',                  '[FIRE] - chase',
+                                       '[ÎÃÎÍÜ] - ñëåäèòü'),
+    ('PLAYER SPECT3',                  '[JUMP] - reset',
+                                       '[ÏÐÛÆÎÊ] - ñáðîñ'),
+    ('PLAYER SPECT4',                  'You can''t spawn until current round ends.',
+                                       'Âû íå ñìîæåòå âîçðîäèòüñÿ äî êîíöà ðàóíäà.'),
+
+    ('PLAYER FLAG GET',                '*** %s stole the %s flag!',
+                                       '*** %s ñõâàòèë %s ôëàã!'),
+    ('PLAYER FLAG RETURN',             '*** %s returned the %s flag!',
+                                       '*** %s âåðíóë %s ôëàã!'),
+    ('PLAYER FLAG CAPTURE',            '*** %s captured the %s flag! (%s ms)',
+                                       '*** %s ïðèíåñ %s ôëàã! (%s ìñ)'),
+    ('PLAYER FLAG DROP',               '*** %s dropped the %s flag!',
+                                       '*** %s ïîòåðÿë %s ôëàã!'),
+    ('PLAYER FLAG RED',                'red',
+                                       'êðàñíûé'),
+    ('PLAYER FLAG BLUE',               'blue',
+                                       'ñèíèé'),
+
+    ('PLAYER SCORE RED',               'Red',
+                                       'Êðàñíàÿ'),
+    ('PLAYER SCORE BLUE',              'Blue',
+                                       'Ñèíÿÿ'),
+    ('PLAYER SCORE TO RED',            'red',
+                                       'êðàñíîé'),
+    ('PLAYER SCORE TO BLUE',           'blue',
+                                       'ñèíåé'),
+    ('PLAYER SCORE ADD OWN',           '*** %s got %d points for %s team!',
+                                       '*** %s çàðàáîòàë %d î÷êîâ %s êîìàíäå!'),
+    ('PLAYER SCORE SUB OWN',           '*** %s lost %d points of %s team...',
+                                       '*** %s ïîòåðÿë %d î÷êîâ %s êîìàíäû...'),
+    ('PLAYER SCORE ADD ENEMY',         '*** %s gave %d points to %s team...',
+                                       '*** %s îòäàë %d î÷êîâ %s êîìàíäå...'),
+    ('PLAYER SCORE SUB ENEMY',         '*** %s took %d points from %s team!',
+                                       '*** %s îòíÿë %d î÷êîâ ó %s êîìàíäû!'),
+    ('PLAYER SCORE WIN OWN',           '*** %s helped to win %s team!',
+                                       '*** %s ïîìîã ïîáåäèòü %s êîìàíäå!'),
+    ('PLAYER SCORE WIN ENEMY',         '*** %s gave victory to %s team...',
+                                       '*** %s îòäàë ïîáåäó %s êîìàíäå...'),
+    ('PLAYER SCORE ADD TEAM',          '*** %s team got %d points!',
+                                       '*** %s êîìàíäà ïîëó÷àåò %d î÷êîâ!'),
+    ('PLAYER SCORE SUB TEAM',          '*** %s team loses %d points...',
+                                       '*** %s êîìàíäà òåðÿåò %d î÷êîâ...'),
+
+    ('MESSAGE FLAG GET',               '%s flag stolen',
+                                       '%s ÔËÀàÓÊÐÀÄÅÍ'),
+    ('MESSAGE FLAG RETURN',            '%s flag returned',
+                                       '%s ÔËÀàÂÎÇÂÐÀÙ¨Í'),
+    ('MESSAGE FLAG CAPTURE',           '%s flag captured',
+                                       '%s ÔËÀàÇÀÕÂÀ×ÅÍ'),
+    ('MESSAGE FLAG DROP',              '%s flag dropped',
+                                       '%s ÔËÀàÏÎÒÅÐßÍ'),
+
+    ('MESSAGE LMS LOSE',               'YOU''RE ALL LOSERS',
+                                       'ÂÑÅ ÏÐÎÈÃÐÀËÈ'),
+    ('MESSAGE LMS WIN',                '%s WINS',
+                                       '%s ÏÎÁÅÄÈË'),
+    ('MESSAGE LMS START',              'FIGHT!',
+                                       'ÏÎÅÕÀËÈ!'),
+    ('MESSAGE TLMS WIN',               '%s TEAM WINS',
+                                       '%s ÊÎÌÀÍÄÀ ÏÎÁÅÄÈËÀ'),
+    ('MESSAGE LMS SURVIVOR',           'YOU ARE THE LAST ONE ALIVE!',
+                                       'ÂÛ - ÏÎÑËÅÄÍÈÉ ÃÅÐÎÉ!'),
+
+    ('MESSAGE SCORE ADD',              '%s TEAM SCORES',
+                                       '%s ÊÎÌÀÍÄÀ ÏÎËÓ×ÈËÀ Î×ÊÈ'),
+    ('MESSAGE SCORE SUB',              '%s TEAM FOULS',
+                                       '%s ÊÎÌÀÍÄÀ ÏÎÒÅÐßËÀ Î×ÊÈ'),
+
+    ('MESSAGE VOTE INPROGRESS',        'A vote is already in progress for "%s".',
+                                       'Óæå èäåò ãîëîñîâàíèå çà êîìàíäó "%s".'),
+    ('MESSAGE VOTE STARTED',           '%s called a vote for "%s".'#10'%d votes are required for it to pass.',
+                                       '%s íà÷àë ãîëîñîâàíèå çà êîìàíäó "%s".'#10'Äëÿ âûïîëíåíèÿ êîìàíäû òðåáóåòñÿ ãîëîñîâ: %d.'),
+    ('MESSAGE VOTE PASSED',            'Vote passed. Executing "%s" in 5 seconds.',
+                                       'Ãîëîñîâàíèå ïðîøëî óñïåøíî. Êîìàíäà "%s" áóäåò âûïîëíåíà ÷åðåç 5 ñåêóíä.'),
+    ('MESSAGE VOTE FAILED',            'Vote failed: not enough supporters.',
+                                       'Ãîëîñîâàíèå ïðîâàëèëîñü: íåäîñòàòî÷íî ãîëîñîâ "çà".'),
+    ('MESSAGE VOTE VOTE',              '%s votes "yes" (%d/%d votes total).',
+                                       '%s ãîëîñóåò "çà" (%d/%d ãîëîñîâ âñåãî).'),
+    ('MESSAGE VOTE REVOKED',           '%s revokes his vote (%d/%d votes total).',
+                                       '%s îòîçâàë ñâîé ãîëîñ (%d/%d ãîëîñîâ âñåãî).'),
+
+    ('KEY UP',                         'Up',
+                                       'Ââåðõ'),
+    ('KEY DOWN',                       'Down',
+                                       'Âíèç'),
+    ('KEY LEFT',                       'Left',
+                                       'Âëåâî'),
+    ('KEY RIGHT',                      'Right',
+                                       'Âïðàâî'),
+
+    ('MONSTER DEMON',                  'Pinky',
+                                       'Äåìîíîì'),
+    ('MONSTER IMP',                    'Imp',
+                                       'Áåñîì'),
+    ('MONSTER ZOMBIE',                 'Zombie',
+                                       'Çîìáè'),
+    ('MONSTER SERGEANT',               'Shotgun Guy',
+                                       'Ñåðæàíòîì'),
+    ('MONSTER CYBER',                  'Cyberdemon',
+                                       'Êèáåðäåìîíîì'),
+    ('MONSTER CGUN',                   'Commando',
+                                       'Ïóëåìåò÷èêîì'),
+    ('MONSTER BARON',                  'Hell Baron',
+                                       'Áàðîíîì àäà'),
+    ('MONSTER KNIGHT',                 'Hell Knight',
+                                       'Ðûöàðåì àäà'),
+    ('MONSTER CACODEMON',              'Cacodemon',
+                                       'Êàêîäåìîíîì'),
+    ('MONSTER SOUL',                   'Lost Soul',
+                                       'Îãíåííûì ÷åðåïîì'),
+    ('MONSTER PAIN',                   'Pain Elemental',
+                                       'Àâèàáàçîé'),
+    ('MONSTER MASTERMIND',             'Spider Mastermind',
+                                       'Áîëüøèì ïàóêîì'),
+    ('MONSTER SPIDER',                 'Arachnotron',
+                                       'Àðàõíîòðîíîì'),
+    ('MONSTER MANCUBUS',               'Mancubus',
+                                       'Ìàíêóáóñîì'),
+    ('MONSTER REVENANT',               'Revenant',
+                                       'Ñêåëåòîì'),
+    ('MONSTER ARCHVILE',               'Arch-Vile',
+                                       'Êîëäóíîì'),
+    ('MONSTER FISH',                   'Piranha',
+                                       'Ðûáîé'),
+    ('MONSTER BARREL',                 'Barrel explosion',
+                                       'âçðûâîì Áî÷êè'),
+    ('MONSTER ROBOT',                  'Robot',
+                                       'Ðîáîòîì'),
+    ('MONSTER PRIKOLIST',              'Prikolist',
+                                       'Ïðèêîëèñòîì'),
+
+    ('LOAD MUSIC',                     'Music',
+                                       'Ìóçûêà'),
+    ('LOAD MODELS',                    'Models',
+                                       'Ìîäåëè'),
+    ('LOAD MENUS',                     'Menus',
+                                       'Ìåíþ'),
+    ('LOAD CONSOLE',                   'Console',
+                                       'Êîíñîëü'),
+    ('LOAD ITEMS DATA',                'Items Data',
+                                       'Äàííûå ïðåäìåòîâ'),
+    ('LOAD WEAPONS DATA',              'Weapons Data',
+                                       'Äàííûå îðóæèÿ'),
+    ('LOAD GAME DATA',                 'Game Data',
+                                       'Äàííûå èãðû'),
+    ('LOAD COLLIDE MAP',               'Collide Map',
+                                       'Êàðòà ñòîëêíîâåíèé'),
+    ('LOAD DOOR MAP',                  'Door Map',
+                                       'Êàðòà äâåðåé'),
+    ('LOAD LIFT MAP',                  'Lift Map',
+                                       'Êàðòà ëèôòîâ'),
+    ('LOAD WATER MAP',                 'Water Map',
+                                       'Êàðòà âîäû'),
+    ('LOAD WAD FILE',                  'WAD File',
+                                       'WAD-ôàéë'),
+    ('LOAD MAP',                       'Map',
+                                       'Êàðòà'),
+    ('LOAD TEXTURES',                  'Textures',
+                                       'Òåêñòóðû'),
+    ('LOAD TRIGGERS',                  'Triggers',
+                                       'Òðèããåðû'),
+    ('LOAD PANELS',                    'Panels',
+                                       'Ïàíåëè'),
+    ('LOAD TRIGGERS TABLE',            'Trigger table',
+                                       'Òàáëèöà òðèããåðîâ'),
+    ('LOAD LINK TRIGGERS',             'Trigger links',
+                                       'Ïðèâÿçêà òðèããåðîâ'),
+    ('LOAD CREATE TRIGGERS',           'Trigger setup',
+                                       'Äîáàâëåíèå òðèããåðîâ'),
+    ('LOAD ITEMS',                     'Items',
+                                       'Ïðåäìåòû'),
+    ('LOAD CREATE ITEMS',              'Item setup',
+                                       'Äîáàâëåíèå ïðåäìåòîâ'),
+    ('LOAD AREAS',                     'Areas',
+                                       'Îáëàñòè'),
+    ('LOAD CREATE AREAS',              'Area setup',
+                                       'Äîáàâëåíèå îáëàñòåé'),
+    ('LOAD MONSTERS',                  'Monsters',
+                                       'Ìîíñòðû'),
+    ('LOAD CREATE MONSTERS',           'Monster setup',
+                                       'Äîáàâëåíèå ìîíñòðîâ'),
+    ('LOAD MAP HEADER',                'Map Description',
+                                       'Îïèñàíèå êàðòû'),
+    ('LOAD SKY',                       'Background',
+                                       'Ôîí'),
+    ('LOAD MONSTER TEXTURES',          'Monsters'' Textures',
+                                       'Òåêñòóðû ìîíñòðîâ'),
+    ('LOAD MONSTER SOUNDS',            'Monsters'' Sounds',
+                                       'Çâóêè ìîíñòðîâ'),
+    ('LOAD SAVE FILE',                 'Save File',
+                                       'Ôàéë ñîõðàíåíèÿ'),
+    ('LOAD MAP STATE',                 'Map State',
+                                       'Íàñòðîéêà êàðòû'),
+    ('LOAD ITEMS STATE',               'Items State',
+                                       'Ðàñïîëîæåíèå ïðåäìåòîâ'),
+    ('LOAD TRIGGERS STATE',            'Triggers State',
+                                       'Óñòàíîâêà òðèããåðîâ'),
+    ('LOAD WEAPONS STATE',             'Weapons State',
+                                       'Ðàñïîëîæåíèå îðóæèÿ'),
+    ('LOAD MONSTERS STATE',            'Monsters State',
+                                       'Ðàñïîëîæåíèå ìîíñòðîâ'),
+    ('LOAD CONNECT',                   'Connecting ([ESC] to abort)',
+                                       'Ñîåäèíåíèå ([ESC] - îòìåíèòü)'),
+    ('LOAD SEND INFO',                 'Sending client info',
+                                       'Îòñûëêà èíôîðìàöèè'),
+    ('LOAD WAIT INFO',                 'Awaiting response ([ESC] to abort)',
+                                       'Îæèäàíèå îòâåòà ([ESC] - îòìåíèòü)'),
+    ('LOAD DL RES',                    'Downloading resources',
+                                       'Ñêà÷èâàíèå ðåñóðñîâ'),
+
+    ('CREDITS CAP 1',                  'Doom 2D: Forever',
+                                       'Doom 2D: Forever'),
+    ('CREDITS CAP 2',                  'version %s, proto %d',
+                                       'Âåðñèÿ %s, ïðîòîêîë %d'),
+    ('CREDITS A 1',                    'Project Author:',
+                                       'Àâòîð ïðîåêòà:'),
+    ('CREDITS A 1 1',                  'rs.falcon',
+                                       'rs.falcon'),
+    ('CREDITS A 2',                    'Programmers:',
+                                       'Ïðîãðàììèñòû:'),
+    ('CREDITS A 2 1',                  'rs.falcon, PSS, PrimuS, Stas''M, ZURG, OutCast',
+                                       'rs.falcon, PSS, PrimuS, Stas''M, ZURG, OutCast'),
+    ('CREDITS A 3',                    'Artists:',
+                                       'Õóäîæíèêè:'),
+    ('CREDITS A 3 1',                  'Jabberwock, Firehawk',
+                                       'Jabberwock, Firehawk'),
+    ('CREDITS A 4',                    'Misc:',
+                                       'Àññèñòåíòû:'),
+    ('CREDITS A 4 1',                  'Jabberwock, Black Doomer, DEAD, Grom PE',
+                                       'Jabberwock, ×åðíûé Äóìåð, DEAD, Grom PE'),
+    ('CREDITS CAP 3',                  'Special thanks to:',
+                                       'Áëàãîäàðèì:'),
+    ('CREDITS CLO 1',                  '- Prikol Software for Doom 2D',
+                                       '- Prikol Software çà Doom 2D'),
+    ('CREDITS CLO 2',                  '- id Software for starting it all',
+                                       '- id Software, áåç êîãî íè÷åãî áû íå áûëî'),
+    ('CREDITS CLO 3',                  '- Everyone who helped our project',
+                                       '- Âñåõ, êòî ïîìîãàë íàøåìó ïðîåêòó'),
+    ('CREDITS CLO 4',                  'www.doom2d.org, 2003-2016',
+                                       'www.doom2d.org, 2003-2016'),
+
+    ('MSG SHOW FPS ON',                'FPS counter enabled',
+                                       'FPS ïîêàçûâàþòñÿ'),
+    ('MSG SHOW FPS OFF',               'FPS counter disabled',
+                                       'FPS íå ïîêàçûâàþòñÿ'),
+    ('MSG GAMEMODE CURRENT',           'Game Mode: %s',
+                                       'Ðåæèì èãðû: %s'),
+    ('MSG GAMEMODE CHANGE',            'Game Mode: %s (switch to %s next round)',
+                                       'Ðåæèì èãðû: %s (áóäåò %s â ñëåäóþùåì ðàóíäå)'),
+    ('MSG FRIENDLY FIRE ON',           'Friendly Fire enabled',
+                                       'Óðîí ñâîèõ âêëþ÷åí'),
+    ('MSG FRIENDLY FIRE OFF',          'Friendly Fire disabled',
+                                       'Óðîíà ñâîèõ íåò'),
+    ('MSG WEAPONSTAY ON',              'Weapon Stay enabled',
+                                       'Îðóæèå îñòàåòñÿ ïîñëå ïîäáîðà'),
+    ('MSG WEAPONSTAY OFF',             'Weapon Stay disabled',
+                                       'Îðóæèå èñ÷åçàåò ïîñëå ïîäáîðà'),
+    ('MSG ALLOWEXIT ON',               'Exit enabled*',
+                                       'Âûõîä ñ êàðòû ðàçðåøåí*'),
+    ('MSG ALLOWEXIT OFF',              'Exit disabled*',
+                                       'Âûõîä ñ êàðòû çàïðåùåí*'),
+    ('MSG ALLOWMON ON',                'Monsters in DM enabled*',
+                                       'Ìîíñòðû â DM âêëþ÷åíû*'),
+    ('MSG ALLOWMON OFF',               'Monsters in DM disabled*',
+                                       'Ìîíñòðû â DM âûêëþ÷åíû*'),
+    ('MSG BOTSVSPLAYERS ON',           'Bots fight players',
+                                       'Áîòû ïðîòèâ èãðîêîâ'),
+    ('MSG BOTSVSPLAYERS OFF',          'Bots don''t fight players',
+                                       'Áîòû çà èãðîêîâ'),
+    ('MSG BOTSVSMONSTERS ON',          'Bots fight monsters',
+                                       'Áîòû ïðîòèâ ìîíñòðîâ'),
+    ('MSG BOTSVSMONSTERS OFF',         'Bots don''t fight monsters',
+                                       'Áîòû èãíîðèðóþò ìîíñòðîâ'),
+    ('MSG TIME ON',                    'Time display on',
+                                       'Âðåìÿ ïîêàçûâàåòñÿ'),
+    ('MSG TIME OFF',                   'Time display off',
+                                       'Âðåìÿ íå ïîêàçûâàåòñÿ'),
+    ('MSG SCORE ON',                   'Score display on',
+                                       'Î÷êè ïîêàçûâàþòñÿ'),
+    ('MSG SCORE OFF',                  'Score display off',
+                                       'Î÷êè íå ïîêàçûâàþòñÿ'),
+    ('MSG STATS ON',                   'Stats display on',
+                                       'Ñòàòèñòèêà ïîêàçûâàåòñÿ'),
+    ('MSG STATS OFF',                  'Stats display off',
+                                       'Ñòàòèñòèêà íå ïîêàçûâàåòñÿ'),
+    ('MSG KILL MSGS ON',               'Death Messages on',
+                                       'Ñîîáùåíèÿ î ñìåðòè åñòü'),
+    ('MSG KILL MSGS OFF',              'Death Messages off',
+                                       'Ñîîáùåíèé î ñìåðòè íåò'),
+    ('MSG LIVES ON',                   'Lives display on',
+                                       'Æèçíè ïîêàçûâàþòñÿ'),
+    ('MSG LIVES OFF',                  'Lives display off',
+                                       'Æèçíè íå ïîêàçûâàþòñÿ'),
+    ('MSG SPECT HUD ON',               'Spectator HUD on',
+                                       'Ñîñòîÿíèå íàáëþäàòåëÿ ïîêàçûâàåòñÿ'),
+    ('MSG SPECT HUD OFF',              'Spectator HUD off',
+                                       'Ñîñòîÿíèå íàáëþäàòåëÿ íå ïîêàçûâàåòñÿ'),
+    ('MSG PING ON',                    'Client ping display on',
+                                       'Ïèíã êëèåíòà ïîêàçûâàåòñÿ'),
+    ('MSG PING OFF',                   'Client ping display off',
+                                       'Ïèíã êëèåíòà íå ïîêàçûâàåòñÿ'),
+    ('MSG NO WAD',                     'WAD "%s" doesn''t exist!',
+                                       'WAD ôàéë "%s" íå íàéäåí!'),
+    ('MSG NO MAPS',                    'WAD "%s" doesn''t contain any maps!',
+                                       'WAD ôàéë "%s" íå ñîäåðæèò êàðò!'),
+    ('MSG NO MAP',                     'Map "%s" doesn''t exist!',
+                                       'Êàðòà "%s" íå íàéäåíà!'),
+    ('MSG NO MONSTER',                 'Wrong monster type: "%s"!',
+                                       '"%s" - íåò òàêîãî ìîíñòðà!'),
+    ('MSG SCORE LIMIT',                'Score Limit is %d',
+                                       'Ëèìèò î÷êîâ - %d'),
+    ('MSG TIME LIMIT',                 'Time Limit is %d:%.2d:%.2d',
+                                       'Ëèìèò âðåìåíè - %d:%.2d:%.2d'),
+    ('MSG LIVES',                      'Max lives: %d',
+                                       'Ìàêñ. æèçíåé ó èãðîêîâ: %d'),
+    ('MSG WARMUP',                     'Warmup time: %d seconds*',
+                                       'Âðåìÿ íà ïîäãîòîâêó ê èãðå: %d ñåêóíä*'),
+    ('MSG WARMUP START',               '%d seconds until round start.',
+                                       '%d ñåêóíä äî íà÷àëà ðàóíäà.'),
+    ('MSG NEXTMAP SET',                'Next map: %s',
+                                       'Ñëåäóþùàÿ êàðòà: %s'),
+    ('MSG NEXTMAP UNSET',              'Next map is not set.',
+                                       'Ñëåäóþùàÿ êàðòà íå çàäàíà.'),
+    ('MSG ONMAPCHANGE',                '* Changes to this variable will be applied upon next map change',
+                                       '* Èçìåíåíèÿ âñòóïÿò â ñèëó ïîñëå ñëåäóþùåé ñìåíû êàðòû'),
+    ('MSG NOT GAME',                   'Command can be accepted only in game.',
+                                       'Êîìàíäà ïðèìåíèìà òîëüêî â èãðå.'),
+    ('MSG NOT DEBUG',                  'Command can be accepted only in debug mode.',
+                                       'Êîìàíäà ïðèìåíèìà òîëüêî â ðåæèìå îòëàäêè.'),
+    ('MSG GM UNAVAIL',                 'This command is unavailable in current game mode.',
+                                       'Ýòà êîìàíäà íåäîñòóïíà â òåêóùåì ðåæèìå èãðû.'),
+    ('MSG SERVERONLY',                 'This command is for server only.',
+                                       'Ýòà êîìàíäà äîñòóïíà òîëüêî â ðåæèìå ñåðâåðà.'),
+    ('MSG NOCLIENTS',                  'No clients connected.',
+                                       'Íåò ïîäêëþ÷¸ííûõ êëèåíòîâ.'),
+    ('MSG UNBAN OK',                   'Client %s was unbanned.',
+                                       'Áûë ñíÿò áàí ñ êëèåíòà %s.'),
+    ('MSG UNBAN FAIL',                 'Client %s not in ban list.',
+                                       'Êëèåíò %s íå ÷èñëèòñÿ â ñïèñêå çàáàíåíûõ.'),
+
+    ('TEXTURE ENDPIC',                 'ENDGAME_EN',
+                                       'ENDGAME_RU'),
+
+    ('VERSION',                        'Doom 2D: Forever %s',
+                                       'Doom 2D: Forever %s'),
+
+    ('FATAL ERROR',                    'Fatal error: %s',
+                                       'Êðèòè÷åñêàÿ îøèáêà: %s'),
+    ('SIMPLE ERROR',                   'Error: %s',
+                                       'Îøèáêà: %s'),
+    ('SYSTEM ERROR UNKNOWN',           'Something went seriously wrong. Guru meditation: $%.8x',
+                                       'Õüþñòîí, ó íàñ íåèçâåñòíàÿ íàóêå ïðîáëåìà: $%.8x'),
+    ('SYSTEM ERROR MSG',               'Something went wrong. Post Mortem: %s',
+                                       'Ïðîèçîøëî ÷òî-òî íåëàäíîå: %s'),
+
+    ('', '', '') );
+
+
+procedure SetupArrays();
+var
+  i: LongWord;
+begin
+// Íàçâàíèÿ êëàâèø ïåðåìåùåíèÿ êóðñîðà:
+  e_KeyNames[IK_UP]    := _lc[I_KEY_UP] + ' ' + Chr(30);
+  e_KeyNames[IK_LEFT]  := _lc[I_KEY_LEFT] + ' ' + Chr(17);
+  e_KeyNames[IK_RIGHT] := _lc[I_KEY_RIGHT] + ' ' + Chr(16);
+  e_KeyNames[IK_DOWN]  := _lc[I_KEY_DOWN] + ' ' + Chr(31);
+
+// Èìåíà ìîíñòðîâ â òâîðèòåëüíîì ïàäåæå:
+  KilledByMonster[MONSTER_DEMON] := _lc[I_MONSTER_DEMON];
+  KilledByMonster[MONSTER_IMP] := _lc[I_MONSTER_IMP];
+  KilledByMonster[MONSTER_ZOMBY] := _lc[I_MONSTER_ZOMBIE];
+  KilledByMonster[MONSTER_SERG] := _lc[I_MONSTER_SERGEANT];
+  KilledByMonster[MONSTER_CYBER] := _lc[I_MONSTER_CYBER];
+  KilledByMonster[MONSTER_CGUN] := _lc[I_MONSTER_CGUN];
+  KilledByMonster[MONSTER_BARON] := _lc[I_MONSTER_BARON];
+  KilledByMonster[MONSTER_KNIGHT] := _lc[I_MONSTER_KNIGHT];
+  KilledByMonster[MONSTER_CACO] := _lc[I_MONSTER_CACODEMON];
+  KilledByMonster[MONSTER_SOUL] := _lc[I_MONSTER_SOUL];
+  KilledByMonster[MONSTER_PAIN] := _lc[I_MONSTER_PAIN];
+  KilledByMonster[MONSTER_SPIDER] := _lc[I_MONSTER_MASTERMIND];
+  KilledByMonster[MONSTER_BSP] := _lc[I_MONSTER_SPIDER];
+  KilledByMonster[MONSTER_MANCUB] := _lc[I_MONSTER_MANCUBUS];
+  KilledByMonster[MONSTER_SKEL] := _lc[I_MONSTER_REVENANT];
+  KilledByMonster[MONSTER_VILE] := _lc[I_MONSTER_ARCHVILE];
+  KilledByMonster[MONSTER_FISH] := _lc[I_MONSTER_FISH];
+  KilledByMonster[MONSTER_BARREL] := _lc[I_MONSTER_BARREL];
+  KilledByMonster[MONSTER_ROBO] := _lc[I_MONSTER_ROBOT];
+  KilledByMonster[MONSTER_MAN] := _lc[I_MONSTER_PRIKOLIST];
+
+// ×èò-êîäû:
+  for i := LongWord(Low(CheatEng)) to LongWord(High(CheatEng)) do
+  begin
+    CheatEng[TStrings_Locale(i)] := g_lang_default[TStrings_Locale(i)][LANGUAGE_ENGLISH_N];
+    CheatRus[TStrings_Locale(i)] := g_lang_default[TStrings_Locale(i)][LANGUAGE_RUSSIAN_N];
+  end;
+end;
+
+procedure g_Language_Load(fileName: String);
+var
+  F: TextFile;
+  key, value: String;
+  i: TStrings_Locale;
+  k: Integer;
+  ok: Boolean;
+begin
+// Çíà÷åíèÿ ïî-óìîë÷àíèþ:
+  for i := Low(TStrings_Locale) to High(TStrings_Locale) do
+    _lc[i] := g_lang_default[i][LANGUAGE_ENGLISH_N];
+
+  if FileExists(fileName) then
+    begin
+      AssignFile(F, fileName);
+      ReSet(F);
+      k := 0;
+
+    // ×èòàåì ôàéë:
+      while not EoF(F) do
+      begin
+      // ×èòàåì ñòðîêó:
+        ReadLn(F, key);
+        key := Trim(key);
+
+      // Ñòðîêà - êëþ÷ ïåðåâîäà:
+        if (key <> '') and
+           (key[1] = '[') and
+           (Pos(']', key) > 2) then
+        begin
+          key := UpperCase(Copy(key, 2, Pos(']', key)-2));
+
+        // Ïðîïóñêàåì ïóñòûå ñòðîêè äî ñòðîêè - ïåðåâîäà:
+          value := '';
+          while (not EoF(F)) and (value = '') do
+          begin
+            ReadLn(F, value);
+            value := Trim(value);
+          end;
+
+        // Åñòü ñòðîêà - ïåðåâîä:
+          if value <> '' then
+          begin
+          // Èùåì èíäåêñ êëþ÷à ïåðåâîäà:
+            ok := False;
+            i := TStrings_Locale(k);
+
+          // Îò òåêóùåãî ê ïåðâîìó:
+            while i > Low(TStrings_Locale) do
+            begin
+              if g_lang_default[i][1] = key then
+              begin
+                _lc[i] := value;
+                ok := True;
+                Break;
+              end;
+
+              Dec(i);
+            end;
+
+          // Ïåðâûé:
+            if not ok then
+            begin
+              i := Low(TStrings_Locale);
+
+              if (g_lang_default[i][1] = key) then
+              begin
+                _lc[i] := value;
+                ok := True;
+              end;
+            end;
+
+          // Îò ñëóäóþùåãî çà òåêóùèì äî ïîñëåäíåãî:
+            if not ok then
+            begin
+              i := TStrings_Locale(k);
+
+              while i < High(TStrings_Locale) do
+              begin
+                Inc(i);
+
+                if g_lang_default[i][1] = key then
+                begin
+                  _lc[i] := value;
+                  Break;
+                end;
+              end;
+            end;
+          end;
+
+          Inc(k);
+        end;
+      end;
+
+      CloseFile(F);
+    end
+  else
+    e_WriteLog('Language file "'+fileName+'" not found!', MSG_WARNING);
+
+  SetupArrays();
+end;
+
+procedure g_Language_Set(lang: String);
+var
+  i: TStrings_Locale;
+  n: Byte;
+begin
+  if lang = LANGUAGE_RUSSIAN then
+    n := LANGUAGE_RUSSIAN_N
+  else
+    n := LANGUAGE_ENGLISH_N;
+
+  for i := Low(TStrings_Locale) to High(TStrings_Locale) do
+    _lc[i] := g_lang_default[i][n];
+
+  SetupArrays();
+end;
+
+procedure g_Language_Dump(fileName: String);
+var
+  F: TextFile;
+  i: TStrings_Locale;
+begin
+  AssignFile(F, fileName);
+  ReWrite(F);
+
+  for i := Low(TStrings_Locale) to High(TStrings_Locale) do
+    WriteLn(F, _lc[i]);
+
+  CloseFile(F);
+end;
+
+end.
diff --git a/src/game/g_main.pas b/src/game/g_main.pas
new file mode 100644 (file)
index 0000000..357f022
--- /dev/null
@@ -0,0 +1,513 @@
+unit g_main;
+
+interface
+
+procedure Main();
+procedure Init();
+procedure Release();
+procedure Update();
+procedure Draw();
+procedure KeyPress(K: Word);
+procedure CharPress(C: Char);
+
+var
+  GameDir: string;
+  DataDir: string;
+  MapsDir: string;
+  ModelsDir: string;
+  GameWAD: string;
+
+implementation
+
+uses
+  SDL, GL, GLExt, WADEDITOR, e_log, g_window,
+  e_graphics, e_input, g_game, g_console, g_gui,
+  e_sound, g_options, g_sound, g_player,
+  g_weapons, SysUtils, g_triggers, MAPDEF, g_map,
+  MAPSTRUCT, g_menu, g_language, g_net;
+
+var
+  charbuff: Array [0..15] of Char;
+
+procedure Main();
+begin
+  GetDir(0, GameDir);
+  MapsDir := GameDir + '/maps/';
+  DataDir := GameDir + '/data/';
+  ModelsDir := DataDir + 'models/';
+  GameWAD := DataDir + 'Game.wad';
+
+  e_InitLog(GameDir + '/' + LOG_FILENAME, WM_NEWFILE);
+
+  e_WriteLog('Read config file', MSG_NOTIFY);
+  g_Options_Read(GameDir + '/' + CONFIG_FILENAME);
+
+  //GetSystemDefaultLCID()
+
+  //e_WriteLog('Read language file', MSG_NOTIFY);
+  //g_Language_Load(DataDir + gLanguage + '.txt');
+  e_WriteLog(gLanguage, MSG_NOTIFY);
+  g_Language_Set(gLanguage);
+
+  if SDL_Init(SDL_INIT_JOYSTICK or SDL_INIT_TIMER or SDL_INIT_AUDIO) < 0 then
+    raise Exception.Create('SDL: Init failed: ' + SDL_GetError());
+
+  e_WriteLog('Entering SDLMain', MSG_NOTIFY);
+
+  {$WARNINGS OFF}
+  SDLMain();
+  {$WARNINGS ON}
+
+  e_WriteLog('Releasing SDL', MSG_NOTIFY);
+  SDL_Quit();
+end;
+
+procedure Init();
+var
+  a: Integer;
+begin
+  Randomize;
+
+  e_WriteLog('Init Input', MSG_NOTIFY);
+  e_InitInput();
+
+  if (e_JoysticksAvailable > 0) then
+    e_WriteLog('Input: Joysticks available.', MSG_NOTIFY)
+  else
+    e_WriteLog('Input: No Joysticks.', MSG_NOTIFY);
+
+  if not gNoSound then
+  begin
+    e_WriteLog('Init FMOD', MSG_NOTIFY);
+    if not e_InitSoundSystem(44100, False) then e_InitSoundSystem(48000, True);
+  end;
+
+  e_WriteLog('Init game', MSG_NOTIFY);
+  g_Game_Init();
+
+  for a := 0 to 15 do charbuff[a] := ' ';
+end;
+
+procedure Release();
+begin
+  e_WriteLog('Releasing engine', MSG_NOTIFY);
+  e_ReleaseEngine();
+
+  e_WriteLog('Releasing Input', MSG_NOTIFY);
+  e_ReleaseInput();
+
+  if not gNoSound then
+  begin
+    e_WriteLog('Releasing FMOD', MSG_NOTIFY);
+    e_ReleaseSoundSystem();
+  end;
+end;
+
+procedure Update();
+begin
+  g_Game_Update();
+end;
+
+procedure Draw();
+begin
+  g_Game_Draw();
+end;
+
+function Translit(S: String): String;
+var
+  i: Integer;
+begin
+  Result := S;
+  for i := 1 to Length(Result) do
+    case Result[i] of
+      'É': Result[i] := 'Q';
+      'Ö': Result[i] := 'W';
+      'Ó': Result[i] := 'E';
+      'Ê': Result[i] := 'R';
+      'Å': Result[i] := 'T';
+      'Í': Result[i] := 'Y';
+      'Ã': Result[i] := 'U';
+      'Ø': Result[i] := 'I';
+      'Ù': Result[i] := 'O';
+      'Ç': Result[i] := 'P';
+      'Õ': Result[i] := Chr(219);
+      'Ú': Result[i] := Chr(221);
+      'Ô': Result[i] := 'A';
+      'Û': Result[i] := 'S';
+      'Â': Result[i] := 'D';
+      'À': Result[i] := 'F';
+      'Ï': Result[i] := 'G';
+      'Ð': Result[i] := 'H';
+      'Î': Result[i] := 'J';
+      'Ë': Result[i] := 'K';
+      'Ä': Result[i] := 'L';
+      'Æ': Result[i] := Chr(186);
+      'Ý': Result[i] := Chr(222);
+      'ß': Result[i] := 'Z';
+      '×': Result[i] := 'X';
+      'Ñ': Result[i] := 'C';
+      'Ì': Result[i] := 'V';
+      'È': Result[i] := 'B';
+      'Ò': Result[i] := 'N';
+      'Ü': Result[i] := 'M';
+      'Á': Result[i] := Chr(188);
+      'Þ': Result[i] := Chr(190);
+    end;
+end;
+
+procedure Cheat();
+const
+  CHEAT_DAMAGE = 500;
+label
+  Cheated;
+var
+  s, s2, ls1, ls2: string;
+  c: Char16;
+  a: Integer;
+begin
+  if (not gGameOn) or (not gCheats) or ((gGameSettings.GameType <> GT_SINGLE) and
+    (gGameSettings.GameMode <> GM_COOP) and (not gDebugMode))
+    or g_Game_IsNet then Exit;
+
+  s := 'SOUND_GAME_RADIO';
+
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_GODMODE];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_GODMODE]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.GodMode := not gPlayer1.GodMode;
+    if gPlayer2 <> nil then gPlayer2.GodMode := not gPlayer2.GodMode;
+    goto Cheated;
+  end;
+  // RAMBO
+  ls1 :=          CheatEng[I_GAME_CHEAT_WEAPONS];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_WEAPONS]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.AllRulez(False);
+    if gPlayer2 <> nil then gPlayer2.AllRulez(False);
+    goto Cheated;
+  end;
+  // TANK
+  ls1 :=          CheatEng[I_GAME_CHEAT_HEALTH];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_HEALTH]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.AllRulez(True);
+    if gPlayer2 <> nil then gPlayer2.AllRulez(True);
+    goto Cheated;
+  end;
+  // IDDQD
+  ls1 :=          CheatEng[I_GAME_CHEAT_DEATH];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_DEATH]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.Damage(CHEAT_DAMAGE, 0, 0, 0, HIT_TRAP);
+    if gPlayer2 <> nil then gPlayer2.Damage(CHEAT_DAMAGE, 0, 0, 0, HIT_TRAP);
+    s := 'SOUND_MONSTER_HAHA';
+    goto Cheated;
+  end;
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_DOORS];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_DOORS]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    g_Triggers_OpenAll();
+    goto Cheated;
+  end;
+  // GOODBYE
+  ls1 :=          CheatEng[I_GAME_CHEAT_NEXTMAP];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_NEXTMAP]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gTriggers <> nil then
+      for a := 0 to High(gTriggers) do
+        if gTriggers[a].TriggerType = TRIGGER_EXIT then
+        begin
+          gExitByTrigger := True;
+          g_Game_ExitLevel(gTriggers[a].Data.MapName);
+          Break;
+        end;
+    goto Cheated;
+  end;
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_CHANGEMAP];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_CHANGEMAP]);
+  s2 := Copy(charbuff, 15, 2);
+  if ((Copy(charbuff, 15 - Length(ls1), Length(ls1)) = ls1) or
+      (Copy(charbuff, 15 - Length(ls2), Length(ls2)) = ls2))
+     and (s2[1] >= '0') and (s2[1] <= '9')
+     and (s2[2] >= '0') and (s2[2] <= '9') then
+  begin
+    if g_Map_Exist(MapsDir+gGameSettings.WAD+':\MAP'+s2) then
+    begin
+      c := 'MAP00';
+      c[3] := s2[1];
+      c[4] := s2[2];
+      g_Game_ExitLevel(c);
+    end;
+    goto Cheated;
+  end;
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_FLY];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_FLY]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    gFly := not gFly;
+    goto Cheated;
+  end;
+  // BULLFROG
+  ls1 :=          CheatEng[I_GAME_CHEAT_JUMPS];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_JUMPS]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    VEL_JUMP := 30-VEL_JUMP;
+    goto Cheated;
+  end;
+  // FORMULA1
+  ls1 :=          CheatEng[I_GAME_CHEAT_SPEED];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_SPEED]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    MAX_RUNVEL := 32-MAX_RUNVEL;
+    goto Cheated;
+  end;
+  // CONDOM
+  ls1 :=          CheatEng[I_GAME_CHEAT_SUIT];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_SUIT]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.GiveItem(ITEM_SUIT);
+    if gPlayer2 <> nil then gPlayer2.GiveItem(ITEM_SUIT);
+    goto Cheated;
+  end;
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_AIR];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_AIR]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.GiveItem(ITEM_OXYGEN);
+    if gPlayer2 <> nil then gPlayer2.GiveItem(ITEM_OXYGEN);
+    goto Cheated;
+  end;
+  // PURELOVE
+  ls1 :=          CheatEng[I_GAME_CHEAT_BERSERK];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_BERSERK]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.GiveItem(ITEM_MEDKIT_BLACK);
+    if gPlayer2 <> nil then gPlayer2.GiveItem(ITEM_MEDKIT_BLACK);
+    goto Cheated;
+  end;
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_JETPACK];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_JETPACK]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.GiveItem(ITEM_JETPACK);
+    if gPlayer2 <> nil then gPlayer2.GiveItem(ITEM_JETPACK);
+    goto Cheated;
+  end;
+  // CASPER
+  ls1 :=          CheatEng[I_GAME_CHEAT_NOCLIP];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_NOCLIP]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.SwitchNoClip;
+    if gPlayer2 <> nil then gPlayer2.SwitchNoClip;
+    goto Cheated;
+  end;
+  //
+  ls1 :=          CheatEng[I_GAME_CHEAT_NOTARGET];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_NOTARGET]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.NoTarget := not gPlayer1.NoTarget;
+    if gPlayer2 <> nil then gPlayer2.NoTarget := not gPlayer2.NoTarget;
+    goto Cheated;
+  end;
+  // INFERNO
+  ls1 :=          CheatEng[I_GAME_CHEAT_NORELOAD];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_NORELOAD]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    if gPlayer1 <> nil then gPlayer1.NoReload := not gPlayer1.NoReload;
+    if gPlayer2 <> nil then gPlayer2.NoReload := not gPlayer2.NoReload;
+    goto Cheated;
+  end;
+  ls1 :=          CheatEng[I_GAME_CHEAT_AIMLINE];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_AIMLINE]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    gAimLine := not gAimLine;
+    goto Cheated;
+  end;
+  ls1 :=          CheatEng[I_GAME_CHEAT_AUTOMAP];
+  ls2 := Translit(CheatRus[I_GAME_CHEAT_AUTOMAP]);
+  if (Copy(charbuff, 17 - Length(ls1), Length(ls1)) = ls1) or
+     (Copy(charbuff, 17 - Length(ls2), Length(ls2)) = ls2) then
+  begin
+    gShowMap := not gShowMap;
+    goto Cheated;
+  end;
+  Exit;
+
+Cheated:
+  g_Sound_PlayEx(s);
+end;
+
+procedure KeyPress(K: Word);
+var
+  Msg: g_gui.TMessage;
+  a: Integer;
+begin
+  case K of
+    IK_PAUSE: // <Pause/Break>:
+      begin
+        if (g_ActiveWindow = nil) then
+          g_Game_Pause(not gPause);
+      end;
+
+    IK_BACKQUOTE: // <`/~/¨/¸>:
+      begin
+        g_Console_Switch();
+      end;
+
+    IK_ESCAPE: // <Esc>:
+      begin
+        if gChatShow then
+        begin
+          g_Console_Chat_Switch();
+          Exit;
+        end;
+
+        if gConsoleShow then
+          g_Console_Switch()
+        else
+          if g_ActiveWindow <> nil then
+            begin
+              Msg.Msg := WM_KEYDOWN;
+              Msg.WParam := IK_ESCAPE;
+              g_ActiveWindow.OnMessage(Msg);
+            end
+          else
+            if gState <> STATE_FOLD then
+              if gGameOn
+              or (gState = STATE_INTERSINGLE)
+              or (gState = STATE_INTERCUSTOM)
+              then
+                g_Game_InGameMenu(True)
+              else
+                if (gExit = 0) and (gState <> STATE_SLIST) then
+                  begin
+                    if gState <> STATE_MENU then
+                      if NetMode <> NET_NONE then
+                      begin
+                        g_Game_StopAllSounds(True);
+                        g_Game_Free;
+                        gState := STATE_MENU;
+                        Exit;
+                      end;
+
+                    g_GUI_ShowWindow('MainMenu');
+                    g_Sound_PlayEx('MENU_OPEN');
+                  end;
+      end;
+
+    IK_F2, IK_F3, IK_F4, IK_F5, IK_F6, IK_F7, IK_F10:
+      begin // <F2> .. <F6> � <F12>
+        if gGameOn and (not gConsoleShow) and (not gChatShow) then
+        begin
+          while g_ActiveWindow <> nil do
+            g_GUI_HideWindow(False);
+
+          if (not g_Game_IsNet) then
+            g_Game_Pause(True);
+
+          case K of
+            IK_F2:
+              g_Menu_Show_SaveMenu();
+            IK_F3:
+              g_Menu_Show_LoadMenu();
+            IK_F4:
+              g_Menu_Show_GameSetGame();
+            IK_F5:
+              g_Menu_Show_OptionsVideo();
+            IK_F6:
+              g_Menu_Show_OptionsSound();
+            IK_F7:
+              g_Menu_Show_EndGameMenu();
+            IK_F10:
+              g_Menu_Show_QuitGameMenu();
+          end;
+        end;
+      end;
+
+    else
+      begin
+        gJustChatted := False;
+        if gConsoleShow or gChatShow then
+          g_Console_Control(K)
+        else
+          if g_ActiveWindow <> nil then
+          begin
+            Msg.Msg := WM_KEYDOWN;
+            Msg.WParam := K;
+            g_ActiveWindow.OnMessage(Msg);
+          end
+          else
+          begin
+            if (gState = STATE_MENU) then
+            begin
+              g_GUI_ShowWindow('MainMenu');
+              g_Sound_PlayEx('MENU_OPEN');
+            end
+            else
+            begin
+              for a := 0 to 14 do
+                charbuff[a] := charbuff[a+1];
+              charbuff[15] := UpCase(Chr(K));
+              Cheat();
+            end;
+          end;
+      end;
+  end;
+end;
+
+procedure CharPress(C: Char);
+var
+  Msg: g_gui.TMessage;
+begin
+  if (not gChatShow) and ((C = '`') or (C = '~') or (C = '¸') or (C = '¨')) then
+    Exit;
+
+  if gConsoleShow or gChatShow then
+    g_Console_Char(C)
+  else
+    if g_ActiveWindow <> nil then
+    begin
+      Msg.Msg := WM_CHAR;
+      Msg.WParam := Ord(C);
+      g_ActiveWindow.OnMessage(Msg);
+    end;
+end;
+
+end.
diff --git a/src/game/g_map.pas b/src/game/g_map.pas
new file mode 100644 (file)
index 0000000..9e14586
--- /dev/null
@@ -0,0 +1,2142 @@
+unit g_map;
+
+interface
+
+uses
+  e_graphics, g_basic, MAPSTRUCT, g_textures, Classes,
+  g_phys, WADEDITOR, BinEditor, g_panel, md5;
+
+type
+  TMapInfo = record
+    Map:           String;
+    Name:          String;
+    Description:   String;
+    Author:        String;
+    MusicName:     String;
+    SkyName:       String;
+    Height:        Word;
+    Width:         Word;
+  end;
+
+  PRespawnPoint = ^TRespawnPoint;
+  TRespawnPoint = record
+    X, Y:      Integer;
+    Direction: TDirection;
+    PointType: Byte;
+  end;
+
+  PFlagPoint = ^TFlagPoint;
+  TFlagPoint = TRespawnPoint;
+
+  PFlag = ^TFlag;
+  TFlag = record
+    Obj:         TObj;
+    RespawnType: Byte;
+    State:       Byte;
+    Count:       Integer;
+    CaptureTime: LongWord;
+    Animation:   TAnimation;
+    Direction:   TDirection;
+  end;
+
+
+function  g_Map_Load(Res: String): Boolean;
+function  g_Map_GetMapInfo(Res: String): TMapInfo;
+function  g_Map_GetMapsList(WADName: String): SArray;
+function  g_Map_Exist(Res: String): Boolean;
+procedure g_Map_Free();
+procedure g_Map_Update();
+procedure g_Map_DrawPanels(PanelType: Word);
+procedure g_Map_DrawBack(dx, dy: Integer);
+function  g_Map_CollidePanel(X, Y: Integer; Width, Height: Word;
+                             PanelType: Word; b1x3: Boolean): Boolean;
+function  g_Map_CollideLiquid_Texture(X, Y: Integer; Width, Height: Word): DWORD;
+procedure g_Map_EnableWall(ID: DWORD);
+procedure g_Map_DisableWall(ID: DWORD);
+procedure g_Map_SwitchTexture(PanelType: Word; ID: DWORD; AnimLoop: Byte = 0);
+procedure g_Map_SetLift(ID: DWORD; t: Integer);
+procedure g_Map_ReAdd_DieTriggers();
+function  g_Map_IsSpecialTexture(Texture: String): Boolean;
+
+function  g_Map_GetPoint(PointType: Byte; var RespawnPoint: TRespawnPoint): Boolean;
+function  g_Map_GetPointCount(PointType: Byte): Word;
+
+function  g_Map_HaveFlagPoints(): Boolean;
+
+procedure g_Map_ResetFlag(Flag: Byte);
+procedure g_Map_DrawFlags();
+
+procedure g_Map_SaveState(Var Mem: TBinMemoryWriter);
+procedure g_Map_LoadState(Var Mem: TBinMemoryReader);
+
+const
+  RESPAWNPOINT_PLAYER1 = 1;
+  RESPAWNPOINT_PLAYER2 = 2;
+  RESPAWNPOINT_DM      = 3;
+  RESPAWNPOINT_RED     = 4;
+  RESPAWNPOINT_BLUE    = 5;
+
+  FLAG_NONE = 0;
+  FLAG_RED  = 1;
+  FLAG_BLUE = 2;
+  FLAG_DOM  = 3;
+
+  FLAG_STATE_NONE     = 0;
+  FLAG_STATE_NORMAL   = 1;
+  FLAG_STATE_DROPPED  = 2;
+  FLAG_STATE_CAPTURED = 3;
+  FLAG_STATE_SCORED   = 4; // Äëÿ ýâåíòîâ ÷åðåç ñåòêó.
+  FLAG_STATE_RETURNED = 5; // Äëÿ ýâåíòîâ ÷åðåç ñåòêó.
+
+  FLAG_TIME = 720; // 20 seconds
+
+  SKY_STRETCH: Single = 1.5;
+
+var
+  gWalls: TPanelArray;
+  gRenderBackgrounds: TPanelArray;
+  gRenderForegrounds: TPanelArray;
+  gWater, gAcid1, gAcid2: TPanelArray;
+  gSteps: TPanelArray;
+  gLifts: TPanelArray;
+  gBlockMon: TPanelArray;
+  gFlags: array [FLAG_RED..FLAG_BLUE] of TFlag;
+  //gDOMFlags: array of TFlag;
+  gMapInfo: TMapInfo;
+  gBackSize: TPoint;
+  gDoorMap: array of array of DWORD;
+  gLiftMap: array of array of DWORD;
+  gWADHash: TMD5Digest;
+  BackID:  DWORD = DWORD(-1);
+  gExternalResources: TStringList;
+
+implementation
+
+uses
+  g_main, e_log, SysUtils, g_items, g_gfx, g_console,
+  GL, GLExt, g_weapons, g_game, g_sound, e_sound, CONFIG,
+  g_options, MAPREADER, g_triggers, g_player, MAPDEF,
+  Math, g_monsters, g_saveload, g_language, g_netmsg;
+
+const
+  FLAGRECT: TRectWH = (X:15; Y:12; Width:33; Height:52);
+  MUSIC_SIGNATURE = $4953554D; // 'MUSI'
+  FLAG_SIGNATURE = $47414C46; // 'FLAG'
+
+var
+  Textures:      TLevelTextureArray;
+  RespawnPoints: Array of TRespawnPoint;
+  FlagPoints:    Array [FLAG_RED..FLAG_BLUE] of PFlagPoint;
+  //DOMFlagPoints: Array of TFlagPoint;
+
+
+function g_Map_IsSpecialTexture(Texture: String): Boolean;
+begin
+  Result := (Texture = TEXTURE_NAME_WATER) or
+            (Texture = TEXTURE_NAME_ACID1) or
+            (Texture = TEXTURE_NAME_ACID2);
+end;
+
+procedure CreateDoorMap();
+var
+  PanelArray: Array of record
+                         X, Y: Integer;
+                         Width, Height: Word;
+                         Active: Boolean;
+                         PanelID: DWORD;
+                       end;
+  a, b, c, m, i, len: Integer;
+  ok: Boolean;
+begin
+  if gWalls = nil then
+    Exit;
+
+  i := 0;
+  len := 128;
+  SetLength(PanelArray, len);
+
+  for a := 0 to High(gWalls) do
+    if gWalls[a].Door then
+    begin
+      PanelArray[i].X := gWalls[a].X;
+      PanelArray[i].Y := gWalls[a].Y;
+      PanelArray[i].Width := gWalls[a].Width;
+      PanelArray[i].Height := gWalls[a].Height;
+      PanelArray[i].Active := True;
+      PanelArray[i].PanelID := a;
+
+      i := i + 1;
+      if i = len then
+      begin
+        len := len + 128;
+        SetLength(PanelArray, len);
+      end;
+    end;
+
+// Íåò äâåðåé:
+  if i = 0 then
+  begin
+    PanelArray := nil;
+    Exit;
+  end;
+
+  SetLength(gDoorMap, 0);
+
+  g_Game_SetLoadingText(_lc[I_LOAD_DOOR_MAP], i-1, False);
+
+  for a := 0 to i-1 do
+    if PanelArray[a].Active then
+    begin
+      PanelArray[a].Active := False;
+      m := Length(gDoorMap);
+      SetLength(gDoorMap, m+1);
+      SetLength(gDoorMap[m], 1);
+      gDoorMap[m, 0] := PanelArray[a].PanelID;
+      ok := True;
+
+      while ok do
+      begin
+        ok := False;
+
+        for b := 0 to i-1 do
+          if PanelArray[b].Active then
+            for c := 0 to High(gDoorMap[m]) do
+              if {((gRenderWalls[PanelArray[b].RenderPanelID].TextureID = gRenderWalls[gDoorMap[m, c]].TextureID) or
+                    gRenderWalls[PanelArray[b].RenderPanelID].Hide or gRenderWalls[gDoorMap[m, c]].Hide) and}
+                g_CollideAround(PanelArray[b].X, PanelArray[b].Y,
+                                PanelArray[b].Width, PanelArray[b].Height,
+                                gWalls[gDoorMap[m, c]].X,
+                                gWalls[gDoorMap[m, c]].Y,
+                                gWalls[gDoorMap[m, c]].Width,
+                                gWalls[gDoorMap[m, c]].Height) then
+              begin
+                PanelArray[b].Active := False;
+                SetLength(gDoorMap[m],
+                          Length(gDoorMap[m])+1);
+                gDoorMap[m, High(gDoorMap[m])] := PanelArray[b].PanelID;
+                ok := True;
+                Break;
+              end;
+      end;
+
+      g_Game_StepLoading();
+    end;
+
+  PanelArray := nil;
+end;
+
+procedure CreateLiftMap();
+var
+  PanelArray: Array of record
+                         X, Y: Integer;
+                         Width, Height: Word;
+                         Active: Boolean;
+                       end;
+  a, b, c, len, i, j: Integer;
+  ok: Boolean;
+begin
+  if gLifts = nil then
+    Exit;
+
+  len := Length(gLifts);
+  SetLength(PanelArray, len);
+
+  for a := 0 to len-1 do
+  begin
+    PanelArray[a].X := gLifts[a].X;
+    PanelArray[a].Y := gLifts[a].Y;
+    PanelArray[a].Width := gLifts[a].Width;
+    PanelArray[a].Height := gLifts[a].Height;
+    PanelArray[a].Active := True;
+  end;
+
+  SetLength(gLiftMap, len);
+  i := 0;
+
+  g_Game_SetLoadingText(_lc[I_LOAD_LIFT_MAP], len-1, False);
+
+  for a := 0 to len-1 do
+    if PanelArray[a].Active then
+    begin
+      PanelArray[a].Active := False;
+      SetLength(gLiftMap[i], 32);
+      j := 0;
+      gLiftMap[i, j] := a;
+      ok := True;
+
+      while ok do
+      begin
+        ok := False;
+        for b := 0 to len-1 do
+          if PanelArray[b].Active then
+            for c := 0 to j do
+              if g_CollideAround(PanelArray[b].X,
+                                 PanelArray[b].Y,
+                                 PanelArray[b].Width,
+                                 PanelArray[b].Height,
+                                 PanelArray[gLiftMap[i, c]].X,
+                                 PanelArray[gLiftMap[i, c]].Y,
+                                 PanelArray[gLiftMap[i, c]].Width,
+                                 PanelArray[gLiftMap[i, c]].Height) then
+              begin
+                PanelArray[b].Active := False;
+                j := j+1;
+                if j > High(gLiftMap[i]) then
+                  SetLength(gLiftMap[i],
+                            Length(gLiftMap[i])+32);
+
+                gLiftMap[i, j] := b;
+                ok := True;
+
+                Break;
+              end;
+      end;
+
+      SetLength(gLiftMap[i], j+1);
+      i := i+1;
+
+      g_Game_StepLoading();
+    end;
+
+  SetLength(gLiftMap, i);
+
+  PanelArray := nil;
+end;
+
+function CreatePanel(PanelRec: TPanelRec_1; AddTextures: TAddTextureArray;
+                     CurTex: Integer; sav: Boolean): Integer;
+var
+  len: Integer;
+  panels: ^TPanelArray;
+begin
+  Result := -1;
+
+  case PanelRec.PanelType of
+    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+      panels := @gWalls;
+    PANEL_BACK:
+      panels := @gRenderBackgrounds;
+    PANEL_FORE:
+      panels := @gRenderForegrounds;
+    PANEL_WATER:
+      panels := @gWater;
+    PANEL_ACID1:
+      panels := @gAcid1;
+    PANEL_ACID2:
+      panels := @gAcid2;
+    PANEL_STEP:
+      panels := @gSteps;
+    PANEL_LIFTUP, PANEL_LIFTDOWN, PANEL_LIFTLEFT, PANEL_LIFTRIGHT:
+      panels := @gLifts;
+    PANEL_BLOCKMON:
+      panels := @gBlockMon;
+    else
+      Exit;
+  end;
+
+  len := Length(panels^);
+  SetLength(panels^, len + 1);
+
+  panels^[len] := TPanel.Create(PanelRec, AddTextures,
+                                CurTex, Textures);
+  if sav then
+    panels^[len].SaveIt := True;
+
+  Result := len;
+end;
+
+procedure CreateNullTexture(RecName: String);
+begin
+  SetLength(Textures, Length(Textures)+1);
+
+  with Textures[High(Textures)] do
+  begin
+    TextureName := RecName;
+    Width := 1;
+    Height := 1;
+    Anim := False;
+    TextureID := TEXTURE_NONE;
+  end;
+end;
+
+function CreateTexture(RecName: String; Map: string; log: Boolean): Boolean;
+var
+  WAD: TWADEditor_1;
+  TextureData: Pointer;
+  WADName: String;
+  SectionName: String;
+  TextureName: String;
+  a, ResLength: Integer;
+begin
+  Result := False;
+
+  if Textures <> nil then
+    for a := 0 to High(Textures) do
+      if Textures[a].TextureName = RecName then
+      begin // Òåêñòóðà ñ òàêèì èìåíåì óæå åñòü
+        Result := True;
+        Exit;
+      end;
+
+// Òåêñòóðû ñî ñïåöèàëüíûìè èìåíàìè (âîäà, ëàâà, êèñëîòà):
+  if (RecName = TEXTURE_NAME_WATER) or
+     (RecName = TEXTURE_NAME_ACID1) or
+     (RecName = TEXTURE_NAME_ACID2) then
+  begin
+    SetLength(Textures, Length(Textures)+1);
+
+    with Textures[High(Textures)] do
+    begin
+      TextureName := RecName;
+
+      if TextureName = TEXTURE_NAME_WATER then
+        TextureID := TEXTURE_SPECIAL_WATER
+      else
+        if TextureName = TEXTURE_NAME_ACID1 then
+          TextureID := TEXTURE_SPECIAL_ACID1
+        else
+          if TextureName = TEXTURE_NAME_ACID2 then
+            TextureID := TEXTURE_SPECIAL_ACID2;
+
+      Anim := False;
+    end;
+
+    Result := True;
+    Exit;
+  end;
+
+// Çàãðóæàåì ðåñóðñ òåêñòóðû â ïàìÿòü èç WAD'à:
+  g_ProcessResourceStr(RecName, WADName, SectionName, TextureName);
+
+  WAD := TWADEditor_1.Create();
+
+  if WADName <> '' then
+    WADName := GameDir+'/wads/'+WADName
+  else
+    WADName := Map;
+
+  WAD.ReadFile(WADName);
+
+  if WAD.GetResource(SectionName, TextureName, TextureData, ResLength) then
+    begin
+      SetLength(Textures, Length(Textures)+1);
+      if not e_CreateTextureMem(TextureData, Textures[High(Textures)].TextureID) then
+        Exit;
+      e_GetTextureSize(Textures[High(Textures)].TextureID,
+                       @Textures[High(Textures)].Width,
+                       @Textures[High(Textures)].Height);
+      FreeMem(TextureData);
+      Textures[High(Textures)].TextureName := RecName;
+      Textures[High(Textures)].Anim := False;
+
+      Result := True;
+    end
+  else // Íåò òàêîãî ðåóñðñà â WAD'å
+    if log then
+      begin
+        e_WriteLog(Format('Error loading texture %s', [RecName]), MSG_WARNING);
+        e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+      end;
+
+  WAD.Free();
+end;
+
+function CreateAnimTexture(RecName: String; Map: string; log: Boolean): Boolean;
+var
+  WAD: TWADEditor_1;
+  TextureWAD: Pointer;
+  TextData: Pointer;
+  TextureData: Pointer;
+  cfg: TConfig;
+  WADName: String;
+  SectionName: String;
+  TextureName: String;
+  ResLength: Integer;
+  TextureResource: String;
+  _width, _height, _framecount, _speed: Integer;
+  _backanimation: Boolean;
+begin
+  Result := False;
+
+// ×èòàåì WAD-ðåñóðñ àíèì.òåêñòóðû èç WAD'à â ïàìÿòü:
+  g_ProcessResourceStr(RecName, WADName, SectionName, TextureName);
+
+  WAD := TWADEditor_1.Create();
+
+  if WADName <> '' then
+    WADName := GameDir+'/wads/'+WADName
+  else
+    WADName := Map;
+
+  WAD.ReadFile(WADName);
+
+  if not WAD.GetResource(SectionName, TextureName, TextureWAD, ResLength) then
+  begin
+    if log then
+    begin
+      e_WriteLog(Format('Error loading animation texture %s', [RecName]), MSG_WARNING);
+      e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+    end;
+    WAD.Free();
+    Exit;
+  end;
+
+  WAD.FreeWAD();
+
+  if not WAD.ReadMemory(TextureWAD, ResLength) then
+  begin
+    FreeMem(TextureWAD);
+    WAD.Free();
+    Exit;
+  end;
+
+// ×èòàåì INI-ðåñóðñ àíèì. òåêñòóðû è çàïîìèíàåì åãî óñòàíîâêè:
+  if not WAD.GetResource('TEXT', 'ANIM', TextData, ResLength) then
+  begin
+    FreeMem(TextureWAD);
+    WAD.Free();
+    Exit;
+  end;
+
+  cfg := TConfig.CreateMem(TextData, ResLength);
+
+  TextureResource := cfg.ReadStr('', 'resource', '');
+
+  if TextureResource = '' then
+  begin
+    FreeMem(TextureWAD);
+    FreeMem(TextData);
+    WAD.Free();
+    cfg.Free();
+    Exit;
+  end;
+
+  _width := cfg.ReadInt('', 'framewidth', 0);
+  _height := cfg.ReadInt('', 'frameheight', 0);
+  _framecount := cfg.ReadInt('', 'framecount', 0);
+  _speed := cfg.ReadInt('', 'waitcount', 0);
+  _backanimation := cfg.ReadBool('', 'backanimation', False);
+
+  cfg.Free();
+
+// ×èòàåì ðåñóðñ òåêñòóð (êàäðîâ) àíèì. òåêñòóðû â ïàìÿòü:
+  if not WAD.GetResource('TEXTURES', TextureResource, TextureData, ResLength) then
+  begin
+    FreeMem(TextureWAD);
+    FreeMem(TextData);
+    WAD.Free();
+    Exit;
+  end;
+
+  WAD.Free();
+
+  SetLength(Textures, Length(Textures)+1);
+  with Textures[High(Textures)] do
+  begin
+  // Ñîçäàåì êàäðû àíèì. òåêñòóðû èç ïàìÿòè:
+    if g_Frames_CreateMemory(@FramesID, '', TextureData,
+         _width, _height, _framecount, _backanimation) then
+      begin
+        TextureName := RecName;
+        Width := _width;
+        Height := _height;
+        Anim := True;
+        FramesCount := _framecount;
+        Speed := _speed;
+
+        Result := True;
+      end
+    else
+      if log then
+        e_WriteLog(Format('Error loading animation texture %s', [RecName]), MSG_WARNING);
+  end;
+
+  FreeMem(TextureWAD);
+  FreeMem(TextData);
+end;
+
+procedure CreateItem(Item: TItemRec_1);
+begin
+  if g_Game_IsClient then Exit;
+
+  if (not (gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF])) and
+     ByteBool(Item.Options and ITEM_OPTION_ONLYDM) then
+    Exit;
+
+  g_Items_Create(Item.X, Item.Y, Item.ItemType, ByteBool(Item.Options and ITEM_OPTION_FALL),
+                 gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF, GM_COOP]);
+end;
+
+procedure CreateArea(Area: TAreaRec_1);
+var
+  a: Integer;
+  id: DWORD;
+begin
+  case Area.AreaType of
+    AREA_DMPOINT, AREA_PLAYERPOINT1, AREA_PLAYERPOINT2,
+    AREA_REDTEAMPOINT, AREA_BLUETEAMPOINT:
+    begin
+      SetLength(RespawnPoints, Length(RespawnPoints)+1);
+      with RespawnPoints[High(RespawnPoints)] do
+      begin
+        X := Area.X;
+        Y := Area.Y;
+        Direction := TDirection(Area.Direction);
+
+        case Area.AreaType of
+          AREA_DMPOINT: PointType := RESPAWNPOINT_DM;
+          AREA_PLAYERPOINT1: PointType := RESPAWNPOINT_PLAYER1;
+          AREA_PLAYERPOINT2: PointType := RESPAWNPOINT_PLAYER2;
+          AREA_REDTEAMPOINT: PointType := RESPAWNPOINT_RED;
+          AREA_BLUETEAMPOINT: PointType := RESPAWNPOINT_BLUE;
+        end;
+      end;
+    end;
+
+    AREA_REDFLAG, AREA_BLUEFLAG:
+    begin
+      if Area.AreaType = AREA_REDFLAG then a := FLAG_RED else a := FLAG_BLUE;
+
+      if FlagPoints[a] <> nil then Exit;
+
+      New(FlagPoints[a]);
+
+      with FlagPoints[a]^ do
+      begin
+        X := Area.X-FLAGRECT.X;
+        Y := Area.Y-FLAGRECT.Y;
+        Direction := TDirection(Area.Direction);
+      end;
+
+      with gFlags[a] do
+      begin
+        case a of
+          FLAG_RED: g_Frames_Get(id, 'FRAMES_FLAG_RED');
+          FLAG_BLUE: g_Frames_Get(id, 'FRAMES_FLAG_BLUE');
+        end;
+
+        Animation := TAnimation.Create(id, True, 8);
+        Obj.Rect := FLAGRECT;
+
+        g_Map_ResetFlag(a);
+      end;
+    end;
+
+    AREA_DOMFLAG:
+    begin
+      {SetLength(DOMFlagPoints, Length(DOMFlagPoints)+1);
+      with DOMFlagPoints[High(DOMFlagPoints)] do
+      begin
+        X := Area.X;
+        Y := Area.Y;
+        Direction := TDirection(Area.Direction);
+      end;
+
+      g_Map_CreateFlag(DOMFlagPoints[High(DOMFlagPoints)], FLAG_DOM, FLAG_STATE_NORMAL);}
+    end;
+  end;
+end;
+
+procedure CreateTrigger(Trigger: TTriggerRec_1; fTexturePanel1Type, fTexturePanel2Type: Word);
+var
+  _trigger: TTrigger;
+begin
+  if g_Game_IsClient and not (Trigger.TriggerType in [TRIGGER_SOUND, TRIGGER_MUSIC]) then Exit;
+
+  with _trigger do
+  begin
+    X := Trigger.X;
+    Y := Trigger.Y;
+    Width := Trigger.Width;
+    Height := Trigger.Height;
+    Enabled := ByteBool(Trigger.Enabled);
+    TexturePanel := Trigger.TexturePanel;
+    TexturePanelType := fTexturePanel1Type;
+    ShotPanelType := fTexturePanel2Type;
+    TriggerType := Trigger.TriggerType;
+    ActivateType := Trigger.ActivateType;
+    Keys := Trigger.Keys;
+    Data.Default := Trigger.DATA;
+  end;
+
+  g_Triggers_Create(_trigger);
+end;
+
+procedure CreateMonster(monster: TMonsterRec_1);
+var
+  a, i: Integer;
+begin
+  if g_Game_IsClient then Exit;
+
+  if (gGameSettings.GameType = GT_SINGLE)
+  or LongBool(gGameSettings.Options and GAME_OPTION_MONSTERS) then
+  begin
+    i := g_Monsters_Create(monster.MonsterType, monster.X, monster.Y,
+                           TDirection(monster.Direction));
+
+    if gTriggers <> nil then
+      for a := 0 to High(gTriggers) do
+        if gTriggers[a].TriggerType in [TRIGGER_PRESS,
+             TRIGGER_ON, TRIGGER_OFF, TRIGGER_ONOFF] then
+          if (gTriggers[a].Data.MonsterID-1) = gMonsters[i].StartID then
+            gMonsters[i].AddTrigger(a);
+
+    if monster.MonsterType <> MONSTER_BARREL then
+      Inc(gTotalMonsters);
+  end;
+end;
+
+procedure g_Map_ReAdd_DieTriggers();
+var
+  i, a: Integer;
+begin
+  if g_Game_IsClient then Exit;
+
+  for i := 0 to High(gMonsters) do
+    if gMonsters[i] <> nil then
+      begin
+        gMonsters[i].ClearTriggers();
+
+        for a := 0 to High(gTriggers) do
+          if gTriggers[a].TriggerType in [TRIGGER_PRESS,
+               TRIGGER_ON, TRIGGER_OFF, TRIGGER_ONOFF] then
+            if (gTriggers[a].Data.MonsterID-1) = gMonsters[i].StartID then
+              gMonsters[i].AddTrigger(a);
+      end;
+end;
+
+function extractWadName(resourceName: string): string;
+var
+  posN: Integer;
+begin
+  posN := Pos(':', resourceName);
+  if posN > 0 then
+    Result:= Copy(resourceName, 0, posN-1)
+  else
+    Result := '';
+end;
+
+procedure addResToExternalResList(res: string);
+begin
+  res := extractWadName(res);
+  if (res <> '') and (gExternalResources.IndexOf(res) = -1) then
+    gExternalResources.Add(res);
+end;
+
+procedure generateExternalResourcesList(mapReader: TMapReader_1);
+var
+  textures: TTexturesRec1Array;
+  mapHeader: TMapHeaderRec_1;
+  i: integer;
+  resFile: String = '';
+begin
+  if gExternalResources = nil then
+    gExternalResources := TStringList.Create;
+
+  gExternalResources.Clear;
+  textures := mapReader.GetTextures();
+  for i := 0 to High(textures) do
+  begin
+    addResToExternalResList(resFile);
+  end;
+
+  textures := nil;
+
+  mapHeader := mapReader.GetMapHeader;
+
+  addResToExternalResList(mapHeader.MusicName);
+  addResToExternalResList(mapHeader.SkyName);
+end;
+
+function g_Map_Load(Res: String): Boolean;
+const
+  DefaultMusRes = 'Standart.wad:STDMUS\MUS1';
+  DefaultSkyRes = 'Standart.wad:STDSKY\SKY0';
+var
+  WAD: TWADEditor_1;
+  MapReader: TMapReader_1;
+  Header: TMapHeaderRec_1;
+  _textures: TTexturesRec1Array;
+  panels: TPanelsRec1Array;
+  items: TItemsRec1Array;
+  monsters: TMonsterRec1Array;
+  areas: TAreasRec1Array;
+  triggers: TTriggersRec1Array;
+  a, b, c, k: Integer;
+  PanelID: DWORD;
+  AddTextures: TAddTextureArray;
+  texture: TTextureRec_1;
+  TriggersTable: Array of record
+                           TexturePanel: Integer;
+                           LiftPanel: Integer;
+                           DoorPanel: Integer;
+                           ShotPanel: Integer;
+                          end;
+  FileName, SectionName, ResName,
+  FileName2, s, TexName: String;
+  Data: Pointer;
+  Len: Integer;
+  ok, isAnim, trigRef: Boolean;
+  CurTex: Integer;
+begin
+  Result := False;
+  gMapInfo.Map := Res;
+
+// Çàãðóçêà WAD:
+  g_ProcessResourceStr(Res, FileName, SectionName, ResName);
+  e_WriteLog('Loading map WAD: ' + FileName, MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_WAD_FILE], 0, False);
+
+  WAD := TWADEditor_1.Create();
+  if not WAD.ReadFile(FileName) then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_WAD], [FileName]));
+    WAD.Free();
+    Exit;
+  end;
+  if not WAD.GetResource('', ResName, Data, Len) then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_RES], [ResName]));
+    WAD.Free();
+    Exit;
+  end;
+  WAD.Free();
+
+// Çàãðóçêà êàðòû:
+  e_WriteLog('Loading map: ' + ResName, MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_MAP], 0, False);
+  MapReader := TMapReader_1.Create();
+
+  if not MapReader.LoadMap(Data) then
+  begin
+    g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [Res]));
+    FreeMem(Data);
+    MapReader.Free();
+    Exit;
+  end;
+
+  FreeMem(Data);
+  generateExternalResourcesList(MapReader);
+// Çàãðóçêà òåêñòóð:
+  g_Game_SetLoadingText(_lc[I_LOAD_TEXTURES], 0, False);
+  _textures := MapReader.GetTextures();
+
+// Äîáàâëåíèå òåêñòóð â Textures[]:
+  if _textures <> nil then
+  begin
+    e_WriteLog('  Loading textures:', MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_TEXTURES], High(_textures), False);
+
+    for a := 0 to High(_textures) do
+    begin
+      SetLength(s, 64);
+      CopyMemory(@s[1], @_textures[a].Resource[0], 64);
+      for b := 1 to Length(s) do
+        if s[b] = #0 then
+        begin
+          SetLength(s, b-1);
+          Break;
+        end;
+      e_WriteLog('    Loading texture: ' + s, MSG_NOTIFY);
+    // Àíèìèðîâàííàÿ òåêñòóðà:
+      if ByteBool(_textures[a].Anim) then
+        begin
+          if not CreateAnimTexture(_textures[a].Resource, FileName, True) then
+          begin
+            g_SimpleError(Format(_lc[I_GAME_ERROR_TEXTURE_ANIM], [s]));
+            CreateNullTexture(_textures[a].Resource);
+          end;
+        end
+      else // Îáû÷íàÿ òåêñòóðà:
+        if not CreateTexture(_textures[a].Resource, FileName, True) then
+        begin
+          g_SimpleError(Format(_lc[I_GAME_ERROR_TEXTURE_SIMPLE], [s]));
+          CreateNullTexture(_textures[a].Resource);
+        end;
+
+      g_Game_StepLoading();
+    end;
+  end;
+
+// Çàãðóçêà òðèããåðîâ:
+  gTriggerClientID := 0;
+  e_WriteLog('  Loading triggers...', MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_TRIGGERS], 0, False);
+  triggers := MapReader.GetTriggers();
+
+// Çàãðóçêà ïàíåëåé:
+  e_WriteLog('  Loading panels...', MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_PANELS], 0, False);
+  panels := MapReader.GetPanels();
+
+// Ñîçäàíèå òàáëèöû òðèããåðîâ (ñîîòâåòñòâèå ïàíåëåé òðèããåðàì):
+  if triggers <> nil then
+  begin
+    e_WriteLog('  Setting up trigger table...', MSG_NOTIFY);
+    SetLength(TriggersTable, Length(triggers));
+    g_Game_SetLoadingText(_lc[I_LOAD_TRIGGERS_TABLE], High(TriggersTable), False);
+
+    for a := 0 to High(TriggersTable) do
+    begin
+    // Ñìåíà òåêñòóðû (âîçìîæíî, êíîïêè):
+      TriggersTable[a].TexturePanel := triggers[a].TexturePanel;
+    // Ëèôòû:
+      if triggers[a].TriggerType in [TRIGGER_LIFTUP, TRIGGER_LIFTDOWN, TRIGGER_LIFT] then
+        TriggersTable[a].LiftPanel := TTriggerData(triggers[a].DATA).PanelID
+      else
+        TriggersTable[a].LiftPanel := -1;
+    // Äâåðè:
+      if triggers[a].TriggerType in [TRIGGER_OPENDOOR,
+          TRIGGER_CLOSEDOOR, TRIGGER_DOOR, TRIGGER_DOOR5,
+          TRIGGER_CLOSETRAP, TRIGGER_TRAP] then
+        TriggersTable[a].DoorPanel := TTriggerData(triggers[a].DATA).PanelID
+      else
+        TriggersTable[a].DoorPanel := -1;
+    // Òóðåëü:
+      if triggers[a].TriggerType = TRIGGER_SHOT then
+        TriggersTable[a].ShotPanel := TTriggerData(triggers[a].DATA).ShotPanelID
+      else
+        TriggersTable[a].ShotPanel := -1;
+
+      g_Game_StepLoading();
+    end;
+  end;
+
+// Ñîçäàåì ïàíåëè:
+  if panels <> nil then
+  begin
+    e_WriteLog('  Setting up trigger links...', MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_LINK_TRIGGERS], High(panels), False);
+
+    for a := 0 to High(panels) do
+    begin
+      SetLength(AddTextures, 0);
+      trigRef := False;
+      CurTex := -1;
+      if _textures <> nil then
+        begin
+          texture := _textures[panels[a].TextureNum];
+          ok := True;
+        end
+      else
+        ok := False;
+
+      if ok then
+      begin
+      // Ñìîòðèì, ññûëàþòñÿ ëè íà ýòó ïàíåëü òðèããåðû.
+      // Åñëè äà - òî íàäî ñîçäàòü åùå òåêñòóð:
+        ok := False;
+        if (TriggersTable <> nil) and (_textures <> nil) then
+          for b := 0 to High(TriggersTable) do
+            if (TriggersTable[b].TexturePanel = a)
+            or (TriggersTable[b].ShotPanel = a) then
+            begin
+              trigRef := True;
+              ok := True;
+              Break;
+            end;
+      end;
+
+      if ok then
+      begin // Åñòü ññûëêè òðèããåðîâ íà ýòó ïàíåëü
+        SetLength(s, 64);
+        CopyMemory(@s[1], @texture.Resource[0], 64);
+      // Èçìåðÿåì äëèíó:
+        Len := Length(s);
+        for c := Len downto 1 do
+          if s[c] <> #0 then
+          begin
+            Len := c;
+            Break;
+          end;
+        SetLength(s, Len);
+
+      // Ñïåö-òåêñòóðû çàïðåùåíû:
+        if g_Map_IsSpecialTexture(s) then
+          ok := False
+        else
+      // Îïðåäåëÿåì íàëè÷èå è ïîëîæåíèå öèôð â êîíöå ñòðîêè:
+          ok := g_Texture_NumNameFindStart(s);
+
+      // Åñëè ok, çíà÷èò åñòü öèôðû â êîíöå.
+      // Çàãðóæàåì òåêñòóðû ñ îñòàëüíûìè #:
+        if ok then
+        begin
+          k := NNF_NAME_BEFORE;
+        // Öèêë ïî èçìåíåíèþ èìåíè òåêñòóðû:
+          while ok or (k = NNF_NAME_BEFORE) or
+                (k = NNF_NAME_EQUALS) do
+          begin
+            k := g_Texture_NumNameFindNext(TexName);
+
+            if (k = NNF_NAME_BEFORE) or
+               (k = NNF_NAME_AFTER) then
+              begin
+              // Ïðîáóåì äîáàâèòü íîâóþ òåêñòóðó:
+                if ByteBool(texture.Anim) then
+                  begin // Íà÷àëüíàÿ - àíèìèðîâàííàÿ, èùåì àíèìèðîâàííóþ
+                    isAnim := True;
+                    ok := CreateAnimTexture(TexName, FileName, False);
+                    if not ok then
+                    begin // Íåò àíèìèðîâàííîé, èùåì îáû÷íóþ
+                      isAnim := False;
+                      ok := CreateTexture(TexName, FileName, False);
+                    end;
+                  end
+                else
+                  begin // Íà÷àëüíàÿ - îáû÷íàÿ, èùåì îáû÷íóþ
+                    isAnim := False;
+                    ok := CreateTexture(TexName, FileName, False);
+                    if not ok then
+                    begin // Íåò îáû÷íîé, èùåì àíèìèðîâàííóþ
+                      isAnim := True;
+                      ok := CreateAnimTexture(TexName, FileName, False);
+                    end;
+                  end;
+
+              // Îíà ñóùåñòâóåò. Çàíîñèì åå ID â ñïèñîê ïàíåëè:
+                if ok then
+                begin
+                  for c := 0 to High(Textures) do
+                    if Textures[c].TextureName = TexName then
+                    begin
+                      SetLength(AddTextures, Length(AddTextures)+1);
+                      AddTextures[High(AddTextures)].Texture := c;
+                      AddTextures[High(AddTextures)].Anim := isAnim;
+                      Break;
+                    end;
+                end;
+              end
+            else
+              if k = NNF_NAME_EQUALS then
+                begin
+                // Çàíîñèì òåêóùóþ òåêñòóðó íà ñâîå ìåñòî:
+                  SetLength(AddTextures, Length(AddTextures)+1);
+                  AddTextures[High(AddTextures)].Texture := panels[a].TextureNum;
+                  AddTextures[High(AddTextures)].Anim := ByteBool(texture.Anim);
+                  CurTex := High(AddTextures);
+                  ok := True;
+                end
+              else // NNF_NO_NAME
+                ok := False;
+          end; // while ok...
+
+          ok := True;
+        end; // if ok - åñòü ñìåæíûå òåêñòóðû
+      end; // if ok - ññûëàþòñÿ òðèããåðû
+
+      if not ok then
+      begin
+      // Çàíîñèì òîëüêî òåêóùóþ òåêñòóðó:
+        SetLength(AddTextures, 1);
+        AddTextures[0].Texture := panels[a].TextureNum;
+        AddTextures[0].Anim := ByteBool(texture.Anim);
+        CurTex := 0;
+      end;
+
+    // Ñîçäàåì ïàíåëü è çàïîìèíàåì åå íîìåð:
+      PanelID := CreatePanel(panels[a], AddTextures, CurTex, trigRef);
+
+    // Åñëè èñïîëüçóåòñÿ â òðèããåðàõ, òî ñòàâèì òî÷íûé ID:
+      if TriggersTable <> nil then
+        for b := 0 to High(TriggersTable) do
+        begin
+        // Òðèããåð äâåðè/ëèôòà:
+          if (TriggersTable[b].LiftPanel = a) or
+             (TriggersTable[b].DoorPanel = a) then
+            TTriggerData(triggers[b].DATA).PanelID := PanelID;
+        // Òðèããåð ñìåíû òåêñòóðû:
+          if TriggersTable[b].TexturePanel = a then
+            triggers[b].TexturePanel := PanelID;
+        // Òðèããåð "Òóðåëü":
+          if TriggersTable[b].ShotPanel = a then
+            TTriggerData(triggers[b].DATA).ShotPanelID := PanelID;
+        end;
+
+      g_Game_StepLoading();
+    end;
+  end;
+
+// Åñëè íå LoadState, òî ñîçäàåì òðèããåðû:
+  if (triggers <> nil) and not gLoadGameMode then
+  begin
+    e_WriteLog('  Creating triggers...', MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_CREATE_TRIGGERS], 0, False);
+  // Óêàçûâàåì òèï ïàíåëè, åñëè åñòü:
+    for a := 0 to High(triggers) do
+    begin
+      if triggers[a].TexturePanel <> -1 then
+        b := panels[TriggersTable[a].TexturePanel].PanelType
+      else
+        b := 0;
+      if (triggers[a].TriggerType = TRIGGER_SHOT) and
+         (TTriggerData(triggers[a].DATA).ShotPanelID <> -1) then
+        c := panels[TriggersTable[a].ShotPanel].PanelType
+      else
+        c := 0;
+      CreateTrigger(triggers[a], b, c);
+    end;
+  end;
+
+// Çàãðóçêà ïðåäìåòîâ:
+  e_WriteLog('  Loading triggers...', MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_ITEMS], 0, False);
+  items := MapReader.GetItems();
+
+// Åñëè íå LoadState, òî ñîçäàåì ïðåäìåòû:
+  if (items <> nil) and not gLoadGameMode then
+  begin
+    e_WriteLog('  Spawning items...', MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_CREATE_ITEMS], 0, False);
+    for a := 0 to High(items) do
+      CreateItem(Items[a]);
+  end;
+
+// Çàãðóçêà îáëàñòåé:
+  e_WriteLog('  Loading areas...', MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_AREAS], 0, False);
+  areas := MapReader.GetAreas();
+
+// Åñëè íå LoadState, òî ñîçäàåì îáëàñòè:
+  if areas <> nil then
+  begin
+    e_WriteLog('  Creating areas...', MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_CREATE_AREAS], 0, False);
+    for a := 0 to High(areas) do
+      CreateArea(areas[a]);
+  end;
+
+// Çàãðóçêà ìîíñòðîâ:
+  e_WriteLog('  Loading monsters...', MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTERS], 0, False);
+  monsters := MapReader.GetMonsters();
+
+  gTotalMonsters := 0;
+
+// Åñëè íå LoadState, òî ñîçäàåì ìîíñòðîâ:
+  if (monsters <> nil) and not gLoadGameMode then
+  begin
+    e_WriteLog('  Spawning monsters...', MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_CREATE_MONSTERS], 0, False);
+    for a := 0 to High(monsters) do
+      CreateMonster(monsters[a]);
+  end;
+
+// Çàãðóçêà îïèñàíèÿ êàðòû:
+  e_WriteLog('  Reading map info...', MSG_NOTIFY);
+  g_Game_SetLoadingText(_lc[I_LOAD_MAP_HEADER], 0, False);
+  Header := MapReader.GetMapHeader();
+
+  MapReader.Free();
+
+  with gMapInfo do
+  begin
+    Name := Header.MapName;
+    Description := Header.MapDescription;
+    Author := Header.MapAuthor;
+    MusicName := Header.MusicName;
+    SkyName := Header.SkyName;
+    Height := Header.Height;
+    Width := Header.Width;
+  end;
+
+// Çàãðóçêà íåáà:
+  if gMapInfo.SkyName <> '' then
+  begin
+    e_WriteLog('  Loading sky: ' + gMapInfo.SkyName, MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_SKY], 0, False);
+    g_ProcessResourceStr(gMapInfo.SkyName, FileName, SectionName, ResName);
+  
+    if FileName <> '' then
+      FileName := GameDir+'/wads/'+FileName
+    else
+      begin
+        g_ProcessResourceStr(Res, @FileName2, nil, nil);
+        FileName := FileName2;
+      end;
+
+    s := FileName+':'+SectionName+'/'+ResName;
+    if g_Texture_CreateWAD(BackID, s) then
+      begin
+        g_Game_SetupScreenSize();
+      end
+    else
+      g_FatalError(Format(_lc[I_GAME_ERROR_SKY], [s]));
+  end;
+
+// Çàãðóçêà ìóçûêè:
+  ok := False;
+  if gMapInfo.MusicName <> '' then
+  begin
+    e_WriteLog('  Loading music: ' + gMapInfo.MusicName, MSG_NOTIFY);
+    g_Game_SetLoadingText(_lc[I_LOAD_MUSIC], 0, False);
+    g_ProcessResourceStr(gMapInfo.MusicName, FileName, SectionName, ResName);
+
+    if FileName <> '' then
+      FileName := GameDir+'/wads/'+FileName
+    else
+      begin
+        g_ProcessResourceStr(Res, @FileName2, nil, nil);
+        FileName := FileName2;
+      end;
+
+    s := FileName+':'+SectionName+'/'+ResName;
+    if g_Sound_CreateWADEx(gMapInfo.MusicName, s, True) then
+      ok := True
+    else
+      g_FatalError(Format(_lc[I_GAME_ERROR_MUSIC], [s]));
+  end;
+
+// Îñòàëüíûå óñòàíâêè:
+  CreateDoorMap();
+  CreateLiftMap();
+
+  g_Items_Init();
+  g_Weapon_Init();
+  g_Monsters_Init();
+
+// Åñëè íå LoadState, òî ñîçäàåì êàðòó ñòîëêíîâåíèé:
+  if not gLoadGameMode then
+    g_GFX_Init();
+
+// Ñáðîñ ëîêàëüíûõ ìàññèâîâ:
+  _textures := nil;
+  panels := nil;
+  items := nil;
+  areas := nil;
+  triggers := nil;
+  TriggersTable := nil;
+  AddTextures := nil;
+
+// Âêëþ÷àåì ìóçûêó, åñëè ýòî íå çàãðóçêà:
+  if ok and (not gLoadGameMode) then
+    begin
+      gMusic.SetByName(gMapInfo.MusicName);
+      gMusic.Play();
+    end
+  else
+    gMusic.SetByName('');
+
+  e_WriteLog('Done loading map.', MSG_NOTIFY);
+  Result := True;
+end;
+
+function g_Map_GetMapInfo(Res: String): TMapInfo;
+var
+  WAD: TWADEditor_1;
+  MapReader: TMapReader_1;
+  Header: TMapHeaderRec_1;
+  FileName, SectionName, ResName: String;
+  Data: Pointer;
+  Len: Integer;
+begin
+  g_ProcessResourceStr(Res, FileName, SectionName, ResName);
+
+  WAD := TWADEditor_1.Create();
+  if not WAD.ReadFile(FileName) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  if not WAD.GetResource('', ResName, Data, Len) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  WAD.Free();
+
+  MapReader := TMapReader_1.Create();
+
+  if not MapReader.LoadMap(Data) then
+    begin
+      g_Console_Add(Format(_lc[I_GAME_ERROR_MAP_LOAD], [Res]), True);
+      ZeroMemory(@Header, SizeOf(Header));
+      Result.Name := _lc[I_GAME_ERROR_MAP_SELECT];
+      Result.Description := _lc[I_GAME_ERROR_MAP_SELECT];
+    end
+  else
+    begin
+      Header := MapReader.GetMapHeader();
+      Result.Name := Header.MapName;
+      Result.Description := Header.MapDescription;
+    end;
+
+  FreeMem(Data);
+  MapReader.Free();
+
+  Result.Map := Res;
+  Result.Author := Header.MapAuthor;
+  Result.Height := Header.Height;
+  Result.Width := Header.Width;
+end;
+
+function g_Map_GetMapsList(WADName: string): SArray;
+var
+  WAD: TWADEditor_1;
+  a: Integer;
+  ResList: SArray;
+  Data: Pointer;
+  Len: Integer;
+  Sign: Array [0..2] of Char;
+begin
+  Result := nil;
+
+  WAD := TWADEditor_1.Create();
+  if not WAD.ReadFile(WADName) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  ResList := WAD.GetResourcesList('');
+
+  if ResList <> nil then
+    for a := 0 to High(ResList) do
+    begin
+      if not WAD.GetResource('', ResList[a], Data, Len) then Continue;
+      CopyMemory(@Sign[0], Data, 3);
+      FreeMem(Data);
+   
+      if Sign = MAP_SIGNATURE then
+      begin
+        SetLength(Result, Length(Result)+1);
+        Result[High(Result)] := ResList[a];
+      end;
+   
+      Sign := '';
+    end;
+
+  WAD.Free();
+end;
+
+function g_Map_Exist(Res: string): Boolean;
+var
+  WAD: TWADEditor_1;
+  FileName, SectionName, ResName: string;
+  ResList: SArray;
+  a: Integer;
+begin
+  Result := False;
+
+  g_ProcessResourceStr(Res, FileName, SectionName, ResName);
+
+  if Pos('.wad', LowerCase(FileName)) = 0 then FileName := FileName+'.wad';
+
+  WAD := TWADEditor_1.Create;
+  if not WAD.ReadFile(FileName) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  ResList := WAD.GetResourcesList('');
+  WAD.Free();
+  if ResList <> nil then
+    for a := 0 to High(ResList) do if ResList[a] = ResName then
+    begin
+      Result := True;
+      Exit;
+    end;
+end;
+
+procedure g_Map_Free();
+var
+  a: Integer;
+
+  procedure FreePanelArray(var panels: TPanelArray);
+  var
+    i: Integer;
+
+  begin
+    if panels <> nil then
+    begin
+      for i := 0 to High(panels) do
+        panels[i].Free();
+      panels := nil;
+    end;
+  end;
+
+begin
+  g_GFX_Free();
+  g_Weapon_Free();
+  g_Items_Free();
+  g_Triggers_Free();
+  g_Monsters_Free();
+
+  RespawnPoints := nil;
+  if FlagPoints[FLAG_RED] <> nil then
+  begin
+    Dispose(FlagPoints[FLAG_RED]);
+    FlagPoints[FLAG_RED] := nil;
+  end;
+  if FlagPoints[FLAG_BLUE] <> nil then
+  begin
+    Dispose(FlagPoints[FLAG_BLUE]);
+    FlagPoints[FLAG_BLUE] := nil;
+  end;
+  //DOMFlagPoints := nil;
+
+  //gDOMFlags := nil;
+
+  if Textures <> nil then
+  begin
+    for a := 0 to High(Textures) do
+      if not g_Map_IsSpecialTexture(Textures[a].TextureName) then
+        if Textures[a].Anim then
+          g_Frames_DeleteByID(Textures[a].FramesID)
+        else
+          if Textures[a].TextureID <> TEXTURE_NONE then
+            e_DeleteTexture(Textures[a].TextureID);
+
+    Textures := nil;
+  end;
+
+  FreePanelArray(gWalls);
+  FreePanelArray(gRenderBackgrounds);
+  FreePanelArray(gRenderForegrounds);
+  FreePanelArray(gWater);
+  FreePanelArray(gAcid1);
+  FreePanelArray(gAcid2);
+  FreePanelArray(gSteps);
+  FreePanelArray(gLifts);
+  FreePanelArray(gBlockMon);
+
+  if BackID <> DWORD(-1) then
+  begin
+    gBackSize.X := 0;
+    gBackSize.Y := 0;
+    e_DeleteTexture(BackID);
+    BackID := DWORD(-1);
+  end;
+
+  g_Game_StopAllSounds(False);
+  gMusic.FreeSound();
+  g_Sound_Delete(gMapInfo.MusicName);
+
+  gMapInfo.Name := '';
+  gMapInfo.Description := '';
+  gMapInfo.MusicName := '';
+  gMapInfo.Height := 0;
+  gMapInfo.Width := 0;
+
+  gDoorMap := nil;
+  gLiftMap := nil;
+end;
+
+procedure g_Map_Update();
+var
+  a, d, j: Integer;
+  m: Word;
+  s: String;
+
+  procedure UpdatePanelArray(var panels: TPanelArray);
+  var
+    i: Integer;
+
+  begin
+    if panels <> nil then
+      for i := 0 to High(panels) do
+        panels[i].Update();
+  end;
+
+begin
+  UpdatePanelArray(gWalls);
+  UpdatePanelArray(gRenderBackgrounds);
+  UpdatePanelArray(gRenderForegrounds);
+  UpdatePanelArray(gWater);
+  UpdatePanelArray(gAcid1);
+  UpdatePanelArray(gAcid2);
+  UpdatePanelArray(gSteps);
+
+  if gGameSettings.GameMode = GM_CTF then
+  begin
+    for a := FLAG_RED to FLAG_BLUE do
+      if not (gFlags[a].State in [FLAG_STATE_NONE, FLAG_STATE_CAPTURED]) then
+        with gFlags[a] do
+        begin
+          if gFlags[a].Animation <> nil then
+            gFlags[a].Animation.Update();
+
+          m := g_Obj_Move(@Obj, True, True);
+
+          if gTime mod (GAME_TICK*2) <> 0 then
+            Continue;
+
+        // Ñîïðîòèâëåíèå âîçäóõà:
+          Obj.Vel.X := z_dec(Obj.Vel.X, 1);
+
+        // Òàéìàóò ïîòåðÿííîãî ôëàãà, ëèáî îí âûïàë çà êàðòó:
+          if ((Count = 0) or ByteBool(m and MOVE_FALLOUT)) and g_Game_IsServer then
+          begin
+            g_Map_ResetFlag(a);
+            gFlags[a].CaptureTime := 0;
+            if a = FLAG_RED then
+              s := _lc[I_PLAYER_FLAG_RED]
+            else
+              s := _lc[I_PLAYER_FLAG_BLUE];
+            g_Game_Message(Format(_lc[I_MESSAGE_FLAG_RETURN], [AnsiUpperCase(s)]), 144);
+
+            if g_Game_IsNet then
+              MH_SEND_FlagEvent(FLAG_STATE_RETURNED, a, 0);
+            Continue;
+          end;
+
+          if Count > 0 then
+            Count := Count - 1;
+
+        // Èãðîê áåðåò ôëàã:
+          if gPlayers <> nil then
+          begin
+            j := Random(Length(gPlayers)) - 1;
+
+            for d := 0 to High(gPlayers) do
+            begin
+              Inc(j);
+              if j > High(gPlayers) then
+                j := 0;
+
+              if gPlayers[j] <> nil then
+                if gPlayers[j].Live and
+                   g_Obj_Collide(@Obj, @gPlayers[j].Obj) then
+                begin
+                  if gPlayers[j].GetFlag(a) then
+                    Break;
+                end;
+            end;
+          end;
+        end;
+  end;
+end;
+
+procedure g_Map_DrawPanels(PanelType: Word);
+
+  procedure DrawPanels(var panels: TPanelArray;
+                       drawDoors: Boolean = False);
+  var
+    a: Integer;
+
+  begin
+    if panels <> nil then
+      for a := 0 to High(panels) do
+        if not (drawDoors xor panels[a].Door) then
+          panels[a].Draw();
+  end;
+          
+begin
+  case PanelType of
+    PANEL_WALL:       DrawPanels(gWalls);
+    PANEL_CLOSEDOOR:  DrawPanels(gWalls, True);
+    PANEL_BACK:       DrawPanels(gRenderBackgrounds);
+    PANEL_FORE:       DrawPanels(gRenderForegrounds);
+    PANEL_WATER:      DrawPanels(gWater);
+    PANEL_ACID1:      DrawPanels(gAcid1);
+    PANEL_ACID2:      DrawPanels(gAcid2);
+    PANEL_STEP:       DrawPanels(gSteps);
+  end;
+end;
+
+procedure g_Map_DrawBack(dx, dy: Integer);
+begin
+  if gDrawBackGround and (BackID <> DWORD(-1)) then
+    e_DrawSize(BackID, dx, dy, 0, False, False, gBackSize.X, gBackSize.Y)
+  else
+    e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+end;
+
+function g_Map_CollidePanel(X, Y: Integer; Width, Height: Word;
+                            PanelType: Word; b1x3: Boolean): Boolean;
+var
+  a, h: Integer;
+begin
+ Result := False;
+
+ if WordBool(PanelType and PANEL_WALL) then
+    if gWalls <> nil then
+    begin
+      h := High(gWalls);
+
+      for a := 0 to h do
+        if gWalls[a].Enabled and
+        g_Collide(X, Y, Width, Height,
+                  gWalls[a].X, gWalls[a].Y,
+                  gWalls[a].Width, gWalls[a].Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+    end;
+
+  if WordBool(PanelType and PANEL_WATER) then
+    if gWater <> nil then
+    begin
+      h := High(gWater);
+
+      for a := 0 to h do
+      if g_Collide(X, Y, Width, Height,
+                   gWater[a].X, gWater[a].Y,
+                   gWater[a].Width, gWater[a].Height) then
+      begin
+        Result := True;
+        Exit;
+      end;
+    end;
+
+  if WordBool(PanelType and PANEL_ACID1) then
+    if gAcid1 <> nil then
+    begin
+      h := High(gAcid1);
+
+      for a := 0 to h do
+        if g_Collide(X, Y, Width, Height,
+                     gAcid1[a].X, gAcid1[a].Y,
+                     gAcid1[a].Width, gAcid1[a].Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+    end;
+
+  if WordBool(PanelType and PANEL_ACID2) then
+    if gAcid2 <> nil then
+    begin
+      h := High(gAcid2);
+
+      for a := 0 to h do
+        if g_Collide(X, Y, Width, Height,
+                     gAcid2[a].X, gAcid2[a].Y,
+                     gAcid2[a].Width, gAcid2[a].Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+    end;
+
+  if WordBool(PanelType and PANEL_STEP) then
+    if gSteps <> nil then
+    begin
+      h := High(gSteps);
+
+      for a := 0 to h do
+        if g_Collide(X, Y, Width, Height,
+                     gSteps[a].X, gSteps[a].Y,
+                     gSteps[a].Width, gSteps[a].Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+    end;
+
+  if WordBool(PanelType and (PANEL_LIFTUP or PANEL_LIFTDOWN or PANEL_LIFTLEFT or PANEL_LIFTRIGHT)) then
+    if gLifts <> nil then
+    begin
+      h := High(gLifts);
+
+      for a := 0 to h do
+        if ((WordBool(PanelType and (PANEL_LIFTUP)) and (gLifts[a].LiftType = 0)) or
+           (WordBool(PanelType and (PANEL_LIFTDOWN)) and (gLifts[a].LiftType = 1)) or
+           (WordBool(PanelType and (PANEL_LIFTLEFT)) and (gLifts[a].LiftType = 2)) or
+           (WordBool(PanelType and (PANEL_LIFTRIGHT)) and (gLifts[a].LiftType = 3))) and
+           g_Collide(X, Y, Width, Height,
+           gLifts[a].X, gLifts[a].Y,
+           gLifts[a].Width, gLifts[a].Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+    end;
+
+  if WordBool(PanelType and PANEL_BLOCKMON) then
+    if gBlockMon <> nil then
+    begin
+      h := High(gBlockMon);
+
+      for a := 0 to h do
+        if ( (not b1x3) or
+             ((gBlockMon[a].Width + gBlockMon[a].Height) >= 64) ) and 
+           g_Collide(X, Y, Width, Height,
+           gBlockMon[a].X, gBlockMon[a].Y,
+           gBlockMon[a].Width, gBlockMon[a].Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+    end;
+end;
+
+function g_Map_CollideLiquid_Texture(X, Y: Integer; Width, Height: Word): DWORD;
+var
+  a, h: Integer;
+begin
+  Result := TEXTURE_NONE;
+
+  if gWater <> nil then
+  begin
+    h := High(gWater);
+
+    for a := 0 to h do
+      if g_Collide(X, Y, Width, Height,
+                   gWater[a].X, gWater[a].Y,
+                   gWater[a].Width, gWater[a].Height) then
+      begin
+        Result := gWater[a].GetTextureID();
+        Exit;
+      end;
+  end;
+
+  if gAcid1 <> nil then
+  begin
+    h := High(gAcid1);
+
+    for a := 0 to h do
+      if g_Collide(X, Y, Width, Height,
+                   gAcid1[a].X, gAcid1[a].Y,
+                   gAcid1[a].Width, gAcid1[a].Height) then
+      begin
+        Result := gAcid1[a].GetTextureID();
+        Exit;
+      end;
+  end;
+
+  if gAcid2 <> nil then
+  begin
+    h := High(gAcid2);
+
+    for a := 0 to h do
+      if g_Collide(X, Y, Width, Height,
+                   gAcid2[a].X, gAcid2[a].Y,
+                   gAcid2[a].Width, gAcid2[a].Height) then
+      begin
+        Result := gAcid2[a].GetTextureID();
+        Exit;
+      end;
+  end;
+end;
+
+procedure g_Map_EnableWall(ID: DWORD);
+begin
+  with gWalls[ID] do
+  begin
+    Enabled := True;
+    g_Mark(X, Y, Width, Height, MARK_DOOR, True);
+
+    if g_Game_IsServer and g_Game_IsNet then MH_SEND_PanelState(PanelType, ID);
+  end;
+end;
+
+procedure g_Map_DisableWall(ID: DWORD);
+begin
+  with gWalls[ID] do
+  begin
+    Enabled := False;
+    g_Mark(X, Y, Width, Height, MARK_DOOR, False);
+
+    if g_Game_IsServer and g_Game_IsNet then MH_SEND_PanelState(PanelType, ID);
+  end;
+end;
+
+procedure g_Map_SwitchTexture(PanelType: Word; ID: DWORD; AnimLoop: Byte = 0);
+var
+  tp: TPanel;
+begin            
+  case PanelType of
+    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+      tp := gWalls[ID];
+    PANEL_FORE:
+      tp := gRenderForegrounds[ID];
+    PANEL_BACK:
+      tp := gRenderBackgrounds[ID];
+    PANEL_WATER:
+      tp := gWater[ID];
+    PANEL_ACID1:
+      tp := gAcid1[ID];
+    PANEL_ACID2:
+      tp := gAcid2[ID];
+    PANEL_STEP:
+      tp := gSteps[ID];
+    else
+      Exit;
+  end;
+
+  tp.NextTexture(AnimLoop);
+  if g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_PanelTexture(PanelType, ID, AnimLoop);
+end;
+
+procedure g_Map_SetLift(ID: DWORD; t: Integer);
+begin
+  if gLifts[ID].LiftType = t then
+    Exit;
+
+  with gLifts[ID] do
+  begin
+    LiftType := t;
+
+    g_Mark(X, Y, Width, Height, MARK_LIFT, False);
+
+    if LiftType = 0 then
+      g_Mark(X, Y, Width, Height, MARK_LIFTUP, True)
+    else if LiftType = 1 then
+      g_Mark(X, Y, Width, Height, MARK_LIFTDOWN, True)
+    else if LiftType = 2 then
+      g_Mark(X, Y, Width, Height, MARK_LIFTLEFT, True)
+    else if LiftType = 3 then
+      g_Mark(X, Y, Width, Height, MARK_LIFTRIGHT, True);
+
+    if g_Game_IsServer and g_Game_IsNet then MH_SEND_PanelState(PanelType, ID);
+  end;
+end;
+
+function g_Map_GetPoint(PointType: Byte; var RespawnPoint: TRespawnPoint): Boolean;
+var
+  a: Integer;
+  PointsArray: Array of TRespawnPoint;
+begin
+  Result := False;
+  SetLength(PointsArray, 0);
+
+  if RespawnPoints = nil then
+    Exit;
+
+  for a := 0 to High(RespawnPoints) do
+    if RespawnPoints[a].PointType = PointType then
+    begin
+      SetLength(PointsArray, Length(PointsArray)+1);
+      PointsArray[High(PointsArray)] := RespawnPoints[a];
+    end;
+
+  if PointsArray = nil then
+    Exit;
+
+  RespawnPoint := PointsArray[Random(Length(PointsArray))];
+  Result := True;
+end;
+
+function g_Map_GetPointCount(PointType: Byte): Word;
+var
+  a: Integer;
+begin
+  Result := 0;
+
+  if RespawnPoints = nil then
+    Exit;
+
+  for a := 0 to High(RespawnPoints) do
+    if RespawnPoints[a].PointType = PointType then
+      Result := Result + 1;
+end;
+
+function g_Map_HaveFlagPoints(): Boolean;
+begin
+  Result := (FlagPoints[FLAG_RED] <> nil) and (FlagPoints[FLAG_BLUE] <> nil);
+end;
+
+procedure g_Map_ResetFlag(Flag: Byte);
+begin
+  with gFlags[Flag] do
+  begin
+    Obj.X := -1000;
+    Obj.Y := -1000;
+    Obj.Vel.X := 0;
+    Obj.Vel.Y := 0;
+    Direction := D_LEFT;
+    State := FLAG_STATE_NONE;
+    if FlagPoints[Flag] <> nil then
+    begin
+      Obj.X := FlagPoints[Flag]^.X;
+      Obj.Y := FlagPoints[Flag]^.Y;
+      Direction := FlagPoints[Flag]^.Direction;
+      State := FLAG_STATE_NORMAL;
+    end;
+    Count := -1;
+  end;
+end;
+
+procedure g_Map_DrawFlags();
+var
+  i, dx: Integer;
+  Mirror: TMirrorType;
+begin
+  if gGameSettings.GameMode <> GM_CTF then
+    Exit;
+
+  for i := FLAG_RED to FLAG_BLUE do
+    with gFlags[i] do
+      if State <> FLAG_STATE_CAPTURED then
+      begin
+        if State = FLAG_STATE_NONE then
+          continue;
+
+        if Direction = D_LEFT then
+          begin
+            Mirror := M_HORIZONTAL;
+            dx := -1;
+          end
+        else
+          begin
+            Mirror := M_NONE;
+            dx := 1;
+          end;
+
+        Animation.Draw(Obj.X+dx, Obj.Y+1, Mirror);
+
+        if g_debug_Frames then
+        begin
+          e_DrawQuad(Obj.X+Obj.Rect.X,
+                     Obj.Y+Obj.Rect.Y,
+                     Obj.X+Obj.Rect.X+Obj.Rect.Width-1,
+                     Obj.Y+Obj.Rect.Y+Obj.Rect.Height-1,
+                     0, 255, 0);
+        end;
+      end;
+end;
+
+procedure g_Map_SaveState(Var Mem: TBinMemoryWriter);
+var
+  dw: DWORD;
+  b: Byte;
+  str: String;
+  boo: Boolean;
+
+  procedure SavePanelArray(var panels: TPanelArray);
+  var
+    PAMem: TBinMemoryWriter;
+    i: Integer;
+  begin
+  // Ñîçäàåì íîâûé ñïèñîê ñîõðàíÿåìûõ ïàíåëåé:
+    PAMem := TBinMemoryWriter.Create((Length(panels)+1) * 40);
+
+    i := 0;
+    while i < Length(panels) do
+    begin
+      if panels[i].SaveIt then
+      begin
+      // ID ïàíåëè:
+        PAMem.WriteInt(i);
+      // Ñîõðàíÿåì ïàíåëü:
+        panels[i].SaveState(PAMem);
+      end;
+      Inc(i);
+    end;
+
+  // Ñîõðàíÿåì ýòîò ñïèñîê ïàíåëåé:
+    PAMem.SaveToMemory(Mem);
+    PAMem.Free();
+  end;
+
+  procedure SaveFlag(flag: PFlag);
+  begin
+  // Ñèãíàòóðà ôëàãà:
+    dw := FLAG_SIGNATURE; // 'FLAG'
+    Mem.WriteDWORD(dw);
+  // Âðåìÿ ïåðåïîÿâëåíèÿ ôëàãà:
+    Mem.WriteByte(flag^.RespawnType);
+  // Ñîñòîÿíèå ôëàãà:
+    Mem.WriteByte(flag^.State);
+  // Íàïðàâëåíèå ôëàãà:
+    if flag^.Direction = D_LEFT then
+      b := 1
+    else // D_RIGHT
+      b := 2;
+    Mem.WriteByte(b);
+  // Îáúåêò ôëàãà:
+    Obj_SaveState(@flag^.Obj, Mem);
+  end;
+
+begin
+  Mem := TBinMemoryWriter.Create(1024 * 1024); // 1 MB
+
+///// Ñîõðàíÿåì ñïèñêè ïàíåëåé: /////
+// Ñîõðàíÿåì ïàíåëè ñòåí è äâåðåé:
+  SavePanelArray(gWalls);
+// Ñîõðàíÿåì ïàíåëè ôîíà:
+  SavePanelArray(gRenderBackgrounds);
+// Ñîõðàíÿåì ïàíåëè ïåðåäíåãî ïëàíà:
+  SavePanelArray(gRenderForegrounds);
+// Ñîõðàíÿåì ïàíåëè âîäû:
+  SavePanelArray(gWater);
+// Ñîõðàíÿåì ïàíåëè êèñëîòû-1:
+  SavePanelArray(gAcid1);
+// Ñîõðàíÿåì ïàíåëè êèñëîòû-2:
+  SavePanelArray(gAcid2);
+// Ñîõðàíÿåì ïàíåëè ñòóïåíåé:
+  SavePanelArray(gSteps);
+// Ñîõðàíÿåì ïàíåëè ëèôòîâ:
+  SavePanelArray(gLifts);
+///// /////
+
+///// Ñîõðàíÿåì ìóçûêó: /////
+// Ñèãíàòóðà ìóçûêè:
+  dw := MUSIC_SIGNATURE; // 'MUSI'
+  Mem.WriteDWORD(dw);
+// Íàçâàíèå ìóçûêè:
+  Assert(gMusic <> nil, 'g_Map_SaveState: gMusic = nil');
+  if gMusic.NoMusic then
+    str := ''
+  else
+    str := gMusic.Name;
+  Mem.WriteString(str, 64);
+// Ïîçèöèÿ ïðîèãðûâàíèÿ ìóçûêè:
+  dw := gMusic.GetPosition();
+  Mem.WriteDWORD(dw);
+// Ñòîèò ëè ìóçûêà íà ñïåö-ïàóçå:
+  boo := gMusic.SpecPause;
+  Mem.WriteBoolean(boo);
+///// /////
+
+///// Ñîõðàíÿåì êîëè÷åñòâî ìîíñòðîâ: /////
+  Mem.WriteInt(gTotalMonsters);
+///// /////
+
+//// Ñîõðàíÿåì ôëàãè, åñëè ýòî CTF: /////
+  if gGameSettings.GameMode = GM_CTF then
+  begin
+  // Ôëàã Êðàñíîé êîìàíäû:
+    SaveFlag(@gFlags[FLAG_RED]);
+  // Ôëàã Ñèíåé êîìàíäû:
+    SaveFlag(@gFlags[FLAG_BLUE]);
+  end;
+///// /////
+
+///// Ñîõðàíÿåì êîëè÷åñòâî ïîáåä, åñëè ýòî TDM/CTF: /////
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+  begin
+  // Î÷êè Êðàñíîé êîìàíäû:
+    Mem.WriteSmallInt(gTeamStat[TEAM_RED].Goals);
+  // Î÷êè Ñèíåé êîìàíäû:
+    Mem.WriteSmallInt(gTeamStat[TEAM_BLUE].Goals);
+  end;
+///// /////
+end;
+
+procedure g_Map_LoadState(Var Mem: TBinMemoryReader);
+var
+  dw: DWORD;
+  b: Byte;
+  str: String;
+  boo: Boolean;
+
+  procedure LoadPanelArray(var panels: TPanelArray);
+  var
+    PAMem: TBinMemoryReader;
+    i, id: Integer;
+  begin
+  // Çàãðóæàåì òåêóùèé ñïèñîê ïàíåëåé:
+    PAMem := TBinMemoryReader.Create();
+    PAMem.LoadFromMemory(Mem);
+
+    for i := 0 to Length(panels)-1 do
+      if panels[i].SaveIt then
+      begin
+      // ID ïàíåëè:
+        PAMem.ReadInt(id);
+        if id <> i then
+        begin
+          raise EBinSizeError.Create('g_Map_LoadState: LoadPanelArray: Wrong Panel ID');
+        end;
+      // Çàãðóæàåì ïàíåëü:
+        panels[i].LoadState(PAMem);
+      end;
+
+  // Ýòîò ñïèñîê ïàíåëåé çàãðóæåí:
+    PAMem.Free();
+  end;
+
+  procedure LoadFlag(flag: PFlag);
+  begin
+  // Ñèãíàòóðà ôëàãà:
+    Mem.ReadDWORD(dw);
+    if dw <> FLAG_SIGNATURE then // 'FLAG'
+    begin
+      raise EBinSizeError.Create('g_Map_LoadState: LoadFlag: Wrong Flag Signature');
+    end;
+  // Âðåìÿ ïåðåïîÿâëåíèÿ ôëàãà:
+    Mem.ReadByte(flag^.RespawnType);
+  // Ñîñòîÿíèå ôëàãà:
+    Mem.ReadByte(flag^.State);
+  // Íàïðàâëåíèå ôëàãà:
+    Mem.ReadByte(b);
+    if b = 1 then
+      flag^.Direction := D_LEFT
+    else // b = 2
+      flag^.Direction := D_RIGHT;
+  // Îáúåêò ôëàãà:
+    Obj_LoadState(@flag^.Obj, Mem);
+  end;
+
+begin
+  if Mem = nil then
+    Exit;
+
+///// Çàãðóæàåì ñïèñêè ïàíåëåé: /////
+// Çàãðóæàåì ïàíåëè ñòåí è äâåðåé:
+  LoadPanelArray(gWalls);
+// Çàãðóæàåì ïàíåëè ôîíà:
+  LoadPanelArray(gRenderBackgrounds);
+// Çàãðóæàåì ïàíåëè ïåðåäíåãî ïëàíà:
+  LoadPanelArray(gRenderForegrounds);
+// Çàãðóæàåì ïàíåëè âîäû:
+  LoadPanelArray(gWater);
+// Çàãðóæàåì ïàíåëè êèñëîòû-1:
+  LoadPanelArray(gAcid1);
+// Çàãðóæàåì ïàíåëè êèñëîòû-2:
+  LoadPanelArray(gAcid2);
+// Çàãðóæàåì ïàíåëè ñòóïåíåé:
+  LoadPanelArray(gSteps);
+// Çàãðóæàåì ïàíåëè ëèôòîâ:
+  LoadPanelArray(gLifts);
+///// /////
+
+// Îáíîâëÿåì êàðòó ñòîëêíîâåíèé:
+  g_GFX_Init();
+
+///// Çàãðóæàåì ìóçûêó: /////
+// Ñèãíàòóðà ìóçûêè:
+  Mem.ReadDWORD(dw);
+  if dw <> MUSIC_SIGNATURE then // 'MUSI'
+  begin
+    raise EBinSizeError.Create('g_Map_LoadState: Wrong Music Signature');
+  end;
+// Íàçâàíèå ìóçûêè:
+  Assert(gMusic <> nil, 'g_Map_LoadState: gMusic = nil');
+  Mem.ReadString(str);
+// Ïîçèöèÿ ïðîèãðûâàíèÿ ìóçûêè:
+  Mem.ReadDWORD(dw);
+// Ñòîèò ëè ìóçûêà íà ñïåö-ïàóçå:
+  Mem.ReadBoolean(boo);
+// Çàïóñêàåì ýòó ìóçûêó:
+  gMusic.SetByName(str);
+  gMusic.SpecPause := boo;
+  gMusic.Play();
+  gMusic.Pause(True);
+  gMusic.SetPosition(dw);
+///// /////
+
+///// Çàãðóæàåì êîëè÷åñòâî ìîíñòðîâ: /////
+  Mem.ReadInt(gTotalMonsters);
+///// /////
+
+//// Çàãðóæàåì ôëàãè, åñëè ýòî CTF: /////
+  if gGameSettings.GameMode = GM_CTF then
+  begin
+  // Ôëàã Êðàñíîé êîìàíäû:
+    LoadFlag(@gFlags[FLAG_RED]);
+  // Ôëàã Ñèíåé êîìàíäû:
+    LoadFlag(@gFlags[FLAG_BLUE]);
+  end;
+///// /////
+
+///// Çàãðóæàåì êîëè÷åñòâî ïîáåä, åñëè ýòî TDM/CTF: /////
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+  begin
+  // Î÷êè Êðàñíîé êîìàíäû:
+    Mem.ReadSmallInt(gTeamStat[TEAM_RED].Goals);
+  // Î÷êè Ñèíåé êîìàíäû:
+    Mem.ReadSmallInt(gTeamStat[TEAM_BLUE].Goals);
+  end;
+///// /////
+end;
+
+end.
diff --git a/src/game/g_menu.pas b/src/game/g_menu.pas
new file mode 100644 (file)
index 0000000..45413de
--- /dev/null
@@ -0,0 +1,3059 @@
+unit g_menu;
+
+interface
+
+procedure g_Menu_Init();
+procedure g_Menu_Free();
+procedure g_Menu_Reset();
+procedure LoadStdFont(cfgres, texture: string; var FontID: DWORD);
+procedure LoadFont(txtres, fntres: string; var FontID: DWORD);
+procedure g_Menu_AskLanguage();
+
+procedure g_Menu_Show_SaveMenu();
+procedure g_Menu_Show_LoadMenu();
+procedure g_Menu_Show_GameSetGame();
+procedure g_Menu_Show_OptionsVideo();
+procedure g_Menu_Show_OptionsSound();
+procedure g_Menu_Show_EndGameMenu();
+procedure g_Menu_Show_QuitGameMenu();
+
+var
+  gMenuFont: DWORD;
+  gMenuSmallFont: DWORD;
+  PromptIP: string;
+  PromptPort: Word;
+
+implementation
+
+uses
+  g_gui, g_textures, e_graphics, g_main, g_window, g_game, g_map,
+  g_basic, g_console, g_sound, g_gfx, g_player, g_options,
+  e_log, SysUtils, CONFIG, g_playermodel, DateUtils,
+  MAPSTRUCT, WADEDITOR, Math, WADSTRUCT, g_saveload,
+  e_textures, GL, GLExt, g_language,
+  g_net, g_netmsg, g_netmaster, g_items, e_input;
+
+procedure ProcChangeColor(Sender: TGUIControl); forward;
+procedure ProcSelectModel(Sender: TGUIControl); forward;
+
+procedure ProcApplyOptions();
+var
+  menu: TGUIMenu;
+  i: Integer;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsVideoMenu').GetControl('mOptionsVideoMenu'));
+
+  if TGUISwitch(menu.GetControl('swBPP')).ItemIndex = 0 then
+    gBPP := 16
+  else
+    gBPP := 32;
+  gVSync := TGUISwitch(menu.GetControl('swVSync')).ItemIndex = 0;
+  gTextureFilter := TGUISwitch(menu.GetControl('swTextureFilter')).ItemIndex = 0;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsSoundMenu').GetControl('mOptionsSoundMenu'));
+
+  g_Sound_SetupAllVolumes(
+    Min(TGUIScroll(menu.GetControl('scSoundLevel')).Value*16, 255),
+    Min(TGUIScroll(menu.GetControl('scMusicLevel')).Value*16, 255)
+  );
+
+  gMaxSimSounds := Max(Min(TGUIScroll(menu.GetControl('scMaxSimSounds')).Value*4+2, 66), 2);
+  gMuteWhenInactive := TGUISwitch(menu.GetControl('swInactiveSounds')).ItemIndex = 1;
+  gAnnouncer := TGUISwitch(menu.GetControl('swAnnouncer')).ItemIndex;
+  gSoundEffectsDF := TGUISwitch(menu.GetControl('swSoundEffects')).ItemIndex = 1;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsGameMenu').GetControl('mOptionsGameMenu'));
+
+  g_GFX_SetMax(TGUIScroll(menu.GetControl('scParticlesCount')).Value*1000);
+  g_Shells_SetMax(TGUIScroll(menu.GetControl('scShellsMax')).Value*30);
+  g_Gibs_SetMax(TGUIScroll(menu.GetControl('scGibsMax')).Value*25);
+  g_Corpses_SetMax(TGUIScroll(menu.GetControl('scCorpsesMax')).Value*5);
+
+  case TGUISwitch(menu.GetControl('swGibsCount')).ItemIndex of
+    0: gGibsCount := 0;
+    1: gGibsCount := 8;
+    2: gGibsCount := 16;
+    3: gGibsCount := 32;
+    else gGibsCount := 48;
+  end;
+
+  gBloodCount := TGUISwitch(menu.GetControl('swBloodCount')).ItemIndex;
+  gFlash := TGUISwitch(menu.GetControl('swScreenFlash')).ItemIndex;
+  gAdvBlood := TGUISwitch(menu.GetControl('swBloodType')).ItemIndex = 1;
+  gAdvCorpses := TGUISwitch(menu.GetControl('swCorpseType')).ItemIndex = 1;
+  gAdvGibs := TGUISwitch(menu.GetControl('swGibsType')).ItemIndex = 1;
+  gDrawBackGround := TGUISwitch(menu.GetControl('swBackGround')).ItemIndex = 0;
+  gShowMessages := TGUISwitch(menu.GetControl('swMessages')).ItemIndex = 0;
+  gRevertPlayers := TGUISwitch(menu.GetControl('swRevertPlayers')).ItemIndex = 0;
+  gChatBubble := TGUISwitch(menu.GetControl('swChatBubble')).ItemIndex;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsMenu').GetControl('mOptionsControlsMenu'));
+
+  with menu, gGameControls.GameControls do
+  begin
+    TakeScreenshot := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_SCREENSHOT])).Key;
+    Stat := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_STAT])).Key;
+    Chat := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_CHAT])).Key;
+    TeamChat := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_TEAMCHAT])).Key;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsP1Menu').GetControl('mOptionsControlsP1Menu'));
+
+  with menu, gGameControls.P1Control do
+  begin
+    KeyRight := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_RIGHT])).Key;
+    KeyLeft := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_LEFT])).Key;
+    KeyUp := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_UP])).Key;
+    KeyDown := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_DOWN])).Key;
+    KeyFire := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_FIRE])).Key;
+    KeyJump := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_JUMP])).Key;
+    KeyNextWeapon := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_NEXT_WEAPON])).Key;
+    KeyPrevWeapon := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_PREV_WEAPON])).Key;
+    KeyOpen := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_USE])).Key;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsP2Menu').GetControl('mOptionsControlsP2Menu'));
+
+  with menu, gGameControls.P2Control do
+  begin
+    KeyRight := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_RIGHT])).Key;
+    KeyLeft := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_LEFT])).Key;
+    KeyUp := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_UP])).Key;
+    KeyDown := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_DOWN])).Key;
+    KeyFire := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_FIRE])).Key;
+    KeyJump := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_JUMP])).Key;
+    KeyNextWeapon := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_NEXT_WEAPON])).Key;
+    KeyPrevWeapon := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_PREV_WEAPON])).Key;
+    KeyOpen := TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_USE])).Key;
+  end;
+  
+  if e_JoysticksAvailable > 0 then
+  begin
+    menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsJoystickMenu').GetControl('mOptionsControlsJoystickMenu'));
+    with menu do
+    begin
+      for i := 0 to e_JoysticksAvailable-1 do
+        e_JoystickDeadzones[i] := TGUIScroll(menu.GetControl('scDeadzone' + IntToStr(i))).Value*(32767 div 20);
+    end;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsPlayersP1Menu').GetControl('mOptionsPlayersP1Menu'));
+
+  gPlayer1Settings.Name := b_Text_Unformat(TGUIEdit(menu.GetControl('edP1Name')).Text);
+  gPlayer1Settings.Team := IfThen(TGUISwitch(menu.GetControl('swP1Team')).ItemIndex = 0,
+                                  TEAM_RED, TEAM_BLUE);
+
+  with TGUIModelView(g_GUI_GetWindow('OptionsPlayersP1Menu').GetControl('mvP1Model')) do
+  begin
+    gPlayer1Settings.Model := Model.Name;
+    gPlayer1Settings.Color := Model.Color;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsPlayersP2Menu').GetControl('mOptionsPlayersP2Menu'));
+
+  gPlayer2Settings.Name := b_Text_Unformat(TGUIEdit(menu.GetControl('edP2Name')).Text);
+  gPlayer2Settings.Team := IfThen(TGUISwitch(menu.GetControl('swP2Team')).ItemIndex = 0,
+                                  TEAM_RED, TEAM_BLUE);
+  with TGUIModelView(g_GUI_GetWindow('OptionsPlayersP2Menu').GetControl('mvP2Model')) do
+  begin
+    gPlayer2Settings.Model := Model.Name;
+    gPlayer2Settings.Color := Model.Color;
+  end;
+
+  if gPlayer1Settings.Name = '' then gPlayer1Settings.Name := 'Player1';
+  if gPlayer2Settings.Name = '' then gPlayer2Settings.Name := 'Player2';
+
+  if g_Game_IsServer then
+  begin
+    if gPlayer1 <> nil then
+    begin
+      gPlayer1.SetModel(gPlayer1Settings.Model);
+      gPlayer1.Name := gPlayer1Settings.Name;
+      if not (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+        gPlayer1.SetColor(gPlayer1Settings.Color)
+      else
+        if gPlayer1.Team <> gPlayer1Settings.Team then
+          gPlayer1.SwitchTeam;
+
+      if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+    end;
+
+    if gPlayer2 <> nil then
+    begin
+      gPlayer2.SetModel(gPlayer2Settings.Model);
+      gPlayer2.Name := gPlayer2Settings.Name;
+      if (gGameSettings.GameMode <> GM_TDM) and (gGameSettings.GameMode <> GM_CTF) then
+        gPlayer2.SetColor(gPlayer2Settings.Color)
+      else
+        if gPlayer2.Team <> gPlayer2Settings.Team then
+          gPlayer2.SwitchTeam;
+    end;
+  end;
+
+  if g_Game_IsClient then MC_SEND_PlayerSettings;
+
+  g_Options_Write(GameDir+'/'+CONFIG_FILENAME);
+end;
+
+procedure ReadOptions();
+var
+  menu: TGUIMenu;
+  i: Integer;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsVideoMenu').GetControl('mOptionsVideoMenu'));
+
+  with TGUISwitch(menu.GetControl('swBPP')) do
+    if gBPP = 16 then
+      ItemIndex := 0
+    else
+      ItemIndex := 1;
+
+  with TGUISwitch(menu.GetControl('swTextureFilter')) do
+    if gTextureFilter then ItemIndex := 0 else ItemIndex := 1;
+
+  with TGUISwitch(menu.GetControl('swVSync')) do
+    if gVSync then ItemIndex := 0 else ItemIndex := 1;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsSoundMenu').GetControl('mOptionsSoundMenu'));
+
+  TGUIScroll(menu.GetControl('scSoundLevel')).Value := Round(gSoundLevel/16);
+  TGUIScroll(menu.GetControl('scMusicLevel')).Value := Round(gMusicLevel/16);
+  TGUIScroll(menu.GetControl('scMaxSimSounds')).Value := Round((gMaxSimSounds-2)/4);
+
+  with TGUISwitch(menu.GetControl('swInactiveSounds')) do
+    if gMuteWhenInactive then
+      ItemIndex := 1
+    else
+      ItemIndex := 0;
+
+  TGUISwitch(menu.GetControl('swAnnouncer')).ItemIndex := gAnnouncer;
+
+  with TGUISwitch(menu.GetControl('swSoundEffects')) do
+    if gSoundEffectsDF then
+      ItemIndex := 1
+    else
+      ItemIndex := 0;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsP1Menu').GetControl('mOptionsControlsP1Menu'));
+
+  with menu, gGameControls.P1Control do
+  begin
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_RIGHT])).Key := KeyRight;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_LEFT])).Key := KeyLeft;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_UP])).Key := KeyUp;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_DOWN])).Key := KeyDown;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_FIRE])).Key := KeyFire;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_JUMP])).Key := KeyJump;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_NEXT_WEAPON])).Key := KeyNextWeapon;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_PREV_WEAPON])).Key := KeyPrevWeapon;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_USE])).Key := KeyOpen;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsP2Menu').GetControl('mOptionsControlsP2Menu'));
+
+  with menu, gGameControls.P2Control do
+  begin
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_RIGHT])).Key := KeyRight;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_LEFT])).Key := KeyLeft;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_UP])).Key := KeyUp;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_DOWN])).Key := KeyDown;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_FIRE])).Key := KeyFire;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_JUMP])).Key := KeyJump;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_NEXT_WEAPON])).Key := KeyNextWeapon;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_PREV_WEAPON])).Key := KeyPrevWeapon;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_USE])).Key := KeyOpen;
+  end;
+  
+  if e_JoysticksAvailable > 0 then
+  begin
+    menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsJoystickMenu').GetControl('mOptionsControlsJoystickMenu'));
+    with menu do
+    begin
+      for i := 0 to e_JoysticksAvailable-1 do
+        TGUIScroll(menu.GetControl('scDeadzone' + IntToStr(i))).Value := e_JoystickDeadzones[i] div (32767 div 20);
+    end;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsControlsMenu').GetControl('mOptionsControlsMenu'));
+  with menu, gGameControls.GameControls do
+  begin
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_SCREENSHOT])).Key := TakeScreenshot;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_STAT])).Key := Stat;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_CHAT])).Key := Chat;
+    TGUIKeyRead(GetControl(_lc[I_MENU_CONTROL_TEAMCHAT])).Key := TeamChat;
+  end;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsGameMenu').GetControl('mOptionsGameMenu'));
+
+  TGUIScroll(menu.GetControl('scParticlesCount')).Value := g_GFX_GetMax() div 1000;
+  TGUIScroll(menu.GetControl('scShellsMax')).Value := g_Shells_GetMax() div 30;
+  TGUIScroll(menu.GetControl('scGibsMax')).Value := g_Gibs_GetMax() div 25;
+  TGUIScroll(menu.GetControl('scCorpsesMax')).Value := g_Corpses_GetMax() div 5;
+  TGUISwitch(menu.GetControl('swBloodCount')).ItemIndex := gBloodCount;
+
+  with TGUISwitch(menu.GetControl('swScreenFlash')) do
+    ItemIndex := gFlash;
+
+  with TGUISwitch(menu.GetControl('swBloodType')) do
+    if gAdvBlood then ItemIndex := 1 else ItemIndex := 0;
+
+  with TGUISwitch(menu.GetControl('swCorpseType')) do
+    if gAdvCorpses then ItemIndex := 1 else ItemIndex := 0;
+
+  with TGUISwitch(menu.GetControl('swGibsType')) do
+    if gAdvGibs then ItemIndex := 1 else ItemIndex := 0;
+
+  with TGUISwitch(menu.GetControl('swGibsCount')) do
+    case gGibsCount of
+      0: ItemIndex := 0;
+      8: ItemIndex := 1;
+      16: ItemIndex := 2;
+      32: ItemIndex := 3;
+      else ItemIndex := 4;
+    end;
+
+  with TGUISwitch(menu.GetControl('swBackGround')) do
+    if gDrawBackGround then ItemIndex := 0 else ItemIndex := 1;
+
+  with TGUISwitch(menu.GetControl('swMessages')) do
+    if gShowMessages then ItemIndex := 0 else ItemIndex := 1;
+
+  with TGUISwitch(menu.GetControl('swRevertPlayers')) do
+    if gRevertPlayers then ItemIndex := 0 else ItemIndex := 1;
+
+  with TGUISwitch(menu.GetControl('swChatBubble')) do
+    ItemIndex := gChatBubble;
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsPlayersP1Menu').GetControl('mOptionsPlayersP1Menu'));
+
+  TGUIListBox(menu.GetControl('lsP1Model')).SelectItem(gPlayer1Settings.Model);
+  TGUIEdit(menu.GetControl('edP1Name')).Text := gPlayer1Settings.Name;
+
+  TGUISwitch(menu.GetControl('swP1Team')).ItemIndex :=
+    IfThen(gPlayer1Settings.Team = TEAM_BLUE, 1, 0);
+
+  TGUIScroll(menu.GetControl('scP1Red')).Value := Round(gPlayer1Settings.Color.R/16);
+  TGUIScroll(menu.GetControl('scP1Green')).Value := Round(gPlayer1Settings.Color.G/16);
+  TGUIScroll(menu.GetControl('scP1Blue')).Value := Round(gPlayer1Settings.Color.B/16);
+
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsPlayersP2Menu').GetControl('mOptionsPlayersP2Menu'));
+
+  TGUIListBox(menu.GetControl('lsP2Model')).SelectItem(gPlayer2Settings.Model);
+  TGUIEdit(menu.GetControl('edP2Name')).Text := gPlayer2Settings.Name;
+
+  TGUISwitch(menu.GetControl('swP2Team')).ItemIndex :=
+    IfThen(gPlayer2Settings.Team = TEAM_BLUE, 1, 0);
+
+  TGUIScroll(menu.GetControl('scP2Red')).Value := Round(gPlayer2Settings.Color.R/16);
+  TGUIScroll(menu.GetControl('scP2Green')).Value := Round(gPlayer2Settings.Color.G/16);
+  TGUIScroll(menu.GetControl('scP2Blue')).Value := Round(gPlayer2Settings.Color.B/16);
+
+  ProcSelectModel(nil);
+end;
+
+procedure ProcSwitchMonstersCustom(Sender: TGUIControl);
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mCustomGameMenu')) do
+    if TGUISwitch(GetControl('swGameMode')).GetText = _lc[I_MENU_GAME_TYPE_COOP] then
+      TGUISwitch(GetControl('swMonsters')).ItemIndex := 0
+    else
+      TGUISwitch(GetControl('swMonsters')).ItemIndex := 1;
+end;
+
+procedure ProcSwitchMonstersNet(Sender: TGUIControl);
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mNetServerMenu')) do
+    if TGUISwitch(GetControl('swGameMode')).GetText = _lc[I_MENU_GAME_TYPE_COOP] then
+      TGUISwitch(GetControl('swMonsters')).ItemIndex := 0
+    else
+      TGUISwitch(GetControl('swMonsters')).ItemIndex := 1;
+end;
+
+procedure ProcStartCustomGame();
+var
+  Map: String;
+  GameMode: Byte;
+  Options: LongWord;
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mCustomGameMenu')) do
+  begin
+    Map := TGUILabel(GetControl('lbMap')).Text;
+    if Map = '' then
+      Exit;
+    if Pos(':\', Map) = 0 then
+      Exit;
+
+    GameMode := TGUISwitch(GetControl('swGameMode')).ItemIndex+1;
+    gcGameMode := TGUISwitch(GetControl('swGameMode')).GetText;
+    gcTimeLimit := StrToIntDef(TGUIEdit(GetControl('edTimeLimit')).Text, 0);
+    gcGoalLimit := StrToIntDef(TGUIEdit(GetControl('edGoalLimit')).Text, 0);
+    gcMaxLives := StrToIntDef(TGUIEdit(GetControl('edMaxLives')).Text, 0);
+
+    gcTeamDamage := TGUISwitch(GetControl('swTeamDamage')).ItemIndex = 0;
+    gcAllowExit := TGUISwitch(GetControl('swEnableExits')).ItemIndex = 0;
+    gcWeaponStay := TGUISwitch(GetControl('swWeaponStay')).ItemIndex = 0;
+    gcMonsters := TGUISwitch(GetControl('swMonsters')).ItemIndex = 0;
+    Options := 0;
+    if gcTeamDamage then
+      Options := Options or GAME_OPTION_TEAMDAMAGE;
+    if gcAllowExit then
+      Options := Options or GAME_OPTION_ALLOWEXIT;
+    if gcWeaponStay then
+      Options := Options or GAME_OPTION_WEAPONSTAY;
+    if gcMonsters then
+      Options := Options or GAME_OPTION_MONSTERS;
+    gcPlayers := TGUISwitch(GetControl('swPlayers')).ItemIndex;
+
+    case TGUISwitch(GetControl('swBotsVS')).ItemIndex of
+      1: begin
+        Options := Options or GAME_OPTION_BOTVSMONSTER;
+        gcBotsVS := 'Monsters';
+      end;
+      2: begin
+        Options := Options or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER;
+        gcBotsVS := 'Everybody';
+      end;
+      else begin
+        Options := Options or GAME_OPTION_BOTVSPLAYER;
+        gcBotsVS := 'Players';
+      end;
+    end;
+
+    gcMap := Map;
+  end;
+
+  g_Options_Write_Gameplay_Custom(GameDir+'/'+CONFIG_FILENAME);
+
+  g_Game_StartCustom(Map, GameMode, gcTimeLimit, gcGoalLimit,
+                     gcMaxLives, Options, gcPlayers);
+end;
+
+
+procedure ProcStartNetGame();
+var
+  Map: String;
+  GameMode: Byte;
+  Options: LongWord;
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mNetServerMenu')) do
+  begin
+    Map := TGUILabel(GetControl('lbMap')).Text;
+    if Map = '' then
+      Exit;
+    if Pos(':\', Map) = 0 then
+      Exit;
+
+    GameMode := TGUISwitch(GetControl('swGameMode')).ItemIndex+1;
+    gnGameMode := TGUISwitch(GetControl('swGameMode')).GetText;
+    gnTimeLimit := StrToIntDef(TGUIEdit(GetControl('edTimeLimit')).Text, 0);
+    gnGoalLimit := StrToIntDef(TGUIEdit(GetControl('edGoalLimit')).Text, 0);
+    gnMaxLives := StrToIntDef(TGUIEdit(GetControl('edMaxLives')).Text, 0);
+    NetPort := StrToIntDef(TGUIEdit(GetControl('edPort')).Text, 0);
+
+    gnTeamDamage := TGUISwitch(GetControl('swTeamDamage')).ItemIndex = 0;
+    gnAllowExit := TGUISwitch(GetControl('swEnableExits')).ItemIndex = 0;
+    gnWeaponStay := TGUISwitch(GetControl('swWeaponStay')).ItemIndex = 0;
+    gnMonsters := TGUISwitch(GetControl('swMonsters')).ItemIndex = 0;
+    Options := 0;
+    if gnTeamDamage then
+      Options := Options or GAME_OPTION_TEAMDAMAGE;
+    if gnAllowExit then
+      Options := Options or GAME_OPTION_ALLOWEXIT;
+    if gnWeaponStay then
+      Options := Options or GAME_OPTION_WEAPONSTAY;
+    if gnMonsters then
+      Options := Options or GAME_OPTION_MONSTERS;
+    gnPlayers := TGUISwitch(GetControl('swPlayers')).ItemIndex;
+
+    case TGUISwitch(GetControl('swBotsVS')).ItemIndex of
+      1: begin
+        Options := Options or GAME_OPTION_BOTVSMONSTER;
+        gnBotsVS := 'Monsters';
+      end;
+      2: begin
+        Options := Options or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER;
+        gnBotsVS := 'Everybody';
+      end;
+      else begin
+        Options := Options or GAME_OPTION_BOTVSPLAYER;
+        gnBotsVS := 'Players';
+      end;
+    end;
+
+    gnMap := Map;
+    NetServerName := TGUIEdit(GetControl('edSrvName')).Text;
+    NetMaxClients := Max(1, StrToIntDef(TGUIEdit(GetControl('edMaxPlayers')).Text, 1));
+    NetMaxClients := Min(NET_MAXCLIENTS, NetMaxClients);
+    NetPassword := TGUIEdit(GetControl('edSrvPassword')).Text;
+    NetUseMaster := TGUISwitch(GetControl('swUseMaster')).ItemIndex = 0;
+  end;
+
+  g_Options_Write_Net_Server(GameDir+'/'+CONFIG_FILENAME);
+  g_Options_Write_Gameplay_Net(GameDir+'/'+CONFIG_FILENAME);
+
+  g_Game_StartServer(Map, GameMode, gnTimeLimit, gnGoalLimit, gnMaxLives,
+                     Options, gnPlayers, 0, NetPort);
+end;
+
+procedure ProcConnectNetGame();
+var
+  PW: String;
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mNetClientMenu')) do
+  begin
+    NetClientIP := TGUIEdit(GetControl('edIP')).Text;
+    NetClientPort := StrToIntDef(TGUIEdit(GetControl('edPort')).Text, 0);
+    PW := TGUIEdit(GetControl('edPW')).Text;
+  end;
+
+  g_Options_Write_Net_Client(GameDir+'/'+CONFIG_FILENAME);
+  g_Game_StartClient(NetClientIP, NetClientPort, PW);
+end;
+
+procedure ProcEnterPassword();
+var
+  PW: string;
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mClientPasswordMenu')) do
+  begin
+    NetClientIP := PromptIP;
+    NetClientPort := PromptPort;
+    PW := TGUIEdit(GetControl('edPW')).Text;
+  end;
+
+  g_Options_Write_Net_Client(GameDir+'/'+CONFIG_FILENAME);
+  g_Game_StartClient(NetClientIP, NetClientPort, PW);
+end;
+
+procedure ProcServerlist();
+begin
+  if not NetInitDone then
+  begin
+    if (not g_Net_Init()) then
+    begin
+      g_Console_Add('NET: ERROR: Failed to init ENet!');
+      Exit;
+    end
+    else
+      NetInitDone := True;
+  end;
+
+  g_Net_Slist_Set(NetSlistIP, NetSlistPort);
+
+  gState := STATE_SLIST;
+  g_ActiveWindow := nil;
+
+  slWaitStr := _lc[I_NET_SLIST_WAIT];
+
+  g_Game_Draw;
+  ReDrawWindow;
+
+  slReturnPressed := True;
+  if g_Net_Slist_Fetch(slCurrent) then
+  begin
+    if slCurrent = nil then
+      slWaitStr := _lc[I_NET_SLIST_NOSERVERS];
+  end
+  else
+    slWaitStr := _lc[I_NET_SLIST_ERROR];
+end;
+
+procedure ProcStartCampaign();
+var
+  WAD: String;
+  TwoPlayers: Boolean;
+  n: Byte;
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mCampaignMenu')) do
+  begin
+    WAD := ExtractRelativePath(MapsDir, TGUIFileListBox(GetControl('lsWAD')).SelectedItem());
+    TwoPlayers := TGUISwitch(GetControl('swPlayers')).ItemIndex = 1;
+  end;
+
+  if TwoPlayers then
+    n := 2
+  else
+    n := 1;
+  g_Game_StartSingle(WAD + ':\MAP01', TwoPlayers, n);
+end;
+
+procedure ProcSelectMap(Sender: TGUIControl);
+var
+  win: TGUIWindow;
+  a: TMapInfo;
+  wad, map, res: String;
+begin
+  win := g_GUI_GetWindow('SelectMapMenu');
+  with TGUIMenu(win.GetControl('mSelectMapMenu')) do
+  begin
+    wad := TGUIFileListBox(GetControl('lsMapWAD')).SelectedItem();
+    map := TGUIListBox(GetControl('lsMapRes')).SelectedItem();
+
+    if (wad = '') or (map = '') then
+      begin // Ýòî íå êàðòà
+        TGUILabel(GetControl('lbMapName')).Text := '';
+        TGUILabel(GetControl('lbMapAuthor')).Text := '';
+        TGUILabel(GetControl('lbMapSize')).Text := '';
+        TGUIMemo(GetControl('meMapDescription')).SetText('');
+        TGUIMapPreview(win.GetControl('mpMapPreview')).ClearMap();
+        TGUILabel(win.GetControl('lbMapScale')).Text := '';
+      end
+    else // Ýòî êàðòà
+      begin
+        res := wad+':\'+map;
+
+        a := g_Map_GetMapInfo(res);
+
+        TGUILabel(GetControl('lbMapName')).Text := a.Name;
+        TGUILabel(GetControl('lbMapAuthor')).Text := a.Author;
+        TGUILabel(GetControl('lbMapSize')).Text := Format('%dx%d', [a.Width, a.Height]);
+        TGUIMemo(GetControl('meMapDescription')).SetText(a.Description);
+        TGUIMapPreview(win.GetControl('mpMapPreview')).SetMap(res);
+        TGUILabel(win.GetControl('lbMapScale')).Text :=
+          TGUIMapPreview(win.GetControl('mpMapPreview')).GetScaleStr;
+      end;
+  end;
+end;
+
+procedure ProcSelectWAD(Sender: TGUIControl);
+var
+  wad: String;
+  list: SArray;
+begin
+  with TGUIMenu(g_GUI_GetWindow('SelectMapMenu').GetControl('mSelectMapMenu')) do
+  begin
+    wad := TGUIFileListBox(GetControl('lsMapWAD')).SelectedItem();
+
+    with TGUIListBox(GetControl('lsMapRes')) do
+    begin
+      Clear();
+
+      if wad <> '' then
+      begin
+        list := g_Map_GetMapsList(wad);
+
+        if list <> nil then
+        begin
+          Items := list;
+          ItemIndex := 0;
+        end
+      end;
+    end;
+ end;
+ ProcSelectMap(nil);
+end;
+
+procedure ProcSelectCampaignWAD(Sender: TGUIControl);
+var
+  win: TGUIWindow;
+  a: TMegaWADInfo;
+  wad, fn: String;
+begin
+  win := g_GUI_GetWindow('CampaignMenu');
+  with TGUIMenu(win.GetControl('mCampaignMenu')) do
+  begin
+    wad := TGUIFileListBox(GetControl('lsWAD')).SelectedItem();
+
+    if wad = '' then
+    begin
+      TGUILabel(GetControl('lbWADName')).Text := '';
+      TGUILabel(GetControl('lbWADAuthor')).Text := '';
+      TGUIMemo(GetControl('meWADDescription')).SetText('');
+    end;
+
+    a := g_Game_GetMegaWADInfo(wad);
+
+    TGUILabel(GetControl('lbWADName')).Text := a.Name;
+    TGUILabel(GetControl('lbWADAuthor')).Text := a.Author;
+    TGUIMemo(GetControl('meWADDescription')).SetText(a.Description);
+
+    TGUIImage(win.GetControl('mpWADImage')).ClearImage();
+
+    if a.pic <> '' then
+    begin
+      g_ProcessResourceStr(a.pic, @fn, nil, nil);
+      if fn = '' then
+        TGUIImage(win.GetControl('mpWADImage')).SetImage(wad+a.pic)
+      else
+        TGUIImage(win.GetControl('mpWADImage')).SetImage(a.pic);
+    end;
+  end;
+end;
+
+procedure ProcChangeColor(Sender: TGUIControl);
+var
+  window: TGUIWindow;
+begin
+  window := g_GUI_GetWindow('OptionsPlayersP1Menu');
+  with TGUIMenu(window.GetControl('mOptionsPlayersP1Menu')) do
+    TGUIModelView(window.GetControl('mvP1Model')).SetColor(
+                  Min(TGUIScroll(GetControl('scP1Red')).Value*16, 255),
+                  Min(TGUIScroll(GetControl('scP1Green')).Value*16, 255),
+                  Min(TGUIScroll(GetControl('scP1Blue')).Value*16, 255));
+
+  window := g_GUI_GetWindow('OptionsPlayersP2Menu');
+  with TGUIMenu(window.GetControl('mOptionsPlayersP2Menu')) do
+    TGUIModelView(window.GetControl('mvP2Model')).SetColor(
+                  Min(TGUIScroll(GetControl('scP2Red')).Value*16, 255),
+                  Min(TGUIScroll(GetControl('scP2Green')).Value*16, 255),
+                  Min(TGUIScroll(GetControl('scP2Blue')).Value*16, 255));
+end;
+
+procedure ProcSelectModel(Sender: TGUIControl);
+var
+  a: string;
+  window: TGUIWindow;
+begin
+  window := g_GUI_GetWindow('OptionsPlayersP1Menu');
+  a := TGUIListBox(TGUIMenu(window.GetControl('mOptionsPlayersP1Menu')).GetControl('lsP1Model')).SelectedItem;
+  if a <> '' then TGUIModelView(window.GetControl('mvP1Model')).SetModel(a);
+
+  window := g_GUI_GetWindow('OptionsPlayersP2Menu');
+  a := TGUIListBox(TGUIMenu(window.GetControl('mOptionsPlayersP2Menu')).GetControl('lsP2Model')).SelectedItem;
+  if a <> '' then TGUIModelView(window.GetControl('mvP2Model')).SetModel(a);
+
+  ProcChangeColor(nil);
+end;
+
+procedure LoadStdFont(cfgres, texture: string; var FontID: DWORD);
+var
+  cwdt, chgt: Byte;
+  spc: ShortInt;
+  ID: DWORD;
+  wad: TWADEditor_1;
+  cfgdata: Pointer;
+  cfglen: Integer;
+  config: TConfig;
+begin
+  cfglen := 0;
+
+  wad := TWADEditor_1.Create;
+  if wad.ReadFile(GameWAD) then
+    wad.GetResource('FONTS', cfgres, cfgdata, cfglen);
+  wad.Free();
+
+  if cfglen <> 0 then
+  begin
+    g_Texture_CreateWADEx('FONT_STD', GameWAD+':FONTS\'+texture);
+
+    config := TConfig.CreateMem(cfgdata, cfglen);
+    cwdt := Min(Max(config.ReadInt('FontMap', 'CharWidth', 0), 0), 255);
+    chgt := Min(Max(config.ReadInt('FontMap', 'CharHeight', 0), 0), 255);
+    spc := Min(Max(config.ReadInt('FontMap', 'Kerning', 0), -128), 127);
+
+    if g_Texture_Get('FONT_STD', ID) then
+      e_TextureFontBuild(ID, FontID, cwdt, chgt, spc);
+
+    config.Free();
+  end;
+
+  if cfglen <> 0 then FreeMem(cfgdata);
+end;
+
+procedure LoadFont(txtres, fntres: string; var FontID: DWORD);
+var
+  cwdt, chgt: Byte;
+  spc: ShortInt;
+  CharID: DWORD;
+  wad: TWADEditor_1;
+  cfgdata, fntdata: Pointer;
+  cfglen, fntlen: Integer;
+  config: TConfig;
+  chrwidth: Integer;
+  a: Byte;
+begin
+  cfglen := 0;
+  fntlen := 0;
+
+  wad := TWADEditor_1.Create;
+  if wad.ReadFile(GameWAD) then
+  begin
+    wad.GetResource('FONTS', txtres, cfgdata, cfglen);
+    wad.GetResource('FONTS', fntres, fntdata, fntlen);
+  end;
+  wad.Free();
+
+  if cfglen <> 0 then
+  begin
+    config := TConfig.CreateMem(cfgdata, cfglen);
+    cwdt := Min(Max(config.ReadInt('FontMap', 'CharWidth', 0), 0), 255);
+    chgt := Min(Max(config.ReadInt('FontMap', 'CharHeight', 0), 0), 255);
+
+    spc := Min(Max(config.ReadInt('FontMap', 'Kerning', 0), -128), 127);
+    FontID := e_CharFont_Create(spc);
+
+    for a := 0 to 255 do
+    begin
+      chrwidth := config.ReadInt(IntToStr(a), 'Width', 0);
+      if chrwidth = 0 then Continue;
+
+      if e_CreateTextureMemEx(fntdata, CharID, cwdt*(a mod 16), chgt*(a div 16),
+                              cwdt, chgt) then
+        e_CharFont_AddChar(FontID, CharID, Chr(a), chrwidth);
+    end;
+
+    config.Free();
+  end;
+
+  if cfglen <> 0 then FreeMem(cfgdata);
+  if fntlen <> 0 then FreeMem(fntdata);
+end;
+
+procedure MenuLoadData();
+begin
+  e_WriteLog('Loading menu data...', MSG_NOTIFY);
+
+  g_Texture_CreateWADEx('MAINMENU_MARKER1', GameWAD+':TEXTURES\MARKER1');
+  g_Texture_CreateWADEx('MAINMENU_MARKER2', GameWAD+':TEXTURES\MARKER2');
+  g_Texture_CreateWADEx('SCROLL_LEFT', GameWAD+':TEXTURES\SLEFT');
+  g_Texture_CreateWADEx('SCROLL_RIGHT', GameWAD+':TEXTURES\SRIGHT');
+  g_Texture_CreateWADEx('SCROLL_MIDDLE', GameWAD+':TEXTURES\SMIDDLE');
+  g_Texture_CreateWADEx('SCROLL_MARKER', GameWAD+':TEXTURES\SMARKER');
+  g_Texture_CreateWADEx('EDIT_LEFT', GameWAD+':TEXTURES\ELEFT');
+  g_Texture_CreateWADEx('EDIT_RIGHT', GameWAD+':TEXTURES\ERIGHT');
+  g_Texture_CreateWADEx('EDIT_MIDDLE', GameWAD+':TEXTURES\EMIDDLE');
+  g_Texture_CreateWADEx('BOX1', GameWAD+':TEXTURES\BOX1');
+  g_Texture_CreateWADEx('BOX2', GameWAD+':TEXTURES\BOX2');
+  g_Texture_CreateWADEx('BOX3', GameWAD+':TEXTURES\BOX3');
+  g_Texture_CreateWADEx('BOX4', GameWAD+':TEXTURES\BOX4');
+  g_Texture_CreateWADEx('BOX5', GameWAD+':TEXTURES\BOX5');
+  g_Texture_CreateWADEx('BOX6', GameWAD+':TEXTURES\BOX6');
+  g_Texture_CreateWADEx('BOX7', GameWAD+':TEXTURES\BOX7');
+  g_Texture_CreateWADEx('BOX8', GameWAD+':TEXTURES\BOX8');
+  g_Texture_CreateWADEx('BOX9', GameWAD+':TEXTURES\BOX9');
+  g_Texture_CreateWADEx('BSCROLL_UP_A', GameWAD+':TEXTURES\SCROLLUPA');
+  g_Texture_CreateWADEx('BSCROLL_UP_U', GameWAD+':TEXTURES\SCROLLUPU');
+  g_Texture_CreateWADEx('BSCROLL_DOWN_A', GameWAD+':TEXTURES\SCROLLDOWNA');
+  g_Texture_CreateWADEx('BSCROLL_DOWN_U', GameWAD+':TEXTURES\SCROLLDOWNU');
+  g_Texture_CreateWADEx('BSCROLL_MIDDLE', GameWAD+':TEXTURES\SCROLLMIDDLE');
+  g_Texture_CreateWADEx('NOPIC', GameWAD+':TEXTURES\NOPIC');
+
+  g_Sound_CreateWADEx('MENU_SELECT', GameWAD+':SOUNDS\MENUSELECT');
+  g_Sound_CreateWADEx('MENU_OPEN', GameWAD+':SOUNDS\MENUOPEN');
+  g_Sound_CreateWADEx('MENU_CLOSE', GameWAD+':SOUNDS\MENUCLOSE');
+  g_Sound_CreateWADEx('MENU_CHANGE', GameWAD+':SOUNDS\MENUCHANGE');
+  g_Sound_CreateWADEx('SCROLL_ADD', GameWAD+':SOUNDS\SCROLLADD');
+  g_Sound_CreateWADEx('SCROLL_SUB', GameWAD+':SOUNDS\SCROLLSUB');
+  g_Sound_CreateWADEx('SOUND_PLAYER_FALL', GameWAD+':SOUNDS\FALL');
+end;
+
+procedure MenuFreeData();
+begin
+  e_CharFont_Remove(gMenuFont);
+  e_CharFont_Remove(gMenuSmallFont);
+
+  g_Texture_Delete('MAINMENU_MARKER1');
+  g_Texture_Delete('MAINMENU_MARKER2');
+  g_Texture_Delete('SCROLL_LEFT');
+  g_Texture_Delete('SCROLL_RIGHT');
+  g_Texture_Delete('SCROLL_MIDDLE');
+  g_Texture_Delete('SCROLL_MARKER');
+  g_Texture_Delete('EDIT_LEFT');
+  g_Texture_Delete('EDIT_RIGHT');
+  g_Texture_Delete('EDIT_MIDDLE');
+  g_Texture_Delete('BOX1');
+  g_Texture_Delete('BOX2');
+  g_Texture_Delete('BOX3');
+  g_Texture_Delete('BOX4');
+  g_Texture_Delete('BOX5');
+  g_Texture_Delete('BOX6');
+  g_Texture_Delete('BOX7');
+  g_Texture_Delete('BOX8');
+  g_Texture_Delete('BOX9');
+  g_Texture_Delete('BSCROLL_UP_A');
+  g_Texture_Delete('BSCROLL_UP_U');
+  g_Texture_Delete('BSCROLL_DOWN_A');
+  g_Texture_Delete('BSCROLL_DOWN_U');
+  g_Texture_Delete('BSCROLL_MIDDLE');
+  g_Texture_Delete('NOPIC');
+
+  g_Sound_Delete('MENU_SELECT');
+  g_Sound_Delete('MENU_OPEN');
+  g_Sound_Delete('MENU_CLOSE');
+  g_Sound_Delete('MENU_CHANGE');
+  g_Sound_Delete('SCROLL_ADD');
+  g_Sound_Delete('SCROLL_SUB');
+  g_Sound_Delete('SOUND_PLAYER_FALL');
+end;
+
+procedure ProcAuthorsMenu();
+begin
+  gMusic.SetByName('MUSIC_INTERMUS');
+  gMusic.Play();
+end;
+
+procedure ProcExitMenuKeyDown(Key: Byte);
+var
+  s: ShortString;
+  snd: TPlayableSound;
+  res: Boolean;
+begin
+  if Key = Ord('y') then
+  begin
+    g_Game_StopAllSounds(True);
+    case (Random(18)) of
+      0: s := 'SOUND_MONSTER_PAIN';
+      1: s := 'SOUND_MONSTER_DIE_3';
+      2: s := 'SOUND_MONSTER_SLOP';
+      3: s := 'SOUND_MONSTER_DEMON_DIE';
+      4: s := 'SOUND_MONSTER_IMP_DIE_2';
+      5: s := 'SOUND_MONSTER_MAN_DIE';
+      6: s := 'SOUND_MONSTER_BSP_DIE';
+      7: s := 'SOUND_MONSTER_VILE_DIE';
+      8: s := 'SOUND_MONSTER_SKEL_DIE';
+      9: s := 'SOUND_MONSTER_MANCUB_ALERT';
+      10: s := 'SOUND_MONSTER_PAIN_PAIN';
+      11: s := 'SOUND_MONSTER_BARON_DIE';
+      12: s := 'SOUND_MONSTER_CACO_DIE';
+      13: s := 'SOUND_MONSTER_CYBER_DIE';
+      14: s := 'SOUND_MONSTER_KNIGHT_ALERT';
+      15: s := 'SOUND_MONSTER_SPIDER_ALERT';
+      else s := 'SOUND_PLAYER_FALL';
+    end;
+
+    snd := TPlayableSound.Create();
+    res := snd.SetByName(s);
+    if not res then
+      res := snd.SetByName('SOUND_PLAYER_FALL');
+
+    if res then
+    begin
+      snd.Play(True);
+      while snd.IsPlaying() do
+        ;
+    end;
+
+    g_Game_Quit();
+  end
+    else
+      if Key = Ord('n') then
+        g_GUI_HideWindow();
+end;
+
+procedure ProcLoadMenu();
+var
+  a: Integer;
+begin
+  for a := 1 to 8 do
+    TGUIEdit(TGUIMenu(g_GUI_GetWindow('LoadMenu').GetControl('mmLoadMenu')).GetControl('edSlot'+IntToStr(a))).Text :=
+    g_GetSaveName(a);
+end;
+
+procedure ProcSaveMenu();
+var
+  a: Integer;
+begin
+  for a := 1 to 8 do
+    TGUIEdit(TGUIMenu(g_GUI_GetWindow('SaveMenu').GetControl('mmSaveMenu')).GetControl('edSlot'+IntToStr(a))).Text :=
+    g_GetSaveName(a);
+end;
+
+procedure ProcSaveGame(Sender: TGUIControl);
+var
+  a: Integer;
+begin
+  if g_Game_IsNet then Exit;
+  if g_Game_IsTestMap then Exit;
+  a := StrToInt(Copy(Sender.Name, Length(Sender.Name), 1));
+  g_Game_PauseAllSounds(True);
+  g_SaveGame(a, TGUIEdit(Sender).Text);
+
+  g_ActiveWindow := nil;
+  g_Game_Pause(False);
+end;
+
+procedure ProcLoadGame(Sender: TGUIControl);
+var
+  a: Integer;
+begin
+  if g_Game_IsNet then Exit;
+  a := StrToInt(Copy(Sender.Name, Length(Sender.Name), 1));
+  if g_LoadGame(a) then
+    g_Game_PauseAllSounds(False)
+  else // Íå çàãðóçèëîñü - âîçâðàò â ìåíþ
+    g_GUI_GetWindow('LoadMenu').SetActive(g_GUI_GetWindow('LoadMenu').GetControl('mmLoadMenu'));
+end;
+
+procedure ProcSingle1Player();
+begin
+  g_Game_StartSingle('megawads/DOOM2D.WAD:\MAP01', False, 1);
+end;
+
+procedure ProcSingle2Players();
+begin
+  g_Game_StartSingle('megawads/DOOM2D.WAD:\MAP01', True, 2);
+end;
+
+procedure ProcSelectMapMenu();
+var
+  menu: TGUIMenu;
+  wad_lb: TGUIFileListBox;
+  map_lb: TGUIListBox;
+  map: String;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('SelectMapMenu').GetControl('mSelectMapMenu'));
+  wad_lb := TGUIFileListBox(menu.GetControl('lsMapWAD'));
+  map_lb := TGUIListBox(menu.GetControl('lsMapRes'));
+
+  if wad_lb.SelectedItem() <> '' then
+    map := map_lb.SelectedItem()
+  else
+    map := '';
+
+  wad_lb.UpdateFileList();
+  map_lb.Clear();
+
+  if wad_lb.SelectedItem() <> '' then
+  begin
+    ProcSelectWAD(nil);
+    map_lb.SelectItem(map);
+
+    if map_lb.SelectedItem() <> '' then
+      ProcSelectMap(nil);
+  end;
+
+  g_GUI_ShowWindow('SelectMapMenu');
+end;
+
+procedure ProcSelectCampaignMenu();
+var
+  menu: TGUIMenu;
+  wad_lb: TGUIFileListBox;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('CampaignMenu').GetControl('mCampaignMenu'));
+  wad_lb := TGUIFileListBox(menu.GetControl('lsWAD'));
+
+  wad_lb.UpdateFileList();
+
+  if wad_lb.SelectedItem() <> '' then
+    ProcSelectCampaignWAD(nil);
+end;
+
+procedure ProcSetMap();
+var
+  wad, map, res: String;
+begin
+  with TGUIMenu(g_ActiveWindow.GetControl('mSelectMapMenu')) do
+  begin
+    wad := ExtractRelativePath(MapsDir, TGUIFileListBox(GetControl('lsMapWAD')).SelectedItem());
+    map := TGUIListBox(GetControl('lsMapRes')).SelectedItem();
+  end;
+
+  if (wad = '') or (map = '') then
+    Exit;
+
+  res := wad+':\'+map;
+
+  TGUILabel(TGUIMenu(g_GUI_GetWindow('CustomGameMenu').GetControl('mCustomGameMenu')).GetControl('lbMap')).Text := res;
+  TGUILabel(TGUIMenu(g_GUI_GetWindow('NetServerMenu').GetControl('mNetServerMenu')).GetControl('lbMap')).Text := res;
+end;
+
+procedure ProcChangeSoundSettings(Sender: TGUIControl);
+var
+  menu: TGUIMenu;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsSoundMenu').GetControl('mOptionsSoundMenu'));
+
+  g_Sound_SetupAllVolumes(
+    Min(TGUIScroll(menu.GetControl('scSoundLevel')).Value*16, 255),
+    Min(TGUIScroll(menu.GetControl('scMusicLevel')).Value*16, 255)
+  );
+end;
+
+procedure ProcOptionsPlayersMIMenu();
+var
+  s, a: string;
+  b: TModelInfo;
+begin
+  if g_ActiveWindow.Name = 'OptionsPlayersP1Menu' then s := 'P1' else s := 'P2';
+
+  a := TGUIListBox(TGUIMenu(g_ActiveWindow.GetControl('mOptionsPlayers'+s+'Menu')).GetControl('ls'+s+'Model')).SelectedItem;
+
+  if a = '' then Exit;
+
+  b := g_PlayerModel_GetInfo(a);
+
+  with TGUIMenu(g_GUI_GetWindow('OptionsPlayersMIMenu').GetControl('mOptionsPlayersMIMenu')) do
+  begin
+    TGUILabel(GetControl('lbName')).Text := b.Name;
+    TGUILabel(GetControl('lbAuthor')).Text := b.Author;
+    TGUIMemo(GetControl('meComment')).SetText(b.Description);
+
+    if b.HaveWeapon then
+      TGUILabel(GetControl('lbWeapon')).Text := _lc[I_MENU_YES]
+    else
+      TGUILabel(GetControl('lbWeapon')).Text := _lc[I_MENU_NO];
+  end;
+
+  g_GUI_ShowWindow('OptionsPlayersMIMenu');
+end;
+
+procedure ProcOptionsPlayersAnim();
+var
+  s: String;
+begin
+  if g_ActiveWindow.Name = 'OptionsPlayersP1Menu' then
+    s := 'P1'
+  else
+    s := 'P2';
+
+  with TGUIModelView(g_ActiveWindow.GetControl('mv'+s+'Model')) do
+  begin
+    NextAnim();
+    Model.GetCurrentAnimation.Loop := True;
+    Model.GetCurrentAnimationMask.Loop := True;
+  end;
+end;
+
+procedure ProcOptionsPlayersWeap();
+var
+  s: String;
+begin
+  if g_ActiveWindow.Name = 'OptionsPlayersP1Menu' then
+    s := 'P1'
+  else
+    s := 'P2';
+
+  with TGUIModelView(g_ActiveWindow.GetControl('mv'+s+'Model')) do
+    NextWeapon();
+end;
+
+procedure ProcOptionsPlayersRot();
+var
+  s: string;
+begin
+  if g_ActiveWindow.Name = 'OptionsPlayersP1Menu' then s := 'P1' else s := 'P2';
+  with TGUIModelView(g_ActiveWindow.GetControl('mv'+s+'Model')).Model do
+    if Direction = D_LEFT then Direction := D_RIGHT else Direction := D_LEFT;
+end;
+
+procedure ProcDefaultMenuKeyDown(Key: Byte);
+begin
+  if Key = Ord('y') then
+  begin
+    g_Options_SetDefault();
+    ReadOptions();
+    g_GUI_HideWindow();
+  end else
+    if Key = Ord('n') then g_GUI_HideWindow;
+end;
+
+procedure ProcSavedMenuKeyDown(Key: Byte);
+begin
+  if Key = Ord('y') then
+  begin
+    ReadOptions();
+    g_GUI_HideWindow();
+  end else
+    if Key = Ord('n') then g_GUI_HideWindow;
+end;
+
+procedure ProcAuthorsClose();
+begin
+  gMusic.SetByName('MUSIC_MENU');
+  gMusic.Play();
+  gState := STATE_MENU;
+end;
+
+procedure ProcGMClose();
+begin
+  g_Game_InGameMenu(False);
+end;
+
+procedure ProcGMShow();
+var
+  Enabled: Boolean;
+begin
+  Enabled := True;
+  if (gGameSettings.GameType = GT_SINGLE) and
+     ((gPlayer1 = nil) or (not gPlayer1.Live)) and
+     ((gPlayer2 = nil) or (not gPlayer2.Live)) then
+    Enabled := False; // Îäèí èç èãðîêîâ ïîãèá â ñèíãëå
+  if not gGameOn then
+    Enabled := False; // Çàïðåòèòü ñîõðàíåíèå â èíòåðìèññèè (íå ðåàëèçîâàíî)
+  if g_Game_IsTestMap then
+    Enabled := False; // Åñëè èãðàåì íà òåñòîâîé èëè âðåìåííîé êàðòå
+  TGUIMainMenu(g_ActiveWindow.GetControl(
+    g_ActiveWindow.DefControl )).EnableButton('save', Enabled);
+end;
+
+procedure ProcChangePlayers();
+var
+  TeamGame, Spectator, AddTwo: Boolean;
+  P1Team{, P2Team}: Byte;
+  bP2: TGUITextButton;
+begin
+  TeamGame := gGameSettings.GameMode in [GM_TDM, GM_CTF];
+  Spectator := (gPlayer1 = nil) and (gPlayer2 = nil);
+  AddTwo := gGameSettings.GameType in [GT_CUSTOM, GT_SERVER];
+  P1Team := TEAM_NONE;
+  if gPlayer1 <> nil then P1Team := gPlayer1.Team;
+  // TODO
+  //P2Team := TEAM_NONE;
+  //if gPlayer2 <> nil then P2Team := gPlayer2.Team;
+
+  TGUIMainMenu(g_ActiveWindow.GetControl(
+    g_ActiveWindow.DefControl )).EnableButton('tmJoinRed', TeamGame and (P1Team <> TEAM_RED));
+  TGUIMainMenu(g_ActiveWindow.GetControl(
+    g_ActiveWindow.DefControl )).EnableButton('tmJoinBlue', TeamGame and (P1Team <> TEAM_BLUE));
+  TGUIMainMenu(g_ActiveWindow.GetControl(
+    g_ActiveWindow.DefControl )).EnableButton('tmJoinGame', Spectator and not TeamGame);
+
+  bP2 := TGUIMainMenu(g_ActiveWindow.GetControl(
+    g_ActiveWindow.DefControl )).GetButton('tmPlayer2');
+  bP2.Enabled := AddTwo and not Spectator;
+  if bP2.Enabled then
+    bP2.Color := MAINMENU_ITEMS_COLOR
+  else
+    bP2.Color := MAINMENU_UNACTIVEITEMS_COLOR;
+  if gPlayer2 = nil then
+    bP2.Caption := _lc[I_MENU_ADD_PLAYER_2]
+  else
+    bP2.Caption := _lc[I_MENU_REM_PLAYER_2];
+
+  TGUIMainMenu(g_ActiveWindow.GetControl(
+    g_ActiveWindow.DefControl )).EnableButton('tmSpectate', not Spectator);
+end;
+
+procedure ProcJoinRed();
+begin
+  if not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT]) then
+    Exit;
+  if g_Game_IsServer then
+  begin
+    if gPlayer1 = nil then
+      g_Game_AddPlayer(TEAM_RED)
+    else
+    begin
+      if gPlayer1.Team <> TEAM_RED then
+      begin
+        gPlayer1.SwitchTeam;
+        gPlayer1Settings.Team := gPlayer1.Team;
+      end;
+
+      if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+    end;
+  end
+  else
+  begin
+    gPlayer1Settings.Team := TEAM_RED;
+    MC_SEND_PlayerSettings;
+    if gPlayer1 = nil then
+      g_Game_AddPlayer(TEAM_RED);
+  end;
+  g_ActiveWindow := nil;
+  g_Game_Pause(False);
+end;
+
+procedure ProcJoinBlue();
+begin
+  if not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT]) then
+    Exit;
+  if g_Game_IsServer then
+  begin
+    if gPlayer1 = nil then
+      g_Game_AddPlayer(TEAM_BLUE)
+    else
+    begin
+      if gPlayer1.Team <> TEAM_BLUE then
+      begin
+        gPlayer1.SwitchTeam;
+        gPlayer1Settings.Team := gPlayer1.Team;
+      end;
+
+      if g_Game_IsNet then MH_SEND_PlayerSettings(gPlayer1.UID);
+    end;
+  end
+  else
+  begin
+    gPlayer1Settings.Team := TEAM_BLUE;
+    MC_SEND_PlayerSettings;
+    if gPlayer1 = nil then
+      g_Game_AddPlayer(TEAM_BLUE);
+  end;
+  g_ActiveWindow := nil;
+  g_Game_Pause(False);
+end;
+
+procedure ProcJoinGame();
+begin
+  if not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT]) then
+    Exit;
+  if gPlayer1 = nil then
+    g_Game_AddPlayer();
+  g_ActiveWindow := nil;
+  g_Game_Pause(False);
+end;
+
+procedure ProcSwitchP2();
+begin
+  if not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER]) then
+    Exit;
+  if gPlayer1 = nil then
+    Exit;
+  if gPlayer2 = nil then
+    g_Game_AddPlayer()
+  else
+    g_Game_RemovePlayer();
+  g_ActiveWindow := nil;
+  g_Game_Pause(False);
+end;
+
+procedure ProcSpectate();
+begin
+  if not (gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT]) then
+    Exit;
+  g_Game_Spectate();
+  g_ActiveWindow := nil;
+  g_Game_Pause(False);
+end;
+
+procedure ProcRestartMenuKeyDown(Key: Byte);
+begin
+  if Key = Ord('y') then g_Game_Restart()
+    else if Key = Ord('n') then g_GUI_HideWindow;
+end;
+
+procedure ProcEndMenuKeyDown(Key: Byte);
+begin
+  if Key = Ord('y') then gExit := EXIT_SIMPLE
+    else if Key = Ord('n') then g_GUI_HideWindow;
+end;
+
+procedure ProcSetRussianLanguage();
+begin
+  if gLanguage <> LANGUAGE_RUSSIAN then
+  begin
+    gLanguage := LANGUAGE_RUSSIAN;
+    gLanguageChange := True;
+    gAskLanguage := False;
+
+    g_Options_Write_Language(GameDir+'/'+CONFIG_FILENAME);
+
+  // Ñîõðàíÿåì èçìåíåíèÿ âñåõ íàñòðîåê:
+    ProcApplyOptions();
+  end;
+end;
+
+procedure ProcSetEnglishLanguage();
+begin
+  if gLanguage <> LANGUAGE_ENGLISH then
+  begin
+    gLanguage := LANGUAGE_ENGLISH;
+    gLanguageChange := True;
+    gAskLanguage := False;
+
+    g_Options_Write_Language(GameDir+'/'+CONFIG_FILENAME);
+
+  // Ñîõðàíÿåì èçìåíåíèÿ âñåõ íàñòðîåê:
+    ProcApplyOptions();
+  end;
+end;
+
+procedure ReadGameSettings();
+var
+  menu: TGUIMenu;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('GameSetGameMenu').GetControl('mGameSetGameMenu'));
+
+  with gGameSettings do
+  begin
+    with TGUISwitch(menu.GetControl('swTeamDamage')) do
+      if LongBool(Options and GAME_OPTION_TEAMDAMAGE) then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+
+    TGUIEdit(menu.GetControl('edTimeLimit')).Text := IntToStr(TimeLimit);
+    TGUIEdit(menu.GetControl('edGoalLimit')).Text := IntToStr(GoalLimit);
+    TGUIEdit(menu.GetControl('edMaxLives')).Text := IntToStr(MaxLives);
+
+    with TGUISwitch(menu.GetControl('swBotsVS')) do
+      if LongBool(Options and GAME_OPTION_BOTVSPLAYER) and
+         LongBool(Options and GAME_OPTION_BOTVSMONSTER) then
+        ItemIndex := 2
+      else
+        if LongBool(Options and GAME_OPTION_BOTVSMONSTER) then
+          ItemIndex := 1
+        else
+          ItemIndex := 0;
+
+    if GameType in [GT_CUSTOM, GT_SERVER] then
+      begin
+        TGUISwitch(menu.GetControl('swTeamDamage')).Enabled := True;
+        TGUIEdit(menu.GetControl('edTimeLimit')).Enabled := True;
+        TGUILabel(menu.GetControlsText('edTimeLimit')).Color := MENU_ITEMSTEXT_COLOR;
+        TGUIEdit(menu.GetControl('edGoalLimit')).Enabled := True;
+        TGUILabel(menu.GetControlsText('edGoalLimit')).Color := MENU_ITEMSTEXT_COLOR;
+        TGUIEdit(menu.GetControl('edMaxLives')).Enabled := True;
+        TGUILabel(menu.GetControlsText('edMaxLives')).Color := MENU_ITEMSTEXT_COLOR;
+        TGUISwitch(menu.GetControl('swBotsVS')).Enabled := True;
+      end
+    else
+      begin
+        TGUISwitch(menu.GetControl('swTeamDamage')).Enabled := True;
+        with TGUIEdit(menu.GetControl('edTimeLimit')) do
+        begin
+          Enabled := False;
+          Text := '';
+        end;
+        TGUILabel(menu.GetControlsText('edTimeLimit')).Color := MENU_UNACTIVEITEMS_COLOR;
+        with TGUIEdit(menu.GetControl('edGoalLimit')) do
+        begin
+          Enabled := False;
+          Text := '';
+        end;
+        TGUILabel(menu.GetControlsText('edGoalLimit')).Color := MENU_UNACTIVEITEMS_COLOR;
+        with TGUIEdit(menu.GetControl('edMaxLives')) do
+        begin
+          Enabled := False;
+          Text := '';
+        end;
+        TGUILabel(menu.GetControlsText('edMaxLives')).Color := MENU_UNACTIVEITEMS_COLOR;
+        TGUISwitch(menu.GetControl('swBotsVS')).Enabled := True;
+      end;
+  end;
+end;
+
+procedure ProcApplyGameSet();
+var
+  menu: TGUIMenu;
+  a, b, n: Integer;
+  stat: TPlayerStatArray;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('GameSetGameMenu').GetControl('mGameSetGameMenu'));
+
+  if not g_Game_IsServer then Exit;
+
+  with gGameSettings do
+  begin
+    if TGUISwitch(menu.GetControl('swTeamDamage')).Enabled then
+    begin
+      if TGUISwitch(menu.GetControl('swTeamDamage')).ItemIndex = 0 then
+        Options := Options or GAME_OPTION_TEAMDAMAGE
+      else
+        Options := Options and (not GAME_OPTION_TEAMDAMAGE);
+    end;
+
+    if TGUIEdit(menu.GetControl('edTimeLimit')).Enabled then
+    begin
+      n := StrToIntDef(TGUIEdit(menu.GetControl('edTimeLimit')).Text, TimeLimit);
+
+      if n = 0 then
+        TimeLimit := 0
+      else
+        begin
+          b := (gTime - gGameStartTime) div 1000 + 10; // 10 ñåêóíä íà ñìåíó
+
+          TimeLimit := Max(n, b);
+        end;
+    end;
+
+    if TGUIEdit(menu.GetControl('edGoalLimit')).Enabled then
+    begin
+      n := StrToIntDef(TGUIEdit(menu.GetControl('edGoalLimit')).Text, GoalLimit);
+
+      if n = 0 then
+        GoalLimit := 0
+      else
+        begin
+          b := 0;
+          if GameMode = GM_DM then
+            begin // DM
+              stat := g_Player_GetStats();
+              if stat <> nil then
+                for a := 0 to High(stat) do
+                  if stat[a].Frags > b then
+                    b := stat[a].Frags;
+            end
+          else // CTF
+            b := Max(gTeamStat[TEAM_RED].Goals, gTeamStat[TEAM_BLUE].Goals);
+
+          GoalLimit := Max(n, b);
+        end;
+    end;
+
+    if TGUIEdit(menu.GetControl('edMaxLives')).Enabled then
+    begin
+      n := StrToIntDef(TGUIEdit(menu.GetControl('edMaxLives')).Text, GoalLimit);
+      if n < 0 then n := 0;
+      if n > 255 then n := 255;
+      if n = 0 then
+        MaxLives := 0
+      else
+        begin
+          b := 0;
+          stat := g_Player_GetStats();
+          if stat <> nil then
+            for a := 0 to High(stat) do
+              if stat[a].Lives > b then
+                b := stat[a].Lives;
+
+          MaxLives := Max(n, b);
+        end;
+    end;
+
+    if TGUISwitch(menu.GetControl('swBotsVS')).Enabled then
+    begin
+      case TGUISwitch(menu.GetControl('swBotsVS')).ItemIndex of
+        1:
+          begin
+            Options := Options and (not GAME_OPTION_BOTVSPLAYER);
+            Options := Options or GAME_OPTION_BOTVSMONSTER;
+          end;
+        2:
+          begin
+            Options := Options or GAME_OPTION_BOTVSPLAYER;
+            Options := Options or GAME_OPTION_BOTVSMONSTER;
+          end;
+        else
+          begin
+            Options := Options or GAME_OPTION_BOTVSPLAYER;
+            Options := Options and (not GAME_OPTION_BOTVSMONSTER);
+          end;
+      end;
+    end;
+  end;
+
+  if g_Game_IsNet then MH_SEND_GameSettings;
+end;
+
+procedure ProcVideoOptionsRes();
+var
+  menu: TGUIMenu;
+  list: SArray;
+  SR: DWORD;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsVideoResMenu').GetControl('mOptionsVideoResMenu'));
+
+  TGUILabel(menu.GetControl('lbCurrentRes')).Text :=
+    IntToStr(gScreenWidth) +
+    ' x ' + IntToStr(gScreenHeight) +
+    ', ' + IntToStr(gBPP) + ' bpp';
+
+  with TGUIListBox(menu.GetControl('lsResolution')) do
+  begin
+    list := GetDisplayModes(gBPP, SR);
+
+    if list <> nil then
+      begin
+        Items := list;
+        ItemIndex := SR;
+      end
+    else
+      Clear();
+  end;
+
+  with TGUISwitch(menu.GetControl('swFullScreen')) do
+    if gFullscreen then
+      ItemIndex := 0
+    else
+      ItemIndex := 1;
+end;
+
+procedure ProcApplyVideoOptions();
+var
+  menu: TGUIMenu;
+  Fullscreen: Boolean;
+  SWidth, SHeight: Integer;
+  str: String;
+begin
+  menu := TGUIMenu(g_GUI_GetWindow('OptionsVideoResMenu').GetControl('mOptionsVideoResMenu'));
+
+  str := TGUIListBox(menu.GetControl('lsResolution')).SelectedItem;
+  SScanf(str, '%dx%d', [@SWidth, @SHeight]);
+
+  Fullscreen := TGUISwitch(menu.GetControl('swFullScreen')).ItemIndex = 0;
+
+  if (SWidth <> gScreenWidth) or
+     (SHeight <> gScreenHeight) or
+     (Fullscreen <> gFullscreen) then
+  begin
+    gResolutionChange := True;
+    gRC_Width := SWidth;
+    gRC_Height := SHeight;
+    gRC_FullScreen := Fullscreen;
+    gRC_Maximized := gWinMaximized;
+  end;
+
+// Ñîõðàíÿåì èçìåíåíèÿ âñåõ íàñòðîåê:
+  ProcApplyOptions();
+end;
+
+function CreateYNMenu(Name, Text: String; MaxLen: Word; FontID: DWORD;
+                      KeyDownProc: Pointer): TGUIWindow;
+var
+  a: Integer;
+  h, _x: Word;
+  lines: SArray;
+begin
+  Result := TGUIWindow.Create(Name);
+
+  with Result do
+  begin
+    OnKeyDown := KeyDownProc;
+
+    lines := GetLines(Text, FontID, MaxLen);
+
+    h := e_CharFont_GetMaxHeight(FontID);
+    _x := (gScreenHeight div 2)-(h*Length(lines) div 2);
+
+    if lines <> nil then
+    begin
+      for a := 0 to High(lines) do
+        with TGUILabel(Result.AddChild(TGUILabel.Create(lines[a], FontID))) do
+        begin
+          X := (gScreenWidth div 2)-(GetWidth div 2);
+          Y := _x;
+          Color := _RGB(255, 0, 0);
+          _x := _x+h;
+        end;
+
+      with TGUILabel(Result.AddChild(TGUILabel.Create('(Y\N)', FontID))) do
+      begin
+        X := (gScreenWidth div 2)-(GetWidth div 2);
+        Y := _x;
+        Color := _RGB(255, 0, 0);
+      end;
+    end;
+
+    DefControl := '';
+    SetActive(nil);
+  end;
+end;
+
+procedure ProcSetFirstRussianLanguage();
+begin
+  gLanguage := LANGUAGE_RUSSIAN;
+  gLanguageChange := True;
+  gAskLanguage := False;
+
+  g_Options_Write_Language(GameDir+'/'+CONFIG_FILENAME);
+end;
+
+procedure ProcSetFirstEnglishLanguage();
+begin
+  gLanguage := LANGUAGE_ENGLISH;
+  gLanguageChange := True;
+  gAskLanguage := False;
+
+  g_Options_Write_Language(GameDir+'/'+CONFIG_FILENAME);
+end;
+
+procedure ProcRecallAddress();
+begin
+  with TGUIMenu(g_GUI_GetWindow('NetClientMenu').GetControl('mNetClientMenu')) do
+  begin
+    TGUIEdit(GetControl('edIP')).Text := NetClientIP;
+    TGUIEdit(GetControl('edPort')).Text := IntToStr(NetClientPort);
+  end;
+end;
+
+procedure CreateFirstLanguageMenu();
+var
+  Menu: TGUIWindow;
+begin
+  Menu := TGUIWindow.Create('FirstLanguageMenu');
+
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, ' '))) do
+  begin
+    Name := 'mmFirstLanguageMenu';
+    AddButton(@ProcSetFirstRussianLanguage, 'Ðóññêèé', '');
+    AddButton(@ProcSetFirstEnglishLanguage, 'English', '');
+  end;
+
+  Menu.DefControl := 'mmFirstLanguageMenu';
+  Menu.MainWindow := True;
+  g_GUI_AddWindow(Menu);
+end;
+
+procedure g_Menu_AskLanguage();
+begin
+  CreateFirstLanguageMenu();
+  g_GUI_ShowWindow('FirstLanguageMenu');
+end;
+
+procedure CreatePlayerOptionsMenu(s: String);
+var
+  Menu: TGUIWindow;
+  a: String;
+begin
+  Menu := TGUIWindow.Create('OptionsPlayers'+s+'Menu');
+  if s = 'P1' then
+    a := _lc[I_MENU_PLAYER_1]
+  else
+    a := _lc[I_MENU_PLAYER_2];
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, a))) do
+  begin
+    Name := 'mOptionsPlayers'+s+'Menu';
+    with AddEdit(_lc[I_MENU_PLAYER_NAME]) do
+    begin
+      Name := 'ed'+s+'Name';
+      MaxLength := 12;
+      Width := 12;
+    end;
+    with AddSwitch(_lc[I_MENU_PLAYER_TEAM]) do
+    begin
+      Name := 'sw'+s+'Team';
+      AddItem(_lc[I_MENU_PLAYER_TEAM_RED]);
+      AddItem(_lc[I_MENU_PLAYER_TEAM_BLUE]);
+    end ;
+    with AddList(_lc[I_MENU_PLAYER_MODEL], 12, 6) do
+    begin
+      Name := 'ls'+s+'Model';
+      Sort := True;
+      Items := g_PlayerModel_GetNames();
+      OnChange := ProcSelectModel;
+    end;
+    with AddScroll(_lc[I_MENU_PLAYER_RED]) do
+    begin
+      Name := 'sc'+s+'Red';
+      Max := 16;
+      OnChange := ProcChangeColor;
+    end;
+    with AddScroll(_lc[I_MENU_PLAYER_GREEN]) do
+    begin
+      Name := 'sc'+s+'Green';
+      Max := 16;
+      OnChange := ProcChangeColor;
+    end;
+    with AddScroll(_lc[I_MENU_PLAYER_BLUE]) do
+    begin
+      Name := 'sc'+s+'Blue';
+      Max := 16;
+      OnChange := ProcChangeColor;
+    end;
+    AddSpace();
+    AddButton(@ProcOptionsPlayersMIMenu, _lc[I_MENU_MODEL_INFO]);
+    AddButton(@ProcOptionsPlayersAnim, _lc[I_MENU_MODEL_ANIMATION]);
+    AddButton(@ProcOptionsPlayersWeap, _lc[I_MENU_MODEL_CHANGE_WEAPON]);
+    AddButton(@ProcOptionsPlayersRot, _lc[I_MENU_MODEL_ROTATE]);
+  
+    with TGUIModelView(Menu.AddChild(TGUIModelView.Create)) do
+    begin
+      Name := 'mv'+s+'Model';
+      X := GetControl('ls'+s+'Model').X+TGUIListBox(GetControl('ls'+s+'Model')).GetWidth+16;
+      Y := GetControl('ls'+s+'Model').Y;
+    end;
+  end;
+  Menu.DefControl := 'mOptionsPlayers'+s+'Menu';
+  g_GUI_AddWindow(Menu);
+end;
+
+procedure CreateAllMenus();
+var
+  Menu: TGUIWindow;
+  //SR: TSearchRec;
+  a, cx, _y, i: Integer;
+  //list: SArray;
+begin
+  Menu := TGUIWindow.Create('MainMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_MAIN_MENU]))) do
+  begin
+    Name := 'mmMainMenu';
+    AddButton(nil, _lc[I_MENU_NEW_GAME], 'NewGameMenu');
+    AddButton(nil, _lc[I_MENU_MULTIPLAYER], 'NetGameMenu');
+    AddButton(nil, _lc[I_MENU_LOAD_GAME], 'LoadMenu');
+    AddButton(@ReadOptions, _lc[I_MENU_OPTIONS], 'OptionsMenu');
+    AddButton(@ProcAuthorsMenu, _lc[I_MENU_AUTHORS], 'AuthorsMenu');
+    AddButton(nil, _lc[I_MENU_EXIT], 'ExitMenu');
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(Format(_lc[I_VERSION], [GAME_VERSION]), gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := gScreenWidth-GetWidth-8;
+    Y := gScreenHeight-GetHeight-8;
+  end;
+  Menu.DefControl := 'mmMainMenu';
+  Menu.MainWindow := True;
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('NewGameMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_NEW_GAME]))) do
+  begin
+    Name := 'mmNewGameMenu';
+    AddButton(@ProcSingle1Player, _lc[I_MENU_1_PLAYER]);
+    AddButton(@ProcSingle2Players, _lc[I_MENU_2_PLAYERS]);
+    AddButton(nil, _lc[I_MENU_CUSTOM_GAME], 'CustomGameMenu');
+    AddButton(@ProcSelectCampaignMenu, _lc[I_MENU_CAMPAIGN], 'CampaignMenu');
+  end;
+  Menu.DefControl := 'mmNewGameMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('NetGameMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_MULTIPLAYER]))) do
+  begin
+    Name := 'mmNetGameMenu';
+    AddButton(@ProcRecallAddress, _lc[I_MENU_START_CLIENT], 'NetClientMenu');
+    AddButton(nil, _lc[I_MENU_START_SERVER], 'NetServerMenu');
+  end;
+  Menu.DefControl := 'mmNetGameMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('NetServerMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_START_SERVER]))) do
+  begin
+    Name := 'mNetServerMenu';
+    with AddEdit(_lc[I_NET_SERVER_NAME]) do
+    begin
+      Name := 'edSrvName';
+      OnlyDigits := False;
+      Width := 16;
+      MaxLength := 64;
+      Text := NetServerName;
+    end;
+    with AddEdit(_lc[I_NET_SERVER_PASSWORD]) do
+    begin
+      Name := 'edSrvPassword';
+      OnlyDigits := False;
+      Width := 16;
+      MaxLength := 24;
+      Text := NetPassword;
+    end;
+    with AddEdit(_lc[I_NET_PORT]) do
+    begin
+      Name := 'edPort';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      Text := IntToStr(NetPort);
+    end;
+    with AddEdit(_lc[I_NET_MAX_CLIENTS]) do
+    begin
+      Name := 'edMaxPlayers';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 2;
+      Text := IntToStr(NetMaxClients);
+    end;
+    with AddSwitch(_lc[I_NET_USE_MASTER]) do
+    begin
+      Name := 'swUseMaster';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if NetUseMaster then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    AddSpace();
+    with AddLabel(_lc[I_MENU_MAP]) do
+    begin
+      Name := 'lbMap';
+      FixedLength := 16;
+      Text := gnMap;
+      OnClick := @ProcSelectMapMenu;
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_TYPE]) do
+    begin
+      Name := 'swGameMode';
+      AddItem(_lc[I_MENU_GAME_TYPE_DM]);
+      AddItem(_lc[I_MENU_GAME_TYPE_TDM]);
+      AddItem(_lc[I_MENU_GAME_TYPE_CTF]);
+      AddItem(_lc[I_MENU_GAME_TYPE_COOP]);
+      case g_Game_TextToMode(gnGameMode) of
+        GM_NONE,
+        GM_DM:   ItemIndex := 0;
+        GM_TDM:  ItemIndex := 1;
+        GM_CTF:  ItemIndex := 2;
+        GM_SINGLE,
+        GM_COOP: ItemIndex := 3;
+      end;
+      OnChange := ProcSwitchMonstersNet;
+    end;
+    with AddEdit(_lc[I_MENU_TIME_LIMIT]) do
+    begin
+      Name := 'edTimeLimit';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      if gnTimeLimit > 0 then
+        Text := IntToStr(gnTimeLimit);
+    end;
+    with AddEdit(_lc[I_MENU_GOAL_LIMIT]) do
+    begin
+      Name := 'edGoalLimit';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      if gnGoalLimit > 0 then
+        Text := IntToStr(gnGoalLimit);
+    end;
+    with AddEdit(_lc[I_MENU_MAX_LIVES]) do
+    begin
+      Name := 'edMaxLives';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 3;
+      if gnMaxLives > 0 then
+        Text := IntToStr(gnMaxLives);
+    end;
+    with AddSwitch(_lc[I_MENU_SERVER_PLAYERS]) do
+    begin
+      Name := 'swPlayers';
+      AddItem(_lc[I_MENU_COUNT_NONE]);
+      AddItem(_lc[I_MENU_PLAYERS_ONE]);
+      AddItem(_lc[I_MENU_PLAYERS_TWO]);
+      ItemIndex := gnPlayers;
+    end;
+    with AddSwitch(_lc[I_MENU_TEAM_DAMAGE]) do
+    begin
+      Name := 'swTeamDamage';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gnTeamDamage then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_ENABLE_EXITS]) do
+    begin
+      Name := 'swEnableExits';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gnAllowExit then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_WEAPONS_STAY]) do
+    begin
+      Name := 'swWeaponStay';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gnWeaponStay then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_ENABLE_MONSTERS]) do
+    begin
+      Name := 'swMonsters';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gnMonsters then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_BOTS_VS]) do
+    begin
+      Name := 'swBotsVS';
+      AddItem(_lc[I_MENU_BOTS_VS_PLAYERS]);
+      AddItem(_lc[I_MENU_BOTS_VS_MONSTERS]);
+      AddItem(_lc[I_MENU_BOTS_VS_ALL]);
+      ItemIndex := 2;
+      if gnBotsVS = 'Players' then
+        ItemIndex := 0;
+      if gnBotsVS = 'Monsters' then
+        ItemIndex := 1;
+    end;
+    AddSpace();
+    AddButton(@ProcStartNetGame, _lc[I_MENU_START_GAME]);
+
+    ReAlign();
+  end;
+  Menu.DefControl := 'mNetServerMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('NetClientMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_START_CLIENT]))) do
+  begin
+    Name := 'mNetClientMenu';
+
+    AddButton(@ProcServerlist, _lc[I_NET_SLIST]);
+    AddSpace();
+
+    with AddEdit(_lc[I_NET_ADDRESS]) do
+    begin
+      Name := 'edIP';
+      OnlyDigits :=False;
+      Width := 12;
+      MaxLength := 64;
+      Text := 'localhost';
+    end;
+    with AddEdit(_lc[I_NET_PORT]) do
+    begin
+      Name := 'edPort';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      Text := '25666';
+    end;
+    with AddEdit(_lc[I_NET_SERVER_PASSWORD]) do
+    begin
+      Name := 'edPW';
+      OnlyDigits := False;
+      Width := 12;
+      MaxLength := 32;
+      Text := '';
+    end;
+
+    AddSpace();
+    AddButton(@ProcConnectNetGame, _lc[I_MENU_CLIENT_CONNECT]);
+
+    ReAlign();
+  end;
+  Menu.DefControl := 'mNetClientMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('LoadMenu');
+  Menu.OnShow := ProcLoadMenu;
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_LOAD_GAME]))) do
+  begin
+    Name := 'mmLoadMenu';
+
+    for a := 1 to 8 do
+      with AddEdit('') do
+      begin
+        Name := 'edSlot'+IntToStr(a);
+        Width := 16;
+        MaxLength := 16;
+        OnEnter := ProcLoadGame;
+      end;
+  end;
+  Menu.DefControl := 'mmLoadMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('SaveMenu');
+  Menu.OnShow := ProcSaveMenu;
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_SAVE_GAME]))) do
+  begin
+    Name := 'mmSaveMenu';
+
+    for a := 1 to 8 do
+      with AddEdit('') do
+      begin
+        Name := 'edSlot'+IntToStr(a);
+        Width := 16;
+        MaxLength := 16;
+        OnChange := ProcSaveGame;
+      end;
+  end;
+  Menu.DefControl := 'mmSaveMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('CustomGameMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_CUSTOM_GAME]))) do
+  begin
+    Name := 'mCustomGameMenu';
+    with AddLabel(_lc[I_MENU_MAP]) do
+    begin
+      Name := 'lbMap';
+      FixedLength := 16;
+      Text := gcMap;
+      OnClick := @ProcSelectMapMenu;
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_TYPE]) do
+    begin
+      Name := 'swGameMode';
+      AddItem(_lc[I_MENU_GAME_TYPE_DM]);
+      AddItem(_lc[I_MENU_GAME_TYPE_TDM]);
+      AddItem(_lc[I_MENU_GAME_TYPE_CTF]);
+      AddItem(_lc[I_MENU_GAME_TYPE_COOP]);
+      case g_Game_TextToMode(gcGameMode) of
+        GM_NONE,
+        GM_DM:   ItemIndex := 0;
+        GM_TDM:  ItemIndex := 1;
+        GM_CTF:  ItemIndex := 2;
+        GM_SINGLE,
+        GM_COOP: ItemIndex := 3;
+      end;
+      OnChange := ProcSwitchMonstersCustom;
+    end;
+    with AddEdit(_lc[I_MENU_TIME_LIMIT]) do
+    begin
+      Name := 'edTimeLimit';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      if gcTimeLimit > 0 then
+        Text := IntToStr(gcTimeLimit);
+    end;
+    with AddEdit(_lc[I_MENU_GOAL_LIMIT]) do
+    begin
+      Name := 'edGoalLimit';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      if gcGoalLimit > 0 then
+        Text := IntToStr(gcGoalLimit);
+    end;
+    with AddEdit(_lc[I_MENU_MAX_LIVES]) do
+    begin
+      Name := 'edMaxLives';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+      if gcMaxLives > 0 then
+        Text := IntToStr(gcMaxLives);
+    end;
+    with AddSwitch(_lc[I_MENU_PLAYERS]) do
+    begin
+      Name := 'swPlayers';
+      AddItem(_lc[I_MENU_COUNT_NONE]);
+      AddItem(_lc[I_MENU_PLAYERS_ONE]);
+      AddItem(_lc[I_MENU_PLAYERS_TWO]);
+      ItemIndex := gcPlayers;
+    end;
+    with AddSwitch(_lc[I_MENU_TEAM_DAMAGE]) do
+    begin
+      Name := 'swTeamDamage';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gcTeamDamage then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_ENABLE_EXITS]) do
+    begin
+      Name := 'swEnableExits';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gcAllowExit then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_WEAPONS_STAY]) do
+    begin
+      Name := 'swWeaponStay';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gcWeaponStay then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_ENABLE_MONSTERS]) do
+    begin
+      Name := 'swMonsters';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      if gcMonsters then
+        ItemIndex := 0
+      else
+        ItemIndex := 1;
+    end;
+    with AddSwitch(_lc[I_MENU_BOTS_VS]) do
+    begin
+      Name := 'swBotsVS';
+      AddItem(_lc[I_MENU_BOTS_VS_PLAYERS]);
+      AddItem(_lc[I_MENU_BOTS_VS_MONSTERS]);
+      AddItem(_lc[I_MENU_BOTS_VS_ALL]);
+      ItemIndex := 2;
+      if gcBotsVS = 'Players' then
+        ItemIndex := 0;
+      if gcBotsVS = 'Monsters' then
+        ItemIndex := 1;
+    end;
+    AddSpace();
+    AddButton(@ProcStartCustomGame, _lc[I_MENU_START_GAME]);
+
+    ReAlign();
+  end;
+  Menu.DefControl := 'mCustomGameMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('CampaignMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_CAMPAIGN]))) do
+  begin
+    Name := 'mCampaignMenu';
+
+    AddSpace();
+    AddSpace();
+    AddSpace();
+    AddSpace();
+    AddSpace();
+    AddSpace();
+
+    with AddFileList('', 15, 4) do
+    begin
+      Name := 'lsWAD';
+      OnChange := ProcSelectCampaignWAD;
+
+      Sort := True;
+      Dirs := True;
+      FileMask := '*.wad';
+      SetBase(MapsDir+'megawads/');
+    end;
+
+    with AddLabel(_lc[I_MENU_MAP_NAME]) do
+    begin
+      Name := 'lbWADName';
+      FixedLength := 8;
+      Enabled := False;
+    end;
+    with AddLabel(_lc[I_MENU_MAP_AUTHOR]) do
+    begin
+      Name := 'lbWADAuthor';
+      FixedLength := 8;
+      Enabled := False;
+    end;
+    AddLine(_lc[I_MENU_MAP_DESCRIPTION]);
+    with AddMemo('', 15, 3) do
+    begin
+      Name := 'meWADDescription';
+      Color := MENU_ITEMSCTRL_COLOR;
+    end;
+    with AddSwitch(_lc[I_MENU_PLAYERS]) do
+    begin
+      Name := 'swPlayers';
+      AddItem(_lc[I_MENU_PLAYERS_ONE]);
+      AddItem(_lc[I_MENU_PLAYERS_TWO]);
+    end;
+    AddSpace();
+    AddButton(@ProcStartCampaign, _lc[I_MENU_START_GAME]);
+
+    ReAlign();
+
+    with TGUIImage(Menu.AddChild(TGUIImage.Create)) do
+    begin
+      Name := 'mpWADImage';
+      DefaultRes := 'NOPIC';
+      X := GetControl('lsWAD').X+4;
+      Y := GetControl('lsWAD').Y-128-MENU_VSPACE;
+    end;
+  end;
+  Menu.DefControl := 'mCampaignMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('SelectMapMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_SELECT_MAP]))) do
+  begin
+    Name := 'mSelectMapMenu';
+    with AddFileList(_lc[I_MENU_MAP_WAD], 12, 4) do
+    begin
+      Name := 'lsMapWAD';
+      OnChange := ProcSelectWAD;
+
+      Sort := True;
+      Dirs := True;
+      FileMask := '*.wad';
+      SetBase(MapsDir);
+    end;
+    with AddList(_lc[I_MENU_MAP_RESOURCE], 12, 4) do
+    begin
+      Name := 'lsMapRes';
+      Sort := True;
+      OnChange := ProcSelectMap;
+    end;
+    AddSpace();
+    with AddLabel(_lc[I_MENU_MAP_NAME]) do
+    begin
+      Name := 'lbMapName';
+      FixedLength := 24;
+      Enabled := False;
+    end;
+    with AddLabel(_lc[I_MENU_MAP_AUTHOR]) do
+    begin
+      Name := 'lbMapAuthor';
+      FixedLength := 16;
+      Enabled := False;
+    end;
+    with AddLabel(_lc[I_MENU_MAP_SIZE]) do
+    begin
+      Name := 'lbMapSize';
+      FixedLength := 10;
+      Enabled := False;
+    end;
+    with AddMemo(_lc[I_MENU_MAP_DESCRIPTION], 12, 4) do
+    begin
+      Name := 'meMapDescription';
+    end;
+
+    ReAlign();
+
+    with TGUIMapPreview(Menu.AddChild(TGUIMapPreview.Create)) do
+    begin
+      Name := 'mpMapPreview';
+      X := GetControl('lsMapWAD').X+TGUIListBox(GetControl('lsMapWAD')).GetWidth()+2;
+      Y := GetControl('lsMapWAD').Y;
+    end;
+    with TGUILabel(Menu.AddChild(TGUILabel.Create('', gMenuSmallFont))) do
+    begin
+      Name := 'lbMapScale';
+      FixedLength := 8;
+      Enabled := False;
+      Color := MENU_ITEMSCTRL_COLOR;
+      X := GetControl('lsMapWAD').X +
+        TGUIListBox(GetControl('lsMapWAD')).GetWidth() +
+        2 + MAPPREVIEW_WIDTH*4;
+      Y := GetControl('lsMapWAD').Y + MAPPREVIEW_HEIGHT*16 + 16;
+    end;
+  end;
+  Menu.OnClose := ProcSetMap;
+  Menu.DefControl := 'mSelectMapMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_OPTIONS]))) do
+  begin
+    Name := 'mmOptionsMenu';
+    AddButton(nil, _lc[I_MENU_VIDEO_OPTIONS], 'OptionsVideoMenu');
+    AddButton(nil, _lc[I_MENU_SOUND_OPTIONS], 'OptionsSoundMenu');
+    AddButton(nil, _lc[I_MENU_GAME_OPTIONS], 'OptionsGameMenu');
+    AddButton(nil, _lc[I_MENU_CONTROLS_OPTIONS], 'OptionsControlsMenu');
+    AddButton(nil, _lc[I_MENU_PLAYER_OPTIONS], 'OptionsPlayersMenu');
+    AddButton(nil, _lc[I_MENU_LANGUAGE_OPTIONS], 'OptionsLanguageMenu');
+    AddSpace();
+    AddButton(nil, _lc[I_MENU_SAVED_OPTIONS], 'SavedOptionsMenu').Color := _RGB(255, 0, 0);
+    AddButton(nil, _lc[I_MENU_DEFAULT_OPTIONS], 'DefaultOptionsMenu').Color := _RGB(255, 0, 0);
+  end;
+  Menu.OnClose := ProcApplyOptions;
+  Menu.DefControl := 'mmOptionsMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := CreateYNMenu('SavedOptionsMenu', _lc[I_MENU_LOAD_SAVED_PROMT], Round(gScreenWidth*0.6),
+                       gMenuSmallFont, @ProcSavedMenuKeyDown);
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsVideoMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_VIDEO_OPTIONS]))) do
+  begin
+    Name := 'mOptionsVideoMenu';
+    AddButton(@ProcVideoOptionsRes, _lc[I_MENU_VIDEO_RESOLUTION], 'OptionsVideoResMenu');
+    with AddSwitch(_lc[I_MENU_VIDEO_BPP]) do
+    begin
+      Name := 'swBPP';
+      AddItem('16');
+      AddItem('32');
+    end;
+    with AddSwitch(_lc[I_MENU_VIDEO_VSYNC]) do
+    begin
+      Name := 'swVSync';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+    end;
+    with AddSwitch(_lc[I_MENU_VIDEO_FILTER_SKY]) do
+    begin
+      Name := 'swTextureFilter';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+    end;
+    AddSpace();
+    AddText(_lc[I_MENU_VIDEO_NEED_RESTART], Round(gScreenWidth*0.6));
+    ReAlign();
+  end;
+  Menu.DefControl := 'mOptionsVideoMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsVideoResMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_RESOLUTION_SELECT]))) do
+  begin
+    Name := 'mOptionsVideoResMenu';
+    with AddLabel(_lc[I_MENU_RESOLUTION_CURRENT]) do
+    begin
+      Name := 'lbCurrentRes';
+      FixedLength := 24;
+      Enabled := False;
+    end;
+    with AddList(_lc[I_MENU_RESOLUTION_LIST], 12, 6) do
+    begin
+      Name := 'lsResolution';
+      Sort := False;
+    end;
+    with AddSwitch(_lc[I_MENU_RESOLUTION_FULLSCREEN]) do
+    begin
+      Name := 'swFullScreen';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+    end;
+    AddSpace();
+    AddButton(@ProcApplyVideoOptions, _lc[I_MENU_RESOLUTION_APPLY]);
+    UpdateIndex();
+  end;
+  Menu.DefControl := 'mOptionsVideoResMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsSoundMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_SOUND_OPTIONS]))) do
+  begin
+    Name := 'mOptionsSoundMenu';
+    with AddScroll(_lc[I_MENU_SOUND_MUSIC_LEVEL]) do
+    begin
+      Name := 'scMusicLevel';
+      Max := 16;
+      OnChange := ProcChangeSoundSettings;
+    end;
+    with AddScroll(_lc[I_MENU_SOUND_SOUND_LEVEL]) do
+    begin
+      Name := 'scSoundLevel';
+      Max := 16;
+      OnChange := ProcChangeSoundSettings;
+    end;
+    with AddScroll(_lc[I_MENU_SOUND_MAX_SIM_SOUNDS]) do
+    begin
+      Name := 'scMaxSimSounds';
+      Max := 16;
+    end;
+    with AddSwitch (_lc[I_MENU_SOUND_ANNOUNCE]) do
+    begin;
+      Name := 'swAnnouncer';
+      AddItem(_lc[I_MENU_ANNOUNCE_NONE]);
+      AddItem(_lc[I_MENU_ANNOUNCE_ME]);
+      AddItem(_lc[I_MENU_ANNOUNCE_MEPLUS]);
+      AddItem(_lc[I_MENU_ANNOUNCE_ALL]);
+    end;
+    // Ïåðåêëþ÷àòåëü çâóêîâûõ ýôôåêòîâ (DF / Doom 2)
+    with AddSwitch (_lc[I_MENU_SOUND_COMPAT]) do
+    begin;
+      Name := 'swSoundEffects';
+      AddItem(_lc[I_MENU_COMPAT_DOOM2]);
+      AddItem(_lc[I_MENU_COMPAT_DF]);
+    end;
+    with AddSwitch(_lc[I_MENU_SOUND_INACTIVE_SOUNDS]) do
+    begin
+      Name := 'swInactiveSounds';
+      AddItem(_lc[I_MENU_SOUND_INACTIVE_SOUNDS_ON]);
+      AddItem(_lc[I_MENU_SOUND_INACTIVE_SOUNDS_OFF]);
+    end;
+    ReAlign();
+  end;
+  Menu.DefControl := 'mOptionsSoundMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsGameMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_GAME_OPTIONS]))) do
+  begin
+    Name := 'mOptionsGameMenu';
+    with AddScroll(_lc[I_MENU_GAME_PARTICLES_COUNT]) do
+    begin
+      Name := 'scParticlesCount';
+      Max := 20;
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_BLOOD_COUNT]) do
+    begin
+      Name := 'swBloodCount';
+      AddItem(_lc[I_MENU_COUNT_NONE]);
+      AddItem(_lc[I_MENU_COUNT_SMALL]);
+      AddItem(_lc[I_MENU_COUNT_NORMAL]);
+      AddItem(_lc[I_MENU_COUNT_BIG]);
+      AddItem(_lc[I_MENU_COUNT_VERYBIG]);
+    end;
+    with AddScroll(_lc[I_MENU_GAME_MAX_SHELLS]) do
+    begin
+      Name := 'scShellsMax';
+      Max := 20;
+    end;
+    with AddScroll(_lc[I_MENU_GAME_GIBS_COUNT]) do
+    begin
+      Name := 'scGibsMax';
+      Max := 20;
+    end;
+    with AddScroll(_lc[I_MENU_GAME_MAX_CORPSES]) do
+    begin
+      Name := 'scCorpsesMax';
+      Max := 20;
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_MAX_GIBS]) do
+    begin
+      Name := 'swGibsCount';
+      AddItem(_lc[I_MENU_COUNT_NONE]);
+      AddItem(_lc[I_MENU_COUNT_SMALL]);
+      AddItem(_lc[I_MENU_COUNT_NORMAL]);
+      AddItem(_lc[I_MENU_COUNT_BIG]);
+      AddItem(_lc[I_MENU_COUNT_VERYBIG]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_CORPSE_TYPE]) do
+    begin
+      Name := 'swCorpseType';
+      AddItem(_lc[I_MENU_GAME_CORPSE_TYPE_SIMPLE]);
+      AddItem(_lc[I_MENU_GAME_CORPSE_TYPE_ADV]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_GIBS_TYPE]) do
+    begin
+      Name := 'swGibsType';
+      AddItem(_lc[I_MENU_GAME_GIBS_TYPE_SIMPLE]);
+      AddItem(_lc[I_MENU_GAME_GIBS_TYPE_ADV]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_BLOOD_TYPE]) do
+    begin
+      Name := 'swBloodType';
+      AddItem(_lc[I_MENU_GAME_BLOOD_TYPE_SIMPLE]);
+      AddItem(_lc[I_MENU_GAME_BLOOD_TYPE_ADV]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_SCREEN_FLASH]) do
+    begin
+      Name := 'swScreenFlash';
+      AddItem(_lc[I_MENU_NO]);
+      AddItem(_lc[I_MENU_COMPAT_DF]);
+      AddItem(_lc[I_MENU_COMPAT_DOOM2]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_BACKGROUND]) do
+    begin
+      Name := 'swBackground';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_MESSAGES]) do
+    begin
+      Name := 'swMessages';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_REVERT_PLAYERS]) do
+    begin
+      Name := 'swRevertPlayers';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+    end;
+    with AddSwitch(_lc[I_MENU_GAME_CHAT_BUBBLE]) do
+    begin
+      Name := 'swChatBubble';
+      AddItem(_lc[I_MENU_GAME_CHAT_TYPE_NONE]);
+      AddItem(_lc[I_MENU_GAME_CHAT_TYPE_SIMPLE]);
+      AddItem(_lc[I_MENU_GAME_CHAT_TYPE_ADV]);
+      AddItem(_lc[I_MENU_GAME_CHAT_TYPE_COLOR]);
+      AddItem(_lc[I_MENU_GAME_CHAT_TYPE_TEXTURE]);
+    end;
+    ReAlign();
+  end;
+  Menu.DefControl := 'mOptionsGameMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsControlsMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_CONTROLS_OPTIONS]))) do
+  begin
+    Name := 'mOptionsControlsMenu';
+    AddLine(_lc[I_MENU_CONTROL_GLOBAL]);
+    AddKeyRead(_lc[I_MENU_CONTROL_SCREENSHOT]).Name := _lc[I_MENU_CONTROL_SCREENSHOT];
+    AddKeyRead(_lc[I_MENU_CONTROL_STAT]).Name := _lc[I_MENU_CONTROL_STAT];
+    AddKeyRead(_lc[I_MENU_CONTROL_CHAT]).Name := _lc[I_MENU_CONTROL_CHAT];
+    AddKeyRead(_lc[I_MENU_CONTROL_TEAMCHAT]).Name := _lc[I_MENU_CONTROL_TEAMCHAT];
+    AddSpace();
+    AddButton(nil, _lc[I_MENU_PLAYER_1], 'OptionsControlsP1Menu');
+    AddButton(nil, _lc[I_MENU_PLAYER_2], 'OptionsControlsP2Menu');
+    AddSpace();
+    if e_JoysticksAvailable <> 0 then
+      AddButton(nil, _lc[I_MENU_CONTROL_JOYSTICKS], 'OptionsControlsJoystickMenu');
+  end;
+  Menu.DefControl := 'mOptionsControlsMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsControlsP1Menu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_PLAYER_1]))) do
+  begin
+    Name := 'mOptionsControlsP1Menu';
+    AddKeyRead(_lc[I_MENU_CONTROL_LEFT]).Name := _lc[I_MENU_CONTROL_LEFT];
+    AddKeyRead(_lc[I_MENU_CONTROL_RIGHT]).Name := _lc[I_MENU_CONTROL_RIGHT];
+    AddKeyRead(_lc[I_MENU_CONTROL_UP]).Name := _lc[I_MENU_CONTROL_UP];
+    AddKeyRead(_lc[I_MENU_CONTROL_DOWN]).Name := _lc[I_MENU_CONTROL_DOWN];
+    AddKeyRead(_lc[I_MENU_CONTROL_JUMP]).Name := _lc[I_MENU_CONTROL_JUMP];
+    AddKeyRead(_lc[I_MENU_CONTROL_FIRE]).Name := _lc[I_MENU_CONTROL_FIRE];
+    AddKeyRead(_lc[I_MENU_CONTROL_USE]).Name := _lc[I_MENU_CONTROL_USE];
+    AddKeyRead(_lc[I_MENU_CONTROL_NEXT_WEAPON]).Name := _lc[I_MENU_CONTROL_NEXT_WEAPON];
+    AddKeyRead(_lc[I_MENU_CONTROL_PREV_WEAPON]).Name := _lc[I_MENU_CONTROL_PREV_WEAPON];
+  end;
+  Menu.DefControl := 'mOptionsControlsP1Menu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsControlsP2Menu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_PLAYER_2]))) do
+  begin
+    Name := 'mOptionsControlsP2Menu';
+    AddKeyRead(_lc[I_MENU_CONTROL_LEFT]).Name := _lc[I_MENU_CONTROL_LEFT];
+    AddKeyRead(_lc[I_MENU_CONTROL_RIGHT]).Name := _lc[I_MENU_CONTROL_RIGHT];
+    AddKeyRead(_lc[I_MENU_CONTROL_UP]).Name := _lc[I_MENU_CONTROL_UP];
+    AddKeyRead(_lc[I_MENU_CONTROL_DOWN]).Name := _lc[I_MENU_CONTROL_DOWN];
+    AddKeyRead(_lc[I_MENU_CONTROL_JUMP]).Name := _lc[I_MENU_CONTROL_JUMP];
+    AddKeyRead(_lc[I_MENU_CONTROL_FIRE]).Name := _lc[I_MENU_CONTROL_FIRE];
+    AddKeyRead(_lc[I_MENU_CONTROL_USE]).Name := _lc[I_MENU_CONTROL_USE];
+    AddKeyRead(_lc[I_MENU_CONTROL_NEXT_WEAPON]).Name := _lc[I_MENU_CONTROL_NEXT_WEAPON];
+    AddKeyRead(_lc[I_MENU_CONTROL_PREV_WEAPON]).Name := _lc[I_MENU_CONTROL_PREV_WEAPON];
+  end;
+  Menu.DefControl := 'mOptionsControlsP2Menu';
+  g_GUI_AddWindow(Menu);
+  
+  Menu := TGUIWindow.Create('OptionsControlsJoystickMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_CONTROL_JOYSTICKS]))) do
+  begin
+    Name := 'mOptionsControlsJoystickMenu';
+    for i := 0 to e_JoysticksAvailable-1 do
+      with AddScroll(Format(_lc[I_MENU_CONTROL_DEADZONE], [i + 1])) do
+      begin
+        Name := 'scDeadzone' + IntToStr(i);
+        Max := 20;
+      end;
+  end;
+  Menu.DefControl := 'mOptionsControlsJoystickMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsPlayersMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_PLAYER_OPTIONS]))) do
+  begin
+    Name := 'mOptionsPlayersMenu';
+    AddButton(nil, _lc[I_MENU_PLAYER_1], 'OptionsPlayersP1Menu');
+    AddButton(nil, _lc[I_MENU_PLAYER_2], 'OptionsPlayersP2Menu');
+  end;
+  Menu.DefControl := 'mOptionsPlayersMenu';
+  g_GUI_AddWindow(Menu);
+
+  CreatePlayerOptionsMenu('P1');
+  CreatePlayerOptionsMenu('P2');
+
+  Menu := TGUIWindow.Create('OptionsPlayersMIMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_MODEL_INFO]))) do
+  begin
+    Name := 'mOptionsPlayersMIMenu';
+    with AddLabel(_lc[I_MENU_MODEL_NAME]) do
+    begin
+      Name := 'lbName';
+      FixedLength := 16;
+    end;
+    with AddLabel(_lc[I_MENU_MODEL_AUTHOR]) do
+    begin
+      Name := 'lbAuthor';
+      FixedLength := 16;
+    end;
+    with AddMemo(_lc[I_MENU_MODEL_COMMENT], 14, 6) do
+    begin
+      Name := 'meComment';
+    end;
+    AddSpace();
+    AddLine(_lc[I_MENU_MODEL_OPTIONS]);
+    with AddLabel(_lc[I_MENU_MODEL_WEAPON]) do
+    begin
+      Name := 'lbWeapon';
+      FixedLength := Max(Length(_lc[I_MENU_YES]), Length(_lc[I_MENU_NO]));
+    end;
+    ReAlign();
+  end;
+  Menu.DefControl := 'mOptionsPlayersMIMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('OptionsLanguageMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_LANGUAGE_OPTIONS]))) do
+  begin
+    Name := 'mOptionsLanguageMenu';
+    AddButton(@ProcSetRussianLanguage, _lc[I_MENU_LANGUAGE_RUSSIAN]);
+    AddButton(@ProcSetEnglishLanguage, _lc[I_MENU_LANGUAGE_ENGLISH]);
+    ReAlign();
+  end;
+  Menu.DefControl := 'mOptionsLanguageMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := CreateYNMenu('DefaultOptionsMenu', _lc[I_MENU_SET_DEFAULT_PROMT], Round(gScreenWidth*0.6),
+                       gMenuSmallFont, @ProcDefaultMenuKeyDown);
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('AuthorsMenu');
+  Menu.BackTexture := 'INTER';
+  Menu.OnClose := ProcAuthorsClose;
+
+// Çàãîëîâîê:
+  _y := 16;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_CAP_1], gMenuFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := (gScreenWidth div 2)-(GetWidth() div 2);
+    Y := _y;
+    _y := _y+GetHeight();
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(Format(_lc[I_CREDITS_CAP_2], [GAME_VERSION, NET_PROTOCOL_VER]), gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := (gScreenWidth div 2)-(GetWidth() div 2);
+    Y := _y;
+    _y := _y+GetHeight()+32;
+  end;
+// ×òî äåëàë: Êòî äåëàë
+  cx := gScreenWidth div 2 - 320 + 64;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_1], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := cx;
+    Y := _y;
+    _y := _y+22;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_1_1], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := cx+32;
+    Y := _y;
+    _y := _y+36;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_2], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := cx;
+    Y := _y;
+    _y := _y+22;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_2_1], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := cx+32;
+    Y := _y;
+    _y := _y+36;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_3], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := cx;
+    Y := _y;
+    _y := _y+22;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_3_1], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X :=  cx+32;
+    Y := _y;
+   _y := _y+36;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_4], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := cx;
+    Y := _y;
+    _y := _y+22;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_A_4_1], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := cx+32;
+    Y := _y;
+    _y := gScreenHeight - 128;
+  end;
+// Çàêëþ÷åíèå:
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_CAP_3], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := cx;
+    Y := _y;
+    _y := _y+16;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_CLO_1], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := cx+32;
+    Y := _y;
+    _y := _y+GetHeight();
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_CLO_2], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := cx+32;
+    Y := _y;
+    _y := _y+GetHeight();
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_CLO_3], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 255, 255);
+    X := cx+32;
+    Y := _y;
+    _y := gScreenHeight - 32;
+  end;
+  with TGUILabel(Menu.AddChild(TGUILabel.Create(_lc[I_CREDITS_CLO_4], gMenuSmallFont))) do
+  begin
+    Color := _RGB(255, 0, 0);
+    X := gScreenWidth div 2 - GetWidth() div 2;
+    Y := _y;
+  end;
+  g_GUI_AddWindow(Menu);
+
+  Menu := CreateYNMenu('ExitMenu', _lc[I_MENU_EXIT_PROMT], Round(gScreenWidth*0.6),
+                       gMenuSmallFont, @ProcExitMenuKeyDown);
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('GameSingleMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_MAIN_MENU]))) do
+  begin
+    Name := 'mmGameSingleMenu';
+    AddButton(nil, _lc[I_MENU_LOAD_GAME], 'LoadMenu');
+    AddButton(nil, _lc[I_MENU_SAVE_GAME], 'SaveMenu').Name := 'save';
+    AddButton(@ReadGameSettings, _lc[I_MENU_SET_GAME], 'GameSetGameMenu');
+    AddButton(@ReadOptions, _lc[I_MENU_OPTIONS], 'OptionsMenu');
+    AddButton(nil, _lc[I_MENU_RESTART], 'RestartGameMenu');
+    AddButton(nil, _lc[I_MENU_END_GAME], 'EndGameMenu');
+  end;
+  Menu.DefControl := 'mmGameSingleMenu';
+  Menu.MainWindow := True;
+  Menu.OnClose := ProcGMClose;
+  Menu.OnShow := ProcGMShow;
+  g_GUI_AddWindow(Menu);
+
+  Menu := CreateYNMenu('EndGameMenu', _lc[I_MENU_END_GAME_PROMT], Round(gScreenWidth*0.6),
+                       gMenuSmallFont, @ProcEndMenuKeyDown);
+  g_GUI_AddWindow(Menu);
+
+  Menu := CreateYNMenu('RestartGameMenu', _lc[I_MENU_RESTART_GAME_PROMT], Round(gScreenWidth*0.6),
+                       gMenuSmallFont, @ProcRestartMenuKeyDown);
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('GameCustomMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_MAIN_MENU]))) do
+  begin
+    Name := 'mmGameCustomMenu';
+    AddButton(nil, _lc[I_MENU_CHANGE_PLAYERS], 'TeamMenu');
+    AddButton(nil, _lc[I_MENU_LOAD_GAME], 'LoadMenu');
+    AddButton(nil, _lc[I_MENU_SAVE_GAME], 'SaveMenu').Name := 'save';
+    AddButton(@ReadGameSettings, _lc[I_MENU_SET_GAME], 'GameSetGameMenu');
+    AddButton(@ReadOptions, _lc[I_MENU_OPTIONS], 'OptionsMenu');
+    AddButton(nil, _lc[I_MENU_RESTART], 'RestartGameMenu');
+    AddButton(nil, _lc[I_MENU_END_GAME], 'EndGameMenu');
+  end;
+  Menu.DefControl := 'mmGameCustomMenu';
+  Menu.MainWindow := True;
+  Menu.OnClose := ProcGMClose;
+  Menu.OnShow := ProcGMShow;
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('GameServerMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_MAIN_MENU]))) do
+  begin
+    Name := 'mmGameServerMenu';
+    AddButton(nil, _lc[I_MENU_CHANGE_PLAYERS], 'TeamMenu');
+    AddButton(@ReadGameSettings, _lc[I_MENU_SET_GAME], 'GameSetGameMenu');
+    AddButton(@ReadOptions, _lc[I_MENU_OPTIONS], 'OptionsMenu');
+    AddButton(nil, _lc[I_MENU_RESTART], 'RestartGameMenu');
+    AddButton(nil, _lc[I_MENU_END_GAME], 'EndGameMenu');
+  end;
+  Menu.DefControl := 'mmGameServerMenu';
+  Menu.MainWindow := True;
+  Menu.OnClose := ProcGMClose;
+  Menu.OnShow := ProcGMShow;
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('GameClientMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_MAIN_MENU]))) do
+  begin
+    Name := 'mmGameClientMenu';
+    AddButton(nil, _lc[I_MENU_CHANGE_PLAYERS], 'TeamMenu');
+    AddButton(@ReadOptions, _lc[I_MENU_OPTIONS], 'OptionsMenu');
+    AddButton(nil, _lc[I_MENU_END_GAME], 'EndGameMenu');
+  end;
+  Menu.DefControl := 'mmGameClientMenu';
+  Menu.MainWindow := True;
+  Menu.OnClose := ProcGMClose;
+  Menu.OnShow := ProcGMShow;
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('ClientPasswordMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuSmallFont, gMenuSmallFont, _lc[I_MENU_ENTERPASSWORD]))) do
+  begin
+    Name := 'mClientPasswordMenu';
+    with AddEdit(_lc[I_NET_SERVER_PASSWORD]) do
+    begin
+      Name := 'edPW';
+      Width := 12;
+      MaxLength := 32;
+    end;
+    AddSpace;
+
+    AddButton(@ProcEnterPassword, _lc[I_MENU_START_GAME]);
+    ReAlign();
+  end;
+  Menu.DefControl := 'mClientPasswordMenu';
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('GameSetGameMenu');
+  with TGUIMenu(Menu.AddChild(TGUIMenu.Create(gMenuFont, gMenuSmallFont, _lc[I_MENU_SET_GAME]))) do
+  begin
+    Name := 'mGameSetGameMenu';
+    with AddSwitch(_lc[I_MENU_TEAM_DAMAGE]) do
+    begin
+      Name := 'swTeamDamage';
+      AddItem(_lc[I_MENU_YES]);
+      AddItem(_lc[I_MENU_NO]);
+      ItemIndex := 1;
+    end;
+    with AddEdit(_lc[I_MENU_TIME_LIMIT]) do
+    begin
+      Name := 'edTimeLimit';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+    end;
+    with AddEdit(_lc[I_MENU_GOAL_LIMIT]) do
+    begin
+      Name := 'edGoalLimit';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+    end;
+    with AddEdit(_lc[I_MENU_MAX_LIVES]) do
+    begin
+      Name := 'edMaxLives';
+      OnlyDigits := True;
+      Width := 4;
+      MaxLength := 5;
+    end;
+    with AddSwitch(_lc[I_MENU_BOTS_VS]) do
+    begin
+      Name := 'swBotsVS';
+      AddItem(_lc[I_MENU_BOTS_VS_PLAYERS]);
+      AddItem(_lc[I_MENU_BOTS_VS_MONSTERS]);
+      AddItem(_lc[I_MENU_BOTS_VS_ALL]);
+      ItemIndex := 2;
+    end;
+
+    ReAlign();
+  end;
+  Menu.DefControl := 'mGameSetGameMenu';
+  Menu.OnClose := ProcApplyGameSet;
+  g_GUI_AddWindow(Menu);
+
+  Menu := TGUIWindow.Create('TeamMenu');
+  with TGUIMainMenu(Menu.AddChild(TGUIMainMenu.Create(gMenuFont, _lc[I_MENU_CHANGE_PLAYERS]))) do
+  begin
+    Name := 'mmTeamMenu';
+    AddButton(@ProcJoinRed, _lc[I_MENU_JOIN_RED], '').Name := 'tmJoinRed';
+    AddButton(@ProcJoinBlue, _lc[I_MENU_JOIN_BLUE], '').Name := 'tmJoinBlue';
+    AddButton(@ProcJoinGame, _lc[I_MENU_JOIN_GAME], '').Name := 'tmJoinGame';
+    AddButton(@ProcSwitchP2, _lc[I_MENU_ADD_PLAYER_2], '').Name := 'tmPlayer2';
+    AddButton(@ProcSpectate, _lc[I_MENU_SPECTATE], '').Name := 'tmSpectate';
+  end;
+  Menu.DefControl := 'mmTeamMenu';
+  Menu.OnShow := ProcChangePlayers;
+  g_GUI_AddWindow(Menu);
+end;
+
+procedure g_Menu_Show_SaveMenu();
+begin
+  if g_Game_IsTestMap then
+    Exit;
+  if gGameSettings.GameType = GT_SINGLE then
+    g_GUI_ShowWindow('GameSingleMenu')
+  else
+  begin
+    if g_Game_IsClient then
+      Exit
+    else
+      if g_Game_IsNet then
+        Exit
+      else
+        g_GUI_ShowWindow('GameCustomMenu');
+  end;
+  g_GUI_ShowWindow('SaveMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Show_LoadMenu();
+begin
+  if gGameSettings.GameType = GT_SINGLE then
+    g_GUI_ShowWindow('GameSingleMenu')
+  else
+  begin
+    if g_Game_IsClient then
+      Exit
+    else
+      if g_Game_IsNet then
+        Exit
+      else
+        g_GUI_ShowWindow('GameCustomMenu');
+  end;
+  g_GUI_ShowWindow('LoadMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Show_GameSetGame();
+begin
+  if gGameSettings.GameType = GT_SINGLE then
+    g_GUI_ShowWindow('GameSingleMenu')
+  else
+  begin
+    if g_Game_IsClient then
+      Exit
+    else
+      if g_Game_IsNet then
+        g_GUI_ShowWindow('GameServerMenu')
+      else
+        g_GUI_ShowWindow('GameCustomMenu');
+  end;
+  ReadGameSettings();
+  g_GUI_ShowWindow('GameSetGameMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Show_OptionsVideo();
+begin
+  if gGameSettings.GameType = GT_SINGLE then
+    g_GUI_ShowWindow('GameSingleMenu')
+  else
+  begin
+    if g_Game_IsClient then
+      g_GUI_ShowWindow('GameClientMenu')
+    else
+      if g_Game_IsNet then
+        g_GUI_ShowWindow('GameServerMenu')
+      else
+        g_GUI_ShowWindow('GameCustomMenu');
+  end;
+  ReadOptions();
+  g_GUI_ShowWindow('OptionsMenu');
+  g_GUI_ShowWindow('OptionsVideoMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Show_OptionsSound();
+begin
+  if gGameSettings.GameType = GT_SINGLE then
+    g_GUI_ShowWindow('GameSingleMenu')
+  else
+  begin
+    if g_Game_IsClient then
+      g_GUI_ShowWindow('GameClientMenu')
+    else
+      if g_Game_IsNet then
+        g_GUI_ShowWindow('GameServerMenu')
+      else
+        g_GUI_ShowWindow('GameCustomMenu');
+  end;
+  ReadOptions();
+  g_GUI_ShowWindow('OptionsMenu');
+  g_GUI_ShowWindow('OptionsSoundMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Show_EndGameMenu();
+begin
+  g_GUI_ShowWindow('EndGameMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Show_QuitGameMenu();
+begin
+  g_GUI_ShowWindow('ExitMenu');
+  g_Sound_PlayEx('MENU_OPEN');
+end;
+
+procedure g_Menu_Init();
+begin
+  MenuLoadData();
+  g_GUI_Init();
+  CreateAllMenus();
+end;
+
+procedure g_Menu_Free();
+begin
+  g_GUI_Destroy();
+
+  e_WriteLog('Releasing menu data...', MSG_NOTIFY);
+
+  MenuFreeData();
+end;
+
+procedure g_Menu_Reset();
+var
+  ex: Boolean;
+begin
+  g_GUI_SaveMenuPos();
+  ex := g_GUI_Destroy();
+
+  if ex then
+  begin
+    e_WriteLog('Recreating menu...', MSG_NOTIFY);
+
+    CreateAllMenus();
+
+    if gDebugMode then
+      g_Game_SetDebugMode();
+
+    g_GUI_LoadMenuPos();
+  end;
+end;
+
+end.
diff --git a/src/game/g_monsters.pas b/src/game/g_monsters.pas
new file mode 100644 (file)
index 0000000..507a560
--- /dev/null
@@ -0,0 +1,4116 @@
+unit g_monsters;
+
+interface
+
+uses
+  g_basic, e_graphics, g_phys, g_textures,
+  g_saveload, BinEditor, g_panel;
+
+const
+  MONSTATE_SLEEP  = 0;
+  MONSTATE_GO     = 1;
+  MONSTATE_RUN    = 2;
+  MONSTATE_CLIMB  = 3;
+  MONSTATE_DIE    = 4;
+  MONSTATE_DEAD   = 5;
+  MONSTATE_ATTACK = 6;
+  MONSTATE_SHOOT  = 7;
+  MONSTATE_PAIN   = 8;
+  MONSTATE_WAIT   = 9;
+  MONSTATE_REVIVE = 10;
+  MONSTATE_RUNOUT = 11;
+
+  BH_NORMAL   = 0;
+  BH_KILLER   = 1;
+  BH_MANIAC   = 2;
+  BH_INSANE   = 3;
+  BH_CANNIBAL = 4;
+  BH_GOOD     = 5;
+
+type
+  TMonster = Class (TObject)
+  private
+    FMonsterType: Byte;
+    FUID: Word;
+    FDirection: TDirection;
+    FStartDirection: TDirection;
+    FStartX, FStartY: Integer;
+    FRemoved: Boolean;
+    FHealth: Integer;
+    FMaxHealth: Integer;
+    FState: Byte;
+    FCurAnim: Byte;
+    FAnim: Array of Array [D_LEFT..D_RIGHT] of TAnimation;
+    FTargetUID: Word;
+    FTargetTime: Integer;
+    FBehaviour: Byte;
+    FAmmo: Integer;
+    FPain: Integer;
+    FSleep: Integer;
+    FPainSound: Boolean;
+    FWaitAttackAnim: Boolean;
+    FChainFire: Boolean;
+    tx, ty: Integer;
+    FStartID: Integer;
+    FObj: TObj;
+    FBloodRed: Byte;
+    FBloodGreen: Byte;
+    FBloodBlue: Byte;
+    FBloodKind: Byte;
+    FShellTimer: Integer;
+    FShellType: Byte;
+    vilefire: TAnimation;
+
+    FDieTriggers: Array of Integer;
+    FSpawnTrigger: Integer;
+
+    procedure Turn();
+    function findNewPrey(): Boolean;
+    procedure ActivateTriggers();
+
+  public
+    FNoRespawn: Boolean;
+
+    constructor Create(MonsterType: Byte; aID: Integer; ForcedUID: Integer = -1);
+    destructor Destroy(); override;
+    function Collide(X, Y: Integer; Width, Height: Word): Boolean; overload;
+    function Collide(Panel: TPanel): Boolean; overload;
+    function Collide(X, Y: Integer): Boolean; overload;
+    function TeleportTo(X, Y: Integer; silent: Boolean; dir: Byte): Boolean;
+    function Live(): Boolean;
+    procedure SetHealth(aH: Integer);
+    procedure Push(vx, vy: Integer);
+    function Damage(Damage: Word; VelX, VelY: Integer; SpawnerUID: Word; t: Byte): Boolean;
+    function Heal(Value: Word): Boolean;
+    procedure BFGHit();
+    procedure Update();
+    procedure ClientUpdate();
+    procedure ClientAttack(wx, wy, tx, ty: Integer);
+    procedure SetDeadAnim;
+    procedure Draw();
+    procedure WakeUp();
+    procedure WakeUpSound();
+    procedure DieSound();
+    procedure PainSound();
+    procedure ActionSound();
+    procedure AddTrigger(t: Integer);
+    procedure ClearTriggers();
+    procedure Respawn();
+    procedure SaveState(var Mem: TBinMemoryWriter);
+    procedure LoadState(var Mem: TBinMemoryReader);
+    procedure SetState(State: Byte; ForceAnim: Byte = 255);
+    procedure MakeBloodVector(Count: Word; VelX, VelY: Integer);
+    procedure MakeBloodSimple(Count: Word);
+    procedure RevertAnim(R: Boolean = True);
+    function  AnimIsReverse: Boolean;
+    function  shoot(o: PObj; immediately: Boolean): Boolean;
+    function  kick(o: PObj): Boolean;
+
+    property MonsterType: Byte read FMonsterType;
+    property MonsterHealth: Integer read FHealth write FHealth;
+    property MonsterAmmo: Integer read FAmmo write FAmmo;
+    property MonsterTargetUID: Word read FTargetUID write FTargetUID;
+    property MonsterTargetTime: Integer read FTargetTime write FTargetTime;
+    property MonsterBehaviour: Byte read FBehaviour write FBehaviour;
+    property MonsterSleep: Integer read FSleep write FSleep;
+    property MonsterState: Byte read FState write FState;
+    property MonsterRemoved: Boolean read FRemoved write FRemoved;
+    property MonsterPain: Integer read FPain write FPain;
+    property MonsterAnim: Byte read FCurAnim write FCurAnim;
+
+    property Obj: TObj read FObj;
+    property UID: Word read FUID write FUID;
+    property SpawnTrigger: Integer read FSpawnTrigger write FSpawnTrigger;
+
+    property GameX: Integer read FObj.X write FObj.X;
+    property GameY: Integer read FObj.Y write FObj.Y;
+    property GameVelX: Integer read FObj.Vel.X write FObj.Vel.X;
+    property GameVelY: Integer read FObj.Vel.Y write FObj.Vel.Y;
+    property GameAccelX: Integer read FObj.Accel.X write FObj.Accel.X;
+    property GameAccelY: Integer read FObj.Accel.Y write FObj.Accel.Y;
+    property GameDirection: TDirection read FDirection write FDirection;
+
+    property StartID: Integer read FStartID;
+  end;
+
+procedure g_Monsters_LoadData();
+procedure g_Monsters_FreeData();
+procedure g_Monsters_Init();
+procedure g_Monsters_Free();
+function  g_Monsters_Create(MonsterType: Byte; X, Y: Integer;
+            Direction: TDirection; AdjCoord: Boolean = False; ForcedUID: Integer = -1): Integer;
+procedure g_Monsters_Update();
+procedure g_Monsters_Draw();
+procedure g_Monsters_DrawHealth();
+function  g_Monsters_Get(UID: Word): TMonster;
+procedure g_Monsters_killedp();
+procedure g_Monsters_SaveState(var Mem: TBinMemoryWriter);
+procedure g_Monsters_LoadState(var Mem: TBinMemoryReader);
+function  g_Monsters_GetIDByName(name: String): Integer;
+function  g_Monsters_GetNameByID(MonsterType: Byte): String;
+function  g_Monsters_GetKilledBy(MonsterType: Byte): String;
+
+var
+  gMonsters: array of TMonster;
+
+implementation
+
+uses
+  e_log, g_main, g_sound, g_gfx, g_player, g_game,
+  g_weapons, g_triggers, MAPDEF, g_items, g_options,
+  g_console, g_map, Math, SysUtils, g_menu, WADEDITOR,
+  g_language, g_netmsg;
+
+const
+  ANIM_SLEEP   = 0;
+  ANIM_GO      = 1;
+  ANIM_DIE     = 2;
+  ANIM_MESS    = 3;
+  ANIM_ATTACK  = 4;
+  ANIM_ATTACK2 = 5;
+  ANIM_PAIN    = 6;
+
+  STATE_SLEEP  = 0;
+  STATE_GO     = 1;
+  STATE_RUN    = 2;
+  STATE_CLIMB  = 3;
+  STATE_DIE    = 4;
+  STATE_DEAD   = 5;
+  STATE_ATTACK = 6;
+  STATE_SHOOT  = 7;
+  STATE_PAIN   = 8;
+  STATE_WAIT   = 9;
+  STATE_REVIVE = 10;
+  STATE_RUNOUT = 11;
+
+  MONSTER_SIGNATURE = $534E4F4D; // 'MONS'
+
+// Òàáëèöà òèïîâ àíèìàöèè ìîíñòðîâ:
+  ANIMTABLE: Array [ANIM_SLEEP..ANIM_PAIN] of
+               record
+                 name: String;
+                 loop: Boolean;
+               end = ((name: 'SLEEP'; loop: True),
+                      (name: 'GO'; loop: True),
+                      (name: 'DIE'; loop: False),
+                      (name: 'MESS'; loop: False),
+                      (name: 'ATTACK'; loop: False),
+                      (name: 'ATTACK2'; loop: False),
+                      (name: 'PAIN'; loop: False));
+
+// Òàáëèöà õàðàêòåðèñòèê ìîíñòðîâ:
+  MONSTERTABLE: Array [MONSTER_DEMON..MONSTER_MAN] of
+                  record
+                    Name: String;
+                    Rect: TRectWH;
+                    Health: Word;
+                    RunVel: Byte;
+                    MinPain: Byte;
+                    Pain: Byte;
+                    Jump: Byte;
+                  end =
+   ((Name:'DEMON'; Rect:(X:7; Y:8; Width:50; Height:52); Health:60;
+     RunVel: 7; MinPain: 10; Pain: 20; Jump: 10),
+
+    (Name:'IMP'; Rect:(X:15; Y:10; Width:34; Height:50); Health:25;
+     RunVel: 3; MinPain: 0; Pain: 15; Jump: 10),
+
+    (Name:'ZOMBY'; Rect:(X:15; Y:8; Width:34; Height:52); Health:15;
+     RunVel: 3; MinPain: 0; Pain: 10; Jump: 10),
+
+    (Name:'SERG'; Rect:(X:15; Y:8; Width:34; Height:52); Health:20;
+     RunVel: 3; MinPain: 0; Pain: 10; Jump: 10),
+                                          
+    (Name:'CYBER'; Rect:(X:24; Y:9; Width:80; Height:110); Health:500;
+     RunVel: 5; MinPain: 50; Pain: 70; Jump: 10),
+
+    (Name:'CGUN'; Rect:(X:15; Y:4; Width:34; Height:56); Health:60;
+     RunVel: 3; MinPain: 10; Pain: 20; Jump: 10),
+
+    (Name:'BARON'; Rect:(X:39; Y:32; Width:50; Height:64); Health:150;
+     RunVel: 3; MinPain: 30; Pain: 40; Jump: 10),
+
+    (Name:'KNIGHT'; Rect:(X:39; Y:32; Width:50; Height:64); Health:75;
+     RunVel: 3; MinPain: 30; Pain: 40; Jump: 10),
+
+    (Name:'CACO'; Rect:(X:34; Y:36; Width:60; Height:56); Health:100;
+     RunVel: 4; MinPain: 0; Pain: 10; Jump: 4),
+
+    (Name:'SOUL'; Rect:(X:16; Y:14; Width:32; Height:36); Health:60;
+     RunVel: 4; MinPain: 0; Pain: 10; Jump: 4),
+
+    (Name:'PAIN'; Rect:(X:34; Y:36; Width:60; Height:56); Health:100;
+     RunVel: 4; MinPain: 0; Pain: 10; Jump: 4),
+
+    (Name:'SPIDER'; Rect:(X:23; Y:14; Width:210; Height:100); Health:500;
+     RunVel: 4; MinPain: 50; Pain: 70; Jump: 10),
+
+    (Name:'BSP'; Rect:(X:14; Y:17; Width:100; Height:42); Health:150;
+     RunVel: 4; MinPain: 0; Pain: 20; Jump: 10),
+
+    (Name:'MANCUB'; Rect:(X:28; Y:34; Width:72; Height:60); Health:200;
+     RunVel: 3; MinPain: 20; Pain: 40; Jump: 7),
+
+    (Name:'SKEL'; Rect:(X:30; Y:28; Width:68; Height:72); Health:200;
+     RunVel: 6; MinPain: 20; Pain: 40; Jump: 11),
+
+    (Name:'VILE'; Rect:(X:30; Y:28; Width:68; Height:72); Health:150;
+     RunVel: 7; MinPain: 10; Pain: 30; Jump: 12),
+
+    (Name:'FISH'; Rect:(X:6; Y:11; Width:20; Height:10); Health:35;
+     RunVel: 14; MinPain: 10; Pain: 20; Jump: 6),
+
+    (Name:'BARREL'; Rect:(X:20; Y:13; Width:24; Height:36); Health:20;
+     RunVel: 0; MinPain: 0; Pain: 0; Jump: 0),
+
+    (Name:'ROBO'; Rect:(X:30; Y:26; Width:68; Height:76); Health:20;
+     RunVel: 3; MinPain: 20; Pain: 40; Jump: 6),
+
+    (Name:'MAN'; Rect:(X:15; Y:6; Width:34; Height:52); Health:400;
+     RunVel: 8; MinPain: 50; Pain: 70; Jump: 10));
+
+// Òàáëèöà ïàðàìåòðîâ àíèìàöèè ìîíñòðîâ:
+  MONSTER_ANIMTABLE: Array [MONSTER_DEMON..MONSTER_MAN] of
+     record
+       LeftAnim: Boolean;
+       wX, wY: Integer; // Îòêóäà âûëåòèò ïóëÿ
+       AnimSpeed: Array [ANIM_SLEEP..ANIM_PAIN] of Byte;
+       AnimDeltaRight: Array [ANIM_SLEEP..ANIM_PAIN] of TPoint;
+       AnimDeltaLeft: Array [ANIM_SLEEP..ANIM_PAIN] of TPoint;
+     end =          // SLEEP           GO              DIE             MESS            ATTACK          ATTACK2         PAIN
+   ((LeftAnim: False; wX: 54; wY: 32; AnimSpeed:(3, 2, 3, 2, 3, 0, 4); //DEMON
+     AnimDeltaRight: ((X:  1; Y:  4), (X:  1; Y:  4), (X:  0; Y:  4), (X:  0; Y:  4), (X:  2; Y:  6), (X:  2; Y:  6), (X:  2; Y:  5));
+     AnimDeltaLeft:  ((X:  1; Y:  4), (X:  1; Y:  4), (X:  0; Y:  4), (X:  0; Y:  4), (X:  2; Y:  6), (X:  2; Y:  6), (X:  2; Y:  5))),
+
+    (LeftAnim: False; wX: 32; wY: 32; AnimSpeed:(3, 2, 3, 2, 3, 0, 4); //IMP
+     AnimDeltaRight: ((X:  8; Y: -4), (X:  8; Y: -4), (X: -2; Y: -1), (X:  3; Y: -2), (X: 14; Y: -4), (X: 14; Y: -4), (X: -5; Y: -4));
+     AnimDeltaLeft:  ((X:  8; Y: -4), (X:  8; Y: -4), (X: -2; Y: -1), (X:  3; Y: -2), (X: 14; Y: -4), (X: 14; Y: -4), (X: -5; Y: -4))),
+
+    (LeftAnim: False; wX: 32; wY: 32; AnimSpeed:(3, 2, 3, 2, 3, 0, 4); //ZOMBY
+     AnimDeltaRight: ((X:  1; Y: -4), (X:  1; Y: -4), (X:  3; Y: -1), (X:  2; Y: -1), (X:  2; Y: -4), (X:  2; Y: -4), (X:  1; Y: -4));
+     AnimDeltaLeft:  ((X:  1; Y: -4), (X:  1; Y: -4), (X:  3; Y: -1), (X:  2; Y: -1), (X:  2; Y: -4), (X:  2; Y: -4), (X:  1; Y: -4))),
+
+    (LeftAnim: False; wX: 32; wY: 32; AnimSpeed:(3, 2, 3, 2, 3, 0, 4); //SERG
+     AnimDeltaRight: ((X:  0; Y: -4), (X:  0; Y: -4), (X: -3; Y: -1), (X: -4; Y: -1), (X:  1; Y: -4), (X:  1; Y: -4), (X:  0; Y: -4));
+     AnimDeltaLeft:  ((X:  0; Y: -4), (X:  0; Y: -4), (X: -3; Y: -1), (X: -4; Y: -1), (X:  1; Y: -4), (X:  1; Y: -4), (X:  0; Y: -4))),
+
+    (LeftAnim: True; wX: 70; wY: 73; AnimSpeed:(3, 3, 3, 3, 3, 0, 3);  //CYBER
+     AnimDeltaRight: ((X:  2; Y: -6), (X:  2; Y: -6), (X: -3; Y: -4), (X: -3; Y: -4), (X: 25; Y: -6), (X: 25; Y: -6), (X: -2; Y: -6));
+     AnimDeltaLeft:  ((X:  3; Y: -3), (X:  3; Y: -3), (X: -3; Y: -4), (X: -3; Y: -4), (X:-26; Y: -3), (X:-26; Y: -3), (X:  1; Y: -3))),
+
+    (LeftAnim: True; wX: 32; wY: 32; AnimSpeed:(3, 2, 2, 2, 1, 0, 4);  //CGUN
+     AnimDeltaRight: ((X: -1; Y: -2), (X: -1; Y: -2), (X: -2; Y:  0), (X: -2; Y:  0), (X:  0; Y: -3), (X:  0; Y: -3), (X: -1; Y: -2));
+     AnimDeltaLeft:  ((X: -1; Y: -2), (X: -1; Y: -2), (X: -2; Y:  0), (X: -2; Y:  0), (X: -1; Y: -4), (X: -1; Y: -4), (X:  2; Y: -4))),
+
+    (LeftAnim: True; wX: 64; wY: 64; AnimSpeed:(3, 2, 3, 4, 2, 0, 4);  //BARON
+     AnimDeltaRight: ((X:  4; Y:  0), (X:  2; Y:  0), (X: -1; Y: -1), (X: -1; Y: -1), (X:  1; Y:  0), (X:  1; Y:  0), (X: -1; Y:  0));
+     AnimDeltaLeft:  ((X:  0; Y:  0), (X:  2; Y:  0), (X: -1; Y: -1), (X: -1; Y: -1), (X: -2; Y:  0), (X: -2; Y:  0), (X:  1; Y:  0))),
+
+    (LeftAnim: True; wX: 64; wY: 64; AnimSpeed:(3, 2, 3, 4, 2, 0, 4);  //KNIGHT
+     AnimDeltaRight: ((X:  4; Y:  0), (X:  2; Y:  0), (X: -1; Y: -1), (X: -1; Y: -1), (X:  1; Y:  0), (X:  1; Y:  0), (X: -1; Y:  0));
+     AnimDeltaLeft:  ((X:  0; Y:  0), (X:  2; Y:  0), (X: -1; Y: -1), (X: -1; Y: -1), (X: -2; Y:  0), (X: -2; Y:  0), (X:  1; Y:  0))),
+
+    (LeftAnim: False; wX: 88; wY: 69; AnimSpeed:(3, 2, 3, 4, 2, 0, 4); //CACO
+     AnimDeltaRight: ((X:  0; Y: -4), (X:  0; Y: -4), (X:  0; Y: -5), (X:  0; Y: -5), (X:  0; Y: -4), (X:  0; Y: -4), (X:  0; Y: -4));
+     AnimDeltaLeft:  ((X:  0; Y: -4), (X:  0; Y: -4), (X:  0; Y: -5), (X:  0; Y: -5), (X:  0; Y: -4), (X:  0; Y: -4), (X:  0; Y: -4))),
+
+    (LeftAnim: False; wX: 32; wY: 32; AnimSpeed:(3, 2, 3, 4, 1, 0, 4); //SOUL
+     AnimDeltaRight: ((X:  1; Y:-10), (X:  1; Y:-10), (X:-33; Y:-34), (X:-33; Y:-34), (X:-16; Y:-10), (X:-16; Y:-10), (X: -1; Y: -7));
+     AnimDeltaLeft:  ((X:  1; Y:-10), (X:  1; Y:-10), (X:-33; Y:-34), (X:-33; Y:-34), (X:-16; Y:-10), (X:-16; Y:-10), (X: -1; Y: -7))),
+
+    (LeftAnim: False; wX: 64; wY: 64; AnimSpeed:(3, 2, 3, 4, 2, 0, 4); //PAIN
+     AnimDeltaRight: ((X: -1; Y: -3), (X: -1; Y: -3), (X: -3; Y:  0), (X: -3; Y:  0), (X: -1; Y: -3), (X: -1; Y: -3), (X: -1; Y: -4));
+     AnimDeltaLeft:  ((X: -1; Y: -3), (X: -1; Y: -3), (X: -3; Y:  0), (X: -3; Y:  0), (X: -1; Y: -3), (X: -1; Y: -3), (X: -1; Y: -4))),
+
+    (LeftAnim: True; wX: 128; wY: 64; AnimSpeed:(3, 2, 4, 4, 1, 0, 4); //SPIDER
+     AnimDeltaRight: ((X: -4; Y: -4), (X: -4; Y: -4), (X: -2; Y:  8), (X: -2; Y:  8), (X: -3; Y: -3), (X: -3; Y: -3), (X: -3; Y: -4));
+     AnimDeltaLeft:  ((X: -4; Y: -4), (X: -4; Y: -4), (X: -2; Y:  8), (X: -2; Y:  8), (X: -3; Y: -3), (X: -3; Y: -3), (X: 18; Y: -5))),
+
+    (LeftAnim: True; wX: 64; wY: 32; AnimSpeed:(3, 2, 3, 4, 1, 0, 4);  //BSP
+     AnimDeltaRight: ((X:  0; Y: -1), (X:  0; Y: -1), (X: -3; Y:  5), (X: -3; Y:  5), (X:  7; Y: -1), (X:  7; Y: -1), (X:  1; Y: -3));
+     AnimDeltaLeft:  ((X:  0; Y: -1), (X:  0; Y: -1), (X: -3; Y:  5), (X: -3; Y:  5), (X:  7; Y: -1), (X:  7; Y: -1), (X:  6; Y: -3))),
+
+    (LeftAnim: False; wX: 64; wY: 64; AnimSpeed:(3, 2, 2, 4, 2, 0, 4); //MANCUB
+     AnimDeltaRight: ((X: -2; Y: -7), (X: -2; Y: -7), (X: -4; Y: -2), (X: -4; Y: -2), (X: -4; Y: -7), (X: -4; Y: -7), (X:-14; Y: -7));
+     AnimDeltaLeft:  ((X: -2; Y: -7), (X: -2; Y: -7), (X: -4; Y: -2), (X: -4; Y: -2), (X: -4; Y: -7), (X: -4; Y: -7), (X:-14; Y: -7))),
+
+    (LeftAnim: True; wX: 64; wY: 32; AnimSpeed:(3, 3, 3, 3, 3, 3, 3);  //SKEL
+     AnimDeltaRight: ((X: -1; Y:  4), (X: -1; Y:  4), (X: -2; Y:  4), (X: -2; Y:  4), (X: -1; Y:  4), (X:  6; Y:  2), (X:-24; Y:  4));
+     AnimDeltaLeft:  ((X:  1; Y:  4), (X: -1; Y:  4), (X: -2; Y:  4), (X: -2; Y:  4), (X: -2; Y:  2), (X: -5; Y:  4), (X: 26; Y:  4))),
+
+    (LeftAnim: True; wX: 64; wY: 32; AnimSpeed:(3, 3, 3, 3, 3, 3, 3);  //VILE
+     AnimDeltaRight: ((X:  5; Y:-21), (X:  5; Y:-21), (X:  1; Y:-21), (X:  1; Y:-21), (X:  8; Y:-23), (X: -1; Y:-23), (X:  4; Y:-20));
+     AnimDeltaLeft:  ((X: -8; Y:-21), (X:  5; Y:-21), (X:  1; Y:-21), (X:  1; Y:-21), (X:-10; Y:-24), (X:  3; Y:-23), (X: -4; Y:-22))),
+
+    (LeftAnim: False; wX: 8; wY: 8; AnimSpeed:(2, 2, 2, 2, 3, 0, 1);   //FISH
+     AnimDeltaRight: ((X: -1; Y:  0), (X: -1; Y:  0), (X: -2; Y: -1), (X: -2; Y: -1), (X: -1; Y: -1), (X: -1; Y: -1), (X: -1; Y: -1));
+     AnimDeltaLeft:  ((X: -1; Y:  0), (X: -1; Y:  0), (X: -2; Y: -1), (X: -2; Y: -1), (X: -1; Y: -1), (X: -1; Y: -1), (X: -1; Y: -1 ))),
+
+    (LeftAnim: False; wX: 32; wY: 32; AnimSpeed:(3, 0, 3, 0, 0, 0, 5); //BARREL
+     AnimDeltaRight: ((X:  0; Y:-15), (X:  0; Y:-15), (X: -1; Y:-15), (X: -1; Y:-15), (X:  0; Y:-15), (X:  0; Y:-15), (X:  0; Y:-15));
+     AnimDeltaLeft:  ((X:  0; Y:-15), (X:  0; Y:-15), (X: -1; Y:-15), (X: -1; Y:-15), (X:  0; Y:-15), (X:  0; Y:-15), (X:  0; Y:-15))),
+
+    (LeftAnim: False; wX: 95; wY: 57; AnimSpeed:(1, 2, 1, 0, 1, 1, 0); //ROBO
+     AnimDeltaRight: ((X: -2; Y:-26), (X: -2; Y:-26), (X:  0; Y:-26), (X:  0; Y:-26), (X:  2; Y:-26), (X: 15; Y:-26), (X: -2; Y:-26));
+     AnimDeltaLeft:  ((X: -2; Y:-26), (X: -2; Y:-26), (X:  0; Y:-26), (X:  0; Y:-26), (X:  2; Y:-26), (X: 15; Y:-26), (X: -2; Y:-26))),
+
+    (LeftAnim: False; wX: 32; wY: 32; AnimSpeed:(3, 2, 2, 2, 2, 0, 5); //MAN
+     AnimDeltaRight: ((X:  0; Y: -6), (X:  0; Y: -6), (X: -2; Y:  0), (X:  2; Y:  0), (X:  1; Y: -6), (X:  1; Y: -6), (X:  0; Y: -6));
+     AnimDeltaLeft:  ((X:  0; Y: -6), (X:  0; Y: -6), (X: -2; Y:  0), (X:  2; Y:  0), (X:  1; Y: -6), (X:  1; Y: -6), (X:  0; Y: -6))) );
+
+  MAX_ATM = 89; // Âðåìÿ îæèäàíèÿ ïîñëå ïîòåðè öåëè
+  MAX_SOUL = 512; // Îãðàíè÷åíèå Lost_Soul'îâ
+
+var
+  pt_x: Integer = 0;
+  pt_xs: Integer = 1;
+  pt_y: Integer = 0;
+  pt_ys: Integer = 1;
+  soulcount: Integer = 0;
+
+function FindMonster(): DWORD;
+var
+  i: Integer;
+begin
+  if gMonsters <> nil then
+    for i := 0 to High(gMonsters) do
+      if gMonsters[i] = nil then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if gMonsters = nil then
+    begin
+      SetLength(gMonsters, 32);
+      Result := 0;
+    end
+  else
+    begin
+      Result := High(gMonsters) + 1;
+      SetLength(gMonsters, Length(gMonsters) + 32);
+    end;
+end;
+
+function IsFriend(a, b: Byte): Boolean;
+begin
+  Result := True;
+
+// Áî÷êà - âñåì äðóã:
+  if (a = MONSTER_BARREL) or (b = MONSTER_BARREL) then
+    Exit;
+
+// Ìîíñòðû îäíîãî âèäà:
+  if a = b then
+    case a of
+      MONSTER_IMP, MONSTER_DEMON, MONSTER_BARON, MONSTER_KNIGHT, MONSTER_CACO,
+      MONSTER_SOUL, MONSTER_PAIN, MONSTER_MANCUB, MONSTER_SKEL, MONSTER_FISH:
+        Exit; // Ýòè íå áüþò ñâîèõ
+    end;
+
+// Lost_Soul íå ìîæåò ðàíèòü Pain_Elemental'à:
+  if (a = MONSTER_SOUL) and (b = MONSTER_PAIN) then
+    Exit;
+// Pain_Elemental íå ìîæåò ðàíèòü Lost_Soul'à:
+  if (b = MONSTER_SOUL) and (a = MONSTER_PAIN) then
+    Exit;
+
+// Â îñòàëüíûõ ñëó÷àÿõ - áóäóò áèòü äðóã äðóãà:
+  Result := False;
+end;
+
+function BehaviourDamage(SpawnerUID: Word; BH, SelfType: Byte): Boolean;
+var
+  m: TMonster;
+  UIDType, MonsterType: Byte;
+begin
+  Result := False;
+  MonsterType := 0;
+
+  UIDType := g_GetUIDType(SpawnerUID);
+  if UIDType = UID_MONSTER then
+  begin
+    m := g_Monsters_Get(SpawnerUID);
+    if m = nil then Exit;
+    MonsterType := m.FMonsterType;
+  end;
+
+  case BH of
+    BH_NORMAL: Result := (UIDType = UID_PLAYER) or
+      ((UIDType = UID_MONSTER) and (not IsFriend(MonsterType, SelfType)));
+
+    BH_KILLER: Result := UIDType = UID_PLAYER;
+    BH_MANIAC: Result := (UIDType = UID_PLAYER) or
+      ((UIDType = UID_MONSTER) and (not IsFriend(MonsterType, SelfType)));
+
+    BH_INSANE: Result := (UIDType = UID_MONSTER) and (not IsFriend(MonsterType, SelfType));
+    BH_CANNIBAL: Result := (UIDType = UID_MONSTER) and (MonsterType = SelfType);
+  end;
+end;
+
+function canShoot(m: Byte): Boolean;
+begin
+  Result := False;
+
+  case m of
+    MONSTER_DEMON, MONSTER_FISH, MONSTER_BARREL:
+      Exit;
+    else
+      Result := True;
+  end;
+end;
+
+function isCorpse(o: PObj; immediately: Boolean): Integer;
+var
+  a: Integer;
+begin
+  Result := -1;
+
+// Åñëè íóæíà âåðîÿòíîñòü:
+  if not immediately then
+    if Random(8) <> 0 then
+      Exit;
+
+  if gMonsters = nil then
+    Exit;
+
+// Èùåì ìåðòâûõ ìîíñòðîâ ïîáëèçîñòè:
+  for a := 0 to High(gMonsters) do
+    if (gMonsters[a] <> nil) and (gMonsters[a].FState = STATE_DEAD) then
+      if g_Obj_Collide(o, @gMonsters[a].FObj) then
+        case gMonsters[a].FMonsterType of // Íå âîñêðåñèòü:
+          MONSTER_SOUL, MONSTER_PAIN, MONSTER_CYBER, MONSTER_SPIDER,
+          MONSTER_VILE, MONSTER_BARREL, MONSTER_ROBO: Continue;
+          else // Îñòàëüíûõ ìîæíî âîñêðåñèòü
+            begin
+              Result := a;
+              Exit;
+            end;
+        end;
+end;
+
+procedure g_Monsters_LoadData();
+begin
+  e_WriteLog('Loading monsters data...', MSG_NOTIFY);
+
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 0%', 0, False);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARREL_SLEEP', GameWAD+':MTEXTURES\BARREL_SLEEP', 64, 64, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARREL_DIE', GameWAD+':MTEXTURES\BARREL_DIE', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARREL_PAIN', GameWAD+':MTEXTURES\BARREL_PAIN', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ZOMBY_SLEEP', GameWAD+':MTEXTURES\ZOMBY_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ZOMBY_GO', GameWAD+':MTEXTURES\ZOMBY_GO', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ZOMBY_DIE', GameWAD+':MTEXTURES\ZOMBY_DIE', 64, 64, 6);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 5%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ZOMBY_MESS', GameWAD+':MTEXTURES\ZOMBY_MESS', 64, 64, 9);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ZOMBY_ATTACK', GameWAD+':MTEXTURES\ZOMBY_ATTACK', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ZOMBY_PAIN', GameWAD+':MTEXTURES\ZOMBY_PAIN', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SERG_SLEEP', GameWAD+':MTEXTURES\SERG_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SERG_GO', GameWAD+':MTEXTURES\SERG_GO', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SERG_DIE', GameWAD+':MTEXTURES\SERG_DIE', 64, 64, 5);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 10%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SERG_MESS', GameWAD+':MTEXTURES\SERG_MESS', 64, 64, 9);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SERG_ATTACK', GameWAD+':MTEXTURES\SERG_ATTACK', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SERG_PAIN', GameWAD+':MTEXTURES\SERG_PAIN', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MAN_SLEEP', GameWAD+':MTEXTURES\MAN_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MAN_GO', GameWAD+':MTEXTURES\MAN_GO', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MAN_DIE', GameWAD+':MTEXTURES\MAN_DIE', 64, 64, 7);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 15%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MAN_MESS', GameWAD+':MTEXTURES\MAN_MESS', 64, 64, 9);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MAN_ATTACK', GameWAD+':MTEXTURES\MAN_ATTACK', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MAN_PAIN', GameWAD+':MTEXTURES\MAN_PAIN', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_SLEEP', GameWAD+':MTEXTURES\CGUN_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_SLEEP_L', GameWAD+':MTEXTURES\CGUN_SLEEP_L', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_GO', GameWAD+':MTEXTURES\CGUN_GO', 64, 64, 4);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 20%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_GO_L', GameWAD+':MTEXTURES\CGUN_GO_L', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_DIE', GameWAD+':MTEXTURES\CGUN_DIE', 64, 64, 7);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_MESS', GameWAD+':MTEXTURES\CGUN_MESS', 64, 64, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_ATTACK', GameWAD+':MTEXTURES\CGUN_ATTACK', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_ATTACK_L', GameWAD+':MTEXTURES\CGUN_ATTACK_L', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_PAIN', GameWAD+':MTEXTURES\CGUN_PAIN', 64, 64, 1);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 25%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CGUN_PAIN_L', GameWAD+':MTEXTURES\CGUN_PAIN_L', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_IMP_SLEEP', GameWAD+':MTEXTURES\IMP_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_IMP_GO', GameWAD+':MTEXTURES\IMP_GO', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_IMP_DIE', GameWAD+':MTEXTURES\IMP_DIE', 64, 64, 5);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_IMP_MESS', GameWAD+':MTEXTURES\IMP_MESS', 64, 64, 8);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_IMP_ATTACK', GameWAD+':MTEXTURES\IMP_ATTACK', 64, 64, 3);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 30%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_IMP_PAIN', GameWAD+':MTEXTURES\IMP_PAIN', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_DEMON_SLEEP', GameWAD+':MTEXTURES\DEMON_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_DEMON_GO', GameWAD+':MTEXTURES\DEMON_GO', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_DEMON_DIE', GameWAD+':MTEXTURES\DEMON_DIE', 64, 64, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_DEMON_ATTACK', GameWAD+':MTEXTURES\DEMON_ATTACK', 64, 64, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_DEMON_PAIN', GameWAD+':MTEXTURES\DEMON_PAIN', 64, 64, 1);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 35%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SOUL_SLEEP', GameWAD+':MTEXTURES\SOUL_SLEEP', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SOUL_GO', GameWAD+':MTEXTURES\SOUL_GO', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SOUL_PAIN', GameWAD+':MTEXTURES\SOUL_PAIN', 64, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SOUL_ATTACK', GameWAD+':MTEXTURES\SOUL_ATTACK', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SOUL_DIE', GameWAD+':MTEXTURES\SOUL_DIE', 128, 128, 7);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_FISH_SLEEP', GameWAD+':MTEXTURES\FISH_SLEEP', 32, 32, 2);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 40%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_FISH_GO', GameWAD+':MTEXTURES\FISH_GO', 32, 32, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_FISH_PAIN', GameWAD+':MTEXTURES\FISH_PAIN', 32, 32, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_FISH_ATTACK', GameWAD+':MTEXTURES\FISH_ATTACK', 32, 32, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_FISH_DIE', GameWAD+':MTEXTURES\FISH_DIE', 32, 32, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SPIDER_SLEEP', GameWAD+':MTEXTURES\SPIDER_SLEEP', 256, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SPIDER_GO', GameWAD+':MTEXTURES\SPIDER_GO', 256, 128, 6);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 45%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SPIDER_PAIN', GameWAD+':MTEXTURES\SPIDER_PAIN', 256, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SPIDER_PAIN_L', GameWAD+':MTEXTURES\SPIDER_PAIN_L', 256, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SPIDER_ATTACK', GameWAD+':MTEXTURES\SPIDER_ATTACK', 256, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SPIDER_DIE', GameWAD+':MTEXTURES\SPIDER_DIE', 256, 128, 10);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BSP_SLEEP', GameWAD+':MTEXTURES\BSP_SLEEP', 128, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BSP_GO', GameWAD+':MTEXTURES\BSP_GO', 128, 64, 6);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 50%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BSP_PAIN', GameWAD+':MTEXTURES\BSP_PAIN', 128, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BSP_PAIN_L', GameWAD+':MTEXTURES\BSP_PAIN_L', 128, 64, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BSP_ATTACK', GameWAD+':MTEXTURES\BSP_ATTACK', 128, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BSP_DIE', GameWAD+':MTEXTURES\BSP_DIE', 128, 64, 7);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CACO_SLEEP', GameWAD+':MTEXTURES\CACO_SLEEP', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CACO_GO', GameWAD+':MTEXTURES\CACO_GO', 128, 128, 1);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 55%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CACO_PAIN', GameWAD+':MTEXTURES\CACO_PAIN', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CACO_ATTACK', GameWAD+':MTEXTURES\CACO_ATTACK', 128, 128, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CACO_DIE', GameWAD+':MTEXTURES\CACO_DIE', 128, 128, 7);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_PAIN_SLEEP', GameWAD+':MTEXTURES\PAIN_SLEEP', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_PAIN_GO', GameWAD+':MTEXTURES\PAIN_GO', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_PAIN_PAIN', GameWAD+':MTEXTURES\PAIN_PAIN', 128, 128, 1);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 60%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_PAIN_ATTACK', GameWAD+':MTEXTURES\PAIN_ATTACK', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_PAIN_DIE', GameWAD+':MTEXTURES\PAIN_DIE', 128, 128, 7);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_SLEEP', GameWAD+':MTEXTURES\BARON_SLEEP', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_SLEEP_L', GameWAD+':MTEXTURES\BARON_SLEEP_L', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_GO', GameWAD+':MTEXTURES\BARON_GO', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_PAIN', GameWAD+':MTEXTURES\BARON_PAIN', 128, 128, 1);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 65%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_PAIN_L', GameWAD+':MTEXTURES\BARON_PAIN_L', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_ATTACK', GameWAD+':MTEXTURES\BARON_ATTACK', 128, 128, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_ATTACK_L', GameWAD+':MTEXTURES\BARON_ATTACK_L', 128, 128, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_BARON_DIE', GameWAD+':MTEXTURES\BARON_DIE', 128, 128, 7);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_SLEEP', GameWAD+':MTEXTURES\KNIGHT_SLEEP', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_SLEEP_L', GameWAD+':MTEXTURES\KNIGHT_SLEEP_L', 128, 128, 2);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 70%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_GO', GameWAD+':MTEXTURES\KNIGHT_GO', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_PAIN', GameWAD+':MTEXTURES\KNIGHT_PAIN', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_PAIN_L', GameWAD+':MTEXTURES\KNIGHT_PAIN_L', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_ATTACK', GameWAD+':MTEXTURES\KNIGHT_ATTACK', 128, 128, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_ATTACK_L', GameWAD+':MTEXTURES\KNIGHT_ATTACK_L', 128, 128, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_KNIGHT_DIE', GameWAD+':MTEXTURES\KNIGHT_DIE', 128, 128, 7);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 75%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MANCUB_SLEEP', GameWAD+':MTEXTURES\MANCUB_SLEEP', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MANCUB_GO', GameWAD+':MTEXTURES\MANCUB_GO', 128, 128, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MANCUB_PAIN', GameWAD+':MTEXTURES\MANCUB_PAIN', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MANCUB_ATTACK', GameWAD+':MTEXTURES\MANCUB_ATTACK', 128, 128, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_MANCUB_DIE', GameWAD+':MTEXTURES\MANCUB_DIE', 128, 128, 10);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_SLEEP', GameWAD+':MTEXTURES\SKEL_SLEEP', 128, 128, 2);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 80%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_SLEEP_L', GameWAD+':MTEXTURES\SKEL_SLEEP_L', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_GO', GameWAD+':MTEXTURES\SKEL_GO', 128, 128, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_PAIN', GameWAD+':MTEXTURES\SKEL_PAIN', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_PAIN_L', GameWAD+':MTEXTURES\SKEL_PAIN_L', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_ATTACK', GameWAD+':MTEXTURES\SKEL_ATTACK', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_ATTACK_L', GameWAD+':MTEXTURES\SKEL_ATTACK_L', 128, 128, 2);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 85%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_ATTACK2', GameWAD+':MTEXTURES\SKEL_ATTACK2', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_ATTACK2_L', GameWAD+':MTEXTURES\SKEL_ATTACK2_L', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_SKEL_DIE', GameWAD+':MTEXTURES\SKEL_DIE', 128, 128, 5);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_SLEEP', GameWAD+':MTEXTURES\VILE_SLEEP', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_SLEEP_L', GameWAD+':MTEXTURES\VILE_SLEEP_L', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_GO', GameWAD+':MTEXTURES\VILE_GO', 128, 128, 6);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 90%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_PAIN', GameWAD+':MTEXTURES\VILE_PAIN', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_PAIN_L', GameWAD+':MTEXTURES\VILE_PAIN_L', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_ATTACK', GameWAD+':MTEXTURES\VILE_ATTACK', 128, 128, 10);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_ATTACK_L', GameWAD+':MTEXTURES\VILE_ATTACK_L', 128, 128, 10);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_ATTACK2', GameWAD+':MTEXTURES\VILE_ATTACK2', 128, 128, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_ATTACK2_L', GameWAD+':MTEXTURES\VILE_ATTACK2_L', 128, 128, 3);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 95%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_VILE_DIE', GameWAD+':MTEXTURES\VILE_DIE', 128, 128, 9);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ROBO_SLEEP', GameWAD+':MTEXTURES\ROBO_SLEEP', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ROBO_GO', GameWAD+':MTEXTURES\ROBO_GO', 128, 128, 12);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ROBO_ATTACK', GameWAD+':MTEXTURES\ROBO_ATTACK', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ROBO_ATTACK2', GameWAD+':MTEXTURES\ROBO_ATTACK2', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_ROBO_DIE', GameWAD+':MTEXTURES\ROBO_DIE', 128, 128, 1);
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_TEXTURES]+' 100%', 0, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_SLEEP', GameWAD+':MTEXTURES\CYBER_SLEEP', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_SLEEP_L', GameWAD+':MTEXTURES\CYBER_SLEEP_L', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_GO', GameWAD+':MTEXTURES\CYBER_GO', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_GO_L', GameWAD+':MTEXTURES\CYBER_GO_L', 128, 128, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_PAIN', GameWAD+':MTEXTURES\CYBER_PAIN', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_PAIN_L', GameWAD+':MTEXTURES\CYBER_PAIN_L', 128, 128, 1);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_ATTACK', GameWAD+':MTEXTURES\CYBER_ATTACK', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_ATTACK_L', GameWAD+':MTEXTURES\CYBER_ATTACK_L', 128, 128, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_MONSTER_CYBER_DIE', GameWAD+':MTEXTURES\CYBER_DIE', 128, 128, 9);
+
+  g_Game_SetLoadingText(_lc[I_LOAD_MONSTER_SOUNDS], 0, False);
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_BARREL_DIE', GameWAD+':MSOUNDS\BARREL_DIE');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_PAIN', GameWAD+':MSOUNDS\PAIN');
+  g_Sound_CreateWADEx('SOUND_MONSTER_PAIN2', GameWAD+':MSOUNDS\PAIN2');
+  g_Sound_CreateWADEx('SOUND_MONSTER_ACTION', GameWAD+':MSOUNDS\ACTION');
+  g_Sound_CreateWADEx('SOUND_MONSTER_ACTION2', GameWAD+':MSOUNDS\ACTION2');
+  g_Sound_CreateWADEx('SOUND_MONSTER_ALERT_1', GameWAD+':MSOUNDS\ALERT_1');
+  g_Sound_CreateWADEx('SOUND_MONSTER_ALERT_2', GameWAD+':MSOUNDS\ALERT_2');
+  g_Sound_CreateWADEx('SOUND_MONSTER_ALERT_3', GameWAD+':MSOUNDS\ALERT_3');
+  g_Sound_CreateWADEx('SOUND_MONSTER_DIE_1', GameWAD+':MSOUNDS\DIE_1');
+  g_Sound_CreateWADEx('SOUND_MONSTER_DIE_2', GameWAD+':MSOUNDS\DIE_2');
+  g_Sound_CreateWADEx('SOUND_MONSTER_DIE_3', GameWAD+':MSOUNDS\DIE_3');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SLOP', GameWAD+':MSOUNDS\SLOP');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_DEMON_ATTACK', GameWAD+':MSOUNDS\DEMON_ATTACK');
+  g_Sound_CreateWADEx('SOUND_MONSTER_DEMON_ALERT', GameWAD+':MSOUNDS\DEMON_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_DEMON_DIE', GameWAD+':MSOUNDS\DEMON_DIE');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_IMP_ALERT_1', GameWAD+':MSOUNDS\IMP_ALERT_1');
+  g_Sound_CreateWADEx('SOUND_MONSTER_IMP_ALERT_2', GameWAD+':MSOUNDS\IMP_ALERT_2');
+  g_Sound_CreateWADEx('SOUND_MONSTER_IMP_DIE_1', GameWAD+':MSOUNDS\IMP_DIE_1');
+  g_Sound_CreateWADEx('SOUND_MONSTER_IMP_DIE_2', GameWAD+':MSOUNDS\IMP_DIE_2');
+  g_Sound_CreateWADEx('SOUND_MONSTER_IMP_ACTION', GameWAD+':MSOUNDS\IMP_ACTION');
+  g_Sound_CreateWADEx('SOUND_MONSTER_IMP_ATTACK', GameWAD+':MSOUNDS\IMP_ATTACK');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_MAN_PAIN', GameWAD+':MSOUNDS\MAN_PAIN');
+  g_Sound_CreateWADEx('SOUND_MONSTER_MAN_ALERT', GameWAD+':MSOUNDS\MAN_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_MAN_DIE', GameWAD+':MSOUNDS\MAN_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_HAHA', GameWAD+':MSOUNDS\HAHA');
+  g_Sound_CreateWADEx('SOUND_MONSTER_TRUP', GameWAD+':MSOUNDS\TRUP');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_SOUL_ATTACK', GameWAD+':MSOUNDS\SOUL_ATTACK');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SOUL_DIE', GameWAD+':MSOUNDS\SOUL_DIE');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_BSP_ACTION', GameWAD+':MSOUNDS\BSP_ACTION');
+  g_Sound_CreateWADEx('SOUND_MONSTER_BSP_DIE', GameWAD+':MSOUNDS\BSP_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_BSP_ALERT', GameWAD+':MSOUNDS\BSP_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_BSP_WALK', GameWAD+':MSOUNDS\BSP_WALK');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_VILE_ACTION', GameWAD+':MSOUNDS\VILE_ACTION');
+  g_Sound_CreateWADEx('SOUND_MONSTER_VILE_PAIN', GameWAD+':MSOUNDS\VILE_PAIN');
+  g_Sound_CreateWADEx('SOUND_MONSTER_VILE_DIE', GameWAD+':MSOUNDS\VILE_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_VILE_ALERT', GameWAD+':MSOUNDS\VILE_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_VILE_ATTACK', GameWAD+':MSOUNDS\VILE_ATTACK');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_SKEL_ACTION', GameWAD+':MSOUNDS\SKEL_ACTION');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SKEL_DIE', GameWAD+':MSOUNDS\SKEL_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SKEL_ALERT', GameWAD+':MSOUNDS\SKEL_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SKEL_ATTACK', GameWAD+':MSOUNDS\SKEL_ATTACK');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SKEL_HIT', GameWAD+':MSOUNDS\SKEL_HIT');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_MANCUB_PAIN', GameWAD+':MSOUNDS\MANCUB_PAIN');
+  g_Sound_CreateWADEx('SOUND_MONSTER_MANCUB_DIE', GameWAD+':MSOUNDS\MANCUB_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_MANCUB_ALERT', GameWAD+':MSOUNDS\MANCUB_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_MANCUB_ATTACK', GameWAD+':MSOUNDS\MANCUB_ATTACK');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_PAIN_PAIN', GameWAD+':MSOUNDS\PAIN_PAIN');
+  g_Sound_CreateWADEx('SOUND_MONSTER_PAIN_DIE', GameWAD+':MSOUNDS\PAIN_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_PAIN_ALERT', GameWAD+':MSOUNDS\PAIN_ALERT');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_BARON_DIE', GameWAD+':MSOUNDS\BARON_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_BARON_ALERT', GameWAD+':MSOUNDS\BARON_ALERT');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_CACO_DIE', GameWAD+':MSOUNDS\CACO_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_CACO_ALERT', GameWAD+':MSOUNDS\CACO_ALERT');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_CYBER_DIE', GameWAD+':MSOUNDS\CYBER_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_CYBER_ALERT', GameWAD+':MSOUNDS\CYBER_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_CYBER_WALK', GameWAD+':MSOUNDS\CYBER_WALK');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_KNIGHT_DIE', GameWAD+':MSOUNDS\KNIGHT_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_KNIGHT_ALERT', GameWAD+':MSOUNDS\KNIGHT_ALERT');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_SPIDER_DIE', GameWAD+':MSOUNDS\SPIDER_DIE');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SPIDER_ALERT', GameWAD+':MSOUNDS\SPIDER_ALERT');
+  g_Sound_CreateWADEx('SOUND_MONSTER_SPIDER_WALK', GameWAD+':MSOUNDS\SPIDER_WALK');
+
+  g_Sound_CreateWADEx('SOUND_MONSTER_FISH_ATTACK', GameWAD+':MSOUNDS\FISH_ATTACK');
+end;
+
+procedure g_Monsters_FreeData();
+begin
+  e_WriteLog('Releasing monsters data...', MSG_NOTIFY);
+
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARREL_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARREL_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARREL_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ZOMBY_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ZOMBY_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ZOMBY_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ZOMBY_MESS');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ZOMBY_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ZOMBY_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SERG_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SERG_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SERG_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SERG_MESS');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SERG_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SERG_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MAN_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MAN_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MAN_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MAN_MESS');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MAN_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MAN_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_SLEEP_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_GO_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_MESS');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_ATTACK_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CGUN_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_IMP_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_IMP_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_IMP_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_IMP_MESS');
+  g_Frames_DeleteByName('FRAMES_MONSTER_IMP_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_IMP_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_DEMON_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_DEMON_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_DEMON_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_DEMON_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_DEMON_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SOUL_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SOUL_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SOUL_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SOUL_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SOUL_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_FISH_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_FISH_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_FISH_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_FISH_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_FISH_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SPIDER_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SPIDER_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SPIDER_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SPIDER_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SPIDER_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SPIDER_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BSP_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BSP_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BSP_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BSP_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BSP_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BSP_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CACO_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CACO_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CACO_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CACO_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CACO_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_PAIN_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_PAIN_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_PAIN_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_PAIN_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_PAIN_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_SLEEP_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_ATTACK_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_BARON_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_SLEEP_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_ATTACK_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_KNIGHT_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MANCUB_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MANCUB_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MANCUB_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MANCUB_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_MANCUB_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_SLEEP_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_ATTACK_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_ATTACK2');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_ATTACK2_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_SKEL_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_SLEEP_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_ATTACK_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_ATTACK2');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_ATTACK2_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_VILE_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ROBO_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ROBO_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ROBO_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ROBO_ATTACK2');
+  g_Frames_DeleteByName('FRAMES_MONSTER_ROBO_DIE');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_SLEEP');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_SLEEP_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_GO');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_GO_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_PAIN');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_PAIN_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_ATTACK');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_ATTACK_L');
+  g_Frames_DeleteByName('FRAMES_MONSTER_CYBER_DIE');
+
+  g_Sound_Delete('SOUND_MONSTER_BARREL_DIE');
+
+  g_Sound_Delete('SOUND_MONSTER_PAIN');
+  g_Sound_Delete('SOUND_MONSTER_PAIN2');
+  g_Sound_Delete('SOUND_MONSTER_ACTION');
+  g_Sound_Delete('SOUND_MONSTER_ACTION2');
+  g_Sound_Delete('SOUND_MONSTER_ALERT_1');
+  g_Sound_Delete('SOUND_MONSTER_ALERT_2');
+  g_Sound_Delete('SOUND_MONSTER_ALERT_3');
+  g_Sound_Delete('SOUND_MONSTER_DIE_1');
+  g_Sound_Delete('SOUND_MONSTER_DIE_2');
+  g_Sound_Delete('SOUND_MONSTER_DIE_3');
+  g_Sound_Delete('SOUND_MONSTER_SLOP');
+
+  g_Sound_Delete('SOUND_MONSTER_DEMON_ATTACK');
+  g_Sound_Delete('SOUND_MONSTER_DEMON_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_DEMON_DIE');
+
+  g_Sound_Delete('SOUND_MONSTER_IMP_ALERT_1');
+  g_Sound_Delete('SOUND_MONSTER_IMP_ALERT_2');
+  g_Sound_Delete('SOUND_MONSTER_IMP_DIE_1');
+  g_Sound_Delete('SOUND_MONSTER_IMP_DIE_2');
+  g_Sound_Delete('SOUND_MONSTER_IMP_ACTION');
+  g_Sound_Delete('SOUND_MONSTER_IMP_ATTACK');
+
+  g_Sound_Delete('SOUND_MONSTER_MAN_PAIN');
+  g_Sound_Delete('SOUND_MONSTER_MAN_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_MAN_DIE');
+  g_Sound_Delete('SOUND_MONSTER_HAHA');
+  g_Sound_Delete('SOUND_MONSTER_TRUP');
+
+  g_Sound_Delete('SOUND_MONSTER_SOUL_ATTACK');
+  g_Sound_Delete('SOUND_MONSTER_SOUL_DIE');
+
+  g_Sound_Delete('SOUND_MONSTER_BSP_ACTION');
+  g_Sound_Delete('SOUND_MONSTER_BSP_DIE');
+  g_Sound_Delete('SOUND_MONSTER_BSP_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_BSP_WALK');
+
+  g_Sound_Delete('SOUND_MONSTER_VILE_ACTION');
+  g_Sound_Delete('SOUND_MONSTER_VILE_PAIN');
+  g_Sound_Delete('SOUND_MONSTER_VILE_DIE');
+  g_Sound_Delete('SOUND_MONSTER_VILE_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_VILE_ATTACK');
+
+  g_Sound_Delete('SOUND_MONSTER_SKEL_ACTION');
+  g_Sound_Delete('SOUND_MONSTER_SKEL_DIE');
+  g_Sound_Delete('SOUND_MONSTER_SKEL_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_SKEL_ATTACK');
+  g_Sound_Delete('SOUND_MONSTER_SKEL_HIT');
+
+  g_Sound_Delete('SOUND_MONSTER_MANCUB_PAIN');
+  g_Sound_Delete('SOUND_MONSTER_MANCUB_DIE');
+  g_Sound_Delete('SOUND_MONSTER_MANCUB_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_MANCUB_ATTACK');
+
+  g_Sound_Delete('SOUND_MONSTER_PAIN_PAIN');
+  g_Sound_Delete('SOUND_MONSTER_PAIN_DIE');
+  g_Sound_Delete('SOUND_MONSTER_PAIN_ALERT');
+
+  g_Sound_Delete('SOUND_MONSTER_BARON_DIE');
+  g_Sound_Delete('SOUND_MONSTER_BARON_ALERT');
+
+  g_Sound_Delete('SOUND_MONSTER_CACO_DIE');
+  g_Sound_Delete('SOUND_MONSTER_CACO_ALERT');
+
+  g_Sound_Delete('SOUND_MONSTER_CYBER_DIE');
+  g_Sound_Delete('SOUND_MONSTER_CYBER_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_CYBER_WALK');
+
+  g_Sound_Delete('SOUND_MONSTER_KNIGHT_DIE');
+  g_Sound_Delete('SOUND_MONSTER_KNIGHT_ALERT');
+
+  g_Sound_Delete('SOUND_MONSTER_SPIDER_DIE');
+  g_Sound_Delete('SOUND_MONSTER_SPIDER_ALERT');
+  g_Sound_Delete('SOUND_MONSTER_SPIDER_WALK');
+
+  g_Sound_Delete('SOUND_MONSTER_FISH_ATTACK');
+end;
+
+procedure g_Monsters_Init();
+begin
+  soulcount := 0;
+end;
+
+procedure g_Monsters_Free();
+var
+  a: Integer;
+begin
+  if gMonsters <> nil then
+    for a := 0 to High(gMonsters) do
+      gMonsters[a].Free();
+
+  gMonsters := nil;
+end;
+
+function g_Monsters_Create(MonsterType: Byte; X, Y: Integer;
+           Direction: TDirection; AdjCoord: Boolean = False; ForcedUID: Integer = -1): Integer;
+var
+  find_id: DWORD;
+begin
+  Result := -1;
+
+// Íåò òàêîãî ìîíñòðà:
+  if (MonsterType > MONSTER_MAN) or (MonsterType = 0) then
+    Exit;
+
+// Ñîáëþäàåì îãðàíè÷åíèå Lost_Soul'îâ:
+  if MonsterType = MONSTER_SOUL then
+    if soulcount > MAX_SOUL then
+      Exit
+    else
+      soulcount := soulcount + 1;
+
+  find_id := FindMonster();
+
+  gMonsters[find_id] := TMonster.Create(MonsterType, find_id, ForcedUID);
+
+// Íàñòðàèâàåì ïîëîæåíèå:
+  with gMonsters[find_id] do
+  begin
+    if AdjCoord then
+      begin
+        FObj.X := X-FObj.Rect.X - (FObj.Rect.Width div 2);
+        FObj.Y := Y-FObj.Rect.Y - FObj.Rect.Height;
+      end
+    else
+      begin
+        FObj.X := X-FObj.Rect.X;
+        FObj.Y := Y-FObj.Rect.Y;
+      end;
+
+    FDirection := Direction;
+    FStartDirection := Direction;
+    FStartX := GameX;
+    FStartY := GameY;
+  end;
+
+  Result := find_id;
+end;
+
+procedure g_Monsters_killedp();
+var
+  a, h: Integer;
+begin
+  if gMonsters = nil then
+    Exit;
+
+// Ïðèêîëèñò ñìååòñÿ íàä ñìåðòüþ èãðîêà:
+  h := High(gMonsters);
+  for a := 0 to h do
+    if (gMonsters[a] <> nil) then
+      with gMonsters[a] do
+        if (FMonsterType = MONSTER_MAN) and
+           (FState <> STATE_DEAD) and
+           (FState <> STATE_SLEEP) and
+           (FState <> STATE_DIE) then
+        begin
+          g_Sound_PlayExAt('SOUND_MONSTER_TRUP', FObj.X, FObj.Y);
+          Exit;
+        end;
+end;
+
+procedure g_Monsters_Update();
+var
+  a: Integer;
+begin
+// Öåëåóêàçàòåëü:
+  if gTime mod (GAME_TICK*2) = 0 then
+  begin
+    pt_x := pt_x+pt_xs;
+    pt_y := pt_y+pt_ys;
+    if Abs(pt_x) > 246 then
+      pt_xs := -pt_xs;
+    if Abs(pt_y) > 100 then
+      pt_ys := -pt_ys;
+  end;
+
+  gMon := True; // Äëÿ ðàáîòû BlockMon'à
+
+  if gMonsters <> nil then
+    for a := 0 to High(gMonsters) do
+      if (gMonsters[a] <> nil) then
+        if not gMonsters[a].FRemoved then
+        begin
+          if g_Game_IsClient then
+            gMonsters[a].ClientUpdate()
+          else
+            gMonsters[a].Update();
+        end
+        else
+          begin 
+            gMonsters[a].Free();
+            gMonsters[a] := nil;
+          end;
+
+  gMon := False;
+end;
+
+procedure g_Monsters_Draw();
+var
+  a: Integer;
+begin
+  if gMonsters <> nil then
+    for a := 0 to High(gMonsters) do
+      if gMonsters[a] <> nil then
+        gMonsters[a].Draw();
+end;
+
+procedure g_Monsters_DrawHealth();
+var
+  a: Integer;
+  fW, fH: Byte;
+begin
+  if gMonsters = nil then Exit;
+  e_TextureFontGetSize(gStdFont, fW, fH);
+
+  for a := 0 to High(gMonsters) do
+    if gMonsters[a] <> nil then
+    begin
+      e_TextureFontPrint(gMonsters[a].FObj.X + gMonsters[a].FObj.Rect.X,
+      gMonsters[a].FObj.Y + gMonsters[a].FObj.Rect.Y + gMonsters[a].FObj.Rect.Height - fH,
+      IntToStr(gMonsters[a].FHealth), gStdFont);
+    end;
+end;
+
+function g_Monsters_Get(UID: Word): TMonster;
+var
+  a: Integer;
+begin
+  Result := nil;
+
+  if gMonsters <> nil then
+    for a := 0 to High(gMonsters) do
+      if (gMonsters[a] <> nil) and
+         (gMonsters[a].FUID = UID) then
+      begin
+        Result := gMonsters[a];
+        Break;
+      end;
+end;
+
+procedure g_Monsters_SaveState(var Mem: TBinMemoryWriter);
+var
+  count, i: Integer;
+  b: Byte;
+begin
+// Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ ìîíñòðîâ:
+  count := 0;
+  if gMonsters <> nil then
+    for i := 0 to High(gMonsters) do
+      if gMonsters[i] <> nil then
+        if gMonsters[i].FMonsterType <> MONSTER_NONE then
+          count := count + 1;
+
+  Mem := TBinMemoryWriter.Create((count+1) * 350);
+
+// Ñîõðàíÿåì èíôîðìàöèþ öåëåóêàçàòåëÿ:
+  Mem.WriteInt(pt_x);
+  Mem.WriteInt(pt_xs);
+  Mem.WriteInt(pt_y);
+  Mem.WriteInt(pt_ys);
+
+// Êîëè÷åñòâî ìîíñòðîâ:
+  Mem.WriteInt(count);
+
+  if count = 0 then
+    Exit;
+
+// Ñîõðàíÿåì ìîíñòðîâ:
+  for i := 0 to High(gMonsters) do
+    if gMonsters[i] <> nil then
+      if gMonsters[i].FMonsterType <> MONSTER_NONE then
+      begin
+      // Òèï ìîíñòðà:
+        b := gMonsters[i].MonsterType;
+        Mem.WriteByte(b);
+      // Ñîõðàíÿåì äàííûå ìîíñòðà:
+        gMonsters[i].SaveState(Mem);
+      end;
+end;
+
+procedure g_Monsters_LoadState(var Mem: TBinMemoryReader);
+var
+  count, i, a: Integer;
+  b: Byte;
+begin
+  if Mem = nil then
+    Exit;
+
+  g_Monsters_Free();
+
+// Çàãðóæàåì èíôîðìàöèþ öåëåóêàçàòåëÿ:
+  Mem.ReadInt(pt_x);
+  Mem.ReadInt(pt_xs);
+  Mem.ReadInt(pt_y);
+  Mem.ReadInt(pt_ys);
+
+// Êîëè÷åñòâî ìîíñòðîâ:
+  Mem.ReadInt(count);
+
+  if count = 0 then
+    Exit;
+
+// Çàãðóæàåì ìîíñòðîâ:
+  for a := 0 to count-1 do
+  begin
+  // Òèï ìîíñòðà:
+    Mem.ReadByte(b);
+  // Ñîçäàåì ìîíñòðà:
+    i := g_Monsters_Create(b, 0, 0, D_LEFT);
+    if i < 0 then
+    begin
+      raise EBinSizeError.Create('g_Monsters_LoadState: ID = -1 (Can''t create)');
+    end;
+  // Çàãðóæàåì äàííûå ìîíñòðà:
+    gMonsters[i].LoadState(Mem);
+  end;
+end;
+
+function g_Monsters_GetIDByName(name: String): Integer;
+var
+  i: Integer;
+begin
+  name := UpperCase(name);
+  i := MONSTER_DEMON;
+  while (i <= MONSTER_MAN) do
+  begin
+    if name = MONSTERTABLE[i].Name then
+    begin
+      Result := i;
+      Exit;
+    end;
+    Inc(i);
+  end;
+
+  Result := -1;
+end;
+
+function g_Monsters_GetNameByID(MonsterType: Byte): String;
+begin
+  if MonsterType in [MONSTER_DEMON..MONSTER_MAN] then
+    Result := MONSTERTABLE[MonsterType].Name;
+end;
+
+function g_Monsters_GetKilledBy(MonsterType: Byte): String;
+begin
+  if MonsterType in [MONSTER_DEMON..MONSTER_MAN] then
+    Result := KilledByMonster[MonsterType]
+  else
+    Result := '?';
+end;
+
+{ T M o n s t e r : }
+
+procedure TMonster.ActionSound();
+begin
+  case FMonsterType of
+    MONSTER_IMP:
+      g_Sound_PlayExAt('SOUND_MONSTER_IMP_ACTION', FObj.X, FObj.Y);
+    MONSTER_ZOMBY, MONSTER_SERG, MONSTER_CGUN,
+    MONSTER_MANCUB:
+      g_Sound_PlayExAt('SOUND_MONSTER_ACTION', FObj.X, FObj.Y);
+    MONSTER_SOUL, MONSTER_BARON, MONSTER_CACO,
+    MONSTER_KNIGHT, MONSTER_PAIN, MONSTER_DEMON,
+    MONSTER_SPIDER:
+      g_Sound_PlayExAt('SOUND_MONSTER_ACTION2', FObj.X, FObj.Y);
+    MONSTER_BSP:
+      g_Sound_PlayExAt('SOUND_MONSTER_BSP_ACTION', FObj.X, FObj.Y);
+    MONSTER_VILE:
+      g_Sound_PlayExAt('SOUND_MONSTER_VILE_ACTION', FObj.X, FObj.Y);
+    MONSTER_SKEL:
+      g_Sound_PlayExAt('SOUND_MONSTER_SKEL_ACTION', FObj.X, FObj.Y);
+    MONSTER_CYBER:
+      ;
+    MONSTER_MAN:
+      g_Sound_PlayExAt('SOUND_MONSTER_HAHA', FObj.X, FObj.Y);
+  end;
+end;
+
+procedure TMonster.PainSound();
+begin
+  if FPainSound then
+    Exit;
+
+  FPainSound := True;
+
+  case FMonsterType of
+    MONSTER_IMP, MONSTER_ZOMBY, MONSTER_SERG,
+    MONSTER_SKEL, MONSTER_CGUN:
+      g_Sound_PlayExAt('SOUND_MONSTER_PAIN', FObj.X, FObj.Y);
+    MONSTER_SOUL, MONSTER_BARON, MONSTER_CACO,
+    MONSTER_KNIGHT, MONSTER_DEMON, MONSTER_SPIDER,
+    MONSTER_CYBER:
+      g_Sound_PlayExAt('SOUND_MONSTER_PAIN2', FObj.X, FObj.Y);
+    MONSTER_VILE:
+      g_Sound_PlayExAt('SOUND_MONSTER_VILE_PAIN', FObj.X, FObj.Y);
+    MONSTER_MANCUB:
+      g_Sound_PlayExAt('SOUND_MONSTER_MANCUB_PAIN', FObj.X, FObj.Y);
+    MONSTER_PAIN:
+      g_Sound_PlayExAt('SOUND_MONSTER_PAIN_PAIN', FObj.X, FObj.Y);
+    MONSTER_MAN:
+      g_Sound_PlayExAt('SOUND_MONSTER_MAN_PAIN', FObj.X, FObj.Y);
+  end;
+end;
+
+procedure TMonster.DieSound();
+begin
+  case FMonsterType of
+    MONSTER_IMP:
+      case Random(2) of
+        0: g_Sound_PlayExAt('SOUND_MONSTER_IMP_DIE_1', FObj.X, FObj.Y);
+        1: g_Sound_PlayExAt('SOUND_MONSTER_IMP_DIE_2', FObj.X, FObj.Y);
+      end;
+    MONSTER_ZOMBY, MONSTER_SERG, MONSTER_CGUN:
+      case Random(3) of
+        0: g_Sound_PlayExAt('SOUND_MONSTER_DIE_1', FObj.X, FObj.Y);
+        1: g_Sound_PlayExAt('SOUND_MONSTER_DIE_2', FObj.X, FObj.Y);
+        2: g_Sound_PlayExAt('SOUND_MONSTER_DIE_3', FObj.X, FObj.Y);
+      end;
+    MONSTER_DEMON:
+      g_Sound_PlayExAt('SOUND_MONSTER_DEMON_DIE', FObj.X, FObj.Y);
+    MONSTER_BARREL:
+      g_Sound_PlayExAt('SOUND_MONSTER_BARREL_DIE', FObj.X, FObj.Y);
+    MONSTER_SOUL:
+      g_Sound_PlayExAt('SOUND_MONSTER_SOUL_DIE', FObj.X, FObj.Y);
+    MONSTER_BSP:
+      g_Sound_PlayExAt('SOUND_MONSTER_BSP_DIE', FObj.X, FObj.Y);
+    MONSTER_VILE:
+      g_Sound_PlayExAt('SOUND_MONSTER_VILE_DIE', FObj.X, FObj.Y);
+    MONSTER_BARON:
+      g_Sound_PlayExAt('SOUND_MONSTER_BARON_DIE', FObj.X, FObj.Y);
+    MONSTER_CACO:
+      g_Sound_PlayExAt('SOUND_MONSTER_CACO_DIE', FObj.X, FObj.Y);
+    MONSTER_CYBER:
+      g_Sound_PlayExAt('SOUND_MONSTER_CYBER_DIE', FObj.X, FObj.Y);
+    MONSTER_KNIGHT:
+      g_Sound_PlayExAt('SOUND_MONSTER_KNIGHT_DIE', FObj.X, FObj.Y);
+    MONSTER_MANCUB:
+      g_Sound_PlayExAt('SOUND_MONSTER_MANCUB_DIE', FObj.X, FObj.Y);
+    MONSTER_PAIN:
+      g_Sound_PlayExAt('SOUND_MONSTER_PAIN_DIE', FObj.X, FObj.Y);
+    MONSTER_SKEL:
+      g_Sound_PlayExAt('SOUND_MONSTER_SKEL_DIE', FObj.X, FObj.Y);
+    MONSTER_SPIDER:
+      g_Sound_PlayExAt('SOUND_MONSTER_SPIDER_DIE', FObj.X, FObj.Y);
+    MONSTER_MAN:
+      g_Sound_PlayExAt('SOUND_MONSTER_MAN_DIE', FObj.X, FObj.Y);
+  end;
+end;
+
+procedure TMonster.WakeUpSound();
+begin
+  case FMonsterType of
+    MONSTER_IMP:
+      case Random(2) of
+        0: g_Sound_PlayExAt('SOUND_MONSTER_IMP_ALERT_1', FObj.X, FObj.Y);
+        1: g_Sound_PlayExAt('SOUND_MONSTER_IMP_ALERT_2', FObj.X, FObj.Y);
+      end;
+    MONSTER_ZOMBY, MONSTER_SERG, MONSTER_CGUN:
+      case Random(3) of
+        0: g_Sound_PlayExAt('SOUND_MONSTER_ALERT_1', FObj.X, FObj.Y);
+        1: g_Sound_PlayExAt('SOUND_MONSTER_ALERT_2', FObj.X, FObj.Y);
+        2: g_Sound_PlayExAt('SOUND_MONSTER_ALERT_3', FObj.X, FObj.Y);
+      end;
+    MONSTER_MAN:
+      g_Sound_PlayExAt('SOUND_MONSTER_MAN_ALERT', FObj.X, FObj.Y);
+    MONSTER_BSP:
+      g_Sound_PlayExAt('SOUND_MONSTER_BSP_ALERT', FObj.X, FObj.Y);
+    MONSTER_VILE:
+      g_Sound_PlayExAt('SOUND_MONSTER_VILE_ALERT', FObj.X, FObj.Y);
+    MONSTER_BARON:
+      g_Sound_PlayExAt('SOUND_MONSTER_BARON_ALERT', FObj.X, FObj.Y);
+    MONSTER_CACO:
+      g_Sound_PlayExAt('SOUND_MONSTER_CACO_ALERT', FObj.X, FObj.Y);
+    MONSTER_CYBER:
+      g_Sound_PlayExAt('SOUND_MONSTER_CYBER_ALERT', FObj.X, FObj.Y);
+    MONSTER_KNIGHT:
+      g_Sound_PlayExAt('SOUND_MONSTER_KNIGHT_ALERT', FObj.X, FObj.Y);
+    MONSTER_MANCUB:
+      g_Sound_PlayExAt('SOUND_MONSTER_MANCUB_ALERT', FObj.X, FObj.Y);
+    MONSTER_PAIN:
+      g_Sound_PlayExAt('SOUND_MONSTER_PAIN_ALERT', FObj.X, FObj.Y);
+    MONSTER_DEMON:
+      g_Sound_PlayExAt('SOUND_MONSTER_DEMON_ALERT', FObj.X, FObj.Y);
+    MONSTER_SKEL:
+      g_Sound_PlayExAt('SOUND_MONSTER_SKEL_ALERT', FObj.X, FObj.Y);
+    MONSTER_SPIDER:
+      g_Sound_PlayExAt('SOUND_MONSTER_SPIDER_ALERT', FObj.X, FObj.Y);
+    MONSTER_SOUL:
+      ;
+  end;
+end;
+
+procedure TMonster.BFGHit();
+begin
+  if FMonsterType = MONSTER_FISH then
+    Exit;
+
+  g_Weapon_BFGHit(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                  FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2));
+  {if g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_Effect(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                   FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                   0, NET_GFX_BFG);}
+end;
+
+function TMonster.Collide(X, Y: Integer; Width, Height: Word): Boolean;
+begin
+  Result := g_Collide(FObj.X+FObj.Rect.X,
+                      FObj.Y+FObj.Rect.Y,
+                      FObj.Rect.Width,
+                      FObj.Rect.Height,
+                      X, Y,
+                      Width, Height);
+end;
+
+function TMonster.Collide(Panel: TPanel): Boolean;
+begin
+  Result := g_Collide(FObj.X+FObj.Rect.X,
+                      FObj.Y+FObj.Rect.Y,
+                      FObj.Rect.Width,
+                      FObj.Rect.Height,
+                      Panel.X, Panel.Y,
+                      Panel.Width, Panel.Height);
+end;
+
+function TMonster.Collide(X, Y: Integer): Boolean;
+begin
+  X := X - FObj.X - FObj.Rect.X;
+  Y := Y - FObj.Y - FObj.Rect.Y;
+  Result := (x >= 0) and (x <= FObj.Rect.Width) and
+            (y >= 0) and (y <= FObj.Rect.Height);
+end;
+
+procedure TMonster.Respawn;
+begin
+  FObj.Vel.X := 0;
+  FObj.Vel.Y := 0;
+  FObj.Accel.X := 0;
+  FObj.Accel.Y := 0;
+  FDirection := FStartDirection;
+  GameX := FStartX;
+  GameY := FStartY;
+  FObj.Rect := MONSTERTABLE[FMonsterType].Rect;
+  FHealth := MONSTERTABLE[FMonsterType].Health;
+  FAmmo := 0;
+  FPain := 0;
+  FTargetUID := 0;
+  FTargetTime := 0;
+  FDieTriggers := nil;
+  FWaitAttackAnim := False;
+  FChainFire := False;
+  FShellTimer := -1;
+  
+  FState := STATE_SLEEP;
+  FCurAnim := ANIM_SLEEP;
+
+  if g_Game_IsNet and g_Game_IsServer then
+  begin
+    MH_SEND_MonsterPos(FUID);
+    MH_SEND_MonsterState(FUID);
+  end;
+end;
+
+constructor TMonster.Create(MonsterType: Byte; aID: Integer; ForcedUID: Integer = -1);
+var
+  a: Integer;
+  FramesID: DWORD;
+  s: String;
+  res: Boolean;
+begin
+  if ForcedUID < 0 then
+    FUID := g_CreateUID(UID_MONSTER)
+  else
+    FUID := ForcedUID;
+
+  FMonsterType := MonsterType;
+
+  g_Obj_Init(@FObj);
+
+  FState := STATE_SLEEP;
+  FCurAnim := ANIM_SLEEP;
+  FHealth := MONSTERTABLE[MonsterType].Health;
+  FMaxHealth := FHealth;
+  FObj.Rect := MONSTERTABLE[MonsterType].Rect;
+  FDieTriggers := nil;
+  FSpawnTrigger := -1;
+  FWaitAttackAnim := False;
+  FChainFire := False;
+  FStartID := aID;
+  FNoRespawn := False;
+  FShellTimer := -1;
+  FBehaviour := BH_NORMAL;
+
+  if FMonsterType in [MONSTER_ROBO, MONSTER_BARREL] then
+    FBloodKind := BLOOD_SPARKS
+  else
+    FBloodKind := BLOOD_NORMAL;
+  if FMonsterType = MONSTER_CACO then
+  begin
+    FBloodRed := 0;
+    FBloodGreen := 0;
+    FBloodBlue := 150;
+  end
+  else if FMonsterType in [MONSTER_BARON, MONSTER_KNIGHT] then
+  begin
+    FBloodRed := 0;
+    FBloodGreen := 150;
+    FBloodBlue := 0;
+  end
+  else
+  begin
+    FBloodRed := 150;
+    FBloodGreen := 0;
+    FBloodBlue := 0;
+  end;
+
+  SetLength(FAnim, Length(ANIMTABLE));
+
+  for a := 0 to High(FAnim) do
+  begin
+    FAnim[a, D_LEFT] := nil;
+    FAnim[a, D_RIGHT] := nil;
+  end;
+
+  for a := ANIM_SLEEP to ANIM_PAIN do
+    if (ANIMTABLE[a].name <> '') and
+       (MONSTER_ANIMTABLE[MonsterType].AnimSpeed[a] <> 0) then
+    begin
+      s := 'FRAMES_MONSTER_'+MONSTERTABLE[MonsterType].Name+
+           '_'+ANIMTABLE[a].name;
+
+      res := g_Frames_Exists(s);
+
+      if res then
+        res := g_Frames_Get(FramesID, s);
+
+    // Åñëè íåò òàêîé àíèìàöèè, òî ïðîáóåì çàìåíèòü åå íà àíèìàöèþ ñìåðòè:
+      if (not res) then
+      begin
+      // Çàìåíÿåì òîëüêî ANIM_MESS íà ANIM_DIE:
+        if a <> ANIM_MESS then
+          Continue;
+
+        if g_Frames_Get(FramesID, 'FRAMES_MONSTER_'+MONSTERTABLE[MonsterType].Name+
+                        '_'+ANIMTABLE[ANIM_DIE].name) then
+        begin
+          FAnim[a, D_RIGHT] := TAnimation.Create(FramesID, ANIMTABLE[ANIM_DIE].loop,
+                                                 MONSTER_ANIMTABLE[MonsterType].AnimSpeed[ANIM_DIE]);
+          FAnim[a, D_LEFT] := TAnimation.Create(FramesID, ANIMTABLE[ANIM_DIE].loop,
+                                                MONSTER_ANIMTABLE[MonsterType].AnimSpeed[ANIM_DIE]);
+          Continue;
+        end;
+      end;
+
+      FAnim[a, D_RIGHT] := TAnimation.Create(FramesID, ANIMTABLE[a].loop,
+                                             MONSTER_ANIMTABLE[MonsterType].AnimSpeed[a]);
+
+    // Åñëè åñòü îòäåëüíàÿ ëåâàÿ àíèìàöèÿ - çàãðóæàåì:
+      if MONSTER_ANIMTABLE[MonsterType].LeftAnim then
+      begin
+        s := 'FRAMES_MONSTER_'+MONSTERTABLE[MonsterType].Name+
+             '_'+ANIMTABLE[a].name+'_L';
+        if g_Frames_Exists(s) then
+          g_Frames_Get(FramesID, s);
+      end;
+
+      FAnim[a, D_LEFT] := TAnimation.Create(FramesID, ANIMTABLE[a].loop,
+                                            MONSTER_ANIMTABLE[MonsterType].AnimSpeed[a]);
+    end;
+
+// Äëÿ êîëäóíà çàãðóæàåì òàêæå àíèìàöèþ îãíÿ:
+  if MonsterType = MONSTER_VILE then
+    begin
+      g_Frames_Get(FramesID, 'FRAMES_FIRE');
+      vilefire := TAnimation.Create(FramesID, True, 2);
+    end
+  else
+    vilefire := nil;
+end;
+
+function TMonster.Damage(Damage: Word; VelX, VelY: Integer; SpawnerUID: Word; t: Byte): Boolean;
+var
+  c, it: Integer;
+  p: TPlayer;
+begin
+  Result := False;
+
+// Óìèðàåò, óìåð èëè âîñêðåøàåòñÿ => óðîí äåëàòü íåêîìó:
+  if (FState = STATE_DEAD) or (FState = STATE_DIE) or (FState = STATE_REVIVE) then
+    Exit;
+
+// Ðûáó â âîäå áüåò òîêîì => ïàíèêà áåç óðîíà:
+  if (t = HIT_ELECTRO) and (FMonsterType = MONSTER_FISH) and g_Game_IsServer then
+  begin
+    FSleep := 20;
+    if Random(2) = 0 then
+      FDirection := D_RIGHT
+    else
+      FDirection := D_LEFT;
+    Result := True;
+    SetState(STATE_RUN);
+    Exit;
+  end;
+
+// Ëîâóøêà óáèâàåò ñðàçó:
+  if t = HIT_TRAP then
+    FHealth := -100;
+
+// Ðîáîòó óðîíà íåò:
+  if FMonsterType = MONSTER_ROBO then
+    Damage := 0;
+
+// Íàíîñèì óðîí:
+  if g_Game_IsServer then Dec(FHealth, Damage);
+
+// Óñèëèâàåì áîëü ìîíñòðà îò óðîíà:
+  if FPain = 0 then
+    FPain := 3;
+  FPain := FPain+Damage;
+
+// Åñëè áîëü ñóùåñòâåííàÿ, òî ìåíÿåì ñîñòîÿíèå íà áîëåâîå:
+  if FState <> STATE_PAIN then
+    if (FPain >= MONSTERTABLE[FMonsterType].MinPain) and
+       (FMonsterType <> MONSTER_BARREL) then
+         SetState(STATE_PAIN);
+
+// Åñëè ðàçðåøåíà êðîâü - ñîçäàåì áðûçãè êðîâè:
+  if (gBloodCount > 0) then
+  begin
+    c := Min(Damage, 200);
+    c := c*gBloodCount - (Damage div 4) + Random(c div 2);
+
+    if (VelX = 0) and (VelY = 0) then
+      MakeBloodSimple(c)
+    else
+      case t of
+        HIT_TRAP, HIT_ACID, HIT_ELECTRO, HIT_FLAME: MakeBloodSimple(c);
+        HIT_BFG, HIT_ROCKET, HIT_SOME: MakeBloodVector(c, VelX, VelY);
+      end;
+  end;
+
+// Òåïåðü öåëü - óäàðèâøèé, åñëè òîëüêî íå ñàì ñåáÿ:
+  if (SpawnerUID <> FUID) and
+  (BehaviourDamage(SpawnerUID, FBehaviour, FMonsterType)) then
+  begin
+    FTargetUID := SpawnerUID;
+    FTargetTime := 0;
+  end;
+
+// Çäîðîâüå çàêîí÷èëîñü:
+  if FHealth <= 0 then
+    begin
+    // Åñëè ýòî íå áî÷êà è óáèë èãðîê, òî åìó +1:
+      if (FMonsterType <> MONSTER_BARREL) then
+      begin
+        if (g_GetUIDType(SpawnerUID) = UID_PLAYER) then
+        begin
+          p := g_Player_Get(SpawnerUID);
+          if (p <> nil) and (gLMSRespawn = LMS_RESPAWN_NONE) then
+          begin
+            p.MonsterKills := p.MonsterKills+1;
+            if gGameSettings.GameMode = GM_COOP then
+              p.Frags := p.Frags + 1;
+            // Uncomment this if you want to double-kill monsters
+            //p.FragCombo();
+          end;
+        end;
+        if gLMSRespawn = LMS_RESPAWN_NONE then
+        begin
+          Inc(gCoopMonstersKilled);
+          if g_Game_IsNet then
+            MH_SEND_GameStats;
+        end;
+      end;
+
+    // Âûáèðàåì ëóò:
+      case FMonsterType of
+        MONSTER_ZOMBY: c := ITEM_AMMO_BULLETS;
+        MONSTER_SERG: c := ITEM_WEAPON_SHOTGUN1;
+        MONSTER_CGUN: c := ITEM_WEAPON_CHAINGUN;
+        MONSTER_MAN: c := ITEM_KEY_RED;
+        else c := 0;
+      end;
+
+    // Áðîñàåì ëóò:
+      if c <> 0 then
+      begin
+        it := g_Items_Create(FObj.X + (FObj.Rect.Width div 2),
+                             FObj.Y + (FObj.Rect.Height div 2),
+                             c, True, False);
+        g_Obj_Push(@gItems[it].Obj, (FObj.Vel.X div 2)-3+Random(7),
+                                    (FObj.Vel.Y div 2)-Random(4));
+        if g_Game_IsServer and g_Game_IsNet then
+          MH_SEND_ItemSpawn(True, it);
+      end;
+
+    // Òðóï äàëüøå íå èäåò:
+      FObj.Vel.X := 0;
+
+    // Ó òðóïà ðàçìåðû ìåíüøå:
+      if (FMonsterType <> MONSTER_FISH) and (FMonsterType <> MONSTER_PAIN) then
+      begin
+        FObj.Rect.Y := FObj.Rect.Y + FObj.Rect.Height-12;
+        FObj.Rect.Height := 12;
+      end;
+
+    // Óðîí áûë ñèëüíûì => ñëàáûå - â êàøó:
+      if (FHealth <= -30) and
+         ((FMonsterType = MONSTER_IMP) or (FMonsterType = MONSTER_ZOMBY) or
+          (FMonsterType = MONSTER_SERG) or (FMonsterType = MONSTER_CGUN) or
+          (FMonsterType = MONSTER_MAN)) then
+        begin
+          g_Sound_PlayExAt('SOUND_MONSTER_SLOP', FObj.X, FObj.Y);
+          SetState(STATE_DIE, ANIM_MESS);
+        end
+      else
+        begin
+          DieSound();
+          SetState(STATE_DIE);
+        end;
+
+    // Àêòèâèðîâàòü òðèããåðû, æäóùèå ñìåðòè ýòîãî ìîíñòðà:
+      if g_Game_IsServer then ActivateTriggers();
+
+      FHealth := 0;
+    end
+  else
+    if FState = STATE_SLEEP then
+    begin // Ñïàë, ðàçáóäèëè íåñìåðòåëüíûì óäàðîì:
+      FPain := MONSTERTABLE[FMonsterType].Pain;
+      SetState(STATE_GO);
+    end;
+
+  if g_Game_IsServer and g_Game_IsNet then MH_SEND_MonsterState(FUID);
+  Result := True;
+end;
+
+function TMonster.Heal(Value: Word): Boolean;
+begin
+  Result := False;
+  if g_Game_IsClient then
+    Exit;
+  if not Live then
+    Exit;
+
+  if FHealth < FMaxHealth then
+  begin
+    IncMax(FHealth, Value, FMaxHealth);
+    if g_Game_IsServer and g_Game_IsNet then
+      MH_SEND_MonsterState(FUID);
+    Result := True;
+  end;
+end;
+
+destructor TMonster.Destroy();
+var
+  a: Integer;
+begin
+  for a := 0 to High(FAnim) do
+  begin
+    FAnim[a, D_LEFT].Free();
+    FAnim[a, D_RIGHT].Free();
+  end;
+
+  vilefire.Free();
+
+  inherited Destroy();
+end;
+
+procedure TMonster.Draw();
+var
+  m: TMirrorType;
+  dx, dy, c: Integer;
+  o: TObj;
+begin
+  //e_CharFont_Print(gMenuSmallFont, Obj.X+Obj.Rect.X, Obj.Y+Obj.Rect.Y, 'TYPE: '+IntToStr(FMonsterType));
+  //e_CharFont_Print(gMenuSmallFont, Obj.X+Obj.Rect.X, Obj.Y+Obj.Rect.Y+16, 'STATE: '+IntToStr(FState));
+
+// Åñëè êîëäóí ñòðåëÿåò, òî ðèñóåì îãîíü:
+  if FMonsterType = MONSTER_VILE then
+    if FState = STATE_SHOOT then
+      if GetPos(FTargetUID, @o) then
+        vilefire.Draw(o.X+o.Rect.X+(o.Rect.Width div 2)-32,
+                      o.Y+o.Rect.Y+o.Rect.Height-128, M_NONE);
+
+// Íå â îáëàñòè ðèñîâàíèÿ íå ðåñóåì:
+  if not g_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y, FObj.Rect.Width, FObj.Rect.Height,
+                   sX-128, sY-128, sWidth+256, sHeight+256) then
+    Exit;
+
+// Ýòè ìîíñòðû, óìèðàÿ, íå îñòàâëÿþò òðóïîâ:
+  if FState = STATE_DEAD then
+    case FMonsterType of
+      MONSTER_BARREL, MONSTER_SOUL, MONSTER_PAIN: Exit;
+    end;
+
+// Åñòü ÷òî ðèñîâàòü ïðè òåêóùåì ïîâåäåíèè:
+  if FAnim[FCurAnim, FDirection] <> nil then
+  begin
+  // Åñëè íåò ëåâîé àíèìàöèè èëè îíà ñîâïàäàåò ñ ïðàâîé => îòðàæàåì ïðàâóþ:
+    if (FDirection = D_LEFT) and
+       ((not MONSTER_ANIMTABLE[FMonsterType].LeftAnim) or
+        (FAnim[FCurAnim, D_LEFT].FramesID = FAnim[FCurAnim, D_RIGHT].FramesID)) and
+        (FMonsterType <> MONSTER_BARREL) then
+      m := M_HORIZONTAL
+    else
+      m := M_NONE;
+
+  // Ëåâàÿ àíèìàöèÿ => ìåíÿåì ñìåùåíèå îòíîñèòåëüíî öåíòðà:
+    if (FDirection = D_LEFT) and
+       (FMonsterType <> MONSTER_BARREL) then
+      begin
+        dx := MONSTER_ANIMTABLE[FMonsterType].AnimDeltaLeft[FCurAnim].X;
+        dy := MONSTER_ANIMTABLE[FMonsterType].AnimDeltaLeft[FCurAnim].Y;
+
+        if m = M_HORIZONTAL then
+        begin // Íåò îòäåëüíîé ëåâîé àíèìàöèè
+        // Ðàññòîÿíèå îò êðàÿ òåêñòóðû äî êðàÿ âèçóàëüíîãî ïîëîæåíèÿ îáúåêòà íà òåêñòóðå:
+          c := (MONSTERTABLE[FMonsterType].Rect.X - dx) + MONSTERTABLE[FMonsterType].Rect.Width;
+        // Ðàññòîÿíèå îò êðàÿ õèò áîêñà äî êðàÿ âèçóàëüíîãî ïîëîæåíèÿ îáúåêòà íà òåêñòóðå:
+          dx := FAnim[FCurAnim, FDirection].Width - c - MONSTERTABLE[FMonsterType].Rect.X;
+        // Ò.ê. äâèãàòü òåêñòóðó íóæíî áóäåò â ïðîòèâîïîëîæíîì íàïðàâëåíèè:
+          dx := -dx;
+        // Ýòî çíà÷èò: dX := -frameWidth - animDeltaX + hitX + hitWidth + hitX
+        end;
+      end
+    else // Ïðàâàÿ àíèìàöèÿ                    
+      begin
+        dx := MONSTER_ANIMTABLE[FMonsterType].AnimDeltaRight[FCurAnim].X;
+        dy := MONSTER_ANIMTABLE[FMonsterType].AnimDeltaRight[FCurAnim].Y;
+      end;
+
+  // Ðèñóåì:
+    FAnim[FCurAnim, FDirection].Draw(Obj.X+dx, Obj.Y+dy, m);
+  end;
+
+  if g_debug_Frames then
+  begin
+    e_DrawQuad(FObj.X+FObj.Rect.X,
+               FObj.Y+FObj.Rect.Y,
+               FObj.X+FObj.Rect.X+FObj.Rect.Width-1,
+               FObj.Y+FObj.Rect.Y+FObj.Rect.Height-1,
+               0, 255, 0);
+  end;
+end;
+
+procedure TMonster.MakeBloodSimple(Count: Word);
+begin
+  g_GFX_Blood(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)+8,
+              FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+              Count div 2, 3, -1, 16, (FObj.Rect.Height*2 div 3),
+              FBloodRed, FBloodGreen, FBloodBlue, FBloodKind);
+  g_GFX_Blood(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-8,
+              FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+              Count div 2, -3, -1, 16, (FObj.Rect.Height*2) div 3,
+              FBloodRed, FBloodGreen, FBloodBlue, FBloodKind);
+end;
+
+procedure TMonster.MakeBloodVector(Count: Word; VelX, VelY: Integer);
+begin
+  g_GFX_Blood(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+              FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+              Count, VelX, VelY, 16, (FObj.Rect.Height*2) div 3,
+              FBloodRed, FBloodGreen, FBloodBlue, FBloodKind);
+end;
+
+procedure TMonster.Push(vx, vy: Integer);
+begin
+  FObj.Accel.X := FObj.Accel.X + vx;
+  FObj.Accel.Y := FObj.Accel.Y + vy;
+  if g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_MonsterPos(FUID);
+end;
+
+procedure TMonster.SetState(State: Byte; ForceAnim: Byte = 255);
+var
+  Anim: Byte;
+begin
+// Åñëè ñîñòîÿíèå = íà÷àëè óìèðàòü, à ýòîò ìîíñòð = Lost_Soul,
+// òî ñîáëþäàåì îãðàíè÷åíèå êîëè÷åñòâà Lost_Soul'îâ:
+  if (State = STATE_DIE) and (MonsterType = MONSTER_SOUL) then
+    soulcount := soulcount-1;
+
+// Ïðèñìåðòè - íåëüçÿ ñðàçó íà÷èíàòü àòàêîâàòü èëè áåãàòü:
+  case FState of
+    STATE_DIE, STATE_DEAD, STATE_REVIVE:
+      if (State <> STATE_DEAD) and (State <> STATE_REVIVE) and
+         (State <> STATE_GO) then
+        Exit;
+  end;
+
+// Ñìåíà ñîñòîÿíèÿ:
+  FState := State;
+
+  if g_Game_IsServer and g_Game_IsNet then MH_SEND_MonsterState(FUID, ForceAnim);
+
+// Íîâàÿ àíèìàöèÿ ïðè íîâîì ñîñòîÿíèè:
+  case FState of
+    STATE_SLEEP: Anim := ANIM_SLEEP;
+    STATE_PAIN: Anim := ANIM_PAIN;
+    STATE_WAIT: Anim := ANIM_SLEEP;
+    STATE_CLIMB, STATE_RUN, STATE_RUNOUT, STATE_GO: Anim := ANIM_GO;
+    STATE_SHOOT: Anim := ANIM_ATTACK;
+    STATE_ATTACK: Anim := ANIM_ATTACK;
+    STATE_DIE: Anim := ANIM_DIE;
+    STATE_REVIVE:
+      begin // íà÷àëè âîñðåøàòüñÿ
+        Anim := FCurAnim;
+        FAnim[Anim, FDirection].Revert(True);
+
+        FObj.Rect := MONSTERTABLE[FMonsterType].Rect;
+        FHealth := MONSTERTABLE[FMonsterType].Health;
+        FAmmo := 0; 
+        FPain := 0;
+      end;
+    else Exit;
+  end;
+
+// Íàäî ñìåíèòü àíèìàöèþ íà íåñòàíäàðòíóþ:
+  if ForceAnim <> 255 then
+    Anim := ForceAnim;
+
+// Åñëè àíèìàöèÿ íîâàÿ - ïåðåçàïóñêàåì å¸: 
+  if FCurAnim <> Anim then
+    if FAnim[Anim, FDirection] <> nil then
+    begin
+      FAnim[Anim, FDirection].Reset();
+      FCurAnim := Anim;
+    end;
+end;
+
+function TMonster.TeleportTo(X, Y: Integer; silent: Boolean; dir: Byte): Boolean;
+var
+  TA: TAnimation;
+  FramesID: DWORD;
+begin
+  Result := False;
+
+// Â òî÷êå íàçíà÷åíèÿ ñòåíà:
+  if g_CollideLevel(X, Y, FObj.Rect.Width, FObj.Rect.Height) then
+  begin
+    g_Sound_PlayExAt('SOUND_GAME_NOTELEPORT', FObj.X, FObj.Y);
+    if g_Game_IsServer and g_Game_IsNet then
+      MH_SEND_Sound(FObj.X, FObj.Y, 'SOUND_GAME_NOTELEPORT');
+    Exit;
+  end;
+
+  TA := nil;
+
+// Ýôôåêò òåëåïîðòà â ïîçèöèè ìîíñòðà:
+  if not silent then
+  begin
+    if g_Frames_Get(FramesID, 'FRAMES_TELEPORT') then
+      TA := TAnimation.Create(FramesID, False, 6);
+    g_Sound_PlayExAt('SOUND_GAME_TELEPORT', Obj.X, Obj.Y);
+    g_GFX_OnceAnim(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-32,
+                   FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2)-32, TA);
+
+    if g_Game_IsServer and g_Game_IsNet then
+      MH_SEND_Effect(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-32,
+                     FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2)-32, 1,
+                     NET_GFX_TELE);
+  end;
+
+  FObj.X := X - FObj.Rect.X;
+  FObj.Y := Y - FObj.Rect.Y;
+
+  if dir = 1 then
+    FDirection := D_LEFT
+  else
+    if dir = 2 then
+      FDirection := D_RIGHT
+    else
+      if dir = 3 then
+      begin // îáðàòíîå
+        if FDirection = D_RIGHT then
+          FDirection := D_LEFT
+        else
+          FDirection := D_RIGHT;
+      end;
+
+// Ýôôåêò òåëåïîðòà â òî÷êå íàçíà÷åíèÿ:
+  if not silent and (TA <> nil) then
+  begin
+    g_GFX_OnceAnim(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-32,
+                   FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2)-32, TA);
+    TA.Free();
+
+    if g_Game_IsServer and g_Game_IsNet then
+     MH_SEND_Effect(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-32,
+                    FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2)-32, 0,
+                    NET_GFX_TELE);
+  end;
+
+  if g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_MonsterPos(FUID);
+  Result := True;
+end;
+
+procedure TMonster.Update();
+var
+  a, b, sx, sy, wx, wy, oldvelx: Integer;
+  st: Word;
+  o, co: TObj;
+  fall: Boolean;
+label
+  _end;
+begin
+  fall := True;
+
+// Ðûáû "ëåòàþò" òîëüêî â âîäå:
+  if FMonsterType = MONSTER_FISH then
+    if g_Obj_CollidePanel(@FObj, 0, 0, PANEL_WATER or PANEL_ACID1 or PANEL_ACID2) then
+      if (FState <> STATE_DIE) and (FState <> STATE_DEAD) then
+        fall := False;
+
+// Ëåòàþùèå ìîíòñðû:
+  if ((FMonsterType = MONSTER_SOUL) or
+      (FMonsterType = MONSTER_PAIN) or
+      (FMonsterType = MONSTER_CACO)) and
+     (FState <> STATE_DIE) and
+     (FState <> STATE_DEAD) then
+    fall := False;
+
+// Ìåíÿåì ñêîðîñòü òîëüêî ïî ÷åòíûì êàäðàì:
+  if gTime mod (GAME_TICK*2) <> 0 then
+  begin
+    g_Obj_Move(@FObj, fall, True, True);
+    Exit;
+  end;
+
+  FPainSound := False;
+
+// Äâèãàåìñÿ:
+  st := g_Obj_Move(@FObj, fall, True, True);
+
+// Âûëåòåë çà êàðòó - óäàëÿåì è çàïóñêàåì òðèããåðû:
+  if WordBool(st and MOVE_FALLOUT) or (FObj.X < -1000) or
+     (FObj.X > gMapInfo.Width+1000) or (FObj.Y < -1000) then
+  begin
+    FRemoved := True;
+    if Live and (gLMSRespawn = LMS_RESPAWN_NONE) then
+    begin
+      Inc(gCoopMonstersKilled);
+      if g_Game_IsNet then
+        MH_SEND_GameStats;
+    end;
+    ActivateTriggers();
+    Exit;
+  end;
+
+  oldvelx := FObj.Vel.X;
+
+// Ñîïðîòèâëåíèå âîçäóõà äëÿ òðóïà:
+  if (FState = STATE_DIE) or (FState = STATE_DEAD) then
+    FObj.Vel.X := z_dec(FObj.Vel.X, 1);
+
+// Ìåðòâûé íè÷åãî íå äåëàåò:
+  if (FState = STATE_DEAD) then
+    goto _end;
+
+// AI ìîíñòðîâ âûêëþ÷åí:
+  if g_debug_MonsterOff then
+  begin
+    FSleep := 1;
+    if FState <> STATE_SLEEP then
+      SetState(STATE_SLEEP);
+  end;
+
+// Âîçìîæíî, ñîçäàåì ïóçûðüêè â âîäå:
+  if WordBool(st and MOVE_INWATER) and (Random(32) = 0) then
+    case FMonsterType of
+      MONSTER_FISH:
+        if Random(4) = 0 then
+          g_GFX_Bubbles(FObj.X+FObj.Rect.X + Random(FObj.Rect.Width),
+                        FObj.Y+FObj.Rect.Y + Random(4), 1, 0, 0);
+      MONSTER_ROBO, MONSTER_BARREL:
+        g_GFX_Bubbles(FObj.X+FObj.Rect.X + Random(FObj.Rect.Width),
+                      FObj.Y+FObj.Rect.Y + Random(4), 1, 0, 0);
+      else begin
+        g_GFX_Bubbles(FObj.X+FObj.Rect.X + Random(FObj.Rect.Width-4),
+                      FObj.Y+FObj.Rect.Y + Random(4), 5, 4, 4);
+        if Random(2) = 0 then
+          g_Sound_PlayExAt('SOUND_GAME_BUBBLE1', FObj.X, FObj.Y)
+        else
+          g_Sound_PlayExAt('SOUND_GAME_BUBBLE2', FObj.X, FObj.Y);
+      end;
+    end;
+
+// Åñëè ïðîøåë ïåðâûé êàäð àíèìàöèè âçðûâà áî÷êè, òî âçðûâ:
+  if FMonsterType = MONSTER_BARREL then
+  begin
+    if (FState = STATE_DIE) and (FAnim[FCurAnim, FDirection].CurrentFrame = 1) and
+       (FAnim[FCurAnim, FDirection].Counter = 0) then
+      g_Weapon_Explode(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                       FObj.Y+FObj.Rect.Y+FObj.Rect.Height-16,
+                       60, FUID);
+  end;
+
+// Lost_Soul âûëåòåë èç âîäû => óñêîðÿåòñÿ:
+  if FMonsterType = MONSTER_SOUL then
+    if WordBool(st and MOVE_HITAIR) then
+      g_Obj_SetSpeed(@FObj, 16);
+
+  if FAmmo < 0 then
+    FAmmo := FAmmo + 1;
+
+// Åñëè íà÷àëè âñïëûâàòü, òî ïðîäîëæàåì:
+  if FObj.Vel.Y < 0 then
+    if WordBool(st and MOVE_INWATER) then
+      FObj.Vel.Y := -4;
+
+// Òàéìåð - æäåì ïîñëå ïîòåðè öåëè:
+  FTargetTime := FTargetTime + 1;
+
+// Ãèëüçû
+  if FShellTimer > -1 then
+    if FShellTimer = 0 then
+    begin
+      if FShellType = SHELL_SHELL then
+        g_Player_CreateShell(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                             FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                             GameVelX, GameVelY-2, SHELL_SHELL)
+      else if FShellType = SHELL_DBLSHELL then
+      begin
+        g_Player_CreateShell(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                             FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                             GameVelX-1, GameVelY-2, SHELL_SHELL);
+        g_Player_CreateShell(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                             FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                             GameVelX+1, GameVelY-2, SHELL_SHELL);
+      end;
+      FShellTimer := -1;
+    end else Dec(FShellTimer);
+
+// Ïðîáóåì óâåðíóòüñÿ îò ëåòÿùåé ïóëè:
+  if fall then
+    if (FState in [STATE_GO, STATE_RUN, STATE_RUNOUT,
+                   STATE_ATTACK, STATE_SHOOT]) then
+      if g_Weapon_Danger(FUID, FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y,
+                         FObj.Rect.Width, FObj.Rect.Height, 50) then
+        if (g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj)) and
+           (FObj.Accel.Y = 0) then
+          FObj.Vel.Y := -MONSTERTABLE[FMonsterType].Jump;
+
+  case FState of
+    STATE_PAIN: // Ñîñòîÿíèå - Áîëü
+      begin
+      // Áîëü ñèëüíàÿ => ìîíñòð êðè÷èò:
+        if FPain >= MONSTERTABLE[FMonsterType].Pain then
+        begin
+          FPain := MONSTERTABLE[FMonsterType].Pain;
+          if gSoundEffectsDF then PainSound();
+        end;
+
+      // Ñíèæàåì áîëü ñî âðåìåíåì:
+        FPain := FPain - 5;
+
+      // Áîëü óæå íå îøóòèìàÿ => èäåì äàëüøå:
+        if FPain <= MONSTERTABLE[FMonsterType].MinPain then
+        begin
+          FPain := 0;
+          FAmmo := -9;
+          SetState(STATE_GO);
+        end;
+      end;
+
+    STATE_SLEEP: // Ñîñòîÿíèå - Ñîí
+      begin
+      // Ñïèì:
+        FSleep := FSleep + 1;
+
+      // Ïðîñïàëè äîñòàòî÷íî:
+        if FSleep >= 18 then
+          FSleep := 0
+        else // åùå ñïèì
+          goto _end;
+
+      // Íà èãðîêîâ èäóò òîëüêî îáû÷íûå ìîíñòðû, êèëëåðû è ìàíüÿêè
+        if (FBehaviour = BH_NORMAL) or (FBehaviour = BH_KILLER) or (FBehaviour = BH_MANIAC) then
+        // Åñëè åñòü èãðîê ðÿäîì, ïðîñûïàåìñÿ è èäåì ê íåìó:
+          if (gPlayers <> nil) then
+            for a := 0 to High(gPlayers) do
+              if (gPlayers[a] <> nil) and (gPlayers[a].Live)
+              and (not gPlayers[a].NoTarget) and (gPlayers[a].FMegaRulez[MR_INVIS] < gTime) then
+                with gPlayers[a] do
+                  if g_Look(@FObj, @Obj, FDirection) then
+                  begin
+                    FTargetUID := gPlayers[a].UID;
+                    FTargetTime := 0;
+                    WakeUpSound();
+                    SetState(STATE_GO);
+                    Break;
+                  end;
+
+      // Íà ìîíñòðîâ òÿíåò ìàíüÿêîâ, ïîåõàâøèõ è êàííèáàëîâ
+        if (FTargetUID = 0) and ((FBehaviour = BH_MANIAC)
+        or (FBehaviour = BH_INSANE) or (FBehaviour = BH_CANNIBAL)) then
+        // Åñëè åñòü ïîäõîäÿùèé ìîíñòð ðÿäîì:
+          if gMonsters <> nil then
+            for a := 0 to High(gMonsters) do
+              if (gMonsters[a] <> nil) and (gMonsters[a].Live) and
+                 (gMonsters[a].FUID <> FUID) then
+              begin
+                // Ìàíüÿêè íàïàäàþò íà âñåõ ìîíñòðîâ, êðîìå äðóçåé
+                if (FBehaviour = BH_MANIAC) and
+                (IsFriend(gMonsters[a].FMonsterType, FMonsterType)) then
+                  Continue;
+                // Ïîåõàâøèå òàêæå, íî ìîãóò îáîçëèòüñÿ íà áî÷êó
+                if (FBehaviour = BH_INSANE) and (gMonsters[a].FMonsterType <> MONSTER_BARREL) and
+                (IsFriend(gMonsters[a].FMonsterType, FMonsterType)) then
+                  Continue;
+                // Êàííèáàëû íàïàäàþò íà ñåáå ïîäîáíûõ
+                if (FBehaviour = BH_CANNIBAL) and (gMonsters[a].FMonsterType <> FMonsterType) then
+                  Continue;
+                if g_Look(@FObj, @gMonsters[a].Obj, FDirection) then
+                begin
+                  FTargetUID := gMonsters[a].UID;
+                  FTargetTime := 0;
+                  WakeUpSound();
+                  SetState(STATE_GO);
+                  Break;
+                end;
+              end;
+      end;
+
+    STATE_WAIT: // Ñîñòîÿíèå - Îæèäàíèå
+      begin
+      // Æäåì:
+        FSleep := FSleep - 1;
+
+      // Âûæäàëè äîñòàòî÷íî - èäåì:
+        if FSleep < 0 then
+          SetState(STATE_GO);
+      end;
+
+    STATE_GO: // Ñîñòîÿíèå - Äâèæåíèå (ñ îñìîòðîì ñèòóàöèè)
+      begin
+      // Åñëè íàòêíóëèñü íà ÁëîêÌîí - óáåãàåì îò íåãî:
+        if WordBool(st and MOVE_BLOCK) then
+        begin
+          Turn();
+          FSleep := 40;
+          SetState(STATE_RUNOUT);
+
+          goto _end;
+        end;
+
+      // Åñëè ìîíñòð - êîëäóí, òî ïðîáóåì âîñêðåñèòü êîãî-íèáóäü:
+        if (FMonsterType = MONSTER_VILE) then
+          if isCorpse(@FObj, False) <> -1 then
+          begin
+            FObj.Vel.X := 0;
+            SetState(STATE_ATTACK, ANIM_ATTACK2);
+
+            goto _end;
+          end;
+
+      // Öåëü ïîãèáëà èëè äàâíî æäåì:
+         if (not GetPos(FTargetUID, @o)) or (FTargetTime > MAX_ATM) then
+           if not findNewPrey() then
+             begin // Íîâûõ öåëåé íåò
+              FTargetUID := 0;
+              o.X := FObj.X+pt_x;
+              o.Y := FObj.Y+pt_y;
+              o.Vel.X := 0;
+              o.Vel.Y := 0;
+              o.Accel.X := 0;
+              o.Accel.Y := 0;
+              o.Rect := _Rect(0, 0, 0, 1);
+             end
+           else // Íîâàÿ öåëü åñòü - áåðåì åå êîîðäèíàòû
+            GetPos(FTargetUID, @o);
+
+      // Öåëü î÷åíü áëèçêî - ïèíàåì:
+        if g_Obj_Collide(@FObj, @o) and (FTargetUID <> 0) then
+        begin
+          FTargetTime := 0;
+          if kick(@o) then
+            goto _end;
+        end;
+
+      // Ðàññòîÿíèå äî öåëè:
+        sx := o.X+o.Rect.X+(o.Rect.Width div 2)-(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2));
+        sy := o.Y+o.Rect.Y+(o.Rect.Height div 2)-(FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2));
+
+      // Ïîâîðà÷èâàåìñÿ â ñòîðîíó öåëè:
+        if sx > 0 then
+          FDirection := D_RIGHT
+        else
+          FDirection := D_LEFT;
+
+      // Åñëè ìîíñòð óìååò ñòðåëÿòü è åñòü ïî êîìó - ñòðåëÿåì:
+        if canShoot(FMonsterType) and (FTargetUID <> 0) then
+          if Abs(sx) > Abs(sy) then // óãîë âûñòðåëà óäîáíûé
+            if shoot(@o, False) then
+              goto _end;
+
+      // Åñëè öåëü ïî÷òè íà îäíîé âåðòèêàëè - áåãàåì òóäà-ñþäà:
+        if Abs(sx) < 40 then
+          if FMonsterType <> MONSTER_FISH then
+          begin
+            FSleep := 15;
+            SetState(STATE_RUN);
+            if Random(2) = 0 then
+              FDirection := D_LEFT
+            else
+              FDirection := D_RIGHT;
+              
+            goto _end;
+          end;
+
+      // Óïåðëèñü â ñòåíó:
+        if WordBool(st and MOVE_HITWALL) then
+        begin
+          if g_Triggers_PressR(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y, FObj.Rect.Width,
+                               FObj.Rect.Height, FUID, ACTIVATE_MONSTERPRESS) <> nil then
+          begin // Ñìîãëè íàæàòü êíîïêó - íåáîëüøîå îæèäàíèå
+            FSleep := 4;
+            SetState(STATE_WAIT);
+
+            goto _end;
+          end;
+
+          case FMonsterType of
+            MONSTER_CACO, MONSTER_SOUL, MONSTER_PAIN, MONSTER_FISH: ;
+            else // Íå ëåòàþò:
+              if (g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj)) and
+                 (FObj.Accel.Y = 0) then
+              begin // Ñòîèì íà òâåðäîì ïîëó èëè ñòóïåíè
+              // Ïðûæîê ÷åðåç ñòåíó:
+                FObj.Vel.Y := -MONSTERTABLE[FMonsterType].Jump;
+                SetState(STATE_CLIMB);
+              end;
+          end;
+
+          goto _end;
+        end;
+
+      // Ìîíñòðû, íå ïîäâåðæåííûå ãðàâèòàöèè:
+        if (FMonsterType = MONSTER_CACO) or (FMonsterType = MONSTER_SOUL) or
+           (FMonsterType = MONSTER_PAIN) or (FMonsterType = MONSTER_FISH) then
+          begin
+            if FMonsterType = MONSTER_FISH then
+              begin // Ðûáà
+                if not WordBool(st and MOVE_INWATER) then
+                  begin // Ðûáà âíå âîäû:
+                    if g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj) then
+                    begin // "Ñòîèò" òâåðäî
+                    // Ðûáà òðåïûõàåòñÿ íà ïîâåðõíîñòè:
+                      if FObj.Accel.Y = 0 then
+                        FObj.Vel.Y := -6;
+                      FObj.Accel.X := FObj.Accel.X - 8 + Random(17);
+                    end;
+
+                  // Ðûáå áîëüíî:
+                    SetState(STATE_PAIN);
+                    FPain := FPain + 50;
+                  end
+                else // Ðûáà â âîäå
+                  begin
+                  // Ïëûâåì â ñòîðîíó öåëè ïî-âåðòèêàëè:
+                    if Abs(sy) > 8 then
+                      FObj.Vel.Y := g_basic.Sign(sy)*4
+                    else
+                      FObj.Vel.Y := 0;
+
+                  // Ðûáà ïëûâåò ââåðõ:
+                    if FObj.Vel.Y < 0 then
+                      if not g_Obj_CollideWater(@FObj, 0, -16) then
+                      begin
+                      // Âñïëûëè äî ïîâåðõíîñòè - ñòîï:
+                        FObj.Vel.Y := 0;
+                      // Ïëàâàåì òóäà-ñþäà:
+                        if Random(2) = 0 then
+                          FDirection := D_LEFT
+                        else
+                          FDirection := D_RIGHT;
+                        FSleep := 20;
+                        SetState(STATE_RUN);
+                      end;
+                   end;
+              end
+            else // Ëåòàþùèå ìîíñòðû
+              begin
+              // Ëåòèì â ñòîðîíó öåëè ïî-âåðòèêàëè:
+                if Abs(sy) > 8 then
+                  FObj.Vel.Y := g_basic.Sign(sy)*4
+                else
+                  FObj.Vel.Y := 0;
+              end;
+          end
+        else // "Íàçåìíûå" ìîíñòðû
+          begin
+          // Âîçìîæíî, ïèíàåì êóñêè:
+            if (FObj.Vel.X <> 0) and (gGibs <> nil) then
+            begin
+              b := Abs(FObj.Vel.X);
+              if b > 1 then b := b * (Random(8 div b) + 1);
+              for a := 0 to High(gGibs) do
+                if gGibs[a].Live and
+                   g_Obj_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y+FObj.Rect.Height-4,
+                                 FObj.Rect.Width, 8, @gGibs[a].Obj) and (Random(3) = 0) then
+                  // Ïèíàåì êóñêè
+                  if FObj.Vel.X < 0 then
+                    g_Obj_PushA(@gGibs[a].Obj, b, Random(61)+120) // íàëåâî
+                  else
+                    g_Obj_PushA(@gGibs[a].Obj, b, Random(61));    // íàïðàâî
+            end;
+          // Áîññû ìîãóò ïèíàòü òðóïû:
+            if (FMonsterType in [MONSTER_CYBER, MONSTER_SPIDER, MONSTER_ROBO]) and
+               (FObj.Vel.X <> 0) and (gCorpses <> nil) then
+            begin
+              b := Abs(FObj.Vel.X);
+              if b > 1 then b := b * (Random(8 div b) + 1);
+              for a := 0 to High(gCorpses) do
+                if (gCorpses[a] <> nil) and (gCorpses[a].State > 0) then
+                begin
+                  co := gCorpses[a].Obj;
+                  if g_Obj_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y+FObj.Rect.Height-4,
+                                   FObj.Rect.Width, 8, @co) and (Random(3) = 0) then
+                    // Ïèíàåì òðóïû
+                    if FObj.Vel.X < 0 then
+                      gCorpses[a].Damage(b*2, -b, Random(7)) // íàëåâî
+                    else
+                      gCorpses[a].Damage(b*2, b, Random(7)); // íàïðàâî
+                end;
+            end;
+          // Åñëè öåëü âûñîêî, òî, âîçìîæíî, ïðûãàåì:
+            if sy < -40 then
+              if g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj) then
+              // ñòîèò òâåðäî
+                if (Random(4) = 0) and (FObj.Accel.Y = 0) then
+                  FObj.Vel.Y := -MONSTERTABLE[FMonsterType].Jump;
+          end;
+
+        FSleep := FSleep + 1;
+
+      // Èíîãäà ðû÷èì:
+        if FSleep >= 8 then
+        begin
+          FSleep := 0;
+          if Random(8) = 0 then
+            ActionSound();
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_RUN: // Ñîñòîÿíèå - Áåã
+      begin
+      // Åñëè íàòêíóëèñü íà ÁëîêÌîí - óáåãàåì îò íåãî:
+        if WordBool(st and MOVE_BLOCK) then
+        begin
+          Turn();
+          FSleep := 40;
+          SetState(STATE_RUNOUT);
+
+          goto _end;
+        end;
+
+        FSleep := FSleep - 1;
+
+      // Ïðîáåæàëè äîñòàòî÷íî èëè âðåçàëèñü â ñòåíó => ïåðåõîäèì íà øàã:
+        if (FSleep <= 0) or (WordBool(st and MOVE_HITWALL) and ((FObj.Vel.Y+FObj.Accel.Y) = 0)) then
+        begin
+          FSleep := 0;
+          SetState(STATE_GO);
+        // Ñòåíà - èäåì îáðàòíî:
+          if WordBool(st and (MOVE_HITWALL or MOVE_BLOCK)) then
+            Turn();
+        // Èíîãäà ðû÷èì:
+          if Random(8) = 0 then
+            ActionSound();
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_RUNOUT: // Ñîñòîÿíèå - Óáåãàåò îò ÷åãî-òî
+      begin
+      // Âûøëè èç ÁëîêÌîíà:
+        if (not WordBool(st and MOVE_BLOCK)) and (FSleep > 0) then
+          FSleep := 0;
+
+        FSleep := FSleep - 1;
+
+      // Óáàæåëè äîñòàòî÷íî äàëåêî => ïåðåõîäèì íà øàã:
+        if FSleep <= -18 then
+        begin
+          FSleep := 0;
+          SetState(STATE_GO);
+        // Ñòåíà/ÁëîêÌîí - èäåì îáðàòíî:
+          if WordBool(st and (MOVE_HITWALL or MOVE_BLOCK)) then
+            Turn();
+        // Èíîãäà ðû÷èì:
+          if Random(8) = 0 then
+            ActionSound();
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_CLIMB: // Ñîñòîÿíèå - Ïðûæîê (÷òîáû îáîéòè ñòåíó)
+      begin
+      // Äîñòèãëè âûñøåé òî÷êè ïðûæêà èëè ñòåíà êîí÷èëàñü => ïåðåõîäèì íà øàã:
+        if ((FObj.Vel.Y+FObj.Accel.Y) >= 0) or
+           (not WordBool(st and MOVE_HITWALL)) then
+        begin
+          FSleep := 0;
+          SetState(STATE_GO);
+
+        // Ñòåíà íå êîí÷èëàñü => áåæèì îò íåå:
+          if WordBool(st and (MOVE_HITWALL or MOVE_BLOCK)) then
+          begin
+            Turn();
+            FSleep := 15;
+            SetState(STATE_RUN);
+          end;
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_ATTACK, // Ñîñòîÿíèå - Àòàêà
+    STATE_SHOOT:  // Ñîñòîÿíèå - Ñòðåëüáà
+      begin
+      // Lost_Soul âðåçàëñÿ â ñòåíó ïðè àòàêå => ïåðåõîäèò íà øàã:
+        if FMonsterType = MONSTER_SOUL then
+        begin
+          if WordBool(st and (MOVE_HITWALL or MOVE_HITCEIL or MOVE_HITLAND)) then
+            SetState(STATE_GO);
+
+          goto _end;
+        end;
+
+      // Çàìåäëÿåìñÿ ïðè àòàêå:
+        if FMonsterType <> MONSTER_FISH then
+          FObj.Vel.X := z_dec(FObj.Vel.X, 1);
+
+      // Íóæíî ñòðåëÿòü, à ìîíñòð - êîëäóí:
+        if (FMonsterType = MONSTER_VILE) and (FState = STATE_SHOOT) then
+        begin
+        // Öåëü ïîãèáëà => èäåì äàëüøå:
+          if not GetPos(FTargetUID, @o) then
+          begin
+            SetState(STATE_GO);
+
+            goto _end;
+          end;
+
+        // Öåëü íå âèäíî => èäåì äàëüøå:
+          if not g_Look(@FObj, @o, FDirection) then
+          begin
+            SetState(STATE_GO);
+
+            goto _end;
+          end;
+
+        // Öåëü â âîäå - íå çàãîðèòñÿ => èäåì äàëüøå:
+          if g_Obj_CollideWater(@o, 0, 0) then
+          begin
+            SetState(STATE_GO);
+
+            goto _end;
+          end;
+
+        // Æàðèì öåëü:
+          tx := o.X+o.Rect.X+(o.Rect.Width div 2);
+          ty := o.Y+o.Rect.Y+(o.Rect.Height div 2);
+          g_Weapon_HitUID(FTargetUID, 2, FUID, HIT_SOME);
+        end;
+      end;
+  end; // case FState of ...
+
+_end:
+
+// Ñîñòîÿíèå - Âîñêðåøåíèå:
+  if FState = STATE_REVIVE then
+    if FAnim[FCurAnim, FDirection].Played then
+    begin // Îáðàòíàÿ àíèìàöèÿ óìèðàíèÿ çàêîí÷èëàñü - èäåì äàëüøå:
+      FAnim[FCurAnim, FDirection].Revert(False);
+      SetState(STATE_GO);
+    end;
+
+// Åñëè åñòü àíèìàöèÿ îãíÿ êîëäóíà - ïóñòü îíà èäåò:
+  if vilefire <> nil then
+    vilefire.Update();
+
+// Ñîñòîÿíèå - Óìèðàåò è òåêóùàÿ àíèìàöèÿ ïðîèãðàíà:
+  if (FState = STATE_DIE) and
+     (FAnim[FCurAnim, FDirection] <> nil) and
+     (FAnim[FCurAnim, FDirection].Played) then
+    begin
+    // Óìåð:
+      SetState(STATE_DEAD);
+
+    // Pain_Elemental ïðè ñìåðòè âûïóñêàåò 3 Lost_Soul'à:
+      if (FMonsterType = MONSTER_PAIN) then
+      begin
+        sx := g_Monsters_Create(MONSTER_SOUL, FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-30,
+                                FObj.Y+FObj.Rect.Y+20, D_LEFT);
+        if sx <> -1 then
+        begin
+          gMonsters[sx].SetState(STATE_GO);
+          gMonsters[sx].FNoRespawn := True;
+          Inc(gTotalMonsters);
+          if g_Game_IsNet then MH_SEND_MonsterSpawn(gMonsters[sx].UID);
+        end;
+
+        sx := g_Monsters_Create(MONSTER_SOUL, FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                                FObj.Y+FObj.Rect.Y+20, D_RIGHT);
+        if sx <> -1 then
+        begin
+          gMonsters[sx].SetState(STATE_GO);
+          gMonsters[sx].FNoRespawn := True;
+          Inc(gTotalMonsters);
+          if g_Game_IsNet then MH_SEND_MonsterSpawn(gMonsters[sx].UID);
+        end;
+
+        sx := g_Monsters_Create(MONSTER_SOUL, FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-15,
+                                FObj.Y+FObj.Rect.Y, D_RIGHT);
+        if sx <> -1 then
+        begin
+          gMonsters[sx].SetState(STATE_GO);
+          gMonsters[sx].FNoRespawn := True;
+          Inc(gTotalMonsters);
+          if g_Game_IsNet then MH_SEND_MonsterSpawn(gMonsters[sx].UID);
+        end;
+        if g_Game_IsNet then MH_SEND_CoopStats(gMonsters[sx].UID);
+      end;
+
+    // Ó ýòèõ ìîíñòðîâ íåò òðóïîâ:
+      if (FMonsterType = MONSTER_PAIN) or
+         (FMonsterType = MONSTER_SOUL) or
+         (FMonsterType = MONSTER_BARREL) then
+        FRemoved := True;
+    end;
+
+// Ñîâåðøåíèå àòàêè è ñòðåëüáû:
+  if (FState = STATE_ATTACK) or (FState = STATE_SHOOT) then
+    if (FAnim[FCurAnim, FDirection] <> nil) then
+    // Àíèìàöèÿ àòàêè åñòü - ìîæíî àòàêîâàòü
+      if (FAnim[FCurAnim, FDirection].Played) then
+        begin // Àíèìàöèÿ àòàêè çàêîí÷èëàñü => ïåðåõîäèì íà øàã
+          if FState = STATE_ATTACK then
+            begin // Ñîñòîÿíèå - Àòàêà
+            // Åñëè ìîíñòð íå Lost_Soul, òî ïîñëå àòàêè ïåðåõîäèì íà øàã:
+              if FMonsterType <> MONSTER_SOUL then
+                SetState(STATE_GO);
+            end
+          else // Ñîñòîÿíèå - Ñòðåëüáà
+            begin
+            // Ïåðåõîäèì íà øàã, åñëè íå íàäî ñòðåëÿòü åùå ðàç:
+              if not FChainFire then
+                SetState(STATE_GO)
+              else
+                begin // Íàäî ñòðåëÿòü åùå
+                  FChainFire := False;
+                // Ò.ê. ñîñòîÿíèå íå èçìåíèëîñü, è íóæåí
+                // íîâûé öèêë îæèäàíèÿ çàâåðøåíèÿ àíèìàöèè:
+                  FAnim[FCurAnim, FDirection].Reset();
+                end;
+            end;
+
+          FWaitAttackAnim := False;
+        end
+        
+      else // Àíèìàöèÿ àòàêè åùå èäåò (èñêëþ÷åíèå - Lost_Soul):
+        if (FMonsterType = MONSTER_SOUL) or
+           ( (not FWaitAttackAnim) and
+             (FAnim[FCurAnim, FDirection].CurrentFrame =
+              (FAnim[FCurAnim, FDirection].TotalFrames div 2))
+           ) then
+        begin // Àòàêè åùå íå áûëî è ýòî ñåðåäèíà àíèìàöèè àòàêè
+          if FState = STATE_ATTACK then
+            begin // Ñîñòîÿíèå - Àòàêà
+            // Åñëè ýòî Lost_Soul, òî ñáðàñûâàåì àíèìàöèþ àòàêè:
+              if FMonsterType = MONSTER_SOUL then
+                FAnim[FCurAnim, FDirection].Reset();
+
+              case FMonsterType of
+                MONSTER_SOUL, MONSTER_IMP, MONSTER_DEMON:
+                // Ãðûçåì ïåðâîãî ïîïàâøåãîñÿ:
+                  if g_Weapon_Hit(@FObj, 15, FUID, HIT_SOME) <> 0 then
+                  // Lost_Soul óêóñèë êîãî-òî => ïåðåõîäèò íà øàã:
+                    if FMonsterType = MONSTER_SOUL then
+                      SetState(STATE_GO);
+
+                MONSTER_FISH:
+                // Ðûáà êóñàåò ïåðâîãî ïîïàâøåãîñÿ ñî çâóêîì:
+                  if g_Weapon_Hit(@FObj, 10, FUID, HIT_SOME) <> 0 then
+                    g_Sound_PlayExAt('SOUND_MONSTER_FISH_ATTACK', FObj.X, FObj.Y);
+
+                MONSTER_SKEL, MONSTER_ROBO:
+                // Ðîáîò èëè ñêåëåò ñèëüíî ïèíàþòñÿ:
+                  if FCurAnim = ANIM_ATTACK2 then
+                  begin
+                    o := FObj;
+                    o.Vel.X := IfThen(FDirection = D_RIGHT, 1, -1)*50;
+                    if g_Weapon_Hit(@o, 50, FUID, HIT_SOME) <> 0 then
+                      g_Sound_PlayExAt('SOUND_MONSTER_SKEL_HIT', FObj.X, FObj.Y);
+                  end;
+
+                MONSTER_VILE:
+                // Êîëäóí ïûòàåòñÿ âîñêðåøàòü:
+                  if FCurAnim = ANIM_ATTACK2 then
+                  begin
+                    sx := isCorpse(@FObj, True);
+                    if sx <> -1 then
+                    begin // Íàøëè, êîãî âîñêðåñèòü
+                      gMonsters[sx].SetState(STATE_REVIVE);
+                      g_Sound_PlayExAt('SOUND_MONSTER_SLOP', FObj.X, FObj.Y);
+                    // Âîñêðåøàòü - ñåáå âðåäèòü:
+                      {g_Weapon_HitUID(FUID, 5, 0, HIT_SOME);}
+                    end;
+                  end;
+              end;
+            end
+
+          else // Ñîñòîÿíèå - Ñòðåëüáà
+            begin
+            // Âû÷èñëÿåì êîîðäèíàòû, îòêóäà âûëåòèò ïóëÿ:
+              wx := MONSTER_ANIMTABLE[FMonsterType].wX;
+
+              if FDirection = D_LEFT then
+              begin
+                wx := MONSTER_ANIMTABLE[FMonsterType].wX-(MONSTERTABLE[FMonsterType].Rect.X+(MONSTERTABLE[FMonsterType].Rect.Width div 2));
+                wx := MONSTERTABLE[FMonsterType].Rect.X+(MONSTERTABLE[FMonsterType].Rect.Width div 2)-wx;
+              end; // Ýòî çíà÷èò: wx := hitX + (hitWidth / 2) - (wx - (hitX + (hitWidth / 2)))
+
+              wx := FObj.X + wx;
+              wy := FObj.Y + MONSTER_ANIMTABLE[FMonsterType].wY;
+
+            // Äåëàåì âûñòðåë íóæíûì îðóæèåì:
+              case FMonsterType of
+                MONSTER_IMP:
+                  g_Weapon_ball1(wx, wy, tx, ty, FUID);
+                MONSTER_ZOMBY:
+                  begin
+                    g_Sound_PlayExAt('SOUND_WEAPON_FIREPISTOL', wx, wy);
+                    g_Weapon_gun(wx, wy, tx, ty, 1, 3, FUID, False);
+                    g_Player_CreateShell(wx, wy, 0, -2, SHELL_BULLET);
+                  end;
+                MONSTER_SERG:
+                  begin
+                    g_Weapon_shotgun(wx, wy, tx, ty, FUID);
+                    if not gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN', wx, wy);
+                    FShellTimer := 10;
+                    FShellType := SHELL_SHELL;
+                  end;
+                MONSTER_MAN:
+                  begin
+                    g_Weapon_dshotgun(wx, wy, tx, ty, FUID);
+                    FShellTimer := 13;
+                    FShellType := SHELL_DBLSHELL;
+                    FAmmo := -36;
+                  end;
+                MONSTER_CYBER:
+                begin
+                  g_Weapon_rocket(wx, wy, tx, ty, FUID);
+                  // MH_SEND_MonsterAttack(FUID, wx, wy, tx, ty);
+                end;
+                MONSTER_SKEL:
+                  g_Weapon_revf(wx, wy, tx, ty, FUID, FTargetUID);
+                MONSTER_CGUN:
+                  begin
+                    g_Weapon_mgun(wx, wy, tx, ty, FUID);
+                    if not gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRECGUN', wx, wy);
+                    g_Player_CreateShell(wx, wy, 0, -2, SHELL_BULLET);
+                  end;
+                MONSTER_SPIDER:
+                  begin
+                    g_Weapon_mgun(wx, wy, tx, ty, FUID);
+                    if not gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRECGUN', wx, wy);
+                    g_Player_CreateShell(wx, wy, 0, -2, SHELL_SHELL);
+                  end;
+                MONSTER_BSP:
+                  g_Weapon_aplasma(wx, wy, tx, ty, FUID);
+                MONSTER_ROBO:
+                  g_Weapon_plasma(wx, wy, tx, ty, FUID);
+                MONSTER_MANCUB:
+                  g_Weapon_manfire(wx, wy, tx, ty, FUID);
+                MONSTER_BARON, MONSTER_KNIGHT:
+                  g_Weapon_ball7(wx, wy, tx, ty, FUID);
+                MONSTER_CACO:
+                  g_Weapon_ball2(wx, wy, tx, ty, FUID);
+                MONSTER_PAIN:
+                  begin // Ñîçäàåì Lost_Soul:
+                    sx := g_Monsters_Create(MONSTER_SOUL, FObj.X+(FObj.Rect.Width div 2),
+                                            FObj.Y+FObj.Rect.Y, FDirection);
+
+                    if sx <> -1 then
+                    begin // Öåëü - öåëü Pain_Elemental'à. Ëåòèì ê íåé:
+                      gMonsters[sx].FTargetUID := FTargetUID;
+                      GetPos(FTargetUID, @o);
+                      gMonsters[sx].FTargetTime := 0;
+                      gMonsters[sx].FNoRespawn := True;
+                      gMonsters[sx].SetState(STATE_GO);
+                      gMonsters[sx].shoot(@o, True);
+                      Inc(gTotalMonsters);
+
+                      if g_Game_IsNet then MH_SEND_MonsterSpawn(gMonsters[sx].UID);
+                    end;
+                  end;
+              end;
+
+              if FMonsterType <> MONSTER_PAIN then
+                if g_Game_IsNet then
+                  MH_SEND_MonsterShot(FUID, wx, wy, tx, ty);
+
+            // Ñêîðîñòðåëüíûå ìîíñòðû:
+              if (FMonsterType = MONSTER_CGUN) or
+                 (FMonsterType = MONSTER_SPIDER) or
+                 (FMonsterType = MONSTER_BSP) or
+                 (FMonsterType = MONSTER_MANCUB) or
+                 (FMonsterType = MONSTER_ROBO) then
+                if not GetPos(FTargetUID, @o) then
+                // Öåëü ìåðòâà - èùåì íîâóþ:
+                  findNewPrey()
+                else // Öåëü æèâà - ïðîäîëæàåì ñòðåëÿòü:
+                  if shoot(@o, False) then
+                    FChainFire := True;
+            end;
+
+        // Àòàêà òîëüêî 1 ðàç çà àíèìàöèþ àòàêè:
+          FWaitAttackAnim := True;
+        end;
+
+// Ïîñëåäíèé êàäð òåêóùåé àíèìàöèè:
+  if FAnim[FCurAnim, FDirection].Counter = FAnim[FCurAnim, FDirection].Speed-1 then
+    case FState of
+      STATE_GO, STATE_RUN, STATE_CLIMB, STATE_RUNOUT:
+      // Çâóêè ïðè ïåðåäâèæåíèè:
+        case FMonsterType of
+          MONSTER_CYBER:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 2) then
+              g_Sound_PlayExAt('SOUND_MONSTER_CYBER_WALK', FObj.X, FObj.Y);
+          MONSTER_SPIDER:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 2) then
+              g_Sound_PlayExAt('SOUND_MONSTER_SPIDER_WALK', FObj.X, FObj.Y);
+          MONSTER_BSP:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 2) then
+              g_Sound_PlayExAt('SOUND_MONSTER_BSP_WALK', FObj.X, FObj.Y);
+          MONSTER_ROBO:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 5) then
+              g_Sound_PlayExAt('SOUND_MONSTER_BSP_WALK', FObj.X, FObj.Y);
+        end;
+    end;
+
+  if g_Obj_CollidePanel(@FObj, 0, 0, PANEL_LIFTLEFT or PANEL_LIFTRIGHT) and
+     not ((FState = STATE_DEAD) or (FState = STATE_DIE))  then
+    FObj.Vel.X := oldvelx;
+
+// Åñëè åñòü àíèìàöèÿ, òî ïóñòü îíà èäåò:
+  if FAnim[FCurAnim, FDirection] <> nil then
+    FAnim[FCurAnim, FDirection].Update();
+end;
+
+procedure TMonster.SetDeadAnim;
+begin
+  if FAnim <> nil then
+    FAnim[FCurAnim, FDirection].CurrentFrame := FAnim[FCurAnim, FDirection].TotalFrames - 1;
+end;
+
+procedure TMonster.RevertAnim(R: Boolean = True);
+begin
+  if FAnim <> nil then
+    if FAnim[FCurAnim, FDirection].IsReverse <> R then
+      FAnim[FCurAnim, FDirection].Revert(R);
+end;
+
+function TMonster.AnimIsReverse: Boolean;
+begin
+  if FAnim <> nil then
+    Result := FAnim[FCurAnim, FDirection].IsReverse
+  else
+    Result := False;
+end;
+
+procedure TMonster.ClientUpdate();
+var
+  a, b, sx, sy, oldvelx: Integer;
+  st: Word;
+  o, co: TObj;
+  fall: Boolean;
+label
+  _end;
+begin
+  sx := 0; // SHUT UP COMPILER
+  sy := 0;
+  fall := True;
+// Ðûáû "ëåòàþò" òîëüêî â âîäå:
+  if FMonsterType = MONSTER_FISH then
+    if g_Obj_CollidePanel(@FObj, 0, 0, PANEL_WATER or PANEL_ACID1 or PANEL_ACID2) then
+      if (FState <> STATE_DIE) and (FState <> STATE_DEAD) then
+        fall := False;
+
+// Ëåòàþùèå ìîíòñðû:
+  if ((FMonsterType = MONSTER_SOUL) or
+      (FMonsterType = MONSTER_PAIN) or
+      (FMonsterType = MONSTER_CACO)) and
+     (FState <> STATE_DIE) and
+     (FState <> STATE_DEAD) then
+    fall := False;
+
+// Ìåíÿåì ñêîðîñòü òîëüêî ïî ÷åòíûì êàäðàì:
+  if gTime mod (GAME_TICK*2) <> 0 then
+  begin
+    g_Obj_Move(@FObj, fall, True, True);
+    Exit;
+  end;
+
+  FPainSound := False;
+
+// Äâèãàåìñÿ:
+  st := g_Obj_Move(@FObj, fall, True, True);
+
+// Âûëåòåë çà êàðòó - óäàëÿåì è çàïóñêàåì òðèããåðû:
+  if WordBool(st and MOVE_FALLOUT) or (FObj.X < -1000) or
+     (FObj.X > gMapInfo.Width+1000) or (FObj.Y < -1000) then
+  begin
+    FRemoved := True;
+    Exit;
+  end;
+
+  oldvelx := FObj.Vel.X;
+
+// Ñîïðîòèâëåíèå âîçäóõà äëÿ òðóïà:
+  if (FState = STATE_DIE) or (FState = STATE_DEAD) then
+    FObj.Vel.X := z_dec(FObj.Vel.X, 1);
+
+// Ìåðòâûé íè÷åãî íå äåëàåò:
+  if (FState = STATE_DEAD) then
+    goto _end;
+
+// Âîçìîæíî, ñîçäàåì ïóçûðüêè â âîäå:
+  if WordBool(st and MOVE_INWATER) and (Random(32) = 0) then
+    case FMonsterType of
+      MONSTER_FISH:
+        if Random(4) = 0 then
+          g_GFX_Bubbles(FObj.X+FObj.Rect.X + Random(FObj.Rect.Width),
+                        FObj.Y+FObj.Rect.Y + Random(4), 1, 0, 0);
+      MONSTER_ROBO, MONSTER_BARREL:
+        g_GFX_Bubbles(FObj.X+FObj.Rect.X + Random(FObj.Rect.Width),
+                      FObj.Y+FObj.Rect.Y + Random(4), 1, 0, 0);
+      else begin
+        g_GFX_Bubbles(FObj.X+FObj.Rect.X + Random(FObj.Rect.Width-4),
+                      FObj.Y+FObj.Rect.Y + Random(4), 5, 4, 4);
+        if Random(2) = 0 then
+          g_Sound_PlayExAt('SOUND_GAME_BUBBLE1', FObj.X, FObj.Y)
+        else
+          g_Sound_PlayExAt('SOUND_GAME_BUBBLE2', FObj.X, FObj.Y);
+      end;
+    end;
+
+// Åñëè ïðîøåë ïåðâûé êàäð àíèìàöèè âçðûâà áî÷êè, òî âçðûâ:
+  if FMonsterType = MONSTER_BARREL then
+  begin
+    if (FState = STATE_DIE) and (FAnim[FCurAnim, FDirection].CurrentFrame = 1) and
+       (FAnim[FCurAnim, FDirection].Counter = 0) then
+      g_Weapon_Explode(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                       FObj.Y+FObj.Rect.Y+FObj.Rect.Height-16,
+                       60, FUID);
+  end;
+
+// Lost_Soul âûëåòåë èç âîäû => óñêîðÿåòñÿ:
+  if FMonsterType = MONSTER_SOUL then
+    if WordBool(st and MOVE_HITAIR) then
+      g_Obj_SetSpeed(@FObj, 16);
+
+  if FAmmo < 0 then
+    FAmmo := FAmmo + 1;
+
+// Åñëè íà÷àëè âñïëûâàòü, òî ïðîäîëæàåì:
+  if FObj.Vel.Y < 0 then
+    if WordBool(st and MOVE_INWATER) then
+      FObj.Vel.Y := -4;
+
+// Òàéìåð - æäåì ïîñëå ïîòåðè öåëè:
+  FTargetTime := FTargetTime + 1;
+
+  if FShellTimer > -1 then
+    if FShellTimer = 0 then
+    begin
+      if FShellType = SHELL_SHELL then
+        g_Player_CreateShell(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                             FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                             GameVelX, GameVelY-2, SHELL_SHELL)
+      else if FShellType = SHELL_DBLSHELL then
+      begin
+        g_Player_CreateShell(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                             FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                             GameVelX-1, GameVelY-2, SHELL_SHELL);
+        g_Player_CreateShell(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                             FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                             GameVelX+1, GameVelY-2, SHELL_SHELL);
+      end;
+      FShellTimer := -1;
+    end else Dec(FShellTimer);
+
+// Ïðîáóåì óâåðíóòüñÿ îò ëåòÿùåé ïóëè:
+  if fall then
+    if (FState in [STATE_GO, STATE_RUN, STATE_RUNOUT,
+                   STATE_ATTACK, STATE_SHOOT]) then
+      if g_Weapon_Danger(FUID, FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y,
+                         FObj.Rect.Width, FObj.Rect.Height, 50) then
+        if (g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj)) and
+           (FObj.Accel.Y = 0) then
+          FObj.Vel.Y := -MONSTERTABLE[FMonsterType].Jump;
+
+  case FState of
+    STATE_PAIN: // Ñîñòîÿíèå - Áîëü
+      begin
+      // Áîëü ñèëüíàÿ => ìîíñòð êðè÷èò:
+        if FPain >= MONSTERTABLE[FMonsterType].Pain then
+        begin
+          FPain := MONSTERTABLE[FMonsterType].Pain;
+          PainSound();
+        end;
+
+      // Ñíèæàåì áîëü ñî âðåìåíåì:
+        FPain := FPain - 5;
+
+      // Áîëü óæå íå îøóòèìàÿ => èäåì äàëüøå:
+        if FPain <= MONSTERTABLE[FMonsterType].MinPain then
+        begin
+          SetState(STATE_GO);
+          FPain := 0;
+        end;
+      end;
+
+    STATE_SLEEP: // Ñîñòîÿíèå - Ñîí
+      begin
+      // Ñïèì:
+        FSleep := FSleep + 1;
+
+      // Ïðîñïàëè äîñòàòî÷íî:
+        if FSleep >= 18 then
+          FSleep := 0
+        else // åùå ñïèì
+          goto _end;
+      end;
+
+    STATE_WAIT: // Ñîñòîÿíèå - Îæèäàíèå
+      begin
+      // Æäåì:
+        FSleep := FSleep - 1;
+      end;
+
+    STATE_GO: // Ñîñòîÿíèå - Äâèæåíèå (ñ îñìîòðîì ñèòóàöèè)
+      begin
+      // Åñëè íàòêíóëèñü íà ÁëîêÌîí - óáåãàåì îò íåãî:
+        if WordBool(st and MOVE_BLOCK) then
+        begin
+          Turn();
+          FSleep := 40;
+          SetState(STATE_RUNOUT);
+
+          goto _end;
+        end;
+
+      // Åñëè ìîíñòð - êîëäóí, òî ïðîáóåì âîñêðåñèòü êîãî-íèáóäü:
+        if (FMonsterType = MONSTER_VILE) then
+          if isCorpse(@FObj, False) <> -1 then
+          begin
+            SetState(STATE_ATTACK, ANIM_ATTACK2);
+            FObj.Vel.X := 0;
+
+            goto _end;
+          end;
+
+      // Åñëè öåëü ïî÷òè íà îäíîé âåðòèêàëè - áåãàåì òóäà-ñþäà:
+        if Abs(sx) < 40 then
+          if FMonsterType <> MONSTER_FISH then
+          begin
+            SetState(STATE_RUN);
+            FSleep := 15;
+
+            goto _end;
+          end;
+
+      // Óïåðëèñü â ñòåíó:
+        if WordBool(st and MOVE_HITWALL) then
+        begin
+          case FMonsterType of
+            MONSTER_CACO, MONSTER_SOUL, MONSTER_PAIN, MONSTER_FISH: ;
+            else // Íå ëåòàþò:
+              if (g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj)) and
+                 (FObj.Accel.Y = 0) then
+              begin // Ñòîèì íà òâåðäîì ïîëó èëè ñòóïåíè
+              // Ïðûæîê ÷åðåç ñòåíó:
+                FObj.Vel.Y := -MONSTERTABLE[FMonsterType].Jump;
+                SetState(STATE_CLIMB);
+              end;
+          end;
+
+          goto _end;
+        end;
+
+      // Ìîíñòðû, íå ïîäâåðæåííûå ãðàâèòàöèè:
+        if (FMonsterType = MONSTER_CACO) or (FMonsterType = MONSTER_SOUL) or
+           (FMonsterType = MONSTER_PAIN) or (FMonsterType = MONSTER_FISH) then
+          begin
+            if FMonsterType = MONSTER_FISH then
+              begin // Ðûáà
+                if not WordBool(st and MOVE_INWATER) then
+                  begin // Ðûáà âíå âîäû:
+                    if g_Obj_CollideLevel(@FObj, 0, 1) or g_Obj_StayOnStep(@FObj) then
+                    begin // "Ñòîèò" òâåðäî
+                    // Ðûáà òðåïûõàåòñÿ íà ïîâåðõíîñòè:
+                      if FObj.Accel.Y = 0 then
+                        FObj.Vel.Y := -6;
+                      FObj.Accel.X := FObj.Accel.X - 8 + Random(17);
+                    end;
+
+                  // Ðûáå áîëüíî:
+                    SetState(STATE_PAIN);
+                    FPain := FPain + 50;
+                  end
+                else // Ðûáà â âîäå
+                  begin
+                  // Ïëûâåì â ñòîðîíó öåëè ïî-âåðòèêàëè:
+                    if Abs(sy) > 8 then
+                      FObj.Vel.Y := g_basic.Sign(sy)*4
+                    else
+                      FObj.Vel.Y := 0;
+
+                  // Ðûáà ïëûâåò ââåðõ:
+                    if FObj.Vel.Y < 0 then
+                      if not g_Obj_CollideWater(@FObj, 0, -16) then
+                      begin
+                      // Âñïëûëè äî ïîâåðõíîñòè - ñòîï:
+                        FObj.Vel.Y := 0;
+                      // Ïëàâàåì òóäà-ñþäà:
+                        SetState(STATE_RUN);
+                        FSleep := 20;
+                      end;
+                   end;
+              end
+            else // Ëåòàþùèå ìîíñòðû
+              begin
+              // Ëåòèì â ñòîðîíó öåëè ïî-âåðòèêàëè:
+                if Abs(sy) > 8 then
+                  FObj.Vel.Y := g_basic.Sign(sy)*4
+                else
+                  FObj.Vel.Y := 0;
+              end;
+          end
+        else // "Íàçåìíûå" ìîíñòðû
+          begin
+          // Âîçìîæíî, ïèíàåì êóñêè:
+            if (FObj.Vel.X <> 0) and (gGibs <> nil) then
+            begin
+              b := Abs(FObj.Vel.X);
+              if b > 1 then b := b * (Random(8 div b) + 1);
+              for a := 0 to High(gGibs) do
+                if gGibs[a].Live and
+                   g_Obj_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y+FObj.Rect.Height-4,
+                                 FObj.Rect.Width, 8, @gGibs[a].Obj) and (Random(3) = 0) then
+                  // Ïèíàåì êóñêè
+                  if FObj.Vel.X < 0 then
+                    g_Obj_PushA(@gGibs[a].Obj, b, Random(61)+120) // íàëåâî
+                  else
+                    g_Obj_PushA(@gGibs[a].Obj, b, Random(61));    // íàïðàâî
+            end;
+          // Áîññû ìîãóò ïèíàòü òðóïû:
+            if (FMonsterType in [MONSTER_CYBER, MONSTER_SPIDER, MONSTER_ROBO]) and
+               (FObj.Vel.X <> 0) and (gCorpses <> nil) then
+            begin
+              b := Abs(FObj.Vel.X);
+              if b > 1 then b := b * (Random(8 div b) + 1);
+              for a := 0 to High(gCorpses) do
+                if (gCorpses[a] <> nil) and (gCorpses[a].State > 0) then
+                begin
+                  co := gCorpses[a].Obj;
+                  if g_Obj_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y+FObj.Rect.Height-4,
+                                   FObj.Rect.Width, 8, @co) and (Random(3) = 0) then
+                    // Ïèíàåì òðóïû
+                    if FObj.Vel.X < 0 then
+                      gCorpses[a].Damage(b*2, -b, Random(7)) // íàëåâî
+                    else
+                      gCorpses[a].Damage(b*2, b, Random(7)); // íàïðàâî
+                end;
+            end;
+          end;
+
+        FSleep := FSleep + 1;
+
+      // Èíîãäà ðû÷èì:
+        if FSleep >= 8 then
+        begin
+          FSleep := 0;
+          if Random(8) = 0 then
+            ActionSound();
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_RUN: // Ñîñòîÿíèå - Áåã
+      begin
+      // Åñëè íàòêíóëèñü íà ÁëîêÌîí - óáåãàåì îò íåãî:
+        if WordBool(st and MOVE_BLOCK) then
+        begin
+          SetState(STATE_RUNOUT);
+          FSleep := 40;
+
+          goto _end;
+        end;
+
+        FSleep := FSleep - 1;
+
+      // Ïðîáåæàëè äîñòàòî÷íî èëè âðåçàëèñü â ñòåíó => ïåðåõîäèì íà øàã:
+        if (FSleep <= 0) or (WordBool(st and MOVE_HITWALL) and ((FObj.Vel.Y+FObj.Accel.Y) = 0)) then
+        begin
+          SetState(STATE_GO);
+          FSleep := 0;
+
+        // Èíîãäà ðû÷èì:
+          if Random(8) = 0 then
+            ActionSound();
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_RUNOUT: // Ñîñòîÿíèå - Óáåãàåò îò ÷åãî-òî
+      begin
+      // Âûøëè èç ÁëîêÌîíà:
+        if (not WordBool(st and MOVE_BLOCK)) and (FSleep > 0) then
+          FSleep := 0;
+
+        FSleep := FSleep - 1;
+
+      // Óáàæåëè äîñòàòî÷íî äàëåêî => ïåðåõîäèì íà øàã:
+        if FSleep <= -18 then
+        begin
+          SetState(STATE_GO);
+          FSleep := 0;
+
+        // Èíîãäà ðû÷èì:
+          if Random(8) = 0 then
+            ActionSound();
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_CLIMB: // Ñîñòîÿíèå - Ïðûæîê (÷òîáû îáîéòè ñòåíó)
+      begin
+      // Äîñòèãëè âûñøåé òî÷êè ïðûæêà èëè ñòåíà êîí÷èëàñü => ïåðåõîäèì íà øàã:
+        if ((FObj.Vel.Y+FObj.Accel.Y) >= 0) or
+           (not WordBool(st and MOVE_HITWALL)) then
+        begin
+          SetState(STATE_GO);
+          FSleep := 0;
+
+        // Ñòåíà íå êîí÷èëàñü => áåæèì îò íåå:
+          if WordBool(st and (MOVE_HITWALL or MOVE_BLOCK)) then
+          begin
+            SetState(STATE_RUN);
+            FSleep := 15;
+          end;
+        end;
+
+      // Áåæèì â âûáðàííóþ ñòîðîíó:
+        if FDirection = D_RIGHT then
+          FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+        else
+          FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+
+      // Åñëè â âîäå, òî çàìåäëÿåìñÿ:
+        if WordBool(st and MOVE_INWATER) then
+          FObj.Vel.X := FObj.Vel.X div 2
+        else // Ðûáàì íå íóæíî çàìåäëÿòüñÿ
+          if FMonsterType = MONSTER_FISH then
+            FObj.Vel.X := 0;
+      end;
+
+    STATE_ATTACK, // Ñîñòîÿíèå - Àòàêà
+    STATE_SHOOT:  // Ñîñòîÿíèå - Ñòðåëüáà
+      begin
+      // Lost_Soul âðåçàëñÿ â ñòåíó ïðè àòàêå => ïåðåõîäèò íà øàã:
+        if FMonsterType = MONSTER_SOUL then
+        begin
+          if WordBool(st and (MOVE_HITWALL or MOVE_HITCEIL or MOVE_HITLAND)) then
+            SetState(STATE_GO);
+
+          goto _end;
+        end;
+
+      // Çàìåäëÿåìñÿ ïðè àòàêå:
+        if FMonsterType <> MONSTER_FISH then
+          FObj.Vel.X := z_dec(FObj.Vel.X, 1);
+
+      // Íóæíî ñòðåëÿòü, à ìîíñòð - êîëäóí:
+        if (FMonsterType = MONSTER_VILE) and (FState = STATE_SHOOT) then
+        begin
+        // Öåëü ïîãèáëà => èäåì äàëüøå:
+          if not GetPos(FTargetUID, @o) then
+          begin
+            SetState(STATE_GO);
+
+            goto _end;
+          end;
+
+        // Öåëü íå âèäíî => èäåì äàëüøå:
+          if not g_Look(@FObj, @o, FDirection) then
+          begin
+            SetState(STATE_GO);
+
+            goto _end;
+          end;
+
+        // Öåëü â âîäå - íå çàãîðèòñÿ => èäåì äàëüøå:
+          if g_Obj_CollideWater(@o, 0, 0) then
+          begin
+            SetState(STATE_GO);
+
+            goto _end;
+          end;
+        end;
+      end;
+  end; // case FState of ...
+
+_end:
+
+// Ñîñòîÿíèå - Âîñêðåøåíèå:
+  if FState = STATE_REVIVE then
+    if FAnim[FCurAnim, FDirection].Played then
+    begin // Îáðàòíàÿ àíèìàöèÿ óìèðàíèÿ çàêîí÷èëàñü - èäåì äàëüøå:
+      FAnim[FCurAnim, FDirection].Revert(False);
+      SetState(STATE_GO);
+    end;
+
+// Åñëè åñòü àíèìàöèÿ îãíÿ êîëäóíà - ïóñòü îíà èäåò:
+  if vilefire <> nil then
+    vilefire.Update();
+
+// Ñîñòîÿíèå - Óìèðàåò è òåêóùàÿ àíèìàöèÿ ïðîèãðàíà:
+  if (FState = STATE_DIE) and
+     (FAnim[FCurAnim, FDirection] <> nil) and
+     (FAnim[FCurAnim, FDirection].Played) then
+    begin
+    // Óìåð:
+      SetState(STATE_DEAD);
+
+    // Ó ýòèõ ìîíñòðîâ íåò òðóïîâ:
+      if (FMonsterType = MONSTER_PAIN) or
+         (FMonsterType = MONSTER_SOUL) or
+         (FMonsterType = MONSTER_BARREL) then
+        FRemoved := True
+      else
+        FAnim[FCurAnim, FDirection].CurrentFrame := FAnim[FCurAnim, FDirection].TotalFrames - 1;
+    end;
+
+// Ñîâåðøåíèå àòàêè è ñòðåëüáû:
+  if (FState = STATE_ATTACK) or (FState = STATE_SHOOT) then
+    if (FAnim[FCurAnim, FDirection] <> nil) then
+    // Àíèìàöèÿ àòàêè åñòü - ìîæíî àòàêîâàòü
+      if (FAnim[FCurAnim, FDirection].Played) then
+        begin // Àíèìàöèÿ àòàêè çàêîí÷èëàñü => ïåðåõîäèì íà øàã
+          if FState = STATE_ATTACK then
+            begin // Ñîñòîÿíèå - Àòàêà
+            // Åñëè ìîíñòð íå Lost_Soul, òî ïîñëå àòàêè ïåðåõîäèì íà øàã:
+              if FMonsterType <> MONSTER_SOUL then
+                SetState(STATE_GO);
+            end
+          else // Ñîñòîÿíèå - Ñòðåëüáà
+            begin
+            // Ïåðåõîäèì íà øàã, åñëè íå íàäî ñòðåëÿòü åùå ðàç:
+              if not FChainFire then
+                SetState(STATE_GO)
+              else
+                begin // Íàäî ñòðåëÿòü åùå
+                  FChainFire := False;
+                // Ò.ê. ñîñòîÿíèå íå èçìåíèëîñü, è íóæåí
+                // íîâûé öèêë îæèäàíèÿ çàâåðøåíèÿ àíèìàöèè:
+                  FAnim[FCurAnim, FDirection].Reset();
+                end;
+            end;
+
+          FWaitAttackAnim := False;
+        end
+        
+      else // Àíèìàöèÿ àòàêè åùå èäåò (èñêëþ÷åíèå - Lost_Soul):
+        if (FMonsterType = MONSTER_SOUL) or
+           ( (not FWaitAttackAnim) and
+             (FAnim[FCurAnim, FDirection].CurrentFrame =
+              (FAnim[FCurAnim, FDirection].TotalFrames div 2))
+           ) then
+        begin // Àòàêè åùå íå áûëî è ýòî ñåðåäèíà àíèìàöèè àòàêè
+          if FState = STATE_ATTACK then
+            begin // Ñîñòîÿíèå - Àòàêà
+            // Åñëè ýòî Lost_Soul, òî ñáðàñûâàåì àíèìàöèþ àòàêè:
+              if FMonsterType = MONSTER_SOUL then
+                FAnim[FCurAnim, FDirection].Reset();
+
+              case FMonsterType of
+                MONSTER_SOUL, MONSTER_IMP, MONSTER_DEMON:
+                // Ãðûçåì ïåðâîãî ïîïàâøåãîñÿ:
+                  if g_Weapon_Hit(@FObj, 15, FUID, HIT_SOME) <> 0 then
+                  // Lost_Soul óêóñèë êîãî-òî => ïåðåõîäèò íà øàã:
+                    if FMonsterType = MONSTER_SOUL then
+                      SetState(STATE_GO);
+                          
+                MONSTER_FISH:
+                  g_Weapon_Hit(@FObj, 10, FUID, HIT_SOME);
+
+                MONSTER_SKEL, MONSTER_ROBO:
+                // Ðîáîò èëè ñêåëåò ñèëüíî ïèíàþòñÿ:
+                  if FCurAnim = ANIM_ATTACK2 then
+                  begin
+                    o := FObj;
+                    o.Vel.X := IfThen(FDirection = D_RIGHT, 1, -1)*50;
+                    g_Weapon_Hit(@o, 50, FUID, HIT_SOME);
+                  end;
+
+                MONSTER_VILE:
+                // Êîëäóí ïûòàåòñÿ âîñêðåøàòü:
+                  if FCurAnim = ANIM_ATTACK2 then
+                  begin
+                    sx := isCorpse(@FObj, True);
+                    if sx <> -1 then
+                    begin // Íàøëè, êîãî âîñêðåñèòü
+                      g_Sound_PlayExAt('SOUND_MONSTER_SLOP', FObj.X, FObj.Y);
+                    // Âîñêðåøàòü - ñåáå âðåäèòü:
+                      {g_Weapon_HitUID(FUID, 5, 0, HIT_SOME);}
+                    end;
+                  end;
+              end;
+            end
+
+          else // Ñîñòîÿíèå - Ñòðåëüáà
+            begin
+            // Ñêîðîñòðåëüíûå ìîíñòðû:
+              if (FMonsterType = MONSTER_CGUN) or
+                 (FMonsterType = MONSTER_SPIDER) or
+                 (FMonsterType = MONSTER_BSP) or
+                 (FMonsterType = MONSTER_MANCUB) or
+                 (FMonsterType = MONSTER_ROBO) then
+                if not GetPos(FTargetUID, @o) then
+                // Öåëü ìåðòâà - èùåì íîâóþ:
+                  findNewPrey()
+                else // Öåëü æèâà - ïðîäîëæàåì ñòðåëÿòü:
+                  if shoot(@o, False) then
+                    FChainFire := True;
+            end;
+
+        // Àòàêà òîëüêî 1 ðàç çà àíèìàöèþ àòàêè:
+          FWaitAttackAnim := True;
+        end;
+
+// Ïîñëåäíèé êàäð òåêóùåé àíèìàöèè:
+  if FAnim[FCurAnim, FDirection].Counter = FAnim[FCurAnim, FDirection].Speed-1 then
+    case FState of
+      STATE_GO, STATE_RUN, STATE_CLIMB, STATE_RUNOUT:
+      // Çâóêè ïðè ïåðåäâèæåíèè:
+        case FMonsterType of
+          MONSTER_CYBER:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 2) then
+              g_Sound_PlayExAt('SOUND_MONSTER_CYBER_WALK', FObj.X, FObj.Y);
+          MONSTER_SPIDER:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 2) then
+              g_Sound_PlayExAt('SOUND_MONSTER_SPIDER_WALK', FObj.X, FObj.Y);
+          MONSTER_BSP:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 2) then
+              g_Sound_PlayExAt('SOUND_MONSTER_BSP_WALK', FObj.X, FObj.Y);
+          MONSTER_ROBO:
+            if (FAnim[FCurAnim, FDirection].CurrentFrame = 0) or
+               (FAnim[FCurAnim, FDirection].CurrentFrame = 5) then
+              g_Sound_PlayExAt('SOUND_MONSTER_BSP_WALK', FObj.X, FObj.Y);
+        end;
+    end;
+
+// Êîñòûëü äëÿ ïîòîêîâ
+  if g_Obj_CollidePanel(@FObj, 0, 0, PANEL_LIFTLEFT or PANEL_LIFTRIGHT) and
+     not ((FState = STATE_DEAD) or (FState = STATE_DIE))  then
+    FObj.Vel.X := oldvelx;
+    
+// Åñëè åñòü àíèìàöèÿ, òî ïóñòü îíà èäåò:
+  if FAnim[FCurAnim, FDirection] <> nil then
+    FAnim[FCurAnim, FDirection].Update();
+end;
+
+procedure TMonster.ClientAttack(wx, wy, tx, ty: Integer);
+begin
+  case FMonsterType of
+    MONSTER_ZOMBY:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIREPISTOL', wx, wy);
+      g_Player_CreateShell(wx, wy, 0, -2, SHELL_BULLET);
+    end;
+    MONSTER_SERG:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN', wx, wy);
+      FShellTimer := 10;
+      FShellType := SHELL_SHELL;
+    end;
+    MONSTER_MAN:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN2', wx, wy);
+      FShellTimer := 13;
+      FShellType := SHELL_DBLSHELL;
+    end;
+    MONSTER_CGUN, MONSTER_SPIDER:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRECGUN', wx, wy);
+      g_Player_CreateShell(wx, wy, 0, -2, SHELL_BULLET);
+    end;
+    MONSTER_IMP:
+      g_Weapon_ball1(wx, wy, tx, ty, FUID);
+    MONSTER_CYBER:
+      g_Weapon_rocket(wx, wy, tx, ty, FUID);
+    MONSTER_SKEL:
+      g_Weapon_revf(wx, wy, tx, ty, FUID, FTargetUID);
+    MONSTER_BSP:
+      g_Weapon_aplasma(wx, wy, tx, ty, FUID);
+    MONSTER_ROBO:
+      g_Weapon_plasma(wx, wy, tx, ty, FUID);
+    MONSTER_MANCUB:
+      g_Weapon_manfire(wx, wy, tx, ty, FUID);
+    MONSTER_BARON, MONSTER_KNIGHT:
+      g_Weapon_ball7(wx, wy, tx, ty, FUID);
+    MONSTER_CACO:
+      g_Weapon_ball2(wx, wy, tx, ty, FUID);
+  end;
+end;
+
+procedure TMonster.Turn();
+begin
+// Ðàçâîðà÷èâàåìñÿ:
+  if FDirection = D_LEFT then
+    FDirection := D_RIGHT
+  else
+    FDirection := D_LEFT;
+
+// Áåæèì â âûáðàííóþ ñòîðîíó:
+  if FDirection = D_RIGHT then
+    FObj.Vel.X := MONSTERTABLE[FMonsterType].RunVel
+  else
+    FObj.Vel.X := -MONSTERTABLE[FMonsterType].RunVel;
+end;
+
+function TMonster.findNewPrey(): Boolean;
+var
+  a: DWORD;
+  l, l2: Integer;
+  PlayersSee, MonstersSee: Array of DWORD;
+  PlayerNear, MonsterNear: Integer;
+begin
+  Result := False;
+  SetLength(MonstersSee, 0);
+  SetLength(PlayersSee, 0);
+
+  FTargetUID := 0;
+  l := 32000;
+  PlayerNear := -1;
+  MonsterNear := -1;
+
+  // Ïîåõàâøèå, êàííèáàëû, è äîáðûå èãðîêîâ íå òðîãàþò
+  if (gPlayers <> nil) and (FBehaviour <> BH_INSANE) and
+  (FBehaviour <> BH_CANNIBAL) and (FBehaviour <> BH_GOOD) then
+    for a := 0 to High(gPlayers) do
+      if (gPlayers[a] <> nil) and (gPlayers[a].Live)
+      and (not gPlayers[a].NoTarget) and (gPlayers[a].FMegaRulez[MR_INVIS] < gTime) then
+      begin
+        if g_Look(@FObj, @gPlayers[a].Obj, FDirection) then
+        begin
+          SetLength(PlayersSee, Length(PlayersSee) + 1);
+          PlayersSee[High(PlayersSee)] := a;
+        end;
+        l2 := Abs(gPlayers[a].GameX-FObj.X)+
+              Abs(gPlayers[a].GameY-FObj.Y);
+        if l2 < l then
+        begin
+          l := l2;
+          PlayerNear := Integer(a);
+        end;
+      end;
+
+  // Êèëëåðû è äîáðûå íå òðîãàþò ìîíñòðîâ
+  if (gMonsters <> nil) and (FBehaviour <> BH_KILLER) and (FBehaviour <> BH_GOOD) then
+    for a := 0 to High(gMonsters) do
+      if (gMonsters[a] <> nil) and (gMonsters[a].Live) and
+         (gMonsters[a].FUID <> FUID) then
+      begin
+        if (FBehaviour = BH_CANNIBAL) and (gMonsters[a].FMonsterType <> FMonsterType) then
+          Continue; // Êàííèáàëû àòàêóþò òîëüêî ñîðîäè÷åé
+        if (FBehaviour = BH_INSANE) and (gMonsters[a].FMonsterType <> MONSTER_BARREL)
+        and (IsFriend(gMonsters[a].FMonsterType, FMonsterType)) then
+          Continue; // Ïîåõàâøèå íå òðîãàþò äðóçåé, íî èì íå íðàâÿòñÿ áî÷êè
+        if ((FBehaviour = BH_NORMAL) or (FBehaviour = BH_MANIAC))
+        and (IsFriend(gMonsters[a].FMonsterType, FMonsterType)) then
+          Continue; // Îñòàâøèåñÿ òèïû, êðîìå êàííèáàëîâ, íå òðîãàþò ñâîèõ äðóçåé
+
+        if g_Look(@FObj, @gMonsters[a].Obj, FDirection) then
+        begin
+          SetLength(MonstersSee, Length(MonstersSee) + 1);
+          MonstersSee[High(MonstersSee)] := a;
+        end;
+        l2 := Abs(gMonsters[a].FObj.X-FObj.X)+
+              Abs(gMonsters[a].FObj.Y-FObj.Y);
+        if l2 < l then
+        begin
+          l := l2;
+          MonsterNear := Integer(a);
+        end;
+      end;
+
+  case FBehaviour of
+    BH_NORMAL, BH_KILLER:
+    begin
+      // Îáû÷íûé è êèëëåð ñíà÷àëà èùóò èãðîêîâ â ïîëå çðåíèÿ
+      if (FTargetUID = 0) and (Length(PlayersSee) > 0) then
+      begin
+        a := PlayersSee[Random(Length(PlayersSee))];
+        FTargetUID := gPlayers[a].UID;
+      end;
+      // Çàòåì ïîáëèçîñòè
+      if (FTargetUID = 0) and (PlayerNear > -1) then
+      begin
+        a := PlayerNear;
+        FTargetUID := gPlayers[a].UID;
+      end;
+      // Ïîòîì îáû÷íûå èùóò ìîíñòðîâ â ïîëå çðåíèÿ
+      if (FTargetUID = 0) and (Length(MonstersSee) > 0) then
+      begin
+        a := MonstersSee[Random(Length(MonstersSee))];
+        FTargetUID := gMonsters[a].UID;
+      end;
+      // Çàòåì ïîáëèçîñòè
+      if (FTargetUID = 0) and (MonsterNear > -1) then
+      begin
+        a := MonsterNear;
+        FTargetUID := gMonsters[a].UID;
+      end;
+    end;
+    BH_MANIAC, BH_INSANE, BH_CANNIBAL:
+    begin
+      // Ìàíüÿêè, ïîåõàâøèå è êàííèáàëû ñíà÷àëà èñòðåáëÿþò âñ¸ â ïîëå çðåíèÿ
+      if (FTargetUID = 0) and (Length(PlayersSee) > 0) then
+      begin
+        a := PlayersSee[Random(Length(PlayersSee))];
+        FTargetUID := gPlayers[a].UID;
+      end;
+      if (FTargetUID = 0) and (Length(MonstersSee) > 0) then
+      begin
+        a := MonstersSee[Random(Length(MonstersSee))];
+        FTargetUID := gMonsters[a].UID;
+      end;
+      // Çàòåì èùóò êîãî-òî ïîáëèçîñòè
+      if (FTargetUID = 0) and (PlayerNear > -1) then
+      begin
+        a := PlayerNear;
+        FTargetUID := gPlayers[a].UID;
+      end;
+      if (FTargetUID = 0) and (MonsterNear > -1) then
+      begin
+        a := MonsterNear;
+        FTargetUID := gMonsters[a].UID;
+      end;
+    end;
+  end;
+
+// Åñëè è ìîíñòðîâ íåò - íà÷èíàåì æäàòü öåëü:
+  if FTargetUID = 0 then
+    begin
+      // Ïîåõàâøèé ïûòàåòñÿ ñàìîóáèòüñÿ
+      if FBehaviour = BH_INSANE then
+        FTargetUID := FUID
+      else
+        FTargetTime := MAX_ATM;
+    end
+  else
+    begin // Öåëü íàøëè
+      FTargetTime := 0;
+      Result := True;
+    end;
+end;
+
+function TMonster.kick(o: PObj): Boolean;
+begin
+  Result := False;
+
+  case FMonsterType of
+    MONSTER_FISH:
+      begin
+        SetState(STATE_ATTACK);
+        Result := True;
+      end;
+    MONSTER_DEMON:
+      begin
+        SetState(STATE_ATTACK);
+        g_Sound_PlayExAt('SOUND_MONSTER_DEMON_ATTACK', FObj.X, FObj.Y);
+        Result := True;
+      end;
+    MONSTER_IMP:
+      begin
+        SetState(STATE_ATTACK);
+        g_Sound_PlayExAt('SOUND_MONSTER_IMP_ATTACK', FObj.X, FObj.Y);
+        Result := True;
+      end;
+    MONSTER_SKEL:
+      begin
+        SetState(STATE_ATTACK, ANIM_ATTACK2);
+        g_Sound_PlayExAt('SOUND_MONSTER_SKEL_ATTACK', FObj.X, FObj.Y);
+        Result := True;
+      end;
+    MONSTER_ROBO:
+      begin
+        SetState(STATE_ATTACK, ANIM_ATTACK2);
+        g_Sound_PlayExAt('SOUND_MONSTER_SKEL_ATTACK', FObj.X, FObj.Y);
+        Result := True;
+      end;
+    MONSTER_BARON, MONSTER_KNIGHT,
+    MONSTER_CACO, MONSTER_MANCUB:
+    // Ýòè ìîíñòðû íå ïèíàþò - îíè ñòðåëÿþò â óïîð:
+      if not g_Game_IsClient then Result := shoot(o, True);
+  end;
+end;
+
+function TMonster.shoot(o: PObj; immediately: Boolean): Boolean;
+var
+  xd, yd, m: Integer;
+begin
+  Result := False;
+
+// Ñòðåëÿòü ðàíî:
+  if FAmmo < 0 then
+    Exit;
+
+// Æäàòü âðåìåíè ãîòîâíîñòè ê âûñòðåëó:
+  if not immediately then
+    case FMonsterType of
+      MONSTER_FISH, MONSTER_BARREL, MONSTER_DEMON:
+        Exit; // íå ñòðåëÿþò
+      MONSTER_CGUN, MONSTER_BSP, MONSTER_ROBO:
+        begin
+          FAmmo := FAmmo + 1;
+        // Âðåìÿ âûñòðåëà óïóùåíî:
+          if FAmmo >= 50 then
+            FAmmo := IfThen(FMonsterType = MONSTER_ROBO, -200, -50);
+        end;
+      MONSTER_MAN: ;
+      MONSTER_MANCUB:
+        begin
+          FAmmo := FAmmo + 1;
+        // Âðåìÿ âûñòðåëà óïóùåíî:
+          if FAmmo >= 5 then
+            FAmmo := -50;
+        end;
+      MONSTER_SPIDER:
+        begin
+          FAmmo := FAmmo + 1;
+        // Âðåìÿ âûñòðåëà óïóùåíî:
+          if FAmmo >= 100 then
+            FAmmo := -50;
+        end;
+      MONSTER_CYBER:
+        begin
+        // Ñòðåëÿåò íå âñåãäà:
+          if Random(2) = 0 then
+            Exit;
+          FAmmo := FAmmo + 1;
+        // Âðåìÿ âûñòðåëà óïóùåíî:
+          if FAmmo >= 10 then
+            FAmmo := -50;
+        end;
+      MONSTER_BARON, MONSTER_KNIGHT: if Random(8) <> 0 then Exit;
+      MONSTER_SKEL: if Random(32) <> 0 then Exit;
+      MONSTER_VILE: if Random(8) <> 0 then Exit;
+      MONSTER_PAIN: if Random(8) <> 0 then Exit;
+      else if Random(16) <> 0 then Exit;
+    end;
+
+// Öåëè íå âèäíî:
+  if not g_Look(@FObj, o, FDirection) then
+    Exit;
+
+  FTargetTime := 0;
+
+  tx := o^.X+o^.Rect.X+(o^.Rect.Width div 2)+((o^.Vel.X{+o^.Accel.X})*12);
+  ty := o^.Y+o^.Rect.Y+(o^.Rect.Height div 2)+((o^.Vel.Y{+o^.Accel.Y})*12);
+
+// Ðàçíèöà ïî âûñîòå áîëüøå ðàçíèöû ïî ãîðèçîíòàëè
+// (íå ìîæåò ñòðåëÿòü ïîä òàêèì áîëüøèì óãëîì):
+  if Abs(tx-(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2))) <
+     Abs(ty-(FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2))) then
+    Exit;
+
+  case FMonsterType of
+    MONSTER_IMP, MONSTER_BARON, MONSTER_KNIGHT, MONSTER_CACO:
+      begin
+        SetState(STATE_SHOOT);
+        {nn}
+      end;
+    MONSTER_SKEL:
+      begin
+        SetState(STATE_SHOOT);
+        {nn}
+      end;
+    MONSTER_VILE:
+      begin // Çàæèãàåì îãîíü
+        tx := o^.X+o^.Rect.X+(o^.Rect.Width div 2);
+        ty := o^.Y+o^.Rect.Y;
+        SetState(STATE_SHOOT);
+
+        vilefire.Reset();
+
+        g_Sound_PlayExAt('SOUND_MONSTER_VILE_ATTACK', FObj.X, FObj.Y);
+        g_Sound_PlayExAt('SOUND_FIRE', o^.X, o^.Y);
+      end;
+    MONSTER_SOUL:
+      begin // Ëåòèò â ñòîðîíó öåëè:
+        SetState(STATE_ATTACK);
+        g_Sound_PlayExAt('SOUND_MONSTER_SOUL_ATTACK', FObj.X, FObj.Y);
+
+        xd := tx-(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2));
+        yd := ty-(FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2));
+        m := Max(Abs(xd), Abs(yd));
+        if m = 0 then
+          m := 1;
+
+        FObj.Vel.X := (xd*16) div m;
+        FObj.Vel.Y := (yd*16) div m;
+      end;
+    MONSTER_MANCUB, MONSTER_ZOMBY, MONSTER_SERG, MONSTER_BSP, MONSTER_ROBO,
+    MONSTER_CYBER, MONSTER_CGUN, MONSTER_SPIDER, MONSTER_PAIN, MONSTER_MAN:
+      begin
+      // Ìàíêóáóñ ðÿâêàåò ïåðåä ïåðâîé àòàêîé:
+        if FMonsterType = MONSTER_MANCUB then
+          if FAmmo = 1 then
+            g_Sound_PlayExAt('SOUND_MONSTER_MANCUB_ATTACK', FObj.X, FObj.Y);
+
+        SetState(STATE_SHOOT);
+      end;
+    else Exit;
+  end;
+
+  Result := True;
+end;
+
+function TMonster.Live(): Boolean;
+begin
+  Result := (FState <> STATE_DIE) and (FState <> STATE_DEAD) and (FHealth > 0);
+end;
+
+procedure TMonster.SetHealth(aH: Integer);
+begin
+  if (aH > 0) and (aH < 1000000) then
+  begin
+    FHealth := aH;
+    if FHealth > FMaxHealth then
+      FMaxHealth := FHealth;
+  end;
+end;
+
+procedure TMonster.WakeUp();
+begin
+  if g_Game_IsClient then Exit;
+  SetState(STATE_GO);
+  FTargetTime := MAX_ATM;
+  WakeUpSound();
+end;
+
+procedure TMonster.SaveState(var Mem: TBinMemoryWriter);
+var
+  i: Integer;
+  sig: DWORD;
+  b: Byte;
+  anim: Boolean;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà ìîíñòðà:
+  sig := MONSTER_SIGNATURE; // 'MONS'
+  Mem.WriteDWORD(sig);
+// UID ìîíñòðà:
+  Mem.WriteWord(FUID);
+// Íàïðàâëåíèå:
+  if FDirection = D_LEFT then
+    b := 1
+  else // D_RIGHT
+    b := 2;
+  Mem.WriteByte(b);
+// Íàäî ëè óäàëèòü åãî:
+  Mem.WriteBoolean(FRemoved);
+// Îñòàëîñü çäîðîâüÿ:
+  Mem.WriteInt(FHealth);
+// Ñîñòîÿíèå:
+  Mem.WriteByte(FState);
+// Òåêóùàÿ àíèìàöèÿ:
+  Mem.WriteByte(FCurAnim);
+// UID öåëè:
+  Mem.WriteWord(FTargetUID);
+// Âðåìÿ ïîñëå ïîòåðè öåëè:
+  Mem.WriteInt(FTargetTime);
+// Ïîâåäåíèå ìîíñòðà:
+  Mem.WriteByte(FBehaviour);
+// Ãîòîâíîñòü ê âûñòðåëó:
+  Mem.WriteInt(FAmmo);
+// Áîëü:
+  Mem.WriteInt(FPain);
+// Âðåìÿ îæèäàíèÿ:
+  Mem.WriteInt(FSleep);
+// Îçâó÷èâàòü ëè áîëü:
+  Mem.WriteBoolean(FPainSound);
+// Áûëà ëè àòàêà âî âðåìÿ àíèìàöèè àòàêè:
+  Mem.WriteBoolean(FWaitAttackAnim);
+// Íàäî ëè ñòðåëÿòü íà ñëåäóþùåì øàãå:
+  Mem.WriteBoolean(FChainFire);
+// Ïîäëåæèò ëè ðåñïàâíó:
+  Mem.WriteBoolean(FNoRespawn);
+// Êîîðäèíàòû öåëè:
+  Mem.WriteInt(tx);
+  Mem.WriteInt(ty);
+// ID ìîíñòðà ïðè ñòàðòå êàðòû:
+  Mem.WriteInt(FStartID);
+// Èíäåêñ òðèããåðà, ñîçäàâøåãî ìîíñòðà:
+  Mem.WriteInt(FSpawnTrigger);
+// Îáúåêò ìîíñòðà:
+  Obj_SaveState(@FObj, Mem);
+// Åñòü ëè àíèìàöèÿ îãíÿ êîëäóíà:
+  anim := vilefire <> nil;
+  Mem.WriteBoolean(anim);
+// Åñëè åñòü - ñîõðàíÿåì:
+  if anim then
+    vilefire.SaveState(Mem);
+// Àíèìàöèè:
+  for i := ANIM_SLEEP to ANIM_PAIN do
+  begin
+  // Åñòü ëè ëåâàÿ àíèìàöèÿ:
+    anim := FAnim[i, D_LEFT] <> nil;
+    Mem.WriteBoolean(anim);
+  // Åñëè åñòü - ñîõðàíÿåì:
+    if anim then
+      FAnim[i, D_LEFT].SaveState(Mem);
+  // Åñòü ëè ïðàâàÿ àíèìàöèÿ:
+    anim := FAnim[i, D_RIGHT] <> nil;
+    Mem.WriteBoolean(anim);
+  // Åñëè åñòü - ñîõðàíÿåì:
+    if anim then
+      FAnim[i, D_RIGHT].SaveState(Mem);
+  end;
+end;
+
+procedure TMonster.LoadState(var Mem: TBinMemoryReader);
+var
+  i: Integer;
+  sig: DWORD;
+  b: Byte;
+  anim: Boolean;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà ìîíñòðà:
+  Mem.ReadDWORD(sig);
+  if sig <> MONSTER_SIGNATURE then // 'MONS'
+  begin
+    raise EBinSizeError.Create('TMonster.LoadState: Wrong Monster Signature');
+  end;
+// UID ìîíñòðà:
+  Mem.ReadWord(FUID);
+// Íàïðàâëåíèå:
+  Mem.ReadByte(b);
+  if b = 1 then
+    FDirection := D_LEFT
+  else // b = 2
+    FDirection := D_RIGHT;
+// Íàäî ëè óäàëèòü åãî:
+  Mem.ReadBoolean(FRemoved);
+// Îñòàëîñü çäîðîâüÿ:
+  Mem.ReadInt(FHealth);
+// Ñîñòîÿíèå:
+  Mem.ReadByte(FState);
+// Òåêóùàÿ àíèìàöèÿ:
+  Mem.ReadByte(FCurAnim);
+// UID öåëè:
+  Mem.ReadWord(FTargetUID);
+// Âðåìÿ ïîñëå ïîòåðè öåëè:
+  Mem.ReadInt(FTargetTime);
+// Ïîâåäåíèå ìîíñòðà:
+  Mem.ReadByte(FBehaviour);
+// Ãîòîâíîñòü ê âûñòðåëó:
+  Mem.ReadInt(FAmmo);
+// Áîëü:
+  Mem.ReadInt(FPain);
+// Âðåìÿ îæèäàíèÿ:
+  Mem.ReadInt(FSleep);
+// Îçâó÷èâàòü ëè áîëü:
+  Mem.ReadBoolean(FPainSound);
+// Áûëà ëè àòàêà âî âðåìÿ àíèìàöèè àòàêè:
+  Mem.ReadBoolean(FWaitAttackAnim);
+// Íàäî ëè ñòðåëÿòü íà ñëåäóþùåì øàãå:
+  Mem.ReadBoolean(FChainFire);
+// Ïîäëåæèò ëè ðåñïàâíó
+  Mem.ReadBoolean(FNoRespawn);
+// Êîîðäèíàòû öåëè:
+  Mem.ReadInt(tx);
+  Mem.ReadInt(ty);
+// ID ìîíñòðà ïðè ñòàðòå êàðòû:
+  Mem.ReadInt(FStartID);
+// Èíäåêñ òðèããåðà, ñîçäàâøåãî ìîíñòðà:
+  Mem.ReadInt(FSpawnTrigger);
+// Îáúåêò ìîíñòðà:
+  Obj_LoadState(@FObj, Mem);
+// Åñòü ëè àíèìàöèÿ îãíÿ êîëäóíà:
+  Mem.ReadBoolean(anim);
+// Åñëè åñòü - çàãðóæàåì:
+  if anim then
+  begin
+    Assert(vilefire <> nil, 'TMonster.LoadState: no vilefire anim');
+    vilefire.LoadState(Mem);
+  end;
+// Àíèìàöèè:
+  for i := ANIM_SLEEP to ANIM_PAIN do
+  begin
+  // Åñòü ëè ëåâàÿ àíèìàöèÿ:
+    Mem.ReadBoolean(anim);
+  // Åñëè åñòü - çàãðóæàåì:
+    if anim then
+    begin
+      Assert(FAnim[i, D_LEFT] <> nil,
+        'TMonster.LoadState: no '+IntToStr(i)+'_left anim');
+      FAnim[i, D_LEFT].LoadState(Mem);
+    end;
+  // Åñòü ëè ïðàâàÿ àíèìàöèÿ:
+     Mem.ReadBoolean(anim);
+  // Åñëè åñòü - çàãðóæàåì:
+    if anim then
+    begin
+      Assert(FAnim[i, D_RIGHT] <> nil,
+        'TMonster.LoadState: no '+IntToStr(i)+'_right anim');
+      FAnim[i, D_RIGHT].LoadState(Mem);
+    end;
+  end;
+end;
+
+procedure TMonster.ActivateTriggers();
+var
+  a: Integer;
+begin
+  if FDieTriggers <> nil then
+    for a := 0 to High(FDieTriggers) do
+      g_Triggers_Press(FDieTriggers[a], ACTIVATE_MONSTERPRESS);
+  if FSpawnTrigger > -1 then
+  begin
+    g_Triggers_DecreaseSpawner(FSpawnTrigger);
+    FSpawnTrigger := -1;
+  end;
+end;
+
+procedure TMonster.AddTrigger(t: Integer);
+begin
+  SetLength(FDieTriggers, Length(FDieTriggers)+1);
+  FDieTriggers[High(FDieTriggers)] := t;
+end;
+
+procedure TMonster.ClearTriggers();
+begin
+  SetLength(FDieTriggers, 0);
+end;
+
+end.
diff --git a/src/game/g_net.pas b/src/game/g_net.pas
new file mode 100644 (file)
index 0000000..74defc3
--- /dev/null
@@ -0,0 +1,1033 @@
+unit g_net;
+
+interface
+
+uses
+  e_log, e_fixedbuffer, ENet, ENetTypes, ENetPlatform, Classes;
+
+const
+  NET_PROTOCOL_VER = 164;
+
+  NET_MAXCLIENTS = 24;
+  NET_CHANS = 11;
+
+  NET_CHAN_SERVICE = 0;
+  NET_CHAN_IMPORTANT = 1;
+  NET_CHAN_GAME = 2;
+  NET_CHAN_PLAYER = 3;
+  NET_CHAN_PLAYERPOS = 4;
+  NET_CHAN_MONSTER = 5;
+  NET_CHAN_MONSTERPOS = 6;
+  NET_CHAN_LARGEDATA = 7;
+  NET_CHAN_CHAT = 8;
+  NET_CHAN_DOWNLOAD = 9;
+  NET_CHAN_SHOTS = 10;
+
+  NET_NONE = 0;
+  NET_SERVER = 1;
+  NET_CLIENT = 2;
+
+  NET_BUFSIZE = 65536;
+
+  NET_EVERYONE = -1;
+
+  NET_DISC_NONE: enet_uint32 = 0;
+  NET_DISC_PROTOCOL: enet_uint32 = 1;
+  NET_DISC_VERSION: enet_uint32 = 2;
+  NET_DISC_FULL: enet_uint32 = 3;
+  NET_DISC_KICK: enet_uint32 = 4;
+  NET_DISC_DOWN: enet_uint32 = 5;
+  NET_DISC_PASSWORD: enet_uint32 = 6;
+  NET_DISC_TEMPBAN: enet_uint32 = 7;
+  NET_DISC_BAN: enet_uint32 = 8;
+  NET_DISC_MAX: enet_uint32 = 8;
+
+  NET_STATE_NONE = 0;
+  NET_STATE_AUTH = 1;
+  NET_STATE_GAME = 2;
+
+  BANLIST_FILENAME = 'banlist.txt';
+
+type
+  TNetClient = record
+    ID:      Byte;
+    Used:    Boolean;
+    State:   Byte;
+    Peer:    pENetPeer;
+    Player:  Word;
+    RequestedFullUpdate: Boolean;
+    RCONAuth: Boolean;
+    Voted:    Boolean;
+  end;
+  TBanRecord = record
+    IP: LongWord;
+    Perm: Boolean;
+  end;
+  pTNetClient = ^TNetClient;
+
+  AByte = array of Byte;
+
+var
+  NetInitDone:     Boolean = False;
+  NetMode:         Byte = NET_NONE;
+
+  NetServerName:   string = 'Unnamed Server';
+  NetPassword:     string = '';
+  NetPort:         Word = 25666;
+
+  NetAllowRCON:    Boolean = False;
+  NetRCONPassword: string = '';
+
+  NetTimeToUpdate:   Cardinal = 0;
+  NetTimeToReliable: Cardinal = 0;
+  NetTimeToMaster:   Cardinal = 0;
+
+  NetHost:       pENetHost = nil;
+  NetPeer:       pENetPeer = nil;
+  NetEvent:      ENetEvent;
+  NetAddr:       ENetAddress;
+
+  NetPongAddr:   ENetAddress;
+  NetPongSock:   ENetSocket = ENET_SOCKET_NULL;
+
+  NetUseMaster: Boolean = True;
+  NetSlistAddr: ENetAddress;
+  NetSlistIP:   string = 'mpms.doom2d.org';
+  NetSlistPort: Word = 25665;
+
+  NetClientIP:   string = '127.0.0.1';
+  NetClientPort: Word   = 25666;
+
+  NetIn, NetOut: TBuffer;
+
+  NetClients:     array of TNetClient;
+  NetClientCount: Byte = 0;
+  NetMaxClients:  Byte = 255;
+  NetBannedHosts: array of TBanRecord;
+
+  NetState:      Integer = NET_STATE_NONE;
+
+  NetMyID:       Integer = -1;
+  NetPlrUID1:    Integer = -1;
+  NetPlrUID2:    Integer = -1;
+
+  NetInterpLevel: Integer = 1;
+  NetUpdateRate:  Cardinal = 0;  // as soon as possible
+  NetRelupdRate:  Cardinal = 18; // around two times a second
+  NetMasterRate:  Cardinal = 60000;
+
+  NetForcePlayerUpdate: Boolean = False;
+  NetPredictSelf:       Boolean = True;
+  NetGotKeys:           Boolean = False;
+
+  NetGotEverything: Boolean = False;
+
+function  g_Net_Init(): Boolean;
+procedure g_Net_Cleanup();
+procedure g_Net_Free();
+procedure g_Net_Flush();
+
+function  g_Net_Host(IPAddr: LongWord; Port: enet_uint16; MaxClients: Cardinal = 16): Boolean;
+procedure g_Net_Host_Die();
+procedure g_Net_Host_Send(ID: Integer; Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+function  g_Net_Host_Update(): enet_size_t;
+
+function  g_Net_Connect(IP: string; Port: enet_uint16): Boolean;
+procedure g_Net_Disconnect(Forced: Boolean = False);
+procedure g_Net_Client_Send(Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+function  g_Net_Client_Update(): enet_size_t;
+function  g_Net_Client_UpdateWhileLoading(): enet_size_t;
+
+function  g_Net_Client_ByName(Name: string): pTNetClient;
+function  g_Net_Client_ByPlayer(PID: Word): pTNetClient;
+function  g_Net_ClientName_ByID(ID: Integer): string;
+
+procedure g_Net_SendData(Data:AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
+function  g_Net_Wait_Event(msgId: Word): TMemoryStream;
+
+function  IpToStr(IP: LongWord): string;
+function  StrToIp(IPstr: string; var IP: LongWord): Boolean;
+
+function  g_Net_IsHostBanned(IP: LongWord; Perm: Boolean = False): Boolean;
+procedure g_Net_BanHost(IP: LongWord; Perm: Boolean = True); overload;
+procedure g_Net_BanHost(IP: string; Perm: Boolean = True); overload;
+function  g_Net_UnbanHost(IP: string): Boolean; overload;
+function  g_Net_UnbanHost(IP: LongWord): Boolean; overload;
+procedure g_Net_UnbanNonPermHosts();
+procedure g_Net_SaveBanList();
+
+implementation
+
+uses
+  SysUtils,
+  e_input, g_nethandler, g_netmsg, g_netmaster, g_player, g_window, g_console,
+  g_main, g_game, g_language, g_weapons;
+
+
+{ /// SERVICE FUNCTIONS /// }
+
+
+function g_Net_FindSlot(): Integer;
+var
+  I: Integer;
+  F: Boolean;
+  N, C: Integer;
+begin
+  N := -1;
+  F := False;
+  C := 0;
+  for I := Low(NetClients) to High(NetClients) do
+  begin
+    if NetClients[I].Used then
+      Inc(C)
+    else
+      if not F then
+      begin
+        F := True;
+        N := I;
+      end;
+  end;
+  if C >= NetMaxClients then
+  begin
+    Result := -1;
+    Exit;
+  end;
+
+  if not F then
+  begin
+    if (Length(NetClients) >= NetMaxClients) then
+      N := -1
+    else
+    begin
+      SetLength(NetClients, Length(NetClients) + 1);
+      N := High(NetClients);
+    end;
+  end;
+
+  if N >= 0 then
+  begin
+    NetClients[N].Used := True;
+    NetClients[N].ID := N;
+    NetClients[N].RequestedFullUpdate := False;
+    NetClients[N].RCONAuth := False;
+    NetClients[N].Voted := False;
+    NetClients[N].Player := 0;
+  end;
+
+  Result := N;
+end;
+
+function g_Net_Init(): Boolean;
+var
+  F: TextFile;
+  IPstr: string;
+  IP: LongWord;
+begin
+  e_Buffer_Clear(@NetIn);
+  e_Buffer_Clear(@NetOut);
+  SetLength(NetClients, 0);
+  NetPeer := nil;
+  NetHost := nil;
+  NetMyID := -1;
+  NetPlrUID1 := -1;
+  NetPlrUID2 := -1;
+  NetAddr.port := 25666;
+  SetLength(NetBannedHosts, 0);
+  if FileExists(DataDir + BANLIST_FILENAME) then
+  begin
+    Assign(F, DataDir + BANLIST_FILENAME);
+    Reset(F);
+    while not EOF(F) do
+    begin
+      Readln(F, IPstr);
+      if StrToIp(IPstr, IP) then
+        g_Net_BanHost(IP);
+    end;
+    CloseFile(F);
+    g_Net_SaveBanList();
+  end;
+
+  Result := (enet_initialize() = 0);
+end;
+
+procedure g_Net_Flush();
+begin
+  enet_host_flush(NetHost);
+end;
+
+procedure g_Net_Cleanup();
+begin
+  e_Buffer_Clear(@NetIn);
+  e_Buffer_Clear(@NetOut);
+
+  SetLength(NetClients, 0);
+  NetClientCount := 0;
+
+  NetPeer := nil;
+  NetHost := nil;
+  NetMPeer := nil;
+  NetMHost := nil;
+  NetMyID := -1;
+  NetPlrUID1 := -1;
+  NetPlrUID2 := -1;
+  NetState := NET_STATE_NONE;
+
+  NetPongSock := ENET_SOCKET_NULL;
+
+  NetTimeToMaster := 0;
+  NetTimeToUpdate := 0;
+  NetTimeToReliable := 0;
+
+  NetMode := NET_NONE;
+end;
+
+procedure g_Net_Free();
+begin
+  g_Net_Cleanup();
+
+  enet_deinitialize();
+  NetInitDone := False;
+end;
+
+
+{ /// SERVER FUNCTIONS /// }
+
+
+function g_Net_Host(IPAddr: LongWord; Port: enet_uint16; MaxClients: Cardinal = 16): Boolean;
+begin
+  if NetMode <> NET_NONE then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_INGAME]);
+    Result := False;
+    Exit;
+  end;
+
+  Result := True;
+
+  g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST], [Port]));
+  if not NetInitDone then
+  begin
+    if (not g_Net_Init()) then
+    begin
+      g_Console_Add(_lc[I_NET_MSG_FERROR] + _lc[I_NET_ERR_ENET]);
+      Result := False;
+      Exit;
+    end
+    else
+      NetInitDone := True;
+  end;
+
+  NetAddr.host := IPAddr;
+  NetAddr.port := Port;
+
+  NetHost := enet_host_create(@NetAddr, NET_MAXCLIENTS, NET_CHANS, 0, 0);
+
+  if (NetHost = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + Format(_lc[I_NET_ERR_HOST], [Port]));
+    Result := False;
+    g_Net_Cleanup;
+    Exit;
+  end;
+
+  NetPongSock := enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+  if NetPongSock <> ENET_SOCKET_NULL then
+  begin
+    NetPongAddr.host := IPAddr;
+    NetPongAddr.port := Port + 1;
+    if enet_socket_bind(NetPongSock, @NetPongAddr) < 0 then
+    begin
+      enet_socket_destroy(NetPongSock);
+      NetPongSock := ENET_SOCKET_NULL;
+    end
+    else
+      enet_socket_set_option(NetPongSock, ENET_SOCKOPT_NONBLOCK, 1);
+  end;
+
+  NetMode := NET_SERVER;
+  e_Buffer_Clear(@NetOut);
+end;
+
+procedure g_Net_Host_Die();
+var
+  I: Integer;
+begin
+  if NetMode <> NET_SERVER then Exit;
+
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_DISCALL]);
+  for I := 0 to High(NetClients) do
+    if NetClients[I].Used then
+      enet_peer_disconnect(NetClients[I].Peer, NET_DISC_DOWN);
+
+  while enet_host_service(NetHost, @NetEvent, 1000) > 0 do
+    if NetEvent.kind = ENET_EVENT_TYPE_RECEIVE then
+      enet_packet_destroy(NetEvent.packet);
+
+  for I := 0 to High(NetClients) do
+    if NetClients[I].Used then
+    begin
+      FreeMemory(NetClients[I].Peer^.data);
+      NetClients[I].Peer^.data := nil;
+      enet_peer_reset(NetClients[I].Peer);
+      NetClients[I].Peer := nil;
+      NetClients[I].Used := False;
+    end;
+
+  if (NetMPeer <> nil) and (NetMHost <> nil) then g_Net_Slist_Disconnect;
+  if NetPongSock <> ENET_SOCKET_NULL then
+    enet_socket_destroy(NetPongSock);
+
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_DIE]);
+  enet_host_destroy(NetHost);
+
+  NetMode := NET_NONE;
+
+  g_Net_Cleanup;
+  e_WriteLog('NET: Server stopped', MSG_NOTIFY);
+end;
+
+
+procedure g_Net_Host_Send(ID: Integer; Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+var
+  P: pENetPacket;
+  F: enet_uint32;
+begin
+  if (Reliable) then
+    F := LongWord(ENET_PACKET_FLAG_RELIABLE)
+  else
+    F := 0;
+
+  if (ID >= 0) then
+  begin
+    if ID > High(NetClients) then Exit;
+    if NetClients[ID].Peer = nil then Exit;
+
+    P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, F);
+    if not Assigned(P) then Exit;
+
+    enet_peer_send(NetClients[ID].Peer, Chan, P);
+  end
+  else
+  begin
+    P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, F);
+    if not Assigned(P) then Exit;
+
+    enet_host_widecast(NetHost, Chan, P);
+  end;
+
+  g_Net_Flush();
+  e_Buffer_Clear(@NetOut);
+end;
+
+procedure g_Net_Host_CheckPings();
+var
+  ClAddr: ENetAddress;
+  Buf: ENetBuffer;
+  Len, ClTime: Integer;
+  Ping: array [0..5] of Byte;
+  NPl: Byte;
+begin
+  if NetPongSock = ENET_SOCKET_NULL then Exit;
+
+  Buf.data := Addr(Ping[0]);
+  Buf.dataLength := 6;
+
+  Ping[0] := 0;
+
+  Len := enet_socket_receive(NetPongSock, @ClAddr, @Buf, 1);
+  if Len < 0 then Exit;
+
+  if (Ping[0] = Ord('D')) and (Ping[1] = Ord('F')) then
+  begin
+    ClTime := Integer(Addr(Ping[2])^);
+
+    e_Buffer_Clear(@NetOut);
+    e_Buffer_Write(@NetOut, Byte(Ord('D')));
+    e_Buffer_Write(@NetOut, Byte(Ord('F')));
+    e_Buffer_Write(@NetOut, ClTime);
+    g_Net_Slist_WriteInfo();
+    NPl := 0;
+    if gPlayer1 <> nil then Inc(NPl);
+    if gPlayer2 <> nil then Inc(NPl);
+    e_Buffer_Write(@NetOut, NPl);
+    e_Buffer_Write(@NetOut, gNumBots);
+
+    Buf.data := Addr(NetOut.Data[0]);
+    Buf.dataLength := NetOut.WritePos;
+    enet_socket_send(NetPongSock, @ClAddr, @Buf, 1);
+
+    e_Buffer_Clear(@NetOut);
+  end;
+end;
+
+function g_Net_Host_Update(): enet_size_t;
+var
+  IP: string;
+  Port: Word;
+  ID: Integer;
+  TC: pTNetClient;
+  TP: TPlayer;
+begin
+  IP := '';
+  Result := 0;
+
+  if NetUseMaster then
+  begin
+    g_Net_Slist_Check;
+    g_Net_Host_CheckPings;
+  end;
+
+  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+  begin
+    case (NetEvent.kind) of
+      ENET_EVENT_TYPE_CONNECT:
+      begin
+        IP := IpToStr(NetEvent.Peer^.address.host);
+        Port := NetEvent.Peer^.address.port;
+        g_Console_Add(_lc[I_NET_MSG] +
+          Format(_lc[I_NET_MSG_HOST_CONN], [IP, Port]));
+
+        if (NetEvent.data <> NET_PROTOCOL_VER) then
+        begin
+          g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+            _lc[I_NET_DISC_PROTOCOL]);
+          NetEvent.peer^.data := GetMemory(SizeOf(Byte));
+          Byte(NetEvent.peer^.data^) := 255;
+          enet_peer_disconnect(NetEvent.peer, NET_DISC_PROTOCOL);
+          enet_host_flush(NetHost);
+          Exit;
+        end;
+
+        ID := g_Net_FindSlot();
+
+        if ID < 0 then
+        begin
+          g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+            _lc[I_NET_DISC_FULL]);
+          NetEvent.Peer^.data := GetMemory(SizeOf(Byte));
+          Byte(NetEvent.peer^.data^) := 255;
+          enet_peer_disconnect(NetEvent.peer, NET_DISC_FULL);
+          enet_host_flush(NetHost);
+          Exit;
+        end;
+
+        NetClients[ID].Peer := NetEvent.peer;
+        NetClients[ID].Peer^.data := GetMemory(SizeOf(Byte));
+        Byte(NetClients[ID].Peer^.data^) := ID;
+        NetClients[ID].State := NET_STATE_AUTH;
+        NetClients[ID].RCONAuth := False;
+
+        enet_peer_timeout(NetEvent.peer, ENET_PEER_TIMEOUT_LIMIT * 2, ENET_PEER_TIMEOUT_MINIMUM * 2, ENET_PEER_TIMEOUT_MAXIMUM * 2);
+
+        Inc(NetClientCount);
+        g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST_ADD], [ID]));
+      end;
+
+      ENET_EVENT_TYPE_RECEIVE:
+      begin
+        ID := Byte(NetEvent.peer^.data^);
+        if ID > High(NetClients) then Exit;
+        TC := @NetClients[ID];
+
+        g_Net_HostMsgHandler(TC, NetEvent.packet);
+      end;
+
+      ENET_EVENT_TYPE_DISCONNECT:
+      begin
+        ID := Byte(NetEvent.peer^.data^);
+        if ID > High(NetClients) then Exit;     
+        TC := @NetClients[ID];
+        if TC = nil then Exit;
+
+        if not (TC^.Used) then Exit;
+
+        TP := g_Player_Get(TC^.Player);
+
+        if TP <> nil then
+        begin
+          TP.Lives := 0;
+          TP.Kill(K_SIMPLEKILL, 0, HIT_DISCON);
+          g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [TP.Name]), True);
+          e_WriteLog('NET: Client ' + TP.Name + ' [' + IntToStr(ID) + '] disconnected.', MSG_NOTIFY);
+          g_Player_Remove(TP.UID);
+        end;
+
+        TC^.Used := False;
+        TC^.State := NET_STATE_NONE;
+        TC^.Peer := nil;
+        TC^.Player := 0;
+        TC^.RequestedFullUpdate := False;
+
+        FreeMemory(NetEvent.peer^.data);
+        NetEvent.peer^.data := nil;
+        g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_HOST_DISC], [ID]));
+        Dec(NetClientCount);
+
+        if NetUseMaster then g_Net_Slist_Update;
+      end;
+    end;
+  end;
+end;
+
+
+{ /// CLIENT FUNCTIONS /// }
+
+
+procedure g_Net_Disconnect(Forced: Boolean = False);
+begin
+  if NetMode <> NET_CLIENT then Exit;
+  if (NetHost = nil) or (NetPeer = nil) then Exit;
+
+  if not Forced then
+  begin
+    enet_peer_disconnect(NetPeer, NET_DISC_NONE);
+
+    while (enet_host_service(NetHost, @NetEvent, 1500) > 0) do
+    begin
+      if (NetEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
+      begin
+        NetPeer := nil;
+        break;
+      end;
+
+      if (NetEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
+        enet_packet_destroy(NetEvent.packet);
+    end;
+
+    if NetPeer <> nil then
+    begin
+      enet_peer_reset(NetPeer);
+      NetPeer := nil;
+    end;
+  end
+  else
+  begin
+    e_WriteLog('NET: Kicked from server: ' + IntToStr(NetEvent.data), MSG_NOTIFY);
+    if (NetEvent.data <= NET_DISC_MAX) then
+      g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_KICK] +
+        _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + NetEvent.data)], True);
+  end;
+
+  if NetHost <> nil then
+  begin
+    enet_host_destroy(NetHost);
+    NetHost := nil;
+  end;
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DISC]);
+
+  g_Net_Cleanup;
+  e_WriteLog('NET: Disconnected', MSG_NOTIFY);
+end;
+
+procedure g_Net_Client_Send(Reliable: Boolean; Chan: Byte = NET_CHAN_GAME);
+var
+  P: pENetPacket;
+  F: enet_uint32;
+begin
+  if (Reliable) then
+    F := LongWord(ENET_PACKET_FLAG_RELIABLE)
+  else
+    F := 0;
+
+  P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, F);
+  if not Assigned(P) then Exit;
+
+  enet_peer_send(NetPeer, Chan, P);
+  g_Net_Flush();
+  e_Buffer_Clear(@NetOut);
+end;
+
+function g_Net_Client_Update(): enet_size_t;
+begin
+  Result := 0;
+  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+  begin
+    case NetEvent.kind of
+      ENET_EVENT_TYPE_RECEIVE:
+        g_Net_ClientMsgHandler(NetEvent.packet);
+
+      ENET_EVENT_TYPE_DISCONNECT:
+      begin
+        g_Net_Disconnect(True);
+        Result := 1;
+        Exit;
+      end;
+    end;
+  end
+end;
+
+function g_Net_Client_UpdateWhileLoading(): enet_size_t;
+begin
+  Result := 0;
+  while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+  begin
+    case NetEvent.kind of
+      ENET_EVENT_TYPE_RECEIVE:
+        g_Net_ClientLightMsgHandler(NetEvent.packet);
+
+      ENET_EVENT_TYPE_DISCONNECT:
+      begin
+        g_Net_Disconnect(True);
+        Result := 1;
+        Exit;
+      end;
+    end;
+  end;
+  g_Net_Flush();
+end;
+
+function g_Net_Connect(IP: string; Port: enet_uint16): Boolean;
+var
+  OuterLoop: Boolean;
+begin
+  if NetMode <> NET_NONE then
+  begin
+    g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_ERR_INGAME], True);
+    Result := False;
+    Exit;
+  end;
+
+  Result := True;
+
+  g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_MSG_CLIENT_CONN],
+    [IP, Port]));
+  if not NetInitDone then
+  begin
+    if (not g_Net_Init()) then
+    begin
+      g_Console_Add(_lc[I_NET_MSG_FERROR] + _lc[I_NET_ERR_ENET], True);
+      Result := False;
+      Exit;
+    end
+    else
+      NetInitDone := True;
+  end;
+
+  NetHost := enet_host_create(nil, 1, NET_CHANS, 0, 0);
+
+  if (NetHost = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
+    g_Net_Cleanup;
+    Result := False;
+    Exit;
+  end;
+
+  enet_address_set_host(@NetAddr, PChar(Addr(IP[1])));
+  NetAddr.port := Port;
+
+  NetPeer := enet_host_connect(NetHost, @NetAddr, NET_CHANS, NET_PROTOCOL_VER);
+
+  if (NetPeer = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
+    enet_host_destroy(NetHost);
+    g_Net_Cleanup;
+    Result := False;
+    Exit;
+  end;
+
+  OuterLoop := True;
+  while OuterLoop do
+  begin
+    while (enet_host_service(NetHost, @NetEvent, 0) > 0) do
+    begin
+      if (NetEvent.kind = ENET_EVENT_TYPE_CONNECT) then
+      begin
+        g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_CLIENT_DONE]);
+        NetMode := NET_CLIENT;
+        e_Buffer_Clear(@NetOut);
+        enet_peer_timeout(NetPeer, ENET_PEER_TIMEOUT_LIMIT * 2, ENET_PEER_TIMEOUT_MINIMUM * 2, ENET_PEER_TIMEOUT_MAXIMUM * 2);
+        NetClientIP := IP;
+        NetClientPort := Port;
+        Exit;
+      end;
+    end;
+
+    ProcessLoading();
+    
+    e_PollInput();
+
+    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
+      OuterLoop := False;
+  end;
+
+  g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_TIMEOUT], True);
+  if NetPeer <> nil then enet_peer_reset(NetPeer);
+  if NetHost <> nil then
+  begin
+    enet_host_destroy(NetHost);
+    NetHost := nil;
+  end;
+  g_Net_Cleanup();
+  Result := False;
+end;
+
+function IpToStr(IP: LongWord): string;
+var
+  Ptr: Pointer;
+begin
+  Result := '';
+  Ptr := Addr(IP);
+
+  e_Raw_Seek(0);
+  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr)) + '.';
+  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr)) + '.';
+  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr)) + '.';
+  Result := Result + IntToStr(e_Raw_Read_Byte(Ptr));
+  e_Raw_Seek(0);
+end;
+
+function StrToIp(IPstr: string; var IP: LongWord): Boolean;
+var
+  EAddr: ENetAddress;
+begin
+  Result := enet_address_set_host(@EAddr, PChar(@IPstr[1])) = 0;
+  IP := EAddr.host;
+end;
+
+function g_Net_Client_ByName(Name: string): pTNetClient;
+var
+  a: Integer;
+  pl: TPlayer;
+begin
+  Result := nil;
+  for a := Low(NetClients) to High(NetClients) do
+    if (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
+    begin
+      pl := g_Player_Get(NetClients[a].Player);
+      if pl = nil then continue;
+      if Copy(LowerCase(pl.Name), 1, Length(Name)) <> LowerCase(Name) then continue;
+      if NetClients[a].Peer <> nil then
+      begin
+        Result := @NetClients[a];
+        Exit;
+      end;
+    end;
+end;
+
+function g_Net_Client_ByPlayer(PID: Word): pTNetClient;
+var
+  a: Integer;
+begin
+  Result := nil;
+  for a := Low(NetClients) to High(NetClients) do
+    if (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
+      if NetClients[a].Player = PID then
+      begin
+        Result := @NetClients[a];
+        Exit;
+      end;
+end;
+
+function g_Net_ClientName_ByID(ID: Integer): string;
+var
+  a: Integer;
+  pl: TPlayer;
+begin
+  Result := '';
+  if ID = NET_EVERYONE then
+    Exit;
+  for a := Low(NetClients) to High(NetClients) do
+    if (NetClients[a].ID = ID) and (NetClients[a].Used) and (NetClients[a].State = NET_STATE_GAME) then
+    begin
+      pl := g_Player_Get(NetClients[a].Player);
+      if pl = nil then Exit;
+      Result := pl.Name;
+    end;
+end;
+
+procedure g_Net_SendData(Data:AByte; peer: pENetPeer; Reliable: Boolean; Chan: Byte = NET_CHAN_DOWNLOAD);
+var
+  P: pENetPacket;
+  F: enet_uint32;
+  dataLength: Cardinal;
+begin
+  dataLength := Length(Data);
+
+  if (Reliable) then
+    F := LongWord(ENET_PACKET_FLAG_RELIABLE)
+  else
+    F := 0;
+
+  if (peer <> nil) then
+  begin
+    P := enet_packet_create(@Data[0], dataLength, F);
+    if not Assigned(P) then Exit;
+    enet_peer_send(peer, Chan, P);
+  end
+  else
+  begin
+    P := enet_packet_create(@Data[0], dataLength, F);
+    if not Assigned(P) then Exit;
+    enet_host_widecast(NetHost, Chan, P);
+  end;
+
+  enet_host_flush(NetHost);
+end;
+
+function g_Net_Wait_Event(msgId: Word): TMemoryStream;
+var
+  downloadEvent: ENetEvent;
+  OuterLoop: Boolean;
+  MID: Byte;
+  Ptr: Pointer;
+  msgStream: TMemoryStream;
+begin
+  msgStream := nil;
+  OuterLoop := True;
+  while OuterLoop do
+  begin
+    while (enet_host_service(NetHost, @downloadEvent, 0) > 0) do
+    begin
+      if (downloadEvent.kind = ENET_EVENT_TYPE_RECEIVE) then
+      begin
+        Ptr := downloadEvent.packet^.data;
+
+        MID := Byte(Ptr^);
+
+        if (MID = msgId) then
+        begin
+          msgStream := TMemoryStream.Create;
+          msgStream.SetSize(downloadEvent.packet^.dataLength);
+          msgStream.WriteBuffer(Ptr^, downloadEvent.packet^.dataLength);
+          msgStream.Seek(0, soFromBeginning);
+
+          OuterLoop := False;
+          enet_packet_destroy(downloadEvent.packet);
+          break;
+        end
+        else begin
+          enet_packet_destroy(downloadEvent.packet);
+        end;
+      end
+      else
+        if (downloadEvent.kind = ENET_EVENT_TYPE_DISCONNECT) then
+        begin
+          if (downloadEvent.data <= NET_DISC_MAX) then
+            g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CONN] + ' ' +
+            _lc[TStrings_Locale(Cardinal(I_NET_DISC_NONE) + downloadEvent.data)], True);
+          OuterLoop := False;
+          Break;
+        end;
+    end;
+
+    ProcessLoading();
+
+    e_PollInput();
+
+    if e_KeyPressed(IK_ESCAPE) or e_KeyPressed(IK_SPACE) then
+      break;
+  end;
+  Result := msgStream;
+end;
+
+function g_Net_IsHostBanned(IP: LongWord; Perm: Boolean = False): Boolean;
+var
+  I: Integer;
+begin
+  Result := False;
+  if NetBannedHosts = nil then
+    Exit;
+  for I := 0 to High(NetBannedHosts) do
+    if (NetBannedHosts[I].IP = IP) and ((not Perm) or (NetBannedHosts[I].Perm)) then
+    begin
+      Result := True;
+      break;
+    end;
+end;
+
+procedure g_Net_BanHost(IP: LongWord; Perm: Boolean = True); overload;
+var
+  I, P: Integer;
+begin
+  if IP = 0 then
+    Exit;
+  if g_Net_IsHostBanned(IP, Perm) then
+    Exit;
+
+  P := -1;
+  for I := Low(NetBannedHosts) to High(NetBannedHosts) do
+    if NetBannedHosts[I].IP = 0 then
+    begin
+      P := I;
+      break;
+    end;
+
+  if P < 0 then
+  begin
+    SetLength(NetBannedHosts, Length(NetBannedHosts) + 1);
+    P := High(NetBannedHosts);
+  end;
+
+  NetBannedHosts[P].IP := IP;
+  NetBannedHosts[P].Perm := Perm;
+end;
+
+procedure g_Net_BanHost(IP: string; Perm: Boolean = True); overload;
+var
+  a: LongWord;
+  b: Boolean;
+begin
+  b := StrToIp(IP, a);
+  if b then
+    g_Net_BanHost(a, Perm);
+end;
+
+procedure g_Net_UnbanNonPermHosts();
+var
+  I: Integer;
+begin
+  if NetBannedHosts = nil then
+    Exit;
+  for I := Low(NetBannedHosts) to High(NetBannedHosts) do
+    if (NetBannedHosts[I].IP > 0) and not NetBannedHosts[I].Perm then
+    begin
+      NetBannedHosts[I].IP := 0;
+      NetBannedHosts[I].Perm := True;
+    end;
+end;
+
+function g_Net_UnbanHost(IP: string): Boolean; overload;
+var
+  a: LongWord;
+begin
+  Result := StrToIp(IP, a);
+  if Result then
+    Result := g_Net_UnbanHost(a);
+end;
+
+function g_Net_UnbanHost(IP: LongWord): Boolean; overload;
+var
+  I: Integer;
+begin
+  Result := False;
+  if IP = 0 then
+    Exit;
+  if NetBannedHosts = nil then
+    Exit;
+  for I := 0 to High(NetBannedHosts) do
+    if NetBannedHosts[I].IP = IP then
+    begin
+      NetBannedHosts[I].IP := 0;
+      NetBannedHosts[I].Perm := True;
+      Result := True;
+      // no break here to clear all bans of this host, perm and non-perm
+    end;
+end;
+
+procedure g_Net_SaveBanList();
+var
+  F: TextFile;
+  I: Integer;
+begin
+  Assign(F, DataDir + BANLIST_FILENAME);
+  Rewrite(F);
+  if NetBannedHosts <> nil then
+    for I := 0 to High(NetBannedHosts) do
+      if NetBannedHosts[I].Perm and (NetBannedHosts[I].IP > 0) then
+        Writeln(F, IpToStr(NetBannedHosts[I].IP));
+  CloseFile(F);
+end;
+
+end.
diff --git a/src/game/g_nethandler.pas b/src/game/g_nethandler.pas
new file mode 100644 (file)
index 0000000..996ddad
--- /dev/null
@@ -0,0 +1,129 @@
+unit g_nethandler;
+
+interface
+
+uses g_net, g_netmsg, ENet;
+
+procedure g_Net_ClientMsgHandler(P: pENetPacket);
+procedure g_Net_ClientLightMsgHandler(P: pENetPacket);
+procedure g_Net_HostMsgHandler(S: pTNetClient; P: pENetPacket);
+
+implementation
+
+uses e_fixedbuffer;
+
+procedure g_Net_ClientMsgHandler(P: pENetPacket);
+var
+  MID: Byte;
+  B: Pointer;
+begin
+  e_Raw_Seek(0);
+
+  B := P^.data;
+  if B = nil then Exit;
+
+  MID := e_Raw_Read_Byte(B);
+
+  case MID of
+    NET_MSG_CHAT:   MC_RECV_Chat(B);
+    NET_MSG_GFX:    MC_RECV_Effect(B);
+    NET_MSG_SND:    MC_RECV_Sound(B);
+    NET_MSG_SCORE:  MC_RECV_GameStats(B);
+    NET_MSG_COOP:   MC_RECV_CoopStats(B);
+    NET_MSG_GEVENT: MC_RECV_GameEvent(B);
+    NET_MSG_FLAG:   MC_RECV_FlagEvent(B);
+    NET_MSG_GSET:   MC_RECV_GameSettings(B);
+
+    NET_MSG_PLR:    MC_RECV_PlayerCreate(B);
+    NET_MSG_PLRPOS: MC_RECV_PlayerPos(B);
+    NET_MSG_PLRSTA: MC_RECV_PlayerStats(B);
+    NET_MSG_PLRDEL: MC_RECV_PlayerDelete(B);
+    NET_MSG_PLRDMG: MC_RECV_PlayerDamage(B);
+    NET_MSG_PLRDIE: MC_RECV_PlayerDeath(B);
+    NET_MSG_PLRFIRE:MC_RECV_PlayerFire(B);
+    NET_MSG_PLRSET: MC_RECV_PlayerSettings(B);
+
+    NET_MSG_MSPAWN: MC_RECV_MonsterSpawn(B);
+    NET_MSG_MPOS:   MC_RECV_MonsterPos(B);
+    NET_MSG_MSTATE: MC_RECV_MonsterState(B);
+    NET_MSG_MSHOT:  MC_RECV_MonsterShot(B);
+    NET_MSG_MDEL:   MC_RECV_MonsterDelete(B);
+
+    NET_MSG_SHADD:  MC_RECV_CreateShot(B);
+    NET_MSG_SHPOS:  MC_RECV_UpdateShot(B);
+    NET_MSG_SHDEL:  MC_RECV_DeleteShot(B);
+
+    NET_MSG_ISPAWN: MC_RECV_ItemSpawn(B);
+    NET_MSG_IDEL:   MC_RECV_ItemDestroy(B);
+
+    NET_MSG_PSTATE: MC_RECV_PanelState(B);
+    NET_MSG_PTEX:   MC_RECV_PanelTexture(B);
+
+    NET_MSG_TSOUND: MC_RECV_TriggerSound(B);
+    NET_MSG_TMUSIC: MC_RECV_TriggerMusic(B);
+
+    NET_MSG_TIME_SYNC:  MC_RECV_TimeSync(B);
+    NET_MSG_VOTE_EVENT: MC_RECV_VoteEvent(B);
+  end;
+
+  enet_packet_destroy(P);
+end;
+
+procedure g_Net_ClientLightMsgHandler(P: pENetPacket);
+var
+  MID: Byte;
+  B: Pointer;
+begin
+  e_Raw_Seek(0);
+
+  B := P^.data;
+  if B = nil then Exit;
+
+  MID := e_Raw_Read_Byte(B);
+
+  case MID of
+    NET_MSG_GEVENT: MC_RECV_GameEvent(B);
+    NET_MSG_GSET:   MC_RECV_GameSettings(B);
+
+    NET_MSG_PLR:    if NetState <> NET_STATE_AUTH then MC_RECV_PlayerCreate(B);
+    NET_MSG_PLRDEL: if NetState <> NET_STATE_AUTH then MC_RECV_PlayerDelete(B);
+  end;
+
+  enet_packet_destroy(P);
+end;
+
+procedure g_Net_HostMsgHandler(S: pTNetClient; P: pENetPacket);
+var
+  MID: Byte;
+  B: Pointer;
+begin
+  e_Raw_Seek(0);
+
+  B := P^.data;
+  if B = nil then Exit;
+
+  MID := e_Raw_Read_Byte(B);
+
+  case MID of
+    NET_MSG_INFO: MH_RECV_Info(S, B);
+    NET_MSG_CHAT: MH_RECV_Chat(S, B);
+    NET_MSG_REQFST: MH_RECV_FullStateRequest(S, B);
+
+    NET_MSG_PLRPOS: MH_RECV_PlayerPos(S, B);
+    NET_MSG_PLRSET: MH_RECV_PlayerSettings(S, B);
+    NET_MSG_CHEAT:  MH_RECV_CheatRequest(S, B);
+
+    NET_MSG_RCON_AUTH: MH_RECV_RCONPassword(S, B);
+    NET_MSG_RCON_CMD:  MH_RECV_RCONCommand(S, B);
+
+    NET_MSG_MAP_REQUEST: MH_RECV_MapRequest(S, B);
+    NET_MSG_RES_REQUEST: MH_RECV_ResRequest(S, B);
+
+    NET_MSG_VOTE_EVENT: MH_RECV_Vote(S, B);
+  end;
+
+  enet_packet_destroy(P);
+end;
+
+end.
+
diff --git a/src/game/g_netmaster.pas b/src/game/g_netmaster.pas
new file mode 100644 (file)
index 0000000..57d53c9
--- /dev/null
@@ -0,0 +1,578 @@
+unit g_netmaster;
+
+interface
+
+uses ENet;
+
+const
+  NET_MCHANS = 2;
+
+  NET_MCHAN_MAIN = 0;
+  NET_MCHAN_UPD  = 1;
+
+  NET_MMSG_UPD = 200;
+  NET_MMSG_DEL = 201;
+  NET_MMSG_GET = 202;
+
+type
+  TNetServer = record
+    Number: Byte;
+    Protocol: Byte;
+    Name: string;
+    IP: string;
+    Port: Word;
+    Map: string;
+    Players, MaxPlayers, LocalPl, Bots: Byte;
+    Ping: Integer;
+    GameMode: Byte;
+    Password: Boolean;
+    PingAddr: ENetAddress;
+  end;
+  pTNetServer = ^TNetServer;
+
+  TNetServerList = array of TNetServer;
+  pTNetServerList = ^TNetServerList;
+
+var
+  NetMHost:       pENetHost = nil;
+  NetMPeer:       pENetPeer = nil;
+
+  slCurrent:       TNetServerList = nil;
+  slWaitStr:       string = '';
+  slReturnPressed: Boolean = True;
+
+procedure g_Net_Slist_Set(IP: string; Port: Word);
+function  g_Net_Slist_Fetch(var SL: TNetServerList): Boolean;
+procedure g_Net_Slist_Update();
+procedure g_Net_Slist_Remove();
+function  g_Net_Slist_Connect(): Boolean;
+procedure g_Net_Slist_Check();
+procedure g_Net_Slist_Disconnect();
+procedure g_Net_Slist_WriteInfo();
+
+procedure g_Serverlist_Draw(var SL: TNetServerList);
+procedure g_Serverlist_Control(var SL: TNetServerList);
+
+implementation
+
+uses
+  SysUtils, e_fixedbuffer, e_input, e_graphics, e_log, g_window, g_net, g_console,
+  g_map, g_game, g_sound, g_textures, g_gui, g_menu, g_options, g_language, WADEDITOR,
+  ENetPlatform;
+
+var
+  NetMEvent:      ENetEvent;
+  slSelection:    Byte = 0;
+  slFetched:      Boolean = False;
+  slDirPressed:   Boolean = False;
+
+function GetTimerMS(): Integer;
+begin
+  Result := GetTimer() div 1000;
+end;
+
+procedure PingServer(var S: TNetServer; Sock: ENetSocket);
+var
+  Buf: ENetBuffer;
+  Ping: array [0..5] of Byte;
+  ClTime: Integer;
+begin
+  ClTime := GetTimerMS();
+
+  Buf.data := Addr(Ping[0]);
+  Buf.dataLength := 6;
+
+  Ping[0] := Ord('D');
+  Ping[1] := Ord('F');
+  LongInt(Addr(Ping[2])^) := ClTime;
+
+  enet_socket_send(Sock, Addr(S.PingAddr), @Buf, 1);
+end;
+
+function g_Net_Slist_Fetch(var SL: TNetServerList): Boolean;
+var
+  Cnt: Byte;
+  P: pENetPacket;
+  MID: Byte;
+  I, T, RX: Integer;
+  Sock: ENetSocket;
+  Buf: ENetBuffer;
+  SvAddr: ENetAddress;
+begin
+  Result := False;
+  SL := nil;
+
+  if (NetMHost <> nil) or (NetMPeer <> nil) then
+    Exit;
+
+  if not g_Net_Slist_Connect then
+    Exit;
+
+  e_WriteLog('Fetching serverlist...', MSG_NOTIFY);
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_SLIST_FETCH]);
+
+  e_Buffer_Clear(@NetOut);
+  e_Buffer_Write(@NetOut, Byte(NET_MMSG_GET));
+
+  P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, Cardinal(ENET_PACKET_FLAG_RELIABLE));
+  enet_peer_send(NetMPeer, NET_MCHAN_MAIN, P);
+  enet_host_flush(NetMHost);
+
+  while enet_host_service(NetMHost, @NetMEvent, 5000) > 0 do
+  begin
+    if NetMEvent.kind = ENET_EVENT_TYPE_RECEIVE then
+    begin
+      e_Raw_Seek(0);
+      MID := e_Raw_Read_Byte(NetMEvent.packet^.data);
+
+      if MID <> NET_MMSG_GET then continue;
+
+      Cnt := e_Raw_Read_Byte(NetMEvent.packet^.data);
+      e_WriteLog('Retrieved ' + IntToStr(Cnt) + ' server(s).', MSG_NOTIFY);
+      g_Console_Add(_lc[I_NET_MSG] + Format(_lc[I_NET_SLIST_RETRIEVED], [Cnt]), True);
+
+      if Cnt > 0 then
+      begin
+        SetLength(SL, Cnt);
+
+        for I := 0 to Cnt - 1 do
+        begin
+          SL[I].Number := I;
+          SL[I].IP := e_Raw_Read_String(NetMEvent.packet^.data);
+          SL[I].Port := e_Raw_Read_Word(NetMEvent.packet^.data);
+          SL[I].Name := e_Raw_Read_String(NetMEvent.packet^.data);
+          SL[I].Map := e_Raw_Read_String(NetMEvent.packet^.data);
+          SL[I].GameMode := e_Raw_Read_Byte(NetMEvent.packet^.data);
+          SL[I].Players := e_Raw_Read_Byte(NetMEvent.packet^.data);
+          SL[I].MaxPlayers := e_Raw_Read_Byte(NetMEvent.packet^.data);
+          SL[I].Protocol := e_Raw_Read_Byte(NetMEvent.packet^.data);
+          SL[I].Password := e_Raw_Read_Byte(NetMEvent.packet^.data) = 1;
+          enet_address_set_host(Addr(SL[I].PingAddr), PChar(Addr(SL[I].IP[1])));
+          SL[I].Ping := -1;
+          SL[I].PingAddr.port := SL[I].Port + 1;
+        end;
+      end;
+
+      Result := True;
+      break;
+    end;
+  end;
+
+  g_Net_Slist_Disconnect;
+  e_Buffer_Clear(@NetOut);
+
+  if Length(SL) = 0 then Exit;
+
+  Sock := enet_socket_create(ENET_SOCKET_TYPE_DATAGRAM);
+  if Sock = ENET_SOCKET_NULL then Exit;
+  enet_socket_set_option(Sock, ENET_SOCKOPT_NONBLOCK, 1);
+
+  for I := Low(SL) to High(SL) do
+    PingServer(SL[I], Sock);
+
+  T := GetTimerMS();
+
+  e_Buffer_Clear(@NetIn);
+  Buf.data := Addr(NetIn.Data);
+  Buf.dataLength := Length(NetIn.Data);
+  Cnt := 0;
+  while Cnt < Length(SL) do
+  begin
+    if GetTimerMS() - T > 500 then break;
+
+    e_Buffer_Clear(@NetIn);
+
+    RX := enet_socket_receive(Sock, @SvAddr, @Buf, 1);
+    if RX <= 0 then continue;
+    NetIn.Len := RX + 1;
+    NetIn.ReadPos := 0;
+
+    if e_Buffer_Read_Char(@NetIn) <> 'D' then continue;
+    if e_Buffer_Read_Char(@NetIn) <> 'F' then continue;
+
+    for I := Low(SL) to High(SL) do
+      if (SL[I].PingAddr.host = SvAddr.host) and
+         (SL[I].PingAddr.port = SvAddr.port) then
+      begin
+        with SL[I] do
+        begin
+          Ping := e_Buffer_Read_LongInt(@NetIn);
+          Ping := GetTimerMS() - Ping;
+          Name := e_Buffer_Read_String(@NetIn);
+          Map := e_Buffer_Read_String(@NetIn);
+          GameMode := e_Buffer_Read_Byte(@NetIn);
+          Players := e_Buffer_Read_Byte(@NetIn);
+          MaxPlayers := e_Buffer_Read_Byte(@NetIn);
+          Protocol := e_Buffer_Read_Byte(@NetIn);
+          Password := e_Buffer_Read_Byte(@NetIn) = 1;
+          LocalPl := e_Buffer_Read_Byte(@NetIn);
+          Bots := e_Buffer_Read_Word(@NetIn);
+        end;
+        Inc(Cnt);
+        break;
+      end;
+  end;
+
+  enet_socket_destroy(Sock);
+end;
+
+procedure g_Net_Slist_WriteInfo();
+var
+  Wad, Map: string;
+  Cli: Byte;
+begin
+  g_ProcessResourceStr(gMapInfo.Map, @Wad, nil, @Map);
+  Wad := ExtractFileName(Wad);
+
+  e_Buffer_Write(@NetOut, NetServerName);
+
+  e_Buffer_Write(@NetOut, Wad + ':\' + Map);
+  e_Buffer_Write(@NetOut, gGameSettings.GameMode);
+
+  Cli := NetClientCount;
+  e_Buffer_Write(@NetOut, Cli);
+
+  e_Buffer_Write(@NetOut, NetMaxClients);
+
+  e_Buffer_Write(@NetOut, Byte(NET_PROTOCOL_VER));
+  e_Buffer_Write(@NetOut, Byte(NetPassword <> ''));
+end;
+
+procedure g_Net_Slist_Update;
+var
+
+  P: pENetPacket;
+
+begin
+  if (NetMHost = nil) or (NetMPeer = nil) then Exit;
+
+  e_Buffer_Clear(@NetOut);
+  e_Buffer_Write(@NetOut, Byte(NET_MMSG_UPD));
+  e_Buffer_Write(@NetOut, NetAddr.port);
+
+  g_Net_Slist_WriteInfo();
+
+  P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, Cardinal(ENET_PACKET_FLAG_RELIABLE));
+  enet_peer_send(NetMPeer, NET_MCHAN_UPD, P);
+
+  enet_host_flush(NetMHost);
+  e_Buffer_Clear(@NetOut);
+end;
+
+procedure g_Net_Slist_Remove;
+var
+  P: pENetPacket;
+begin
+  if (NetMHost = nil) or (NetMPeer = nil) then Exit;
+  e_Buffer_Clear(@NetOut);
+  e_Buffer_Write(@NetOut, Byte(NET_MMSG_DEL));
+  e_Buffer_Write(@NetOut, NetAddr.port);
+
+  P := enet_packet_create(Addr(NetOut.Data), NetOut.Len, Cardinal(ENET_PACKET_FLAG_RELIABLE));
+  enet_peer_send(NetMPeer, NET_MCHAN_MAIN, P);
+
+  enet_host_flush(NetMHost);
+  e_Buffer_Clear(@NetOut);
+end;
+
+function g_Net_Slist_Connect: Boolean;
+begin
+  Result := False;
+
+  NetMHost := enet_host_create(nil, 1, NET_MCHANS, 0, 0);
+  if (NetMHost = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
+    Exit;
+  end;
+
+  NetMPeer := enet_host_connect(NetMHost, @NetSlistAddr, NET_MCHANS, 0);
+  if (NetMPeer = nil) then
+  begin
+    g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_ERR_CLIENT], True);
+    enet_host_destroy(NetMHost);
+    NetMHost := nil;
+    Exit;
+  end;
+
+  if (enet_host_service(NetMHost, @NetMEvent, 3000) > 0) then
+    if NetMEvent.kind = ENET_EVENT_TYPE_CONNECT then
+    begin
+      Result := True;
+      g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_SLIST_CONN]);
+      Exit;
+    end
+    else
+      if NetMEvent.kind = ENET_EVENT_TYPE_RECEIVE then
+        enet_packet_destroy(NetMEvent.packet);
+
+  g_Console_Add(_lc[I_NET_MSG_ERROR] + _lc[I_NET_SLIST_ERROR], True);
+
+  NetMHost := nil;
+  NetMPeer := nil;
+end;
+
+procedure g_Net_Slist_Disconnect;
+begin
+  if (NetMHost = nil) and (NetMPeer = nil) then Exit;
+
+  if NetMode = NET_SERVER then g_Net_Slist_Remove;
+
+  enet_peer_disconnect(NetMPeer, 0);
+  enet_host_flush(NetMHost);
+
+  enet_peer_reset(NetMPeer);
+  enet_host_destroy(NetMHost);
+
+  NetMPeer := nil;
+  NetMHost := nil;
+
+  g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_SLIST_DISC]);
+end;
+
+procedure g_Net_Slist_Check;
+begin
+  if (NetMHost = nil) or (NetMPeer = nil) then Exit;
+
+  while (enet_host_service(NetMHost, @NetMEvent, 0) > 0) do
+  begin
+    if NetMEvent.kind = ENET_EVENT_TYPE_DISCONNECT then
+    begin
+      g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_SLIST_LOST], True);
+      if NetMPeer <> nil then enet_peer_reset(NetMPeer);
+      if NetMHost <> nil then enet_host_destroy(NetMHost);
+      NetMPeer := nil;
+      NetMHost := nil;
+      Break;
+    end
+    else
+      if NetMEvent.kind = ENET_EVENT_TYPE_RECEIVE then
+        enet_packet_destroy(NetMEvent.packet);
+  end;
+end;
+
+procedure g_Net_Slist_Set(IP: string; Port: Word);
+begin
+  if NetInitDone then
+  begin
+    enet_address_set_host(@NetSlistAddr, PChar(Addr(IP[1])));
+    NetSlistAddr.Port := Port;
+    e_WriteLog('Masterserver address set to ' + IP + ':' + IntToStr(Port), MSG_NOTIFY);
+  end;
+end;
+
+procedure g_Serverlist_Draw(var SL: TNetServerList);
+var
+  sy, i, y, mw, mx, l: Integer;
+  cw, ch: Byte;
+  ww, hh: Word;
+  ip: string;
+begin
+  ip := '';
+  sy := 0;
+
+  e_CharFont_GetSize(gMenuFont, _lc[I_NET_SLIST], ww, hh);
+  e_CharFont_Print(gMenuFont, (gScreenWidth div 2) - (ww div 2), 16, _lc[I_NET_SLIST]);
+
+  e_TextureFontGetSize(gStdFont, cw, ch);
+
+  ip := _lc[I_NET_SLIST_HELP];
+  mw := (Length(ip) * cw) div 2;
+
+  e_DrawFillQuad(16, 64, gScreenWidth-16, gScreenHeight-44, 64, 64, 64, 110);
+  e_DrawQuad(16, 64, gScreenWidth-16, gScreenHeight-44, 255, 127, 0);
+
+  e_TextureFontPrintEx(gScreenWidth div 2 - mw, gScreenHeight-24, ip, gStdFont, 225, 225, 225, 1);
+
+  if SL = nil then
+  begin
+    l := Length(slWaitStr) div 2;
+    e_DrawFillQuad(16, 64, gScreenWidth-16, gScreenHeight-44, 64, 64, 64, 128);
+    e_DrawQuad(gScreenWidth div 2 - 192, gScreenHeight div 2 - 10,
+      gScreenWidth div 2 + 192, gScreenHeight div 2 + 11, 255, 127, 0);
+    e_TextureFontPrint(gScreenWidth div 2 - cw * l, gScreenHeight div 2 - ch div 2,
+      slWaitStr, gStdFont);
+    Exit;
+  end;
+
+  y := 90;
+  if (slSelection < Length(SL)) then
+  begin
+    I := slSelection;
+    sy := y + 42 * I - 4;
+    ip := _lc[I_NET_ADDRESS] + ' ' + SL[I].IP + ':' + IntToStr(SL[I].Port);
+    if SL[I].Password then
+      ip := ip + '  ' + _lc[I_NET_SERVER_PASSWORD] + ' ' + _lc[I_MENU_YES]
+    else
+      ip := ip + '  ' + _lc[I_NET_SERVER_PASSWORD] + ' ' + _lc[I_MENU_NO];
+  end else
+    if Length(SL) > 0 then
+      slSelection := 0;
+
+  mw := (gScreenWidth - 188);
+  mx := 16 + mw;
+
+  e_DrawFillQuad(16 + 1, sy, gScreenWidth - 16 - 1, sy + 40, 64, 64, 64, 0);
+  e_DrawLine(1, 16 + 1, sy, gScreenWidth - 16 - 1, sy, 205, 205, 205);
+  e_DrawLine(1, 16 + 1, sy + 41, gScreenWidth - 16 - 1, sy + 41, 255, 255, 255);
+
+  e_DrawLine(1, 16, 85, gScreenWidth - 16, 85, 255, 127, 0);
+  e_DrawLine(1, 16, gScreenHeight-64, gScreenWidth-16, gScreenHeight-64, 255, 127, 0);
+
+  e_DrawLine(1, mx - 70, 64, mx - 70, gScreenHeight-44, 255, 127, 0);
+  e_DrawLine(1, mx, 64, mx, gScreenHeight-64, 255, 127, 0);
+  e_DrawLine(1, mx + 52, 64, mx + 52, gScreenHeight-64, 255, 127, 0);
+  e_DrawLine(1, mx + 104, 64, mx + 104, gScreenHeight-64, 255, 127, 0);
+
+  e_TextureFontPrintEx(18, 68, 'NAME/MAP', gStdFont, 255, 127, 0, 1);
+
+  y := 90;
+  for I := 0 to High(SL) do
+  begin
+    e_TextureFontPrintEx(18, y, SL[I].Name, gStdFont, 255, 255, 255, 1);
+    e_TextureFontPrintEx(18, y + 16, SL[I].Map, gStdFont, 210, 210, 210, 1);
+
+    y := y + 42;
+  end;
+
+  e_TextureFontPrintEx(mx - 68, 68, 'PING', gStdFont, 255, 127, 0, 1);
+  y := 90;
+  for I := 0 to High(SL) do
+  begin
+    if (SL[I].Ping < 0) or (SL[I].Ping > 999) then
+      e_TextureFontPrintEx(mx - 68, y, _lc[I_NET_SLIST_NO_ACCESS], gStdFont, 255, 0, 0, 1)
+    else
+      if SL[I].Ping = 0 then
+        e_TextureFontPrintEx(mx - 68, y, '<1' + _lc[I_NET_SLIST_PING_MS], gStdFont, 255, 255, 255, 1)
+      else
+        e_TextureFontPrintEx(mx - 68, y, IntToStr(SL[I].Ping) + _lc[I_NET_SLIST_PING_MS], gStdFont, 255, 255, 255, 1);
+
+    y := y + 42;
+  end;
+
+  e_TextureFontPrintEx(mx + 2, 68, 'MODE', gStdFont, 255, 127, 0, 1);
+  y := 90;
+  for I := 0 to High(SL) do
+  begin
+    e_TextureFontPrintEx(mx + 2, y, g_Game_ModeToText(SL[I].GameMode), gStdFont, 255, 255, 255, 1);
+
+    y := y + 42;
+  end;
+
+  e_TextureFontPrintEx(mx + 54, 68, 'PLRS', gStdFont, 255, 127, 0, 1);
+  y := 90;
+  for I := 0 to High(SL) do
+  begin
+    e_TextureFontPrintEx(mx + 54, y, IntToStr(SL[I].Players) + '/' + IntToStr(SL[I].MaxPlayers), gStdFont, 255, 255, 255, 1);
+    e_TextureFontPrintEx(mx + 54, y + 16, IntToStr(SL[I].LocalPl) + '+' + IntToStr(SL[I].Bots), gStdFont, 210, 210, 210, 1);
+    y := y + 42;
+  end;
+
+  e_TextureFontPrintEx(mx + 106, 68, 'VER', gStdFont, 255, 127, 0, 1);
+  y := 90;
+  for I := 0 to High(SL) do
+  begin
+    e_TextureFontPrintEx(mx + 106, y, IntToStr(SL[I].Protocol), gStdFont, 255, 255, 255, 1);
+
+    y := y + 42;
+  end;
+
+  e_TextureFontPrintEx(20, gScreenHeight-61, ip, gStdFont, 205, 205, 205, 1);
+  ip := IntToStr(Length(SL)) + _lc[I_NET_SLIST_SERVERS];
+  e_TextureFontPrintEx(gScreenWidth - 48 - (Length(ip) + 1)*cw,
+    gScreenHeight-61, ip, gStdFont, 205, 205, 205, 1);
+end;
+
+procedure g_Serverlist_Control(var SL: TNetServerList);
+begin
+  if gConsoleShow or gChatShow then
+    Exit;
+
+  e_PollInput();
+
+  if e_KeyPressed(IK_ESCAPE) then
+  begin
+    SL := nil;
+    gState := STATE_MENU;
+    g_GUI_ShowWindow('MainMenu');
+    g_GUI_ShowWindow('NetGameMenu');
+    g_GUI_ShowWindow('NetClientMenu');
+    g_Sound_PlayEx(WINDOW_CLOSESOUND);
+    Exit;
+  end;
+
+  if e_KeyPressed(IK_SPACE) then
+  begin
+    if not slFetched then
+    begin
+      slWaitStr := _lc[I_NET_SLIST_WAIT];
+
+      g_Game_Draw;
+      g_window.ReDrawWindow;
+
+      if g_Net_Slist_Fetch(SL) then
+      begin
+        if SL = nil then
+          slWaitStr := _lc[I_NET_SLIST_NOSERVERS];
+      end
+      else
+        slWaitStr := _lc[I_NET_SLIST_ERROR];
+      slFetched := True;
+      slSelection := 0;
+    end;
+  end
+  else
+    slFetched := False;
+
+  if SL = nil then Exit;
+
+  if e_KeyPressed(IK_RETURN) then
+  begin
+    if not slReturnPressed then
+    begin
+      if SL[slSelection].Password then
+      begin
+        PromptIP := SL[slSelection].IP;
+        PromptPort := SL[slSelection].Port;
+        gState := STATE_MENU;
+        g_GUI_ShowWindow('ClientPasswordMenu');
+        SL := nil;
+        slReturnPressed := True;
+        Exit;
+      end
+      else
+        g_Game_StartClient(SL[slSelection].IP, SL[slSelection].Port, '');
+      SL := nil;
+      slReturnPressed := True;
+      Exit;
+    end;
+  end
+  else
+    slReturnPressed := False;
+
+  if e_KeyPressed(IK_DOWN) then
+  begin
+    if not slDirPressed then
+    begin
+      Inc(slSelection);
+      if slSelection > High(SL) then slSelection := 0;
+      slDirPressed := True;
+    end;
+  end;
+  
+  if e_KeyPressed(IK_UP) then
+  begin
+    if not slDirPressed then
+    begin
+      if slSelection = 0 then slSelection := Length(SL);
+      Dec(slSelection);
+
+      slDirPressed := True;
+    end;
+  end;
+
+  if (not e_KeyPressed(IK_DOWN)) and (not e_KeyPressed(IK_UP)) then
+    slDirPressed := False;
+end;
+
+end.
diff --git a/src/game/g_netmsg.pas b/src/game/g_netmsg.pas
new file mode 100644 (file)
index 0000000..27b8081
--- /dev/null
@@ -0,0 +1,3023 @@
+unit g_netmsg;
+
+interface
+
+uses g_net, g_triggers, Classes, SysUtils, md5;
+
+const
+  NET_MSG_INFO   = 100;
+
+  NET_MSG_CHAT   = 101;
+  NET_MSG_SND    = 102;
+  NET_MSG_GFX    = 103;
+  NET_MSG_GEVENT = 104;
+  NET_MSG_SCORE  = 105;
+  NET_MSG_COOP   = 106;
+  NET_MSG_FLAG   = 107;
+  NET_MSG_REQFST = 108;
+  NET_MSG_GSET   = 109;
+
+  NET_MSG_PLR    = 111;
+  NET_MSG_PLRPOS = 112;
+  NET_MSG_PLRSTA = 113;
+  NET_MSG_PLRDEL = 114;
+  NET_MSG_PLRDMG = 115;
+  NET_MSG_PLRDIE = 116;
+  NET_MSG_PLRFIRE= 117;
+  NET_MSG_PLRSET = 119;
+  NET_MSG_CHEAT  = 120;
+
+  NET_MSG_ISPAWN = 121;
+  NET_MSG_IDEL   = 122;
+
+  NET_MSG_MSPAWN = 131;
+  NET_MSG_MPOS   = 132;
+  NET_MSG_MSTATE = 133;
+  NET_MSG_MSHOT  = 134;
+  NET_MSG_MDEL   = 135;
+
+  NET_MSG_PSTATE = 141;
+  NET_MSG_PTEX   = 142;
+
+  NET_MSG_TSOUND = 151;
+  NET_MSG_TMUSIC = 152;
+
+  NET_MSG_SHDEL  = 161;
+  NET_MSG_SHADD  = 162;
+  NET_MSG_SHPOS  = 163;
+
+  NET_MSG_RCON_AUTH  = 191;
+  NET_MSG_RCON_CMD   = 192;
+  NET_MSG_TIME_SYNC  = 194;
+  NET_MSG_VOTE_EVENT = 195;
+
+  NET_MSG_MAP_REQUEST = 201;
+  NET_MSG_MAP_RESPONSE = 202;
+  NET_MSG_RES_REQUEST = 203;
+  NET_MSG_RES_RESPONSE = 204;
+
+  NET_CHAT_SYSTEM = 0;
+  NET_CHAT_PLAYER = 1;
+  NET_CHAT_TEAM   = 2;
+
+  NET_RCON_NOAUTH = 0;
+  NET_RCON_PWGOOD = 1;
+  NET_RCON_PWBAD  = 2;
+
+  NET_GFX_SPARK   = 1;
+  NET_GFX_TELE    = 2;
+  NET_GFX_RESPAWN = 3;
+  NET_GFX_FIRE    = 4;
+  NET_GFX_EXPLODE = 5;
+  NET_GFX_BFGEXPL = 6;
+  NET_GFX_BFGHIT  = 7;
+  NET_GFX_SHELL1  = 8;
+  NET_GFX_SHELL2  = 9;
+  NET_GFX_SHELL3  = 10;
+
+  NET_EV_MAPSTART     = 1;
+  NET_EV_MAPEND       = 2;
+  NET_EV_CHANGE_TEAM  = 3;
+  NET_EV_PLAYER_KICK  = 4;
+  NET_EV_PLAYER_BAN   = 5;
+  NET_EV_LMS_WARMUP   = 6;
+  NET_EV_LMS_SURVIVOR = 7;
+  NET_EV_RCON         = 8;
+  NET_EV_BIGTEXT      = 9;
+  NET_EV_SCORE        = 10;
+  NET_EV_SCORE_MSG    = 11;
+  NET_EV_LMS_START    = 12;
+  NET_EV_LMS_WIN      = 13;
+  NET_EV_TLMS_WIN     = 14;
+  NET_EV_LMS_LOSE     = 15;
+  NET_EV_LMS_DRAW     = 16;
+  NET_EV_KILLCOMBO    = 17;
+  NET_EV_PLAYER_TOUCH = 18;
+
+  NET_VE_STARTED      = 1;
+  NET_VE_PASSED       = 2;
+  NET_VE_FAILED       = 3;
+  NET_VE_VOTE         = 4;
+  NET_VE_REVOKE       = 5;
+  NET_VE_INPROGRESS   = 6;
+
+  NET_FLAG_GET    = 1;
+  NET_FLAG_DROP   = 2;
+  NET_FLAG_CAP    = 3;
+  NET_FLAG_RETURN = 4;
+
+  NET_CHEAT_SUICIDE  = 1;
+  NET_CHEAT_SPECTATE = 2;
+
+  NET_MAX_DIFFTIME = 5000 div 36;
+
+// HOST MESSAGES
+
+procedure MH_RECV_Info(C: pTNetClient; P: Pointer);
+procedure MH_RECV_Chat(C: pTNetClient; P: Pointer);
+procedure MH_RECV_FullStateRequest(C: pTNetClient; P: Pointer);
+function  MH_RECV_PlayerPos(C: pTNetClient; P: Pointer): Word;
+procedure MH_RECV_PlayerSettings(C: pTNetClient; P: Pointer);
+procedure MH_RECV_CheatRequest(C: pTNetClient; P: Pointer);
+procedure MH_RECV_RCONPassword(C: pTNetClient; P: Pointer);
+procedure MH_RECV_RCONCommand(C: pTNetClient; P: Pointer);
+procedure MH_RECV_MapRequest(C: pTNetClient; P: Pointer);
+procedure MH_RECV_ResRequest(C: pTNetClient; P: Pointer);
+procedure MH_RECV_Vote(C: pTNetClient; P: Pointer);
+
+// GAME
+procedure MH_SEND_Everything(CreatePlayers: Boolean = False; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_Info(ID: Byte);
+procedure MH_SEND_Chat(Txt: string; Mode: Byte; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_Effect(X, Y: Integer; Ang: SmallInt; Kind: Byte; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_Sound(X, Y: Integer; Name: string; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_CreateShot(Proj: LongInt; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_UpdateShot(Proj: LongInt; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_DeleteShot(Proj: LongInt; X, Y: LongInt; Loud: Boolean = True; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_GameStats(ID: Integer = NET_EVERYONE);
+procedure MH_SEND_CoopStats(ID: Integer = NET_EVERYONE);
+procedure MH_SEND_GameEvent(EvType: Byte; EvNum: Integer = 0; EvStr: string = 'N'; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_FlagEvent(EvType: Byte; Flag: Byte; PID: Word; Quiet: Boolean = False; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_GameSettings(ID: Integer = NET_EVERYONE);
+// PLAYER
+procedure MH_SEND_PlayerCreate(PID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerPos(Reliable: Boolean; PID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerStats(PID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerDelete(PID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerDamage(PID: Word; Kind: Byte; Attacker, Value: Word; VX, VY: Integer; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerFire(PID: Word; Weapon: Byte; X, Y, AX, AY: Integer; ShotID: Integer = -1; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerDeath(PID: Word; KillType, DeathType: Byte; Attacker: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PlayerSettings(PID: Word; Mdl: string = ''; ID: Integer = NET_EVERYONE);
+// ITEM
+procedure MH_SEND_ItemSpawn(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_ItemDestroy(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
+// PANEL
+procedure MH_SEND_PanelTexture(PType: Word; PID: LongWord; AnimLoop: Byte; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_PanelState(PType: Word; PID: LongWord; ID: Integer = NET_EVERYONE);
+// MONSTER
+procedure MH_SEND_MonsterSpawn(UID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_MonsterPos(UID: Word; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_MonsterState(UID: Word; ForcedAnim: Byte = 255; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_MonsterShot(UID: Word; X, Y, VX, VY: Integer; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_MonsterDelete(UID: Word; ID: Integer = NET_EVERYONE);
+// TRIGGER
+procedure MH_SEND_TriggerSound(var T: TTrigger; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_TriggerMusic(ID: Integer = NET_EVERYONE);
+// MISC
+procedure MH_SEND_TimeSync(Time: LongWord; ID: Integer = NET_EVERYONE);
+procedure MH_SEND_VoteEvent(EvType: Byte;
+                            StrArg1: string = 'a'; StrArg2: string = 'b';
+                            IntArg1: SmallInt = 0; IntArg2: SmallInt = 0;
+                            ID: Integer = NET_EVERYONE);
+
+// CLIENT MESSAGES //
+
+// GAME
+procedure MC_RECV_Chat(P: Pointer);
+procedure MC_RECV_Effect(P: Pointer);
+procedure MC_RECV_Sound(P: Pointer);
+procedure MC_RECV_GameStats(P: Pointer);
+procedure MC_RECV_CoopStats(P: Pointer);
+procedure MC_RECV_GameEvent(P: Pointer);
+procedure MC_RECV_FlagEvent(P: Pointer);
+procedure MC_RECV_GameSettings(P: Pointer);
+// PLAYER
+function  MC_RECV_PlayerCreate(P: Pointer): Word;
+function  MC_RECV_PlayerPos(P: Pointer): Word;
+function  MC_RECV_PlayerStats(P: Pointer): Word;
+function  MC_RECV_PlayerDelete(P: Pointer): Word;
+function  MC_RECV_PlayerDamage(P: Pointer): Word;
+function  MC_RECV_PlayerDeath(P: Pointer): Word;
+function  MC_RECV_PlayerFire(P: Pointer): Word;
+procedure MC_RECV_PlayerSettings(P: Pointer);
+// ITEM
+procedure MC_RECV_ItemSpawn(P: Pointer);
+procedure MC_RECV_ItemDestroy(P: Pointer);
+// PANEL
+procedure MC_RECV_PanelTexture(P: Pointer);
+procedure MC_RECV_PanelState(P: Pointer);
+// MONSTER
+procedure MC_RECV_MonsterSpawn(P: Pointer);
+procedure MC_RECV_MonsterPos(P: Pointer);
+procedure MC_RECV_MonsterState(P: Pointer);
+procedure MC_RECV_MonsterShot(P: Pointer);
+procedure MC_RECV_MonsterDelete(P: Pointer);
+// SHOT
+procedure MC_RECV_CreateShot(P: Pointer);
+procedure MC_RECV_UpdateShot(P: Pointer);
+procedure MC_RECV_DeleteShot(P: Pointer);
+// TRIGGER
+procedure MC_RECV_TriggerSound(P: Pointer);
+procedure MC_RECV_TriggerMusic(P: Pointer);
+// MISC
+procedure MC_RECV_TimeSync(P: Pointer);
+procedure MC_RECV_VoteEvent(P: Pointer);
+// SERVICE
+procedure MC_SEND_Info(Password: string);
+procedure MC_SEND_Chat(Txt: string; Mode: Byte);
+procedure MC_SEND_PlayerPos();
+procedure MC_SEND_FullStateRequest();
+procedure MC_SEND_PlayerSettings();
+procedure MC_SEND_CheatRequest(Kind: Byte);
+procedure MC_SEND_RCONPassword(Password: string);
+procedure MC_SEND_RCONCommand(Cmd: string);
+procedure MC_SEND_Vote(Start: Boolean = False; Command: string = 'a');
+// DOWNLOAD
+procedure MC_SEND_MapRequest();
+procedure MC_SEND_ResRequest(const resName: AnsiString);
+
+type
+  TExternalResourceInfo = record
+    Name: string[255];
+    md5: TMD5Digest;
+  end;
+
+  TResDataMsg = record
+    MsgId: Byte;
+    FileSize: Integer;
+    FileData: AByte;
+  end;
+
+  TMapDataMsg = record
+    MsgId: Byte;
+    FileSize: Integer;
+    FileData: AByte;
+    ExternalResources: array of TExternalResourceInfo;
+  end;
+
+function MapDataFromMsgStream(msgStream: TMemoryStream):TMapDataMsg;
+function ResDataFromMsgStream(msgStream: TMemoryStream):TResDataMsg;
+
+implementation
+
+uses
+  Math, ENet, e_input, e_fixedbuffer, e_graphics, e_log,
+  g_textures, g_gfx, g_sound, g_console, g_basic, g_options, g_main,
+  g_game, g_player, g_map, g_panel, g_items, g_weapons, g_phys, g_gui,
+  g_language, g_monsters, g_netmaster,
+  WADEDITOR, MAPDEF;
+
+const
+  NET_KEY_LEFT     = 1;
+  NET_KEY_RIGHT    = 2;
+  NET_KEY_UP       = 4;
+  NET_KEY_DOWN     = 8;
+  NET_KEY_JUMP     = 16;
+  NET_KEY_FIRE     = 32;
+  NET_KEY_OPEN     = 64;
+  NET_KEY_NW       = 256;
+  NET_KEY_PW       = 512;
+  NET_KEY_CHAT     = 2048;
+  NET_KEY_FORCEDIR = 4096;
+
+//var
+  //kBytePrev: Word = 0;
+  //kDirPrev: TDirection = D_LEFT;
+  //HostGameTime: Word = 0;
+
+// HOST MESSAGES //
+
+
+// GAME
+
+procedure MH_RECV_Chat(C: pTNetClient; P: Pointer);
+var
+  Txt: string;
+  Mode: Byte;
+  PID: Word;
+  Pl: TPlayer;
+begin
+  PID := C^.Player;
+  Pl := g_Player_Get(PID);
+
+  Txt := e_Raw_Read_String(P);
+  Mode := e_Raw_Read_Byte(P);
+  if (Mode = NET_CHAT_SYSTEM) then
+    Mode := NET_CHAT_PLAYER; // prevent sending system messages from clients
+  if (Mode = NET_CHAT_TEAM) and (not gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+    Mode := NET_CHAT_PLAYER; // revert to player chat in non-team game
+
+  if Pl = nil then
+    MH_SEND_Chat(Txt, Mode)
+  else
+    MH_SEND_Chat(Pl.Name + ': ' + Txt, Mode, IfThen(Mode = NET_CHAT_TEAM, Pl.Team, NET_EVERYONE));
+end;
+
+procedure MH_RECV_Info(C: pTNetClient; P: Pointer);
+var
+  Ver, PName, Model, Pw: string;
+  R, G, B, T: Byte;
+  PID: Word;
+  Color: TRGB;
+  I: Integer;
+begin
+  Ver := e_Raw_Read_String(P);
+  Pw := e_Raw_Read_String(P);
+  PName := e_Raw_Read_String(P);
+  Model := e_Raw_Read_String(P);
+  R := e_Raw_Read_Byte(P);
+  G := e_Raw_Read_Byte(P);
+  B := e_Raw_Read_Byte(P);
+  T := e_Raw_Read_Byte(P);
+
+  if Ver <> GAME_VERSION then
+  begin
+    g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+      _lc[I_NET_DISC_VERSION]);
+    enet_peer_disconnect(C^.Peer, NET_DISC_VERSION);
+    Exit;
+  end;
+
+  if g_Net_IsHostBanned(C^.Peer^.address.host) then
+  begin
+    if g_Net_IsHostBanned(C^.Peer^.address.host, True) then
+    begin
+      g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+        _lc[I_NET_DISC_BAN]);
+      enet_peer_disconnect(C^.Peer, NET_DISC_BAN);
+    end
+    else
+    begin
+      g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+        _lc[I_NET_DISC_BAN]);
+      enet_peer_disconnect(C^.Peer, NET_DISC_TEMPBAN);
+    end;
+    Exit;
+  end;
+
+  if NetPassword <> '' then
+    if AnsiLowerCase(NetPassword) <> AnsiLowerCase(Pw) then
+    begin
+      g_Console_Add(_lc[I_NET_MSG] + _lc[I_NET_MSG_HOST_REJECT] +
+        _lc[I_NET_DISC_PASSWORD]);
+      enet_peer_disconnect(C^.Peer, NET_DISC_PASSWORD);
+      Exit;
+    end;
+
+  Color.R := R;
+  Color.B := B;
+  Color.G := G;
+
+  PID := g_Player_Create(Model, Color, T, False);
+  with g_Player_Get(PID) do
+  begin
+    Name := PName;
+    Reset(True);
+  end;
+
+  C^.Player := PID;
+
+  g_Console_Add(Format(_lc[I_PLAYER_JOIN], [PName]), True);
+  e_WriteLog('NET: Client ' + PName + ' [' + IntToStr(C^.ID) +
+             '] connected. Assigned player #' + IntToStr(PID) + '.', MSG_NOTIFY);
+
+  MH_SEND_Info(C^.ID);
+
+  with g_Player_Get(PID) do
+  begin
+    Name := PName;
+    FClientID := C^.ID;
+    // round in progress, don't spawn
+    if (gGameSettings.MaxLives > 0) and (gLMSRespawn = LMS_RESPAWN_NONE) then
+    begin
+      Lives := 0;
+      FNoRespawn := True;
+      Spectate;
+      FWantsInGame := True; // TODO: look into this later
+    end
+    else
+      Respawn(gGameSettings.GameType = GT_SINGLE);
+  end;
+
+  for I := Low(NetClients) to High(NetClients) do
+  begin
+    if NetClients[I].ID = C^.ID then Continue;
+    MH_SEND_PlayerCreate(PID, NetClients[I].ID);
+    MH_SEND_PlayerPos(True, PID, NetClients[I].ID);
+    MH_SEND_PlayerStats(PID, NetClients[I].ID);
+  end;
+
+  if gState in [STATE_INTERCUSTOM, STATE_FOLD] then
+    MH_SEND_GameEvent(NET_EV_MAPEND, 0, 'N', C^.ID);
+
+  if NetUseMaster then g_Net_Slist_Update;
+end;
+
+procedure MH_RECV_FullStateRequest(C: pTNetClient; P: Pointer);
+begin
+  if gGameOn then
+    MH_SEND_Everything((C^.State = NET_STATE_AUTH), C^.ID)
+  else
+    C^.RequestedFullUpdate := True;
+end;
+
+// PLAYER
+
+function  MH_RECV_PlayerPos(C: pTNetClient; P: Pointer): Word;
+var
+  Dir: Byte;
+  PID: Word;
+  kByte: Word;
+  Pl: TPlayer;
+  GT: LongWord;
+begin
+  Result := 0;
+  if not gGameOn then Exit;
+  
+  GT := e_Raw_Read_LongWord(P);
+  PID := C^.Player;
+  Pl := g_Player_Get(PID);
+  if Pl = nil then
+    Exit;
+
+  if (GT > gTime + NET_MAX_DIFFTIME) or (GT < Pl.NetTime) then Exit;
+
+  with Pl do
+  begin
+    NetTime := GT;
+    kByte := e_Raw_Read_Word(P);
+    Dir := e_Raw_Read_Byte(P);
+    if Direction <> TDirection(Dir) then
+      JustTeleported := False;
+    SetDirection(TDirection(Dir));
+    ReleaseKeys;
+
+    if kByte = NET_KEY_CHAT then
+    begin
+      PressKey(KEY_CHAT, 10000);
+      Exit;
+    end;
+
+    if LongBool(kByte and NET_KEY_LEFT) then PressKey(KEY_LEFT, 10000);
+    if LongBool(kByte and NET_KEY_RIGHT) then PressKey(KEY_RIGHT, 10000);
+    if LongBool(kByte and NET_KEY_UP) then PressKey(KEY_UP, 10000);
+    if LongBool(kByte and NET_KEY_DOWN) then PressKey(KEY_DOWN, 10000);
+    if LongBool(kByte and NET_KEY_JUMP) then PressKey(KEY_JUMP, 10000);
+    if LongBool(kByte and NET_KEY_FIRE) then PressKey(KEY_FIRE, 10000);
+    if LongBool(kByte and NET_KEY_OPEN) then PressKey(KEY_OPEN, 10000);
+    if LongBool(kByte and NET_KEY_NW) then PressKey(KEY_NEXTWEAPON, 10000);
+    if LongBool(kByte and NET_KEY_PW) then PressKey(KEY_PREVWEAPON, 10000);
+  end;
+
+  // MH_SEND_PlayerPos(False, PID, C^.ID);
+end;
+
+procedure MH_RECV_CheatRequest(C: pTNetClient; P: Pointer);
+var
+  CheatKind: Byte;
+  Pl: TPlayer;
+begin
+  Pl := g_Player_Get(C^.Player);
+  if Pl = nil then Exit;
+
+  CheatKind := e_Raw_Read_Byte(P);
+
+  case CheatKind of
+    NET_CHEAT_SUICIDE:
+      Pl.Damage(SUICIDE_DAMAGE, Pl.UID, 0, 0, HIT_SELF);
+    NET_CHEAT_SPECTATE:
+    begin
+      if Pl.FSpectator then
+        Pl.Respawn(False)
+      else
+        Pl.Spectate;
+    end;
+  end;
+end;
+
+procedure MH_RECV_PlayerSettings(C: pTNetClient; P: Pointer);
+var
+  TmpName: string;
+  TmpModel: string;
+  TmpColor: TRGB;
+  TmpTeam: Byte;
+  Pl: TPlayer;
+begin
+  TmpName := e_Raw_Read_String(P);
+  TmpModel := e_Raw_Read_String(P);
+  TmpColor.R := e_Raw_Read_Byte(P);
+  TmpColor.G := e_Raw_Read_Byte(P);
+  TmpColor.B := e_Raw_Read_Byte(P);
+  TmpTeam := e_Raw_Read_Byte(P);
+
+  Pl := g_Player_Get(C^.Player);
+  if Pl = nil then Exit;
+
+  if (gGameSettings.GameMode in [GM_TDM, GM_CTF]) and (Pl.Team <> TmpTeam) then
+    Pl.SwitchTeam
+  else
+    Pl.SetColor(TmpColor);
+
+  if Pl.Name <> TmpName then
+  begin
+    g_Console_Add(Format(_lc[I_PLAYER_NAME], [Pl.Name, TmpName]), True);
+    Pl.Name := TmpName;
+  end;
+
+  if TmpModel <> Pl.Model.Name then
+    Pl.SetModel(TmpModel);
+
+  MH_SEND_PlayerSettings(Pl.UID, TmpModel);
+end;
+
+// RCON
+
+procedure MH_RECV_RCONPassword(C: pTNetClient; P: Pointer);
+var
+  Pwd: string;
+begin
+  Pwd := e_Raw_Read_String(P);
+  if not NetAllowRCON then Exit;
+  if Pwd = NetRCONPassword then
+  begin
+    C^.RCONAuth := True;
+    MH_SEND_GameEvent(NET_EV_RCON, NET_RCON_PWGOOD, 'N', C^.ID);
+  end
+  else
+    MH_SEND_GameEvent(NET_EV_RCON, NET_RCON_PWBAD, 'N', C^.ID);
+end;
+
+procedure MH_RECV_RCONCommand(C: pTNetClient; P: Pointer);
+var
+  Cmd: string;
+begin
+  Cmd := e_Raw_Read_String(P);
+  if not NetAllowRCON then Exit;
+  if not C^.RCONAuth then
+  begin
+    MH_SEND_GameEvent(NET_EV_RCON, NET_RCON_NOAUTH, 'N', C^.ID);
+    Exit;
+  end;
+  g_Console_Process(Cmd);
+end;
+
+// MISC
+
+procedure MH_RECV_Vote(C: pTNetClient; P: Pointer);
+var
+  Start: Boolean;
+  Name, Command: string;
+  Need: Integer;
+  Pl: TPlayer;
+begin
+  Start := e_Raw_Read_Byte(P) <> 0;
+  Command := e_Raw_Read_String(P);
+
+  Pl := g_Player_Get(C^.Player);
+  if Pl = nil then Exit;
+  Name := Pl.Name;
+    
+  if Start then
+  begin
+    if not g_Console_CommandBlacklisted(Command) then
+      g_Game_StartVote(Command, Name);
+  end
+  else if gVoteInProgress then
+  begin
+    if (gPlayer1 <> nil) or (gPlayer2 <> nil) then
+      Need := Floor((NetClientCount+1)/2.0) + 1
+    else
+      Need := Floor(NetClientCount/2.0) + 1;
+    if C^.Voted then
+    begin
+      Dec(gVoteCount);
+      C^.Voted := False;
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_REVOKED], [Name, gVoteCount, Need]), True);
+      MH_SEND_VoteEvent(NET_VE_REVOKE, Name, 'a', gVoteCount, Need);
+    end
+    else
+    begin
+      Inc(gVoteCount);
+      C^.Voted := True;
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_VOTE], [Name, gVoteCount, Need]), True);
+      MH_SEND_VoteEvent(NET_VE_VOTE, Name, 'a', gVoteCount, Need);
+      g_Game_CheckVote;
+    end;
+  end;
+end;
+
+// GAME (SEND)
+
+procedure MH_SEND_Everything(CreatePlayers: Boolean = False; ID: Integer = NET_EVERYONE);
+var
+  I: Integer;
+begin
+  if gPlayers <> nil then
+    for I := Low(gPlayers) to High(gPlayers) do
+      if gPlayers[I] <> nil then
+      begin
+        if CreatePlayers then MH_SEND_PlayerCreate(gPlayers[I].UID, ID);
+        MH_SEND_PlayerPos(True, gPlayers[I].UID, ID);
+        MH_SEND_PlayerStats(gPlayers[I].UID, ID);
+
+        if (gPlayers[I].Flag <> FLAG_NONE) and (gGameSettings.GameMode = GM_CTF) then
+          MH_SEND_FlagEvent(FLAG_STATE_CAPTURED, gPlayers[I].Flag, gPlayers[I].UID, True, ID);
+      end;
+
+  if gItems <> nil then
+  begin
+    for I := High(gItems) downto Low(gItems) do
+      if gItems[I].Live then
+        MH_SEND_ItemSpawn(True, I, ID);
+  end;
+
+  if gMonsters <> nil then
+    for I := 0 to High(gMonsters) do
+      if gMonsters[I] <> nil then
+        MH_SEND_MonsterSpawn(gMonsters[I].UID, ID);
+
+  if gWalls <> nil then
+    for I := Low(gWalls) to High(gWalls) do
+      if gWalls[I] <> nil then
+        with gWalls[I] do
+        begin
+          if Door then
+            MH_SEND_PanelState(PanelType, I, ID);
+
+          if GetTextureCount > 1 then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+        end;
+
+  if gLifts <> nil then
+    for I := Low(gLifts) to High(gLifts) do
+      if gLifts[I] <> nil then
+        with gLifts[I] do
+          MH_SEND_PanelState(PanelType, I, ID);
+
+  if gRenderForegrounds <> nil then
+    for I := Low(gRenderForegrounds) to High(gRenderForegrounds) do
+      if gRenderForegrounds[I] <> nil then
+        with gRenderForegrounds[I] do
+          if (GetTextureCount > 1) then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+  if gRenderBackgrounds <> nil then
+    for I := Low(gRenderBackgrounds) to High(gRenderBackgrounds) do
+      if gRenderBackgrounds[I] <> nil then
+        with gRenderBackgrounds[I] do
+          if GetTextureCount > 1 then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+  if gWater <> nil then
+    for I := Low(gWater) to High(gWater) do
+      if gWater[I] <> nil then
+        with gWater[I] do
+          if GetTextureCount > 1 then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+  if gAcid1 <> nil then
+    for I := Low(gAcid1) to High(gAcid1) do
+      if gAcid1[I] <> nil then
+        with gAcid1[I] do
+          if GetTextureCount > 1 then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+  if gAcid2 <> nil then
+    for I := Low(gAcid2) to High(gAcid2) do
+      if gAcid2[I] <> nil then
+        with gAcid2[I] do
+          if GetTextureCount > 1 then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+  if gSteps <> nil then
+    for I := Low(gSteps) to High(gSteps) do
+      if gSteps[I] <> nil then
+        with gSteps[I] do
+          if GetTextureCount > 1 then
+            MH_SEND_PanelTexture(PanelType, I, LastAnimLoop, ID);
+
+  if gTriggers <> nil then
+    for I := Low(gTriggers) to High(gTriggers) do
+      if gTriggers[I].TriggerType = TRIGGER_SOUND then
+        MH_SEND_TriggerSound(gTriggers[I], ID);
+
+  if Shots <> nil then
+    for I := Low(Shots) to High(Shots) do
+      if Shots[i].ShotType in [6, 7, 8] then
+        MH_SEND_CreateShot(i, ID);
+
+  MH_SEND_TriggerMusic(ID);
+
+  MH_SEND_GameStats(ID);
+  MH_SEND_CoopStats(ID);
+
+  if gGameSettings.GameMode = GM_CTF then
+  begin
+    if gFlags[FLAG_RED].State <> FLAG_STATE_CAPTURED then
+      MH_SEND_FlagEvent(gFlags[FLAG_RED].State, FLAG_RED, 0, True, ID);
+    if gFlags[FLAG_BLUE].State <> FLAG_STATE_CAPTURED then
+      MH_SEND_FlagEvent(gFlags[FLAG_BLUE].State, FLAG_BLUE, 0, True, ID);
+  end;
+
+  if CreatePlayers and (ID >= 0) then NetClients[ID].State := NET_STATE_GAME;
+
+  if gLMSRespawn > LMS_RESPAWN_NONE then
+    MH_SEND_GameEvent(NET_EV_LMS_WARMUP, (gLMSRespawnTime - gTime) div 1000, 'N', ID);
+end;
+
+procedure MH_SEND_Info(ID: Byte);
+var
+  Map: string;
+begin
+  g_ProcessResourceStr(gMapInfo.Map, nil, nil, @Map);
+
+  e_Buffer_Clear(@NetOut);
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_INFO));
+  e_Buffer_Write(@NetOut, ID);
+  e_Buffer_Write(@NetOut, NetClients[ID].Player);
+  e_Buffer_Write(@NetOut, gGameSettings.WAD);
+  e_Buffer_Write(@NetOut, Map);
+  e_Buffer_Write(@NetOut, gWADHash);
+  e_Buffer_Write(@NetOut, gGameSettings.GameMode);
+  e_Buffer_Write(@NetOut, gGameSettings.GoalLimit);
+  e_Buffer_Write(@NetOut, gGameSettings.TimeLimit);
+  e_Buffer_Write(@NetOut, gGameSettings.MaxLives);
+  e_Buffer_Write(@NetOut, gGameSettings.Options);
+  e_Buffer_Write(@NetOut, gTime);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_SERVICE);
+end;
+
+procedure MH_SEND_Chat(Txt: string; Mode: Byte; ID: Integer = NET_EVERYONE);
+var
+  Name: string;
+  i: Integer;
+  Team: Byte;
+begin
+  if (Mode = NET_CHAT_TEAM) and (not gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+    Mode := NET_CHAT_PLAYER;
+
+  Team := 0;
+  if (Mode = NET_CHAT_TEAM) then
+  begin
+    for i := Low(gPlayers) to High(gPlayers) do
+      if (gPlayers[i] <> nil) and (gPlayers[i].FClientID >= 0) and
+         (gPlayers[i].Team = ID) then
+      begin
+        e_Buffer_Write(@NetOut, Byte(NET_MSG_CHAT));
+        e_Buffer_Write(@NetOut, Txt);
+        e_Buffer_Write(@NetOut, Mode);
+        g_Net_Host_Send(gPlayers[i].FClientID, True, NET_CHAN_CHAT);
+      end;
+    Team := ID;
+    ID := NET_EVERYONE;
+  end
+  else
+  begin
+    e_Buffer_Write(@NetOut, Byte(NET_MSG_CHAT));
+    e_Buffer_Write(@NetOut, Txt);
+    e_Buffer_Write(@NetOut, Mode);
+    g_Net_Host_Send(ID, True, NET_CHAN_CHAT);
+  end;
+
+  if Mode = NET_CHAT_SYSTEM then
+    Exit;
+
+  if ID = NET_EVERYONE then
+  begin
+    if Mode = NET_CHAT_PLAYER then
+    begin
+      g_Console_Add(Txt, True);
+      e_WriteLog('[Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
+      g_Sound_PlayEx('SOUND_GAME_RADIO');
+    end
+    else
+    if Mode = NET_CHAT_TEAM then
+      if gPlayer1 <> nil then
+      begin
+        if (gPlayer1.Team = TEAM_RED) and (Team = TEAM_RED) then
+        begin
+          g_Console_Add(#18'[Team] '#2 + Txt, True);
+          e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
+          g_Sound_PlayEx('SOUND_GAME_RADIO');
+        end
+        else if (gPlayer1.Team = TEAM_BLUE) and (Team = TEAM_BLUE) then
+        begin
+          g_Console_Add(#20'[Team] '#2 + Txt, True);
+          e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
+          g_Sound_PlayEx('SOUND_GAME_RADIO');
+        end;
+      end;
+  end
+  else
+  begin
+    Name := g_Net_ClientName_ByID(ID);
+    g_Console_Add('-> ' + Name + ': ' + Txt, True);
+    e_WriteLog('[Tell ' + Name + '] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
+    g_Sound_PlayEx('SOUND_GAME_RADIO');
+  end;
+end;
+
+procedure MH_SEND_Effect(X, Y: Integer; Ang: SmallInt; Kind: Byte; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_GFX));
+  e_Buffer_Write(@NetOut, Kind);
+  e_Buffer_Write(@NetOut, X);
+  e_Buffer_Write(@NetOut, Y);
+  e_Buffer_Write(@NetOut, Ang);
+
+  g_Net_Host_Send(ID, False, NET_CHAN_GAME);
+end;
+
+procedure MH_SEND_Sound(X, Y: Integer; Name: string; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_SND));
+  e_Buffer_Write(@NetOut, Name);
+  e_Buffer_Write(@NetOut, X);
+  e_Buffer_Write(@NetOut, Y);
+
+  g_Net_Host_Send(ID, False, NET_CHAN_GAME);
+end;
+
+procedure MH_SEND_CreateShot(Proj: LongInt; ID: Integer = NET_EVERYONE);
+begin
+  if (Shots = nil) or (Proj < 0) or (Proj > High(Shots)) then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_SHADD));
+  e_Buffer_Write(@NetOut, Proj);
+  e_Buffer_Write(@NetOut, Shots[Proj].ShotType);
+  e_Buffer_Write(@NetOut, Shots[Proj].Target);
+  e_Buffer_Write(@NetOut, Shots[Proj].SpawnerUID);
+  e_Buffer_Write(@NetOut, Shots[Proj].Timeout);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.X);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.Y);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.Vel.X);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.Vel.Y);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_SHOTS);
+end;
+
+procedure MH_SEND_UpdateShot(Proj: LongInt; ID: Integer = NET_EVERYONE);
+begin
+  if (Shots = nil) or (Proj < 0) or (Proj > High(Shots)) then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_SHPOS));
+  e_Buffer_Write(@NetOut, Proj);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.X);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.Y);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.Vel.X);
+  e_Buffer_Write(@NetOut, Shots[Proj].Obj.Vel.Y);
+
+  g_Net_Host_Send(ID, False, NET_CHAN_SHOTS);
+end;
+
+procedure MH_Send_DeleteShot(Proj: LongInt; X, Y: LongInt; Loud: Boolean = True; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_SHDEL));
+  e_Buffer_Write(@NetOut, Proj);
+  e_Buffer_Write(@NetOut, Byte(Loud));
+  e_Buffer_Write(@NetOut, X);
+  e_Buffer_Write(@NetOut, Y);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_SHOTS);
+end;
+
+procedure MH_SEND_GameStats(ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_SCORE));
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+  begin
+    e_Buffer_Write(@NetOut, gTeamStat[TEAM_RED].Goals);
+    e_Buffer_Write(@NetOut, gTeamStat[TEAM_BLUE].Goals);
+  end
+  else
+    if gGameSettings.GameMode = GM_COOP then
+    begin
+      e_Buffer_Write(@NetOut, gCoopMonstersKilled);
+      e_Buffer_Write(@NetOut, gCoopSecretsFound);
+    end;
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MH_SEND_CoopStats(ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_COOP));
+  e_Buffer_Write(@NetOut, gTotalMonsters);
+  e_Buffer_Write(@NetOut, gSecretsCount);
+  e_Buffer_Write(@NetOut, gCoopTotalMonstersKilled);
+  e_Buffer_Write(@NetOut, gCoopTotalSecretsFound);
+  e_Buffer_Write(@NetOut, gCoopTotalMonsters);
+  e_Buffer_Write(@NetOut, gCoopTotalSecrets);
+end;
+
+procedure MH_SEND_GameEvent(EvType: Byte; EvNum: Integer = 0; EvStr: string = 'N'; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_GEVENT));
+  e_Buffer_Write(@NetOut, EvType);
+  e_Buffer_Write(@NetOut, EvNum);
+  e_Buffer_Write(@NetOut, EvStr);
+  e_Buffer_Write(@NetOut, Byte(gLastMap));
+  e_Buffer_Write(@NetOut, gTime);
+  if (EvType = NET_EV_MAPSTART) and (Pos(':\', EvStr) > 0) then
+  begin
+    e_Buffer_Write(@NetOut, Byte(1));
+    e_Buffer_Write(@NetOut, gWADHash);
+  end else
+    e_Buffer_Write(@NetOut, Byte(0));
+
+  g_Net_Host_Send(ID, True, NET_CHAN_SERVICE);
+end;
+
+procedure MH_SEND_FlagEvent(EvType: Byte; Flag: Byte; PID: Word; Quiet: Boolean = False; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_FLAG));
+  e_Buffer_Write(@NetOut, EvType);
+  e_Buffer_Write(@NetOut, Flag);
+  e_Buffer_Write(@NetOut, Byte(Quiet));
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, gFlags[Flag].State);
+  e_Buffer_Write(@NetOut, gFlags[Flag].CaptureTime);
+  e_Buffer_Write(@NetOut, gFlags[Flag].Obj.X);
+  e_Buffer_Write(@NetOut, gFlags[Flag].Obj.Y);
+  e_Buffer_Write(@NetOut, gFlags[Flag].Obj.Vel.X);
+  e_Buffer_Write(@NetOut, gFlags[Flag].Obj.Vel.Y);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MH_SEND_GameSettings(ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_GSET));
+  e_Buffer_Write(@NetOut, gGameSettings.GameMode);
+  e_Buffer_Write(@NetOut, gGameSettings.GoalLimit);
+  e_Buffer_Write(@NetOut, gGameSettings.TimeLimit);
+  e_Buffer_Write(@NetOut, gGameSettings.MaxLives);
+  e_Buffer_Write(@NetOut, gGameSettings.Options);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT);
+end;
+
+// PLAYER (SEND)
+
+procedure MH_SEND_PlayerCreate(PID: Word; ID: Integer = NET_EVERYONE);
+var
+  P: TPlayer;
+begin
+  P := g_Player_Get(PID);
+  if P = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLR));
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, P.Name);
+
+  e_Buffer_Write(@NetOut, P.FActualModelName);
+  e_Buffer_Write(@NetOut, P.FColor.R);
+  e_Buffer_Write(@NetOut, P.FColor.G);
+  e_Buffer_Write(@NetOut, P.FColor.B);
+  e_Buffer_Write(@NetOut, P.Team);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT)
+end;
+
+procedure MH_SEND_PlayerPos(Reliable: Boolean; PID: Word; ID: Integer = NET_EVERYONE);
+var
+  kByte: Word;
+  Pl: TPlayer;
+begin
+  Pl := g_Player_Get(PID);
+  if Pl = nil then Exit;
+  if Pl.FDummy then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRPOS));
+  e_Buffer_Write(@NetOut, gTime);
+  e_Buffer_Write(@NetOut, PID);
+
+  kByte := 0;
+
+  with Pl do
+  begin
+    e_Buffer_Write(@NetOut, FPing);
+    e_Buffer_Write(@NetOut, FLoss);
+    if IsKeyPressed(KEY_CHAT) then
+      kByte := NET_KEY_CHAT
+    else
+    begin
+      if IsKeyPressed(KEY_LEFT) then kByte := kByte or NET_KEY_LEFT;
+      if IsKeyPressed(KEY_RIGHT) then kByte := kByte or NET_KEY_RIGHT;
+      if IsKeyPressed(KEY_UP) then kByte := kByte or NET_KEY_UP;
+      if IsKeyPressed(KEY_DOWN) then kByte := kByte or NET_KEY_DOWN;
+      if IsKeyPressed(KEY_JUMP) then kByte := kByte or NET_KEY_JUMP;
+      if JustTeleported then kByte := kByte or NET_KEY_FORCEDIR;
+    end;
+
+    e_Buffer_Write(@NetOut, kByte);
+    if Direction = D_LEFT then e_Buffer_Write(@NetOut, Byte(0)) else e_Buffer_Write(@NetOut, Byte(1));
+    e_Buffer_Write(@NetOut, GameX);
+    e_Buffer_Write(@NetOut, GameY);
+    e_Buffer_Write(@NetOut, GameVelX);
+    e_Buffer_Write(@NetOut, GameVelY);
+    e_Buffer_Write(@NetOut, GameAccelX);
+    e_Buffer_Write(@NetOut, GameAccelY);
+  end;
+
+  g_Net_Host_Send(ID, Reliable, NET_CHAN_PLAYERPOS);
+end;
+
+procedure MH_SEND_PlayerStats(PID: Word; ID: Integer = NET_EVERYONE);
+var
+  P: TPlayer;
+  I: Integer;
+begin
+  P := g_Player_Get(PID);
+  if P = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRSTA));
+  e_Buffer_Write(@NetOut, PID);
+
+  with P do
+  begin
+    e_Buffer_Write(@NetOut, Byte(Live));
+    e_Buffer_Write(@NetOut, Byte(GodMode));
+    e_Buffer_Write(@NetOut, Health);
+    e_Buffer_Write(@NetOut, Armor);
+    e_Buffer_Write(@NetOut, Air);
+    e_Buffer_Write(@NetOut, JetFuel);
+    e_Buffer_Write(@NetOut, Lives);
+    e_Buffer_Write(@NetOut, Team);
+
+    for I := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+      e_Buffer_Write(@NetOut, Byte(FWeapon[I]));
+
+    for I := A_BULLETS to A_CELLS do
+      e_Buffer_Write(@NetOut, FAmmo[I]);
+
+    for I := A_BULLETS to A_CELLS do
+      e_Buffer_Write(@NetOut, FMaxAmmo[I]);
+
+    for I := MR_SUIT to MR_MAX do
+      e_Buffer_Write(@NetOut, LongWord(FMegaRulez[I]));
+
+    e_Buffer_Write(@NetOut, Byte(R_ITEM_BACKPACK in FRulez));
+    e_Buffer_Write(@NetOut, Byte(R_KEY_RED in FRulez));
+    e_Buffer_Write(@NetOut, Byte(R_KEY_GREEN in FRulez));
+    e_Buffer_Write(@NetOut, Byte(R_KEY_BLUE in FRulez));
+    e_Buffer_Write(@NetOut, Byte(R_BERSERK in FRulez));
+
+    e_Buffer_Write(@NetOut, Frags);
+    e_Buffer_Write(@NetOut, Death);
+
+    e_Buffer_Write(@NetOut, CurrWeap);
+
+    e_Buffer_Write(@NetOut, Byte(FSpectator));
+    e_Buffer_Write(@NetOut, Byte(FGhost));
+    e_Buffer_Write(@NetOut, Byte(FPhysics));
+    e_Buffer_Write(@NetOut, Byte(FNoRespawn));
+    e_Buffer_Write(@NetOut, Byte(FJetpack));
+  end;
+
+  g_Net_Host_Send(ID, True, NET_CHAN_PLAYER);
+end;
+
+procedure MH_SEND_PlayerDamage(PID: Word; Kind: Byte; Attacker, Value: Word; VX, VY: Integer; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRDMG));
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, Kind);
+  e_Buffer_Write(@NetOut, Attacker);
+  e_Buffer_Write(@NetOut, Value);
+  e_Buffer_Write(@NetOut, VX);
+  e_Buffer_Write(@NetOut, VY);
+
+  g_Net_Host_Send(ID, False, NET_CHAN_PLAYER);
+end;
+
+procedure MH_SEND_PlayerDeath(PID: Word; KillType, DeathType: Byte; Attacker: Word; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRDIE));
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, KillType);
+  e_Buffer_Write(@NetOut, DeathType);
+  e_Buffer_Write(@NetOut, Attacker);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_PLAYER);
+end;
+
+procedure MH_SEND_PlayerFire(PID: Word; Weapon: Byte; X, Y, AX, AY: Integer; ShotID: Integer = -1; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRFIRE));
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, Weapon);
+  e_Buffer_Write(@NetOut, X);
+  e_Buffer_Write(@NetOut, Y);
+  e_Buffer_Write(@NetOut, AX);
+  e_Buffer_Write(@NetOut, AY);
+  e_Buffer_Write(@NetOut, ShotID);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_SHOTS);
+end;
+
+procedure MH_SEND_PlayerDelete(PID: Word; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRDEL));
+  e_Buffer_Write(@NetOut, PID);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MH_SEND_PlayerSettings(PID: Word; Mdl: string = ''; ID: Integer = NET_EVERYONE);
+var
+  Pl: TPlayer;
+begin
+  Pl := g_Player_Get(PID);
+  if Pl = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRSET));
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, Pl.Name);
+  if Mdl = '' then
+    e_Buffer_Write(@NetOut, Pl.Model.Name)
+  else
+    e_Buffer_Write(@NetOut, Mdl);
+  e_Buffer_Write(@NetOut, Pl.FColor.R);
+  e_Buffer_Write(@NetOut, Pl.FColor.G);
+  e_Buffer_Write(@NetOut, Pl.FColor.B);
+  e_Buffer_Write(@NetOut, Pl.Team);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT);
+end;
+
+// ITEM (SEND)
+
+procedure MH_SEND_ItemSpawn(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_ISPAWN));
+  e_Buffer_Write(@NetOut, IID);
+  e_Buffer_Write(@NetOut, Byte(Quiet));
+  e_Buffer_Write(@NetOut, gItems[IID].ItemType);
+  e_Buffer_Write(@NetOut, Byte(gItems[IID].Fall));
+  e_Buffer_Write(@NetOut, Byte(gItems[IID].Respawnable));
+  e_Buffer_Write(@NetOut, gItems[IID].Obj.X);
+  e_Buffer_Write(@NetOut, gItems[IID].Obj.Y);
+  e_Buffer_Write(@NetOut, gItems[IID].Obj.Vel.X);
+  e_Buffer_Write(@NetOut, gItems[IID].Obj.Vel.Y);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
+end;
+
+procedure MH_SEND_ItemDestroy(Quiet: Boolean; IID: Word; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_IDEL));
+  e_Buffer_Write(@NetOut, IID);
+  e_Buffer_Write(@NetOut, Byte(Quiet));
+
+  g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
+end;
+
+// PANEL
+
+procedure MH_SEND_PanelTexture(PType: Word; PID: LongWord; AnimLoop: Byte; ID: Integer = NET_EVERYONE);
+var
+  TP: TPanel;
+begin
+  case PType of
+    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+      TP := gWalls[PID];
+    PANEL_FORE:
+      TP := gRenderForegrounds[PID];
+    PANEL_BACK:
+      TP := gRenderBackgrounds[PID];
+    PANEL_WATER:
+      TP := gWater[PID];
+    PANEL_ACID1:
+      TP := gAcid1[PID];
+    PANEL_ACID2:
+      TP := gAcid2[PID];
+    PANEL_STEP:
+      TP := gSteps[PID];
+    else
+      Exit;
+  end;
+
+  with TP do
+  begin
+    e_Buffer_Write(@NetOut, Byte(NET_MSG_PTEX));
+    e_Buffer_Write(@NetOut, PType);
+    e_Buffer_Write(@NetOut, PID);
+    e_Buffer_Write(@NetOut, FCurTexture);
+    e_Buffer_Write(@NetOut, FCurFrame);
+    e_Buffer_Write(@NetOut, FCurFrameCount);
+    e_Buffer_Write(@NetOut, AnimLoop);
+  end;
+
+  g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
+end;
+
+procedure MH_SEND_PanelState(PType: Word; PID: LongWord; ID: Integer = NET_EVERYONE);
+var
+  TP: TPanel;
+begin
+  case PType of
+    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+      TP := gWalls[PID];
+    PANEL_LIFTUP, PANEL_LIFTDOWN, PANEL_LIFTLEFT, PANEL_LIFTRIGHT:
+      TP := gLifts[PID];
+    else
+      Exit;
+  end;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PSTATE));
+  e_Buffer_Write(@NetOut, PType);
+  e_Buffer_Write(@NetOut, PID);
+  e_Buffer_Write(@NetOut, Byte(TP.Enabled));
+  e_Buffer_Write(@NetOut, TP.LiftType);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
+end;
+
+// TRIGGER
+
+procedure MH_SEND_TriggerSound(var T: TTrigger; ID: Integer = NET_EVERYONE);
+begin
+  if gTriggers = nil then Exit;
+  if T.Sound = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_TSOUND));
+  e_Buffer_Write(@NetOut, T.ClientID);
+  e_Buffer_Write(@NetOut, Byte(T.Sound.IsPlaying));
+  e_Buffer_Write(@NetOut, LongWord(T.Sound.GetPosition));
+  e_Buffer_Write(@NetOut, T.SoundPlayCount);
+
+  g_Net_Host_Send(ID, True);
+end;
+
+procedure MH_SEND_TriggerMusic(ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_TMUSIC));
+  e_Buffer_Write(@NetOut, gMusic.Name);
+  e_Buffer_Write(@NetOut, Byte(gMusic.IsPlaying));
+  e_Buffer_Write(@NetOut, LongWord(gMusic.GetPosition));
+  e_Buffer_Write(@NetOut, Byte(gMusic.SpecPause or gMusic.IsPaused));
+
+  g_Net_Host_Send(ID, True);
+end;
+
+// MONSTER
+
+procedure MH_SEND_MonsterSpawn(UID: Word; ID: Integer = NET_EVERYONE);
+var
+  M: TMonster;
+begin
+  M := g_Monsters_Get(UID);
+  if M = nil then
+    Exit;
+
+  with M do
+  begin
+    e_Buffer_Write(@NetOut, Byte(NET_MSG_MSPAWN));
+    e_Buffer_Write(@NetOut, UID);
+    e_Buffer_Write(@NetOut, MonsterType);
+    e_Buffer_Write(@NetOut, MonsterState);
+    e_Buffer_Write(@NetOut, MonsterAnim);
+    e_Buffer_Write(@NetOut, MonsterTargetUID);
+    e_Buffer_Write(@NetOut, MonsterTargetTime);
+    e_Buffer_Write(@NetOut, MonsterBehaviour);
+    e_Buffer_Write(@NetOut, MonsterSleep);
+    e_Buffer_Write(@NetOut, MonsterHealth);
+    e_Buffer_Write(@NetOut, MonsterAmmo);
+    e_Buffer_Write(@NetOut, GameX);
+    e_Buffer_Write(@NetOut, GameY);
+    e_Buffer_Write(@NetOut, GameVelX);
+    e_Buffer_Write(@NetOut, GameVelY);
+    e_Buffer_Write(@NetOut, Byte(GameDirection));
+  end;
+
+  g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
+end;
+
+procedure MH_SEND_MonsterPos(UID: Word; ID: Integer = NET_EVERYONE);
+var
+  M: TMonster;
+begin
+  M := g_Monsters_Get(UID);
+  if M = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_MPOS));
+  e_Buffer_Write(@NetOut, UID);
+
+  with M do
+  begin
+    e_Buffer_Write(@NetOut, GameX);
+    e_Buffer_Write(@NetOut, GameY);
+    e_Buffer_Write(@NetOut, GameVelX);
+    e_Buffer_Write(@NetOut, GameVelY);
+    e_Buffer_Write(@NetOut, Byte(GameDirection));
+  end;
+
+  g_Net_Host_Send(ID, False, NET_CHAN_MONSTERPOS);
+end;
+
+procedure MH_SEND_MonsterState(UID: Word; ForcedAnim: Byte = 255; ID: Integer = NET_EVERYONE);
+var
+  M: TMonster;
+begin
+  M := g_Monsters_Get(UID);
+  if M = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_MSTATE));
+  e_Buffer_Write(@NetOut, UID);
+
+  with M do
+  begin
+    e_Buffer_Write(@NetOut, MonsterState);
+    e_Buffer_Write(@NetOut, ForcedAnim);
+    e_Buffer_Write(@NetOut, MonsterTargetUID);
+    e_Buffer_Write(@NetOut, MonsterTargetTime);
+    e_Buffer_Write(@NetOut, MonsterSleep);
+    e_Buffer_Write(@NetOut, MonsterHealth);
+    e_Buffer_Write(@NetOut, MonsterAmmo);
+    e_Buffer_Write(@NetOut, MonsterPain);
+    e_Buffer_Write(@NetOut, Byte(AnimIsReverse));
+  end;
+
+  g_Net_Host_Send(ID, True, NET_CHAN_MONSTER);
+end;
+
+procedure MH_SEND_MonsterShot(UID: Word; X, Y, VX, VY: Integer; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_MSHOT));
+  e_Buffer_Write(@NetOut, UID);
+  e_Buffer_Write(@NetOut, X);
+  e_Buffer_Write(@NetOut, Y);
+  e_Buffer_Write(@NetOut, VX);
+  e_Buffer_Write(@NetOut, VY);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_MONSTER);
+end;
+
+procedure MH_SEND_MonsterDelete(UID: Word; ID: Integer = NET_EVERYONE);
+var
+  M: TMonster;
+begin
+  M := g_Monsters_Get(UID);
+  if M = nil then Exit;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_MDEL));
+  e_Buffer_Write(@NetOut, UID);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_LARGEDATA);
+end;
+
+// MISC
+
+procedure MH_SEND_TimeSync(Time: LongWord; ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_TIME_SYNC));
+  e_Buffer_Write(@NetOut, Time);
+
+  g_Net_Host_Send(ID, False, NET_CHAN_SERVICE);
+end;
+
+procedure MH_SEND_VoteEvent(EvType: Byte;
+                            StrArg1: string = 'a'; StrArg2: string = 'b';
+                            IntArg1: SmallInt = 0; IntArg2: SmallInt = 0;
+                            ID: Integer = NET_EVERYONE);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_VOTE_EVENT));
+  e_Buffer_Write(@NetOut, EvType);
+  e_Buffer_Write(@NetOut, IntArg1);
+  e_Buffer_Write(@NetOut, IntArg2);
+  e_Buffer_Write(@NetOut, StrArg1);
+  e_Buffer_Write(@NetOut, StrArg2);
+
+  g_Net_Host_Send(ID, True, NET_CHAN_IMPORTANT);
+end;
+
+// CLIENT MESSAGES //
+
+// GAME
+
+procedure MC_RECV_Chat(P: Pointer);
+var
+  Txt: string;
+  Mode: Byte;
+begin
+  Txt := e_Raw_Read_String(P);
+  Mode := e_Raw_Read_Byte(P);
+
+  if Mode <> NET_CHAT_SYSTEM then
+  begin
+    if Mode = NET_CHAT_PLAYER then
+    begin
+      g_Console_Add(Txt, True);
+      e_WriteLog('[Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
+      g_Sound_PlayEx('SOUND_GAME_RADIO');
+    end else
+    if (Mode = NET_CHAT_TEAM) and (gPlayer1 <> nil) then
+    begin
+      if gPlayer1.Team = TEAM_RED then
+        g_Console_Add(b_Text_Format('\r[Team] ') + Txt, True);
+      if gPlayer1.Team = TEAM_BLUE then
+        g_Console_Add(b_Text_Format('\b[Team] ') + Txt, True);
+      e_WriteLog('[Team Chat] ' + b_Text_Unformat(Txt), MSG_NOTIFY);
+      g_Sound_PlayEx('SOUND_GAME_RADIO');
+    end;
+  end else
+    g_Console_Add(Txt, True);
+end;
+
+procedure MC_RECV_Effect(P: Pointer);
+var
+  Kind: Byte;
+  X, Y: Integer;
+  Ang: SmallInt;
+  Anim: TAnimation;
+  ID: LongWord;
+begin
+  if not gGameOn then Exit;
+  Kind := e_Raw_Read_Byte(P);
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  Ang := e_Raw_Read_SmallInt(P);
+
+  case Kind of
+    NET_GFX_SPARK:
+      g_GFX_Spark(X, Y, 2 + Random(2), Ang, 0, 0);
+
+    NET_GFX_TELE:
+    begin
+      if g_Frames_Get(ID, 'FRAMES_TELEPORT') then
+      begin
+        Anim := TAnimation.Create(ID, False, 3);
+        g_GFX_OnceAnim(X, Y, Anim);
+        Anim.Free();
+      end;
+      if Ang = 1 then
+        g_Sound_PlayExAt('SOUND_GAME_TELEPORT', X, Y);
+    end;
+
+    NET_GFX_EXPLODE:
+    begin
+      if g_Frames_Get(ID, 'FRAMES_EXPLODE_ROCKET') then
+      begin
+        Anim := TAnimation.Create(ID, False, 6);
+        Anim.Blending := False;
+        g_GFX_OnceAnim(X-64, Y-64, Anim);
+        Anim.Free();
+      end;
+      if Ang = 1 then
+        g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEROCKET', X, Y);
+    end;
+
+    NET_GFX_BFGEXPL:
+    begin
+      if g_Frames_Get(ID, 'FRAMES_EXPLODE_BFG') then
+      begin
+        Anim := TAnimation.Create(ID, False, 6);
+        Anim.Blending := False;
+        g_GFX_OnceAnim(X-64, Y-64, Anim);
+        Anim.Free();
+      end;
+      if Ang = 1 then
+        g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBFG', X, Y);
+    end;
+
+    NET_GFX_BFGHIT:
+    begin
+      if g_Frames_Get(ID, 'FRAMES_BFGHIT') then
+      begin
+        Anim := TAnimation.Create(ID, False, 4);
+        g_GFX_OnceAnim(X-32, Y-32, Anim);
+        Anim.Free();
+      end;
+    end;
+
+    NET_GFX_FIRE:
+    begin
+      if g_Frames_Get(ID, 'FRAMES_FIRE') then
+      begin
+        Anim := TAnimation.Create(ID, False, 4);
+        g_GFX_OnceAnim(X, Y, Anim);
+        Anim.Free();
+      end;
+      if Ang = 1 then
+        g_Sound_PlayExAt('SOUND_FIRE', X, Y);
+    end;
+
+    NET_GFX_RESPAWN:
+    begin
+      if g_Frames_Get(ID, 'FRAMES_ITEM_RESPAWN') then
+      begin
+        Anim := TAnimation.Create(ID, False, 4);
+        g_GFX_OnceAnim(X, Y, Anim);
+        Anim.Free();
+      end;
+      if Ang = 1 then
+        g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', X, Y);
+    end;
+
+    NET_GFX_SHELL1:
+      g_Player_CreateShell(X, Y, 0, -2, SHELL_BULLET);
+
+    NET_GFX_SHELL2:
+      g_Player_CreateShell(X, Y, 0, -2, SHELL_SHELL);
+
+    NET_GFX_SHELL3:
+    begin
+      g_Player_CreateShell(X, Y, 0, -2, SHELL_SHELL);
+      g_Player_CreateShell(X, Y, 0, -2, SHELL_SHELL);
+    end;
+  end;
+end;
+
+procedure MC_RECV_Sound(P: Pointer);
+var
+  Name: string;
+  X, Y: Integer;
+begin
+  Name := e_Raw_Read_String(P);
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  g_Sound_PlayExAt(Name, X, Y);
+end;
+
+procedure MC_RECV_CreateShot(P: Pointer);
+var
+  I, X, Y, XV, YV: Integer;
+  Timeout: LongWord;
+  Target, Spawner: Word;
+  ShType: Byte;
+begin
+  I := e_Raw_Read_LongInt(P);
+  ShType := e_Raw_Read_Byte(P);
+  Target := e_Raw_Read_Word(P);
+  Spawner := e_Raw_Read_Word(P);
+  Timeout := e_Raw_Read_LongWord(P);
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  XV := e_Raw_Read_LongInt(P);
+  YV := e_Raw_Read_LongInt(P);
+
+  I := g_Weapon_CreateShot(I, ShType, Spawner, Target, X, Y, XV, YV);
+  if (Shots <> nil) and (I <= High(Shots)) then
+  begin
+    Shots[I].Timeout := Timeout;
+    //Shots[I].Target := Target; // TODO: find a use for Target later
+  end;
+end;
+
+procedure MC_RECV_UpdateShot(P: Pointer);
+var
+  I, TX, TY, TXV, TYV: Integer;
+begin
+  I := e_Raw_Read_LongInt(P);
+  TX := e_Raw_Read_LongInt(P);
+  TY := e_Raw_Read_LongInt(P);
+  TXV := e_Raw_Read_LongInt(P);
+  TYV := e_Raw_Read_LongInt(P);
+
+  if (Shots <> nil) and (I <= High(Shots)) then
+    with (Shots[i]) do
+    begin
+      Obj.X := TX;
+      Obj.Y := TY;
+      Obj.Vel.X := TXV;
+      Obj.Vel.Y := TYV;
+    end;
+end;
+
+procedure MC_RECV_DeleteShot(P: Pointer);
+var
+  I, X, Y: Integer;
+  L: Boolean;
+begin
+  if not gGameOn then Exit;
+  I := e_Raw_Read_LongInt(P);
+  L := (e_Raw_Read_Byte(P) <> 0);
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+
+  g_Weapon_DestroyShot(I, X, Y, L);
+end;
+
+procedure MC_RECV_GameStats(P: Pointer);
+begin
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+  begin
+    gTeamStat[TEAM_RED].Goals := e_Raw_Read_SmallInt(P);
+    gTeamStat[TEAM_BLUE].Goals := e_Raw_Read_SmallInt(P);
+  end
+  else
+    if gGameSettings.GameMode = GM_COOP then
+    begin
+      gCoopMonstersKilled := e_Raw_Read_Word(P);
+      gCoopSecretsFound := e_Raw_Read_Word(P);
+    end;
+end;
+
+procedure MC_RECV_CoopStats(P: Pointer);
+begin
+  gTotalMonsters := e_Raw_Read_LongInt(P);
+  gSecretsCount := e_Raw_Read_LongInt(P);
+  gCoopTotalMonstersKilled := e_Raw_Read_Word(P);
+  gCoopTotalSecretsFound := e_Raw_Read_Word(P);
+  gCoopTotalMonsters := e_Raw_Read_Word(P);
+  gCoopTotalSecrets := e_Raw_Read_Word(P);
+end;
+
+procedure MC_RECV_GameEvent(P: Pointer);
+var
+  EvType: Byte;
+  EvNum: Integer;
+  EvStr: string;
+  EvTime: LongWord;
+  BHash: Boolean;
+  EvHash: TMD5Digest;
+  pl: TPlayer;
+  i1, i2: TStrings_Locale;
+  pln: String;
+  cnt: Byte;
+begin
+  EvType := e_Raw_Read_Byte(P);
+  EvNum := e_Raw_Read_LongInt(P);
+  EvStr := e_Raw_Read_String(P);
+  gLastMap := e_Raw_Read_Byte(P) <> 0;
+  if gLastMap and (gGameSettings.GameMode = GM_COOP) then gStatsOff := True;
+  gStatsPressed := True;
+  EvTime := e_Raw_Read_LongWord(P);
+  BHash := e_Raw_Read_Byte(P) <> 0;
+  if BHash then
+    EvHash := e_Raw_Read_MD5(P);
+
+  gTime := EvTime;
+
+  case EvType of
+    NET_EV_MAPSTART:
+    begin
+      gGameOn := False;
+      g_Game_ClearLoading();
+      g_Game_StopAllSounds(True);
+
+      gSwitchGameMode := Byte(EvNum);
+      gGameSettings.GameMode := gSwitchGameMode;
+
+      gWADHash := EvHash;
+      if not g_Game_StartMap(EvStr, True) then
+      begin
+        if Pos(':\', EvStr) = 0 then
+          g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [gGameSettings.WAD + ':\' + EvStr]))
+        else
+          g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [EvStr]));
+        Exit;
+      end;
+
+      MC_SEND_FullStateRequest;
+    end;
+
+    NET_EV_MAPEND:
+    begin
+      gMissionFailed := EvNum <> 0;
+      gExit := EXIT_ENDLEVELCUSTOM;
+    end;
+
+    NET_EV_RCON:
+    begin
+      case EvNum of
+        NET_RCON_NOAUTH:
+          g_Console_Add(_lc[I_NET_RCON_NOAUTH], True);
+        NET_RCON_PWGOOD:
+          g_Console_Add(_lc[I_NET_RCON_PWD_VALID], True);
+        NET_RCON_PWBAD:
+          g_Console_Add(_lc[I_NET_RCON_PWD_INVALID], True);
+      end;
+    end;
+
+    NET_EV_CHANGE_TEAM:
+    begin
+      if EvNum = TEAM_RED then
+        g_Console_Add(Format(_lc[I_PLAYER_CHTEAM_RED], [EvStr]), True);
+      if EvNum = TEAM_BLUE then
+        g_Console_Add(Format(_lc[I_PLAYER_CHTEAM_BLUE], [EvStr]), True);
+    end;
+
+    NET_EV_PLAYER_KICK:
+      g_Console_Add(Format(_lc[I_PLAYER_KICK], [EvStr]), True);
+
+    NET_EV_PLAYER_BAN:
+      g_Console_Add(Format(_lc[I_PLAYER_BAN], [EvStr]), True);
+
+    NET_EV_LMS_WARMUP:
+      g_Console_Add(Format(_lc[I_MSG_WARMUP_START], [EvNum]), True);
+
+    NET_EV_LMS_SURVIVOR:
+      g_Console_Add('*** ' + _lc[I_MESSAGE_LMS_SURVIVOR] + ' ***', True);
+
+    NET_EV_BIGTEXT:
+      g_Game_Message(AnsiUpperCase(EvStr), Word(EvNum));
+
+    NET_EV_SCORE:
+    begin
+      pl := g_Player_Get(EvNum and $FFFF);
+      if pl = nil then
+        pln := '?'
+      else
+        pln := pl.Name;
+      cnt := (EvNum shr 16) and $FF;
+      if Pos('w', EvStr) = 0 then
+      begin
+        // Default score
+        if Pos('t', EvStr) = 0 then
+        begin
+          // Player +/- score
+          if Pos('-', EvStr) = 0 then
+          begin
+            if Pos('e', EvStr) = 0 then
+              i1 := I_PLAYER_SCORE_ADD_OWN
+            else
+              i1 := I_PLAYER_SCORE_ADD_ENEMY;
+          end else
+          begin
+            if Pos('e', EvStr) = 0 then
+              i1 := I_PLAYER_SCORE_SUB_OWN
+            else
+              i1 := I_PLAYER_SCORE_SUB_ENEMY;
+          end;
+          // Which team
+          if Pos('r', EvStr) > 0 then
+            i2 := I_PLAYER_SCORE_TO_RED
+          else
+            i2 := I_PLAYER_SCORE_TO_BLUE;
+          g_Console_Add(Format(_lc[i1], [pln, cnt, _lc[i2]]), True);
+        end else
+        begin
+          // Team +/- score
+          if Pos('-', EvStr) = 0 then
+            i1 := I_PLAYER_SCORE_ADD_TEAM
+          else
+            i1 := I_PLAYER_SCORE_SUB_TEAM;
+          // Which team
+          if Pos('r', EvStr) > 0 then
+            i2 := I_PLAYER_SCORE_RED
+          else
+            i2 := I_PLAYER_SCORE_BLUE;
+          g_Console_Add(Format(_lc[i1], [_lc[i2], cnt]), True);
+        end;
+      end else
+      begin
+        // Game Win
+        if Pos('e', EvStr) = 0 then
+          i1 := I_PLAYER_SCORE_WIN_OWN
+        else
+          i1 := I_PLAYER_SCORE_WIN_ENEMY;
+        // Which team
+        if Pos('r', EvStr) > 0 then
+          i2 := I_PLAYER_SCORE_TO_RED
+        else
+          i2 := I_PLAYER_SCORE_TO_BLUE;
+        g_Console_Add(Format(_lc[i1], [pln, _lc[i2]]), True);
+      end;
+    end;
+
+    NET_EV_SCORE_MSG:
+    begin
+      if EvNum = TEAM_RED then
+        g_Game_Message(Format(_lc[I_MESSAGE_SCORE_ADD], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 108);
+      if EvNum = TEAM_BLUE then
+        g_Game_Message(Format(_lc[I_MESSAGE_SCORE_ADD], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 108);
+      if EvNum = -TEAM_RED then
+        g_Game_Message(Format(_lc[I_MESSAGE_SCORE_SUB], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 108);
+      if EvNum = -TEAM_BLUE then
+        g_Game_Message(Format(_lc[I_MESSAGE_SCORE_SUB], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 108);
+    end;
+
+    NET_EV_LMS_START:
+    begin
+      g_Player_RemoveAllCorpses;
+      g_Game_Message(_lc[I_MESSAGE_LMS_START], 144);
+    end;
+
+    NET_EV_LMS_WIN:
+      g_Game_Message(Format(_lc[I_MESSAGE_LMS_WIN], [AnsiUpperCase(EvStr)]), 144);
+
+    NET_EV_TLMS_WIN:
+    begin
+      if EvNum = TEAM_RED then
+        g_Game_Message(Format(_lc[I_MESSAGE_TLMS_WIN], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 144);
+      if EvNum = TEAM_BLUE then
+        g_Game_Message(Format(_lc[I_MESSAGE_TLMS_WIN], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 144);
+    end;
+
+    NET_EV_LMS_LOSE:
+      g_Game_Message(_lc[I_MESSAGE_LMS_LOSE], 144);
+
+    NET_EV_LMS_DRAW:
+      g_Game_Message(_lc[I_GAME_WIN_DRAW], 144);
+
+    NET_EV_KILLCOMBO:
+      g_Game_Announce_KillCombo(EvNum);
+
+    NET_EV_PLAYER_TOUCH:
+    begin
+      pl := g_Player_Get(EvNum);
+      if pl <> nil then
+        pl.Touch();
+    end;
+
+  end;
+end;
+
+procedure MC_RECV_FlagEvent(P: Pointer);
+var
+  PID: Word;
+  Pl: TPlayer;
+  EvType: Byte;
+  Fl: Byte;
+  Quiet: Boolean;
+  s, ts: string;
+begin
+  EvType := e_Raw_Read_Byte(P);
+  Fl := e_Raw_Read_Byte(P);
+
+  if Fl = FLAG_NONE then Exit;
+
+  Quiet := (e_Raw_Read_Byte(P) <> 0);
+  PID := e_Raw_Read_Word(P);
+
+  gFlags[Fl].State := e_Raw_Read_Byte(P);
+  gFlags[Fl].CaptureTime := e_Raw_Read_LongWord(P);
+  gFlags[Fl].Obj.X := e_Raw_Read_LongInt(P);
+  gFlags[Fl].Obj.Y := e_Raw_Read_LongInt(P);
+  gFlags[Fl].Obj.Vel.X := e_Raw_Read_LongInt(P);
+  gFlags[Fl].Obj.Vel.Y := e_Raw_Read_LongInt(P);
+
+  Pl := g_Player_Get(PID);
+  if (Pl = nil) and
+     (EvType <> FLAG_STATE_NORMAL) and
+     (EvType <> FLAG_STATE_DROPPED) and
+     (EvType <> FLAG_STATE_RETURNED) then
+    Exit;
+
+  case EvType of
+    FLAG_STATE_NORMAL:
+    begin
+      if Quiet or (Pl = nil) then Exit;
+
+      if Fl = FLAG_RED then
+        s := _lc[I_PLAYER_FLAG_RED]
+      else
+        s := _lc[I_PLAYER_FLAG_BLUE];
+
+      g_Game_Message(Format(_lc[I_MESSAGE_FLAG_RETURN], [AnsiUpperCase(s)]), 144);
+    end;
+
+    FLAG_STATE_CAPTURED:
+    begin
+      if (Pl <> nil) then Pl.SetFlag(Fl);
+
+      if Quiet then Exit;
+
+      if Fl = FLAG_RED then
+        s := _lc[I_PLAYER_FLAG_RED]
+      else
+        s := _lc[I_PLAYER_FLAG_BLUE];
+
+      g_Console_Add(Format(_lc[I_PLAYER_FLAG_GET], [Pl.Name, s]), True);
+      g_Game_Message(Format(_lc[I_MESSAGE_FLAG_GET], [AnsiUpperCase(s)]), 144);
+    end;
+
+    FLAG_STATE_DROPPED:
+    begin
+      if (Pl <> nil) then Pl.SetFlag(FLAG_NONE);
+
+      if Quiet or (Pl = nil) then Exit;
+
+      if Fl = FLAG_RED then
+        s := _lc[I_PLAYER_FLAG_RED]
+      else
+        s := _lc[I_PLAYER_FLAG_BLUE];
+
+      g_Console_Add(Format(_lc[I_PLAYER_FLAG_DROP], [Pl.Name, s]), True);
+      g_Game_Message(Format(_lc[I_MESSAGE_FLAG_DROP], [AnsiUpperCase(s)]), 144);
+    end;
+
+    FLAG_STATE_SCORED:
+    begin
+      g_Map_ResetFlag(FLAG_RED);
+      g_Map_ResetFlag(FLAG_BLUE);
+      if Quiet or (Pl = nil) then Exit;
+      Pl.SetFlag(FLAG_NONE);
+
+      if Fl = FLAG_RED then
+        s := _lc[I_PLAYER_FLAG_RED]
+      else
+        s := _lc[I_PLAYER_FLAG_BLUE];
+
+      ts := Format('%.4d', [gFlags[Fl].CaptureTime]);
+      Insert('.', ts, Length(ts) + 1 - 3);
+      g_Console_Add(Format(_lc[I_PLAYER_FLAG_CAPTURE], [Pl.Name, s, ts]), True);
+      g_Game_Message(Format(_lc[I_MESSAGE_FLAG_CAPTURE], [AnsiUpperCase(s)]), 144);
+    end;
+
+    FLAG_STATE_RETURNED:
+    begin
+      g_Map_ResetFlag(Fl);
+      if Quiet then Exit;
+
+      if Fl = FLAG_RED then
+        s := _lc[I_PLAYER_FLAG_RED]
+      else
+        s := _lc[I_PLAYER_FLAG_BLUE];
+
+      g_Game_Message(Format(_lc[I_MESSAGE_FLAG_RETURN], [AnsiUpperCase(s)]), 144);
+    end;
+  end;
+end;
+
+procedure MC_RECV_GameSettings(P: Pointer);
+begin
+  gGameSettings.GameMode := e_Raw_Read_Byte(P);
+  gGameSettings.GoalLimit := e_Raw_Read_Word(P);
+  gGameSettings.TimeLimit := e_Raw_Read_Word(P);
+  gGameSettings.MaxLives := e_Raw_Read_Byte(P);
+  gGameSettings.Options := e_Raw_Read_LongWord(P);
+end;
+
+// PLAYER
+
+function MC_RECV_PlayerCreate(P: Pointer): Word;
+var
+  PID, DID: Word;
+  PName, Model: string;
+  Color: TRGB;
+  T: Byte;
+  Pl: TPlayer;
+begin
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+
+  PName := e_Raw_Read_String(P);
+  Model := e_Raw_Read_String(P);
+  Color.R := e_Raw_Read_Byte(P);
+  Color.G := e_Raw_Read_Byte(P);
+  Color.B := e_Raw_Read_Byte(P);
+  T := e_Raw_Read_Byte(P);
+
+  Result := 0;
+  if (PID <> NetPlrUID1) and (PID <> NetPlrUID2) then
+  begin
+    if (Pl <> nil) then Exit;
+    DID := g_Player_Create(Model, Color, T, False);
+    with g_Player_Get(DID) do
+    begin
+      UID := PID;
+      Name := PName;
+      Reset(True);
+    end;
+  end
+  else
+  begin
+    if (PID = NetPlrUID1) and (gPlayer1 <> nil) then begin
+      gPlayer1.UID := PID;
+      gPlayer1.Model.SetColor(Color.R, Color.G, Color.B);
+      gPlayer1.ChangeTeam(T);
+    end;
+    if (PID = NetPlrUID2) and (gPlayer2 <> nil) then begin
+      gPlayer2.UID := PID;
+      gPlayer2.Model.SetColor(Color.R, Color.G, Color.B);
+      gPlayer2.ChangeTeam(T);
+    end;
+  end;
+
+  g_Console_Add(Format(_lc[I_PLAYER_JOIN], [PName]), True);
+  e_WriteLog('NET: Player ' + PName + ' [' + IntToStr(PID) + '] added.', MSG_NOTIFY);
+  Result := PID;
+end;
+
+function MC_RECV_PlayerPos(P: Pointer): Word;
+var
+  GT: LongWord;
+  PID: Word;
+  kByte: Word;
+  Pl: TPlayer;
+  Dir: Byte;
+  TmpX, TmpY: Integer;
+begin
+  Result := 0;
+
+  GT := e_Raw_Read_LongWord(P);
+  if GT < gTime - NET_MAX_DIFFTIME then
+  begin
+    gTime := GT;
+    Exit;
+  end;
+  gTime := GT;
+
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+
+  if Pl = nil then Exit;
+
+  Result := PID;
+
+  with Pl do
+  begin
+    FPing := e_Raw_Read_Word(P);
+    FLoss := e_Raw_Read_Byte(P);
+    kByte := e_Raw_Read_Word(P);
+    Dir := e_Raw_Read_Byte(P);
+
+    TmpX := e_Raw_Read_LongInt(P);
+    TmpY := e_Raw_Read_LongInt(P);
+
+    ReleaseKeys;
+
+    if (kByte = NET_KEY_CHAT) then
+      PressKey(KEY_CHAT, 10000)
+    else
+    begin
+      if LongBool(kByte and NET_KEY_LEFT) then PressKey(KEY_LEFT, 10000);
+      if LongBool(kByte and NET_KEY_RIGHT) then PressKey(KEY_RIGHT, 10000);
+      if LongBool(kByte and NET_KEY_UP) then PressKey(KEY_UP, 10000);
+      if LongBool(kByte and NET_KEY_DOWN) then PressKey(KEY_DOWN, 10000);
+      if LongBool(kByte and NET_KEY_JUMP) then PressKey(KEY_JUMP, 10000);
+    end;
+
+    if ((Pl <> gPlayer1) and (Pl <> gPlayer2)) or LongBool(kByte and NET_KEY_FORCEDIR) then
+      SetDirection(TDirection(Dir));
+
+    GameVelX := e_Raw_Read_LongInt(P);
+    GameVelY := e_Raw_Read_LongInt(P);
+    GameAccelX := e_Raw_Read_LongInt(P);
+    GameAccelY := e_Raw_Read_LongInt(P);
+    SetLerp(TmpX, TmpY);
+    if NetForcePlayerUpdate then Update();
+  end;
+end;
+
+function MC_RECV_PlayerStats(P: Pointer): Word;
+var
+  PID: Word;
+  Pl: TPlayer;
+  I: Integer;
+  OldJet: Boolean;
+  NewTeam: Byte;
+begin
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+  Result := 0;
+  if Pl = nil then
+    Exit;
+
+  with Pl do
+  begin
+    Live := (e_Raw_Read_Byte(P) <> 0);
+    GodMode := (e_Raw_Read_Byte(P) <> 0);
+    Health := e_Raw_Read_LongInt(P);
+    Armor := e_Raw_Read_LongInt(P);
+    Air := e_Raw_Read_LongInt(P);
+    JetFuel := e_Raw_Read_LongInt(P);
+    Lives := e_Raw_Read_Byte(P);
+    NewTeam := e_Raw_Read_Byte(P);
+
+    for I := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+      FWeapon[I] := (e_Raw_Read_Byte(P) <> 0);
+
+    for I := A_BULLETS to A_CELLS do
+      FAmmo[I] := e_Raw_Read_Word(P);
+
+    for I := A_BULLETS to A_CELLS do
+      FMaxAmmo[I] := e_Raw_Read_Word(P);
+
+    for I := MR_SUIT to MR_MAX do
+      FMegaRulez[I] := e_Raw_Read_LongWord(P);
+
+    FRulez := [];
+    if (e_Raw_Read_Byte(P) <> 0) then
+      FRulez := FRulez + [R_ITEM_BACKPACK];
+    if (e_Raw_Read_Byte(P) <> 0) then
+      FRulez := FRulez + [R_KEY_RED];
+    if (e_Raw_Read_Byte(P) <> 0) then
+      FRulez := FRulez + [R_KEY_GREEN];
+    if (e_Raw_Read_Byte(P) <> 0) then
+      FRulez := FRulez + [R_KEY_BLUE];
+    if (e_Raw_Read_Byte(P) <> 0) then
+      FRulez := FRulez + [R_BERSERK];
+
+    Frags := e_Raw_Read_LongInt(P);
+    Death := e_Raw_Read_LongInt(P);
+
+    SetWeapon(e_Raw_Read_Byte(P));
+
+    FSpectator := e_Raw_Read_Byte(P) <> 0;
+    if FSpectator then
+    begin
+      if Pl = gPlayer1 then
+      begin
+        gLMSPID1 := UID;
+        gPlayer1 := nil;
+      end;
+      if Pl = gPlayer2 then
+      begin
+        gLMSPID2 := UID;
+        gPlayer2 := nil;
+      end;
+    end
+    else
+    begin
+      if (gPlayer1 = nil) and (gLMSPID1 > 0) then
+        gPlayer1 := g_Player_Get(gLMSPID1);
+      if (gPlayer2 = nil) and (gLMSPID2 > 0) then
+        gPlayer2 := g_Player_Get(gLMSPID2);
+    end;
+    FGhost := e_Raw_Read_Byte(P) <> 0;
+    FPhysics := e_Raw_Read_Byte(P) <> 0;
+    FNoRespawn := e_Raw_Read_Byte(P) <> 0;
+    OldJet := FJetpack;
+    FJetpack := e_Raw_Read_Byte(P) <> 0;
+    if OldJet and not FJetpack then
+      JetpackOff
+    else if not OldJet and FJetpack then
+      JetpackOn;
+    if Team <> NewTeam then
+      Pl.ChangeTeam(NewTeam);
+  end;
+
+  Result := PID;
+end;
+
+function MC_RECV_PlayerDamage(P: Pointer): Word;
+var
+  PID: Word;
+  Pl: TPlayer;
+  Kind: Byte;
+  Attacker, Value: Word;
+  VX, VY: Integer;
+begin
+  Result := 0;
+  if not gGameOn then Exit;
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+  if Pl = nil then Exit;
+
+  Kind := e_Raw_Read_Byte(P);
+  Attacker := e_Raw_Read_Word(P);
+  Value := e_Raw_Read_Word(P);
+  VX := e_Raw_Read_Word(P);
+  VY := e_Raw_Read_Word(P);
+
+  with Pl do
+    Damage(Value, Attacker, VX, VY, Kind);
+
+  Result := PID;
+end;
+
+function MC_RECV_PlayerDeath(P: Pointer): Word;
+var
+  PID: Word;
+  Pl: TPlayer;
+  KillType, DeathType: Byte;
+  Attacker: Word;
+begin
+  Result := 0;
+  if not gGameOn then Exit;
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+  if Pl = nil then Exit;
+
+  KillType := e_Raw_Read_Byte(P);
+  DeathType := e_Raw_Read_Byte(P);
+  Attacker := e_Raw_Read_Word(P);
+
+  with Pl do
+  begin
+    Kill(KillType, Attacker, DeathType);
+    SoftReset;
+  end;
+end;
+
+function MC_RECV_PlayerDelete(P: Pointer): Word;
+var
+  PID: Word;
+  Pl: TPlayer;
+begin
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+  Result := 0;
+  if Pl = nil then Exit;
+
+  g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [Pl.Name]), True);
+  e_WriteLog('NET: Player ' + Pl.Name + ' [' + IntToStr(PID) + '] removed.', MSG_NOTIFY);
+
+  g_Player_Remove(PID);
+
+  Result := PID;
+end;
+
+function MC_RECV_PlayerFire(P: Pointer): Word;
+var
+  PID: Word;
+  Weap: Byte;
+  Pl: TPlayer;
+  X, Y, AX, AY: Integer;
+  SHID: Integer;
+begin
+  Result := 0;
+  if not gGameOn then Exit;
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+  if Pl = nil then Exit;
+
+  Weap := e_Raw_Read_Byte(P);
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  AX := e_Raw_Read_LongInt(P);
+  AY := e_Raw_Read_LongInt(P);
+  SHID := e_Raw_Read_LongInt(P);
+
+  with Pl do
+    if Live then NetFire(Weap, X, Y, AX, AY, SHID);
+end;
+
+procedure MC_RECV_PlayerSettings(P: Pointer);
+var
+  TmpName: string;
+  TmpModel: string;
+  TmpColor: TRGB;
+  TmpTeam: Byte;
+  Pl: TPlayer;
+  PID: Word;
+begin
+  PID := e_Raw_Read_Word(P);
+  Pl := g_Player_Get(PID);
+  if Pl = nil then Exit;
+
+  TmpName := e_Raw_Read_String(P);
+  TmpModel := e_Raw_Read_String(P);
+  TmpColor.R := e_Raw_Read_Byte(P);
+  TmpColor.G := e_Raw_Read_Byte(P);
+  TmpColor.B := e_Raw_Read_Byte(P);
+  TmpTeam := e_Raw_Read_Byte(P);
+
+  if (gGameSettings.GameMode in [GM_TDM, GM_CTF]) and (Pl.Team <> TmpTeam) then
+  begin
+    Pl.ChangeTeam(TmpTeam);
+    if gPlayer1 = Pl then
+      gPlayer1Settings.Team := TmpTeam;
+    if gPlayer2 = Pl then
+      gPlayer2Settings.Team := TmpTeam;
+  end else
+    Pl.SetColor(TmpColor);
+
+  if Pl.Name <> TmpName then
+  begin
+    g_Console_Add(Format(_lc[I_PLAYER_NAME], [Pl.Name, TmpName]), True);
+    Pl.Name := TmpName;
+  end;
+
+  if TmpModel <> Pl.Model.Name then
+    Pl.SetModel(TmpModel);
+end;
+
+// ITEM
+
+procedure MC_RECV_ItemSpawn(P: Pointer);
+var
+  ID: Word;
+  AID: DWord;
+  X, Y, VX, VY: Integer;
+  T: Byte;
+  Quiet, Fall{, Resp}: Boolean;
+  Anim: TAnimation;
+begin
+  if not gGameOn then Exit;
+  ID := e_Raw_Read_Word(P);
+  Quiet := e_Raw_Read_Byte(P) <> 0;
+  T := e_Raw_Read_Byte(P);
+  Fall := e_Raw_Read_Byte(P) <> 0;
+  {Resp :=} e_Raw_Read_Byte(P);
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  VX := e_Raw_Read_LongInt(P);
+  VY := e_Raw_Read_LongInt(P);
+
+  g_Items_Create(X, Y, T, Fall, False, False, ID);
+  gItems[ID].Obj.Vel.X := VX;
+  gItems[ID].Obj.Vel.Y := VY;
+
+  if not Quiet then
+  begin
+    g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', X, Y);
+    if g_Frames_Get(AID, 'FRAMES_ITEM_RESPAWN') then
+    begin
+      Anim := TAnimation.Create(AID, False, 4);
+      g_GFX_OnceAnim(X+(gItems[ID].Obj.Rect.Width div 2)-16, Y+(gItems[ID].Obj.Rect.Height div 2)-16, Anim);
+      Anim.Free();
+    end;
+  end;
+end;
+
+procedure MC_RECV_ItemDestroy(P: Pointer);
+var
+  ID: Word;
+  Quiet: Boolean;
+begin
+  if not gGameOn then Exit;
+  ID := e_Raw_Read_Word(P);
+  Quiet := e_Raw_Read_Byte(P) <> 0;
+  if gItems = nil then Exit;
+  if (ID > High(gItems)) then Exit;
+
+  if not Quiet then
+    if gSoundEffectsDF then
+    begin
+      if gItems[ID].ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL,
+                                 ITEM_INVIS, ITEM_MEDKIT_BLACK, ITEM_JETPACK] then
+        g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ',
+          gItems[ID].Obj.X, gItems[ID].Obj.Y)
+        else
+          if gItems[ID].ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
+                                     ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
+                                     ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERPULEMET, ITEM_AMMO_BACKPACK] then
+            g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON',
+              gItems[ID].Obj.X, gItems[ID].Obj.Y)
+            else
+              g_Sound_PlayExAt('SOUND_ITEM_GETITEM',
+                gItems[ID].Obj.X, gItems[ID].Obj.Y);
+    end
+    else
+    begin
+      if gItems[ID].ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_SUIT,
+                                 ITEM_MEDKIT_BLACK, ITEM_INVUL, ITEM_INVIS, ITEM_JETPACK] then
+        g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ',
+          gItems[ID].Obj.X, gItems[ID].Obj.Y)
+      else
+        if gItems[ID].ItemType in [ITEM_WEAPON_SAW, ITEM_WEAPON_PISTOL, ITEM_WEAPON_SHOTGUN1, ITEM_WEAPON_SHOTGUN2,
+                                   ITEM_WEAPON_CHAINGUN, ITEM_WEAPON_ROCKETLAUNCHER, ITEM_WEAPON_PLASMA,
+                                   ITEM_WEAPON_BFG, ITEM_WEAPON_SUPERPULEMET] then
+          g_Sound_PlayExAt('SOUND_ITEM_GETWEAPON',
+            gItems[ID].Obj.X, gItems[ID].Obj.Y)
+        else
+          g_Sound_PlayExAt('SOUND_ITEM_GETITEM',
+            gItems[ID].Obj.X, gItems[ID].Obj.Y);
+    end;
+
+  g_Items_Remove(ID);
+end;
+
+// PANEL
+
+procedure MC_RECV_PanelTexture(P: Pointer);
+var
+  TP: TPanel;
+  PType: Word;
+  ID: LongWord;
+  Tex, Fr: Integer;
+  Loop, Cnt: Byte;
+begin
+  if not gGameOn then Exit;
+  PType := e_Raw_Read_Word(P);
+  ID := e_Raw_Read_LongWord(P);
+  Tex := e_Raw_Read_LongInt(P);
+  Fr := e_Raw_Read_LongInt(P);
+  Cnt := e_Raw_Read_Byte(P);
+  Loop := e_Raw_Read_Byte(P);
+
+  TP := nil;
+
+  case PType of
+    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+     if gWalls <> nil then
+      TP := gWalls[ID];
+    PANEL_FORE:
+     if gRenderForegrounds <> nil then
+      TP := gRenderForegrounds[ID];
+    PANEL_BACK:
+     if gRenderBackgrounds <> nil then
+      TP := gRenderBackgrounds[ID];
+    PANEL_WATER:
+     if gWater <> nil then
+      TP := gWater[ID];
+    PANEL_ACID1:
+     if gAcid1 <> nil then
+      TP := gAcid1[ID];
+    PANEL_ACID2:
+     if gAcid2 <> nil then
+      TP := gAcid2[ID];
+    PANEL_STEP:
+     if gSteps <> nil then
+      TP := gSteps[ID];
+    else
+      Exit;
+  end;
+
+  if TP <> nil then
+    if Loop = 0 then
+    begin    // switch texture
+      TP.SetTexture(Tex, Loop);
+      TP.SetFrame(Fr, Cnt);
+    end else // looped or non-looped animation
+      TP.NextTexture(Loop);
+end;
+
+procedure MC_RECV_PanelState(P: Pointer);
+var
+  ID: LongWord;
+  E: Boolean;
+  Lift: Byte;
+  PType: Word;
+begin
+  if not gGameOn then Exit;
+  PType := e_Raw_Read_Word(P);
+  ID := e_Raw_Read_LongWord(P);
+  E := (e_Raw_Read_Byte(P) <> 0);
+  Lift := e_Raw_Read_Byte(P);
+
+  case PType of
+    PANEL_WALL, PANEL_OPENDOOR, PANEL_CLOSEDOOR:
+      if E then
+        g_Map_EnableWall(ID)
+      else
+        g_Map_DisableWall(ID);
+
+    PANEL_LIFTUP, PANEL_LIFTDOWN, PANEL_LIFTLEFT, PANEL_LIFTRIGHT:
+      g_Map_SetLift(ID, Lift);
+  end;
+end;
+
+// TRIGGERS
+
+procedure MC_RECV_TriggerSound(P: Pointer);
+var
+  SPlaying: Boolean;
+  SPos, SID: LongWord;
+  SCount: LongInt;
+  I: Integer;
+begin
+  if not gGameOn then Exit;
+  if gTriggers = nil then Exit;
+
+  SID := e_Raw_Read_LongWord(P);
+  SPlaying := e_Raw_Read_Byte(P) <> 0;
+  SPos := e_Raw_Read_LongWord(P);
+  SCount := e_Raw_Read_LongInt(P);
+
+  for I := Low(gTriggers) to High(gTriggers) do
+    if gTriggers[I].TriggerType = TRIGGER_SOUND then
+      if gTriggers[I].ClientID = SID then
+        with gTriggers[I] do
+        begin
+          if SPlaying then
+          begin
+            if Data.Local then
+              Sound.PlayVolumeAt(X+(Width div 2), Y+(Height div 2), Data.Volume/255.0)
+            else
+              Sound.PlayPanVolume((Data.Pan-127.0)/128.0, Data.Volume/255.0);
+            Sound.SetPosition(SPos);
+          end
+          else
+            if Sound.IsPlaying then Sound.Stop;
+
+          SoundPlayCount := SCount;
+        end;
+end;
+
+procedure MC_RECV_TriggerMusic(P: Pointer);
+var
+  MName: string;
+  MPlaying: Boolean;
+  MPos: LongWord;
+  MPaused: Boolean;
+begin
+  if not gGameOn then Exit;
+
+  MName := e_Raw_Read_String(P);
+  MPlaying := e_Raw_Read_Byte(P) <> 0;
+  MPos := e_Raw_Read_LongWord(P);
+  MPaused := e_Raw_Read_Byte(P) <> 0;
+
+  if MPlaying then
+  begin
+    gMusic.SetByName(MName);
+    gMusic.Play(True);
+    gMusic.SetPosition(MPos);
+    gMusic.SpecPause := MPaused;
+  end
+  else
+    if gMusic.IsPlaying then gMusic.Stop;
+end;
+
+// MONSTERS
+
+procedure MC_RECV_MonsterSpawn(P: Pointer);
+var
+  ID: Word;
+  MType, MState, MDir, MAnim, MBehav: Byte;
+  X, Y, VX, VY, MTargTime, MHealth, MAmmo, MSleep: Integer;
+  MTarg: Word;
+  M: TMonster;
+begin
+  ID := e_Raw_Read_Word(P);
+  M := g_Monsters_Get(ID);
+  if M <> nil then
+    Exit;
+
+  MType := e_Raw_Read_Byte(P);
+  MState := e_Raw_Read_Byte(P);
+  MAnim := e_Raw_Read_Byte(P);
+  MTarg := e_Raw_Read_Word(P);
+  MTargTime := e_Raw_Read_LongInt(P);
+  MBehav := e_Raw_Read_Byte(P);
+  MSleep := e_Raw_Read_LongInt(P);
+  MHealth := e_Raw_Read_LongInt(P);
+  MAmmo := e_Raw_Read_LongInt(P);
+
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  VX := e_Raw_Read_LongInt(P);
+  VY := e_Raw_Read_LongInt(P);
+  MDir := e_Raw_Read_Byte(P);
+
+  g_Monsters_Create(MType, X, Y, TDirection(MDir), False, ID);
+  M := g_Monsters_Get(ID);
+  if M = nil then
+    Exit;
+
+  with M do
+  begin
+    GameX := X;
+    GameY := Y;
+    GameVelX := VX;
+    GameVelY := VY;
+
+    MonsterAnim := MAnim;
+    MonsterTargetUID := MTarg;
+    MonsterTargetTime := MTargTime;
+    MonsterBehaviour := MBehav;
+    MonsterSleep := MSleep;
+    MonsterAmmo := MAmmo;
+    SetHealth(MHealth);
+
+    SetState(MState);
+  end;
+end;
+
+procedure MC_RECV_MonsterPos(P: Pointer);
+var
+  M: TMonster;
+  ID: Word;
+begin
+  ID := e_Raw_Read_Word(P);
+  M := g_Monsters_Get(ID);
+  if M = nil then
+    Exit;
+
+  with M do
+  begin
+    GameX := e_Raw_Read_LongInt(P);
+    GameY := e_Raw_Read_LongInt(P);
+    GameVelX := e_Raw_Read_LongInt(P);
+    GameVelY := e_Raw_Read_LongInt(P);
+    GameDirection := TDirection(e_Raw_Read_Byte(P));
+  end;
+end;
+
+procedure MC_RECV_MonsterState(P: Pointer);
+var
+  ID: Integer;
+  MState, MFAnm: Byte;
+  M: TMonster;
+  AnimRevert: Boolean;
+begin
+  ID := e_Raw_Read_Word(P);
+  M := g_Monsters_Get(ID);
+  if M = nil then Exit;
+
+  MState := e_Raw_Read_Byte(P);
+  MFAnm := e_Raw_Read_Byte(P);
+
+  with M do
+  begin
+    MonsterTargetUID := e_Raw_Read_Word(P);
+    MonsterTargetTime := e_Raw_Read_LongInt(P);
+    MonsterSleep := e_Raw_Read_LongInt(P);
+    MonsterHealth := e_Raw_Read_LongInt(P);
+    MonsterAmmo := e_Raw_Read_LongInt(P);
+    MonsterPain := e_Raw_Read_LongInt(P);
+    AnimRevert := e_Raw_Read_Byte(P) <> 0;
+    RevertAnim(AnimRevert);
+
+    if MonsterState <> MState then
+    begin
+      if (MState = MONSTATE_GO) and (MonsterState = MONSTATE_SLEEP) then
+        WakeUpSound;
+      if (MState = MONSTATE_DIE) then
+        DieSound;
+      if (MState = MONSTATE_PAIN) then
+        MakeBloodSimple(Min(200, MonsterPain));
+      if (MState = MONSTATE_ATTACK) then
+        kick(nil);
+      if (MState = MONSTATE_DEAD) then
+        SetDeadAnim;
+
+      SetState(MState, MFAnm);
+    end;
+  end;
+end;
+
+procedure MC_RECV_MonsterShot(P: Pointer);
+var
+  ID: Integer;
+  M: TMonster;
+  X, Y, VX, VY: Integer;
+begin
+  ID := e_Raw_Read_Word(P);
+
+  M := g_Monsters_Get(ID);
+  if M = nil then Exit;
+
+  X := e_Raw_Read_LongInt(P);
+  Y := e_Raw_Read_LongInt(P);
+  VX := e_Raw_Read_LongInt(P);
+  VY := e_Raw_Read_LongInt(P);
+
+  M.ClientAttack(X, Y, VX, VY);
+end;
+
+procedure MC_RECV_MonsterDelete(P: Pointer);
+var
+  ID: Integer;
+  M: TMonster;
+begin
+  ID := e_Raw_Read_Word(P);
+  M := g_Monsters_Get(ID);
+  if M = nil then Exit;
+
+  gMonsters[ID].SetState(5);
+  gMonsters[ID].MonsterRemoved := True;
+end;
+
+procedure MC_RECV_TimeSync(P: Pointer);
+var
+  Time: LongWord;
+begin
+  Time := e_Raw_Read_LongWord(P);
+
+  if gState = STATE_INTERCUSTOM then
+    gServInterTime := Min(Time, 255);
+end;
+
+procedure MC_RECV_VoteEvent(P: Pointer);
+var
+  EvID: Byte;
+  Str1, Str2: string;
+  Int1, Int2: SmallInt;
+begin
+  EvID := e_Raw_Read_Byte(P);
+  Int1 := e_Raw_Read_SmallInt(P);
+  Int2 := e_Raw_Read_SmallInt(P);
+  Str1 := e_Raw_Read_String(P);
+  Str2 := e_Raw_Read_String(P);
+
+  case EvID of
+    NET_VE_STARTED:
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_STARTED], [Str1, Str2, Int1]), True);
+    NET_VE_PASSED:
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_PASSED], [Str1]), True);
+    NET_VE_FAILED:
+      g_Console_Add(_lc[I_MESSAGE_VOTE_FAILED], True);
+    NET_VE_VOTE:
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_VOTE], [Str1, Int1, Int2]), True);
+    NET_VE_INPROGRESS:
+      g_Console_Add(Format(_lc[I_MESSAGE_VOTE_INPROGRESS], [Str1]), True);
+  end;
+end;
+
+// CLIENT SEND
+
+procedure MC_SEND_Info(Password: string);
+begin
+  e_Buffer_Clear(@NetOut);
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_INFO));
+  e_Buffer_Write(@NetOut, GAME_VERSION);
+  e_Buffer_Write(@NetOut, Password);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Name);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Model);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Color.R);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Color.G);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Color.B);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Team);
+
+  g_Net_Client_Send(True, NET_CHAN_SERVICE);
+end;
+
+procedure MC_SEND_Chat(Txt: string; Mode: Byte);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_CHAT));
+  e_Buffer_Write(@NetOut, Txt);
+  e_Buffer_Write(@NetOut, Mode);
+
+  g_Net_Client_Send(True, NET_CHAN_CHAT);
+end;
+
+procedure MC_SEND_PlayerPos();
+var
+  kByte: Word;
+  Predict: Boolean;
+begin
+  if not gGameOn then Exit;
+  if gPlayers = nil then Exit;
+  if gPlayer1 = nil then Exit;
+
+  kByte := 0;
+  Predict := NetPredictSelf; // and (not NetGotKeys);
+
+  if (not gConsoleShow) and (not gChatShow) and (g_ActiveWindow = nil) then
+    with gGameControls.P1Control do
+    begin
+      if e_KeyPressed(KeyLeft) and (not e_KeyPressed(KeyRight)) then
+        P1MoveButton := 1
+      else
+        if (not e_KeyPressed(KeyLeft)) and e_KeyPressed(KeyRight) then
+          P1MoveButton := 2
+        else
+          if (not e_KeyPressed(KeyLeft)) and (not e_KeyPressed(KeyRight)) then
+            P1MoveButton := 0;
+
+      if (P1MoveButton = 2) and e_KeyPressed(KeyLeft) then
+        gPlayer1.SetDirection(D_LEFT)
+      else
+       if (P1MoveButton = 1) and e_KeyPressed(KeyRight) then
+          gPlayer1.SetDirection(D_RIGHT)
+        else
+          if P1MoveButton <> 0 then
+            gPlayer1.SetDirection(TDirection(P1MoveButton-1));
+
+      gPlayer1.ReleaseKeys;
+      if P1MoveButton = 1 then
+      begin
+        kByte := kByte or NET_KEY_LEFT;
+        if Predict then gPlayer1.PressKey(KEY_LEFT, 10000);
+      end;
+      if P1MoveButton = 2 then
+      begin
+        kByte := kByte or NET_KEY_RIGHT;
+        if Predict then gPlayer1.PressKey(KEY_RIGHT, 10000);
+      end;
+      if e_KeyPressed(KeyUp) then
+      begin
+        kByte := kByte or NET_KEY_UP;
+        gPlayer1.PressKey(KEY_UP, 10000);
+      end;
+      if e_KeyPressed(KeyDown) then
+      begin
+        kByte := kByte or NET_KEY_DOWN;
+        gPlayer1.PressKey(KEY_DOWN, 10000);
+      end;
+      if e_KeyPressed(KeyJump) then
+      begin
+        kByte := kByte or NET_KEY_JUMP;
+        // gPlayer1.PressKey(KEY_JUMP, 10000); // TODO: Make a prediction option
+      end;
+      if e_KeyPressed(KeyFire) then kByte := kByte or NET_KEY_FIRE;
+      if e_KeyPressed(KeyOpen) then kByte := kByte or NET_KEY_OPEN;
+      if e_KeyPressed(KeyNextWeapon) then kByte := kByte or NET_KEY_NW;
+      if e_KeyPressed(KeyPrevWeapon) then kByte := kByte or NET_KEY_PW;
+    end
+  else
+    kByte := NET_KEY_CHAT;
+
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRPOS));
+  e_Buffer_Write(@NetOut, gTime);
+  e_Buffer_Write(@NetOut, kByte);
+  e_Buffer_Write(@NetOut, Byte(gPlayer1.Direction));
+  g_Net_Client_Send(True, NET_CHAN_PLAYERPOS);
+
+  //kBytePrev := kByte;
+  //kDirPrev := gPlayer1.Direction;
+end;
+
+procedure MC_SEND_Vote(Start: Boolean = False; Command: string = 'a');
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_VOTE_EVENT));
+  e_Buffer_Write(@NetOut, Byte(Start));
+  e_Buffer_Write(@NetOut, Command);
+  g_Net_Client_Send(True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MC_SEND_PlayerSettings();
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_PLRSET));
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Name);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Model);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Color.R);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Color.G);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Color.B);
+  e_Buffer_Write(@NetOut, gPlayer1Settings.Team);
+
+  g_Net_Client_Send(True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MC_SEND_FullStateRequest();
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_REQFST));
+
+  g_Net_Client_Send(True, NET_CHAN_SERVICE);
+end;
+
+procedure MC_SEND_CheatRequest(Kind: Byte);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_CHEAT));
+  e_Buffer_Write(@NetOut, Kind);
+
+  g_Net_Client_Send(True, NET_CHAN_IMPORTANT);
+end;
+procedure MC_SEND_RCONPassword(Password: string);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_RCON_AUTH));
+  e_Buffer_Write(@NetOut, Password);
+
+  g_Net_Client_Send(True, NET_CHAN_SERVICE);
+end;
+procedure MC_SEND_RCONCommand(Cmd: string);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_RCON_CMD));
+  e_Buffer_Write(@NetOut, Cmd);
+
+  g_Net_Client_Send(True, NET_CHAN_SERVICE);
+end;
+
+// i have no idea why all this stuff is in here
+
+function ReadFile(const FileName: TFileName): AByte;
+var
+  FileStream : TFileStream;
+begin
+  Result := nil;
+  FileStream:= TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
+  try
+    if FileStream.Size>0 then
+    begin
+      SetLength(Result, FileStream.Size);
+      FileStream.Read(Result[0], FileStream.Size);
+    end;
+  finally
+    FileStream.Free;
+  end;
+end;
+
+function CreateMapDataMsg(const FileName: TFileName; ResList: TStringList): TMapDataMsg;
+var
+  i: Integer;
+begin
+  Result.MsgId := NET_MSG_MAP_RESPONSE;
+  Result.FileData := ReadFile(FileName);
+  Result.FileSize := Length(Result.FileData);
+
+  SetLength(Result.ExternalResources, ResList.Count);
+  for i:=0 to ResList.Count-1 do
+  begin
+    Result.ExternalResources[i].Name := ResList.Strings[i];
+    Result.ExternalResources[i].md5 := MD5File(GameDir+'/wads/'+ResList.Strings[i]);
+  end;
+end;
+
+procedure ResDataMsgToBytes(var bytes: AByte; const ResData: TResDataMsg);
+var
+  ResultStream: TMemoryStream;
+begin
+  ResultStream := TMemoryStream.Create;
+
+  ResultStream.WriteBuffer(ResData.MsgId, SizeOf(ResData.MsgId)); //msgId
+  ResultStream.WriteBuffer(ResData.FileSize, SizeOf(ResData.FileSize));  //file size
+  ResultStream.WriteBuffer(ResData.FileData[0], ResData.FileSize);       //file data
+
+  SetLength(bytes, ResultStream.Size);
+  ResultStream.Seek(0, soFromBeginning);
+  ResultStream.ReadBuffer(bytes[0], ResultStream.Size);
+
+  ResultStream.Free;
+end;
+
+function ResDataFromMsgStream(msgStream: TMemoryStream):TResDataMsg;
+begin
+  msgStream.ReadBuffer(Result.MsgId, SizeOf(Result.MsgId));
+  msgStream.ReadBuffer(Result.FileSize, SizeOf(Result.FileSize));
+  SetLength(Result.FileData, Result.FileSize);
+  msgStream.ReadBuffer(Result.FileData[0], Result.FileSize);
+end;
+
+procedure MapDataMsgToBytes(var bytes: AByte; const MapDataMsg: TMapDataMsg);
+var
+  ResultStream: TMemoryStream;
+  resCount: Integer;
+begin
+  resCount := Length(MapDataMsg.ExternalResources);
+
+  ResultStream := TMemoryStream.Create;
+
+  ResultStream.WriteBuffer(MapDataMsg.MsgId, SizeOf(MapDataMsg.MsgId)); //msgId
+  ResultStream.WriteBuffer(MapDataMsg.FileSize, SizeOf(MapDataMsg.FileSize));  //file size
+  ResultStream.WriteBuffer(MapDataMsg.FileData[0], MapDataMsg.FileSize);       //file data
+
+  ResultStream.WriteBuffer(resCount, SizeOf(resCount));   //res count
+  ResultStream.WriteBuffer(MapDataMsg.ExternalResources[0], resCount*SizeOf(TExternalResourceInfo)); //res data
+
+  SetLength(bytes, ResultStream.Size);
+  ResultStream.Seek(0, soFromBeginning);
+  ResultStream.ReadBuffer(bytes[0], ResultStream.Size);
+
+  ResultStream.Free;
+end;
+
+function MapDataFromMsgStream(msgStream: TMemoryStream):TMapDataMsg;
+var
+  resCount: Integer;
+begin
+  msgStream.ReadBuffer(Result.MsgId, SizeOf(Result.MsgId));
+  msgStream.ReadBuffer(Result.FileSize, SizeOf(Result.FileSize));   //file size
+
+  SetLength(Result.FileData, Result.FileSize);
+  msgStream.ReadBuffer(Result.FileData[0], Result.FileSize);  //file data
+
+  msgStream.ReadBuffer(resCount, SizeOf(resCount));  //res count
+  SetLength(Result.ExternalResources, resCount);
+
+  msgStream.ReadBuffer(Result.ExternalResources[0], resCount * SizeOf(TExternalResourceInfo)); //res data
+end;
+
+function IsValidFileName(const S: String): Boolean;
+const
+  Forbidden: set of Char = ['<', '>', '|', '"', ':', '*', '?'];
+var
+  I: Integer;
+begin
+  Result := S <> '';
+  for I := 1 to Length(S) do
+    Result := Result and (not(S[I] in Forbidden));
+end;
+
+function IsValidFilePath(const S: String): Boolean;
+var
+  I: Integer;
+begin
+  Result := False;
+  if not IsValidFileName(S) then exit;
+  if FileExists(S) then exit;
+  I := LastDelimiter('\/', S);
+  if (I > 0) then
+    if (not DirectoryExists(Copy(S, 1, I-1))) then
+      exit;
+  Result := True;
+end;
+
+procedure MC_SEND_MapRequest();
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_MAP_REQUEST));
+  g_Net_Client_Send(True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MC_SEND_ResRequest(const resName: AnsiString);
+begin
+  e_Buffer_Write(@NetOut, Byte(NET_MSG_RES_REQUEST));
+  e_Buffer_Write(@NetOut, resName);
+  g_Net_Client_Send(True, NET_CHAN_IMPORTANT);
+end;
+
+procedure MH_RECV_MapRequest(C: pTNetClient; P: Pointer);
+var
+  payload: AByte;
+  peer: pENetPeer;
+  mapDataMsg: TMapDataMsg;
+begin
+  e_WriteLog('NET: Received map request from ' +
+             DecodeIPV4(C.Peer.address.host), MSG_NOTIFY);
+
+  mapDataMsg := CreateMapDataMsg(MapsDir + gGameSettings.WAD, gExternalResources);
+  peer := NetClients[C.ID].Peer;
+
+  MapDataMsgToBytes(payload, mapDataMsg);
+  g_Net_SendData(payload, peer, True, NET_CHAN_DOWNLOAD);
+
+  payload := nil;
+  mapDataMsg.FileData := nil;
+  mapDataMsg.ExternalResources := nil;
+end;
+
+procedure MH_RECV_ResRequest(C: pTNetClient; P: Pointer);
+var
+  payload: AByte;
+  peer: pENetPeer;
+  FileName: String;
+  resDataMsg: TResDataMsg;
+begin
+  FileName := ExtractFileName(e_Raw_Read_String(P));
+  e_WriteLog('NET: Received res request: ' + FileName +
+             ' from ' + DecodeIPV4(C.Peer.address.host), MSG_NOTIFY);
+
+  if not IsValidFilePath(FileName) then
+  begin
+    e_WriteLog('Invalid filename: ' + FileName, MSG_WARNING);
+    exit;
+  end;
+
+  peer := NetClients[C.ID].Peer;
+
+  if gExternalResources.IndexOf(FileName) > -1 then
+  begin
+    resDataMsg.MsgId := NET_MSG_RES_RESPONSE;
+    resDataMsg.FileData := ReadFile(GameDir+'/wads/'+FileName);
+    resDataMsg.FileSize := Length(resDataMsg.FileData);
+
+    ResDataMsgToBytes(payload, resDataMsg);
+    g_Net_SendData(payload, peer, True, NET_CHAN_DOWNLOAD);
+  end;
+end;
+
+end.
diff --git a/src/game/g_options.pas b/src/game/g_options.pas
new file mode 100644 (file)
index 0000000..574a28b
--- /dev/null
@@ -0,0 +1,701 @@
+unit g_options;
+
+interface
+
+uses
+  g_language;
+
+type
+  TPlayerControl = record
+    KeyRight:      Word;
+    KeyLeft:       Word;
+    KeyUp:         Word;
+    KeyDown:       Word;
+    KeyFire:       Word;
+    KeyJump:       Word;
+    KeyNextWeapon: Word;
+    KeyPrevWeapon: Word;
+    KeyOpen:       Word;
+  end;
+
+  TGameControls = record
+    TakeScreenshot: Word;
+    Stat:           Word;
+    Chat:           Word;
+    TeamChat:       Word;
+  end;
+
+  TControls = record
+    GameControls: TGameControls;
+    P1Control:    TPlayerControl;
+    P2Control:    TPlayerControl;
+  end;
+
+procedure g_Options_SetDefault();
+procedure g_Options_Read(FileName: String);
+procedure g_Options_Write(FileName: String);
+procedure g_Options_Write_Language(FileName: String);
+procedure g_Options_Write_Video(FileName: String);
+procedure g_Options_Write_Gameplay_Custom(FileName: String);
+procedure g_Options_Write_Gameplay_Net(FileName: String);
+procedure g_Options_Write_Net_Server(FileName: String);
+procedure g_Options_Write_Net_Client(FileName: String);
+
+var
+  gGameControls: TControls;
+  gScreenWidth: Word          = 800;
+  gScreenHeight: Word         = 600;
+  gWinRealPosX: Integer       = 0;
+  gWinRealPosY: Integer       = 0;
+  gBPP: Byte                  = 32;
+  gFreq: Byte                 = 0;
+  gFullscreen: Boolean        = False;
+  gWinMaximized: Boolean      = False;
+  gVSync: Boolean             = False;
+  gTextureFilter: Boolean     = True;
+  gNoSound: Boolean           = False;
+  gSoundLevel: Byte           = 75;
+  gMusicLevel: Byte           = 65;
+  gMaxSimSounds: Byte         = 8;
+  gMuteWhenInactive: Boolean  = False;
+  gAdvCorpses: Boolean        = True;
+  gAdvBlood: Boolean          = True;
+  gAdvGibs: Boolean           = True;
+  gGibsCount: Integer         = 32;
+  gBloodCount: Byte           = 3;
+  gFlash: Byte                = 1;
+  gDrawBackGround: Boolean    = True;
+  gShowMessages: Boolean      = True;
+  gRevertPlayers: Boolean     = False;
+  gLanguage: String           = LANGUAGE_ENGLISH;
+  gAskLanguage: Boolean       = True;
+  gcMap: String               = '';
+  gcGameMode: String          = '';
+  gcTimeLimit: Word           = 0;
+  gcGoalLimit: Word           = 0;
+  gcMaxLives: Byte            = 0;
+  gcPlayers: Byte             = 1;
+  gcTeamDamage: Boolean       = False;
+  gcAllowExit: Boolean        = True;
+  gcWeaponStay: Boolean       = False;
+  gcMonsters: Boolean         = False;
+  gcBotsVS: String            = 'Everybody';
+  gnMap: String               = '';
+  gnGameMode: String          = '';
+  gnTimeLimit: Word           = 0;
+  gnGoalLimit: Word           = 0;
+  gnMaxLives: Byte            = 0;
+  gnPlayers: Byte             = 1;
+  gnTeamDamage: Boolean       = False;
+  gnAllowExit: Boolean        = True;
+  gnWeaponStay: Boolean       = False;
+  gnMonsters: Boolean         = False;
+  gnBotsVS: String            = 'Everybody';
+
+implementation
+
+uses
+  e_log, e_input, g_window, g_sound, g_gfx, g_player, Math,
+  g_map, g_net, g_netmaster, SysUtils, CONFIG, g_game, g_main, e_textures,
+  g_items, GL, GLExt;
+
+procedure g_Options_SetDefault();
+var
+  i: Integer;
+begin
+  g_Sound_SetupAllVolumes(75, 65);
+  gMaxSimSounds := 8;
+  gMuteWhenInactive := False;
+  gAnnouncer := ANNOUNCE_MEPLUS;
+  gSoundEffectsDF := True;
+  g_GFX_SetMax(2000);
+  g_Gibs_SetMax(150);
+  g_Corpses_SetMax(20);
+  g_Shells_SetMax(300);
+  gGibsCount := 32;
+  gBloodCount := 3;
+  gAdvBlood := True;
+  gAdvCorpses := True;
+  gAdvGibs := True;
+  gFlash := 1;
+  gDrawBackGround := True;
+  gShowMessages := True;
+  gRevertPlayers := False;
+  
+  for i := 0 to e_MaxJoys-1 do
+    e_JoystickDeadzones[i] := 8192;
+
+  with gGameControls.GameControls do
+  begin
+    TakeScreenshot := 183;
+    Stat := 15;
+    Chat := 20; // [T]
+    TeamChat := 21; // [Y]
+  end;
+
+  with gGameControls.P1Control do
+  begin
+    KeyRight := 77;
+    KeyLeft := 75;
+    KeyUp := 72;
+    KeyDown := 76;
+    KeyFire := 184;
+    KeyJump := 157;
+    KeyNextWeapon := 73;
+    KeyPrevWeapon := 71;
+    KeyOpen := 54;
+  end;
+
+  with gGameControls.P2Control do
+  begin
+    KeyRight := 33;
+    KeyLeft := 31;
+    KeyUp := 18;
+    KeyDown := 32;
+    KeyFire := 30;
+    KeyJump := 16;
+    KeyNextWeapon := 19;
+    KeyPrevWeapon := 17;
+    KeyOpen := 58;
+  end;
+
+  with gPlayer1Settings do
+  begin
+    Name := 'Player1';
+    Model := STD_PLAYER_MODEL;
+    Color.R := PLAYER1_DEF_COLOR.R;
+    Color.G := PLAYER1_DEF_COLOR.G;
+    Color.B := PLAYER1_DEF_COLOR.B;
+    Team := TEAM_RED;
+  end;
+
+  with gPlayer2Settings do
+  begin
+    Name := 'Player2';
+    Model := STD_PLAYER_MODEL;
+    Color.R := PLAYER2_DEF_COLOR.R;
+    Color.G := PLAYER2_DEF_COLOR.G;
+    Color.B := PLAYER2_DEF_COLOR.B;
+    Team := TEAM_BLUE;
+  end;
+
+  NetUseMaster := True;
+  g_Net_Slist_Set('mpms.doom2d.org', 25665);
+end;
+
+procedure g_Options_Read(FileName: String);
+var
+  config: TConfig;
+  str: String;
+  i: Integer;
+begin
+  gAskLanguage := True;
+  e_WriteLog('Reading config', MSG_NOTIFY);
+
+  if not FileExists(FileName) then
+  begin
+    e_WriteLog('Config file '+FileName+' not found', MSG_WARNING);
+    g_Options_SetDefault();
+
+  // Default video options:
+    gScreenWidth := 800;
+    gScreenHeight := 600;
+    gWinRealPosX := 0;
+    gWinRealPosY := 0;
+    gWinMaximized := False;
+    gFullScreen := False;
+    gBPP := 32;
+    gVSync := False;
+    gTextureFilter := True;
+    fUseMipmaps := False;
+
+    Exit;
+  end;
+
+  config := TConfig.CreateFile(FileName);
+
+  gScreenWidth := config.ReadInt('Video', 'ScreenWidth', 800);
+  if gScreenWidth < 640 then
+    gScreenWidth := 640;
+  gScreenHeight := config.ReadInt('Video', 'ScreenHeight', 600);
+  if gScreenHeight < 480 then
+    gScreenHeight := 480;
+  gWinRealPosX := config.ReadInt('Video', 'WinPosX', 0);
+  if gWinRealPosX < 0 then
+    gWinRealPosX := 0;
+  gWinRealPosY := config.ReadInt('Video', 'WinPosY', 0);
+  if gWinRealPosY < 0 then
+    gWinRealPosY := 0;
+  gFullScreen := config.ReadBool('Video', 'Fullscreen', False);
+  gWinMaximized := config.ReadBool('Video', 'Maximized', False);
+  gBPP := config.ReadInt('Video', 'BPP', 32);
+  gFreq := config.ReadInt('Video', 'Freq', 0);
+  gVSync := config.ReadBool('Video', 'VSync', True);
+  gTextureFilter := config.ReadBool('Video', 'TextureFilter', True);
+  fUseMipmaps := config.ReadBool('Video', 'LegacyCompatible', False);
+
+  gNoSound := config.ReadBool('Sound', 'NoSound', False);
+  gSoundLevel := Min(config.ReadInt('Sound', 'SoundLevel', 75), 255);
+  gMusicLevel := Min(config.ReadInt('Sound', 'MusicLevel', 65), 255);
+  gMaxSimSounds := Max(Min(config.ReadInt('Sound', 'MaxSimSounds', 8), 66), 2);
+  gMuteWhenInactive := config.ReadBool('Sound', 'MuteInactive', False);
+  gAnnouncer := Min(Max(config.ReadInt('Sound', 'Announcer', ANNOUNCE_MEPLUS), ANNOUNCE_NONE), ANNOUNCE_ALL);
+  gSoundEffectsDF := config.ReadBool('Sound', 'SoundEffectsDF', True);
+
+  with gGameControls.GameControls do
+  begin
+    TakeScreenshot := config.ReadInt('GameControls', 'TakeScreenshot', 183);
+    Stat := config.ReadInt('GameControls', 'Stat', 15);
+    Chat := config.ReadInt('GameControls', 'Chat', 20);
+    TeamChat := config.ReadInt('GameControls', 'TeamChat', 21);
+  end;
+
+  with gGameControls.P1Control, config do
+  begin
+    KeyRight := ReadInt('Player1', 'KeyRight', 33);
+    KeyLeft := ReadInt('Player1', 'KeyLeft', 31);
+    KeyUp := ReadInt('Player1', 'KeyUp', 18);
+    KeyDown := ReadInt('Player1', 'KeyDown', 32);
+    KeyFire := ReadInt('Player1', 'KeyFire', 30);
+    KeyJump := ReadInt('Player1', 'KeyJump', 16);
+    KeyNextWeapon := ReadInt('Player1', 'KeyNextWeapon', 19);
+    KeyPrevWeapon := ReadInt('Player1', 'KeyPrevWeapon', 17);
+    KeyOpen := ReadInt('Player1', 'KeyOpen', 58);
+  end;
+
+  with gPlayer1Settings, config do
+  begin
+    Name := ReadStr('Player1', 'name', 'Player1');
+    Model := ReadStr('Player1', 'model', STD_PLAYER_MODEL);
+    Color.R := Min(Abs(ReadInt('Player1', 'red', PLAYER1_DEF_COLOR.R)), 255);
+    Color.G := Min(Abs(ReadInt('Player1', 'green', PLAYER1_DEF_COLOR.G)), 255);
+    Color.B := Min(Abs(ReadInt('Player1', 'blue', PLAYER1_DEF_COLOR.B)), 255);
+    Team := ReadInt('Player1', 'team', TEAM_RED);
+    if (Team < TEAM_RED) or (Team > TEAM_BLUE) then
+      Team := TEAM_RED;
+  end;
+
+  with gGameControls.P2Control, config do
+  begin
+    KeyRight := ReadInt('Player2', 'KeyRight', 205);
+    KeyLeft := ReadInt('Player2', 'KeyLeft', 203);
+    KeyUp := ReadInt('Player2', 'KeyUp', 200);
+    KeyDown := ReadInt('Player2', 'KeyDown', 208);
+    KeyFire := ReadInt('Player2', 'KeyFire', 184);
+    KeyJump := ReadInt('Player2', 'KeyJump', 157);
+    KeyNextWeapon := ReadInt('Player2', 'KeyNextWeapon', 73);
+    KeyPrevWeapon := ReadInt('Player2', 'KeyPrevWeapon', 71);
+    KeyOpen := ReadInt('Player2', 'KeyOpen', 54);
+  end;
+
+  with gPlayer2Settings, config do
+  begin
+    Name := ReadStr('Player2', 'name', 'Player2');
+    Model := ReadStr('Player2', 'model', STD_PLAYER_MODEL);
+    Color.R := Min(Abs(ReadInt('Player2', 'red', PLAYER2_DEF_COLOR.R)), 255);
+    Color.G := Min(Abs(ReadInt('Player2', 'green', PLAYER2_DEF_COLOR.G)), 255);
+    Color.B := Min(Abs(ReadInt('Player2', 'blue', PLAYER2_DEF_COLOR.B)), 255);
+    Team := ReadInt('Player2', 'team', TEAM_BLUE);
+    if (Team < TEAM_RED) or (Team > TEAM_BLUE) then
+      Team := TEAM_RED;
+  end;
+  
+  for i := 0 to e_MaxJoys-1 do
+    e_JoystickDeadzones[i] := config.ReadInt('Joysticks', 'Deadzone' + IntToStr(i), 8192);
+
+  g_GFX_SetMax(Min(config.ReadInt('Game', 'MaxParticles', 1000), 50000));
+  g_Shells_SetMax(Min(config.ReadInt('Game', 'MaxShells', 300), 600));
+  g_Gibs_SetMax(Min(config.ReadInt('Game', 'MaxGibs', 150), 500));
+  g_Corpses_SetMax(Min(config.ReadInt('Game', 'MaxCorpses', 20), 100));
+
+  case config.ReadInt('Game', 'GibsCount', 3) of
+    0: gGibsCount := 0;
+    1: gGibsCount := 8;
+    2: gGibsCount := 16;
+    3: gGibsCount := 32;
+    else gGibsCount := 48;
+  end;
+
+  ITEM_RESPAWNTIME := 36*Max(config.ReadInt('Game', 'ItemRespawnTime', 60), 0);
+  gBloodCount := Min(config.ReadInt('Game', 'BloodCount', 4), 4);
+  gAdvBlood := config.ReadBool('Game', 'AdvancesBlood', True);
+  gAdvCorpses := config.ReadBool('Game', 'AdvancesCorpses', True);
+  gAdvGibs := config.ReadBool('Game', 'AdvancesGibs', True);
+  gFlash := Min(Max(config.ReadInt('Game', 'Flash', 1), 0), 2);
+  gDrawBackGround := config.ReadBool('Game', 'BackGround', True);
+  gShowMessages := config.ReadBool('Game', 'Messages', True);
+  gRevertPlayers := config.ReadBool('Game', 'RevertPlayers', False);
+  gChatBubble := Min(Max(config.ReadInt('Game', 'ChatBubble', 4), 0), 4);
+
+// Ãåéìïëåé â ñâîåé èãðå
+  gcMap := config.ReadStr('GameplayCustom', 'Map', '');
+  gcGameMode := config.ReadStr('GameplayCustom', 'GameMode', _lc[I_MENU_GAME_TYPE_DM]);
+  gcTimeLimit := Min(Max(config.ReadInt('GameplayCustom', 'TimeLimit', 0), 0), 65535);
+  gcGoalLimit := Min(Max(config.ReadInt('GameplayCustom', 'GoalLimit', 0), 0), 65535);
+  gcMaxLives := Min(Max(config.ReadInt('GameplayCustom', 'MaxLives', 0), 0), 255);
+  gcPlayers := Min(Max(config.ReadInt('GameplayCustom', 'Players', 1), 0), 2);
+  gcTeamDamage := config.ReadBool('GameplayCustom', 'TeamDamage', False);
+  gcAllowExit := config.ReadBool('GameplayCustom', 'AllowExit', True);
+  gcWeaponStay := config.ReadBool('GameplayCustom', 'WeaponStay', False);
+  gcMonsters := config.ReadBool('GameplayCustom', 'Monsters', False);
+  gcBotsVS := config.ReadStr('GameplayCustom', 'BotsVS', 'Everybody');
+
+  with gGameSettings do
+  begin
+    GameMode := g_Game_TextToMode(gcGameMode);
+    if GameMode = GM_NONE then
+      GameMode := GM_DM;
+    if GameMode = GM_SINGLE then
+      GameMode := GM_COOP;
+    TimeLimit := gcTimeLimit;
+    GoalLimit := gcGoalLimit;
+    MaxLives := gcMaxLives;
+
+    Options := 0;
+    if gcTeamDamage then
+      Options := Options or GAME_OPTION_TEAMDAMAGE;
+    if gcAllowExit then
+      Options := Options or GAME_OPTION_ALLOWEXIT;
+    if gcWeaponStay then
+      Options := Options or GAME_OPTION_WEAPONSTAY;
+    if gcMonsters then
+      Options := Options or GAME_OPTION_MONSTERS;
+    if gcBotsVS = 'Everybody' then
+      Options := Options or GAME_OPTION_BOTVSPLAYER or GAME_OPTION_BOTVSMONSTER;
+    if gcBotsVS = 'Players' then
+      Options := Options or GAME_OPTION_BOTVSPLAYER;
+    if gcBotsVS = 'Monsters' then
+      Options := Options or GAME_OPTION_BOTVSMONSTER;
+  end;
+
+// Ãåéìïëåé â ñåòåâîé èãðå
+  gnMap := config.ReadStr('GameplayNetwork', 'Map', '');
+  gnGameMode := config.ReadStr('GameplayNetwork', 'GameMode', _lc[I_MENU_GAME_TYPE_DM]);
+  gnTimeLimit := Min(Max(config.ReadInt('GameplayNetwork', 'TimeLimit', 0), 0), 65535);
+  gnGoalLimit := Min(Max(config.ReadInt('GameplayNetwork', 'GoalLimit', 0), 0), 65535);
+  gnMaxLives := Min(Max(config.ReadInt('GameplayNetwork', 'MaxLives', 0), 0), 255);
+  gnPlayers := Min(Max(config.ReadInt('GameplayNetwork', 'Players', 1), 0), 2);
+  gnTeamDamage := config.ReadBool('GameplayNetwork', 'TeamDamage', False);
+  gnAllowExit := config.ReadBool('GameplayNetwork', 'AllowExit', True);
+  gnWeaponStay := config.ReadBool('GameplayNetwork', 'WeaponStay', False);
+  gnMonsters := config.ReadBool('GameplayNetwork', 'Monsters', False);
+  gnBotsVS := config.ReadStr('GameplayNetwork', 'BotsVS', 'Everybody');
+
+// Îáùèå ñåòåâûå
+  NetSlistIP := config.ReadStr('MasterServer', 'IP', 'mpms.doom2d.org');
+  NetSlistPort := config.ReadInt('MasterServer', 'Port', 25665);
+
+// Ñåðâåð
+  NetServerName := config.ReadStr('Server', 'Name', 'Unnamed Server');
+  NetPassword :=  config.ReadStr('Server', 'Password', '');
+  NetPort := Min(Max(0, config.ReadInt('Server', 'Port', 25666)), 65535);
+  NetMaxClients := Min(Max(0, config.ReadInt('Server', 'MaxClients', 16)), NET_MAXCLIENTS);
+  NetAllowRCON := config.ReadBool('Server', 'RCON', False);
+  NetRCONPassword := config.ReadStr('Server', 'RCONPassword', 'default');
+  NetUseMaster := config.ReadBool('Server', 'SyncWithMaster', True);
+  NetUpdateRate := Max(0, config.ReadInt('Server', 'UpdateInterval', 0));
+  NetRelupdRate := Max(0, config.ReadInt('Server', 'ReliableUpdateInterval', 18));
+  NetMasterRate := Max(1, config.ReadInt('Server', 'MasterSyncInterval', 60000));
+
+// Êëèåíò
+  NetInterpLevel := Max(0, config.ReadInt('Client', 'InterpolationSteps', 2));
+  NetForcePlayerUpdate := config.ReadBool('Client', 'ForcePlayerUpdate', False);
+  NetPredictSelf := config.ReadBool('Client', 'PredictSelf', True);
+  NetClientIP := config.ReadStr('Client', 'LastIP', '127.0.0.1');
+  NetClientPort := Max(0, config.ReadInt('Client', 'LastPort', 25666));
+
+// ßçûê:
+  str := config.ReadStr('Game', 'Language', '');
+  if (str = LANGUAGE_RUSSIAN) or
+     (str = LANGUAGE_ENGLISH) then
+  begin
+    gLanguage := str;
+    gAskLanguage := False;
+  end
+  else
+    gLanguage := LANGUAGE_ENGLISH;
+
+  config.Free();
+
+  if gTextureFilter then
+    TEXTUREFILTER := GL_LINEAR
+  else
+    TEXTUREFILTER := GL_NEAREST;
+end;
+
+procedure g_Options_Write(FileName: String);
+var
+  config: TConfig;
+  i: Integer;
+begin
+  e_WriteLog('Writing config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+
+  config.WriteInt('Video', 'ScreenWidth', gScreenWidth);
+  config.WriteInt('Video', 'ScreenHeight', gScreenHeight);
+  config.WriteInt('Video', 'WinPosX', gWinRealPosX);
+  config.WriteInt('Video', 'WinPosY', gWinRealPosY);
+  config.WriteBool('Video', 'Fullscreen', gFullScreen);
+  config.WriteBool('Video', 'Maximized', gWinMaximized);
+  config.WriteInt('Video', 'BPP', gBPP);
+  config.WriteBool('Video', 'VSync', gVSync);
+  config.WriteBool('Video', 'TextureFilter', gTextureFilter);
+  config.WriteBool('Video', 'LegacyCompatible', fUseMipmaps);
+
+  config.WriteBool('Sound', 'NoSound', gNoSound);
+  config.WriteInt('Sound', 'SoundLevel', gSoundLevel);
+  config.WriteInt('Sound', 'MusicLevel', gMusicLevel);
+  config.WriteInt('Sound', 'MaxSimSounds', gMaxSimSounds);
+  config.WriteBool('Sound', 'MuteInactive', gMuteWhenInactive);
+  config.WriteInt('Sound', 'Announcer', gAnnouncer);
+  config.WriteBool('Sound', 'SoundEffectsDF', gSoundEffectsDF);
+
+  with config, gGameControls.GameControls do
+  begin
+    WriteInt('GameControls', 'TakeScreenshot', TakeScreenshot);
+    WriteInt('GameControls', 'Stat', Stat);
+    WriteInt('GameControls', 'Chat', Chat);
+    WriteInt('GameControls', 'TeamChat', TeamChat);
+  end;
+
+  with config, gGameControls.P1Control, gPlayer1Settings do
+  begin
+    WriteInt('Player1', 'KeyRight', KeyRight);
+    WriteInt('Player1', 'KeyLeft', KeyLeft);
+    WriteInt('Player1', 'KeyUp', KeyUp);
+    WriteInt('Player1', 'KeyDown', KeyDown);
+    WriteInt('Player1', 'KeyFire', KeyFire);
+    WriteInt('Player1', 'KeyJump', KeyJump);
+    WriteInt('Player1', 'KeyNextWeapon', KeyNextWeapon);
+    WriteInt('Player1', 'KeyPrevWeapon', KeyPrevWeapon);
+    WriteInt('Player1', 'KeyOpen', KeyOpen);
+
+    WriteStr('Player1', 'Name', Name);
+    WriteStr('Player1', 'model', Model);
+    WriteInt('Player1', 'red', Color.R);
+    WriteInt('Player1', 'green', Color.G);
+    WriteInt('Player1', 'blue', Color.B);
+    WriteInt('Player1', 'team', Team);
+  end;
+
+  with config, gGameControls.P2Control, gPlayer2Settings do
+  begin
+    WriteInt('Player2', 'KeyRight', KeyRight);
+    WriteInt('Player2', 'KeyLeft', KeyLeft);
+    WriteInt('Player2', 'KeyUp', KeyUp);
+    WriteInt('Player2', 'KeyDown', KeyDown);
+    WriteInt('Player2', 'KeyFire', KeyFire);
+    WriteInt('Player2', 'KeyJump', KeyJump);
+    WriteInt('Player2', 'KeyNextWeapon', KeyNextWeapon);
+    WriteInt('Player2', 'KeyPrevWeapon', KeyPrevWeapon);
+    WriteInt('Player2', 'KeyOpen', KeyOpen);
+
+    WriteStr('Player2', 'Name', Name);
+    WriteStr('Player2', 'model', Model);
+    WriteInt('Player2', 'red', Color.R);
+    WriteInt('Player2', 'green', Color.G);
+    WriteInt('Player2', 'blue', Color.B);
+    WriteInt('Player2', 'team', Team);
+  end;
+  
+  for i := 0 to e_MaxJoys-1 do
+    config.WriteInt('Joysticks', 'Deadzone' + IntToStr(i), e_JoystickDeadzones[i]);
+
+  with config do
+    case gGibsCount of
+      0: config.WriteInt('Game', 'GibsCount', 0);
+      8: config.WriteInt('Game', 'GibsCount', 1);
+      16: config.WriteInt('Game', 'GibsCount', 2);
+      32: config.WriteInt('Game', 'GibsCount', 3);
+      else config.WriteInt('Game', 'GibsCount', 4);
+    end;
+
+  config.WriteInt('Game', 'ItemRespawnTime', ITEM_RESPAWNTIME div 36);
+  config.WriteInt('Game', 'MaxParticles', g_GFX_GetMax());
+  config.WriteInt('Game', 'MaxShells', g_Shells_GetMax());
+  config.WriteInt('Game', 'MaxGibs', g_Gibs_GetMax());
+  config.WriteInt('Game', 'MaxCorpses', g_Corpses_GetMax());
+  config.WriteInt('Game', 'BloodCount', gBloodCount);
+  config.WriteBool('Game', 'AdvancesBlood', gAdvBlood);
+  config.WriteBool('Game', 'AdvancesCorpses', gAdvCorpses);
+  config.WriteBool('Game', 'AdvancesGibs', gAdvGibs);
+  config.WriteInt('Game', 'Flash', gFlash);
+  config.WriteBool('Game', 'BackGround', gDrawBackGround);
+  config.WriteBool('Game', 'Messages', gShowMessages);
+  config.WriteBool('Game', 'RevertPlayers', gRevertPlayers);
+  config.WriteInt('Game', 'ChatBubble', gChatBubble);
+
+  config.WriteStr ('GameplayCustom', 'Map', gcMap);
+  config.WriteStr ('GameplayCustom', 'GameMode', gcGameMode);
+  config.WriteInt ('GameplayCustom', 'TimeLimit', gcTimeLimit);
+  config.WriteInt ('GameplayCustom', 'GoalLimit', gcGoalLimit);
+  config.WriteInt ('GameplayCustom', 'MaxLives', gcMaxLives);
+  config.WriteInt ('GameplayCustom', 'Players', gcPlayers);
+  config.WriteBool('GameplayCustom', 'TeamDamage', gcTeamDamage);
+  config.WriteBool('GameplayCustom', 'AllowExit', gcAllowExit);
+  config.WriteBool('GameplayCustom', 'WeaponStay', gcWeaponStay);
+  config.WriteBool('GameplayCustom', 'Monsters', gcMonsters);
+  config.WriteStr ('GameplayCustom', 'BotsVS', gcBotsVS);
+
+  config.WriteStr ('GameplayNetwork', 'Map', gnMap);
+  config.WriteStr ('GameplayNetwork', 'GameMode', gnGameMode);
+  config.WriteInt ('GameplayNetwork', 'TimeLimit', gnTimeLimit);
+  config.WriteInt ('GameplayNetwork', 'GoalLimit', gnGoalLimit);
+  config.WriteInt ('GameplayNetwork', 'MaxLives', gnMaxLives);
+  config.WriteInt ('GameplayNetwork', 'Players', gnPlayers);
+  config.WriteBool('GameplayNetwork', 'TeamDamage', gnTeamDamage);
+  config.WriteBool('GameplayNetwork', 'AllowExit', gnAllowExit);
+  config.WriteBool('GameplayNetwork', 'WeaponStay', gnWeaponStay);
+  config.WriteBool('GameplayNetwork', 'Monsters', gnMonsters);
+  config.WriteStr ('GameplayNetwork', 'BotsVS', gnBotsVS);
+
+  config.WriteStr('MasterServer', 'IP', NetSlistIP);
+  config.WriteInt('MasterServer', 'Port', NetSlistPort);
+
+  config.WriteStr ('Server', 'Name', NetServerName);
+  config.WriteStr ('Server', 'Password', NetPassword);
+  config.WriteInt ('Server', 'Port', NetPort);
+  config.WriteInt ('Server', 'MaxClients', NetMaxClients);
+  config.WriteBool('Server', 'RCON', NetAllowRCON);
+  config.WriteStr ('Server', 'RCONPassword', NetRCONPassword);
+  config.WriteBool('Server', 'SyncWithMaster', NetUseMaster);
+  config.WriteInt ('Server', 'UpdateInterval', NetUpdateRate);
+  config.WriteInt ('Server', 'ReliableUpdateInterval', NetRelupdRate);
+  config.WriteInt ('Server', 'MasterSyncInterval', NetMasterRate);
+
+  config.WriteInt  ('Client', 'InterpolationSteps', NetInterpLevel);
+  config.WriteBool ('Client', 'ForcePlayerUpdate', NetForcePlayerUpdate);
+  config.WriteBool ('Client', 'PredictSelf', NetPredictSelf);
+  config.WriteStr  ('Client', 'LastIP', NetClientIP);
+  config.WriteInt  ('Client', 'LastPort', NetClientPort);
+
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+procedure g_Options_Write_Language(FileName: String);
+var
+  config: TConfig;
+begin
+  e_WriteLog('Writing language config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+  config.WriteStr('Game', 'Language', gLanguage);
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+procedure g_Options_Write_Video(FileName: String);
+var
+  config: TConfig;
+  sW, sH: Integer;
+begin
+  e_WriteLog('Writing resolution to config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+
+  if gWinMaximized and (not gFullscreen) then
+    begin
+      sW := gWinSizeX;
+      sH := gWinSizeY;
+    end
+  else
+    begin
+      sW := gScreenWidth;
+      sH := gScreenHeight;
+    end;
+
+  config.WriteInt('Video', 'ScreenWidth', sW);
+  config.WriteInt('Video', 'ScreenHeight', sH);
+  config.WriteInt('Video', 'WinPosX', gWinRealPosX);
+  config.WriteInt('Video', 'WinPosY', gWinRealPosY);
+  config.WriteBool('Video', 'Fullscreen', gFullscreen);
+  config.WriteBool('Video', 'Maximized', gWinMaximized);
+
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+procedure g_Options_Write_Gameplay_Custom(FileName: String);
+var
+  config: TConfig;
+begin
+  e_WriteLog('Writing custom gameplay config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+
+  config.WriteStr ('GameplayCustom', 'Map', gcMap);
+  config.WriteStr ('GameplayCustom', 'GameMode', gcGameMode);
+  config.WriteInt ('GameplayCustom', 'TimeLimit', gcTimeLimit);
+  config.WriteInt ('GameplayCustom', 'GoalLimit', gcGoalLimit);
+  config.WriteInt ('GameplayCustom', 'MaxLives', gcMaxLives);
+  config.WriteInt ('GameplayCustom', 'Players', gcPlayers);
+  config.WriteBool('GameplayCustom', 'TeamDamage', gcTeamDamage);
+  config.WriteBool('GameplayCustom', 'AllowExit', gcAllowExit);
+  config.WriteBool('GameplayCustom', 'WeaponStay', gcWeaponStay);
+  config.WriteBool('GameplayCustom', 'Monsters', gcMonsters);
+  config.WriteStr ('GameplayCustom', 'BotsVS', gcBotsVS);
+
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+procedure g_Options_Write_Gameplay_Net(FileName: String);
+var
+  config: TConfig;
+begin
+  e_WriteLog('Writing network gameplay config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+
+  config.WriteStr ('GameplayNetwork', 'Map', gnMap);
+  config.WriteStr ('GameplayNetwork', 'GameMode', gnGameMode);
+  config.WriteInt ('GameplayNetwork', 'TimeLimit', gnTimeLimit);
+  config.WriteInt ('GameplayNetwork', 'GoalLimit', gnGoalLimit);
+  config.WriteInt ('GameplayNetwork', 'MaxLives', gnMaxLives);
+  config.WriteInt ('GameplayNetwork', 'Players', gnPlayers);
+  config.WriteBool('GameplayNetwork', 'TeamDamage', gnTeamDamage);
+  config.WriteBool('GameplayNetwork', 'AllowExit', gnAllowExit);
+  config.WriteBool('GameplayNetwork', 'WeaponStay', gnWeaponStay);
+  config.WriteBool('GameplayNetwork', 'Monsters', gnMonsters);
+  config.WriteStr ('GameplayNetwork', 'BotsVS', gnBotsVS);
+
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+procedure g_Options_Write_Net_Server(FileName: String);
+var
+  config: TConfig;
+begin
+  e_WriteLog('Writing server config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+
+  config.WriteStr ('Server', 'Name', NetServerName);
+  config.WriteStr ('Server', 'Password', NetPassword);
+  config.WriteInt ('Server', 'Port', NetPort);
+  config.WriteInt ('Server', 'MaxClients', NetMaxClients);
+  config.WriteBool('Server', 'SyncWithMaster', NetUseMaster);
+
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+procedure g_Options_Write_Net_Client(FileName: String);
+var
+  config: TConfig;
+begin
+  e_WriteLog('Writing client config', MSG_NOTIFY);
+
+  config := TConfig.CreateFile(FileName);
+
+  config.WriteStr('Client', 'LastIP', NetClientIP);
+  config.WriteInt('Client', 'LastPort', NetClientPort);
+
+  config.SaveFile(FileName);
+  config.Free();
+end;
+
+end.
diff --git a/src/game/g_panel.pas b/src/game/g_panel.pas
new file mode 100644 (file)
index 0000000..b522ae2
--- /dev/null
@@ -0,0 +1,494 @@
+unit g_panel;
+
+interface
+
+uses
+  MAPSTRUCT, BinEditor, g_textures;
+
+type
+  TAddTextureArray = Array of
+    record
+      Texture: Cardinal;
+      Anim: Boolean;
+    end;
+
+  TPanel = Class (TObject)
+  private
+    FTextureWidth:    Word;
+    FTextureHeight:   Word;
+    FAlpha:           Byte;
+    FBlending:        Boolean;
+    FTextureIDs:      Array of
+                        record
+                          case Anim: Boolean of
+                            False: (Tex: Cardinal);
+                            True:  (AnTex: TAnimation);
+                        end;
+
+  public
+    FCurTexture:      Integer; // Íîìåð òåêóùåé òåêñòóðû
+    FCurFrame:        Integer;
+    FCurFrameCount:   Byte;
+    X, Y:             Integer;
+    Width, Height:    Word;
+    PanelType:        Word;
+    SaveIt:           Boolean; // Ñîõðàíÿòü ïðè SaveState?
+    Enabled:          Boolean;
+    Door:             Boolean;
+    LiftType:         Byte;
+    LastAnimLoop:     Byte;
+
+    constructor Create(PanelRec: TPanelRec_1;
+                       AddTextures: TAddTextureArray;
+                       CurTex: Integer;
+                       var Textures: TLevelTextureArray);
+    destructor  Destroy(); override;
+
+    procedure   Draw();
+    procedure   Update();
+    procedure   SetFrame(Frame: Integer; Count: Byte);
+    procedure   NextTexture(AnimLoop: Byte = 0);
+    procedure   SetTexture(ID: Integer; AnimLoop: Byte = 0);
+    function    GetTextureID(): Cardinal;
+    function    GetTextureCount(): Integer;
+
+    procedure   SaveState(var Mem: TBinMemoryWriter);
+    procedure   LoadState(var Mem: TBinMemoryReader);
+  end;
+
+  TPanelArray = Array of TPanel;
+
+implementation
+
+uses
+  g_basic, g_map, MAPDEF, g_game, e_graphics,
+  g_console, g_language;
+
+const
+  PANEL_SIGNATURE = $4C4E4150; // 'PANL'
+
+{ T P a n e l : }
+
+constructor TPanel.Create(PanelRec: TPanelRec_1;
+                          AddTextures: TAddTextureArray;
+                          CurTex: Integer;
+                          var Textures: TLevelTextureArray);
+var
+  i: Integer;
+begin
+  X := PanelRec.X;
+  Y := PanelRec.Y;
+  Width := PanelRec.Width;
+  Height := PanelRec.Height;
+  FAlpha := 0;
+  FBlending := False;
+  FCurFrame := 0;
+  FCurFrameCount := 0;
+  LastAnimLoop := 0;
+
+// Òèï ïàíåëè:
+  PanelType := PanelRec.PanelType;
+  Enabled := True;
+  Door := False;
+  LiftType := 0;
+  SaveIt := False;
+
+  case PanelType of
+    PANEL_OPENDOOR:
+      begin
+        Enabled := False;
+        Door := True;
+        SaveIt := True;
+      end;
+    PANEL_CLOSEDOOR:
+      begin
+        Door := True;
+        SaveIt := True;
+      end;
+    PANEL_LIFTUP:
+      SaveIt := True;
+    PANEL_LIFTDOWN:
+      begin
+        LiftType := 1;
+        SaveIt := True;
+      end;
+    PANEL_LIFTLEFT:
+      begin
+        LiftType := 2;
+        SaveIt := True;
+      end;
+    PANEL_LIFTRIGHT:
+      begin
+        LiftType := 3;
+        SaveIt := True;
+      end;
+  end;
+
+// Íåâèäèìàÿ:
+  if ByteBool(PanelRec.Flags and PANEL_FLAG_HIDE) then
+  begin
+    SetLength(FTextureIDs, 0);
+    FCurTexture := -1;
+    Exit;
+  end;
+// Ïàíåëè, íå èñïîëüçóþùèå òåêñòóðû:
+  if ByteBool(PanelType and
+    (PANEL_LIFTUP or
+     PANEL_LIFTDOWN or
+     PANEL_LIFTLEFT or
+     PANEL_LIFTRIGHT or
+     PANEL_BLOCKMON)) then
+  begin
+    SetLength(FTextureIDs, 0);
+    FCurTexture := -1;
+    Exit;
+  end;
+
+// Åñëè ýòî æèäêîñòü áåç òåêñòóðû - ñïåöòåêñòóðó:
+  if WordBool(PanelType and (PANEL_WATER or PANEL_ACID1 or PANEL_ACID2)) and
+     (not ByteBool(PanelRec.Flags and PANEL_FLAG_WATERTEXTURES)) then
+  begin
+    SetLength(FTextureIDs, 1);
+    FTextureIDs[0].Anim := False;
+
+    case PanelRec.PanelType of
+      PANEL_WATER:
+        FTextureIDs[0].Tex := TEXTURE_SPECIAL_WATER;
+      PANEL_ACID1:
+        FTextureIDs[0].Tex := TEXTURE_SPECIAL_ACID1;
+      PANEL_ACID2:
+        FTextureIDs[0].Tex := TEXTURE_SPECIAL_ACID2;
+    end;
+
+    FCurTexture := 0;
+    Exit;
+  end;
+
+  SetLength(FTextureIDs, Length(AddTextures));
+
+  if CurTex < 0 then
+    FCurTexture := -1
+  else
+    if CurTex >= Length(FTextureIDs) then
+      FCurTexture := Length(FTextureIDs) - 1
+    else
+      FCurTexture := CurTex;
+
+  for i := 0 to Length(FTextureIDs)-1 do
+  begin
+    FTextureIDs[i].Anim := AddTextures[i].Anim;
+    if FTextureIDs[i].Anim then
+      begin // Àíèìèðîâàííàÿ òåêñòóðà
+        FTextureIDs[i].AnTex :=
+           TAnimation.Create(Textures[AddTextures[i].Texture].FramesID,
+                             True, Textures[AddTextures[i].Texture].Speed);
+        FTextureIDs[i].AnTex.Blending := ByteBool(PanelRec.Flags and PANEL_FLAG_BLENDING);
+        FTextureIDs[i].AnTex.Alpha := PanelRec.Alpha;
+        SaveIt := True;
+      end
+    else
+      begin // Îáû÷íàÿ òåêñòóðà
+        FTextureIDs[i].Tex := Textures[AddTextures[i].Texture].TextureID;
+      end;
+  end;
+
+// Òåêñòóð íåñêîëüêî - íóæíî ñîõðàíÿòü òåêóùóþ:
+  if Length(FTextureIDs) > 1 then
+    SaveIt := True;
+
+// Åñëè íå ñïåöòåêñòóðà, òî çàäàåì ðàçìåðû:
+  if not g_Map_IsSpecialTexture(Textures[PanelRec.TextureNum].TextureName) then
+  begin
+    FTextureWidth := Textures[PanelRec.TextureNum].Width;
+    FTextureHeight := Textures[PanelRec.TextureNum].Height;
+    FAlpha := PanelRec.Alpha;
+    FBlending := ByteBool(PanelRec.Flags and PANEL_FLAG_BLENDING);
+  end;
+end;
+
+destructor TPanel.Destroy();
+var
+  i: Integer;
+begin
+  for i := 0 to High(FTextureIDs) do
+    if FTextureIDs[i].Anim then
+      FTextureIDs[i].AnTex.Free();
+  SetLength(FTextureIDs, 0);
+
+  Inherited;
+end;
+
+procedure TPanel.Draw();
+var
+  xx, yy: Integer;
+  NoTextureID: DWORD;
+  NW, NH: Word;
+begin
+  if Enabled and (FCurTexture >= 0) and
+     (Width > 0) and (Height > 0) and (FAlpha < 255) and
+     g_Collide(X, Y, Width, Height,
+               sX, sY, sWidth, sHeight) then
+  begin
+    if FTextureIDs[FCurTexture].Anim then
+      begin // Àíèìèðîâàííàÿ òåêñòóðà
+        if FTextureIDs[FCurTexture].AnTex = nil then
+          Exit;
+  
+        for xx := 0 to (Width div FTextureWidth)-1 do
+          for yy := 0 to (Height div FTextureHeight)-1 do
+            FTextureIDs[FCurTexture].AnTex.Draw(
+              X + xx*FTextureWidth,
+              Y + yy*FTextureHeight, M_NONE);
+      end
+    else
+      begin // Îáû÷íàÿ òåêñòóðà
+        case FTextureIDs[FCurTexture].Tex of
+          TEXTURE_SPECIAL_WATER:
+            e_DrawFillQuad(X, Y, X+Width-1, Y+Height-1,
+                           0, 0, 255, 0, B_FILTER);
+          TEXTURE_SPECIAL_ACID1:
+            e_DrawFillQuad(X, Y, X+Width-1, Y+Height-1,
+                           0, 128, 0, 0, B_FILTER);
+          TEXTURE_SPECIAL_ACID2:
+            e_DrawFillQuad(X, Y, X+Width-1, Y+Height-1,
+                           128, 0, 0, 0, B_FILTER);
+          TEXTURE_NONE:
+            if g_Texture_Get('NOTEXTURE', NoTextureID) then
+            begin
+              e_GetTextureSize(NoTextureID, @NW, @NH);
+              e_DrawFill(NoTextureID, X, Y, Width div NW, Height div NH,
+                         0, False, False);
+            end else
+            begin
+              xx := X + (Width div 2);
+              yy := Y + (Height div 2);
+              e_DrawFillQuad(X, Y, xx, yy,
+                             255, 0, 255, 0);
+              e_DrawFillQuad(xx, Y, X+Width-1, yy,
+                             255, 255, 0, 0);
+              e_DrawFillQuad(X, yy, xx, Y+Height-1,
+                             255, 255, 0, 0);
+              e_DrawFillQuad(xx, yy, X+Width-1, Y+Height-1,
+                             255, 0, 255, 0);
+            end;
+
+          else
+            e_DrawFill(FTextureIDs[FCurTexture].Tex, X, Y,
+                       Width div FTextureWidth,
+                       Height div FTextureHeight,
+                       FAlpha, True, FBlending);
+        end;
+      end;
+  end;
+end;
+
+procedure TPanel.Update();
+begin
+  if Enabled and (FCurTexture >= 0) and
+    (FTextureIDs[FCurTexture].Anim) and
+    (FTextureIDs[FCurTexture].AnTex <> nil) and
+    (Width > 0) and (Height > 0) and (FAlpha < 255) then
+  begin
+    FTextureIDs[FCurTexture].AnTex.Update();
+    FCurFrame := FTextureIDs[FCurTexture].AnTex.CurrentFrame;
+    FCurFrameCount := FTextureIDs[FCurTexture].AnTex.CurrentCounter;
+  end;
+end;
+
+procedure TPanel.SetFrame(Frame: Integer; Count: Byte);
+
+  function ClampInt(X, A, B: Integer): Integer;
+  begin
+    Result := X;
+    if X < A then Result := A else if X > B then Result := B;
+  end;
+
+begin
+  if Enabled and (FCurTexture >= 0) and
+    (FTextureIDs[FCurTexture].Anim) and
+    (FTextureIDs[FCurTexture].AnTex <> nil) and
+    (Width > 0) and (Height > 0) and (FAlpha < 255) then
+  begin
+    FCurFrame := ClampInt(Frame, 0, FTextureIDs[FCurTexture].AnTex.TotalFrames);
+    FCurFrameCount := Count;
+    FTextureIDs[FCurTexture].AnTex.CurrentFrame := FCurFrame;
+    FTextureIDs[FCurTexture].AnTex.CurrentCounter := FCurFrameCount;
+  end;
+end;
+
+procedure TPanel.NextTexture(AnimLoop: Byte = 0);
+begin
+  Assert(FCurTexture >= -1, 'FCurTexture < -1');
+
+// Íåò òåêñòóð:
+  if Length(FTextureIDs) = 0 then
+    FCurTexture := -1
+  else
+  // Òîëüêî îäíà òåêñòóðà:
+    if Length(FTextureIDs) = 1 then
+      begin
+        if FCurTexture = 0 then
+          FCurTexture := -1
+        else
+          FCurTexture := 0;
+      end
+    else
+    // Áîëüøå îäíîé òåêñòóðû:
+      begin
+      // Ñëåäóþùàÿ:
+        Inc(FCurTexture);
+      // Ñëåäóþùåé íåò - âîçâðàò ê íà÷àëó:
+        if FCurTexture >= Length(FTextureIDs) then
+          FCurTexture := 0;
+      end;
+
+// Ïåðåêëþ÷èëèñü íà âèäèìóþ àíèì. òåêñòóðó:
+  if (FCurTexture >= 0) and FTextureIDs[FCurTexture].Anim then
+  begin
+    if (FTextureIDs[FCurTexture].AnTex = nil) then
+    begin
+      g_FatalError(_lc[I_GAME_ERROR_SWITCH_TEXTURE]);
+      Exit;
+    end;
+
+    if AnimLoop = 1 then
+      FTextureIDs[FCurTexture].AnTex.Loop := True
+    else
+      if AnimLoop = 2 then
+        FTextureIDs[FCurTexture].AnTex.Loop := False;
+        
+    FTextureIDs[FCurTexture].AnTex.Reset();
+  end;
+
+  LastAnimLoop := AnimLoop;
+end;
+
+procedure TPanel.SetTexture(ID: Integer; AnimLoop: Byte = 0);
+begin
+// Íåò òåêñòóð:
+  if Length(FTextureIDs) = 0 then
+    FCurTexture := -1
+  else
+  // Òîëüêî îäíà òåêñòóðà:
+    if Length(FTextureIDs) = 1 then
+      begin
+        if (ID = 0) or (ID = -1) then
+          FCurTexture := ID;
+      end
+    else
+    // Áîëüøå îäíîé òåêñòóðû:
+      begin
+        if (ID >= -1) and (ID <= High(FTextureIDs)) then
+          FCurTexture := ID;
+      end;
+
+// Ïåðåêëþ÷èëèñü íà âèäèìóþ àíèì. òåêñòóðó:
+  if (FCurTexture >= 0) and FTextureIDs[FCurTexture].Anim then
+  begin
+    if (FTextureIDs[FCurTexture].AnTex = nil) then
+    begin
+      g_FatalError(_lc[I_GAME_ERROR_SWITCH_TEXTURE]);
+      Exit;
+    end;
+
+    if AnimLoop = 1 then
+      FTextureIDs[FCurTexture].AnTex.Loop := True
+    else
+      if AnimLoop = 2 then
+        FTextureIDs[FCurTexture].AnTex.Loop := False;
+        
+    FTextureIDs[FCurTexture].AnTex.Reset();
+  end;
+
+  LastAnimLoop := AnimLoop;
+end;
+
+function TPanel.GetTextureID(): DWORD;
+begin
+  Result := TEXTURE_NONE;
+
+  if (FCurTexture >= 0) then
+  begin
+    if FTextureIDs[FCurTexture].Anim then
+      Result := FTextureIDs[FCurTexture].AnTex.FramesID
+    else
+      Result := FTextureIDs[FCurTexture].Tex;
+  end;
+end;
+
+function TPanel.GetTextureCount(): Integer;
+begin
+  Result := Length(FTextureIDs);
+  if Enabled and (FCurTexture >= 0) then
+     if (FTextureIDs[FCurTexture].Anim) and
+        (FTextureIDs[FCurTexture].AnTex <> nil) and
+        (Width > 0) and (Height > 0) and (FAlpha < 255) then
+       Result := Result + 100;
+end;
+
+procedure TPanel.SaveState(Var Mem: TBinMemoryWriter);
+var
+  sig: DWORD;
+  anim: Boolean;
+begin
+  if (not SaveIt) or (Mem = nil) then
+    Exit;
+
+// Ñèãíàòóðà ïàíåëè:
+  sig := PANEL_SIGNATURE; // 'PANL'
+  Mem.WriteDWORD(sig);
+// Îòêðûòà/çàêðûòà, åñëè äâåðü:
+  Mem.WriteBoolean(Enabled);
+// Íàïðàâëåíèå ëèôòà, åñëè ëèôò:
+  Mem.WriteByte(LiftType);
+// Íîìåð òåêóùåé òåêñòóðû:
+  Mem.WriteInt(FCurTexture);
+// Àíèìèðîâàííàÿ ëè òåêóùàÿ òåêñòóðà:
+  if (FCurTexture >= 0) and (FTextureIDs[FCurTexture].Anim) then
+    begin
+      Assert(FTextureIDs[FCurTexture].AnTex <> nil,
+             'TPanel.SaveState: No animation object');
+      anim := True;
+    end
+  else
+    anim := False;
+  Mem.WriteBoolean(anim);
+// Åñëè äà - ñîõðàíÿåì àíèìàöèþ:
+  if anim then
+    FTextureIDs[FCurTexture].AnTex.SaveState(Mem);
+end;
+
+procedure TPanel.LoadState(var Mem: TBinMemoryReader);
+var
+  sig: DWORD;
+  anim: Boolean;
+begin
+  if (not SaveIt) or (Mem = nil) then
+    Exit;
+
+// Ñèãíàòóðà ïàíåëè:
+  Mem.ReadDWORD(sig);
+  if sig <> PANEL_SIGNATURE then // 'PANL'
+  begin
+    raise EBinSizeError.Create('TPanel.LoadState: Wrong Panel Signature');
+  end;
+// Îòêðûòà/çàêðûòà, åñëè äâåðü:
+  Mem.ReadBoolean(Enabled);
+// Íàïðàâëåíèå ëèôòà, åñëè ëèôò:
+  Mem.ReadByte(LiftType);
+// Íîìåð òåêóùåé òåêñòóðû:
+  Mem.ReadInt(FCurTexture);
+// Àíèìèðîâàííàÿ ëè òåêóùàÿ òåêñòóðà:
+  Mem.ReadBoolean(anim);
+// Åñëè äà - çàãðóæàåì àíèìàöèþ:
+  if anim then
+  begin
+    Assert((FCurTexture >= 0) and
+           (FTextureIDs[FCurTexture].Anim) and
+           (FTextureIDs[FCurTexture].AnTex <> nil),
+           'TPanel.LoadState: No animation object');
+    FTextureIDs[FCurTexture].AnTex.LoadState(Mem);
+  end;
+end;
+
+end.
diff --git a/src/game/g_phys.pas b/src/game/g_phys.pas
new file mode 100644 (file)
index 0000000..90e330d
--- /dev/null
@@ -0,0 +1,584 @@
+unit g_phys;
+
+interface
+
+uses
+  e_graphics;
+
+type
+  PObj = ^TObj;
+  TObj = record
+    X, Y:    Integer;
+    Rect:    TRectWH;
+    Vel:     TPoint2i;
+    Accel:   TPoint2i;
+  end;
+
+const
+  MAX_YV = 30;
+  LIMIT_VEL   = 16384;
+  LIMIT_ACCEL = 1024;
+
+  MOVE_NONE       = 0;
+  MOVE_HITWALL    = 1;
+  MOVE_HITCEIL    = 2;
+  MOVE_HITLAND    = 4;
+  MOVE_FALLOUT    = 8;
+  MOVE_INWATER    = 16;
+  MOVE_HITWATER   = 32;
+  MOVE_HITAIR     = 64;
+  MOVE_BLOCK      = 128;
+
+procedure g_Obj_Init(Obj: PObj);
+function  g_Obj_Move(Obj: PObj; Fallable: Boolean; Splash: Boolean; ClimbSlopes: Boolean = False): Word;
+function  g_Obj_Collide(Obj1, Obj2: PObj): Boolean; overload;
+function  g_Obj_Collide(X, Y: Integer; Width, Height: Word; Obj: PObj): Boolean; overload;
+function  g_Obj_CollidePoint(X, Y: Integer; Obj: PObj): Boolean;
+function  g_Obj_CollideLevel(Obj: PObj; XInc, YInc: Integer): Boolean;
+function  g_Obj_CollideStep(Obj: PObj; XInc, YInc: Integer): Boolean;
+function  g_Obj_CollideWater(Obj: PObj; XInc, YInc: Integer): Boolean;
+function  g_Obj_CollideLiquid(Obj: PObj; XInc, YInc: Integer): Boolean;
+function  g_Obj_CollidePanel(Obj: PObj; XInc, YInc: Integer; PanelType: Word): Boolean;
+function  g_Obj_StayOnStep(Obj: PObj): Boolean;
+procedure g_Obj_Push(Obj: PObj; VelX, VelY: Integer);
+procedure g_Obj_PushA(Obj: PObj; Vel: Integer; Angle: SmallInt);
+procedure g_Obj_SetSpeed(Obj: PObj; s: Integer);
+function  z_dec(a, b: Integer): Integer;
+function  z_fdec(a, b: Double): Double;
+
+var
+  gMon: Boolean = False;
+
+implementation
+
+uses
+  g_map, g_basic, Math, g_player, g_console, SysUtils,
+  g_sound, g_gfx, MAPDEF, g_monsters, g_game, BinEditor;
+
+function g_Obj_StayOnStep(Obj: PObj): Boolean;
+begin
+  Result := not g_Map_CollidePanel(Obj^.X+Obj^.Rect.X, Obj^.Y+Obj^.Rect.Y+Obj^.Rect.Height-1,
+                                   Obj^.Rect.Width, 1,
+                                   PANEL_STEP, False)
+            and g_Map_CollidePanel(Obj^.X+Obj^.Rect.X, Obj^.Y+Obj^.Rect.Y+Obj^.Rect.Height,
+                                   Obj^.Rect.Width, 1,
+                                   PANEL_STEP, False);
+end;
+
+function CollideLiquid(Obj: PObj; XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj^.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height*2 div 3,
+                               PANEL_WATER or PANEL_ACID1 or PANEL_ACID2, False);
+end;
+
+function CollideLift(Obj: PObj; XInc, YInc: Integer): Integer;
+begin
+  if g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj^.Rect.Y+YInc,
+                        Obj^.Rect.Width, Obj^.Rect.Height,
+                        PANEL_LIFTUP, False) then
+    Result := -1
+  else if g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj^.Rect.Y+YInc,
+                          Obj^.Rect.Width, Obj^.Rect.Height,
+                          PANEL_LIFTDOWN, False) then
+    Result := 1
+  else
+    Result := 0;
+end;
+
+function CollideHorLift(Obj: PObj; XInc, YInc: Integer): Integer;
+var
+  left, right: Boolean;
+begin
+  left := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj^.Rect.Y+YInc,
+                             Obj^.Rect.Width, Obj^.Rect.Height,
+                             PANEL_LIFTLEFT, False);
+  right := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj^.Rect.Y+YInc,
+                             Obj^.Rect.Width, Obj^.Rect.Height,
+                             PANEL_LIFTRIGHT, False);
+  if left and not right then
+    Result := -1
+  else if right and not left then
+    Result := 1
+  else
+    Result := 0;
+end;
+
+function CollidePlayers(_Obj: PObj; XInc, YInc: Integer): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if gPlayers = nil then
+    Exit;
+
+  for a := 0 to High(gPlayers) do
+    if gPlayers[a] <> nil then
+      with gPlayers[a] do
+        if Live and
+           g_Collide(GameX+PLAYER_RECT.X, GameY+PLAYER_RECT.Y,
+                     PLAYER_RECT.Width, PLAYER_RECT.Height,
+                     _Obj^.X+_Obj^.Rect.X+XInc, _Obj^.Y+_Obj^.Rect.Y+YInc,
+                     _Obj^.Rect.Width, _Obj^.Rect.Height) then
+        begin
+          Result := True;
+          Exit;
+        end;
+end;
+
+function CollideMonsters(Obj: PObj; XInc, YInc: Integer): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if gMonsters = nil then
+    Exit;
+
+  for a := 0 to High(gMonsters) do
+    if gMonsters[a] <> nil then
+      if gMonsters[a].Live and
+         gMonsters[a].Collide(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj^.Rect.Y+YInc,
+                              Obj^.Rect.Width, Obj^.Rect.Height) then
+      begin
+        Result := True;
+        Exit;
+      end;
+end;
+
+function Blocked(Obj: PObj; XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height,
+                               PANEL_BLOCKMON, False);
+end;
+
+function g_Obj_CollideLevel(Obj: PObj; XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height,
+                               PANEL_WALL, False);
+end;
+
+function g_Obj_CollideStep(Obj: PObj; XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height,
+                               PANEL_STEP, False);
+end;
+
+function g_Obj_CollideWater(Obj: PObj; XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height,
+                               PANEL_WATER, False);
+end;
+
+function g_Obj_CollideLiquid(Obj: PObj; XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height,
+                               PANEL_WATER or PANEL_ACID1 or PANEL_ACID2, False);
+end;
+
+function g_Obj_CollidePanel(Obj: PObj; XInc, YInc: Integer; PanelType: Word): Boolean;
+begin
+  Result := g_Map_CollidePanel(Obj^.X+Obj^.Rect.X+XInc, Obj^.Y+Obj.Rect.Y+YInc,
+                               Obj^.Rect.Width, Obj^.Rect.Height,
+                               PanelType, False);
+end;
+
+procedure g_Obj_Splash(Obj: PObj; Color: Byte);
+var
+  MaxVel: Integer;
+begin
+  MaxVel := Max(Abs(Obj^.Vel.X), Abs(Obj^.Vel.Y));
+  if MaxVel > 4 then begin
+    if MaxVel < 10 then
+      g_Sound_PlayExAt('SOUND_GAME_BULK1', Obj^.X, Obj^.Y)
+    else
+      g_Sound_PlayExAt('SOUND_GAME_BULK2', Obj^.X, Obj^.Y);
+  end;
+
+  g_GFX_Water(Obj^.X+Obj^.Rect.X+(Obj^.Rect.Width div 2),
+              Obj^.Y+Obj^.Rect.Y+(Obj^.Rect.Height div 2),
+              Min(5*(Abs(Obj^.Vel.X)+Abs(Obj^.Vel.Y)), 50),
+              -Obj^.Vel.X, -Obj^.Vel.Y,
+              Obj^.Rect.Width, 16, Color);
+end;
+
+function move(Obj: PObj; dx, dy: Integer; ClimbSlopes: Boolean): Word;
+var
+  i: Integer;
+  sx, sy: ShortInt;
+  st: Word;
+
+  procedure slope(s: Integer);
+  var
+    i: Integer;
+  begin
+    i := 0;
+    while g_Obj_CollideLevel(Obj, sx, 0) and (i < 4) do
+    begin
+      Obj^.Y := Obj^.Y + s;
+      Inc(i);
+    end;
+    Obj^.X := Obj^.X + sx;
+  end;
+
+  function movex(): Boolean;
+  begin
+    Result := False;
+
+  // Åñëè ìîíñòðó øàãíóòü â ñòîðîíó, à òàì áëîêìîí:
+    if gMon and not WordBool(st and MOVE_BLOCK) then
+      if Blocked(Obj, sx, 0) then
+        st := st or MOVE_BLOCK;
+
+  // Åñëè øàãíóòü â ñòîðîíó, à òàì ñòåíà => øàãàòü íåëüçÿ:
+    if g_Obj_CollideLevel(Obj, sx, 0) then
+      begin
+        if ClimbSlopes and (Abs(dy) < 2) then
+          begin
+            Result := True;
+            if (not g_Obj_CollideLevel(Obj, sx, -12)) // çàáèðàåìñÿ íà 12 ïèêñåëåé âëåâî/âïðàâî
+            and g_Obj_CollidePanel(Obj, 0, 1, PANEL_WALL or PANEL_STEP) // òîëüêî åñëè åñòü çåìëÿ ïîä íîãàìè
+            then
+              slope(-1)
+            else
+              begin
+                Result := False;
+                st := st or MOVE_HITWALL;
+              end;
+          end
+          else
+            st := st or MOVE_HITWALL;
+      end
+    else // Òàì ñòåíû íåò
+      begin
+        if CollideLiquid(Obj, sx, 0) then
+          begin // Åñëè øàãíóòü â ñòîðîíó, à òàì òåïåðü æèäêîñòü
+            if not WordBool(st and MOVE_INWATER) then
+              st := st or MOVE_HITWATER;
+          end
+        else // Åñëè øàãíóòü â ñòîðîíó, à òàì óæå íåò æèäêîñòè
+          if WordBool(st and MOVE_INWATER) then
+            st := st or MOVE_HITAIR;
+
+      // Øàã:
+        Obj^.X := Obj^.X + sx;
+        Result := True;
+      end;
+  end;
+
+  function movey(): Boolean;
+  begin
+    Result := False;
+
+  // Åñëè ìîíñòðó øàãíóòü ïî âåðòèêàëè, à òàì áëîêìîí:
+    if gMon and not WordBool(st and MOVE_BLOCK) then
+      if Blocked(Obj, 0, sy) then
+        st := st or MOVE_BLOCK;
+
+  // Åñëè øàãíóòü â ïî âåðòèêàëè, à òàì ñòåíà => øàãàòü íåëüçÿ:
+  // Èëè åñëè øàãíóòü âíèç, à òàì ñòóïåíü => øàãàòü íåëüçÿ:
+    if g_Obj_CollideLevel(Obj, 0, sy) or
+       ((sy > 0) and g_Obj_StayOnStep(Obj)) then
+      begin
+        if sy > 0 then
+          st := st or MOVE_HITLAND
+        else
+          st := st or MOVE_HITCEIL;
+      end
+    else // Òàì ñòåíû íåò. È ñòóïåíè ñíèçó òîæå íåò
+      begin
+        if CollideLiquid(Obj, 0, sy) then
+          begin // Åñëè øàãíóòü â ïî âåðòèêàëè, à òàì òåïåðü æèäêîñòü
+            if not WordBool(st and MOVE_INWATER) then
+              st := st or MOVE_HITWATER;
+          end
+        else // Åñëè øàãíóòü â ïî âåðòèêàëè, à òàì óæå íåò æèäêîñòè
+          if WordBool(st and MOVE_INWATER) then
+            st := st or MOVE_HITAIR;
+
+      // Øàã:
+        Obj^.Y := Obj^.Y + sy;
+
+        Result := True;
+      end;
+  end;
+
+begin
+  st := MOVE_NONE;
+
+// Îáúåêò â æèäêîñòè:
+  if CollideLiquid(Obj, 0, 0) then
+    st := st or MOVE_INWATER;
+
+// Ìîíñòð â áëîêìîíå:
+  if gMon then
+    if Blocked(Obj, 0, 0) then
+      st := st or MOVE_BLOCK;
+
+// Äâèãàòüñÿ íå íàäî:
+  if (dx = 0) and (dy = 0) then
+  begin
+    Result := st;
+    Exit;
+  end;
+
+  sx := g_basic.Sign(dx);
+  sy := g_basic.Sign(dy);
+  dx := Abs(dx);
+  dy := Abs(dy);
+
+  for i := 1 to dx do
+    if not movex() then
+      Break;
+
+  for i := 1 to dy do
+    if not movey() then
+      Break;
+
+  Result := st;
+end;
+
+procedure g_Obj_Init(Obj: PObj);
+begin
+  ZeroMemory(Obj, SizeOf(TObj));
+end;
+
+function g_Obj_Move(Obj: PObj; Fallable: Boolean; Splash: Boolean; ClimbSlopes: Boolean = False): Word;
+var
+  xv, yv, dx, dy: Integer;
+  inwater: Boolean;
+  c: Boolean;
+  wtx: DWORD;
+label
+  _move;
+begin
+// Ëèìèòû íà ñêîðîñòü è óñêîðåíèå
+  if      Obj^.Vel.X < -LIMIT_VEL then Obj^.Vel.X := -LIMIT_VEL
+  else if Obj^.Vel.X >  LIMIT_VEL then Obj^.Vel.X :=  LIMIT_VEL;
+  if      Obj^.Vel.Y < -LIMIT_VEL then Obj^.Vel.Y := -LIMIT_VEL
+  else if Obj^.Vel.Y >  LIMIT_VEL then Obj^.Vel.Y :=  LIMIT_VEL;
+  if      Obj^.Accel.X < -LIMIT_ACCEL then Obj^.Accel.X := -LIMIT_ACCEL
+  else if Obj^.Accel.X >  LIMIT_ACCEL then Obj^.Accel.X :=  LIMIT_ACCEL;
+  if      Obj^.Accel.Y < -LIMIT_ACCEL then Obj^.Accel.Y := -LIMIT_ACCEL
+  else if Obj^.Accel.Y >  LIMIT_ACCEL then Obj^.Accel.Y :=  LIMIT_ACCEL;
+
+// Âûëåòåë çà íèæíþþ ãðàíèöó êàðòû:
+  if Obj^.Y > gMapInfo.Height+128 then
+  begin
+    Result := MOVE_FALLOUT;
+    Exit;
+  end;
+
+// Ìåíÿåì ñêîðîñòü è óñêîðåíèå òîëüêî ïî ÷åòíûì êàäðàì:
+  c := gTime mod (GAME_TICK*2) <> 0;
+
+  if c then
+    goto _move;
+
+  case CollideLift(Obj, 0, 0) of
+    -1: //up
+      begin
+        Obj^.Vel.Y := Obj^.Vel.Y - 1; // Ëèôò ââåðõ
+        if Obj^.Vel.Y < -5 then
+          Obj^.Vel.Y := Obj^.Vel.Y + 1;
+      end;
+
+    1: //down
+      begin
+        if Obj^.Vel.Y > 5 then
+          Obj^.Vel.Y := Obj^.Vel.Y - 1;
+        Obj^.Vel.Y := Obj^.Vel.Y + 1; // Ãðàâèòàöèÿ èëè ëèôò âíèç
+      end;
+
+    0:
+      begin
+        if Fallable then
+          Obj^.Vel.Y := Obj^.Vel.Y + 1; // Ãðàâèòàöèÿ
+        if Obj^.Vel.Y > MAX_YV then
+          Obj^.Vel.Y := Obj^.Vel.Y - 1;
+      end;
+  end;
+
+  case CollideHorLift(Obj, 0, 0) of
+    -1: //left
+      begin
+        Obj^.Vel.X := Obj^.Vel.X - 3; // Ëèôò ââåðõ
+        if Obj^.Vel.X < -9 then
+          Obj^.Vel.X := Obj^.Vel.X + 3;
+      end;
+
+    1: //right
+      begin
+        Obj^.Vel.X := Obj^.Vel.X + 3;
+        if Obj^.Vel.X > 9 then
+          Obj^.Vel.X := Obj^.Vel.X - 3;
+      end;
+    // 0 is not needed here
+  end;
+
+  inwater := CollideLiquid(Obj, 0, 0);
+  if inwater then
+  begin
+    xv := Abs(Obj^.Vel.X)+1;
+    if xv > 5 then
+      Obj^.Vel.X := z_dec(Obj^.Vel.X, (xv div 2)-2);
+
+    yv := Abs(Obj^.Vel.Y)+1;
+    if yv > 5 then
+      Obj^.Vel.Y := z_dec(Obj^.Vel.Y, (yv div 2)-2);
+
+    xv := Abs(Obj^.Accel.X)+1;
+    if xv > 5 then
+      Obj^.Accel.X := z_dec(Obj^.Accel.X, (xv div 2)-2);
+
+    yv := Abs(Obj^.Accel.Y)+1;
+    if yv > 5 then
+      Obj^.Accel.Y := z_dec(Obj^.Accel.Y, (yv div 2)-2);
+  end;
+
+// Óìåíüøàåì ïðèáàâêó ê ñêîðîñòè:
+  Obj^.Accel.X := z_dec(Obj^.Accel.X, 1);
+  Obj^.Accel.Y := z_dec(Obj^.Accel.Y, 1);
+
+_move:
+
+  xv := Obj^.Vel.X + Obj^.Accel.X;
+  yv := Obj^.Vel.Y + Obj^.Accel.Y;
+
+  dx := xv;
+  dy := yv;
+
+  Result := move(Obj, dx, dy, ClimbSlopes);
+
+// Áðûçãè (åñëè íóæíû):
+  if Splash then
+    if WordBool(Result and MOVE_HITWATER) then
+    begin
+      wtx := g_Map_CollideLiquid_Texture(Obj^.X+Obj^.Rect.X,
+                                         Obj^.Y+Obj^.Rect.Y,
+                                         Obj^.Rect.Width,
+                                         Obj^.Rect.Height*2 div 3);
+      case wtx of
+        TEXTURE_SPECIAL_WATER:
+          g_Obj_Splash(Obj, 3);
+        TEXTURE_SPECIAL_ACID1:
+          g_Obj_Splash(Obj, 2);
+        TEXTURE_SPECIAL_ACID2:
+          g_Obj_Splash(Obj, 1);
+        TEXTURE_NONE:
+          ;
+        else
+          g_Obj_Splash(Obj, 0);
+      end;
+    end;
+
+// Ìåíÿåì ñêîðîñòü è óñêîðåíèå òîëüêî ïî ÷åòíûì êàäðàì:
+  if c then
+    Exit;
+
+// Âðåçàëèñü â ñòåíó - ñòîï:
+  if WordBool(Result and MOVE_HITWALL) then
+  begin
+    Obj^.Vel.X := 0;
+    Obj^.Accel.X := 0;
+  end;
+
+// Âðåçàëèñü â ïîë èëè ïîòîëîê - ñòîï:
+  if WordBool(Result and (MOVE_HITCEIL or MOVE_HITLAND)) then
+  begin
+    Obj^.Vel.Y := 0;
+    Obj^.Accel.Y := 0;
+  end;
+end;
+
+function g_Obj_Collide(Obj1, Obj2: PObj): Boolean;
+begin
+  Result := g_Collide(Obj1^.X+Obj1^.Rect.X, Obj1^.Y+Obj1^.Rect.Y,
+                      Obj1^.Rect.Width, Obj1^.Rect.Height,
+                      Obj2^.X+Obj2^.Rect.X, Obj2^.Y+Obj2^.Rect.Y,
+                      Obj2^.Rect.Width, Obj2^.Rect.Height);
+end;
+
+function g_Obj_Collide(X, Y: Integer; Width, Height: Word; Obj: PObj): Boolean;
+begin
+  Result := g_Collide(X, Y,
+                      Width, Height,
+                      Obj^.X+Obj^.Rect.X, Obj^.Y+Obj^.Rect.Y,
+                      Obj^.Rect.Width, Obj^.Rect.Height);
+end;
+
+function g_Obj_CollidePoint(X, Y: Integer; Obj: PObj): Boolean;
+begin
+  Result := g_CollidePoint(X, Y, Obj^.X+Obj^.Rect.X, Obj^.Y+Obj^.Rect.Y,
+                           Obj^.Rect.Width, Obj^.Rect.Height);
+end;
+
+procedure g_Obj_Push(Obj: PObj; VelX, VelY: Integer);
+begin
+  Obj^.Vel.X := Obj^.Vel.X + VelX;
+  Obj^.Vel.Y := Obj^.Vel.Y + VelY;
+end;
+
+procedure g_Obj_PushA(Obj: PObj; Vel: Integer; Angle: SmallInt);
+var
+  s, c: Extended;
+
+begin
+  SinCos(DegToRad(-Angle), s, c);
+
+  Obj^.Vel.X := Obj^.Vel.X + Round(Vel*c);
+  Obj^.Vel.Y := Obj^.Vel.Y + Round(Vel*s);
+end;
+
+procedure g_Obj_SetSpeed(Obj: PObj; s: Integer);
+var
+  m, vx, vy: Integer;
+begin
+  vx := Obj^.Vel.X;
+  vy := Obj^.Vel.Y;
+
+  m := Max(Abs(vx), Abs(vy));
+  if m = 0 then
+    m := 1;
+
+  Obj^.Vel.X := (vx*s) div m;
+  Obj^.Vel.Y := (vy*s) div m;
+end;
+
+function z_dec(a, b: Integer): Integer;
+begin
+// Ïðèáëèæàåì a ê 0 íà b åäèíèö:
+  if Abs(a) < b then
+    Result := 0
+  else
+    if a > 0 then
+      Result := a - b
+    else
+      if a < 0 then
+        Result := a + b
+      else // a = 0
+        Result := 0;
+end;
+
+function z_fdec(a, b: Double): Double;
+begin
+// Ïðèáëèæàåì a ê 0.0 íà b åäèíèö:
+  if Abs(a) < b then
+    Result := 0.0
+  else
+    if a > 0.0 then
+      Result := a - b
+    else
+      if a < 0.0 then
+        Result := a + b
+      else // a = 0.0
+        Result := 0.0;
+end;
+
+end.
diff --git a/src/game/g_player.pas b/src/game/g_player.pas
new file mode 100644 (file)
index 0000000..bb9b414
--- /dev/null
@@ -0,0 +1,7037 @@
+unit g_player;
+
+interface
+
+uses
+  e_graphics, g_playermodel, g_basic, g_textures,
+  g_weapons, g_phys, g_sound, g_saveload, MAPSTRUCT,
+  BinEditor, g_panel;
+
+const
+  KEY_LEFT       = 1;
+  KEY_RIGHT      = 2;
+  KEY_UP         = 3;
+  KEY_DOWN       = 4;
+  KEY_FIRE       = 5;
+  KEY_NEXTWEAPON = 6;
+  KEY_PREVWEAPON = 7;
+  KEY_OPEN       = 8;
+  KEY_JUMP       = 9;
+  KEY_CHAT       = 10;
+
+  R_ITEM_BACKPACK   = 0;
+  R_KEY_RED         = 1;
+  R_KEY_GREEN       = 2;
+  R_KEY_BLUE        = 3;
+  R_BERSERK         = 4;
+
+  MR_SUIT           = 0;
+  MR_INVUL          = 1;
+  MR_INVIS          = 2;
+  MR_MAX            = 2;
+
+  A_BULLETS         = 0;
+  A_SHELLS          = 1;
+  A_ROCKETS         = 2;
+  A_CELLS           = 3;
+
+  K_SIMPLEKILL      = 0;
+  K_HARDKILL        = 1;
+  K_EXTRAHARDKILL   = 2;
+  K_FALLKILL        = 3;
+
+  T_RESPAWN         = 0;
+  T_SWITCH          = 1;
+  T_USE             = 2;
+  T_FLAGCAP         = 3;
+
+  TEAM_NONE         = 0;
+  TEAM_RED          = 1;
+  TEAM_BLUE         = 2;
+  TEAM_COOP         = 3;
+
+  SHELL_BULLET      = 0;
+  SHELL_SHELL       = 1;
+  SHELL_DBLSHELL    = 2;
+
+  ANGLE_NONE        = Low(SmallInt);
+
+  CORPSE_STATE_REMOVEME = 0;
+  CORPSE_STATE_NORMAL   = 1;
+  CORPSE_STATE_MESS     = 2;
+
+  PLAYER_RECT: TRectWH = (X:15; Y:12; Width:34; Height:52);
+  PLAYER_RECT_CX       = 15+(34 div 2);
+  PLAYER_RECT_CY       = 12+(52 div 2);
+  PLAYER_CORPSERECT: TRectWH = (X:15; Y:48; Width:34; Height:16);
+
+  PLAYER_HP_SOFT  = 100;
+  PLAYER_HP_LIMIT = 200;
+  PLAYER_AP_SOFT  = 100;
+  PLAYER_AP_LIMIT = 200;
+  SUICIDE_DAMAGE  = 112;
+
+  PLAYER1_DEF_COLOR: TRGB = (R:64; G:175; B:48);
+  PLAYER2_DEF_COLOR: TRGB = (R:96; G:96; B:96);
+
+type
+  TPlayerStat = record
+    Ping: Word;
+    Loss: Byte;
+    Name: String;
+    Team: Byte;
+    Frags: SmallInt;
+    Deaths: SmallInt;
+    Lives: Byte;
+    Kills: Word;
+    Color: TRGB;
+    Spectator: Boolean;
+  end;
+
+  TPlayerStatArray = Array of TPlayerStat;
+
+  TPlayerSavedState = record
+    Health:     Integer;
+    Armor:      Integer;
+    Air:        Integer;
+    JetFuel:    Integer;
+    CurrWeap:   Byte;
+    Ammo:       Array [A_BULLETS..A_CELLS] of Word;
+    MaxAmmo:    Array [A_BULLETS..A_CELLS] of Word;
+    Weapon:     Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Boolean;
+    Rulez:      Set of R_ITEM_BACKPACK..R_BERSERK;
+    WaitRecall: Boolean;
+  end;
+
+  TKeyState = record
+    Pressed: Boolean;
+    Time: Word;
+  end;
+
+  TPlayer = class (TObject)
+  private
+    FIamBot:    Boolean;
+    FUID:       Word;
+    FName:      String;
+    FTeam:      Byte;
+    FLive:      Boolean;
+    FSpawned:   Boolean;
+    FDirection: TDirection;
+    FHealth:    Integer;
+    FLives:     Byte;
+    FArmor:     Integer;
+    FAir:       Integer;
+    FPain:      Integer;
+    FPickup:    Integer;
+    FKills:     Integer;
+    FMonsterKills: Integer;
+    FFrags:     Integer;
+    FFragCombo: Byte;
+    FLastFrag:  LongWord;
+    FComboEvnt: Integer;
+    FDeath:     Integer;
+    FCanJetpack: Boolean;
+    FJetFuel:   Integer;
+    FFlag:      Byte;
+    FSecrets:   Integer;
+    FCurrWeap:  Byte;
+    FBFGFireCounter: SmallInt;
+    FLastSpawnerUID: Word;
+    FLastHit:   Byte;
+    FObj:       TObj;
+    FXTo, FYTo: Integer;
+    FSpectatePlayer: Integer;
+
+    FSavedState: TPlayerSavedState;
+
+    FModel:     TPlayerModel;
+    FActionPrior:    Byte;
+    FActionAnim:     Byte;
+    FActionForce:    Boolean;
+    FActionChanged:  Boolean;
+    FAngle:     SmallInt;
+    FFireAngle: SmallInt;
+    FIncCam:         Integer;
+    FShellTimer:     Integer;
+    FShellType:      Byte;
+    FSawSound:       TPlayableSound;
+    FSawSoundIdle:   TPlayableSound;
+    FSawSoundHit:    TPlayableSound;
+    FSawSoundSelect: TPlayableSound;
+    FJetSoundOn:     TPlayableSound;
+    FJetSoundOff:    TPlayableSound;
+    FJetSoundFly:    TPlayableSound;
+    FGodMode:   Boolean;
+    FNoTarget:  Boolean;
+    FNoReload:  Boolean;
+    FJustTeleported: Boolean;
+    FNetTime: LongWord;
+
+    function    CollideLevel(XInc, YInc: Integer): Boolean;
+    function    StayOnStep(XInc, YInc: Integer): Boolean;
+    function    HeadInLiquid(XInc, YInc: Integer): Boolean;
+    function    BodyInLiquid(XInc, YInc: Integer): Boolean;
+    function    BodyInAcid(XInc, YInc: Integer): Boolean;
+    function    FullInLift(XInc, YInc: Integer): Integer;
+    {procedure   CollideItem();}
+    procedure   FlySmoke(Times: DWORD = 1);
+    function    GetAmmoByWeapon(Weapon: Byte): Word;
+    procedure   SetAction(Action: Byte; Force: Boolean = False);
+    procedure   OnDamage(Angle: SmallInt); virtual;
+    function    firediry(): Integer;
+
+    procedure   Run(Direction: TDirection);
+    procedure   NextWeapon();
+    procedure   PrevWeapon();
+    procedure   SeeUp();
+    procedure   SeeDown();
+    procedure   Fire();
+    procedure   Jump();
+    procedure   Use();
+
+  public
+    FDamageBuffer:   Integer;
+
+    FAmmo:      Array [A_BULLETS..A_CELLS] of Word;
+    FMaxAmmo:   Array [A_BULLETS..A_CELLS] of Word;
+    FWeapon:    Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Boolean;
+    FRulez:     Set of R_ITEM_BACKPACK..R_BERSERK;
+    FBerserk:   Integer;
+    FMegaRulez: Array [MR_SUIT..MR_MAX] of DWORD;
+    FReloading: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Word;
+    FTime:      Array [T_RESPAWN..T_FLAGCAP] of DWORD;
+    FKeys:      Array [KEY_LEFT..KEY_CHAT] of TKeyState;
+    FColor:     TRGB;
+    FPreferredTeam: Byte;
+    FSpectator: Boolean;
+    FNoRespawn: Boolean;
+    FWantsInGame: Boolean;
+    FGhost:     Boolean;
+    FPhysics:   Boolean;
+    FJetpack:   Boolean;
+    FActualModelName: string;
+    FClientID:  SmallInt;
+    FPing:      Word;
+    FLoss:      Byte;
+    FDummy:     Boolean;
+
+    constructor Create(); virtual;
+    destructor  Destroy(); override;
+    procedure   Respawn(Silent: Boolean; Force: Boolean = False); virtual;
+    function    GetRespawnPoint(): Byte;
+    procedure   PressKey(Key: Byte; Time: Word = 1);
+    procedure   ReleaseKeys();
+    procedure   SetModel(ModelName: String);
+    procedure   SetColor(Color: TRGB);
+    procedure   SetWeapon(W: Byte);
+    function    IsKeyPressed(K: Byte): Boolean;
+    function    GetKeys(): Byte;
+    function    PickItem(ItemType: Byte; respawn: Boolean; var remove: Boolean): Boolean; virtual;
+    function    Collide(X, Y: Integer; Width, Height: Word): Boolean; overload;
+    function    Collide(Panel: TPanel): Boolean; overload;
+    function    Collide(X, Y: Integer): Boolean; overload;
+    procedure   SetDirection(Direction: TDirection);
+    procedure   GetSecret();
+    function    TeleportTo(X, Y: Integer; silent: Boolean; dir: Byte): Boolean;
+    procedure   Touch();
+    procedure   Push(vx, vy: Integer);
+    procedure   ChangeModel(ModelName: String);
+    procedure   SwitchTeam;
+    procedure   ChangeTeam(Team: Byte);
+    procedure   BFGHit();
+    function    GetFlag(Flag: Byte): Boolean;
+    procedure   SetFlag(Flag: Byte);
+    function    DropFlag(): Boolean;
+    procedure   AllRulez(Health: Boolean);
+    procedure   FragCombo();
+    procedure   GiveItem(ItemType: Byte);
+    procedure   Damage(value: Word; SpawnerUID: Word; vx, vy: Integer; t: Byte); virtual;
+    function    Heal(value: Word; Soft: Boolean): Boolean; virtual;
+    procedure   MakeBloodVector(Count: Word; VelX, VelY: Integer);
+    procedure   MakeBloodSimple(Count: Word);
+    procedure   Kill(KillType: Byte; SpawnerUID: Word; t: Byte);
+    procedure   Reset(Force: Boolean);
+    procedure   Spectate(NoMove: Boolean = False);
+    procedure   SwitchNoClip;
+    procedure   SoftReset();
+    procedure   Draw(); virtual;
+    procedure   DrawPain();
+    procedure   DrawPickup();
+    procedure   DrawRulez();
+    procedure   DrawAim();
+    procedure   DrawBubble();
+    procedure   DrawGUI();
+    procedure   Update(); virtual;
+    procedure   RememberState();
+    procedure   RecallState();
+    procedure   SaveState(var Mem: TBinMemoryWriter); virtual;
+    procedure   LoadState(var Mem: TBinMemoryReader); virtual;
+    procedure   PauseSounds(Enable: Boolean);
+    procedure   NetFire(Wpn: Byte; X, Y, AX, AY: Integer; WID: Integer = -1);
+    procedure   DoLerp(Level: Integer = 2);
+    procedure   SetLerp(XTo, YTo: Integer);
+    procedure   JetpackOn;
+    procedure   JetpackOff;
+
+    property    Name: String read FName write FName;
+    property    Model: TPlayerModel read FModel;
+    property    Health: Integer read FHealth write FHealth;
+    property    Lives: Byte read FLives write FLives;
+    property    Armor: Integer read FArmor write FArmor;
+    property    Air:   Integer read FAir write FAir;
+    property    JetFuel: Integer read FJetFuel write FJetFuel;
+    property    Frags: Integer read FFrags write FFrags;
+    property    Death: Integer read FDeath write FDeath;
+    property    Kills: Integer read FKills write FKills;
+    property    CurrWeap: Byte read FCurrWeap write FCurrWeap;
+    property    MonsterKills: Integer read FMonsterKills write FMonsterKills;
+    property    Secrets: Integer read FSecrets;
+    property    GodMode: Boolean read FGodMode write FGodMode;
+    property    NoTarget: Boolean read FNoTarget write FNoTarget;
+    property    NoReload: Boolean read FNoReload write FNoReload;
+    property    Live: Boolean read FLive write FLive;
+    property    Flag: Byte read FFlag;
+    property    Team: Byte read FTeam write FTeam;
+    property    Direction: TDirection read FDirection;
+    property    GameX: Integer read FObj.X write FObj.X;
+    property    GameY: Integer read FObj.Y write FObj.Y;
+    property    GameVelX: Integer read FObj.Vel.X write FObj.Vel.X;
+    property    GameVelY: Integer read FObj.Vel.Y write FObj.Vel.Y;
+    property    GameAccelX: Integer read FObj.Accel.X write FObj.Accel.X;
+    property    GameAccelY: Integer read FObj.Accel.Y write FObj.Accel.Y;
+    property    Vel: TPoint2i read FObj.Vel;
+    property    Obj: TObj read FObj;
+    property    IncCam: Integer read FIncCam write FIncCam;
+    property    UID: Word read FUID write FUID;
+    property    JustTeleported: Boolean read FJustTeleported write FJustTeleported;
+    property    NetTime: LongWord read FNetTime write FNetTime;
+  end;
+
+  TDifficult = record
+    DiagFire: Byte;
+    InvisFire: Byte;
+    DiagPrecision: Byte;
+    FlyPrecision: Byte;
+    Cover: Byte;
+    CloseJump: Byte;
+    WeaponPrior: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte;
+    CloseWeaponPrior: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte;
+    //SafeWeaponPrior: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte;
+  end;
+
+  TAIFlag = record
+    Name: String;
+    Value: String;
+  end;
+
+  TBot = class (TPlayer)
+  private
+    FSelectedWeapon:  Byte;
+    FTargetUID:       Word;
+    FLastVisible:     DWORD;
+    FAIFlags:         Array of TAIFlag;
+    FDifficult:       TDifficult;
+
+    function    GetRnd(a: Byte): Boolean;
+    function    GetInterval(a: Byte; radius: SmallInt): SmallInt;
+    function    RunDirection(): TDirection;
+    function    FullInStep(XInc, YInc: Integer): Boolean;
+    //function    NeedItem(Item: Byte): Byte;
+    procedure   SelectWeapon(Dist: Integer);
+    procedure   SetAIFlag(fName, fValue: String20);
+    function    GetAIFlag(fName: String20): String20;
+    procedure   RemoveAIFlag(fName: String20);
+    function    Healthy(): Byte;
+    procedure   UpdateMove();
+    procedure   UpdateCombat();
+    function    KeyPressed(Key: Word): Boolean;
+    procedure   ReleaseKey(Key: Byte);
+    function    TargetOnScreen(TX, TY: Integer): Boolean;
+    procedure   OnDamage(Angle: SmallInt); override;
+
+  public
+    procedure   Respawn(Silent: Boolean; Force: Boolean = False); override;
+    constructor Create(); override;
+    destructor  Destroy(); override;
+    procedure   Draw(); override;
+    function    PickItem(ItemType: Byte; force: Boolean; var remove: Boolean): Boolean; override;
+    function    Heal(value: Word; Soft: Boolean): Boolean; override;
+    procedure   Update(); override;
+    procedure   SaveState(var Mem: TBinMemoryWriter); override;
+    procedure   LoadState(var Mem: TBinMemoryReader); override;
+  end;
+
+  TGib = record
+    Live:     Boolean;
+    ID:       DWORD;
+    MaskID:   DWORD;
+    RAngle:   Integer;
+    Color:    TRGB;
+    Obj:      TObj;
+  end;
+
+  TShell = record
+    SpriteID: DWORD;
+    Live:     Boolean;
+    SType:    Byte;
+    RAngle:   Integer;
+    Timeout:  Cardinal;
+    CX, CY:   Integer;
+    Obj:      TObj;
+  end;
+
+  TCorpse = class (TObject)
+  private
+    FModelName:     String;
+    FMess:          Boolean;
+    FState:         Byte;
+    FDamage:        Byte;
+    FColor:         TRGB;
+    FObj:           TObj;
+    FAnimation:     TAnimation;
+    FAnimationMask: TAnimation;
+
+  public
+    constructor Create(X, Y: Integer; ModelName: String; aMess: Boolean);
+    destructor  Destroy(); override;
+    procedure   Damage(Value: Word; vx, vy: Integer);
+    procedure   Update();
+    procedure   Draw();
+    procedure   SaveState(var Mem: TBinMemoryWriter);
+    procedure   LoadState(var Mem: TBinMemoryReader);
+    
+    property    Obj: TObj read FObj;
+    property    State: Byte read FState;
+    property    Mess: Boolean read FMess;
+  end;
+
+  TTeamStat = Array [TEAM_RED..TEAM_BLUE] of
+    record
+      Goals: SmallInt;
+    end;
+
+var
+  gPlayers: Array of TPlayer;
+  gCorpses: Array of TCorpse;
+  gGibs: Array of TGib;
+  gShells: Array of TShell;
+  gTeamStat: TTeamStat;
+  gFly: Boolean = False;
+  gAimLine: Boolean = False;
+  gChatBubble: Byte = 0;
+  gNumBots: Word = 0;
+  gLMSPID1: Word = 0;
+  gLMSPID2: Word = 0;
+  MAX_RUNVEL: Integer = 8;
+  VEL_JUMP: Integer = 10;
+  SHELL_TIMEOUT: Cardinal = 60000;
+
+function  Lerp(X, Y, Factor: Integer): Integer;
+
+procedure g_Gibs_SetMax(Count: Word);
+function  g_Gibs_GetMax(): Word;
+procedure g_Corpses_SetMax(Count: Word);
+function  g_Corpses_GetMax(): Word;
+procedure g_Shells_SetMax(Count: Word);
+function  g_Shells_GetMax(): Word;
+
+procedure g_Player_Init();
+procedure g_Player_Free();
+function  g_Player_Create(ModelName: String; Color: TRGB; Team: Byte; Bot: Boolean): Word;
+function  g_Player_CreateFromState(var Mem: TBinMemoryReader): Word;
+procedure g_Player_Remove(UID: Word);
+procedure g_Player_ResetTeams();
+procedure g_Player_UpdateAll();
+procedure g_Player_DrawAll();
+procedure g_Player_DrawDebug(p: TPlayer);
+procedure g_Player_DrawHealth();
+procedure g_Player_RememberAll();
+procedure g_Player_ResetAll(Force, Silent: Boolean);
+function  g_Player_Get(UID: Word): TPlayer;
+function  g_Player_GetCount(): Byte;
+function  g_Player_GetStats(): TPlayerStatArray;
+function  g_Player_ValidName(Name: String): Boolean;
+procedure g_Player_CreateCorpse(Player: TPlayer);
+procedure g_Player_CreateGibs(fX, fY: Integer; ModelName: String; fColor: TRGB);
+procedure g_Player_CreateShell(fX, fY, dX, dY: Integer; T: Byte);
+procedure g_Player_UpdatePhysicalObjects();
+procedure g_Player_DrawCorpses();
+procedure g_Player_DrawShells();
+procedure g_Player_RemoveAllCorpses();
+procedure g_Player_Corpses_SaveState(var Mem: TBinMemoryWriter);
+procedure g_Player_Corpses_LoadState(var Mem: TBinMemoryReader);
+procedure g_Bot_Add(Team, Difficult: Byte);
+procedure g_Bot_AddList(Team: Byte; lname: ShortString; num: Integer = -1);
+procedure g_Bot_MixNames();
+procedure g_Bot_RemoveAll();
+
+implementation
+
+uses
+  e_log, g_map, g_items, g_console, SysUtils, g_gfx, Math,
+  g_options, g_triggers, g_menu, MAPDEF, g_game,
+  WADEDITOR, g_main, g_monsters, CONFIG, g_language, g_net, g_netmsg;
+
+type
+  TBotProfile = record
+    name: ShortString;
+    model: ShortString;
+    team: Byte;
+    color: TRGB;
+    diag_fire: Byte;
+    invis_fire: Byte;
+    diag_precision: Byte;
+    fly_precision: Byte;
+    cover: Byte;
+    close_jump: Byte;
+    w_prior1: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte;
+    w_prior2: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte;
+    w_prior3: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte;
+  end;
+
+const
+  TIME_RESPAWN1 = 1500;
+  TIME_RESPAWN2 = 2000;
+  TIME_RESPAWN3 = 3000;
+  AIR_DEF = 360;
+  AIR_MAX = 1091;
+  JET_MAX = 540; // ~30 sec
+  PLAYER_SUIT_TIME    = 30000;
+  PLAYER_INVUL_TIME   = 30000;
+  PLAYER_INVIS_TIME   = 35000;
+  FRAG_COMBO_TIME = 3000;
+  VEL_SW  = 4;
+  VEL_FLY = 6;
+  ANGLE_RIGHTUP   = 55;
+  ANGLE_RIGHTDOWN = -35;
+  ANGLE_LEFTUP    = 125;
+  ANGLE_LEFTDOWN  = -145;
+  PLAYER_HEADRECT: TRectWH = (X:24; Y:12; Width:20; Height:12);
+  WEAPONPOINT: Array [TDirection] of TPoint = ((X:16; Y:32), (X:47; Y:32));
+  BOT_MAXJUMP = 84;
+  BOT_LONGDIST   = 300;
+  BOT_UNSAFEDIST = 128;
+  TEAMCOLOR: Array [TEAM_RED..TEAM_BLUE] of TRGB = ((R:255; G:0; B:0),
+                                                   (R:0; G:0; B:255));
+  DIFFICULT_EASY: TDifficult = (DiagFire: 32; InvisFire: 32; DiagPrecision: 32;
+                                FlyPrecision: 32; Cover: 32; CloseJump: 32);
+  DIFFICULT_MEDIUM: TDifficult = (DiagFire: 127; InvisFire: 127; DiagPrecision: 127;
+                                  FlyPrecision: 127; Cover: 127; CloseJump: 127);
+  DIFFICULT_HARD: TDifficult = (DiagFire: 255; InvisFire: 255; DiagPrecision: 255;
+                                FlyPrecision: 255; Cover: 255; CloseJump: 255);
+  WEAPON_PRIOR1: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte =
+                                (WEAPON_SUPERPULEMET, WEAPON_SHOTGUN2, WEAPON_SHOTGUN1,
+                                 WEAPON_CHAINGUN, WEAPON_PLASMA, WEAPON_ROCKETLAUNCHER,
+                                 WEAPON_BFG, WEAPON_PISTOL, WEAPON_SAW, WEAPON_KASTET);
+  WEAPON_PRIOR2: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte =
+                                (WEAPON_SUPERPULEMET, WEAPON_BFG, WEAPON_ROCKETLAUNCHER,
+                                 WEAPON_SHOTGUN2, WEAPON_PLASMA, WEAPON_SHOTGUN1,
+                                 WEAPON_CHAINGUN, WEAPON_PISTOL, WEAPON_SAW, WEAPON_KASTET);
+  //WEAPON_PRIOR3: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte =
+  //                              (WEAPON_SUPERPULEMET, WEAPON_BFG, WEAPON_PLASMA,
+  //                               WEAPON_SHOTGUN2, WEAPON_CHAINGUN, WEAPON_SHOTGUN1,
+  //                               WEAPON_SAW, WEAPON_ROCKETLAUNCHER, WEAPON_PISTOL, WEAPON_KASTET);
+  WEAPON_RELOAD: Array [WEAPON_KASTET..WEAPON_SUPERPULEMET] of Byte =
+                                (5, 2, 6, 18, 36, 2, 12, 2, 14, 2);
+
+  PLAYER_SIGNATURE = $52594C50; // 'PLYR'
+  CORPSE_SIGNATURE = $50524F43; // 'CORP'
+
+  BOTNAMES_FILENAME = 'botnames.txt';
+  BOTLIST_FILENAME = 'botlist.txt';
+
+var
+  MaxGibs: Word = 150;
+  MaxCorpses: Word = 20;
+  MaxShells: Word = 300;
+  CurrentGib: Integer = 0;
+  CurrentShell: Integer = 0;
+  BotNames: Array of String;
+  BotList: Array of TBotProfile;
+
+function Lerp(X, Y, Factor: Integer): Integer;
+begin
+  Result := X + ((Y - X) div Factor);
+end;
+
+function SameTeam(UID1, UID2: Word): Boolean;
+begin
+  Result := False;
+
+  if (UID1 > UID_MAX_PLAYER) or (UID1 <= UID_MAX_GAME) or
+     (UID2 > UID_MAX_PLAYER) or (UID2 <= UID_MAX_GAME) then Exit;
+
+  if (g_Player_Get(UID1) = nil) or (g_Player_Get(UID2) = nil) then Exit;
+
+  if ((g_Player_Get(UID1).Team = TEAM_NONE) or
+      (g_Player_Get(UID2).Team = TEAM_NONE)) then Exit;
+
+  Result := g_Player_Get(UID1).FTeam = g_Player_Get(UID2).FTeam;
+end;
+
+procedure g_Gibs_SetMax(Count: Word);
+begin
+  MaxGibs := Count;
+  SetLength(gGibs, Count);
+
+  if CurrentGib >= Count then
+    CurrentGib := 0;
+end;
+
+function g_Gibs_GetMax(): Word;
+begin
+  Result := MaxGibs;
+end;
+
+procedure g_Shells_SetMax(Count: Word);
+begin
+  MaxShells := Count;
+  SetLength(gShells, Count);
+
+  if CurrentShell >= Count then
+    CurrentShell := 0;
+end;
+
+function g_Shells_GetMax(): Word;
+begin
+  Result := MaxShells;
+end;
+
+
+procedure g_Corpses_SetMax(Count: Word);
+begin
+  MaxCorpses := Count;
+  SetLength(gCorpses, Count);
+end;
+
+function g_Corpses_GetMax(): Word;
+begin
+  Result := MaxCorpses;
+end;
+
+function g_Player_Create(ModelName: String; Color: TRGB; Team: Byte; Bot: Boolean): Word;
+var
+  a: Integer;
+  ok: Boolean;
+begin
+  Result := 0;
+
+  ok := False;
+  a := 0;
+
+// Åñòü ëè ìåñòî â gPlayers:
+  if gPlayers <> nil then
+    for a := 0 to High(gPlayers) do
+      if gPlayers[a] = nil then
+      begin
+        ok := True;
+        Break;
+      end;
+
+// Íåò ìåñòà - ðàñøèðÿåì gPlayers:
+  if not ok then
+  begin
+    SetLength(gPlayers, Length(gPlayers)+1);
+    a := High(gPlayers);
+  end;
+
+// Ñîçäàåì îáúåêò èãðîêà:
+  if Bot then
+    gPlayers[a] := TBot.Create()
+  else
+    gPlayers[a] := TPlayer.Create();
+
+
+  gPlayers[a].FActualModelName := ModelName;
+  gPlayers[a].SetModel(ModelName);
+
+// Íåò ìîäåëè - ñîçäàíèå íå âîçìîæíî:
+  if gPlayers[a].FModel = nil then
+  begin
+    gPlayers[a].Free();
+    gPlayers[a] := nil;
+    g_FatalError(Format(_lc[I_GAME_ERROR_MODEL], [ModelName]));
+    Exit;
+  end;
+
+  if not (Team in [TEAM_RED, TEAM_BLUE]) then
+    if Random(2) = 0 then
+      Team := TEAM_RED
+    else
+      Team := TEAM_BLUE;
+  gPlayers[a].FPreferredTeam := Team;
+
+  case gGameSettings.GameMode of
+    GM_DM: gPlayers[a].FTeam := TEAM_NONE;
+    GM_TDM,
+    GM_CTF: gPlayers[a].FTeam := gPlayers[a].FPreferredTeam;
+    GM_SINGLE,
+    GM_COOP: gPlayers[a].FTeam := TEAM_COOP;
+  end;
+
+// Åñëè êîìàíäíàÿ èãðà - êðàñèì ìîäåëü â öâåò êîìàíäû:
+  gPlayers[a].FColor := Color;
+  if gPlayers[a].FTeam in [TEAM_RED, TEAM_BLUE] then
+    gPlayers[a].FModel.Color := TEAMCOLOR[gPlayers[a].FTeam]
+  else
+    gPlayers[a].FModel.Color := Color;
+
+  gPlayers[a].FUID := g_CreateUID(UID_PLAYER);
+  gPlayers[a].FLive := False;
+
+  Result := gPlayers[a].FUID;
+end;
+
+function g_Player_CreateFromState(var Mem: TBinMemoryReader): Word;
+var
+  a, i: Integer;
+  ok, Bot: Boolean;
+  sig: DWORD;
+  b: Byte;
+begin
+  Result := 0;
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà èãðîêà:
+  Mem.ReadDWORD(sig);
+  if sig <> PLAYER_SIGNATURE then // 'PLYR'
+  begin
+    raise EBinSizeError.Create('g_Player_CreateFromState: Wrong Player Signature');
+  end;
+
+// Áîò èëè ÷åëîâåê:
+  Mem.ReadBoolean(Bot);
+
+  ok := False;
+  a := 0;
+
+// Åñòü ëè ìåñòî â gPlayers:
+  if gPlayers <> nil then
+    for a := 0 to High(gPlayers) do
+      if gPlayers[a] = nil then
+      begin
+        ok := True;
+        Break;
+      end;
+
+// Íåò ìåñòà - ðàñøèðÿåì gPlayers:
+  if not ok then
+  begin
+    SetLength(gPlayers, Length(gPlayers)+1);
+    a := High(gPlayers);
+  end;
+
+// Ñîçäàåì îáúåêò èãðîêà:
+  if Bot then
+    gPlayers[a] := TBot.Create()
+  else
+    gPlayers[a] := TPlayer.Create();
+  gPlayers[a].FIamBot := Bot;
+  gPlayers[a].FPhysics := True;
+
+// UID èãðîêà:
+  Mem.ReadWord(gPlayers[a].FUID);
+// Èìÿ èãðîêà:
+  Mem.ReadString(gPlayers[a].FName);
+// Êîìàíäà:
+  Mem.ReadByte(gPlayers[a].FTeam);
+  gPlayers[a].FPreferredTeam := gPlayers[a].FTeam;
+// Æèâ ëè:
+  Mem.ReadBoolean(gPlayers[a].FLive);
+// Èçðàñõîäîâàë ëè âñå æèçíè:
+  Mem.ReadBoolean(gPlayers[a].FNoRespawn);
+// Íàïðàâëåíèå:
+  Mem.ReadByte(b);
+  if b = 1 then
+    gPlayers[a].FDirection := D_LEFT
+  else // b = 2
+    gPlayers[a].FDirection := D_RIGHT;
+// Çäîðîâüå:
+  Mem.ReadInt(gPlayers[a].FHealth);
+// Æèçíè:
+  Mem.ReadByte(gPlayers[a].FLives);
+// Áðîíÿ:
+  Mem.ReadInt(gPlayers[a].FArmor);
+// Çàïàñ âîçäóõà:
+  Mem.ReadInt(gPlayers[a].FAir);
+// Çàïàñ ãîðþ÷åãî:
+  Mem.ReadInt(gPlayers[a].FJetFuel);
+// Áîëü:
+  Mem.ReadInt(gPlayers[a].FPain);
+// Óáèë:
+  Mem.ReadInt(gPlayers[a].FKills);
+// Óáèë ìîíñòðîâ:
+  Mem.ReadInt(gPlayers[a].FMonsterKills);
+// Ôðàãîâ:
+  Mem.ReadInt(gPlayers[a].FFrags);
+// Ôðàãîâ ïîäðÿä:
+  Mem.ReadByte(gPlayers[a].FFragCombo);
+// Âðåìÿ ïîñëåäíåãî ôðàãà:
+  Mem.ReadDWORD(gPlayers[a].FLastFrag);
+// Ñìåðòåé:
+  Mem.ReadInt(gPlayers[a].FDeath);
+// Êàêîé ôëàã íåñåò:
+  Mem.ReadByte(gPlayers[a].FFlag);
+// Íàøåë ñåêðåòîâ:
+  Mem.ReadInt(gPlayers[a].FSecrets);
+// Òåêóùåå îðóæèå:
+  Mem.ReadByte(gPlayers[a].FCurrWeap);
+// Âðåìÿ çàðÿäêè BFG:
+  Mem.ReadSmallInt(gPlayers[a].FBFGFireCounter);
+// Áóôåð óðîíà:
+  Mem.ReadInt(gPlayers[a].FDamageBuffer);
+// Ïîñëåäíèé óäàðèâøèé:
+  Mem.ReadWord(gPlayers[a].FLastSpawnerUID);
+// Òèï ïîñëåäíåãî ïîëó÷åííîãî óðîíà:
+  Mem.ReadByte(gPlayers[a].FLastHit);
+// Îáúåêò èãðîêà:
+  Obj_LoadState(@gPlayers[a].FObj, Mem);
+// Òåêóùåå êîëè÷åñòâî ïàòðîíîâ:
+  for i := A_BULLETS to A_CELLS do
+    Mem.ReadWord(gPlayers[a].FAmmo[i]);
+// Ìàêñèìàëüíîå êîëè÷åñòâî ïàòðîíîâ:
+  for i := A_BULLETS to A_CELLS do
+    Mem.ReadWord(gPlayers[a].FMaxAmmo[i]);
+// Íàëè÷èå îðóæèÿ:
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    Mem.ReadBoolean(gPlayers[a].FWeapon[i]);
+// Âðåìÿ ïåðåçàðÿäêè îðóæèÿ:
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    Mem.ReadWord(gPlayers[a].FReloading[i]);
+// Íàëè÷èå ðþêçàêà:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(gPlayers[a].FRulez, R_ITEM_BACKPACK);
+// Íàëè÷èå êðàñíîãî êëþ÷à:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(gPlayers[a].FRulez, R_KEY_RED);
+// Íàëè÷èå çåëåíîãî êëþ÷à:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(gPlayers[a].FRulez, R_KEY_GREEN);
+// Íàëè÷èå ñèíåãî êëþ÷à:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(gPlayers[a].FRulez, R_KEY_BLUE);
+// Íàëè÷èå áåðñåðêà:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(gPlayers[a].FRulez, R_BERSERK);
+// Âðåìÿ äåéñòâèÿ ñïåöèàëüíûõ ïðåäìåòîâ:
+  for i := MR_SUIT to MR_MAX do
+    Mem.ReadDWORD(gPlayers[a].FMegaRulez[i]);
+// Âðåìÿ äî ïîâòîðíîãî ðåñïàóíà, ñìåíû îðóæèÿ, èñîëüçîâàíèÿ, çàõâàòà ôëàãà:
+  for i := T_RESPAWN to T_FLAGCAP do
+    Mem.ReadDWORD(gPlayers[a].FTime[i]);
+
+// Íàçâàíèå ìîäåëè:
+  Mem.ReadString(gPlayers[a].FActualModelName);
+// Öâåò ìîäåëè:
+  Mem.ReadByte(gPlayers[a].FColor.R);
+  Mem.ReadByte(gPlayers[a].FColor.G);
+  Mem.ReadByte(gPlayers[a].FColor.B);
+// Îáíîâëÿåì ìîäåëü èãðîêà:
+  gPlayers[a].SetModel(gPlayers[a].FActualModelName);
+
+// Íåò ìîäåëè - ñîçäàíèå íå âîçìîæíî:
+  if gPlayers[a].FModel = nil then
+  begin
+    gPlayers[a].Free();
+    gPlayers[a] := nil;
+    g_FatalError(Format(_lc[I_GAME_ERROR_MODEL], [gPlayers[a].FActualModelName]));
+    Exit;
+  end;
+
+// Åñëè êîìàíäíàÿ èãðà - êðàñèì ìîäåëü â öâåò êîìàíäû:
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+    gPlayers[a].FModel.Color := TEAMCOLOR[gPlayers[a].FTeam]
+  else
+    gPlayers[a].FModel.Color := gPlayers[a].FColor;
+
+  Result := gPlayers[a].FUID;
+end;
+
+procedure g_Player_ResetTeams();
+var
+  a: Integer;
+begin
+  if g_Game_IsClient then
+    Exit;
+  if gPlayers = nil then
+    Exit;
+  for a := Low(gPlayers) to High(gPlayers) do
+    if gPlayers[a] <> nil then
+      case gGameSettings.GameMode of
+        GM_DM:
+          gPlayers[a].ChangeTeam(TEAM_NONE);
+        GM_TDM, GM_CTF:
+          if not (gPlayers[a].Team in [TEAM_RED, TEAM_BLUE]) then
+            if gPlayers[a].FPreferredTeam in [TEAM_RED, TEAM_BLUE] then
+              gPlayers[a].ChangeTeam(gPlayers[a].FPreferredTeam)
+            else
+              if a mod 2 = 0 then
+                gPlayers[a].ChangeTeam(TEAM_RED)
+              else
+                gPlayers[a].ChangeTeam(TEAM_BLUE);
+        GM_SINGLE,
+        GM_COOP:
+          gPlayers[a].ChangeTeam(TEAM_COOP);
+      end;
+end;
+
+procedure g_Bot_Add(Team, Difficult: Byte);
+var
+  m: SArray;
+  _name, _model: String;
+  a, tr, tb: Integer;
+begin
+  if not g_Game_IsServer then Exit;
+
+// Ñïèñîê íàçâàíèé ìîäåëåé:
+  m := g_PlayerModel_GetNames();
+  if m = nil then
+    Exit;
+
+// Êîìàíäà:
+  if (gGameSettings.GameType = GT_SINGLE) or (gGameSettings.GameMode = GM_COOP) then
+    Team := TEAM_COOP // COOP
+  else
+    if gGameSettings.GameMode = GM_DM then
+      Team := TEAM_NONE // DM
+    else
+      if Team = TEAM_NONE then // CTF / TDM
+      begin
+       // Àâòîáàëàíñ êîìàíä:
+        tr := 0;
+        tb := 0;
+
+        for a := 0 to High(gPlayers) do
+          if gPlayers[a] <> nil then
+          begin
+            if gPlayers[a].Team = TEAM_RED then
+              Inc(tr)
+            else
+              if gPlayers[a].Team = TEAM_BLUE then
+                Inc(tb);
+          end;
+
+        if tr > tb then
+          Team := TEAM_BLUE
+        else
+          if tb > tr then
+            Team := TEAM_RED
+          else // tr = tb
+            if Random(2) = 0 then
+              Team := TEAM_RED
+            else
+              Team := TEAM_BLUE;
+      end;
+
+// Âûáèðàåì áîòó èìÿ:
+  _name := '';
+  if BotNames <> nil then
+    for a := 0 to High(BotNames) do
+      if g_Player_ValidName(BotNames[a]) then
+      begin
+        _name := BotNames[a];
+        Break;
+      end;
+
+// Èìåíè íåò, çàäàåì ñëó÷àéíîå:
+  if _name = '' then
+    repeat
+      _name := Format('DFBOT%.2d', [Random(100)]);
+    until g_Player_ValidName(_name);
+
+// Âûáèðàåì ñëó÷àéíóþ ìîäåëü:
+  _model := m[Random(Length(m))];
+
+// Ñîçäàåì áîòà:
+  with g_Player_Get(g_Player_Create(_model,
+                                    _RGB(Min(Random(9)*32, 255),
+                                         Min(Random(9)*32, 255),
+                                         Min(Random(9)*32, 255)),
+                                    Team, True)) as TBot do
+  begin
+    Name := _name;
+
+    case Difficult of
+      1: FDifficult := DIFFICULT_EASY;
+      2: FDifficult := DIFFICULT_MEDIUM;
+      else FDifficult := DIFFICULT_HARD;
+    end;
+
+    for a := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    begin
+      FDifficult.WeaponPrior[a] := WEAPON_PRIOR1[a];
+      FDifficult.CloseWeaponPrior[a] := WEAPON_PRIOR2[a];
+      //FDifficult.SafeWeaponPrior[a] := WEAPON_PRIOR3[a];
+    end;
+
+    g_Console_Add(Format(_lc[I_PLAYER_JOIN], [Name]), True);
+
+    if g_Game_IsNet then MH_SEND_PlayerCreate(UID);
+    if g_Game_IsServer and (gGameSettings.MaxLives > 0) then
+      Spectate();
+  end;
+end;
+
+procedure g_Bot_AddList(Team: Byte; lName: ShortString; num: Integer = -1);
+var
+  m: SArray;
+  _name, _model: String;
+  a: Integer;
+begin
+  if not g_Game_IsServer then Exit;
+
+// Ñïèñîê íàçâàíèé ìîäåëåé:
+  m := g_PlayerModel_GetNames();
+  if m = nil then
+    Exit;
+
+// Êîìàíäà:
+  if (gGameSettings.GameType = GT_SINGLE) or (gGameSettings.GameMode = GM_COOP) then
+    Team := TEAM_COOP // COOP
+  else
+    if gGameSettings.GameMode = GM_DM then
+      Team := TEAM_NONE // DM
+    else
+      if Team = TEAM_NONE then
+        Team := BotList[num].team; // CTF / TDM
+
+// Âûáèðàåì íàñòðîéêè áîòà èç ñïèñêà ïî íîìåðó èëè èìåíè:
+  lName := AnsiLowerCase(lName);
+  if (num < 0) or (num > Length(BotList)-1) then
+    num := -1;
+  if (num = -1) and (lName <> '') and (BotList <> nil) then
+    for a := 0 to High(BotList) do
+      if AnsiLowerCase(BotList[a].name) = lName then
+      begin
+        num := a;
+        Break;
+      end;
+  if num = -1 then
+    Exit;
+
+// Èìÿ áîòà:
+  _name := BotList[num].name;
+// Çàíÿòî - âûáèðàåì ñëó÷àéíîå:
+  if not g_Player_ValidName(_name) then
+  repeat
+    _name := Format('DFBOT%.2d', [Random(100)]);
+  until g_Player_ValidName(_name);
+
+// Ìîäåëü:
+  _model := BotList[num].model;
+// Íåò òàêîé - âûáèðàåì ñëó÷àéíóþ:
+  if not InSArray(_model, m) then
+    _model := m[Random(Length(m))];
+
+// Ñîçäàåì áîòà:
+  with g_Player_Get(g_Player_Create(_model, BotList[num].color, Team, True)) as TBot do
+  begin
+    Name := _name;
+
+    FDifficult.DiagFire := BotList[num].diag_fire;
+    FDifficult.InvisFire := BotList[num].invis_fire;
+    FDifficult.DiagPrecision := BotList[num].diag_precision;
+    FDifficult.FlyPrecision := BotList[num].fly_precision;
+    FDifficult.Cover := BotList[num].cover;
+    FDifficult.CloseJump := BotList[num].close_jump;
+
+    for a := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    begin
+      FDifficult.WeaponPrior[a] := BotList[num].w_prior1[a];
+      FDifficult.CloseWeaponPrior[a] := BotList[num].w_prior2[a];
+      //FDifficult.SafeWeaponPrior[a] := BotList[num].w_prior3[a];
+    end;
+
+    g_Console_Add(Format(_lc[I_PLAYER_JOIN], [Name]), True);
+
+    if g_Game_IsNet then MH_SEND_PlayerCreate(UID);
+  end;
+end;
+
+procedure g_Bot_RemoveAll();
+var
+  a: Integer;
+begin
+  if not g_Game_IsServer then Exit;
+  if gPlayers = nil then Exit;
+
+  for a := 0 to High(gPlayers) do
+    if gPlayers[a] <> nil then
+      if gPlayers[a] is TBot then
+      begin
+        gPlayers[a].Lives := 0;
+        gPlayers[a].Kill(K_SIMPLEKILL, 0, HIT_DISCON);
+        g_Console_Add(Format(_lc[I_PLAYER_LEAVE], [gPlayers[a].Name]), True);
+        g_Player_Remove(gPlayers[a].FUID);
+      end;
+
+  g_Bot_MixNames();
+end;
+
+procedure g_Bot_MixNames();
+var
+  s: String;
+  a, b: Integer;
+begin
+  if BotNames <> nil then
+    for a := 0 to High(BotNames) do
+    begin
+      b := Random(Length(BotNames));
+      s := BotNames[a];
+      Botnames[a] := BotNames[b];
+      BotNames[b] := s;
+    end;
+end;
+
+procedure g_Player_Remove(UID: Word);
+var
+  i: Integer;
+begin
+  if gPlayers = nil then Exit;
+
+  if g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_PlayerDelete(UID);
+
+  for i := 0 to High(gPlayers) do
+    if gPlayers[i] <> nil then
+      if gPlayers[i].FUID = UID then
+      begin
+        if gPlayers[i] is TPlayer then
+          TPlayer(gPlayers[i]).Free()
+        else
+          TBot(gPlayers[i]).Free();
+        gPlayers[i] := nil;
+        Exit;
+      end;
+end;
+
+procedure g_Player_Init();
+var
+  F: TextFile;
+  s: String;
+  a, b: Integer;
+  config: TConfig;
+  sa: SArray;
+begin
+  BotNames := nil;
+
+  if not FileExists(DataDir + BOTNAMES_FILENAME) then
+    Exit;
+
+// ×èòàåì âîçìîæíûå èìåíà áîòîâ èç ôàéëà:
+  AssignFile(F, DataDir + BOTNAMES_FILENAME);
+  Reset(F);
+
+  while not EOF(F) do
+  begin
+    ReadLn(F, s);
+
+    s := Trim(s);
+    if s = '' then
+      Continue;
+
+    SetLength(BotNames, Length(BotNames)+1);
+    BotNames[High(BotNames)] := s;
+  end;
+
+  CloseFile(F);
+
+// Ïåðåìåøèâàåì èõ:
+  g_Bot_MixNames();
+
+// ×èòàåì ôàéë ñ ïàðàìåòðàìè áîòîâ:
+  config := TConfig.CreateFile(DataDir + BOTLIST_FILENAME);
+  BotList := nil;
+  a := 0;
+
+  while config.SectionExists(IntToStr(a)) do
+  begin
+    SetLength(BotList, Length(BotList)+1);
+
+    with BotList[High(BotList)] do
+    begin
+    // Èìÿ áîòà:
+      name := config.ReadStr(IntToStr(a), 'name', '');
+    // Ìîäåëü:
+      model := config.ReadStr(IntToStr(a), 'model', '');
+    // Êîìàíäà:
+      if config.ReadStr(IntToStr(a), 'team', 'red') = 'red' then
+        team := TEAM_RED
+      else
+        team := TEAM_BLUE;
+    // Öâåò ìîäåëè:
+      sa := parse(config.ReadStr(IntToStr(a), 'color', ''));
+      color.R := StrToIntDef(sa[0], 0);
+      color.G := StrToIntDef(sa[1], 0);
+      color.B := StrToIntDef(sa[2], 0);
+    // Âåðîÿòíîñòü ñòðåëüáû ïîä óãëîì:
+      diag_fire := config.ReadInt(IntToStr(a), 'diag_fire', 0);
+    // Âåðîÿòíîñòü îòâåòíîãî îãíÿ ïî íåâèäèìîìó ñîïåðíèêó:
+      invis_fire := config.ReadInt(IntToStr(a), 'invis_fire', 0);
+    // Òî÷íîñòü ñòðåëüáû ïîä óãëîì:
+      diag_precision := config.ReadInt(IntToStr(a), 'diag_precision', 0);
+    // Òî÷íîñòü ñòðåëüáû â ïîëåòå:
+      fly_precision := config.ReadInt(IntToStr(a), 'fly_precision', 0);
+    // Òî÷íîñòü óêëîíåíèÿ îò ñíàðÿäîâ:
+      cover := config.ReadInt(IntToStr(a), 'cover', 0);
+    // Âåðîÿòíîñòü ïðûæêà ïðè ïðèáëèæåíèè ñîïåðíèêà:
+      close_jump := config.ReadInt(IntToStr(a), 'close_jump', 0);
+    // Ïðèîðèòåòû îðóæèÿ äëÿ äàëüíåãî áîÿ:
+      sa := parse(config.ReadStr(IntToStr(a), 'w_prior1', ''));
+      if Length(sa) = 10 then
+        for b := 0 to 9 do
+          w_prior1[b] := EnsureRange(StrToInt(sa[b]), 0, 9);
+    // Ïðèîðèòåòû îðóæèÿ äëÿ áëèæíåãî áîÿ:
+      sa := parse(config.ReadStr(IntToStr(a), 'w_prior2', ''));
+      if Length(sa) = 10 then
+        for b := 0 to 9 do
+          w_prior2[b] := EnsureRange(StrToInt(sa[b]), 0, 9);
+
+      {sa := parse(config.ReadStr(IntToStr(a), 'w_prior3', ''));
+      if Length(sa) = 10 then
+        for b := 0 to 9 do
+          w_prior3[b] := EnsureRange(StrToInt(sa[b]), 0, 9);}
+    end;
+
+    a := a + 1;
+  end;
+
+  config.Free();
+end;
+
+procedure g_Player_Free();
+var
+  i: Integer;
+begin
+  if gPlayers <> nil then
+  begin
+    for i := 0 to High(gPlayers) do
+      if gPlayers[i] <> nil then
+      begin
+        if gPlayers[i] is TPlayer then
+          TPlayer(gPlayers[i]).Free()
+        else
+          TBot(gPlayers[i]).Free();
+        gPlayers[i] := nil;
+      end;
+
+    gPlayers := nil;
+  end;
+
+  gPlayer1 := nil;
+  gPlayer2 := nil;
+end;
+
+procedure g_Player_UpdateAll();
+var
+  i: Integer;
+begin
+  if gPlayers = nil then Exit;
+
+  for i := 0 to High(gPlayers) do
+    if gPlayers[i] <> nil then
+      if gPlayers[i] is TPlayer then gPlayers[i].Update()
+      else TBot(gPlayers[i]).Update();
+end;
+
+procedure g_Player_DrawAll();
+var
+  i: Integer;
+begin
+  if gPlayers = nil then Exit;
+
+  for i := 0 to High(gPlayers) do
+    if gPlayers[i] <> nil then
+      if gPlayers[i] is TPlayer then gPlayers[i].Draw()
+      else TBot(gPlayers[i]).Draw();
+end;
+
+procedure g_Player_DrawDebug(p: TPlayer);
+var
+  fW, fH: Byte;
+begin
+  if p = nil then Exit;
+  if (@p.FObj) = nil then Exit;
+
+  e_TextureFontGetSize(gStdFont, fW, fH);
+
+  e_TextureFontPrint(0, 0     , 'Pos X: ' + IntToStr(p.FObj.X), gStdFont);
+  e_TextureFontPrint(0, fH    , 'Pos Y: ' + IntToStr(p.FObj.Y), gStdFont);
+  e_TextureFontPrint(0, fH * 2, 'Vel X: ' + IntToStr(p.FObj.Vel.X), gStdFont);
+  e_TextureFontPrint(0, fH * 3, 'Vel Y: ' + IntToStr(p.FObj.Vel.Y), gStdFont);
+  e_TextureFontPrint(0, fH * 4, 'Acc X: ' + IntToStr(p.FObj.Accel.X), gStdFont);
+  e_TextureFontPrint(0, fH * 5, 'Acc Y: ' + IntToStr(p.FObj.Accel.Y), gStdFont);
+end;
+
+procedure g_Player_DrawHealth();
+var
+  i: Integer;
+  fW, fH: Byte;
+begin
+  if gPlayers = nil then Exit;
+  e_TextureFontGetSize(gStdFont, fW, fH);
+
+  for i := 0 to High(gPlayers) do
+    if gPlayers[i] <> nil then
+    begin
+      e_TextureFontPrint(gPlayers[i].FObj.X + gPlayers[i].FObj.Rect.X,
+      gPlayers[i].FObj.Y + gPlayers[i].FObj.Rect.Y + gPlayers[i].FObj.Rect.Height - fH * 2,
+      IntToStr(gPlayers[i].FHealth), gStdFont);
+      e_TextureFontPrint(gPlayers[i].FObj.X + gPlayers[i].FObj.Rect.X,
+      gPlayers[i].FObj.Y + gPlayers[i].FObj.Rect.Y + gPlayers[i].FObj.Rect.Height - fH,
+      IntToStr(gPlayers[i].FArmor), gStdFont);
+    end;
+end;
+
+function g_Player_Get(UID: Word): TPlayer;
+var
+  a: Integer;
+begin
+  Result := nil;
+
+  if gPlayers = nil then
+    Exit;
+
+  for a := 0 to High(gPlayers) do
+    if gPlayers[a] <> nil then
+      if gPlayers[a].FUID = UID then
+      begin
+        Result := gPlayers[a];
+        Exit;
+      end;
+end;
+
+function g_Player_GetCount(): Byte;
+var
+  a: Integer;
+begin
+  Result := 0;
+
+  if gPlayers = nil then
+    Exit;
+
+  for a := 0 to High(gPlayers) do
+    if gPlayers[a] <> nil then
+      Result := Result + 1;
+end;
+
+function g_Player_GetStats(): TPlayerStatArray;
+var
+  a: Integer;
+begin
+  Result := nil;
+
+  if gPlayers = nil then Exit;
+
+  for a := 0 to High(gPlayers) do
+    if gPlayers[a] <> nil then
+    begin
+      SetLength(Result, Length(Result)+1);
+      with Result[High(Result)] do
+      begin
+        Ping := gPlayers[a].FPing;
+        Loss := gPlayers[a].FLoss;
+        Name := gPlayers[a].FName;
+        Team := gPlayers[a].FTeam;
+        Frags := gPlayers[a].FFrags;
+        Deaths := gPlayers[a].FDeath;
+        Kills := gPlayers[a].FKills;
+        Color := gPlayers[a].FModel.Color;
+        Lives := gPlayers[a].FLives;
+        Spectator := gPlayers[a].FSpectator;
+      end;
+    end;
+end;
+
+procedure g_Player_RememberAll;
+var
+  i: Integer;
+begin
+  for i := Low(gPlayers) to High(gPlayers) do
+    if (gPlayers[i] <> nil) and gPlayers[i].Live then
+      gPlayers[i].RememberState;
+end;
+
+procedure g_Player_ResetAll(Force, Silent: Boolean);
+var
+  i: Integer;
+begin
+  gTeamStat[TEAM_RED].Goals := 0;
+  gTeamStat[TEAM_BLUE].Goals := 0;
+
+  if gPlayers <> nil then
+    for i := 0 to High(gPlayers) do
+      if gPlayers[i] <> nil then
+      begin
+        gPlayers[i].Reset(Force);
+
+        if gPlayers[i] is TPlayer then
+        begin
+          if (not gPlayers[i].FSpectator) or gPlayers[i].FWantsInGame then
+            gPlayers[i].Respawn(Silent)
+          else
+            gPlayers[i].Spectate();
+        end
+        else
+          TBot(gPlayers[i]).Respawn(Silent);
+      end;
+end;
+
+procedure g_Player_CreateCorpse(Player: TPlayer);
+var
+  find_id: DWORD;
+  ok: Boolean;
+begin
+  if Player.Live then
+    Exit;
+  if Player.FObj.Y >= gMapInfo.Height+128 then
+    Exit;
+
+  with Player do
+  begin
+    if (FHealth >= -50) or (gGibsCount = 0) then
+      begin
+        if (gCorpses = nil) or (Length(gCorpses) = 0) then
+          Exit;
+
+        ok := False;
+        for find_id := 0 to High(gCorpses) do
+          if gCorpses[find_id] = nil then
+          begin
+            ok := True;
+            Break;
+          end;
+
+        if not ok then
+          find_id := Random(Length(gCorpses));
+
+        gCorpses[find_id] := TCorpse.Create(FObj.X, FObj.Y, FModel.Name, FHealth < -20);
+        gCorpses[find_id].FColor := FModel.Color;
+        gCorpses[find_id].FObj.Vel := FObj.Vel;
+        gCorpses[find_id].FObj.Accel := FObj.Accel;
+      end
+    else
+      g_Player_CreateGibs(FObj.X + PLAYER_RECT_CX,
+                          FObj.Y + PLAYER_RECT_CY,
+                          FModel.Name, FModel.Color);
+  end;
+end;
+
+procedure g_Player_CreateShell(fX, fY, dX, dY: Integer; T: Byte);
+var
+  SID: DWORD;
+begin
+  if (gShells = nil) or (Length(gShells) = 0) then
+    Exit;
+  
+  with gShells[CurrentShell] do
+  begin
+    SpriteID := 0;
+    g_Obj_Init(@Obj);
+    Obj.Rect.X := 0;
+    Obj.Rect.Y := 0;
+    if T = SHELL_BULLET then
+    begin
+      if g_Texture_Get('TEXTURE_SHELL_BULLET', SID) then
+        SpriteID := SID;
+      CX := 2;
+      CY := 1;
+      Obj.Rect.Width := 4;
+      Obj.Rect.Height := 2;
+    end
+    else
+    begin
+      if g_Texture_Get('TEXTURE_SHELL_SHELL', SID) then
+        SpriteID := SID;
+      CX := 4;
+      CY := 2;
+      Obj.Rect.Width := 7;
+      Obj.Rect.Height := 3;
+    end;
+    SType := T;
+    Live := True;
+    Obj.X := fX;
+    Obj.Y := fY;
+    g_Obj_Push(@Obj, dX + Random(4)-Random(4), dY-Random(4));
+    RAngle := Random(360);
+    Timeout := gTime + SHELL_TIMEOUT;
+
+    if CurrentShell >= High(gShells) then
+      CurrentShell := 0
+    else
+      Inc(CurrentShell);
+  end;
+end;
+
+procedure g_Player_CreateGibs(fX, fY: Integer; ModelName: string; fColor: TRGB);
+var
+  a: Integer;
+  GibsArray: TGibsArray;
+begin
+  if (gGibs = nil) or (Length(gGibs) = 0) then
+    Exit;
+  if not g_PlayerModel_GetGibs(ModelName, GibsArray) then
+    Exit;
+
+  for a := 0 to High(GibsArray) do
+    with gGibs[CurrentGib] do
+    begin
+      Color := fColor;
+      ID := GibsArray[a].ID;
+      MaskID := GibsArray[a].MaskID;
+      Live := True;
+      g_Obj_Init(@Obj);
+      Obj.Rect := GibsArray[a].Rect;
+      Obj.X := fX-GibsArray[a].Rect.X-(GibsArray[a].Rect.Width div 2);
+      Obj.Y := fY-GibsArray[a].Rect.Y-(GibsArray[a].Rect.Height div 2);
+      g_Obj_PushA(@Obj, 25 + Random(10), Random(361));
+      RAngle := Random(360);
+
+      if gBloodCount > 0 then
+        g_GFX_Blood(fX, fY, 16*gBloodCount+Random(5*gBloodCount), -16+Random(33), -16+Random(33),
+                    Random(48), Random(48), 150, 0, 0);
+
+      if CurrentGib >= High(gGibs) then
+        CurrentGib := 0
+      else
+        Inc(CurrentGib);
+    end;
+end;
+
+procedure g_Player_UpdatePhysicalObjects();
+var
+  i: Integer;
+  vel: TPoint2i;
+  mr: Word;
+
+  procedure ShellSound_Bounce(X, Y: Integer; T: Byte);
+  var
+    k: Integer;
+  begin
+    k := 1 + Random(2);
+    if T = SHELL_BULLET then
+      g_Sound_PlayExAt('SOUND_PLAYER_CASING' + IntToStr(k), X, Y)
+    else
+      g_Sound_PlayExAt('SOUND_PLAYER_SHELL' + IntToStr(k), X, Y);
+  end;
+  
+begin
+// Êóñêè ìÿñà:
+  if gGibs <> nil then
+    for i := 0 to High(gGibs) do
+      if gGibs[i].Live then
+        with gGibs[i] do
+        begin
+          vel := Obj.Vel;
+          mr := g_Obj_Move(@Obj, True, False, True);
+
+          if WordBool(mr and MOVE_FALLOUT) then
+          begin
+            Live := False;
+            Continue;
+          end;
+
+        // Îòëåòàåò îò óäàðà î ñòåíó/ïîòîëîê/ïîë:
+          if WordBool(mr and MOVE_HITWALL) then
+            Obj.Vel.X := -(vel.X div 2);
+          if WordBool(mr and (MOVE_HITCEIL or MOVE_HITLAND)) then
+            Obj.Vel.Y := -(vel.Y div 2);
+          
+          if (Obj.Vel.X >= 0) then
+          begin // Clockwise
+            RAngle := RAngle + Abs(Obj.Vel.X)*6 + Abs(Obj.Vel.Y);
+            if RAngle >= 360 then
+              RAngle := RAngle mod 360;
+          end else begin // Counter-clockwise
+            RAngle := RAngle - Abs(Obj.Vel.X)*6 - Abs(Obj.Vel.Y);
+            if RAngle < 0 then
+              RAngle := (360 - (Abs(RAngle) mod 360)) mod 360;
+          end;
+
+        // Ñîïðîòèâëåíèå âîçäóõà äëÿ êóñêà òðóïà:
+          if gTime mod (GAME_TICK*3) = 0 then
+            Obj.Vel.X := z_dec(Obj.Vel.X, 1);
+        end;
+
+// Òðóïû:
+  if gCorpses <> nil then
+    for i := 0 to High(gCorpses) do
+      if gCorpses[i] <> nil then
+        if gCorpses[i].State = CORPSE_STATE_REMOVEME then
+          begin
+            gCorpses[i].Free();
+            gCorpses[i] := nil;
+          end
+        else
+          gCorpses[i].Update();
+
+// Ãèëüçû:
+  if gShells <> nil then
+    for i := 0 to High(gShells) do
+      if gShells[i].Live then
+        with gShells[i] do
+        begin
+          vel := Obj.Vel;
+          mr := g_Obj_Move(@Obj, True, False, True);
+
+          if WordBool(mr and MOVE_FALLOUT) or (gShells[i].Timeout < gTime) then
+          begin
+            Live := False;
+            Continue;
+          end;
+
+        // Îòëåòàåò îò óäàðà î ñòåíó/ïîòîëîê/ïîë:
+          if WordBool(mr and MOVE_HITWALL) then
+          begin
+            Obj.Vel.X := -(vel.X div 2);
+            if not WordBool(mr and MOVE_INWATER) then
+              ShellSound_Bounce(Obj.X, Obj.Y, SType);
+          end;
+          if WordBool(mr and (MOVE_HITCEIL or MOVE_HITLAND)) then
+          begin
+            Obj.Vel.Y := -(vel.Y div 2);
+            if Obj.Vel.X <> 0 then Obj.Vel.X := Obj.Vel.X div 2;
+            if (Obj.Vel.X = 0) and (Obj.Vel.Y = 0) then
+            begin
+              if RAngle mod 90 <> 0 then
+                RAngle := (RAngle div 90) * 90;
+            end
+            else if not WordBool(mr and MOVE_INWATER) then
+              ShellSound_Bounce(Obj.X, Obj.Y, SType);
+          end;
+
+          if (Obj.Vel.X >= 0) then
+          begin // Clockwise
+            RAngle := RAngle + Abs(Obj.Vel.X)*8 + Abs(Obj.Vel.Y);
+            if RAngle >= 360 then
+              RAngle := RAngle mod 360;
+          end else begin // Counter-clockwise
+            RAngle := RAngle - Abs(Obj.Vel.X)*8 - Abs(Obj.Vel.Y);
+            if RAngle < 0 then
+              RAngle := (360 - (Abs(RAngle) mod 360)) mod 360;
+          end;
+        end;
+end;
+
+procedure g_Player_DrawCorpses();
+var
+  i: Integer;
+  a: TPoint;
+begin
+  if gGibs <> nil then
+    for i := 0 to High(gGibs) do
+      if gGibs[i].Live then
+        with gGibs[i] do
+        begin
+          if not g_Obj_Collide(sX, sY, sWidth, sHeight, @Obj) then
+            Continue;
+
+          a.X := Obj.Rect.X+(Obj.Rect.Width div 2);
+          a.y := Obj.Rect.Y+(Obj.Rect.Height div 2);
+
+          e_DrawAdv(ID, Obj.X, Obj.Y, 0, True, False, RAngle, @a, M_NONE);
+
+          e_Colors := Color;
+          e_DrawAdv(MaskID, Obj.X, Obj.Y, 0, True, False, RAngle, @a, M_NONE);
+          e_Colors.R := 255;
+          e_Colors.G := 255;
+          e_Colors.B := 255;
+        end;
+
+  if gCorpses <> nil then
+    for i := 0 to High(gCorpses) do
+      if gCorpses[i] <> nil then
+        gCorpses[i].Draw();
+end;
+
+procedure g_Player_DrawShells();
+var
+  i: Integer;
+  a: TPoint;
+begin
+  if gShells <> nil then
+    for i := 0 to High(gShells) do
+      if gShells[i].Live then
+        with gShells[i] do
+        begin
+          if not g_Obj_Collide(sX, sY, sWidth, sHeight, @Obj) then
+            Continue;
+
+          a.X := CX;
+          a.Y := CY;
+
+          e_DrawAdv(SpriteID, Obj.X, Obj.Y, 0, True, False, RAngle, @a, M_NONE);
+        end;
+end;
+
+procedure g_Player_RemoveAllCorpses();
+var
+  i: Integer;
+begin
+  gGibs := nil;
+  gShells := nil;
+  SetLength(gGibs, MaxGibs);
+  SetLength(gShells, MaxGibs);
+  CurrentGib := 0;
+  CurrentShell := 0;
+  
+  if gCorpses <> nil then
+    for i := 0 to High(gCorpses) do
+      gCorpses[i].Free();
+
+  gCorpses := nil;
+  SetLength(gCorpses, MaxCorpses);
+end;
+
+procedure g_Player_Corpses_SaveState(var Mem: TBinMemoryWriter);
+var
+  count, i: Integer;
+  b: Boolean;
+begin
+// Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ òðóïîâ:
+  count := 0;
+  if gCorpses <> nil then
+    for i := 0 to High(gCorpses) do
+      if gCorpses[i] <> nil then
+        count := count + 1;
+
+  Mem := TBinMemoryWriter.Create((count+1) * 128);
+
+// Êîëè÷åñòâî òðóïîâ:
+  Mem.WriteInt(count);
+
+  if count = 0 then
+    Exit;
+
+// Ñîõðàíÿåì òðóïû:
+  for i := 0 to High(gCorpses) do
+    if gCorpses[i] <> nil then
+    begin
+    // Íàçâàíèå ìîäåëè:
+      Mem.WriteString(gCorpses[i].FModelName);
+    // Òèï ñìåðòè:
+      b := gCorpses[i].Mess;
+      Mem.WriteBoolean(b);
+    // Ñîõðàíÿåì äàííûå òðóïà:
+      gCorpses[i].SaveState(Mem);
+    end;
+end;
+
+procedure g_Player_Corpses_LoadState(var Mem: TBinMemoryReader);
+var
+  count, i: Integer;
+  str: String;
+  b: Boolean;
+begin
+  if Mem = nil then
+    Exit;
+
+  g_Player_RemoveAllCorpses();
+
+// Êîëè÷åñòâî òðóïîâ:
+  Mem.ReadInt(count);
+
+  if count > Length(gCorpses) then
+  begin
+    raise EBinSizeError.Create('g_Player_Corpses_LoadState: Too Many Corpses');
+  end;
+
+  if count = 0 then
+    Exit;
+
+// Çàãðóæàåì òðóïû:
+  for i := 0 to count-1 do
+  begin
+  // Íàçâàíèå ìîäåëè:
+    Mem.ReadString(str);
+  // Òèï ñìåðòè:
+    Mem.ReadBoolean(b);
+  // Ñîçäàåì òðóï:
+    gCorpses[i] := TCorpse.Create(0, 0, str, b);
+  // Çàãðóæàåì äàííûå òðóïà:
+    gCorpses[i].LoadState(Mem);
+  end;
+end;
+
+{ T P l a y e r : }
+
+procedure TPlayer.BFGHit();
+begin
+  g_Weapon_BFGHit(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                  FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2));
+  if g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_Effect(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                   FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                   0, NET_GFX_BFGHIT);
+end;
+
+procedure TPlayer.ChangeModel(ModelName: string);
+var
+  Model: TPlayerModel;
+begin
+  Model := g_PlayerModel_Get(ModelName);
+  if Model = nil then Exit;
+
+  FModel.Free();
+  FModel := Model;
+end;
+
+procedure TPlayer.SetModel(ModelName: string);
+var
+  m: TPlayerModel;
+begin
+  m := g_PlayerModel_Get(ModelName);
+  if m = nil then
+  begin
+    g_SimpleError(Format(_lc[I_GAME_ERROR_MODEL_FALLBACK], [ModelName]));
+    m := g_PlayerModel_Get('doomer');
+    if m = nil then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_MODEL], ['doomer']));
+      Exit;
+    end;
+  end;
+
+  if FModel <> nil then
+    FModel.Free();
+
+  FModel := m;
+
+  if not (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+    FModel.Color := FColor
+  else
+    FModel.Color := TEAMCOLOR[FTeam];
+  FModel.SetWeapon(FCurrWeap);
+  FModel.SetFlag(FFlag);
+  SetDirection(FDirection);
+end;
+
+procedure TPlayer.SetColor(Color: TRGB);
+begin
+  FColor := Color;
+  if not (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+    if FModel <> nil then FModel.Color := Color;
+end;
+
+procedure TPlayer.SwitchTeam;
+begin
+  if g_Game_IsClient then
+    Exit;
+  if not (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then Exit;
+
+  if gGameOn and FLive then
+    Kill(K_SIMPLEKILL, FUID, HIT_SELF);
+
+  if FTeam = TEAM_RED then
+  begin
+    ChangeTeam(TEAM_BLUE);
+    g_Console_Add(Format(_lc[I_PLAYER_CHTEAM_BLUE], [FName]), True);
+    if g_Game_IsNet then
+      MH_SEND_GameEvent(NET_EV_CHANGE_TEAM, TEAM_BLUE, FName);
+  end
+  else
+  begin
+    ChangeTeam(TEAM_RED);
+    g_Console_Add(Format(_lc[I_PLAYER_CHTEAM_RED], [FName]), True);
+    if g_Game_IsNet then
+      MH_SEND_GameEvent(NET_EV_CHANGE_TEAM, TEAM_RED, FName);
+  end;
+  FPreferredTeam := FTeam;
+end;
+
+procedure TPlayer.ChangeTeam(Team: Byte);
+var
+  OldTeam: Byte;
+begin
+  OldTeam := FTeam;
+  FTeam := Team;
+  case Team of
+    TEAM_RED, TEAM_BLUE:
+      FModel.Color := TEAMCOLOR[Team];
+    else
+      FModel.Color := FColor;
+  end;
+  if (FTeam <> OldTeam) and g_Game_IsNet and g_Game_IsServer then
+    MH_SEND_PlayerStats(FUID);
+end;
+
+{
+procedure TPlayer.CollideItem();
+var
+  i: Integer;
+  r: Boolean;
+begin
+ if gItems = nil then Exit;
+ if not FLive then Exit;
+
+ for i := 0 to High(gItems) do
+  with gItems[i] do
+  begin
+   if (ItemType <> ITEM_NONE) and Live then
+    if g_Obj_Collide(FObj.X+PLAYER_RECT.X, FObj.Y+PLAYER_RECT.Y, PLAYER_RECT.Width,
+                     PLAYER_RECT.Height, @Obj) then
+   begin
+    if not PickItem(ItemType, gItems[i].Respawnable, r) then Continue;
+
+    if ItemType in [ITEM_SPHERE_BLUE, ITEM_SPHERE_WHITE, ITEM_INVUL] then
+     g_Sound_PlayExAt('SOUND_ITEM_GETRULEZ', FObj.X, FObj.Y)
+    else if ItemType in [ITEM_MEDKIT_SMALL, ITEM_MEDKIT_LARGE, ITEM_MEDKIT_BLACK] then
+     g_Sound_PlayExAt('SOUND_ITEM_GETMED', FObj.X, FObj.Y)
+    else g_Sound_PlayExAt('SOUND_ITEM_GETITEM', FObj.X, FObj.Y);
+
+    // Íàäî óáðàòü ñ êàðòû, åñëè ýòî íå êëþ÷, êîòîðûì íóæíî ïîäåëèòñÿ ñ äðóãèì èãðîêîì:
+    if r and not ((ItemType in [ITEM_KEY_RED, ITEM_KEY_GREEN, ITEM_KEY_BLUE]) and
+                  (gGameSettings.GameType = GT_SINGLE) and
+                  (g_Player_GetCount() > 1)) then
+     if not Respawnable then g_Items_Remove(i) else g_Items_Pick(i);
+   end;
+  end;
+end;
+}
+
+function TPlayer.CollideLevel(XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(FObj.X+PLAYER_RECT.X+XInc, FObj.Y+PLAYER_RECT.Y+YInc,
+                               PLAYER_RECT.Width, PLAYER_RECT.Height, PANEL_WALL,
+                               False);
+end;
+
+constructor TPlayer.Create();
+begin
+  FIamBot := False;
+  FDummy := False;
+  FSpawned := False;
+
+  FSawSound := TPlayableSound.Create();
+  FSawSoundIdle := TPlayableSound.Create();
+  FSawSoundHit := TPlayableSound.Create();
+  FSawSoundSelect := TPlayableSound.Create();
+  FJetSoundFly := TPlayableSound.Create();
+  FJetSoundOn := TPlayableSound.Create();
+  FJetSoundOff := TPlayableSound.Create();
+
+  FSawSound.SetByName('SOUND_WEAPON_FIRESAW');
+  FSawSoundIdle.SetByName('SOUND_WEAPON_IDLESAW');
+  FSawSoundHit.SetByName('SOUND_WEAPON_HITSAW');
+  FSawSoundSelect.SetByName('SOUND_WEAPON_SELECTSAW');
+  FJetSoundFly.SetByName('SOUND_PLAYER_JETFLY');
+  FJetSoundOn.SetByName('SOUND_PLAYER_JETON');
+  FJetSoundOff.SetByName('SOUND_PLAYER_JETOFF');
+
+  FSpectatePlayer := -1;
+  FClientID := -1;
+  FPing := 0;
+  FLoss := 0;
+  FSavedState.WaitRecall := False;
+  FShellTimer := -1;
+
+  FActualModelName := 'doomer';
+
+  g_Obj_Init(@FObj);
+  FObj.Rect := PLAYER_RECT;
+
+  FBFGFireCounter := -1;
+  FJustTeleported := False;
+  FNetTime := 0;
+end;
+
+procedure TPlayer.Damage(value: Word; SpawnerUID: Word; vx, vy: Integer; t: Byte);
+var
+  c: Word;
+begin
+  if (not g_Game_IsClient) and (not FLive) then
+    Exit;
+
+  FLastHit := t;
+
+// Íåóÿçâèìîñòü íå ñïàñàåò îò ëîâóøåê:
+  if ((t = HIT_TRAP) or (t = HIT_SELF)) and (not FGodMode) then
+  begin
+    if not g_Game_IsClient then
+    begin
+      FArmor := 0;
+      if t = HIT_TRAP then
+      begin
+        // Ëîâóøêà óáèâàåò ñðàçó:
+        FHealth := -100;
+        Kill(K_EXTRAHARDKILL, SpawnerUID, t);
+      end;
+      if t = HIT_SELF then
+      begin
+        // Ñàìîóáèéñòâî:
+        FHealth := 0;
+        Kill(K_SIMPLEKILL, SpawnerUID, t);
+      end;
+    end;
+    // Îáíóëèòü äåéñòâèÿ ïðèìî÷åê, ÷òîáû ôîí ïðîïàë
+    FMegaRulez[MR_SUIT] := 0;
+    FMegaRulez[MR_INVUL] := 0;
+    FMegaRulez[MR_INVIS] := 0;
+    FBerserk := 0;
+  end;
+
+// Íî îò îñòàëüíîãî ñïàñàåò:
+  if FMegaRulez[MR_INVUL] >= gTime then
+    Exit;
+
+// ×èò-êîä "ÃÎÐÅÖ":
+  if FGodMode then
+    Exit;
+
+// Åñëè åñòü óðîí ñâîèì, èëè ðàíèë ñàì ñåáÿ, èëè òåáÿ ðàíèë ïðîòèâíèê:
+  if LongBool(gGameSettings.Options and GAME_OPTION_TEAMDAMAGE) or
+     (SpawnerUID = FUID) or
+     (not SameTeam(FUID, SpawnerUID)) then
+  begin
+    FLastSpawnerUID := SpawnerUID;
+
+  // Êðîâü (ïóçûðüêè, åñëè â âîäå):
+    if gBloodCount > 0 then
+    begin
+      c := Min(value, 200)*gBloodCount + Random(Min(value, 200) div 2);
+      if value div 4 <= c then
+        c := c - (value div 4)
+      else
+        c := 0;
+
+      if (t = HIT_SOME) and (vx = 0) and (vy = 0) then
+        MakeBloodSimple(c)
+      else
+        case t of
+          HIT_TRAP, HIT_ACID, HIT_FLAME, HIT_SELF: MakeBloodSimple(c);
+          HIT_BFG, HIT_ROCKET, HIT_SOME: MakeBloodVector(c, vx, vy);
+        end;
+
+      if t = HIT_WATER then
+        g_GFX_Bubbles(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2),
+                      FObj.Y+PLAYER_RECT.Y-4, value div 2, 8, 4);
+    end;
+
+  // Áóôåð óðîíà:
+    if FLive then
+      Inc(FDamageBuffer, value);
+
+  // Âñïûøêà áîëè:
+    if gFlash <> 0 then
+      FPain := FPain + value;
+  end;
+
+  if g_Game_IsServer and g_Game_IsNet then
+  begin
+    MH_SEND_PlayerDamage(FUID, t, SpawnerUID, value, vx, vy);
+    MH_SEND_PlayerStats(FUID);
+    MH_SEND_PlayerPos(False, FUID);
+  end;
+end;
+
+function TPlayer.Heal(value: Word; Soft: Boolean): Boolean;
+begin
+  Result := False;
+  if g_Game_IsClient then
+    Exit;
+  if not FLive then
+    Exit;
+
+  if Soft and (FHealth < PLAYER_HP_SOFT) then
+  begin
+    IncMax(FHealth, value, PLAYER_HP_SOFT);
+    Result := True;
+  end;
+  if (not Soft) and (FHealth < PLAYER_HP_LIMIT) then
+  begin
+    IncMax(FHealth, value, PLAYER_HP_LIMIT);
+    Result := True;
+  end;
+
+  if Result and g_Game_IsServer and g_Game_IsNet then
+    MH_SEND_PlayerStats(FUID);
+end;
+
+destructor TPlayer.Destroy();
+begin
+  if (gPlayer1 <> nil) and (gPlayer1.FUID = FUID) then
+    gPlayer1 := nil;
+  if (gPlayer2 <> nil) and (gPlayer2.FUID = FUID) then
+    gPlayer2 := nil;
+
+  FSawSound.Free();
+  FSawSoundIdle.Free();
+  FSawSoundHit.Free();
+  FJetSoundFly.Free();
+  FJetSoundOn.Free();
+  FJetSoundOff.Free();
+  FModel.Free();
+
+  inherited;
+end;
+
+procedure TPlayer.DrawBubble();
+var
+  bubX, bubY: Integer;
+  ID: LongWord;
+  Rb, Gb, Bb,
+  Rw, Gw, Bw: SmallInt;
+  Dot: Byte;
+begin
+  bubX := FObj.X+FObj.Rect.X + IfThen(FDirection = D_LEFT, -4, 18);
+  bubY := FObj.Y+FObj.Rect.Y - 18;
+  Rb := 64;
+  Gb := 64;
+  Bb := 64;
+  Rw := 240;
+  Gw := 240;
+  Bw := 240;
+  case gChatBubble of
+    1: // simple textual non-bubble
+    begin
+      bubX := FObj.X+FObj.Rect.X - 11;
+      bubY := FObj.Y+FObj.Rect.Y - 17;
+      e_TextureFontPrint(bubX, bubY, '[...]', gStdFont);
+      Exit;
+    end;
+    2: // advanced pixel-perfect bubble
+    begin
+      if FTeam = TEAM_RED then
+        Rb := 255
+      else
+        if FTeam = TEAM_BLUE then
+          Bb := 255;
+    end;
+    3: // colored bubble
+    begin
+      Rb := FModel.Color.R;
+      Gb := FModel.Color.G;
+      Bb := FModel.Color.B;
+      Rw := Min(Rb * 2 + 64, 255);
+      Gw := Min(Gb * 2 + 64, 255);
+      Bw := Min(Bb * 2 + 64, 255);
+      if (Abs(Rw - Rb) < 32)
+      or (Abs(Gw - Gb) < 32)
+      or (Abs(Bw - Bb) < 32) then
+      begin
+        Rb := Max(Rw div 2 - 16, 0);
+        Gb := Max(Gw div 2 - 16, 0);
+        Bb := Max(Bw div 2 - 16, 0);
+      end;
+    end;
+    4: // custom textured bubble
+    begin
+      if g_Texture_Get('TEXTURE_PLAYER_TALKBUBBLE', ID) then
+        if FDirection = D_RIGHT then
+          e_Draw(ID, bubX - 6, bubY - 7, 0, True, False)
+        else
+          e_Draw(ID, bubX - 6, bubY - 7, 0, True, False, M_HORIZONTAL);
+      Exit;
+    end;
+  end;
+
+  // Outer borders
+  e_DrawQuad(bubX + 1, bubY    , bubX + 18, bubY + 13, Rb, Gb, Bb);
+  e_DrawQuad(bubX    , bubY + 1, bubX + 19, bubY + 12, Rb, Gb, Bb);
+  // Inner box
+  e_DrawFillQuad(bubX + 1, bubY + 1, bubX + 18, bubY + 12, Rw, Gw, Bw, 0);
+
+  // Tail
+  Dot := IfThen(FDirection = D_LEFT, 14, 5);
+  e_DrawLine(1, bubX + Dot, bubY + 14, bubX + Dot, bubY + 16, Rb, Gb, Bb);
+  e_DrawLine(1, bubX + IfThen(FDirection = D_LEFT, Dot - 1, Dot + 1), bubY + 13, bubX + IfThen(FDirection = D_LEFT, Dot - 1, Dot + 1), bubY + 15, Rw, Gw, Bw);
+  e_DrawLine(1, bubX + IfThen(FDirection = D_LEFT, Dot - 2, Dot + 2), bubY + 13, bubX + IfThen(FDirection = D_LEFT, Dot - 2, Dot + 2), bubY + 14, Rw, Gw, Bw);
+  e_DrawLine(1, bubX + IfThen(FDirection = D_LEFT, Dot - 3, Dot + 3), bubY + 13, bubX + IfThen(FDirection = D_LEFT, Dot - 3, Dot + 3), bubY + 13, Rw, Gw, Bw);
+  e_DrawLine(1, bubX + IfThen(FDirection = D_LEFT, Dot - 3, Dot + 3), bubY + 14, bubX + IfThen(FDirection = D_LEFT, Dot - 1, Dot + 1), bubY + 16, Rb, Gb, Bb);
+
+  // Dots
+  Dot := 6;
+  e_DrawFillQuad(bubX + Dot,     bubY + 8, bubX + Dot + 1, bubY + 9, Rb, Gb, Bb, 0);
+  e_DrawFillQuad(bubX + Dot + 3, bubY + 8, bubX + Dot + 4, bubY + 9, Rb, Gb, Bb, 0);
+  e_DrawFillQuad(bubX + Dot + 6, bubY + 8, bubX + Dot + 7, bubY + 9, Rb, Gb, Bb, 0);
+end;
+
+procedure TPlayer.Draw();
+var
+  ID: DWORD;
+  w, h: Word;
+begin
+  if FLive then
+  begin
+    if (FMegaRulez[MR_INVUL] > gTime) and (gPlayerDrawn <> Self) then
+      if g_Texture_Get('TEXTURE_PLAYER_INVULPENTA', ID) then
+      begin
+        e_GetTextureSize(ID, @w, @h);
+        if FDirection = D_LEFT then
+          e_Draw(ID, FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-(w div 2)+4,
+                     FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2)-(h div 2)-7, 0, True, False)
+        else
+          e_Draw(ID, FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2)-(w div 2)-2,
+                     FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2)-(h div 2)-7, 0, True, False);
+      end;
+
+    if FMegaRulez[MR_INVIS] > gTime then
+    begin
+      if (gPlayerDrawn <> nil) and ((Self = gPlayerDrawn) or
+         ((FTeam = gPlayerDrawn.Team) and (gGameSettings.GameMode <> GM_DM))) then
+        FModel.Draw(FObj.X, FObj.Y, 200)
+      else
+        FModel.Draw(FObj.X, FObj.Y, 254);
+    end
+    else
+      FModel.Draw(FObj.X, FObj.Y);
+  end;
+
+  if g_debug_Frames then
+  begin
+    e_DrawQuad(FObj.X+FObj.Rect.X,
+               FObj.Y+FObj.Rect.Y,
+               FObj.X+FObj.Rect.X+FObj.Rect.Width-1,
+               FObj.Y+FObj.Rect.Y+FObj.Rect.Height-1,
+               0, 255, 0);
+  end;
+
+  if (gChatBubble > 0) and (FKeys[KEY_CHAT].Pressed) and not FGhost then
+    DrawBubble();
+ // e_DrawPoint(5, 335, 288, 255, 0, 0); // DL, UR, DL, UR
+  if gAimLine and Live and
+  ((Self = gPlayer1) or (Self = gPlayer2)) then
+    DrawAim();
+end;
+
+procedure TPlayer.DrawAim();
+var
+  wx, wy, xx, yy: Integer;
+  angle: SmallInt;
+  sz, len: Word;
+begin
+  wx := FObj.X + WEAPONPOINT[FDirection].X + IfThen(FDirection = D_LEFT, 7, -7);
+  wy := FObj.Y + WEAPONPOINT[FDirection].Y;
+  angle := FAngle;
+  len := 1024;
+  sz := 2;
+  case FCurrWeap of
+    0: begin // Punch
+      len := 12;
+      sz := 4;
+    end;
+    1: begin // Chainsaw
+      len := 24;
+      sz := 6;
+    end;
+    2: begin // Pistol
+      len := 1024;
+      sz := 2;
+      if angle = ANGLE_RIGHTUP then Dec(angle, 2);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 4);
+      if angle = ANGLE_LEFTUP then Inc(angle, 2);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 4);
+    end;
+    3: begin // Shotgun
+      len := 1024;
+      sz := 3;
+      if angle = ANGLE_RIGHTUP then Dec(angle, 2);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 4);
+      if angle = ANGLE_LEFTUP then Inc(angle, 2);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 4);
+    end;
+    4: begin // Double Shotgun
+      len := 1024;
+      sz := 4;
+      if angle = ANGLE_RIGHTUP then Dec(angle, 2);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 4);
+      if angle = ANGLE_LEFTUP then Inc(angle, 2);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 4);
+    end;
+    5: begin // Chaingun
+      len := 1024;
+      sz := 3;
+      if angle = ANGLE_RIGHTUP then Dec(angle, 2);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 4);
+      if angle = ANGLE_LEFTUP then Inc(angle, 2);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 4);
+    end;
+    6: begin // Rocket Launcher
+      len := 1024;
+      sz := 7;
+      if angle = ANGLE_RIGHTUP then Inc(angle, 2);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 4);
+      if angle = ANGLE_LEFTUP then Dec(angle, 2);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 4);
+    end;
+    7: begin // Plasmagun
+      len := 1024;
+      sz := 5;
+      if angle = ANGLE_RIGHTUP then Inc(angle);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 3);
+      if angle = ANGLE_LEFTUP then Dec(angle);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 3);
+    end;
+    8: begin // BFG
+      len := 1024;
+      sz := 12;
+      if angle = ANGLE_RIGHTUP then Inc(angle, 1);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 2);
+      if angle = ANGLE_LEFTUP then Dec(angle, 1);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 2);
+    end;
+    9: begin // Super Chaingun
+      len := 1024;
+      sz := 4;
+      if angle = ANGLE_RIGHTUP then Dec(angle, 2);
+      if angle = ANGLE_RIGHTDOWN then Inc(angle, 4);
+      if angle = ANGLE_LEFTUP then Inc(angle, 2);
+      if angle = ANGLE_LEFTDOWN then Dec(angle, 4);
+    end;
+  end;
+  xx := Trunc(Cos(-DegToRad(angle)) * len) + wx;
+  yy := Trunc(Sin(-DegToRad(angle)) * len) + wy;
+  e_DrawLine(sz, wx, wy, xx, yy, 255, 0, 0, 96);
+end;
+
+procedure TPlayer.DrawGUI();
+var
+  ID: DWORD;
+  X, Y, SY, a, p, m: Integer;
+  tw, th: Word;
+  cw, ch: Byte;
+  s: string;
+  stat: TPlayerStatArray;
+begin
+  X := gPlayerScreenSize.X;
+  SY := gPlayerScreenSize.Y;
+  Y := 0;
+
+  if gShowGoals and (gGameSettings.GameMode in [GM_TDM, GM_CTF]) then
+  begin
+    if gGameSettings.GameMode = GM_CTF then
+      a := 32 + 8
+    else
+      a := 0;
+    if gGameSettings.GameMode = GM_CTF then
+    begin
+      s := 'TEXTURE_PLAYER_REDFLAG';
+      if gFlags[FLAG_RED].State = FLAG_STATE_CAPTURED then
+        s := 'TEXTURE_PLAYER_REDFLAG_S';
+      if gFlags[FLAG_RED].State = FLAG_STATE_DROPPED then
+        s := 'TEXTURE_PLAYER_REDFLAG_D';
+      if g_Texture_Get(s, ID) then
+        e_Draw(ID, X-16-32, 240-72-4, 0, True, False);
+    end;
+
+    s := IntToStr(gTeamStat[TEAM_RED].Goals);
+    e_CharFont_GetSize(gMenuFont, s, tw, th);
+    e_CharFont_PrintEx(gMenuFont, X-16-a-tw, 240-72-4, s, TEAMCOLOR[TEAM_RED]);
+
+    if gGameSettings.GameMode = GM_CTF then
+    begin
+      s := 'TEXTURE_PLAYER_BLUEFLAG';
+      if gFlags[FLAG_BLUE].State = FLAG_STATE_CAPTURED then
+        s := 'TEXTURE_PLAYER_BLUEFLAG_S';
+      if gFlags[FLAG_BLUE].State = FLAG_STATE_DROPPED then
+        s := 'TEXTURE_PLAYER_BLUEFLAG_D';
+      if g_Texture_Get(s, ID) then
+        e_Draw(ID,  X-16-32, 240-32-4, 0, True, False);
+    end;
+
+    s := IntToStr(gTeamStat[TEAM_BLUE].Goals);
+    e_CharFont_GetSize(gMenuFont, s, tw, th);
+    e_CharFont_PrintEx(gMenuFont, X-16-a-tw, 240-32-4, s, TEAMCOLOR[TEAM_BLUE]);
+  end;
+
+  if g_Texture_Get('TEXTURE_PLAYER_HUDBG', ID) then
+    e_DrawFill(ID, X, 0, 1, (gPlayerScreenSize.Y div 256)+IfThen(gPlayerScreenSize.Y mod 256 > 0, 1, 0),
+               0, False, False);
+
+  if g_Texture_Get('TEXTURE_PLAYER_HUD', ID) then
+    e_Draw(ID, X+2, Y, 0, True, False);
+
+  if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
+  begin
+    if gShowStat then
+    begin
+      s := IntToStr(Frags);
+      e_CharFont_GetSize(gMenuFont, s, tw, th);
+      e_CharFont_PrintEx(gMenuFont, X-16-tw, Y, s, _RGB(255, 0, 0));
+
+      s := '';
+      p := 1;
+      m := 0;
+      stat := g_Player_GetStats();
+      if stat <> nil then
+      begin
+        p := 1;
+
+        for a := 0 to High(stat) do
+          if stat[a].Name <> Name then
+          begin
+            if stat[a].Frags > m then m := stat[a].Frags;
+            if stat[a].Frags > Frags then p := p+1;
+          end;
+      end;
+
+      s := IntToStr(p)+' / '+IntToStr(Length(stat))+' ';
+      if Frags >= m then s := s+'+' else s := s+'-';
+      s := s+IntToStr(Abs(Frags-m));
+
+      e_CharFont_GetSize(gMenuSmallFont, s, tw, th);
+      e_CharFont_PrintEx(gMenuSmallFont, X-16-tw, Y+32, s, _RGB(255, 0, 0));
+    end;
+
+    if gShowLives and (gGameSettings.MaxLives > 0) then
+    begin
+      s := IntToStr(Lives);
+      e_CharFont_GetSize(gMenuFont, s, tw, th);
+      e_CharFont_PrintEx(gMenuFont, X-16-tw, SY-32, s, _RGB(0, 255, 0));
+    end;
+  end;
+
+  e_CharFont_GetSize(gMenuSmallFont, FName, tw, th);
+  e_CharFont_PrintEx(gMenuSmallFont, X+98-(tw div 2), Y+8, FName, _RGB(255, 0, 0));
+
+  if R_BERSERK in FRulez then
+    e_Draw(gItemsTexturesID[ITEM_MEDKIT_BLACK], X+37, Y+45, 0, True, False)
+  else
+    e_Draw(gItemsTexturesID[ITEM_MEDKIT_LARGE], X+37, Y+45, 0, True, False);
+
+  if g_Texture_Get('TEXTURE_PLAYER_ARMORHUD', ID) then
+    e_Draw(ID, X+36, Y+77, 0, True, False);
+
+  s := IntToStr(IfThen(FHealth > 0, FHealth, 0));
+  e_CharFont_GetSize(gMenuFont, s, tw, th);
+  e_CharFont_PrintEx(gMenuFont, X+178-tw, Y+40, s, _RGB(255, 0, 0));
+
+  s := IntToStr(FArmor);
+  e_CharFont_GetSize(gMenuFont, s, tw, th);
+  e_CharFont_PrintEx(gMenuFont, X+178-tw, Y+68, s, _RGB(255, 0, 0));
+
+  s := IntToStr(GetAmmoByWeapon(FCurrWeap));
+
+  case FCurrWeap of
+    WEAPON_KASTET:
+    begin
+      s := '--';
+      ID := gItemsTexturesID[ITEM_WEAPON_KASTET];
+    end;
+    WEAPON_SAW:
+    begin
+      s := '--';
+      ID := gItemsTexturesID[ITEM_WEAPON_SAW];
+    end;
+    WEAPON_PISTOL: ID := gItemsTexturesID[ITEM_WEAPON_PISTOL];
+    WEAPON_CHAINGUN: ID := gItemsTexturesID[ITEM_WEAPON_CHAINGUN];
+    WEAPON_SHOTGUN1: ID := gItemsTexturesID[ITEM_WEAPON_SHOTGUN1];
+    WEAPON_SHOTGUN2: ID := gItemsTexturesID[ITEM_WEAPON_SHOTGUN2];
+    WEAPON_SUPERPULEMET: ID := gItemsTexturesID[ITEM_WEAPON_SUPERPULEMET];
+    WEAPON_ROCKETLAUNCHER: ID := gItemsTexturesID[ITEM_WEAPON_ROCKETLAUNCHER];
+    WEAPON_PLASMA: ID := gItemsTexturesID[ITEM_WEAPON_PLASMA];
+    WEAPON_BFG: ID := gItemsTexturesID[ITEM_WEAPON_BFG];
+  end;
+
+  e_CharFont_GetSize(gMenuFont, s, tw, th);
+  e_CharFont_PrintEx(gMenuFont, X+178-tw, Y+158, s, _RGB(255, 0, 0));
+  e_Draw(ID, X+20, Y+160, 0, True, False);
+
+  if R_KEY_RED in FRulez then
+    e_Draw(gItemsTexturesID[ITEM_KEY_RED], X+78, Y+214, 0, True, False);
+
+  if R_KEY_GREEN in FRulez then
+    e_Draw(gItemsTexturesID[ITEM_KEY_GREEN], X+95, Y+214, 0, True, False);
+
+  if R_KEY_BLUE in FRulez then
+    e_Draw(gItemsTexturesID[ITEM_KEY_BLUE], X+112, Y+214, 0, True, False);
+
+  if FJetFuel > 0 then
+  begin
+    if g_Texture_Get('TEXTURE_PLAYER_HUDAIR', ID) then
+      e_Draw(ID, X+2, Y+116, 0, True, False);
+    if g_Texture_Get('TEXTURE_PLAYER_HUDJET', ID) then
+      e_Draw(ID, X+2, Y+126, 0, True, False);
+    e_DrawLine(4, X+16, Y+122, X+16+Trunc(168*IfThen(FAir > 0, FAir, 0)/AIR_MAX), Y+122, 0, 0, 196);
+    e_DrawLine(4, X+16, Y+132, X+16+Trunc(168*FJetFuel/JET_MAX), Y+132, 208, 0, 0);
+  end
+  else
+  begin
+    if g_Texture_Get('TEXTURE_PLAYER_HUDAIR', ID) then
+      e_Draw(ID, X+2, Y+124, 0, True, False);
+    e_DrawLine(4, X+16, Y+130, X+16+Trunc(168*IfThen(FAir > 0, FAir, 0)/AIR_MAX), Y+130, 0, 0, 196);
+  end;
+
+  if gShowPing and g_Game_IsClient then
+  begin
+    s := _lc[I_GAME_PING_HUD] + IntToStr(NetPeer.lastRoundTripTime) + _lc[I_NET_SLIST_PING_MS];
+    e_TextureFontPrint(X + 4, Y + 242, s, gStdFont);
+    Y := Y + 16;
+  end;
+
+  if FSpectator then
+  begin
+    e_TextureFontPrint(X + 4, Y + 242, _lc[I_PLAYER_SPECT], gStdFont);
+    e_TextureFontPrint(X + 4, Y + 258, _lc[I_PLAYER_SPECT2], gStdFont);
+    e_TextureFontPrint(X + 4, Y + 274, _lc[I_PLAYER_SPECT1], gStdFont);
+    if FNoRespawn then
+    begin
+      e_TextureFontGetSize(gStdFont, cw, ch);
+      s := _lc[I_PLAYER_SPECT4];
+      e_TextureFontPrintEx(gScreenWidth div 2 - cw*(Length(s) div 2),
+                         gScreenHeight-4-ch, s, gStdFont, 255, 255, 255, 1, True);
+      e_TextureFontPrint(X + 4, Y + 290, _lc[I_PLAYER_SPECT1S], gStdFont);
+    end;
+
+  end;
+end;
+
+procedure TPlayer.DrawRulez();
+var
+  dr: Boolean;
+begin
+  // Ïðè âçÿòèè íåóÿçâèìîñòè ðèñóåòñÿ èíâåðñèîííûé áåëûé ôîí
+  if FMegaRulez[MR_INVUL] >= gTime then
+  begin
+    if (FMegaRulez[MR_INVUL]-gTime) <= 2100 then
+      dr := not Odd((FMegaRulez[MR_INVUL]-gTime) div 300)
+    else
+      dr := True;
+
+    if dr then
+      e_DrawFillQuad(0, 0, gPlayerScreenSize.X-1, gPlayerScreenSize.Y-1,
+                     191, 191, 191, 0, B_INVERT);
+  end;
+
+  // Ïðè âçÿòèè çàùèòíîãî êîñòþìà ðèñóåòñÿ çåëåíîâàòûé ôîí
+  if FMegaRulez[MR_SUIT] >= gTime then
+  begin
+    if (FMegaRulez[MR_SUIT]-gTime) <= 2100 then
+      dr := not Odd((FMegaRulez[MR_SUIT]-gTime) div 300)
+    else
+      dr := True;
+
+    if dr then
+      e_DrawFillQuad(0, 0, gPlayerScreenSize.X-1, gPlayerScreenSize.Y-1,
+                     0, 96, 0, 200, B_NONE);
+  end;
+
+  // Ïðè âçÿòèè áåðñåðêà ðèñóåòñÿ êðàñíîâàòûé ôîí
+  if (FBerserk >= 0) and (LongWord(FBerserk) >= gTime) and (gFlash = 2) then
+  begin
+    e_DrawFillQuad(0, 0, gPlayerScreenSize.X-1, gPlayerScreenSize.Y-1,
+                     255, 0, 0, 200, B_NONE);
+  end;
+end;
+
+procedure TPlayer.DrawPain();
+var
+  a, h: Integer;
+begin
+  if FPain = 0 then Exit;
+
+  a := FPain;
+
+  if a < 15 then h := 0
+  else if a < 35 then h := 1
+  else if a < 55 then h := 2
+  else if a < 75 then h := 3
+  else if a < 95 then h := 4
+  else h := 5;
+
+  //if a > 255 then a := 255;
+
+  e_DrawFillQuad(0, 0, gPlayerScreenSize.X-1, gPlayerScreenSize.Y-1, 255, 0, 0, 255-h*50);
+  //e_DrawFillQuad(0, 0, gPlayerScreenSize.X-1, gPlayerScreenSize.Y-1, 255-min(128, a), 255-a, 255-a, 0, B_FILTER);
+end;
+
+procedure TPlayer.DrawPickup();
+var
+  a, h: Integer;
+begin
+  if FPickup = 0 then Exit;
+
+  a := FPickup;
+
+  if a < 15 then h := 1
+  else if a < 35 then h := 2
+  else if a < 55 then h := 3
+  else if a < 75 then h := 4
+  else h := 5;
+
+  e_DrawFillQuad(0, 0, gPlayerScreenSize.X-1, gPlayerScreenSize.Y-1, 150, 200, 150, 255-h*50);
+end;
+
+procedure TPlayer.Fire();
+var
+  f, DidFire: Boolean;
+  wx, wy, xd, yd: Integer;
+  obj: TObj;
+begin
+  if g_Game_IsClient then Exit;
+// FBFGFireCounter - âðåìÿ ïåðåä âûñòðåëîì (äëÿ BFG)
+// FReloading - âðåìÿ ïîñëå âûñòðåëà (äëÿ âñåãî)
+
+  if FSpectator then
+  begin
+    Respawn(False);
+    Exit;
+  end;
+
+  if FReloading[FCurrWeap] <> 0 then Exit;
+
+  DidFire := False;
+
+  f := False;
+  wx := FObj.X+WEAPONPOINT[FDirection].X;
+  wy := FObj.Y+WEAPONPOINT[FDirection].Y;
+  xd := wx+IfThen(FDirection = D_LEFT, -30, 30);
+  yd := wy+firediry();
+
+  case FCurrWeap of
+    WEAPON_KASTET:
+    begin
+      if R_BERSERK in FRulez then
+      begin
+        //g_Weapon_punch(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y, 75, FUID);
+        obj.X := FObj.X+FObj.Rect.X;
+        obj.Y := FObj.Y+FObj.Rect.Y;
+        obj.rect.X := 0;
+        obj.rect.Y := 0;
+        obj.rect.Width := 39;
+        obj.rect.Height := 52;
+        obj.Vel.X := (xd-wx) div 2;
+        obj.Vel.Y := (yd-wy) div 2;
+        obj.Accel.X := xd-wx;
+        obj.Accel.y := yd-wy;
+
+        if g_Weapon_Hit(@obj, 50, FUID, HIT_SOME) <> 0 then
+          g_Sound_PlayExAt('SOUND_WEAPON_HITBERSERK', FObj.X, FObj.Y)
+        else
+          g_Sound_PlayExAt('SOUND_WEAPON_MISSBERSERK', FObj.X, FObj.Y);
+
+        if gFlash = 1 then
+          if FPain < 50 then
+            FPain := min(FPain + 25, 50);
+      end else g_Weapon_punch(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y, 3, FUID);
+
+      DidFire := True;
+      FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+    end;
+
+    WEAPON_SAW:
+    begin
+      if g_Weapon_chainsaw(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y,
+                           IfThen(gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF], 9, 3), FUID) <> 0 then
+      begin
+        FSawSoundSelect.Stop();
+        FSawSound.Stop();
+        FSawSoundHit.PlayAt(FObj.X, FObj.Y);
+      end
+      else if not FSawSoundHit.IsPlaying() then
+      begin
+        FSawSoundSelect.Stop();
+        FSawSound.PlayAt(FObj.X, FObj.Y);
+      end;
+
+      FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+      DidFire := True;
+      f := True;
+    end;
+
+    WEAPON_PISTOL:
+      if FAmmo[A_BULLETS] > 0 then
+      begin
+        g_Weapon_pistol(wx, wy, xd, yd, FUID);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_BULLETS]);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+        g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                             GameVelX, GameVelY-2, SHELL_BULLET);
+      end;
+
+    WEAPON_SHOTGUN1:
+      if FAmmo[A_SHELLS] > 0 then
+      begin
+        g_Weapon_shotgun(wx, wy, xd, yd, FUID);
+        if not gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN', wx, wy);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_SHELLS]);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+        FShellTimer := 10;
+        FShellType := SHELL_SHELL;
+      end;
+
+    WEAPON_SHOTGUN2:
+      if FAmmo[A_SHELLS] >= 2 then
+      begin
+        g_Weapon_dshotgun(wx, wy, xd, yd, FUID);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_SHELLS], 2);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+        FShellTimer := 13;
+        FShellType := SHELL_DBLSHELL;
+      end;
+
+    WEAPON_CHAINGUN:
+      if FAmmo[A_BULLETS] > 0 then
+      begin
+        g_Weapon_mgun(wx, wy, xd, yd, FUID);
+        if not gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIREPISTOL', wx, wy);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_BULLETS]);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+        g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                             GameVelX, GameVelY-2, SHELL_BULLET);
+      end;
+
+    WEAPON_ROCKETLAUNCHER:
+      if FAmmo[A_ROCKETS] > 0 then
+      begin
+        g_Weapon_rocket(wx, wy, xd, yd, FUID);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_ROCKETS]);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+      end;
+
+    WEAPON_PLASMA:
+      if FAmmo[A_CELLS] > 0 then
+      begin
+        g_Weapon_plasma(wx, wy, xd, yd, FUID);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_CELLS]);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+      end;
+
+    WEAPON_BFG:
+      if (FAmmo[A_CELLS] >= 40) and (FBFGFireCounter = -1) then
+      begin
+        FBFGFireCounter := 17;
+        if not FNoReload then
+          g_Sound_PlayExAt('SOUND_WEAPON_STARTFIREBFG', FObj.X, FObj.Y);
+        Dec(FAmmo[A_CELLS], 40);
+        DidFire := True;
+      end;
+
+    WEAPON_SUPERPULEMET:
+      if FAmmo[A_SHELLS] > 0 then
+      begin
+        g_Weapon_shotgun(wx, wy, xd, yd, FUID);
+        if not gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRECGUN', wx, wy);
+        FReloading[FCurrWeap] := WEAPON_RELOAD[FCurrWeap];
+        Dec(FAmmo[A_SHELLS]);
+        FFireAngle := FAngle;
+        f := True;
+        DidFire := True;
+        g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                             GameVelX, GameVelY-2, SHELL_SHELL);
+      end;
+  end;
+
+  if g_Game_IsNet then
+  begin
+    if DidFire then
+    begin
+      if FCurrWeap <> WEAPON_BFG then
+        MH_SEND_PlayerFire(FUID, FCurrWeap, wx, wy, xd, yd, LastShotID)
+      else
+        if not FNoReload then
+          MH_SEND_Sound(FObj.X, FObj.Y, 'SOUND_WEAPON_STARTFIREBFG');
+    end;
+
+    MH_SEND_PlayerStats(FUID);
+  end;
+
+  if not f then Exit;
+
+  if (FAngle = 0) or (FAngle = 180) then SetAction(A_ATTACK)
+    else if (FAngle = ANGLE_LEFTDOWN) or (FAngle = ANGLE_RIGHTDOWN) then SetAction(A_ATTACKDOWN)
+      else if (FAngle = ANGLE_LEFTUP) or (FAngle = ANGLE_RIGHTUP) then SetAction(A_ATTACKUP);
+end;
+
+function TPlayer.GetAmmoByWeapon(Weapon: Byte): Word;
+begin
+  case Weapon of
+    WEAPON_PISTOL, WEAPON_CHAINGUN: Result := FAmmo[A_BULLETS];
+    WEAPON_SHOTGUN1, WEAPON_SHOTGUN2, WEAPON_SUPERPULEMET: Result := FAmmo[A_SHELLS];
+    WEAPON_ROCKETLAUNCHER: Result := FAmmo[A_ROCKETS];
+    WEAPON_PLASMA, WEAPON_BFG: Result := FAmmo[A_CELLS];
+    else Result := 0;
+  end;
+end;
+
+function TPlayer.HeadInLiquid(XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(FObj.X+PLAYER_HEADRECT.X+XInc, FObj.Y+PLAYER_HEADRECT.Y+YInc,
+                               PLAYER_HEADRECT.Width, PLAYER_HEADRECT.Height,
+                               PANEL_WATER or PANEL_ACID1 or PANEL_ACID2, True);
+end;
+
+procedure TPlayer.JetpackOn;
+begin
+  FJetSoundFly.Stop;
+  FJetSoundOff.Stop;
+  FJetSoundOn.SetPosition(0);
+  FJetSoundOn.PlayAt(FObj.X, FObj.Y);
+  FlySmoke(8);
+end;
+
+procedure TPlayer.JetpackOff;
+begin
+  FJetSoundFly.Stop;
+  FJetSoundOn.Stop;
+  FJetSoundOff.SetPosition(0);
+  FJetSoundOff.PlayAt(FObj.X, FObj.Y);
+end;
+
+procedure TPlayer.Jump();
+begin
+  if gFly or FJetpack then
+  begin
+    // Ïîëåò (÷èò-êîä èëè äæåòïàê):
+    if FObj.Vel.Y > -VEL_FLY then
+      FObj.Vel.Y := FObj.Vel.Y - 3;
+    if FJetpack then
+    begin
+      if FJetFuel > 0 then
+        Dec(FJetFuel);
+      if (FJetFuel < 1) and g_Game_IsServer then
+      begin
+        FJetpack := False;
+        JetpackOff;
+        if g_Game_IsNet then
+          MH_SEND_PlayerStats(FUID);
+      end;
+    end;
+    Exit;
+  end;
+
+// Íå âêëþ÷àòü äæåòïàê â ðåæèìå ïðîõîæäåíèÿ ñêâîçü ñòåíû
+  if FGhost then
+    FCanJetpack := False;
+
+// Ïðûãàåì èëè âñïëûâàåì:
+  if (CollideLevel(0, 1) or
+      g_Map_CollidePanel(FObj.X+PLAYER_RECT.X, FObj.Y+PLAYER_RECT.Y+36, PLAYER_RECT.Width,
+                       PLAYER_RECT.Height-33, PANEL_STEP, False)
+     ) and (FObj.Accel.Y = 0) then // Íå ïðûãàòü, åñëè åñòü âåðòèêàëüíîå óñêîðåíèå
+  begin
+    FObj.Vel.Y := -VEL_JUMP;
+    FCanJetpack := False;
+  end
+  else
+  begin
+    if BodyInLiquid(0, 0) then
+      FObj.Vel.Y := -VEL_SW
+    else if (FJetFuel > 0) and FCanJetpack and
+            g_Game_IsServer and (not g_Obj_CollideLiquid(@FObj, 0, 0)) then
+    begin
+      FJetpack := True;
+      JetpackOn;
+      if g_Game_IsNet then
+        MH_SEND_PlayerStats(FUID);
+    end;
+  end;
+end;
+
+procedure TPlayer.Kill(KillType: Byte; SpawnerUID: Word; t: Byte);
+var
+  a, i, k, ab, ar: Byte;
+  s: String;
+  mon: TMonster;
+  plr: TPlayer;
+  srv, netsrv: Boolean;
+  DoFrags: Boolean;
+  OldLR: Byte;
+  KP: TPlayer;
+
+  procedure PushItem(t: Byte);
+  var
+    id: DWORD;
+  begin
+    id := g_Items_Create(FObj.X, FObj.Y, t, True, False);
+    if KillType = K_EXTRAHARDKILL then // -7..+7; -8..0
+      g_Obj_Push(@gItems[id].Obj, (FObj.Vel.X div 2)-7+Random(15),
+                                  (FObj.Vel.Y div 2)-Random(9))
+    else
+      if KillType = K_HARDKILL then // -5..+5; -5..0
+        g_Obj_Push(@gItems[id].Obj, (FObj.Vel.X div 2)-5+Random(11),
+                                    (FObj.Vel.Y div 2)-Random(6))
+      else // -3..+3; -3..0
+        g_Obj_Push(@gItems[id].Obj, (FObj.Vel.X div 2)-3+Random(7),
+                                    (FObj.Vel.Y div 2)-Random(4));
+
+    if g_Game_IsNet and g_Game_IsServer then
+      MH_SEND_ItemSpawn(True, id);
+  end;
+
+begin
+  DoFrags := (gGameSettings.MaxLives = 0) or (gGameSettings.GameMode = GM_COOP);
+  Srv := g_Game_IsServer;
+  Netsrv := g_Game_IsServer and g_Game_IsNet;
+  if Srv then FDeath := FDeath + 1;
+  if FLive then
+  begin
+    if FGhost then
+      FGhost := False;
+    if not FPhysics then
+      FPhysics := True;
+    FLive := False;
+  end;
+  FShellTimer := -1;
+
+  if (gGameSettings.MaxLives > 0) and Srv and (gLMSRespawn = LMS_RESPAWN_NONE) then
+  begin
+    if FLives > 0 then FLives := FLives - 1;
+    if FLives = 0 then FNoRespawn := True;
+  end;
+
+// Íîìåð òèïà ñìåðòè:
+  a := 1;
+  case KillType of
+    K_SIMPLEKILL:    a := 1;
+    K_HARDKILL:      a := 2;
+    K_EXTRAHARDKILL: a := 3;
+    K_FALLKILL:      a := 4;
+  end;
+
+// Çâóê ñìåðòè:
+  if not FModel.PlaySound(MODELSOUND_DIE, a, FObj.X, FObj.Y) then
+    for i := 1 to 3 do
+      if FModel.PlaySound(MODELSOUND_DIE, i, FObj.X, FObj.Y) then
+        Break;
+
+// Âðåìÿ ðåñïàóíà:
+  if Srv then
+    case KillType of
+      K_SIMPLEKILL:
+        FTime[T_RESPAWN] := gTime + TIME_RESPAWN1;
+      K_HARDKILL:
+        FTime[T_RESPAWN] := gTime + TIME_RESPAWN2;
+      K_EXTRAHARDKILL, K_FALLKILL:
+        FTime[T_RESPAWN] := gTime + TIME_RESPAWN3;
+    end;
+
+// Ïåðåêëþ÷àåì ñîñòîÿíèå:
+  case KillType of
+    K_SIMPLEKILL:
+      SetAction(A_DIE1);
+    K_HARDKILL, K_EXTRAHARDKILL:
+      SetAction(A_DIE2);
+  end;
+
+// Ðåàêöèÿ ìîíñòðîâ íà ñìåðòü èãðîêà:
+  if (KillType <> K_FALLKILL) and (Srv) then
+    g_Monsters_killedp();
+
+  if SpawnerUID = FUID then
+    begin // Ñàìîóáèëñÿ
+      if Srv and (DoFrags or (gGameSettings.GameMode = GM_TDM)) then
+      begin
+        Dec(FFrags);
+        FLastFrag := 0;
+      end;
+      g_Console_Add(Format(_lc[I_PLAYER_KILL_SELF], [FName]), True);
+    end
+  else
+    if g_GetUIDType(SpawnerUID) = UID_PLAYER then
+      begin // Óáèò äðóãèì èãðîêîì
+        KP := g_Player_Get(SpawnerUID);
+        if (KP <> nil) and Srv then
+        begin
+          if (DoFrags or (gGameSettings.GameMode = GM_TDM)) then
+            if SameTeam(FUID, SpawnerUID) then
+            begin
+              Dec(KP.FFrags);
+              KP.FLastFrag := 0;
+            end else
+            begin
+              Inc(KP.FFrags);
+              KP.FragCombo();
+            end;
+
+          if (gGameSettings.GameMode = GM_TDM) and DoFrags then
+            Inc(gTeamStat[KP.Team].Goals,
+              IfThen(SameTeam(FUID, SpawnerUID), -1, 1));
+
+          if netsrv then MH_SEND_PlayerStats(SpawnerUID);
+        end;
+
+        plr := g_Player_Get(SpawnerUID);
+        if plr = nil then
+          s := '?'
+        else
+          s := plr.FName;
+
+        case KillType of
+          K_HARDKILL:
+            g_Console_Add(Format(_lc[I_PLAYER_KILL_EXTRAHARD_2],
+                                 [FName, s]),
+                          gShowKillMsg);
+          K_EXTRAHARDKILL:
+            g_Console_Add(Format(_lc[I_PLAYER_KILL_EXTRAHARD_1],
+                                 [FName, s]),
+                          gShowKillMsg);
+          else
+            g_Console_Add(Format(_lc[I_PLAYER_KILL],
+                                 [FName, s]),
+                          gShowKillMsg);
+        end;
+      end
+    else if g_GetUIDType(SpawnerUID) = UID_MONSTER then
+      begin // Óáèò ìîíñòðîì
+        mon := g_Monsters_Get(SpawnerUID);
+        if mon = nil then
+          s := '?'
+        else
+          s := g_Monsters_GetKilledBy(mon.MonsterType);
+
+        case KillType of
+          K_HARDKILL:
+            g_Console_Add(Format(_lc[I_PLAYER_KILL_EXTRAHARD_2],
+                                 [FName, s]),
+                          gShowKillMsg);
+          K_EXTRAHARDKILL:
+            g_Console_Add(Format(_lc[I_PLAYER_KILL_EXTRAHARD_1],
+                                 [FName, s]),
+                          gShowKillMsg);
+          else
+            g_Console_Add(Format(_lc[I_PLAYER_KILL],
+                                 [FName, s]),
+                          gShowKillMsg);
+        end;
+      end
+    else // Îñîáûå òèïû ñìåðòè
+      case t of
+        HIT_DISCON: ;
+        HIT_SELF: g_Console_Add(Format(_lc[I_PLAYER_KILL_SELF], [FName]), True);
+        HIT_FALL: g_Console_Add(Format(_lc[I_PLAYER_KILL_FALL], [FName]), True);
+        HIT_WATER: g_Console_Add(Format(_lc[I_PLAYER_KILL_WATER], [FName]), True);
+        HIT_ACID: g_Console_Add(Format(_lc[I_PLAYER_KILL_ACID], [FName]), True);
+        HIT_TRAP: g_Console_Add(Format(_lc[I_PLAYER_KILL_TRAP], [FName]), True);
+        else g_Console_Add(Format(_lc[I_PLAYER_DIED], [FName]), True);
+      end;
+
+  if Srv then
+  begin
+// Âûáðîñ îðóæèÿ:
+    for a := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+      if FWeapon[a] then
+      begin
+        case a of
+          WEAPON_SAW: i := ITEM_WEAPON_SAW;
+          WEAPON_SHOTGUN1: i := ITEM_WEAPON_SHOTGUN1;
+          WEAPON_SHOTGUN2: i := ITEM_WEAPON_SHOTGUN2;
+          WEAPON_CHAINGUN: i := ITEM_WEAPON_CHAINGUN;
+          WEAPON_ROCKETLAUNCHER: i := ITEM_WEAPON_ROCKETLAUNCHER;
+          WEAPON_PLASMA: i := ITEM_WEAPON_PLASMA;
+          WEAPON_BFG: i := ITEM_WEAPON_BFG;
+          WEAPON_SUPERPULEMET: i := ITEM_WEAPON_SUPERPULEMET;
+          else i := 0;
+        end;
+
+        if i <> 0 then
+          PushItem(i);
+      end;
+
+// Âûáðîñ ðþêçàêà:
+    if R_ITEM_BACKPACK in FRulez then
+      PushItem(ITEM_AMMO_BACKPACK);
+
+// Âûáðîñ ðàêåòíîãî ðàíöà:
+    if FJetFuel > 0 then
+      PushItem(ITEM_JETPACK);
+
+// Âûáðîñ êëþ÷åé:
+    if not (gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF]) then
+    begin
+      if R_KEY_RED in FRulez then
+        PushItem(ITEM_KEY_RED);
+
+      if R_KEY_GREEN in FRulez then
+        PushItem(ITEM_KEY_GREEN);
+
+      if R_KEY_BLUE in FRulez then
+        PushItem(ITEM_KEY_BLUE);
+    end;
+
+// Âûáðîñ ôëàãà:
+    DropFlag();
+  end;
+
+  g_Player_CreateCorpse(Self);
+
+  if Srv and (gGameSettings.MaxLives > 0) and FNoRespawn and
+     (gLMSRespawn = LMS_RESPAWN_NONE) then
+  begin
+    a := 0;
+    k := 0;
+    ar := 0;
+    ab := 0;
+    for i := Low(gPlayers) to High(gPlayers) do
+    begin
+      if gPlayers[i] = nil then continue;
+      if (not gPlayers[i].FNoRespawn) and (not gPlayers[i].FSpectator) then
+      begin
+        Inc(a);
+        if gPlayers[i].FTeam = TEAM_RED then Inc(ar)
+        else if gPlayers[i].FTeam = TEAM_BLUE then Inc(ab);
+        k := i;
+      end;
+    end;
+
+    OldLR := gLMSRespawn;
+    if (gGameSettings.GameMode = GM_COOP) then
+    begin
+      if (a = 0) then
+      begin
+        // everyone is dead, restart the map
+        g_Game_Message(_lc[I_MESSAGE_LMS_LOSE], 144);
+        if Netsrv then
+          MH_SEND_GameEvent(NET_EV_LMS_LOSE);
+        gLMSRespawn := LMS_RESPAWN_FINAL;
+        gLMSRespawnTime := gTime + 5000;
+      end
+      else if (a = 1) then
+      begin
+        if (gPlayers[k] <> nil) and not (gPlayers[k] is TBot) then
+          if (gPlayers[k] = gPlayer1) or
+             (gPlayers[k] = gPlayer2) then
+            g_Console_Add('*** ' + _lc[I_MESSAGE_LMS_SURVIVOR] + ' ***', True)
+          else if Netsrv and (gPlayers[k].FClientID >= 0) then
+            MH_SEND_GameEvent(NET_EV_LMS_SURVIVOR, 0, 'N', gPlayers[k].FClientID);
+      end;
+    end
+    else if (gGameSettings.GameMode = GM_TDM) then
+    begin
+      if (ab = 0) and (ar <> 0) then
+      begin
+        // blu team ded
+        g_Game_Message(Format(_lc[I_MESSAGE_TLMS_WIN], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 144);
+        if Netsrv then
+          MH_SEND_GameEvent(NET_EV_TLMS_WIN, TEAM_RED);
+        Inc(gTeamStat[TEAM_RED].Goals);
+        gLMSRespawn := LMS_RESPAWN_FINAL;
+        gLMSRespawnTime := gTime + 5000;
+      end
+      else if (ar = 0) and (ab <> 0) then
+      begin
+        // red team ded
+        g_Game_Message(Format(_lc[I_MESSAGE_TLMS_WIN], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 144);
+        if Netsrv then
+          MH_SEND_GameEvent(NET_EV_TLMS_WIN, TEAM_BLUE);
+        Inc(gTeamStat[TEAM_BLUE].Goals);
+        gLMSRespawn := LMS_RESPAWN_FINAL;
+        gLMSRespawnTime := gTime + 5000;
+      end
+      else if (ar = 0) and (ab = 0) then
+      begin
+        // everyone ded
+        g_Game_Message(_lc[I_GAME_WIN_DRAW], 144);
+        if Netsrv then
+          MH_SEND_GameEvent(NET_EV_LMS_DRAW, 0, FName);
+        gLMSRespawn := LMS_RESPAWN_FINAL;
+        gLMSRespawnTime := gTime + 5000;
+      end;
+    end
+    else if (gGameSettings.GameMode = GM_DM) then
+    begin
+      if (a = 1) then
+      begin
+        if gPlayers[k] <> nil then
+          with gPlayers[k] do
+          begin
+            // survivor is the winner
+            g_Game_Message(Format(_lc[I_MESSAGE_LMS_WIN], [AnsiUpperCase(FName)]), 144);
+            if Netsrv then
+              MH_SEND_GameEvent(NET_EV_LMS_WIN, 0, FName);
+            Inc(FFrags);
+          end;
+        gLMSRespawn := LMS_RESPAWN_FINAL;
+        gLMSRespawnTime := gTime + 5000;
+      end
+      else if (a = 0) then
+      begin
+        // everyone is dead, restart the map
+        g_Game_Message(_lc[I_GAME_WIN_DRAW], 144);
+        if Netsrv then
+          MH_SEND_GameEvent(NET_EV_LMS_DRAW, 0, FName);
+        gLMSRespawn := LMS_RESPAWN_FINAL;
+        gLMSRespawnTime := gTime + 5000;
+      end;
+    end;
+    if srv and (OldLR = LMS_RESPAWN_NONE) and (gLMSRespawn > LMS_RESPAWN_NONE) then
+    begin
+      if NetMode = NET_SERVER then
+        MH_SEND_GameEvent(NET_EV_LMS_WARMUP, (gLMSRespawnTime - gTime) div 1000)
+      else
+        g_Console_Add(Format(_lc[I_MSG_WARMUP_START], [(gLMSRespawnTime - gTime) div 1000]), True);
+    end;
+  end;
+
+  if Netsrv then
+  begin
+    MH_SEND_PlayerStats(FUID);
+    MH_SEND_PlayerDeath(FUID, KillType, t, SpawnerUID);
+    if gGameSettings.GameMode = GM_TDM then MH_SEND_GameStats;
+  end;
+
+  if srv and FNoRespawn then Spectate(True);
+  FWantsInGame := True;
+end;
+
+function TPlayer.BodyInLiquid(XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(FObj.X+PLAYER_RECT.X+XInc, FObj.Y+PLAYER_RECT.Y+YInc, PLAYER_RECT.Width,
+                               PLAYER_RECT.Height-20, PANEL_WATER or PANEL_ACID1 or PANEL_ACID2, False);
+end;
+
+function TPlayer.BodyInAcid(XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(FObj.X+PLAYER_RECT.X+XInc, FObj.Y+PLAYER_RECT.Y+YInc, PLAYER_RECT.Width,
+                               PLAYER_RECT.Height-20, PANEL_ACID1 or PANEL_ACID2, False);
+end;
+
+procedure TPlayer.MakeBloodSimple(Count: Word);
+begin
+  g_GFX_Blood(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)+8,
+              FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2),
+              Count div 2, 3, -1, 16, (PLAYER_RECT.Height*2 div 3),
+              150, 0, 0);
+  g_GFX_Blood(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-8,
+              FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2),
+              Count div 2, -3, -1, 16, (PLAYER_RECT.Height*2) div 3,
+              150, 0, 0);
+end;
+
+procedure TPlayer.MakeBloodVector(Count: Word; VelX, VelY: Integer);
+begin
+  g_GFX_Blood(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2),
+              FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2),
+              Count, VelX, VelY, 16, (PLAYER_RECT.Height*2) div 3,
+              150, 0, 0);
+end;
+
+procedure TPlayer.NextWeapon();
+var
+  i: Byte;
+  ok: Boolean;
+begin
+  if g_Game_IsClient then Exit;
+  if FBFGFireCounter <> -1 then Exit;
+
+  if FTime[T_SWITCH] > gTime then Exit;
+
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    if FReloading[i] > 0 then Exit;
+
+  ok := False;
+
+  for i := FCurrWeap+1 to WEAPON_SUPERPULEMET do
+    if FWeapon[i] then
+    begin
+      FCurrWeap := i;
+      ok := True;
+      Break;
+    end;
+
+  if not ok then
+    for i := WEAPON_KASTET to FCurrWeap-1 do
+      if FWeapon[i] then
+      begin
+        FCurrWeap := i;
+        Break;
+      end;
+
+  FTime[T_SWITCH] := gTime+156;
+
+  if FCurrWeap = WEAPON_SAW then
+    FSawSoundSelect.PlayAt(FObj.X, FObj.Y);
+
+  FModel.SetWeapon(FCurrWeap);
+
+  if g_Game_IsNet then MH_SEND_PlayerStats(FUID);
+end;
+
+procedure TPlayer.PrevWeapon();
+var
+  i: Byte;
+  ok: Boolean;
+begin
+  if g_Game_IsClient then Exit;
+  if FBFGFireCounter <> -1 then Exit;
+
+  if FTime[T_SWITCH] > gTime then Exit;
+
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    if FReloading[i] > 0 then Exit;
+
+  ok := False;
+
+  if FCurrWeap > 0 then
+    for i := FCurrWeap-1 downto WEAPON_KASTET do
+      if FWeapon[i] then
+      begin
+        FCurrWeap := i;
+        ok := True;
+        Break;
+      end;
+
+  if not ok then
+    for i := WEAPON_SUPERPULEMET downto FCurrWeap+1 do
+      if FWeapon[i] then
+      begin
+        FCurrWeap := i;
+        Break;
+      end;
+
+  FTime[T_SWITCH] := gTime+156;
+
+  if FCurrWeap = WEAPON_SAW then
+    FSawSoundSelect.PlayAt(FObj.X, FObj.Y);
+
+  FModel.SetWeapon(FCurrWeap);
+
+  if g_Game_IsNet then MH_SEND_PlayerStats(FUID);
+end;
+
+procedure TPlayer.SetWeapon(W: Byte);
+begin
+  if FCurrWeap <> W then
+    if W = WEAPON_SAW then
+      FSawSoundSelect.PlayAt(FObj.X, FObj.Y);
+
+  FCurrWeap := W;
+  FModel.SetWeapon(CurrWeap);
+end;
+
+function TPlayer.PickItem(ItemType: Byte; respawn: Boolean; var remove: Boolean): Boolean;
+var
+  a: Boolean;
+begin
+  Result := False;
+  if g_Game_IsClient then Exit;
+
+  // a = true - ìåñòî ñïàâíà ïðåäìåòà:
+  a := LongBool(gGameSettings.Options and GAME_OPTION_WEAPONSTAY) and respawn;
+  remove := not a;
+
+  case ItemType of
+    ITEM_MEDKIT_SMALL:
+      if FHealth < PLAYER_HP_SOFT then
+      begin
+        IncMax(FHealth, 10, PLAYER_HP_SOFT);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_MEDKIT_LARGE:
+      if FHealth < PLAYER_HP_SOFT then
+      begin
+        IncMax(FHealth, 25, PLAYER_HP_SOFT);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_ARMOR_GREEN:
+      if FArmor < PLAYER_AP_SOFT then
+      begin
+        FArmor := PLAYER_AP_SOFT;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_ARMOR_BLUE:
+      if FArmor < PLAYER_AP_LIMIT then
+      begin
+        FArmor := PLAYER_AP_LIMIT;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_SPHERE_BLUE:
+      if FHealth < PLAYER_HP_LIMIT then
+      begin
+        IncMax(FHealth, 100, PLAYER_HP_LIMIT);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_SPHERE_WHITE:
+      if (FHealth < PLAYER_HP_LIMIT) or (FArmor < PLAYER_AP_LIMIT) then
+      begin
+        if FHealth < PLAYER_HP_LIMIT then
+          FHealth := PLAYER_HP_LIMIT;
+        if FArmor < PLAYER_AP_LIMIT then
+          FArmor := PLAYER_AP_LIMIT;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_WEAPON_SAW:
+      if (not FWeapon[WEAPON_SAW]) or ((not respawn) and (gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF])) then
+      begin
+        FWeapon[WEAPON_SAW] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_SHOTGUN1:
+      if (FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS]) or not FWeapon[WEAPON_SHOTGUN1] then
+      begin
+        // Íóæíî, ÷òîáû íå âçÿòü âñå ïóëè ñðàçó:
+        if a and FWeapon[WEAPON_SHOTGUN1] then Exit;
+
+        IncMax(FAmmo[A_SHELLS], 4, FMaxAmmo[A_SHELLS]);
+        FWeapon[WEAPON_SHOTGUN1] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_SHOTGUN2:
+      if (FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS]) or not FWeapon[WEAPON_SHOTGUN2] then
+      begin
+        if a and FWeapon[WEAPON_SHOTGUN2] then Exit;
+
+        IncMax(FAmmo[A_SHELLS], 4, FMaxAmmo[A_SHELLS]);
+        FWeapon[WEAPON_SHOTGUN2] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_CHAINGUN:
+      if (FAmmo[A_BULLETS] < FMaxAmmo[A_BULLETS]) or not FWeapon[WEAPON_CHAINGUN] then
+      begin
+        if a and FWeapon[WEAPON_CHAINGUN] then Exit;
+
+        IncMax(FAmmo[A_BULLETS], 50, FMaxAmmo[A_BULLETS]);
+        FWeapon[WEAPON_CHAINGUN] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_ROCKETLAUNCHER:
+      if (FAmmo[A_ROCKETS] < FMaxAmmo[A_ROCKETS]) or not FWeapon[WEAPON_ROCKETLAUNCHER] then
+      begin
+        if a and FWeapon[WEAPON_ROCKETLAUNCHER] then Exit;
+
+        IncMax(FAmmo[A_ROCKETS], 2, FMaxAmmo[A_ROCKETS]);
+        FWeapon[WEAPON_ROCKETLAUNCHER] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_PLASMA:
+      if (FAmmo[A_CELLS] < FMaxAmmo[A_CELLS]) or not FWeapon[WEAPON_PLASMA] then
+      begin
+        if a and FWeapon[WEAPON_PLASMA] then Exit;
+
+        IncMax(FAmmo[A_CELLS], 40, FMaxAmmo[A_CELLS]);
+        FWeapon[WEAPON_PLASMA] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_BFG:
+      if (FAmmo[A_CELLS] < FMaxAmmo[A_CELLS]) or not FWeapon[WEAPON_BFG] then
+      begin
+        if a and FWeapon[WEAPON_BFG] then Exit;
+
+        IncMax(FAmmo[A_CELLS], 40, FMaxAmmo[A_CELLS]);
+        FWeapon[WEAPON_BFG] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_WEAPON_SUPERPULEMET:
+      if (FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS]) or not FWeapon[WEAPON_SUPERPULEMET] then
+      begin
+        if a and FWeapon[WEAPON_SUPERPULEMET] then Exit;
+
+        IncMax(FAmmo[A_SHELLS], 4, FMaxAmmo[A_SHELLS]);
+        FWeapon[WEAPON_SUPERPULEMET] := True;
+        Result := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+        if a and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETWEAPON');
+      end;
+
+    ITEM_AMMO_BULLETS:
+      if FAmmo[A_BULLETS] < FMaxAmmo[A_BULLETS] then
+      begin
+        IncMax(FAmmo[A_BULLETS], 10, FMaxAmmo[A_BULLETS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_BULLETS_BOX:
+      if FAmmo[A_BULLETS] < FMaxAmmo[A_BULLETS] then
+      begin
+        IncMax(FAmmo[A_BULLETS], 50, FMaxAmmo[A_BULLETS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_SHELLS:
+      if FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS] then
+      begin
+        IncMax(FAmmo[A_SHELLS], 4, FMaxAmmo[A_SHELLS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_SHELLS_BOX:
+      if FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS] then
+      begin
+        IncMax(FAmmo[A_SHELLS], 25, FMaxAmmo[A_SHELLS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_ROCKET:
+      if FAmmo[A_ROCKETS] < FMaxAmmo[A_ROCKETS] then
+      begin
+        IncMax(FAmmo[A_ROCKETS], 1, FMaxAmmo[A_ROCKETS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_ROCKET_BOX:
+      if FAmmo[A_ROCKETS] < FMaxAmmo[A_ROCKETS] then
+      begin
+        IncMax(FAmmo[A_ROCKETS], 5, FMaxAmmo[A_ROCKETS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_CELL:
+      if FAmmo[A_CELLS] < FMaxAmmo[A_CELLS] then
+      begin
+        IncMax(FAmmo[A_CELLS], 40, FMaxAmmo[A_CELLS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_CELL_BIG:
+      if FAmmo[A_CELLS] < FMaxAmmo[A_CELLS] then
+      begin
+        IncMax(FAmmo[A_CELLS], 100, FMaxAmmo[A_CELLS]);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_AMMO_BACKPACK:
+      if not(R_ITEM_BACKPACK in FRulez) or
+            (FAmmo[A_BULLETS] < FMaxAmmo[A_BULLETS]) or
+            (FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS]) or
+            (FAmmo[A_ROCKETS] < FMaxAmmo[A_ROCKETS]) or
+            (FAmmo[A_CELLS] < FMaxAmmo[A_CELLS]) then
+      begin
+        FMaxAmmo[A_BULLETS] := 400;
+        FMaxAmmo[A_SHELLS] := 100;
+        FMaxAmmo[A_ROCKETS] := 100;
+        FMaxAmmo[A_CELLS] := 600;
+
+        if FAmmo[A_BULLETS] < FMaxAmmo[A_BULLETS] then
+          IncMax(FAmmo[A_BULLETS], 10, FMaxAmmo[A_BULLETS]);
+        if FAmmo[A_SHELLS] < FMaxAmmo[A_SHELLS] then
+          IncMax(FAmmo[A_SHELLS], 4, FMaxAmmo[A_SHELLS]);
+        if FAmmo[A_ROCKETS] < FMaxAmmo[A_ROCKETS] then
+          IncMax(FAmmo[A_ROCKETS], 1, FMaxAmmo[A_ROCKETS]);
+        if FAmmo[A_CELLS] < FMaxAmmo[A_CELLS] then
+          IncMax(FAmmo[A_CELLS], 40, FMaxAmmo[A_CELLS]);
+
+        FRulez := FRulez + [R_ITEM_BACKPACK];
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_KEY_RED:
+      if not(R_KEY_RED in FRulez) then
+      begin
+        Include(FRulez, R_KEY_RED);
+        Result := True;
+        remove := (gGameSettings.GameMode <> GM_COOP) and (g_Player_GetCount() < 2);
+        if gFlash = 2 then Inc(FPickup, 5);
+        if (not remove) and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETITEM');
+      end;
+
+    ITEM_KEY_GREEN:
+      if not(R_KEY_GREEN in FRulez) then
+      begin
+        Include(FRulez, R_KEY_GREEN);
+        Result := True;
+        remove := (gGameSettings.GameMode <> GM_COOP) and (g_Player_GetCount() < 2);
+        if gFlash = 2 then Inc(FPickup, 5);
+        if (not remove) and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETITEM');
+      end;
+
+    ITEM_KEY_BLUE:
+      if not(R_KEY_BLUE in FRulez) then
+      begin
+        Include(FRulez, R_KEY_BLUE);
+        Result := True;
+        remove := (gGameSettings.GameMode <> GM_COOP) and (g_Player_GetCount() < 2);
+        if gFlash = 2 then Inc(FPickup, 5);
+        if (not remove) and g_Game_IsNet then MH_SEND_Sound(GameX, GameY, 'SOUND_ITEM_GETITEM');
+      end;
+
+    ITEM_SUIT:
+      if FMegaRulez[MR_SUIT] < gTime+PLAYER_SUIT_TIME then
+      begin
+        FMegaRulez[MR_SUIT] := gTime+PLAYER_SUIT_TIME;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_OXYGEN:
+      if FAir < AIR_MAX then
+      begin
+        FAir := AIR_MAX;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_MEDKIT_BLACK:
+      begin
+        if not (R_BERSERK in FRulez) then
+        begin
+          Include(FRulez, R_BERSERK);
+          if FBFGFireCounter = -1 then
+          begin
+            FCurrWeap := WEAPON_KASTET;
+            FModel.SetWeapon(WEAPON_KASTET);
+          end;
+          if gFlash <> 0 then
+            Inc(FPain, 100);
+            if gFlash = 2 then Inc(FPickup, 5);
+          FBerserk := gTime+30000;
+          Result := True;
+          remove := True;
+        end;
+        if FHealth < PLAYER_HP_SOFT then
+        begin
+          FHealth := PLAYER_HP_SOFT;
+          FBerserk := gTime+30000;
+          Result := True;
+          remove := True;
+        end;
+      end;
+
+    ITEM_INVUL:
+      if FMegaRulez[MR_INVUL] < gTime+PLAYER_INVUL_TIME then
+      begin
+        FMegaRulez[MR_INVUL] := gTime+PLAYER_INVUL_TIME;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_BOTTLE:
+      if FHealth < PLAYER_HP_LIMIT then
+      begin
+        IncMax(FHealth, 4, PLAYER_HP_LIMIT);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_HELMET:
+      if FArmor < PLAYER_AP_LIMIT then
+      begin
+        IncMax(FArmor, 5, PLAYER_AP_LIMIT);
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_JETPACK:
+      if FJetFuel < JET_MAX then
+      begin
+        FJetFuel := JET_MAX;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+
+    ITEM_INVIS:
+      if FMegaRulez[MR_INVIS] < gTime+PLAYER_INVIS_TIME then
+      begin
+        FMegaRulez[MR_INVIS] := gTime+PLAYER_INVIS_TIME;
+        Result := True;
+        remove := True;
+        if gFlash = 2 then Inc(FPickup, 5);
+      end;
+  end;
+end;
+
+procedure TPlayer.Touch();
+begin
+  if not FLive then
+    Exit;
+  //FModel.PlaySound(MODELSOUND_PAIN, 1, FObj.X, FObj.Y);
+  if FIamBot then
+  begin
+  // Áðîñèòü ôëàã òîâàðèùó:
+    if gGameSettings.GameMode = GM_CTF then
+      DropFlag();
+  end;
+end;
+
+procedure TPlayer.Push(vx, vy: Integer);
+begin
+  if (not FPhysics) and FGhost then
+    Exit;
+  FObj.Accel.X := FObj.Accel.X + vx;
+  FObj.Accel.Y := FObj.Accel.Y + vy;
+  if g_Game_IsNet and g_Game_IsServer then
+    MH_SEND_PlayerPos(True, FUID, NET_EVERYONE);
+end;
+
+procedure TPlayer.Reset(Force: Boolean);
+begin
+  if Force then
+    FLive := False;
+
+  FSpawned := False;
+  FTime[T_RESPAWN] := 0;
+  FTime[T_FLAGCAP] := 0;
+  FGodMode := False;
+  FNoTarget := False;
+  FNoReload := False;
+  FFrags := 0;
+  FLastFrag := 0;
+  FComboEvnt := -1;
+  FKills := 0;
+  FMonsterKills := 0;
+  FDeath := 0;
+  FSecrets := 0;
+  if FNoRespawn then
+  begin
+    FSpectator := False;
+    FGhost := False;
+    FPhysics := True;
+    FSpectatePlayer := -1;
+    FNoRespawn := False;
+  end;
+  FLives := gGameSettings.MaxLives;
+
+  SetFlag(FLAG_NONE);
+end;
+
+procedure TPlayer.SoftReset();
+begin
+  ReleaseKeys();
+
+  FDamageBuffer := 0;
+  FIncCam := 0;
+  FBFGFireCounter := -1;
+  FShellTimer := -1;
+  FPain := 0;
+  FLastHit := 0;
+  FLastFrag := 0;
+  FComboEvnt := -1;
+
+  SetFlag(FLAG_NONE);
+  SetAction(A_STAND, True);
+end;
+
+function TPlayer.GetRespawnPoint(): Byte;
+var
+  c: Byte;
+begin
+  Result := 255;
+  // Íà áóäóùåå: FSpawn - èãðîê óæå èãðàë è ïåðåðîæäàåòñÿ
+
+  // Îäèíî÷íàÿ èãðà/êîîïåðàòèâ
+  if gGameSettings.GameMode in [GM_COOP, GM_SINGLE] then
+  begin
+    if (Self = gPlayer1) or (Self = gPlayer2) then
+    begin
+      // Òî÷êà ïîÿâëåíèÿ ñâîåãî èãðîêà
+      if Self = gPlayer1 then
+        c := RESPAWNPOINT_PLAYER1
+      else
+        c := RESPAWNPOINT_PLAYER2;
+      if g_Map_GetPointCount(c) > 0 then
+      begin
+        Result := c;
+        Exit;
+      end;
+
+      // Òî÷êà ïîÿâëåíèÿ äðóãîãî èãðîêà
+      if Self = gPlayer1 then
+        c := RESPAWNPOINT_PLAYER2
+      else
+        c := RESPAWNPOINT_PLAYER1;
+      if g_Map_GetPointCount(c) > 0 then
+      begin
+        Result := c;
+        Exit;
+      end;
+    end else
+    begin
+      // Òî÷êà ïîÿâëåíèÿ ëþáîãî èãðîêà (áîòà)
+      if Random(2) = 0 then
+        c := RESPAWNPOINT_PLAYER1
+      else
+        c := RESPAWNPOINT_PLAYER2;
+      if g_Map_GetPointCount(c) > 0 then
+      begin
+        Result := c;
+        Exit;
+      end;
+    end;
+
+    // Òî÷êà ëþáîé èç êîìàíä
+    if Random(2) = 0 then
+      c := RESPAWNPOINT_RED
+    else
+      c := RESPAWNPOINT_BLUE;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+
+    // Òî÷êà DM
+    c := RESPAWNPOINT_DM;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+  end;
+
+  // Ìÿñîïîâàë
+  if gGameSettings.GameMode = GM_DM then
+  begin
+    // Òî÷êà DM
+    c := RESPAWNPOINT_DM;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+
+    // Òî÷êà ïîÿâëåíèÿ ëþáîãî èãðîêà
+    if Random(2) = 0 then
+      c := RESPAWNPOINT_PLAYER1
+    else
+      c := RESPAWNPOINT_PLAYER2;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+
+    // Òî÷êà ëþáîé èç êîìàíä
+    if Random(2) = 0 then
+      c := RESPAWNPOINT_RED
+    else
+      c := RESPAWNPOINT_BLUE;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+  end;
+
+  // Êîìàíäíûå
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+  begin
+    // Òî÷êà ñâîåé êîìàíäû
+    c := RESPAWNPOINT_DM;
+    if FTeam = TEAM_RED then
+      c := RESPAWNPOINT_RED;
+    if FTeam = TEAM_BLUE then
+      c := RESPAWNPOINT_BLUE;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+
+    // Òî÷êà DM
+    c := RESPAWNPOINT_DM;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+
+    // Òî÷êà ïîÿâëåíèÿ ëþáîãî èãðîêà
+    if Random(2) = 0 then
+      c := RESPAWNPOINT_PLAYER1
+    else
+      c := RESPAWNPOINT_PLAYER2;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+
+    // Òî÷êà äðóãîé êîìàíäû
+    c := RESPAWNPOINT_DM;
+    if FTeam = TEAM_RED then
+      c := RESPAWNPOINT_BLUE;
+    if FTeam = TEAM_BLUE then
+      c := RESPAWNPOINT_RED;
+    if g_Map_GetPointCount(c) > 0 then
+    begin
+      Result := c;
+      Exit;
+    end;
+  end;
+end;
+
+procedure TPlayer.Respawn(Silent: Boolean; Force: Boolean = False);
+var
+  RespawnPoint: TRespawnPoint;
+  a, b, c: Byte;
+  Anim: TAnimation;
+  ID: DWORD;
+begin
+  if not g_Game_IsServer then
+    Exit;
+  if FDummy then
+    Exit;
+  FWantsInGame := True;
+  FJustTeleported := True;
+  if Force then
+  begin
+    FTime[T_RESPAWN] := 0;
+    FLive := False;
+  end;
+  FNetTime := 0;
+  // if server changes MaxLives we gotta be ready
+  if gGameSettings.MaxLives = 0 then FNoRespawn := False;
+
+// Åùå íåëüçÿ âîçðîäèòüñÿ:
+  if FTime[T_RESPAWN] > gTime then
+    Exit;
+
+// Ïðîñðàë âñå æèçíè:
+  if FNoRespawn then
+  begin
+    if not FSpectator then Spectate(True);
+    FWantsInGame := True;
+    Exit;
+  end;
+
+  if (gGameSettings.GameType <> GT_SINGLE) and (gGameSettings.GameMode <> GM_COOP) then
+    begin // "Ñâîÿ èãðà"
+    // Áåðñåðê íå ñîõðàíÿåòñÿ ìåæäó óðîâíÿìè:
+      FRulez := FRulez-[R_BERSERK];
+    end
+  else // "Îäèíî÷íàÿ èãðà"/"Êîîï"
+    begin
+    // Áåðñåðê è êëþ÷è íå ñîõðàíÿþòñÿ ìåæäó óðîâíÿìè:
+      FRulez := FRulez-[R_KEY_RED, R_KEY_GREEN, R_KEY_BLUE, R_BERSERK];
+    end;
+
+// Ïîëó÷àåì òî÷êó ñïàóíà èãðîêà:
+  c := GetRespawnPoint();
+
+  ReleaseKeys();
+  SetFlag(FLAG_NONE);
+
+// Âîñêðåøåíèå áåç îðóæèÿ:
+  if not FLive then
+  begin
+    FHealth := PLAYER_HP_SOFT;
+    FArmor := 0;
+    FLive := True;
+    FAir := AIR_DEF;
+    FJetFuel := 0;
+
+    for a := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    begin
+      FWeapon[a] := False;
+      FReloading[a] := 0;
+    end;
+
+    FWeapon[WEAPON_PISTOL] := True;
+    FWeapon[WEAPON_KASTET] := True;
+    FCurrWeap := WEAPON_PISTOL;
+
+    FModel.SetWeapon(FCurrWeap);
+
+    for b := A_BULLETS to A_CELLS do
+      FAmmo[b] := 0;
+
+    FAmmo[A_BULLETS] := 50;
+
+    FMaxAmmo[A_BULLETS] := 200;
+    FMaxAmmo[A_SHELLS] := 50;
+    FMaxAmmo[A_ROCKETS] := 50;
+    FMaxAmmo[A_CELLS] := 300;
+
+    if gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF] then
+      FRulez := [R_KEY_RED, R_KEY_GREEN, R_KEY_BLUE]
+    else
+      FRulez := [];
+  end;
+
+// Ïîëó÷àåì êîîðäèíàòû òî÷êè âîçðîæäåíèÿ:
+  if not g_Map_GetPoint(c, RespawnPoint) then
+  begin
+    g_FatalError(_lc[I_GAME_ERROR_GET_SPAWN]);
+    Exit;
+  end;
+
+// Óñòàíîâêà êîîðäèíàò è ñáðîñ âñåõ ïàðàìåòðîâ:
+  FObj.X := RespawnPoint.X-PLAYER_RECT.X;
+  FObj.Y := RespawnPoint.Y-PLAYER_RECT.Y;
+  FObj.Vel.X := 0;
+  FObj.Vel.Y := 0;
+  FObj.Accel.X := 0;
+  FObj.Accel.Y := 0;
+
+  FDirection := RespawnPoint.Direction;
+  if FDirection = D_LEFT then
+    FAngle := 180
+  else
+    FAngle := 0;
+
+  FIncCam := 0;
+  FBFGFireCounter := -1;
+  FShellTimer := -1;
+  FPain := 0;
+  FLastHit := 0;
+
+  SetAction(A_STAND, True);
+  FModel.Direction := FDirection;
+
+  for a := Low(FTime) to High(FTime) do
+    FTime[a] := 0;
+
+  for a := Low(FMegaRulez) to High(FMegaRulez) do
+    FMegaRulez[a] := 0;
+
+  FDamageBuffer := 0;
+  FJetpack := False;
+  FCanJetpack := False;
+
+// Àíèìàöèÿ âîçðîæäåíèÿ:
+  if (not gLoadGameMode) and (not Silent) then
+    if g_Frames_Get(ID, 'FRAMES_TELEPORT') then
+    begin
+      Anim := TAnimation.Create(ID, False, 3);
+      g_GFX_OnceAnim(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-32,
+                     FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)-32, Anim);
+      Anim.Free();
+    end;
+
+  FSpectator := False;
+  FGhost := False;
+  FPhysics := True;
+  FSpectatePlayer := -1;
+  FSpawned := True;
+
+  if g_Game_IsNet then
+  begin
+    MH_SEND_PlayerPos(True, FUID, NET_EVERYONE);
+    MH_SEND_PlayerStats(FUID, NET_EVERYONE);
+    if not Silent then
+      MH_SEND_Effect(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-32,
+                     FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)-32,
+                     0, NET_GFX_TELE);
+  end;
+end;
+
+procedure TPlayer.Spectate(NoMove: Boolean = False);
+begin
+  if FLive then
+    Kill(K_EXTRAHARDKILL, FUID, HIT_SOME)
+  else if (not NoMove) then
+  begin
+    GameX := gMapInfo.Width div 2;
+    GameY := gMapInfo.Height div 2;
+  end;
+  FXTo := GameX;
+  FYTo := GameY;
+
+  FLive := False;
+  FSpectator := True;
+  FGhost := True;
+  FPhysics := False;
+  FWantsInGame := False;
+  FSpawned := False;
+
+  if FNoRespawn then
+  begin
+    if Self = gPlayer1 then
+    begin
+      gLMSPID1 := FUID;
+      gPlayer1 := nil;
+    end;
+    if Self = gPlayer2 then
+    begin
+      gLMSPID2 := FUID;
+      gPlayer2 := nil;
+    end;
+  end;
+
+  if g_Game_IsNet then
+    MH_SEND_PlayerStats(FUID);
+end;
+
+procedure TPlayer.SwitchNoClip;
+begin
+  if not FLive then
+    Exit;
+  FGhost := not FGhost;
+  FPhysics := not FGhost;
+  if FGhost then
+  begin
+    FXTo := FObj.X;
+    FYTo := FObj.Y;
+  end else
+  begin
+    FObj.Accel.X := 0;
+    FObj.Accel.Y := 0;
+  end;
+end;
+
+procedure TPlayer.Run(Direction: TDirection);
+var
+  a, b: Integer;
+begin
+  if MAX_RUNVEL > 8 then
+    FlySmoke();
+
+// Áåæèì:
+  if Direction = D_LEFT then
+    begin
+      if FObj.Vel.X > -MAX_RUNVEL then
+        FObj.Vel.X := FObj.Vel.X - (MAX_RUNVEL shr 3);
+    end
+  else
+    if FObj.Vel.X < MAX_RUNVEL then
+      FObj.Vel.X := FObj.Vel.X + (MAX_RUNVEL shr 3);
+
+// Âîçìîæíî, ïèíàåì êóñêè:
+  if (FObj.Vel.X <> 0) and (gGibs <> nil) then
+  begin
+    b := Abs(FObj.Vel.X);
+    if b > 1 then b := b * (Random(8 div b) + 1);
+    for a := 0 to High(gGibs) do
+      if gGibs[a].Live and
+         g_Obj_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y+FObj.Rect.Height-4,
+                       FObj.Rect.Width, 8, @gGibs[a].Obj) and (Random(3) = 0) then
+        // Ïèíàåì êóñêè
+        if FObj.Vel.X < 0 then
+          g_Obj_PushA(@gGibs[a].Obj, b, Random(61)+120) // íàëåâî
+        else
+          g_Obj_PushA(@gGibs[a].Obj, b, Random(61));    // íàïðàâî
+  end;
+
+  SetAction(A_WALK);
+end;
+
+procedure TPlayer.SeeDown();
+begin
+  SetAction(A_SEEDOWN);
+
+  if FDirection = D_LEFT then FAngle := ANGLE_LEFTDOWN else FAngle := ANGLE_RIGHTDOWN;
+
+  if FIncCam > -120 then DecMin(FIncCam, 5, -120);
+end;
+
+procedure TPlayer.SeeUp();
+begin
+  SetAction(A_SEEUP);
+
+  if FDirection = D_LEFT then FAngle := ANGLE_LEFTUP else FAngle := ANGLE_RIGHTUP;
+
+  if FIncCam < 120 then IncMax(FIncCam, 5, 120);
+end;
+
+procedure TPlayer.SetAction(Action: Byte; Force: Boolean = False);
+var
+  Prior: Byte;
+begin
+  case Action of
+    A_WALK: Prior := 3;
+    A_DIE1: Prior := 5;
+    A_DIE2: Prior := 5;
+    A_ATTACK: Prior := 2;
+    A_SEEUP: Prior := 1;
+    A_SEEDOWN: Prior := 1;
+    A_ATTACKUP: Prior := 2;
+    A_ATTACKDOWN: Prior := 2;
+    A_PAIN: Prior := 4;
+    else Prior := 0;
+  end;
+
+  if (Prior > FActionPrior) or Force then
+    if not ((Prior = 2) and (FCurrWeap = WEAPON_SAW)) then
+    begin
+      FActionPrior := Prior;
+      FActionAnim := Action;
+      FActionForce := Force;
+      FActionChanged := True;
+    end;
+
+  if Action in [A_ATTACK, A_ATTACKUP, A_ATTACKDOWN] then FModel.SetFire(True);
+end;
+
+function TPlayer.StayOnStep(XInc, YInc: Integer): Boolean;
+begin
+  Result :=  not g_Map_CollidePanel(FObj.X+PLAYER_RECT.X, FObj.Y+YInc+PLAYER_RECT.Y+PLAYER_RECT.Height-1,
+                                    PLAYER_RECT.Width, 1, PANEL_STEP, False)
+             and g_Map_CollidePanel(FObj.X+PLAYER_RECT.X, FObj.Y+YInc+PLAYER_RECT.Y+PLAYER_RECT.Height,
+                                    PLAYER_RECT.Width, 1, PANEL_STEP, False);
+end;
+
+function TPlayer.TeleportTo(X, Y: Integer; silent: Boolean; dir: Byte): Boolean;
+var
+  Anim: TAnimation;
+  ID: DWORD;
+begin
+  Result := False;
+
+  if g_CollideLevel(X, Y, PLAYER_RECT.Width, PLAYER_RECT.Height) then
+  begin
+    g_Sound_PlayExAt('SOUND_GAME_NOTELEPORT', FObj.X, FObj.Y);
+    if g_Game_IsServer and g_Game_IsNet then
+      MH_SEND_Sound(FObj.X, FObj.Y, 'SOUND_GAME_NOTELEPORT');
+    Exit;
+  end;
+
+  FJustTeleported := True;
+
+  Anim := nil;
+  if not silent then
+  begin
+    if g_Frames_Get(ID, 'FRAMES_TELEPORT') then
+    begin
+      Anim := TAnimation.Create(ID, False, 3);
+    end;
+
+    g_Sound_PlayExAt('SOUND_GAME_TELEPORT', FObj.X, FObj.Y);
+    g_GFX_OnceAnim(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-32,
+                   FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)-32, Anim);
+    if g_Game_IsServer and g_Game_IsNet then
+      MH_SEND_Effect(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-32,
+                     FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)-32, 1,
+                     NET_GFX_TELE);
+  end;
+
+  FObj.X := X-PLAYER_RECT.X;
+  FObj.Y := Y-PLAYER_RECT.Y;
+  if FLive and FGhost then
+  begin
+    FXTo := FObj.X;
+    FYTo := FObj.Y;
+  end;
+
+  if not g_Game_IsNet then
+  begin
+    if dir = 1 then
+    begin
+      SetDirection(D_LEFT);
+      FAngle := 180;
+    end
+    else
+      if dir = 2 then
+      begin
+        SetDirection(D_RIGHT);
+        FAngle := 0;
+      end
+      else
+        if dir = 3 then
+        begin // îáðàòíîå
+          if FDirection = D_RIGHT then
+          begin
+            SetDirection(D_LEFT);
+            FAngle := 180;
+          end
+          else
+          begin
+            SetDirection(D_RIGHT);
+            FAngle := 0;
+          end;
+        end;
+  end;
+
+  if not silent and (Anim <> nil) then
+  begin
+    g_GFX_OnceAnim(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-32,
+                   FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)-32, Anim);
+    Anim.Free();
+
+    if g_Game_IsServer and g_Game_IsNet then
+      MH_SEND_Effect(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2)-32,
+                     FObj.Y+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)-32, 0,
+                     NET_GFX_TELE);
+  end;
+
+  Result := True;
+end;
+
+function nonz(a: Single): Single;
+begin
+  if a <> 0 then
+    Result := a
+  else
+    Result := 1;
+end;
+
+procedure TPlayer.Update();
+var
+  b: Byte;
+  i, ii, wx, wy, xd, yd, k: Integer;
+  blockmon, headwater, dospawn: Boolean;
+  NetServer: Boolean;
+  AnyServer: Boolean;
+  SetSpect: Boolean;
+begin
+  NetServer := g_Game_IsNet and g_Game_IsServer;
+  AnyServer := g_Game_IsServer;
+
+  if g_Game_IsClient and (NetInterpLevel > 0) then
+    DoLerp(NetInterpLevel + 1)
+  else
+    if FGhost then
+      DoLerp(4);
+
+  if NetServer then
+    if FClientID >= 0 then
+    begin
+      FPing := NetClients[FClientID].Peer^.lastRoundTripTime;
+      if NetClients[FClientID].Peer^.packetsSent > 0 then
+        FLoss := Round(100*NetClients[FClientID].Peer^.packetsLost/NetClients[FClientID].Peer^.packetsSent)
+      else
+        FLoss := 0;
+    end else
+    begin
+      FPing := 0;
+      FLoss := 0;
+    end;
+
+  if FLive and (gFly or FJetpack) then
+    FlySmoke();
+
+  if FDirection = D_LEFT then
+    FAngle := 180
+  else
+    FAngle := 0;
+
+  if FLive and (not FGhost) then
+  begin
+    if FKeys[KEY_UP].Pressed then
+      SeeUp();
+    if FKeys[KEY_DOWN].Pressed then
+      SeeDown();
+  end;
+
+  if (not (FKeys[KEY_UP].Pressed or FKeys[KEY_DOWN].Pressed)) and
+     (FIncCam <> 0) then
+  begin
+    i := g_basic.Sign(FIncCam);
+    FIncCam := Abs(FIncCam);
+    DecMin(FIncCam, 5, 0);
+    FIncCam := FIncCam*i;
+  end;
+
+  if gTime mod (GAME_TICK*2) <> 0 then
+  begin
+    if (FObj.Vel.X = 0) and FLive then
+    begin
+      if FKeys[KEY_LEFT].Pressed then
+        Run(D_LEFT);
+      if FKeys[KEY_RIGHT].Pressed then
+        Run(D_RIGHT);
+    end;
+
+    if FPhysics then
+      g_Obj_Move(@FObj, True, True, True);
+
+    Exit;
+  end;
+
+  FActionChanged := False;
+
+  if FLive then
+  begin
+    // Let alive player do some actions
+    if FKeys[KEY_LEFT].Pressed then Run(D_LEFT);
+    if FKeys[KEY_RIGHT].Pressed then Run(D_RIGHT);
+    if FKeys[KEY_NEXTWEAPON].Pressed and AnyServer then NextWeapon();
+    if FKeys[KEY_PREVWEAPON].Pressed and AnyServer then PrevWeapon();
+    if FKeys[KEY_FIRE].Pressed and AnyServer then Fire();
+    if FKeys[KEY_OPEN].Pressed and AnyServer then Use();
+    if FKeys[KEY_JUMP].Pressed then Jump()
+    else
+    begin
+      if AnyServer and FJetpack then
+      begin
+        FJetpack := False;
+        JetpackOff;
+        if NetServer then MH_SEND_PlayerStats(FUID);
+      end;
+      FCanJetpack := True;
+    end;
+  end
+  else // Dead
+  begin
+    dospawn := False;
+    if not FGhost then
+      for k := Low(FKeys) to KEY_CHAT-1 do
+      begin
+        if FKeys[k].Pressed then
+        begin
+          dospawn := True;
+          break;
+        end;
+      end;
+    if dospawn then
+    begin
+      if gGameSettings.GameType in [GT_CUSTOM, GT_SERVER, GT_CLIENT] then
+        Respawn(False)
+      else // Single
+        if (FTime[T_RESPAWN] <= gTime) and
+          gGameOn and (not FLive) then
+        begin
+          if (g_Player_GetCount() > 1) then
+            Respawn(False)
+          else
+          begin
+            gExit := EXIT_RESTART;
+            Exit;
+          end;
+        end;
+    end;
+    // Dead spectator actions
+    if FGhost then
+    begin
+      if FKeys[KEY_OPEN].Pressed and AnyServer then Fire();
+      if FKeys[KEY_FIRE].Pressed and AnyServer then
+      begin
+        if FSpectator then
+        begin
+          if (FSpectatePlayer >= High(gPlayers)) then
+            FSpectatePlayer := -1
+          else
+          begin
+            SetSpect := False;
+            for I := FSpectatePlayer + 1 to High(gPlayers) do
+              if gPlayers[I] <> nil then
+                if gPlayers[I].Live then
+                  if gPlayers[I].UID <> FUID then
+                  begin
+                    FSpectatePlayer := I;
+                    SetSpect := True;
+                    break;
+                  end;
+
+            if not SetSpect then FSpectatePlayer := -1;
+          end;
+
+          ReleaseKeys;
+        end;
+      end;
+    end;
+  end;
+  // No clipping
+  if FGhost then
+  begin
+    if FKeys[KEY_UP].Pressed or FKeys[KEY_JUMP].Pressed then
+    begin
+      FYTo := FObj.Y - 32;
+      FSpectatePlayer := -1;
+    end;
+    if FKeys[KEY_DOWN].Pressed then
+    begin
+      FYTo := FObj.Y + 32;
+      FSpectatePlayer := -1;
+    end;
+    if FKeys[KEY_LEFT].Pressed then
+    begin
+      FXTo := FObj.X - 32;
+      FSpectatePlayer := -1;
+    end;
+    if FKeys[KEY_RIGHT].Pressed then
+    begin
+      FXTo := FObj.X + 32;
+      FSpectatePlayer := -1;
+    end;
+
+    if (FXTo < -64) then
+      FXTo := -64
+    else if (FXTo > gMapInfo.Width + 32) then
+      FXTo := gMapInfo.Width + 32;
+    if (FYTo < -72) then
+      FYTo := -72
+    else if (FYTo > gMapInfo.Height + 32) then
+      FYTo := gMapInfo.Height + 32;
+  end;
+
+  if FPhysics then
+    g_Obj_Move(@FObj, True, True, True)
+  else
+  begin
+    FObj.Vel.X := 0;
+    FObj.Vel.Y := 0;
+    if FSpectator then
+      if (FSpectatePlayer <= High(gPlayers)) and (FSpectatePlayer >= 0) then
+        if gPlayers[FSpectatePlayer] <> nil then
+          if gPlayers[FSpectatePlayer].Live then
+          begin
+            FXTo := gPlayers[FSpectatePlayer].GameX;
+            FYTo := gPlayers[FSpectatePlayer].GameY;
+          end;
+  end;
+
+  blockmon := g_Map_CollidePanel(FObj.X+PLAYER_HEADRECT.X, FObj.Y+PLAYER_HEADRECT.Y,
+                                 PLAYER_HEADRECT.Width, PLAYER_HEADRECT.Height,
+                                 PANEL_BLOCKMON, True);
+  headwater := HeadInLiquid(0, 0);
+
+// Ñîïðîòèâëåíèå âîçäóõà:
+  if (not FLive) or not (FKeys[KEY_LEFT].Pressed or FKeys[KEY_RIGHT].Pressed) then
+    if FObj.Vel.X <> 0 then
+      FObj.Vel.X := z_dec(FObj.Vel.X, 1);
+
+  if (FLastHit = HIT_TRAP) and (FPain > 90) then FPain := 90;
+  DecMin(FPain, 5, 0);
+  DecMin(FPickup, 1, 0);
+
+  if FLive and (FObj.Y > gMapInfo.Height+128) and AnyServer then
+  begin
+    // Îáíóëèòü äåéñòâèÿ ïðèìî÷åê, ÷òîáû ôîí ïðîïàë
+    FMegaRulez[MR_SUIT] := 0;
+    FMegaRulez[MR_INVUL] := 0;
+    FMegaRulez[MR_INVIS] := 0;
+    Kill(K_FALLKILL, 0, HIT_FALL);
+  end;
+
+  i := 9;
+
+  if FLive then
+  begin
+    if FCurrWeap = WEAPON_SAW then
+      if not (FSawSound.IsPlaying() or FSawSoundHit.IsPlaying() or
+              FSawSoundSelect.IsPlaying()) then
+        FSawSoundIdle.PlayAt(FObj.X, FObj.Y);
+
+    if FJetpack then
+      if (not FJetSoundFly.IsPlaying()) and (not FJetSoundOn.IsPlaying()) and
+         (not FJetSoundOff.IsPlaying()) then
+      begin
+        FJetSoundFly.SetPosition(0);
+        FJetSoundFly.PlayAt(FObj.X, FObj.Y);
+      end;
+
+    for b := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+      if FReloading[b] > 0 then
+        if FNoReload then
+          FReloading[b] := 0
+        else
+          Dec(FReloading[b]);
+
+    if FShellTimer > -1 then
+      if FShellTimer = 0 then
+      begin
+        if FShellType = SHELL_SHELL then
+          g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                               GameVelX, GameVelY-2, SHELL_SHELL)
+        else if FShellType = SHELL_DBLSHELL then
+        begin
+          g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                               GameVelX+1, GameVelY-2, SHELL_SHELL);
+          g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                               GameVelX-1, GameVelY-2, SHELL_SHELL);
+        end;
+        FShellTimer := -1;
+      end else Dec(FShellTimer);
+
+    if (FBFGFireCounter > -1) then
+      if FBFGFireCounter = 0 then
+      begin
+        if AnyServer then
+        begin
+          wx := FObj.X+WEAPONPOINT[FDirection].X;
+          wy := FObj.Y+WEAPONPOINT[FDirection].Y;
+          xd := wx+IfThen(FDirection = D_LEFT, -30, 30);
+          yd := wy+firediry();
+          g_Weapon_bfgshot(wx, wy, xd, yd, FUID);
+          if NetServer then MH_SEND_PlayerFire(FUID, WEAPON_BFG, wx, wy, xd, yd);
+          if (FAngle = 0) or (FAngle = 180) then SetAction(A_ATTACK)
+            else if (FAngle = ANGLE_LEFTDOWN) or (FAngle = ANGLE_RIGHTDOWN) then SetAction(A_ATTACKDOWN)
+              else if (FAngle = ANGLE_LEFTUP) or (FAngle = ANGLE_RIGHTUP) then SetAction(A_ATTACKUP);
+        end;
+
+        FReloading[WEAPON_BFG] := WEAPON_RELOAD[WEAPON_BFG];
+        FBFGFireCounter := -1;
+      end else
+        if FNoReload then
+          FBFGFireCounter := 0
+        else
+          Dec(FBFGFireCounter);
+
+    if (FMegaRulez[MR_SUIT] < gTime) and AnyServer then
+    begin
+      b := g_GetAcidHit(FObj.X+PLAYER_RECT.X, FObj.Y+PLAYER_RECT.Y, PLAYER_RECT.Width, PLAYER_RECT.Height);
+
+      if (b > 0) and (gTime mod (15*GAME_TICK) = 0) then Damage(b, 0, 0, 0, HIT_ACID);
+    end;
+
+    if (headwater or blockmon) then
+    begin
+      Dec(FAir);
+
+      if FAir < -9 then
+      begin
+        if AnyServer then Damage(10, 0, 0, 0, HIT_WATER);
+          FAir := 0;
+      end
+      else if (FAir mod 31 = 0) and not blockmon then
+      begin
+        g_GFX_Bubbles(FObj.X+PLAYER_RECT.X+(PLAYER_RECT.Width div 2), FObj.Y+PLAYER_RECT.Y-4, 5+Random(6), 8, 4);
+        if Random(2) = 0 then
+          g_Sound_PlayExAt('SOUND_GAME_BUBBLE1', FObj.X, FObj.Y)
+        else
+          g_Sound_PlayExAt('SOUND_GAME_BUBBLE2', FObj.X, FObj.Y);
+      end;
+    end else if FAir < AIR_DEF then
+      FAir := AIR_DEF;
+
+    if FDamageBuffer > 0 then
+    begin
+      if FDamageBuffer >= 9 then
+      begin
+        SetAction(A_PAIN);
+
+        if FDamageBuffer < 30 then i := 9
+          else if FDamageBuffer < 100 then i := 18
+            else i := 27;
+      end;
+
+      ii := Round(FDamageBuffer*FHealth / nonz(FArmor*(3/4)+FHealth));
+      FArmor := FArmor-(FDamageBuffer-ii);
+      FHealth := FHealth-ii;
+      if FArmor < 0 then
+      begin
+        FHealth := FHealth+FArmor;
+        FArmor := 0;
+      end;
+
+      if AnyServer then
+        if FHealth <= 0 then
+          if FHealth > -30 then Kill(K_SIMPLEKILL, FLastSpawnerUID, FLastHit)
+            else if FHealth > -50 then Kill(K_HARDKILL, FLastSpawnerUID, FLastHit)
+              else Kill(K_EXTRAHARDKILL, FLastSpawnerUID, FLastHit);
+
+      if FLive then
+      begin
+        if FDamageBuffer <= 20 then FModel.PlaySound(MODELSOUND_PAIN, 1, FObj.X, FObj.Y)
+          else if FDamageBuffer <= 55 then FModel.PlaySound(MODELSOUND_PAIN, 2, FObj.X, FObj.Y)
+            else if FDamageBuffer <= 120 then FModel.PlaySound(MODELSOUND_PAIN, 3, FObj.X, FObj.Y)
+              else FModel.PlaySound(MODELSOUND_PAIN, 4, FObj.X, FObj.Y);
+      end;
+
+      FDamageBuffer := 0;
+    end;
+
+    {CollideItem();}
+  end; // if FLive then ...
+
+  if (FActionAnim = A_PAIN) and (FModel.Animation <> A_PAIN) then
+  begin
+    FModel.ChangeAnimation(FActionAnim, FActionForce);
+    FModel.GetCurrentAnimation.MinLength := i;
+    FModel.GetCurrentAnimationMask.MinLength := i;
+  end else FModel.ChangeAnimation(FActionAnim, FActionForce and (FModel.Animation <> A_STAND));
+
+  if (FModel.GetCurrentAnimation.Played or ((not FActionChanged) and (FModel.Animation = A_WALK)))
+  then SetAction(A_STAND, True);
+
+  if not ((FModel.Animation = A_WALK) and (Abs(FObj.Vel.X) < 4) and not FModel.Fire) then FModel.Update;
+
+  for b := Low(FKeys) to High(FKeys) do
+    if FKeys[b].Time = 0 then FKeys[b].Pressed := False else Dec(FKeys[b].Time);
+end;
+
+function TPlayer.Collide(X, Y: Integer; Width, Height: Word): Boolean;
+begin
+  Result := g_Collide(FObj.X+PLAYER_RECT.X,
+                      FObj.Y+PLAYER_RECT.Y,
+                      PLAYER_RECT.Width,
+                      PLAYER_RECT.Height,
+                      X, Y,
+                      Width, Height);
+end;
+
+function TPlayer.Collide(Panel: TPanel): Boolean;
+begin
+  Result := g_Collide(FObj.X+PLAYER_RECT.X,
+                      FObj.Y+PLAYER_RECT.Y,
+                      PLAYER_RECT.Width,
+                      PLAYER_RECT.Height,
+                      Panel.X, Panel.Y,
+                      Panel.Width, Panel.Height);
+end;
+
+function TPlayer.Collide(X, Y: Integer): Boolean;
+begin
+  X := X-FObj.X-PLAYER_RECT.X;
+  Y := Y-FObj.Y-PLAYER_RECT.Y;
+  Result := (x >= 0) and (x <= PLAYER_RECT.Width) and
+            (y >= 0) and (y <= PLAYER_RECT.Height);
+end;
+
+function g_Player_ValidName(Name: string): Boolean;
+var
+  a: Integer;
+begin
+  Result := True;
+
+  if gPlayers = nil then Exit;
+
+  for a := 0 to High(gPlayers) do
+    if gPlayers[a] <> nil then
+      if LowerCase(Name) = LowerCase(gPlayers[a].FName) then
+      begin
+        Result := False;
+        Exit;
+      end;
+end;
+
+procedure TPlayer.SetDirection(Direction: TDirection);
+var
+  d: TDirection;
+begin
+  d := FModel.Direction;
+
+  FModel.Direction := Direction;
+  if d <> Direction then FModel.ChangeAnimation(FModel.Animation, True);
+
+  FDirection := Direction;
+end;
+
+function TPlayer.GetKeys(): Byte;
+begin
+  Result := 0;
+
+  if R_KEY_RED in FRulez then Result := KEY_RED;
+  if R_KEY_GREEN in FRulez then Result := Result or KEY_GREEN;
+  if R_KEY_BLUE in FRulez then Result := Result or KEY_BLUE;
+
+  if FTeam = TEAM_RED then Result := Result or KEY_REDTEAM;
+  if FTeam = TEAM_BLUE then Result := Result or KEY_BLUETEAM;
+end;
+
+procedure TPlayer.Use();
+var
+  a: Integer;
+begin
+  if FTime[T_USE] > gTime then Exit;
+
+  g_Triggers_PressR(FObj.X+PLAYER_RECT.X, FObj.Y+PLAYER_RECT.Y, PLAYER_RECT.Width,
+                    PLAYER_RECT.Height, FUID, ACTIVATE_PLAYERPRESS);
+
+  for a := 0 to High(gPlayers) do
+    if (gPlayers[a] <> nil) and (gPlayers[a] <> Self) and
+       gPlayers[a].Live and SameTeam(FUID, gPlayers[a].FUID) and
+       g_Obj_Collide(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y,
+                     FObj.Rect.Width, FObj.Rect.Height, @gPlayers[a].FObj) then
+    begin
+      gPlayers[a].Touch();
+      if g_Game_IsNet and g_Game_IsServer then
+        MH_SEND_GameEvent(NET_EV_PLAYER_TOUCH, gPlayers[a].FUID);
+    end;
+
+  FTime[T_USE] := gTime+120;
+end;
+
+procedure TPlayer.NetFire(Wpn: Byte; X, Y, AX, AY: Integer; WID: Integer = -1);
+var
+  Obj: TObj;
+  F: Boolean;
+  WX, WY, XD, YD: Integer;
+begin
+  F := False;
+  WX := X;
+  WY := Y;
+  XD := AX;
+  YD := AY;
+
+  case FCurrWeap of
+    WEAPON_KASTET:
+    begin
+      if R_BERSERK in FRulez then
+      begin
+        //g_Weapon_punch(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y, 75, FUID);
+        obj.X := FObj.X+FObj.Rect.X;
+        obj.Y := FObj.Y+FObj.Rect.Y;
+        obj.rect.X := 0;
+        obj.rect.Y := 0;
+        obj.rect.Width := 39;
+        obj.rect.Height := 52;
+        obj.Vel.X := (xd-wx) div 2;
+        obj.Vel.Y := (yd-wy) div 2;
+        obj.Accel.X := xd-wx;
+        obj.Accel.y := yd-wy;
+
+        if g_Weapon_Hit(@obj, 50, FUID, HIT_SOME) <> 0 then
+          g_Sound_PlayExAt('SOUND_WEAPON_HITBERSERK', FObj.X, FObj.Y)
+        else
+          g_Sound_PlayExAt('SOUND_WEAPON_MISSBERSERK', FObj.X, FObj.Y);
+
+        if gFlash = 1 then
+          if FPain < 50 then
+            FPain := min(FPain + 25, 50);
+      end else
+        g_Weapon_punch(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y, 3, FUID);
+    end;
+
+    WEAPON_SAW:
+    begin
+      if g_Weapon_chainsaw(FObj.X+FObj.Rect.X, FObj.Y+FObj.Rect.Y,
+                           IfThen(gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF], 9, 3), FUID) <> 0 then
+      begin
+        FSawSoundSelect.Stop();
+        FSawSound.Stop();
+        FSawSoundHit.PlayAt(FObj.X, FObj.Y);
+      end
+      else if not FSawSoundHit.IsPlaying() then
+      begin
+        FSawSoundSelect.Stop();
+        FSawSound.PlayAt(FObj.X, FObj.Y);
+      end;
+      f := True;
+    end;
+
+    WEAPON_PISTOL:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIREPISTOL', GameX, Gamey);
+      FFireAngle := FAngle;
+      f := True;
+      g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                             GameVelX, GameVelY-2, SHELL_BULLET);
+    end;
+
+    WEAPON_SHOTGUN1:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN', Gamex, Gamey);
+      FFireAngle := FAngle;
+      f := True;
+      FShellTimer := 10;
+      FShellType := SHELL_SHELL;
+    end;
+
+    WEAPON_SHOTGUN2:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN2', Gamex, Gamey);
+      FFireAngle := FAngle;
+      f := True;
+      FShellTimer := 13;
+      FShellType := SHELL_DBLSHELL;
+    end;
+
+    WEAPON_CHAINGUN:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRECGUN', Gamex, Gamey);
+      FFireAngle := FAngle;
+      f := True;
+      g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                             GameVelX, GameVelY-2, SHELL_BULLET);
+    end;
+
+    WEAPON_ROCKETLAUNCHER:
+    begin
+      g_Weapon_Rocket(wx, wy, xd, yd, FUID, WID);
+      FFireAngle := FAngle;
+      f := True;
+    end;
+
+    WEAPON_PLASMA:
+    begin
+      g_Weapon_Plasma(wx, wy, xd, yd, FUID, WID);
+      FFireAngle := FAngle;
+      f := True;
+    end;
+
+    WEAPON_BFG:
+    begin
+      g_Weapon_BFGShot(wx, wy, xd, yd, FUID, WID);
+      FFireAngle := FAngle;
+      f := True;
+    end;
+
+    WEAPON_SUPERPULEMET:
+    begin
+      g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN', Gamex, Gamey);
+      FFireAngle := FAngle;
+      f := True;
+      g_Player_CreateShell(GameX+PLAYER_RECT_CX, GameY+PLAYER_RECT_CX,
+                             GameVelX, GameVelY-2, SHELL_SHELL);
+    end;
+  end;
+
+  if not f then Exit;
+
+  if (FAngle = 0) or (FAngle = 180) then SetAction(A_ATTACK)
+    else if (FAngle = ANGLE_LEFTDOWN) or (FAngle = ANGLE_RIGHTDOWN) then SetAction(A_ATTACKDOWN)
+      else if (FAngle = ANGLE_LEFTUP) or (FAngle = ANGLE_RIGHTUP) then SetAction(A_ATTACKUP);
+end;
+
+procedure TPlayer.DoLerp(Level: Integer = 2);
+begin
+  if FObj.X <> FXTo then FObj.X := Lerp(FObj.X, FXTo, Level);
+  if FObj.Y <> FYTo then FObj.Y := Lerp(FObj.Y, FYTo, Level);
+end;
+
+procedure TPlayer.SetLerp(XTo, YTo: Integer);
+var
+  AX, AY: Integer;
+begin
+  if NetInterpLevel < 1 then
+  begin
+    FObj.X := XTo;
+    FObj.Y := YTo;
+  end
+  else
+  begin
+    FXTo := XTo;
+    FYTo := YTo;
+
+    AX := Abs(FXTo - FObj.X);
+    AY := Abs(FYTo - FObj.Y);
+    if (AX > 32) or (AX <= NetInterpLevel) then
+      FObj.X := FXTo;
+    if (AY > 32) or (AY <= NetInterpLevel) then
+      FObj.Y := FYTo;
+  end;
+end;
+
+function TPlayer.FullInLift(XInc, YInc: Integer): Integer;
+begin
+  if g_Map_CollidePanel(FObj.X+PLAYER_RECT.X+XInc, FObj.Y+PLAYER_RECT.Y+YInc,
+                        PLAYER_RECT.Width, PLAYER_RECT.Height-8,
+                        PANEL_LIFTUP, False) then Result := -1
+  else
+  if g_Map_CollidePanel(FObj.X+PLAYER_RECT.X+XInc, FObj.Y+PLAYER_RECT.Y+YInc,
+                        PLAYER_RECT.Width, PLAYER_RECT.Height-8,
+                        PANEL_LIFTDOWN, False) then Result := 1
+  else Result := 0;
+end;
+
+function TPlayer.GetFlag(Flag: Byte): Boolean;
+var
+  s, ts: String;
+  evtype: Byte;
+begin
+  Result := False;
+
+  if Flag = FLAG_NONE then
+    Exit;
+
+  if not g_Game_IsServer then Exit;
+
+// Ïðèíåñ ÷óæîé ôëàã íà ñâîþ áàçó:
+  if (Flag = FTeam) and
+     (gFlags[Flag].State = FLAG_STATE_NORMAL) and
+     (FFlag <> FLAG_NONE) then
+  begin
+    if FFlag = FLAG_RED then
+      s := _lc[I_PLAYER_FLAG_RED]
+    else
+      s := _lc[I_PLAYER_FLAG_BLUE];
+
+    evtype := FLAG_STATE_SCORED;
+
+    ts := Format('%.4d', [gFlags[FFlag].CaptureTime]);
+    Insert('.', ts, Length(ts) + 1 - 3);
+    g_Console_Add(Format(_lc[I_PLAYER_FLAG_CAPTURE], [FName, s, ts]), True);
+
+    g_Map_ResetFlag(FFlag);
+    g_Game_Message(Format(_lc[I_MESSAGE_FLAG_CAPTURE], [AnsiUpperCase(s)]), 144);
+
+    gTeamStat[FTeam].Goals := gTeamStat[FTeam].Goals + 1;
+
+    Result := True;
+    if g_Game_IsNet then
+    begin
+      MH_SEND_FlagEvent(evtype, FFlag, FUID, False);
+      MH_SEND_GameStats;
+    end;
+
+    gFlags[FFlag].CaptureTime := 0;
+    SetFlag(FLAG_NONE);
+    Exit;
+  end;
+
+// Ïîäîáðàë ñâîé ôëàã - âåðíóë åãî íà áàçó:
+  if (Flag = FTeam) and
+     (gFlags[Flag].State = FLAG_STATE_DROPPED) then
+  begin
+    if Flag = FLAG_RED then
+      s := _lc[I_PLAYER_FLAG_RED]
+    else
+      s := _lc[I_PLAYER_FLAG_BLUE];
+
+    evtype := FLAG_STATE_RETURNED;
+    gFlags[Flag].CaptureTime := 0;
+
+    g_Console_Add(Format(_lc[I_PLAYER_FLAG_RETURN], [FName, s]), True);
+
+    g_Map_ResetFlag(Flag);
+    g_Game_Message(Format(_lc[I_MESSAGE_FLAG_RETURN], [AnsiUpperCase(s)]), 144);
+
+    Result := True;
+    if g_Game_IsNet then
+    begin
+      MH_SEND_FlagEvent(evtype, Flag, FUID, False);
+      MH_SEND_GameStats;
+    end;
+    Exit;
+  end;
+
+// Ïîäîáðàë ÷óæîé ôëàã:
+  if (Flag <> FTeam) and (FTime[T_FLAGCAP] <= gTime) then
+  begin
+    SetFlag(Flag);
+
+    if Flag = FLAG_RED then
+      s := _lc[I_PLAYER_FLAG_RED]
+    else
+      s := _lc[I_PLAYER_FLAG_BLUE];
+
+    evtype := FLAG_STATE_CAPTURED;
+
+    g_Console_Add(Format(_lc[I_PLAYER_FLAG_GET], [FName, s]), True);
+
+    g_Game_Message(Format(_lc[I_MESSAGE_FLAG_GET], [AnsiUpperCase(s)]), 144);
+
+    gFlags[Flag].State := FLAG_STATE_CAPTURED;
+
+    Result := True;
+    if g_Game_IsNet then
+    begin
+      MH_SEND_FlagEvent(evtype, Flag, FUID, False);
+      MH_SEND_GameStats;
+    end;
+  end;
+end;
+
+procedure TPlayer.SetFlag(Flag: Byte);
+begin
+  FFlag := Flag;
+  if FModel <> nil then
+    FModel.SetFlag(FFlag);
+end;
+
+function TPlayer.DropFlag(): Boolean;
+var
+  s: String;
+begin
+  Result := False;
+  if (not g_Game_IsServer) or (FFlag = FLAG_NONE) then
+    Exit;
+  FTime[T_FLAGCAP] := gTime + 2000;
+  with gFlags[FFlag] do
+  begin
+    Obj.X := FObj.X;
+    Obj.Y := FObj.Y;
+    Direction := FDirection;
+    State := FLAG_STATE_DROPPED;
+    Count := FLAG_TIME;
+    g_Obj_Push(@Obj, (FObj.Vel.X div 2)-2+Random(5),
+                     (FObj.Vel.Y div 2)-2+Random(5));
+
+    if FFlag = FLAG_RED then
+      s := _lc[I_PLAYER_FLAG_RED]
+    else
+      s := _lc[I_PLAYER_FLAG_BLUE];
+
+    g_Console_Add(Format(_lc[I_PLAYER_FLAG_DROP], [FName, s]), True);
+    g_Game_Message(Format(_lc[I_MESSAGE_FLAG_DROP], [AnsiUpperCase(s)]), 144);
+
+    if g_Game_IsNet then
+      MH_SEND_FlagEvent(FLAG_STATE_DROPPED, Flag, FUID, False);
+  end;
+  SetFlag(FLAG_NONE);
+  Result := True;
+end;
+
+procedure TPlayer.GetSecret();
+begin
+  Inc(FSecrets);
+end;
+
+procedure TPlayer.PressKey(Key: Byte; Time: Word = 1);
+begin
+  Assert(Key <= High(FKeys));
+
+  FKeys[Key].Pressed := True;
+  FKeys[Key].Time := Time;
+end;
+
+function TPlayer.IsKeyPressed(K: Byte): Boolean;
+begin
+  Result := FKeys[K].Pressed;
+end;
+
+procedure TPlayer.ReleaseKeys();
+var
+  a: Integer;
+begin
+  for a := Low(FKeys) to High(FKeys) do
+  begin
+    FKeys[a].Pressed := False;
+    FKeys[a].Time := 0;
+  end;
+end;
+
+procedure TPlayer.OnDamage(Angle: SmallInt);
+begin
+end;
+
+function TPlayer.firediry(): Integer;
+begin
+  if FKeys[KEY_UP].Pressed then Result := -42
+  else if FKeys[KEY_DOWN].Pressed then Result := 19
+  else Result := 0;
+end;
+
+procedure TPlayer.RememberState();
+var
+  i: Integer;
+begin
+  FSavedState.Health := FHealth;
+  FSavedState.Armor := FArmor;
+  FSavedState.Air := FAir;
+  FSavedState.JetFuel := FJetFuel;
+  FSavedState.CurrWeap := FCurrWeap;
+
+  for i := 0 to 3 do
+    FSavedState.Ammo[i] := FAmmo[i];
+  for i := 0 to 3 do
+    FSavedState.MaxAmmo[i] := FMaxAmmo[i];
+
+  FSavedState.Rulez := FRulez;
+  FSavedState.WaitRecall := True;
+end;
+
+procedure TPlayer.RecallState();
+var
+  i: Integer;
+begin
+  if not FSavedState.WaitRecall then Exit;
+
+  FHealth := FSavedState.Health;
+  FArmor := FSavedState.Armor;
+  FAir := FSavedState.Air;
+  FJetFuel := FSavedState.JetFuel;
+  FCurrWeap := FSavedState.CurrWeap;
+
+  for i := 0 to 3 do
+    FAmmo[i] := FSavedState.Ammo[i];
+  for i := 0 to 3 do
+    FMaxAmmo[i] := FSavedState.MaxAmmo[i];
+
+  FRulez := FSavedState.Rulez;
+  FSavedState.WaitRecall := False;
+
+  if gGameSettings.GameType = GT_SERVER then
+    MH_SEND_PlayerStats(FUID);
+end;
+
+procedure TPlayer.SaveState(var Mem: TBinMemoryWriter);
+var
+  i: Integer;
+  sig: DWORD;
+  str: String;
+  b: Byte;
+begin
+  if FIamBot then
+    i := 512
+  else
+    i := 256;
+
+  Mem := TBinMemoryWriter.Create(i);
+
+// Ñèãíàòóðà èãðîêà:
+  sig := PLAYER_SIGNATURE; // 'PLYR'
+  Mem.WriteDWORD(sig);
+// Áîò èëè ÷åëîâåê:
+  Mem.WriteBoolean(FIamBot);
+// UID èãðîêà:
+  Mem.WriteWord(FUID);
+// Èìÿ èãðîêà:
+  Mem.WriteString(FName, 32);
+// Êîìàíäà:
+  Mem.WriteByte(FTeam);
+// Æèâ ëè:
+  Mem.WriteBoolean(FLive);
+// Èçðàñõîäîâàë ëè âñå æèçíè:
+  Mem.WriteBoolean(FNoRespawn);
+// Íàïðàâëåíèå:
+  if FDirection = D_LEFT then
+    b := 1
+  else // D_RIGHT
+    b := 2;
+  Mem.WriteByte(b);
+// Çäîðîâüå:
+  Mem.WriteInt(FHealth);
+// Æèçíè:
+  Mem.WriteByte(FLives);
+// Áðîíÿ:
+  Mem.WriteInt(FArmor);
+// Çàïàñ âîçäóõà:
+  Mem.WriteInt(FAir);
+// Çàïàñ ãîðþ÷åãî:
+  Mem.WriteInt(FJetFuel);
+// Áîëü:
+  Mem.WriteInt(FPain);
+// Óáèë:
+  Mem.WriteInt(FKills);
+// Óáèë ìîíñòðîâ:
+  Mem.WriteInt(FMonsterKills);
+// Ôðàãîâ:
+  Mem.WriteInt(FFrags);
+// Ôðàãîâ ïîäðÿä:
+  Mem.WriteByte(FFragCombo);
+// Âðåìÿ ïîñëåäíåãî ôðàãà:
+  Mem.WriteDWORD(FLastFrag);
+// Ñìåðòåé:
+  Mem.WriteInt(FDeath);
+// Êàêîé ôëàã íåñåò:
+  Mem.WriteByte(FFlag);
+// Íàøåë ñåêðåòîâ:
+  Mem.WriteInt(FSecrets);
+// Òåêóùåå îðóæèå:
+  Mem.WriteByte(FCurrWeap);
+// Âðåìÿ çàðÿäêè BFG:
+  Mem.WriteSmallInt(FBFGFireCounter);
+// Áóôåð óðîíà:
+  Mem.WriteInt(FDamageBuffer);
+// Ïîñëåäíèé óäàðèâøèé:
+  Mem.WriteWord(FLastSpawnerUID);
+// Òèï ïîñëåäíåãî ïîëó÷åííîãî óðîíà:
+  Mem.WriteByte(FLastHit);
+// Îáúåêò èãðîêà:
+  Obj_SaveState(@FObj, Mem);
+// Òåêóùåå êîëè÷åñòâî ïàòðîíîâ:
+  for i := A_BULLETS to A_CELLS do
+    Mem.WriteWord(FAmmo[i]);
+// Ìàêñèìàëüíîå êîëè÷åñòâî ïàòðîíîâ:
+  for i := A_BULLETS to A_CELLS do
+    Mem.WriteWord(FMaxAmmo[i]);
+// Íàëè÷èå îðóæèÿ:
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    Mem.WriteBoolean(FWeapon[i]);
+// Âðåìÿ ïåðåçàðÿäêè îðóæèÿ:
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    Mem.WriteWord(FReloading[i]);
+// Íàëè÷èå ðþêçàêà:
+  if R_ITEM_BACKPACK in FRulez then
+    b := 1
+  else
+    b := 0;
+  Mem.WriteByte(b);
+// Íàëè÷èå êðàñíîãî êëþ÷à:
+  if R_KEY_RED in FRulez then
+    b := 1
+  else
+    b := 0;
+  Mem.WriteByte(b);
+// Íàëè÷èå çåëåíîãî êëþ÷à:
+  if R_KEY_GREEN in FRulez then
+    b := 1
+  else
+    b := 0;
+  Mem.WriteByte(b);
+// Íàëè÷èå ñèíåãî êëþ÷à:
+  if R_KEY_BLUE in FRulez then
+    b := 1
+  else
+    b := 0;
+  Mem.WriteByte(b);
+// Íàëè÷èå áåðñåðêà:
+  if R_BERSERK in FRulez then
+    b := 1
+  else
+    b := 0;
+  Mem.WriteByte(b);
+// Âðåìÿ äåéñòâèÿ ñïåöèàëüíûõ ïðåäìåòîâ:
+  for i := MR_SUIT to MR_MAX do
+    Mem.WriteDWORD(FMegaRulez[i]);
+// Âðåìÿ äî ïîâòîðíîãî ðåñïàóíà, ñìåíû îðóæèÿ, èñîëüçîâàíèÿ, çàõâàòà ôëàãà:
+  for i := T_RESPAWN to T_FLAGCAP do
+    Mem.WriteDWORD(FTime[i]);
+// Íàçâàíèå ìîäåëè:
+  str := FModel.Name;
+  Mem.WriteString(str);
+// Öâåò ìîäåëè:
+  b := FColor.R;
+  Mem.WriteByte(b);
+  b := FColor.G;
+  Mem.WriteByte(b);
+  b := FColor.B;
+  Mem.WriteByte(b);
+end;
+
+procedure TPlayer.LoadState(var Mem: TBinMemoryReader);
+var
+  i: Integer;
+  sig: DWORD;
+  str: String;
+  b: Byte;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà èãðîêà:
+  Mem.ReadDWORD(sig);
+  if sig <> PLAYER_SIGNATURE then // 'PLYR'
+  begin
+    raise EBinSizeError.Create('TPlayer.LoadState: Wrong Player Signature');
+  end;
+// Áîò èëè ÷åëîâåê:
+  Mem.ReadBoolean(FIamBot);
+// UID èãðîêà:
+  Mem.ReadWord(FUID);
+// Èìÿ èãðîêà:
+  Mem.ReadString(str);
+  if (Self <> gPlayer1) and (Self <> gPlayer2) then
+    FName := str;
+// Êîìàíäà:
+  Mem.ReadByte(FTeam);
+// Æèâ ëè:
+  Mem.ReadBoolean(FLive);
+// Èçðàñõîäîâàë ëè âñå æèçíè:
+  Mem.ReadBoolean(FNoRespawn);
+// Íàïðàâëåíèå:
+  Mem.ReadByte(b);
+  if b = 1 then
+    FDirection := D_LEFT
+  else // b = 2
+    FDirection := D_RIGHT;
+// Çäîðîâüå:
+  Mem.ReadInt(FHealth);
+// Æèçíè:
+  Mem.ReadByte(FLives);
+// Áðîíÿ:
+  Mem.ReadInt(FArmor);
+// Çàïàñ âîçäóõà:
+  Mem.ReadInt(FAir);
+// Çàïàñ ãîðþ÷åãî:
+  Mem.ReadInt(FJetFuel);
+// Áîëü:
+  Mem.ReadInt(FPain);
+// Óáèë:
+  Mem.ReadInt(FKills);
+// Óáèë ìîíñòðîâ:
+  Mem.ReadInt(FMonsterKills);
+// Ôðàãîâ:
+  Mem.ReadInt(FFrags);
+// Ôðàãîâ ïîäðÿä:
+  Mem.ReadByte(FFragCombo);
+// Âðåìÿ ïîñëåäíåãî ôðàãà:
+  Mem.ReadDWORD(FLastFrag);
+// Ñìåðòåé:
+  Mem.ReadInt(FDeath);
+// Êàêîé ôëàã íåñåò:
+  Mem.ReadByte(FFlag);
+// Íàøåë ñåêðåòîâ:
+  Mem.ReadInt(FSecrets);
+// Òåêóùåå îðóæèå:
+  Mem.ReadByte(FCurrWeap);
+// Âðåìÿ çàðÿäêè BFG:
+  Mem.ReadSmallInt(FBFGFireCounter);
+// Áóôåð óðîíà:
+  Mem.ReadInt(FDamageBuffer);
+// Ïîñëåäíèé óäàðèâøèé:
+  Mem.ReadWord(FLastSpawnerUID);
+// Òèï ïîñëåäíåãî ïîëó÷åííîãî óðîíà:
+  Mem.ReadByte(FLastHit);
+// Îáúåêò èãðîêà:
+  Obj_LoadState(@FObj, Mem);
+// Òåêóùåå êîëè÷åñòâî ïàòðîíîâ:
+  for i := A_BULLETS to A_CELLS do
+    Mem.ReadWord(FAmmo[i]);
+// Ìàêñèìàëüíîå êîëè÷åñòâî ïàòðîíîâ:
+  for i := A_BULLETS to A_CELLS do
+    Mem.ReadWord(FMaxAmmo[i]);
+// Íàëè÷èå îðóæèÿ:
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    Mem.ReadBoolean(FWeapon[i]);
+// Âðåìÿ ïåðåçàðÿäêè îðóæèÿ:
+  for i := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+    Mem.ReadWord(FReloading[i]);
+// Íàëè÷èå ðþêçàêà:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(FRulez, R_ITEM_BACKPACK);
+// Íàëè÷èå êðàñíîãî êëþ÷à:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(FRulez, R_KEY_RED);
+// Íàëè÷èå çåëåíîãî êëþ÷à:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(FRulez, R_KEY_GREEN);
+// Íàëè÷èå ñèíåãî êëþ÷à:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(FRulez, R_KEY_BLUE);
+// Íàëè÷èå áåðñåðêà:
+  Mem.ReadByte(b);
+  if b = 1 then
+    Include(FRulez, R_BERSERK);
+// Âðåìÿ äåéñòâèÿ ñïåöèàëüíûõ ïðåäìåòîâ:
+  for i := MR_SUIT to MR_MAX do
+    Mem.ReadDWORD(FMegaRulez[i]);
+// Âðåìÿ äî ïîâòîðíîãî ðåñïàóíà, ñìåíû îðóæèÿ, èñîëüçîâàíèÿ, çàõâàòà ôëàãà:
+  for i := T_RESPAWN to T_FLAGCAP do
+    Mem.ReadDWORD(FTime[i]);
+// Íàçâàíèå ìîäåëè:
+  Mem.ReadString(str);
+// Öâåò ìîäåëè:
+  Mem.ReadByte(FColor.R);
+  Mem.ReadByte(FColor.G);
+  Mem.ReadByte(FColor.B);
+  if Self = gPlayer1 then
+  begin
+    str := gPlayer1Settings.Model;
+    FColor := gPlayer1Settings.Color;
+  end;
+  if Self = gPlayer2 then
+  begin
+    str := gPlayer2Settings.Model;
+    FColor := gPlayer2Settings.Color;
+  end;
+// Îáíîâëÿåì ìîäåëü èãðîêà:
+  SetModel(str);
+  if gGameSettings.GameMode in [GM_TDM, GM_CTF] then
+    FModel.Color := TEAMCOLOR[FTeam]
+  else
+    FModel.Color := FColor;
+end;
+
+procedure TPlayer.AllRulez(Health: Boolean);
+var
+  a: Integer;
+begin
+  if Health then
+  begin
+    FHealth := PLAYER_HP_LIMIT;
+    FArmor := PLAYER_AP_LIMIT;
+    Exit;
+  end;
+
+  for a := WEAPON_KASTET to WEAPON_SUPERPULEMET do FWeapon[a] := True;
+  for a := A_BULLETS to A_CELLS do FAmmo[a] := 30000;
+  FRulez := FRulez+[R_KEY_RED, R_KEY_GREEN, R_KEY_BLUE];
+end;
+
+procedure TPlayer.FragCombo();
+var
+  Param: Integer;
+begin
+  if (gGameSettings.GameMode in [GM_COOP, GM_SINGLE]) or g_Game_IsClient then
+    Exit;
+  if gTime - FLastFrag < FRAG_COMBO_TIME then
+  begin
+    if FFragCombo < 5 then
+      Inc(FFragCombo);
+    Param := FUID or (FFragCombo shl 16);
+    if (FComboEvnt >= Low(gDelayedEvents)) and
+       (FComboEvnt <= High(gDelayedEvents)) and
+       gDelayedEvents[FComboEvnt].Pending and
+       (gDelayedEvents[FComboEvnt].DEType = DE_KILLCOMBO) and
+       (gDelayedEvents[FComboEvnt].DENum and $FFFF = FUID) then
+    begin
+      gDelayedEvents[FComboEvnt].Time := gTime + 500;
+      gDelayedEvents[FComboEvnt].DENum := Param;
+    end
+    else
+      FComboEvnt := g_Game_DelayEvent(DE_KILLCOMBO, 500, Param);
+  end
+  else
+    FFragCombo := 1;
+
+  FLastFrag := gTime;
+end;
+
+procedure TPlayer.GiveItem(ItemType: Byte);
+begin
+  case ItemType of
+    ITEM_SUIT:
+      if FMegaRulez[MR_SUIT] < gTime+PLAYER_SUIT_TIME then
+      begin
+        FMegaRulez[MR_SUIT] := gTime+PLAYER_SUIT_TIME;
+      end;
+
+    ITEM_OXYGEN:
+      if FAir < AIR_MAX then
+      begin
+        FAir := AIR_MAX;
+      end;
+
+    ITEM_MEDKIT_BLACK:
+      begin
+        if not (R_BERSERK in FRulez) then
+        begin
+          Include(FRulez, R_BERSERK);
+          if FBFGFireCounter < 1 then
+          begin
+            FCurrWeap := WEAPON_KASTET;
+            FModel.SetWeapon(WEAPON_KASTET);
+          end;
+          if gFlash <> 0 then
+            Inc(FPain, 100);
+          FBerserk := gTime+30000;
+        end;
+        if FHealth < PLAYER_HP_SOFT then
+        begin
+          FHealth := PLAYER_HP_SOFT;
+          FBerserk := gTime+30000;
+        end;
+      end;
+
+    ITEM_INVUL:
+      if FMegaRulez[MR_INVUL] < gTime+PLAYER_INVUL_TIME then
+      begin
+        FMegaRulez[MR_INVUL] := gTime+PLAYER_INVUL_TIME;
+      end;
+
+    ITEM_INVIS:
+      if FMegaRulez[MR_INVIS] < gTime+PLAYER_INVIS_TIME then
+      begin
+        FMegaRulez[MR_INVIS] := gTime+PLAYER_INVIS_TIME;
+      end;
+
+    ITEM_JETPACK:
+      if FJetFuel < JET_MAX then
+      begin
+        FJetFuel := JET_MAX;
+      end;
+
+    else
+      Exit;
+  end;
+  if g_Game_IsNet and g_Game_IsServer then
+    MH_SEND_PlayerStats(FUID);
+end;
+
+procedure TPlayer.FlySmoke(Times: DWORD = 1);
+var
+  id, i: DWORD;
+  Anim: TAnimation;
+begin
+  if (Random(5) = 1) and (Times = 1) then
+    Exit;
+
+  if BodyInLiquid(0, 0) then
+  begin
+    g_GFX_Bubbles(Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2)+Random(3)-1,
+                  Obj.Y+Obj.Rect.Height+8, 1, 8, 4);
+    if Random(2) = 0 then
+      g_Sound_PlayExAt('SOUND_GAME_BUBBLE1', FObj.X, FObj.Y)
+    else
+      g_Sound_PlayExAt('SOUND_GAME_BUBBLE2', FObj.X, FObj.Y);
+    Exit;
+  end;
+
+  if g_Frames_Get(id, 'FRAMES_SMOKE') then
+  begin
+    for i := 1 to Times do
+    begin
+      Anim := TAnimation.Create(id, False, 3);
+      Anim.Alpha := 150;
+      g_GFX_OnceAnim(Obj.X+Obj.Rect.X+Random(Obj.Rect.Width+Times*2)-(Anim.Width div 2),
+                   Obj.Y+Obj.Rect.Height-4+Random(8+Times*2), Anim, ONCEANIM_SMOKE);
+      Anim.Free();
+    end;
+  end;
+end;
+
+procedure TPlayer.PauseSounds(Enable: Boolean);
+begin
+  FSawSound.Pause(Enable);
+  FSawSoundIdle.Pause(Enable);
+  FSawSoundHit.Pause(Enable);
+  FSawSoundSelect.Pause(Enable);
+end;
+
+{ T C o r p s e : }
+
+constructor TCorpse.Create(X, Y: Integer; ModelName: String; aMess: Boolean);
+begin
+  g_Obj_Init(@FObj);
+  FObj.X := X;
+  FObj.Y := Y;
+  FObj.Rect := PLAYER_CORPSERECT;
+  FModelName := ModelName;
+  FMess := aMess;
+
+  if FMess then
+    begin
+      FState := CORPSE_STATE_MESS;
+      g_PlayerModel_GetAnim(ModelName, A_DIE2, FAnimation, FAnimationMask);
+    end
+  else
+    begin
+      FState := CORPSE_STATE_NORMAL;
+      g_PlayerModel_GetAnim(ModelName, A_DIE1, FAnimation, FAnimationMask);
+    end;
+end;
+
+destructor TCorpse.Destroy();
+begin
+  FAnimation.Free();
+
+  inherited;
+end;
+
+procedure TCorpse.Damage(Value: Word; vx, vy: Integer);
+var
+  pm: TPlayerModel;
+begin
+  if FState = CORPSE_STATE_REMOVEME then
+    Exit;
+
+  FDamage := FDamage + Value;
+
+  if FDamage > 150 then
+    begin
+      if FAnimation <> nil then
+      begin
+        FAnimation.Free();
+        FAnimation := nil;
+
+        FState := CORPSE_STATE_REMOVEME;
+
+        g_Player_CreateGibs(FObj.X+FObj.Rect.X+(FObj.Rect.Width div 2),
+                            FObj.Y+FObj.Rect.Y+(FObj.Rect.Height div 2),
+                            FModelName, FColor);
+        pm := g_PlayerModel_Get(FModelName);
+        pm.PlaySound(MODELSOUND_DIE, 3, FObj.X, FObj.Y);
+        pm.Free;
+      end;
+    end
+  else
+    begin
+      FObj.Vel.X := FObj.Vel.X + vx;
+      FObj.Vel.Y := FObj.Vel.Y + vy;
+      g_GFX_Blood(FObj.X+PLAYER_CORPSERECT.X+(PLAYER_CORPSERECT.Width div 2),
+                  FObj.Y+PLAYER_CORPSERECT.Y+(PLAYER_CORPSERECT.Height div 2),
+                  Value, vx, vy, 16, (PLAYER_CORPSERECT.Height*2) div 3,
+                  150, 0, 0);
+    end;
+end;
+
+procedure TCorpse.Draw();
+begin
+  if FState = CORPSE_STATE_REMOVEME then
+    Exit;
+
+  if FAnimation <> nil then
+    FAnimation.Draw(FObj.X, FObj.Y, M_NONE);
+
+  if FAnimationMask <> nil then
+  begin
+    e_Colors := FColor;
+    FAnimationMask.Draw(FObj.X, FObj.Y, M_NONE);
+    e_Colors.R := 255;
+    e_Colors.G := 255;
+    e_Colors.B := 255;
+  end;
+end;
+
+procedure TCorpse.Update();
+var
+  st: Word;
+begin
+  if FState = CORPSE_STATE_REMOVEME then
+    Exit;
+
+  if gTime mod (GAME_TICK*2) <> 0 then
+  begin
+    g_Obj_Move(@FObj, True, True, True);
+    
+    Exit;
+  end;
+
+// Ñîïðîòèâëåíèå âîçäóõà äëÿ òðóïà:
+  FObj.Vel.X := z_dec(FObj.Vel.X, 1);
+
+  st := g_Obj_Move(@FObj, True, True, True);
+
+  if WordBool(st and MOVE_FALLOUT) then
+  begin
+    FState := CORPSE_STATE_REMOVEME;
+    Exit;
+  end;
+
+  if FAnimation <> nil then
+    FAnimation.Update();
+  if FAnimationMask <> nil then
+    FAnimationMask.Update();
+end;
+
+procedure TCorpse.SaveState(var Mem: TBinMemoryWriter);
+var
+  sig: DWORD;
+  anim: Boolean;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà òðóïà:
+  sig := CORPSE_SIGNATURE; // 'CORP'
+  Mem.WriteDWORD(sig);
+// Ñîñòîÿíèå:
+  Mem.WriteByte(FState);
+// Íàêîïëåííûé óðîí:
+  Mem.WriteByte(FDamage);
+// Öâåò:
+  Mem.WriteByte(FColor.R);
+  Mem.WriteByte(FColor.G);
+  Mem.WriteByte(FColor.B);
+// Îáúåêò òðóïà:
+  Obj_SaveState(@FObj, Mem);
+// Åñòü ëè àíèìàöèÿ:
+  anim := FAnimation <> nil;
+  Mem.WriteBoolean(anim);
+// Åñëè åñòü - ñîõðàíÿåì:
+  if anim then
+    FAnimation.SaveState(Mem);
+// Åñòü ëè ìàñêà àíèìàöèè:
+  anim := FAnimationMask <> nil;
+  Mem.WriteBoolean(anim);
+// Åñëè åñòü - ñîõðàíÿåì:
+  if anim then
+    FAnimationMask.SaveState(Mem);
+end;
+
+procedure TCorpse.LoadState(var Mem: TBinMemoryReader);
+var
+  sig: DWORD;
+  anim: Boolean;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà òðóïà:
+  Mem.ReadDWORD(sig);
+  if sig <> CORPSE_SIGNATURE then // 'CORP'
+  begin
+    raise EBinSizeError.Create('TCorpse.LoadState: Wrong Corpse Signature');
+  end;
+// Ñîñòîÿíèå:
+  Mem.ReadByte(FState);
+// Íàêîïëåííûé óðîí:
+  Mem.ReadByte(FDamage);
+// Öâåò:
+  Mem.ReadByte(FColor.R);
+  Mem.ReadByte(FColor.G);
+  Mem.ReadByte(FColor.B);
+// Îáúåêò òðóïà:
+  Obj_LoadState(@FObj, Mem);
+// Åñòü ëè àíèìàöèÿ:
+  Mem.ReadBoolean(anim);
+// Åñëè åñòü - çàãðóæàåì:
+  if anim then
+  begin
+    Assert(FAnimation <> nil, 'TCorpse.LoadState: no FAnimation');
+    FAnimation.LoadState(Mem);
+  end;
+// Åñòü ëè ìàñêà àíèìàöèè:
+  Mem.ReadBoolean(anim);
+// Åñëè åñòü - çàãðóæàåì:
+  if anim then
+  begin
+    Assert(FAnimationMask <> nil, 'TCorpse.LoadState: no FAnimationMask');
+    FAnimationMask.LoadState(Mem);
+  end;
+end;
+
+{ T B o t : }
+
+constructor TBot.Create();
+var
+  a: Integer;
+begin
+  inherited Create();
+
+  FPhysics := True;
+  FSpectator := False;
+  FGhost := False;
+
+  FIamBot := True;
+
+  Inc(gNumBots);
+
+  for a := WEAPON_KASTET to WEAPON_SUPERPULEMET do
+  begin
+    FDifficult.WeaponPrior[a] := WEAPON_PRIOR1[a];
+    FDifficult.CloseWeaponPrior[a] := WEAPON_PRIOR2[a];
+    //FDifficult.SafeWeaponPrior[a] := WEAPON_PRIOR3[a];
+  end;
+end;
+
+destructor TBot.Destroy();
+begin
+  Dec(gNumBots);
+  inherited Destroy();
+end;
+
+procedure TBot.Draw();
+begin
+  inherited Draw();
+
+  //if FTargetUID <> 0 then e_DrawLine(1, FObj.X, FObj.Y, g_Player_Get(FTargetUID).FObj.X,
+  //                                   g_Player_Get(FTargetUID).FObj.Y, 255, 0, 0);
+end;
+
+procedure TBot.Respawn(Silent: Boolean; Force: Boolean = False);
+begin
+  inherited Respawn(Silent, Force);
+
+  FAIFlags := nil;
+  FSelectedWeapon := FCurrWeap;
+  FTargetUID := 0;
+end;
+
+procedure TBot.UpdateCombat();
+type
+  TTarget = record
+    UID: Word;
+    X, Y: Integer;
+    Rect: TRectWH;
+    cX, cY: Integer;
+    Dist: Word;
+    Line: Boolean;
+    Visible: Boolean;
+    IsPlayer: Boolean;
+  end;
+
+  TTargetRecord = array of TTarget;
+
+  function Compare(a, b: TTarget): Integer;
+  begin
+    if a.Line and not b.Line then // A íà ëèíèè îãíÿ
+      Result := -1
+   else
+     if not a.Line and b.Line then // B íà ëèíèè îãíÿ
+       Result := 1
+     else // È A, è B íà ëèíèè èëè íå íà ëèíèè îãíÿ
+       if (a.Line and b.Line) or ((not a.Line) and (not b.Line)) then
+         begin
+           if a.Dist > b.Dist then // B áëèæå
+             Result := 1
+           else // A áëèæå èëè ðàâíîóäàëåííî ñ B
+             Result := -1;
+         end
+       else // Ñòðàííî -> A
+         Result := -1;
+  end;
+
+var
+  a, x1, y1, x2, y2: Integer;
+  targets: TTargetRecord;
+  ammo: Word;
+  Target, BestTarget: TTarget;
+  firew, fireh: Integer;
+  angle: SmallInt;
+  mon: TMonster;
+  pla: TPlayer;
+  vsPlayer, vsMonster, ok: Boolean;
+begin
+  vsPlayer := LongBool(gGameSettings.Options and GAME_OPTION_BOTVSPLAYER);
+  vsMonster := LongBool(gGameSettings.Options and GAME_OPTION_BOTVSMONSTER);
+
+// Åñëè òåêóùåå îðóæèå íå òî, ÷òî íóæíî, òî ìåíÿåì:
+  if FCurrWeap <> FSelectedWeapon then
+    NextWeapon();
+
+// Åñëè íóæíî ñòðåëÿòü è íóæíîå îðóæèå, òî íàæàòü "Ñòðåëÿòü":
+  if (GetAIFlag('NEEDFIRE') <> '') and (FCurrWeap = FSelectedWeapon) then
+    begin
+      RemoveAIFlag('NEEDFIRE');
+
+      case FCurrWeap of
+        WEAPON_PLASMA, WEAPON_SUPERPULEMET, WEAPON_CHAINGUN: PressKey(KEY_FIRE, 20);
+        WEAPON_SAW, WEAPON_KASTET, WEAPON_MEGAKASTET: PressKey(KEY_FIRE, 40);
+        else PressKey(KEY_FIRE);
+      end;
+    end;
+
+// Êîîðäèíàòû ñòâîëà:
+  x1 := FObj.X + WEAPONPOINT[FDirection].X;
+  y1 := FObj.Y + WEAPONPOINT[FDirection].Y;
+
+  Target.UID := FTargetUID;
+
+  ok := False;
+  if Target.UID <> 0 then
+    begin // Öåëü åñòü - íàñòðàèâàåì
+      if (g_GetUIDType(Target.UID) = UID_PLAYER) and
+          vsPlayer then
+        begin // Èãðîê
+          with g_Player_Get(Target.UID) do
+            begin
+              if (@FObj) <> nil then
+              begin
+                Target.X := FObj.X;
+                Target.Y := FObj.Y;
+              end;
+            end;
+
+          Target.cX := Target.X + PLAYER_RECT_CX;
+          Target.cY := Target.Y + PLAYER_RECT_CY;
+          Target.Rect := PLAYER_RECT;
+          Target.Visible := g_TraceVector(x1, y1, Target.cX, Target.cY);
+          Target.Line := (y1+4 < Target.Y+PLAYER_RECT.Y+PLAYER_RECT.Height) and
+                         (y1-4 > Target.Y+PLAYER_RECT.Y);
+          Target.IsPlayer := True;
+          ok := True;
+        end
+      else
+        if (g_GetUIDType(Target.UID) = UID_MONSTER) and
+            vsMonster then
+          begin // Ìîíñòð
+            mon := g_Monsters_Get(Target.UID);
+            if mon <> nil then
+              begin
+                Target.X := mon.Obj.X;
+                Target.Y := mon.Obj.Y;
+
+                Target.cX := Target.X + mon.Obj.Rect.X + (mon.Obj.Rect.Width div 2);
+                Target.cY := Target.Y + mon.Obj.Rect.Y + (mon.Obj.Rect.Height div 2);
+                Target.Rect := mon.Obj.Rect;
+                Target.Visible := g_TraceVector(x1, y1, Target.cX, Target.cY);
+                Target.Line := (y1+4 < Target.Y + mon.Obj.Rect.Y + mon.Obj.Rect.Height) and
+                               (y1-4 > Target.Y + mon.Obj.Rect.Y);
+                Target.IsPlayer := False;
+                ok := True;
+              end;
+          end;
+    end;
+
+  if not ok then
+    begin // Öåëè íåò - îáíóëÿåì
+      Target.X := 0;
+      Target.Y := 0;
+      Target.cX := 0;
+      Target.cY := 0;
+      Target.Visible := False;
+      Target.Line := False;
+      Target.IsPlayer := False;
+    end;
+
+  targets := nil;
+
+// Åñëè öåëü íå âèäèìà èëè íå íà ëèíèè îãíÿ, òî èùåì âñå âîçìîæíûå öåëè:
+  if (not Target.Line) or (not Target.Visible) then
+  begin
+  // Èãðîêè:
+    if vsPlayer then
+      for a := 0 to High(gPlayers) do
+        if (gPlayers[a] <> nil) and (gPlayers[a].Live) and
+           (gPlayers[a].FUID <> FUID) and
+           (not SameTeam(FUID, gPlayers[a].FUID)) and
+           (not gPlayers[a].NoTarget) and
+           (gPlayers[a].FMegaRulez[MR_INVIS] < gTime) then
+          begin
+            if not TargetOnScreen(gPlayers[a].FObj.X + PLAYER_RECT.X,
+                                  gPlayers[a].FObj.Y + PLAYER_RECT.Y) then
+              Continue;
+
+            x2 := gPlayers[a].FObj.X + PLAYER_RECT_CX;
+            y2 := gPlayers[a].FObj.Y + PLAYER_RECT_CY;
+
+          // Åñëè èãðîê íà ýêðàíå è íå ïðèêðûò ñòåíîé:
+            if g_TraceVector(x1, y1, x2, y2) then
+              begin
+              // Äîáàâëÿåì ê ñïèñêó âîçìîæíûõ öåëåé:
+                SetLength(targets, Length(targets)+1);
+                with targets[High(targets)] do
+                  begin
+                    UID := gPlayers[a].FUID;
+                    X := gPlayers[a].FObj.X;
+                    Y := gPlayers[a].FObj.Y;
+                    cX := x2;
+                    cY := y2;
+                    Rect := PLAYER_RECT;
+                    Dist := g_PatchLength(x1, y1, x2, y2);
+                    Line := (y1+4 < Target.Y+PLAYER_RECT.Y+PLAYER_RECT.Height) and
+                            (y1-4 > Target.Y+PLAYER_RECT.Y);
+                    Visible := True;
+                    IsPlayer := True;
+                  end;
+              end;
+          end;
+
+  // Ìîíñòðû:
+    if vsMonster and (gMonsters <> nil) then
+      for a := 0 to High(gMonsters) do
+        if (gMonsters[a] <> nil) and (gMonsters[a].Live) and
+           (gMonsters[a].MonsterType <> MONSTER_BARREL) then
+          begin
+            mon := gMonsters[a];
+
+            if not TargetOnScreen(mon.Obj.X + mon.Obj.Rect.X,
+                                  mon.Obj.Y + mon.Obj.Rect.Y) then
+              Continue;
+
+            x2 := mon.Obj.X + mon.Obj.Rect.X + (mon.Obj.Rect.Width div 2);
+            y2 := mon.Obj.Y + mon.Obj.Rect.Y + (mon.Obj.Rect.Height div 2);
+
+          // Åñëè ìîíñòð íà ýêðàíå è íå ïðèêðûò ñòåíîé:
+            if g_TraceVector(x1, y1, x2, y2) then
+              begin
+              // Äîáàâëÿåì ê ñïèñêó âîçìîæíûõ öåëåé:
+                SetLength(targets, Length(targets)+1);
+                with targets[High(targets)] do
+                  begin
+                    UID := mon.UID;
+                    X := mon.Obj.X;
+                    Y := mon.Obj.Y;
+                    cX := x2;
+                    cY := y2;
+                    Rect := mon.Obj.Rect;
+                    Dist := g_PatchLength(x1, y1, x2, y2);
+                    Line := (y1+4 < Target.Y + mon.Obj.Rect.Y + mon.Obj.Rect.Height) and
+                            (y1-4 > Target.Y + mon.Obj.Rect.Y);
+                    Visible := True;
+                    IsPlayer := False;
+                  end;
+              end;
+          end;
+  end;
+
+// Åñëè åñòü âîçìîæíûå öåëè:
+// (Âûáèðàåì ëó÷øóþ, ìåíÿåì îðóæèå è áåæèì ê íåé/îò íåå)
+  if targets <> nil then
+  begin
+  // Âûáèðàåì íàèëó÷øóþ öåëü:
+    BestTarget := targets[0];
+    if Length(targets) > 1 then
+      for a := 1 to High(targets) do
+        if Compare(BestTarget, targets[a]) = 1 then
+          BestTarget := targets[a];
+
+  // Åñëè ëó÷øàÿ öåëü "âèäíåå" òåêóùåé, òî òåêóùàÿ := ëó÷øàÿ:
+    if ((not Target.Visible) and BestTarget.Visible and (Target.UID <> BestTarget.UID)) or
+        ((not Target.Line) and BestTarget.Line and BestTarget.Visible) then
+      begin
+        Target := BestTarget;
+
+        if (Healthy() = 3) or ((Healthy() = 2)) then
+          begin // Åñëè çäîðîâû - äîãîíÿåì
+            if ((RunDirection() = D_LEFT) and (Target.X > FObj.X)) then
+              SetAIFlag('GORIGHT', '1');
+            if ((RunDirection() = D_RIGHT) and (Target.X < FObj.X)) then
+              SetAIFlag('GOLEFT', '1');
+          end
+        else
+          begin // Åñëè ïîáèòû - óáåãàåì
+            if ((RunDirection() = D_LEFT) and (Target.X < FObj.X)) then
+              SetAIFlag('GORIGHT', '1');
+            if ((RunDirection() = D_RIGHT) and (Target.X > FObj.X)) then
+              SetAIFlag('GOLEFT', '1');
+          end;
+
+      // Âûáèðàåì îðóæèå íà îñíîâå ðàññòîÿíèÿ è ïðèîðèòåòîâ:
+        SelectWeapon(Abs(x1-Target.cX));
+      end;
+  end;
+
+// Åñëè åñòü öåëü:
+// (Äîãîíÿåì/óáåãàåì, ñòðåëÿåì ïî íàïðàâëåíèþ ê öåëè)
+// (Åñëè öåëü äàëåêî, òî õâàòèò ñëåäèòü çà íåé)
+  if Target.UID <> 0 then
+  begin
+    if not TargetOnScreen(Target.X + Target.Rect.X,
+                          Target.Y + Target.Rect.Y) then
+      begin // Öåëü ñáåæàëà ñ "ýêðàíà"
+        if (Healthy() = 3) or ((Healthy() = 2)) then
+          begin // Åñëè çäîðîâû - äîãîíÿåì
+            if ((RunDirection() = D_LEFT) and (Target.X > FObj.X)) then
+              SetAIFlag('GORIGHT', '1');
+            if ((RunDirection() = D_RIGHT) and (Target.X < FObj.X)) then
+              SetAIFlag('GOLEFT', '1');
+          end
+        else
+          begin // Åñëè ïîáèòû - çàáûâàåì î öåëè è óáåãàåì
+            Target.UID := 0;
+            if ((RunDirection() = D_LEFT) and (Target.X < FObj.X)) then
+              SetAIFlag('GORIGHT', '1');
+            if ((RunDirection() = D_RIGHT) and (Target.X > FObj.X)) then
+              SetAIFlag('GOLEFT', '1');
+          end;
+      end
+    else
+      begin // Öåëü ïîêà íà "ýêðàíå"
+      // Åñëè öåëü íå çàãîðîæåíà ñòåíîé, òî îòìå÷àåì, êîãäà åå âèäåëè:
+        if g_TraceVector(x1, y1, Target.cX, Target.cY) then
+          FLastVisible := gTime;
+      // Åñëè ðàçíèöà âûñîò íå âåëèêà, òî äîãîíÿåì:
+        if (Abs(FObj.Y-Target.Y) <= 128) then
+          begin
+            if ((RunDirection() = D_LEFT) and (Target.X > FObj.X)) then
+              SetAIFlag('GORIGHT', '1');
+            if ((RunDirection() = D_RIGHT) and (Target.X < FObj.X)) then
+              SetAIFlag('GOLEFT', '1');
+          end;
+      end;
+
+  // Âûáèðàåì óãîë ââåðõ:
+    if FDirection = D_LEFT then
+      angle := ANGLE_LEFTUP
+    else
+      angle := ANGLE_RIGHTUP;
+
+    firew := Trunc(Cos(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+    fireh := Trunc(Sin(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+
+  // Åñëè ïðè óãëå ââåðõ ìîæíî ïîïàñòü â ïðèáëèçèòåëüíîå ïîëîæåíèå öåëè:
+    if g_CollideLine(x1, y1, x1+firew, y1+fireh,
+          Target.X+Target.Rect.X+GetInterval(FDifficult.DiagPrecision, 128), //96
+          Target.Y+Target.Rect.Y+GetInterval(FDifficult.DiagPrecision, 128),
+          Target.Rect.Width, Target.Rect.Height) and
+        g_TraceVector(x1, y1, Target.cX, Target.cY) then
+      begin // òî íóæíî ñòðåëÿòü ââåðõ
+        SetAIFlag('NEEDFIRE', '1');
+        SetAIFlag('NEEDSEEUP', '1');
+      end;
+
+  // Âûáèðàåì óãîë âíèç:
+    if FDirection = D_LEFT then
+      angle := ANGLE_LEFTDOWN
+    else
+      angle := ANGLE_RIGHTDOWN;
+
+    firew := Trunc(Cos(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+    fireh := Trunc(Sin(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+
+  // Åñëè ïðè óãëå âíèç ìîæíî ïîïàñòü â ïðèáëèçèòåëüíîå ïîëîæåíèå öåëè:
+    if g_CollideLine(x1, y1, x1+firew, y1+fireh,
+          Target.X+Target.Rect.X+GetInterval(FDifficult.DiagPrecision, 128),
+          Target.Y+Target.Rect.Y+GetInterval(FDifficult.DiagPrecision, 128),
+          Target.Rect.Width, Target.Rect.Height) and
+        g_TraceVector(x1, y1, Target.cX, Target.cY) then
+      begin // òî íóæíî ñòðåëÿòü âíèç
+        SetAIFlag('NEEDFIRE', '1');
+        SetAIFlag('NEEDSEEDOWN', '1');
+      end;
+
+  // Åñëè öåëü âèäíî è îíà íà òàêîé æå âûñîòå:
+    if Target.Visible and
+        (y1+4 < Target.Y+Target.Rect.Y+Target.Rect.Height) and
+        (y1-4 > Target.Y+Target.Rect.Y) then
+      begin
+      // Åñëè èäåì â ñòîðîíó öåëè, òî íàäî ñòðåëÿòü:
+        if ((FDirection = D_LEFT) and (Target.X < FObj.X)) or
+            ((FDirection = D_RIGHT) and (Target.X > FObj.X)) then
+        begin // òî íóæíî ñòðåëÿòü âïåðåä
+          SetAIFlag('NEEDFIRE', '1');
+          SetAIFlag('NEEDSEEDOWN', '');
+          SetAIFlag('NEEDSEEUP', '');
+        end;
+      // Åñëè öåëü â ïðåäåëàõ "ýêðàíà" è ñëîæíîñòü ïîçâîëÿåò ïðûæêè ñáëèæåíèÿ:
+        if Abs(FObj.X-Target.X) < Trunc(gPlayerScreenSize.X*0.75) then
+          if GetRnd(FDifficult.CloseJump) then
+            begin // òî åñëè ïîâåçåò - ïðûãàåì (îñîáåííî, åñëè áëèçêî)
+              if Abs(FObj.X-Target.X) < 128 then
+                a := 4
+              else
+                a := 30;
+              if Random(a) = 0 then
+                SetAIFlag('NEEDJUMP', '1');
+            end;
+      end;
+
+  // Åñëè öåëü âñå åùå åñòü:
+    if Target.UID <> 0 then
+      if gTime-FLastVisible > 2000 then // Åñëè âèäåëè äàâíî
+        Target.UID := 0 // òî çàáûòü öåëü
+      else // Åñëè âèäåëè íåäàâíî
+        begin // íî öåëü óáèëè
+          if Target.IsPlayer then
+            begin // Öåëü - èãðîê
+              pla := g_Player_Get(Target.UID);
+              if (pla = nil) or (not pla.Live) or pla.NoTarget or
+                 (pla.FMegaRulez[MR_INVIS] >= gTime) then
+                Target.UID := 0; // òî çàáûòü öåëü
+            end
+          else
+            begin // Öåëü - ìîíñòð
+              mon := g_Monsters_Get(Target.UID);
+              if (mon = nil) or (not mon.Live) then
+                Target.UID := 0; // òî çàáûòü öåëü
+            end;
+        end;
+  end; // if Target.UID <> 0
+
+  FTargetUID := Target.UID;
+
+// Åñëè âîçìîæíûõ öåëåé íåò:
+// (Àòàêà ÷åãî-íèáóäü ñëåâà èëè ñïðàâà)
+  if targets = nil then
+    if GetAIFlag('ATTACKLEFT') <> '' then
+      begin // Åñëè íóæíî àòàêîâàòü íàëåâî
+        RemoveAIFlag('ATTACKLEFT');
+
+        SetAIFlag('NEEDJUMP', '1');
+
+        if RunDirection() = D_RIGHT then
+          begin // Èäåì íå â òó ñòîðîíó
+            if (Healthy() > 1) and GetRnd(FDifficult.InvisFire) then
+              begin // Åñëè çäîðîâû, òî, âîçìîæíî, ñòðåëÿåì áåæèì âëåâî è ñòðåëÿåì
+                SetAIFlag('NEEDFIRE', '1');
+                SetAIFlag('GOLEFT', '1');
+              end;
+          end
+        else
+          begin // Èäåì â íóæíóþ ñòîðîíó
+            if GetRnd(FDifficult.InvisFire) then // Âîçìîæíî, ñòðåëÿåì âñëåïóþ
+              SetAIFlag('NEEDFIRE', '1');
+            if Healthy() <= 1 then // Ïîáèòû - óáåãàåì
+              SetAIFlag('GORIGHT', '1');
+          end;
+      end
+    else
+      if GetAIFlag('ATTACKRIGHT') <> '' then
+        begin // Åñëè íóæíî àòàêîâàòü íàïðàâî
+          RemoveAIFlag('ATTACKRIGHT');
+
+          SetAIFlag('NEEDJUMP', '1');
+
+          if RunDirection() = D_LEFT then
+            begin // Èäåì íå â òó ñòîðîíó
+              if (Healthy() > 1) and GetRnd(FDifficult.InvisFire) then
+                begin // Åñëè çäîðîâû, òî, âîçìîæíî, áåæèì âïðàâî è ñòðåëÿåì
+                  SetAIFlag('NEEDFIRE', '1');
+                  SetAIFlag('GORIGHT', '1');
+                end;
+            end
+          else
+            begin
+              if GetRnd(FDifficult.InvisFire) then // Âîçìîæíî, ñòðåëÿåì âñëåïóþ
+                SetAIFlag('NEEDFIRE', '1');
+              if Healthy() <= 1 then // Ïîáèòû - óáåãàåì
+                SetAIFlag('GOLEFT', '1');
+            end;
+        end;
+
+// Åñëè åñòü âîçìîæíûå öåëè:
+// (Ñòðåëÿåì ïî íàïðàâëåíèþ ê öåëÿì)
+  if (targets <> nil) and (GetAIFlag('NEEDFIRE') <> '') then
+    for a := 0 to High(targets) do
+      begin
+      // Åñëè ìîæåì ñòðåëÿòü ïî äèàãîíàëè:
+        if GetRnd(FDifficult.DiagFire) then
+          begin
+          // Èùåì öåëü ñâåðõó è ñòðåëÿåì, åñëè åñòü:
+            if FDirection = D_LEFT then
+              angle := ANGLE_LEFTUP
+            else
+              angle := ANGLE_RIGHTUP;
+
+            firew := Trunc(Cos(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+            fireh := Trunc(Sin(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+
+            if g_CollideLine(x1, y1, x1+firew, y1+fireh,
+                  targets[a].X+targets[a].Rect.X+GetInterval(FDifficult.DiagPrecision, 128),
+                  targets[a].Y+targets[a].Rect.Y+GetInterval(FDifficult.DiagPrecision, 128),
+                  targets[a].Rect.Width, targets[a].Rect.Height) and
+                g_TraceVector(x1, y1, targets[a].cX, targets[a].cY) then
+              begin
+                SetAIFlag('NEEDFIRE', '1');
+                SetAIFlag('NEEDSEEUP', '1');
+              end;
+
+          // Èùåì öåëü ñíèçó è ñòðåëÿåì, åñëè åñòü:
+            if FDirection = D_LEFT then
+              angle := ANGLE_LEFTDOWN
+            else
+              angle := ANGLE_RIGHTDOWN;
+
+            firew := Trunc(Cos(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+            fireh := Trunc(Sin(DegToRad(-angle))*gPlayerScreenSize.X*0.6);
+
+            if g_CollideLine(x1, y1, x1+firew, y1+fireh,
+                  targets[a].X+targets[a].Rect.X+GetInterval(FDifficult.DiagPrecision, 128),
+                  targets[a].Y+targets[a].Rect.Y+GetInterval(FDifficult.DiagPrecision, 128),
+                  targets[a].Rect.Width, targets[a].Rect.Height) and
+                g_TraceVector(x1, y1, targets[a].cX, targets[a].cY) then
+              begin
+                SetAIFlag('NEEDFIRE', '1');
+                SetAIFlag('NEEDSEEDOWN', '1');
+              end;
+          end;
+
+      // Åñëè öåëü "ïåðåä íîñîì", òî ñòðåëÿåì:
+        if targets[a].Line and targets[a].Visible and
+            (((FDirection = D_LEFT) and (targets[a].X < FObj.X)) or
+            ((FDirection = D_RIGHT) and (targets[a].X > FObj.X))) then
+        begin
+          SetAIFlag('NEEDFIRE', '1');
+          Break;
+        end;
+      end;
+
+// Åñëè ëåòèò ïóëÿ, òî, âîçìîæíî, ïîäïðûãèâàåì:
+  if g_Weapon_Danger(FUID, FObj.X+PLAYER_RECT.X, FObj.Y+PLAYER_RECT.Y,
+                    PLAYER_RECT.Width, PLAYER_RECT.Height,
+                    40+GetInterval(FDifficult.Cover, 40)) then
+    SetAIFlag('NEEDJUMP', '1');
+
+// Åñëè êîí÷èëèñü ïàòîðíû, òî íóæíî ñìåíèòü îðóæèå:
+  ammo := GetAmmoByWeapon(FCurrWeap);
+  if ((FCurrWeap = WEAPON_SHOTGUN2) and (ammo < 2)) or
+      ((FCurrWeap = WEAPON_BFG) and (ammo < 40)) or
+      (ammo = 0) then
+    SetAIFlag('SELECTWEAPON', '1');
+
+// Åñëè íóæíî ñìåíèòü îðóæèå, òî âûáèðàåì íóæíîå:
+  if GetAIFlag('SELECTWEAPON') = '1' then
+  begin
+    SelectWeapon(-1);
+    RemoveAIFlag('SELECTWEAPON');
+  end;
+end;
+
+procedure TBot.Update();
+var
+  EnableAI: Boolean;
+begin
+  if not FLive then
+  begin // Respawn
+    ReleaseKeys();
+    PressKey(KEY_UP);
+  end
+  else
+  begin
+    EnableAI := True;
+    
+    // Ïðîâåðÿåì, îòêëþ÷¸í ëè AI áîòîâ
+    if (g_debug_BotAIOff = 1) and (Team = TEAM_RED) then
+      EnableAI := False;
+    if (g_debug_BotAIOff = 2) and (Team = TEAM_BLUE) then
+      EnableAI := False;
+    if g_debug_BotAIOff = 3 then
+      EnableAI := False;
+
+    if EnableAI then
+    begin
+      UpdateMove();
+      UpdateCombat();
+    end;
+  end;
+
+  inherited Update();
+end;
+
+procedure TBot.ReleaseKey(Key: Byte);
+begin
+  with FKeys[Key] do
+  begin
+    Pressed := False;
+    Time := 0;
+  end;
+end;
+
+function TBot.KeyPressed(Key: Word): Boolean;
+begin
+  Result := FKeys[Key].Pressed;
+end;
+
+function TBot.GetAIFlag(fName: String20): String20;
+var
+  a: Integer;
+begin
+  Result := '';
+
+  fName := LowerCase(fName);
+
+  if FAIFlags <> nil then
+    for a := 0 to High(FAIFlags) do
+      if LowerCase(FAIFlags[a].Name) = fName then
+      begin
+        Result := FAIFlags[a].Value;
+        Break;
+      end;
+end;
+
+procedure TBot.RemoveAIFlag(fName: String20);
+var
+  a, b: Integer;
+begin
+  if FAIFlags = nil then Exit;
+
+  fName := LowerCase(fName);
+
+  for a := 0 to High(FAIFlags) do
+    if LowerCase(FAIFlags[a].Name) = fName then
+    begin
+      if a <> High(FAIFlags) then
+        for b := a to High(FAIFlags)-1 do
+          FAIFlags[b] := FAIFlags[b+1];
+
+      SetLength(FAIFlags, Length(FAIFlags)-1);
+      Break;
+    end;
+end;
+
+procedure TBot.SetAIFlag(fName, fValue: String20);
+var
+  a: Integer;
+  ok: Boolean;
+begin
+  a := 0;
+  ok := False;
+
+  fName := LowerCase(fName);
+
+  if FAIFlags <> nil then
+    for a := 0 to High(FAIFlags) do
+      if LowerCase(FAIFlags[a].Name) = fName then
+      begin
+        ok := True;
+        Break;
+      end;
+
+  if ok then FAIFlags[a].Value := fValue
+  else
+  begin
+    SetLength(FAIFlags, Length(FAIFlags)+1);
+    with FAIFlags[High(FAIFlags)] do
+    begin
+      Name := fName;
+      Value := fValue;
+    end;
+  end;
+end;
+
+procedure TBot.UpdateMove;
+
+  procedure GoLeft(Time: Word = 1);
+  begin
+    ReleaseKey(KEY_LEFT);
+    ReleaseKey(KEY_RIGHT);
+    PressKey(KEY_LEFT, Time);
+    SetDirection(D_LEFT);
+  end;
+
+  procedure GoRight(Time: Word = 1);
+  begin
+    ReleaseKey(KEY_LEFT);
+    ReleaseKey(KEY_RIGHT);
+    PressKey(KEY_RIGHT, Time);
+    SetDirection(D_RIGHT);
+  end;
+
+  function Rnd(a: Word): Boolean;
+  begin
+    Result := Random(a) = 0;
+  end;
+
+  procedure Turn(Time: Word = 1200);
+  begin
+    if RunDirection() = D_LEFT then GoRight(Time) else GoLeft(Time);
+  end;
+
+  procedure Stop();
+  begin
+    ReleaseKey(KEY_LEFT);
+    ReleaseKey(KEY_RIGHT);
+  end;
+
+  function CanRunLeft(): Boolean;
+  begin
+    Result := not CollideLevel(-1, 0);
+  end;
+
+  function CanRunRight(): Boolean;
+  begin
+    Result := not CollideLevel(1, 0);
+  end;
+
+  function CanRun(): Boolean;
+  begin
+    if RunDirection() = D_LEFT then Result := CanRunLeft() else Result := CanRunRight();
+  end;
+
+  procedure Jump(Time: Word = 30);
+  begin
+    PressKey(KEY_JUMP, Time);
+  end;
+
+  function NearHole(): Boolean;
+  var
+    x, sx: Integer;
+  begin
+    { TODO 5 : Ëåñòíèöû }
+    sx := IfThen(RunDirection() = D_LEFT, -1, 1);
+    for x := 1 to PLAYER_RECT.Width do
+      if (not StayOnStep(x*sx, 0)) and
+         (not CollideLevel(x*sx, PLAYER_RECT.Height)) and
+         (not CollideLevel(x*sx, PLAYER_RECT.Height*2)) then
+      begin
+        Result := True;
+        Exit;
+      end;
+
+    Result := False;
+  end;
+
+  function BorderHole(): Boolean;
+  var
+    x, sx, xx: Integer;
+  begin
+    { TODO 5 : Ëåñòíèöû }
+    sx := IfThen(RunDirection() = D_LEFT, -1, 1);
+    for x := 1 to PLAYER_RECT.Width do
+      if (not StayOnStep(x*sx, 0)) and
+         (not CollideLevel(x*sx, PLAYER_RECT.Height)) and
+         (not CollideLevel(x*sx, PLAYER_RECT.Height*2)) then
+      begin
+        for xx := x to x+32 do
+          if CollideLevel(xx*sx, PLAYER_RECT.Height) then
+          begin
+            Result := True;
+            Exit;
+          end;
+      end;
+
+    Result := False;
+  end;
+
+  function NearDeepHole(): Boolean;
+  var
+    x, sx, y: Integer;
+  begin
+    Result := False;
+
+    sx := IfThen(RunDirection() = D_LEFT, -1, 1);
+    y := 3;
+
+    for x := 1 to PLAYER_RECT.Width do
+      if (not StayOnStep(x*sx, 0)) and
+         (not CollideLevel(x*sx, PLAYER_RECT.Height)) and
+         (not CollideLevel(x*sx, PLAYER_RECT.Height*2)) then
+      begin
+        while FObj.Y+y*PLAYER_RECT.Height < gMapInfo.Height do
+        begin
+          if CollideLevel(x*sx, PLAYER_RECT.Height*y) then Exit;
+          y := y+1;
+        end;
+
+        Result := True;
+      end else Result := False;
+  end;
+
+  function OverDeepHole(): Boolean;
+  var
+    y: Integer;
+  begin
+    Result := False;
+
+    y := 1;
+    while FObj.Y+y*PLAYER_RECT.Height < gMapInfo.Height do
+    begin
+      if CollideLevel(0, PLAYER_RECT.Height*y) then Exit;
+      y := y+1;
+    end;
+
+    Result := True;
+  end;
+
+  function OnGround(): Boolean;
+  begin
+    Result := StayOnStep(0, 0) or CollideLevel(0, 1);
+  end;
+
+  function OnLadder(): Boolean;
+  begin
+    Result := FullInStep(0, 0);
+  end;
+
+  function BelowLadder(): Boolean;
+  begin
+    Result := (FullInStep(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -PLAYER_RECT.Height) and
+              not CollideLevel(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -PLAYER_RECT.Height)) or
+              (FullInStep(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -BOT_MAXJUMP) and
+              not CollideLevel(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -BOT_MAXJUMP));
+  end;
+
+  function BelowLiftUp(): Boolean;
+  begin
+    Result := ((FullInLift(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -PLAYER_RECT.Height) = -1) and
+              not CollideLevel(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -PLAYER_RECT.Height)) or
+              ((FullInLift(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -BOT_MAXJUMP) = -1) and
+              not CollideLevel(IfThen(RunDirection() = D_LEFT, -1, 1)*(PLAYER_RECT.Width div 2), -BOT_MAXJUMP));
+  end;
+
+  function OnTopLift(): Boolean;
+  begin
+    Result := (FullInLift(0, 0) = -1) and (FullInLift(0, -32) = 0);
+  end;
+
+  function CanJumpOver(): Boolean;
+  var
+    sx, y: Integer;
+  begin
+    sx := IfThen(RunDirection() = D_LEFT, -1, 1);
+
+    Result := False;
+
+    if not CollideLevel(sx, 0) then Exit;
+
+    for y := 1 to BOT_MAXJUMP do
+      if CollideLevel(0, -y) then Exit else
+        if not CollideLevel(sx, -y) then
+        begin
+          Result := True;
+          Exit;
+        end;
+  end;
+
+  function CanJumpUp(Dist: ShortInt): Boolean;
+  var
+    y, yy: Integer;
+    c: Boolean;
+  begin
+    Result := False;
+
+    if CollideLevel(Dist, 0) then Exit;
+
+    c := False;
+    for y := 0 to BOT_MAXJUMP do
+      if CollideLevel(Dist, -y) then
+      begin
+        c := True;
+        Break;
+      end;
+
+    if not c then Exit;
+
+    c := False;
+    for yy := y+1 to BOT_MAXJUMP do
+      if not CollideLevel(Dist, -yy) then
+      begin
+        c := True;
+        Break;
+      end;
+
+    if not c then Exit;
+
+    c := False;
+    for y := 0 to BOT_MAXJUMP do
+      if CollideLevel(0, -y) then
+      begin
+        c := True;
+        Break;
+      end;
+
+    if c then Exit;
+
+    if y < yy then Exit;
+
+    Result := True;
+  end;
+
+  function IsSafeTrigger(): Boolean;
+  var
+    a: Integer;
+  begin
+    Result := True;
+    if gTriggers = nil then
+      Exit;
+    for a := 0 to High(gTriggers) do
+      if Collide(gTriggers[a].X,
+                 gTriggers[a].Y,
+                 gTriggers[a].Width,
+                 gTriggers[a].Height) and
+         (gTriggers[a].TriggerType in [TRIGGER_EXIT, TRIGGER_CLOSEDOOR,
+                                       TRIGGER_CLOSETRAP, TRIGGER_TRAP,
+                                       TRIGGER_PRESS, TRIGGER_ON, TRIGGER_OFF,
+                                       TRIGGER_ONOFF, TRIGGER_SPAWNMONSTER,
+                                       TRIGGER_DAMAGE, TRIGGER_SHOT]) then
+        Result := False;
+  end;
+
+begin
+// Âîçìîæíî, íàæèìàåì êíîïêó:
+  if Rnd(16) and IsSafeTrigger() then
+    PressKey(KEY_OPEN);
+
+// Åñëè ïîä ëèôòîì èëè ñòóïåíüêàìè, òî, âîçìîæíî, ïðûãàåì:
+  if OnLadder() or ((BelowLadder() or BelowLiftUp()) and Rnd(8)) then
+    begin
+      ReleaseKey(KEY_LEFT);
+      ReleaseKey(KEY_RIGHT);
+      Jump();
+    end;
+
+// Èäåì âëåâî, åñëè íàäî áûëî:
+  if GetAIFlag('GOLEFT') <> '' then
+    begin
+      RemoveAIFlag('GOLEFT');
+      if CanRunLeft() then
+        GoLeft(360);
+    end;
+
+// Èäåì âïðàâî, åñëè íàäî áûëî:
+  if GetAIFlag('GORIGHT') <> '' then
+    begin
+      RemoveAIFlag('GORIGHT');
+      if CanRunRight() then
+        GoRight(360);
+    end;
+
+// Åñëè âûëåòåëè çà êàðòó, òî ïðîáóåì âåðíóòüñÿ:
+  if FObj.X < -32 then
+    GoRight(360)
+  else
+    if FObj.X+32 > gMapInfo.Width then
+      GoLeft(360);
+
+// Ïðûãàåì, åñëè íàäî áûëî:
+  if GetAIFlag('NEEDJUMP') <> '' then
+    begin
+      Jump(0);
+      RemoveAIFlag('NEEDJUMP');
+    end;
+
+// Ñìîòðèì ââåðõ, åñëè íàäî áûëî:
+  if GetAIFlag('NEEDSEEUP') <> '' then
+    begin
+      ReleaseKey(KEY_UP);
+      ReleaseKey(KEY_DOWN);
+      PressKey(KEY_UP, 20);
+      RemoveAIFlag('NEEDSEEUP');
+    end;
+
+// Ñìîòðèì âíèç, åñëè íàäî áûëî:
+  if GetAIFlag('NEEDSEEDOWN') <> '' then
+    begin
+      ReleaseKey(KEY_UP);
+      ReleaseKey(KEY_DOWN);
+      PressKey(KEY_DOWN, 20);
+      RemoveAIFlag('NEEDSEEDOWN');
+    end;
+
+// Åñëè íóæíî áûëî â äûðó è ìû íå íà çåìëå, òî ïîêîðíî ëåòèì:
+  if GetAIFlag('GOINHOLE') <> '' then
+    if not OnGround() then
+      begin
+        ReleaseKey(KEY_LEFT);
+        ReleaseKey(KEY_RIGHT);
+        RemoveAIFlag('GOINHOLE');
+        SetAIFlag('FALLINHOLE', '1');
+      end;
+
+// Åñëè ïàäàëè è äîñòèãëè çåìëè, òî õâàòèò ïàäàòü:
+  if GetAIFlag('FALLINHOLE') <> '' then
+    if OnGround() then
+      RemoveAIFlag('FALLINHOLE');
+
+// Åñëè ëåòåëè ïðÿìî è ñåé÷àñ íå íà ëåñòíèöå èëè íà âåðøèíå ëèôòà, òî îòõîäèì â ñòîðîíó:
+  if not (KeyPressed(KEY_LEFT) or KeyPressed(KEY_RIGHT)) then
+    if GetAIFlag('FALLINHOLE') = '' then
+      if (not OnLadder()) or (FObj.Vel.Y >= 0) or (OnTopLift()) then
+        if Rnd(2) then
+          GoLeft(360)
+        else
+          GoRight(360);
+
+// Åñëè íà çåìëå è ìîæíî ïîäïðûãíóòü, òî, âîçìîæíî, ïðûãàåì:
+  if OnGround() and
+      CanJumpUp(IfThen(RunDirection() = D_LEFT, -1, 1)*32) and
+      Rnd(8) then
+    Jump();
+
+// Åñëè íà çåìëå è âîçëå äûðû (ãëóáèíà > 2 ðîñòîâ èãðîêà):
+  if OnGround() and NearHole() then
+    if NearDeepHole() then // Åñëè ýòî áåçäíà
+      case Random(6) of
+        0..3: Turn(); // Áåæèì îáðàòíî
+        4: Jump(); // Ïðûãàåì
+        5: begin // Ïðûãàåì îáðàòíî
+             Turn();
+             Jump();
+           end;
+      end
+    else // Ýòî íå áåçäíà è ìû åùå íå ëåòèì òóäà
+      if GetAIFlag('GOINHOLE') = '' then
+        case Random(6) of
+          0: Turn(); // Íå íóæíî òóäà
+          1: Jump(); // Âäðóã ïîâåçåò - ïðûãàåì
+          else // Åñëè ÿìà ñ ãðàíèöåé, òî ïðè ñëó÷àå ìîæíî òóäà ïðûãíóòü
+            if BorderHole() then
+              SetAIFlag('GOINHOLE', '1');
+   end;
+
+// Åñëè íà çåìëå, íî íåêóäà èäòè:
+  if (not CanRun()) and OnGround() then
+    begin
+    // Åñëè ìû íà ëåñòíèöå èëè ìîæíî ïåðåïðûãíóòü, òî ïðûãàåì:
+      if CanJumpOver() or OnLadder() then
+        Jump()
+      else // èíà÷å ïîïûòàåìñÿ â äðóãóþ ñòîðîíó
+        if Random(2) = 0 then
+        begin
+          if IsSafeTrigger() then
+            PressKey(KEY_OPEN);
+        end else
+          Turn();
+    end;
+
+// Îñòàëîñü ìàëî âîçäóõà:
+  if FAir < 36 * 2 then
+    Jump(20);
+
+// Âûáèðàåìñÿ èç êèñëîòû, åñëè íåò êîñòþìà, îáîæãëèñü, èëè ìàëî çäîðîâüÿ:
+  if (FMegaRulez[MR_SUIT] < gTime) and ((FLastHit = HIT_ACID) or (Healthy() <= 1)) then
+    if BodyInAcid(0, 0) then
+      Jump();
+end;
+
+function TBot.FullInStep(XInc, YInc: Integer): Boolean;
+begin
+  Result := g_Map_CollidePanel(FObj.X+PLAYER_RECT.X+XInc, FObj.Y+PLAYER_RECT.Y+YInc,
+                               PLAYER_RECT.Width, PLAYER_RECT.Height, PANEL_STEP, False);
+end;
+
+{function TBot.NeedItem(Item: Byte): Byte;
+begin
+  Result := 4;
+end;}
+
+procedure TBot.SelectWeapon(Dist: Integer);
+var
+  a: Integer;
+  
+  function HaveAmmo(weapon: Byte): Boolean;
+  begin
+    case weapon of
+      WEAPON_PISTOL: Result := FAmmo[A_BULLETS] >= 1;
+      WEAPON_SHOTGUN1: Result := FAmmo[A_SHELLS] >= 1;
+      WEAPON_SHOTGUN2: Result := FAmmo[A_SHELLS] >= 2;
+      WEAPON_CHAINGUN: Result := FAmmo[A_BULLETS] >= 10;
+      WEAPON_ROCKETLAUNCHER: Result := FAmmo[A_ROCKETS] >= 1;
+      WEAPON_PLASMA: Result := FAmmo[A_CELLS] >= 10;
+      WEAPON_BFG: Result := FAmmo[A_CELLS] >= 40;
+      WEAPON_SUPERPULEMET: Result := FAmmo[A_SHELLS] >= 1;
+      else Result := True;
+    end;
+  end;
+
+begin
+  if Dist = -1 then Dist := BOT_LONGDIST;
+
+  if Dist > BOT_LONGDIST then
+  begin // Äàëüíèé áîé
+    for a := 0 to 9 do
+      if FWeapon[FDifficult.WeaponPrior[a]] and HaveAmmo(FDifficult.WeaponPrior[a]) then
+      begin
+        FSelectedWeapon := FDifficult.WeaponPrior[a];
+        Break;
+      end;
+  end
+  else //if Dist > BOT_UNSAFEDIST then
+  begin // Áëèæíèé áîé
+    for a := 0 to 9 do
+      if FWeapon[FDifficult.CloseWeaponPrior[a]] and HaveAmmo(FDifficult.CloseWeaponPrior[a]) then
+      begin
+        FSelectedWeapon := FDifficult.CloseWeaponPrior[a];
+        Break;
+      end;
+  end;
+  { else
+  begin
+    for a := 0 to 9 do
+      if FWeapon[FDifficult.SafeWeaponPrior[a]] and HaveAmmo(FDifficult.SafeWeaponPrior[a]) then
+      begin
+        FSelectedWeapon := FDifficult.SafeWeaponPrior[a];
+        Break;
+      end;
+  end;}
+end;
+
+function TBot.PickItem(ItemType: Byte; force: Boolean; var remove: Boolean): Boolean;
+begin
+  Result := inherited PickItem(ItemType, force, remove);
+
+  if Result then SetAIFlag('SELECTWEAPON', '1');
+end;
+
+function TBot.Heal(value: Word; Soft: Boolean): Boolean;
+begin
+  Result := inherited Heal(value, Soft);
+end;
+
+function TBot.Healthy(): Byte;
+begin
+  if FMegaRulez[MR_INVUL] >= gTime then Result := 3
+  else if (FHealth > 80) or ((FHealth > 50) and (FArmor > 20)) then Result := 3
+  else if (FHealth > 50) then Result := 2
+  else if (FHealth > 20) then Result := 1
+  else Result := 0;
+end;
+
+function TBot.TargetOnScreen(TX, TY: Integer): Boolean;
+begin
+  Result := (Abs(FObj.X-TX) <= Trunc(gPlayerScreenSize.X*0.6)) and
+            (Abs(FObj.Y-TY) <= Trunc(gPlayerScreenSize.Y*0.6));
+end;
+
+procedure TBot.OnDamage(Angle: SmallInt);
+var
+  pla: TPlayer;
+  mon: TMonster;
+  ok: Boolean;
+begin
+  inherited;
+
+  if (Angle = 0) or (Angle = 180) then
+    begin
+      ok := False;
+      if (g_GetUIDType(FLastSpawnerUID) = UID_PLAYER) and
+          LongBool(gGameSettings.Options and GAME_OPTION_BOTVSPLAYER) then
+        begin // Èãðîê
+          pla := g_Player_Get(FLastSpawnerUID);
+          ok := not TargetOnScreen(pla.FObj.X + PLAYER_RECT.X,
+                                   pla.FObj.Y + PLAYER_RECT.Y);
+        end
+      else
+        if (g_GetUIDType(FLastSpawnerUID) = UID_MONSTER) and
+           LongBool(gGameSettings.Options and GAME_OPTION_BOTVSMONSTER) then
+        begin // Ìîíñòð
+          mon := g_Monsters_Get(FLastSpawnerUID);
+          ok := not TargetOnScreen(mon.Obj.X + mon.Obj.Rect.X,
+                                   mon.Obj.Y + mon.Obj.Rect.Y);
+        end;
+
+      if ok then
+        if Angle = 0 then
+          SetAIFlag('ATTACKLEFT', '1')
+        else
+          SetAIFlag('ATTACKRIGHT', '1');
+    end;
+end;
+
+function TBot.RunDirection(): TDirection;
+begin
+  if Abs(Vel.X) >= 1 then
+  begin
+    if Vel.X > 0 then Result := D_RIGHT else Result := D_LEFT;
+  end else
+    Result := FDirection;
+end;
+
+function TBot.GetRnd(a: Byte): Boolean;
+begin
+  if a = 0 then Result := False
+    else if a = 255 then Result := True
+      else Result := Random(256) > 255-a;
+end;
+
+function TBot.GetInterval(a: Byte; radius: SmallInt): SmallInt;
+begin
+  Result := Round((255-a)/255*radius*(Random(2)-1));
+end;
+
+procedure TBot.SaveState(var Mem: TBinMemoryWriter);
+var
+  i: Integer;
+  dw: DWORD;
+  p: Pointer;
+begin
+  inherited SaveState(Mem);
+
+// Âûáðàííîå îðóæèå:
+  Mem.WriteByte(FSelectedWeapon);
+// UID öåëè:
+  Mem.WriteWord(FTargetUID);
+// Âðåìÿ ïîòåðè öåëè:
+  Mem.WriteDWORD(FLastVisible);
+// Êîëè÷åñòâî ôëàãîâ ÈÈ:
+  dw := Length(FAIFlags);
+  Mem.WriteDWORD(dw);
+// Ôëàãè ÈÈ:
+  for i := 0 to Integer(dw)-1 do
+  begin
+    Mem.WriteString(FAIFlags[i].Name, 20);
+    Mem.WriteString(FAIFlags[i].Value, 20);
+  end;
+// Íàñòðîéêè ñëîæíîñòè:
+  p := @FDifficult;
+  Mem.WriteMemory(p, SizeOf(TDifficult));
+end;
+
+procedure TBot.LoadState(var Mem: TBinMemoryReader);
+var
+  i: Integer;
+  dw: DWORD;
+  p: Pointer;
+begin
+  inherited LoadState(Mem);
+
+// Âûáðàííîå îðóæèå:
+  Mem.ReadByte(FSelectedWeapon);
+// UID öåëè:
+  Mem.ReadWord(FTargetUID);
+// Âðåìÿ ïîòåðè öåëè:
+  Mem.ReadDWORD(FLastVisible);
+// Êîëè÷åñòâî ôëàãîâ ÈÈ:
+  Mem.ReadDWORD(dw);
+  SetLength(FAIFlags, dw);
+// Ôëàãè ÈÈ:
+  for i := 0 to Integer(dw)-1 do
+  begin
+    Mem.ReadString(FAIFlags[i].Name);
+    Mem.ReadString(FAIFlags[i].Value);
+  end;
+// Íàñòðîéêè ñëîæíîñòè:
+  Mem.ReadMemory(p, dw);
+  if dw <> SizeOf(TDifficult) then
+  begin
+    raise EBinSizeError.Create('TBot.LoadState: Wrong FDifficult Size');
+  end;
+  FDifficult := TDifficult(p^);
+end;
+
+end.
diff --git a/src/game/g_playermodel.pas b/src/game/g_playermodel.pas
new file mode 100644 (file)
index 0000000..283113e
--- /dev/null
@@ -0,0 +1,860 @@
+unit g_playermodel;
+
+interface
+
+uses
+  g_textures, g_basic, e_graphics, WADEDITOR,
+  WADSTRUCT, g_weapons;
+
+const
+  A_STAND      = 0;
+  A_WALK       = 1;
+  A_DIE1       = 2;
+  A_DIE2       = 3;
+  A_ATTACK     = 4;
+  A_SEEUP      = 5;
+  A_SEEDOWN    = 6;
+  A_ATTACKUP   = 7;
+  A_ATTACKDOWN = 8;
+  A_PAIN       = 9;
+
+  MODELSOUND_PAIN = 0;
+  MODELSOUND_DIE  = 1;
+  
+type
+  TModelInfo = record
+    Name:        String;
+    Author:      String;
+    Description: String;
+    HaveWeapon:  Boolean;
+  end;
+
+  TModelSound = record
+    ID:    DWORD;
+    Level: Byte;
+  end;
+
+  TGibSprite = record
+    ID: DWORD;
+    MaskID: DWORD;
+    Rect: TRectWH;
+    OnlyOne: Boolean;
+  end;
+
+  TModelSoundArray = Array of TModelSound;
+  TGibsArray = Array of TGibSprite;
+  TWeaponPoints = Array [WEAPON_SAW..WEAPON_SUPERPULEMET] of
+                  Array [A_STAND..A_PAIN] of
+                  Array [D_LEFT..D_RIGHT] of Array of TPoint;
+
+  TPlayerModel = class (TObject)
+  private
+    FName:             String;
+    FDirection:        TDirection;
+    FColor:            TRGB;
+    FCurrentAnimation: Byte;
+    FAnim:             Array [D_LEFT..D_RIGHT] of Array [A_STAND..A_PAIN] of TAnimation;
+    FMaskAnim:         Array [D_LEFT..D_RIGHT] of Array [A_STAND..A_PAIN] of TAnimation;
+    FWeaponPoints:     TWeaponPoints;
+    FPainSounds:       TModelSoundArray;
+    FDieSounds:        TModelSoundArray;
+    FSlopSound:        Byte;
+    FCurrentWeapon:    Byte;
+    FDrawWeapon:       Boolean;
+    FFlag:             Byte;
+    FFlagPoint:        TPoint;
+    FFlagAngle:        SmallInt;
+    FFlagAnim:         TAnimation;
+    FFire:             Boolean;
+    FFireCounter:      Byte;
+
+  public
+    destructor  Destroy(); override;
+    procedure   ChangeAnimation(Animation: Byte; Force: Boolean = False);
+    function    GetCurrentAnimation: TAnimation;
+    function    GetCurrentAnimationMask: TAnimation;
+    procedure   SetColor(Red, Green, Blue: Byte);
+    procedure   SetWeapon(Weapon: Byte);
+    procedure   SetFlag(Flag: Byte);
+    procedure   SetFire(Fire: Boolean);
+    function    PlaySound(SoundType, Level: Byte; X, Y: Integer): Boolean;
+    procedure   Update();
+    procedure   Draw(X, Y: Integer; Alpha: Byte = 0);
+
+    property    Fire: Boolean read FFire;
+    property    Direction: TDirection read FDirection write FDirection;
+    property    Animation: Byte read FCurrentAnimation;
+    property    Weapon: Byte read FCurrentWeapon;
+    property    Name: String read FName;
+    property    Color: TRGB read FColor write FColor;
+  end;
+
+procedure g_PlayerModel_LoadData();
+procedure g_PlayerModel_FreeData();
+function  g_PlayerModel_Load(FileName: String): Boolean;
+function  g_PlayerModel_GetNames(): SArray;
+function  g_PlayerModel_GetInfo(ModelName: String): TModelInfo;
+function  g_PlayerModel_Get(ModelName: String): TPlayerModel;
+function  g_PlayerModel_GetAnim(ModelName: String; Anim: Byte; var _Anim, _Mask: TAnimation): Boolean;
+function  g_PlayerModel_GetGibs(ModelName: String; var Gibs: TGibsArray): Boolean;
+
+implementation
+
+uses
+  g_main, g_sound, g_console, SysUtils, g_player, CONFIG,
+  GL, GLExt, e_sound, g_options, g_map, Math, e_log;
+
+type
+  TPlayerModelInfo = record
+    Info:         TModelInfo;
+    ModelSpeed:   Array [A_STAND..A_PAIN] of Byte;
+    FlagPoint:    TPoint;
+    FlagAngle:    SmallInt;
+    WeaponPoints: TWeaponPoints;
+    Gibs:         TGibsArray;
+    PainSounds:   TModelSoundArray;
+    DieSounds:    TModelSoundArray;
+    SlopSound:    Byte;
+  end;
+
+const
+  W_POS_NORMAL = 0;
+  W_POS_UP     = 1;
+  W_POS_DOWN   = 2;
+
+  W_ACT_NORMAL = 0;
+  W_ACT_FIRE   = 1;
+
+  FLAG_BASEPOINT: TPoint = (X:16; Y:43);
+  FLAG_DEFPOINT:  TPoint = (X:32; Y:16);
+  FLAG_DEFANGLE = -20;
+  WEAPONBASE: Array [WEAPON_SAW..WEAPON_SUPERPULEMET] of TPoint =
+              ((X:8; Y:4), (X:8; Y:8), (X:16; Y:16), (X:16; Y:24),
+               (X:16; Y:16), (X:24; Y:24), (X:16; Y:16), (X:24; Y:24), (X:16; Y:16));
+
+  AnimNames: Array [A_STAND..A_PAIN] of String =
+             ('StandAnim','WalkAnim','Die1Anim','Die2Anim','AttackAnim',
+              'SeeUpAnim','SeeDownAnim','AttackUpAnim','AttackDownAnim','PainAnim');
+  WeapNames: Array [WEAPON_SAW..WEAPON_SUPERPULEMET] of String =
+             ('csaw', 'hgun', 'sg', 'ssg', 'mgun', 'rkt', 'plz', 'bfg', 'spl');
+
+var
+  WeaponID: Array [WEAPON_SAW..WEAPON_SUPERPULEMET] of
+            Array [W_POS_NORMAL..W_POS_DOWN] of
+            Array [W_ACT_NORMAL..W_ACT_FIRE] of DWORD;
+  PlayerModelsArray: Array of TPlayerModelInfo;
+
+procedure g_PlayerModel_LoadData();
+var
+  a: Integer;
+begin
+  for a := WEAPON_SAW to WEAPON_SUPERPULEMET do
+  begin
+    g_Texture_CreateWAD(WeaponID[a][W_POS_NORMAL][W_ACT_NORMAL], GameWAD+':WEAPONS\'+UpperCase(WeapNames[a]));
+    g_Texture_CreateWAD(WeaponID[a][W_POS_NORMAL][W_ACT_FIRE], GameWAD+':WEAPONS\'+UpperCase(WeapNames[a])+'_FIRE');
+    g_Texture_CreateWAD(WeaponID[a][W_POS_UP][W_ACT_NORMAL], GameWAD+':WEAPONS\'+UpperCase(WeapNames[a])+'_UP');
+    g_Texture_CreateWAD(WeaponID[a][W_POS_UP][W_ACT_FIRE], GameWAD+':WEAPONS\'+UpperCase(WeapNames[a])+'_UP_FIRE');
+    g_Texture_CreateWAD(WeaponID[a][W_POS_DOWN][W_ACT_NORMAL], GameWAD+':WEAPONS\'+UpperCase(WeapNames[a])+'_DN');
+    g_Texture_CreateWAD(WeaponID[a][W_POS_DOWN][W_ACT_FIRE], GameWAD+':WEAPONS\'+UpperCase(WeapNames[a])+'_DN_FIRE');
+  end;
+end;
+
+function GetPoint(var str: String; var point: TPoint): Boolean;
+var
+  a, x, y: Integer;
+  s: String;
+begin
+  Result := False;
+
+  str := Trim(str);
+  if Length(str) < 3 then
+    Exit;
+
+  for a := 1 to Length(str) do
+    if (str[a] = ',') or (a = Length(str)) then
+    begin
+      s := Copy(str, 1, a);
+      if s[Length(s)] = ',' then
+        SetLength(s, Length(s)-1);
+      Delete(str, 1, a);
+
+      if (Sscanf(s, '%d:%d', [@x, @y]) < 2) or
+         (x < -64) or (x > 128) or
+         (y < -64) or (y > 128) then
+        Exit;
+
+      point.X := x;
+      point.Y := y;
+
+      Break;
+    end;
+
+  Result := True;
+end;
+
+function GetWeapPoints(str: String; weapon: Byte; anim: Byte; dir: TDirection;
+                       frames: Word; backanim: Boolean; var wpoints: TWeaponPoints): Boolean;
+var
+  a, b, h: Integer;
+begin
+  Result := False;
+
+  if frames = 0 then
+    Exit;
+
+  backanim := backanim and (frames > 2);
+
+  for a := 1 to frames do
+  begin
+    if not GetPoint(str, wpoints[weapon, anim, dir, a-1]) then
+      Exit;
+
+    with wpoints[weapon, anim, dir, a-1] do
+    begin
+      X := X - WEAPONBASE[weapon].X;
+      Y := Y - WEAPONBASE[weapon].Y;
+      if dir = D_LEFT then
+        X := -X;
+    end;
+  end;
+
+  h := High(wpoints[weapon, anim, dir]);
+  if backanim then
+    for b := h downto frames do
+      wpoints[weapon, anim, dir, b] := wpoints[weapon, anim, dir, h-b+1];
+
+  Result := True;
+end;
+
+function g_PlayerModel_Load(FileName: string): Boolean;
+var
+  ID: DWORD;
+  a, b, len, aa, bb, f: Integer;
+  cc: TDirection;
+  config: TConfig;
+  pData, pData2: Pointer;
+  WAD: TWADEditor_1;
+  s: string;
+  prefix: string;
+  ok: Boolean;
+begin
+  e_WriteLog(Format('Loading player model: %s', [ExtractFileName(FileName)]), MSG_NOTIFY);
+
+  Result := False;
+
+  WAD := TWADEditor_1.Create;
+  WAD.ReadFile(FileName);
+
+  if WAD.GetLastError <> DFWAD_NOERROR then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  if not WAD.GetResource('TEXT', 'MODEL', pData, len) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+  config := TConfig.CreateMem(pData, len);
+  FreeMem(pData);
+
+  s := config.ReadStr('Model', 'name', '');
+  if s = '' then
+  begin
+    config.Free();
+    WAD.Free();
+    Exit;
+  end; 
+
+  SetLength(PlayerModelsArray, Length(PlayerModelsArray)+1);
+  ID := High(PlayerModelsArray);
+
+  prefix := FileName+':TEXTURES\';
+
+  with PlayerModelsArray[ID].Info do
+  begin
+    Name := s;
+    Author := config.ReadStr('Model', 'author', '');
+    Description := config.ReadStr('Model', 'description', '');
+  end;
+
+  for b := A_STAND to A_PAIN do
+  begin
+    if not (g_Frames_CreateWAD(nil, s+'_RIGHTANIM'+IntToStr(b),
+                               prefix+config.ReadStr(AnimNames[b], 'resource', ''),
+                               64, 64, config.ReadInt(AnimNames[b], 'frames', 1),
+                               config.ReadBool(AnimNames[b], 'backanim', False)) and
+            g_Frames_CreateWAD(nil, s+'_RIGHTANIM'+IntToStr(b)+'_MASK',
+                               prefix+config.ReadStr(AnimNames[b], 'mask', ''),
+                               64, 64, config.ReadInt(AnimNames[b], 'frames', 1),
+                               config.ReadBool(AnimNames[b], 'backanim', False))) then
+    begin
+      config.Free();
+      WAD.Free();
+      Exit;
+    end;
+
+    for aa := WEAPON_SAW to WEAPON_SUPERPULEMET do
+      for bb := A_STAND to A_PAIN do
+        for cc := D_LEFT to D_RIGHT do
+        begin
+          f := config.ReadInt(AnimNames[bb], 'frames', 1);
+          if config.ReadBool(AnimNames[bb], 'backanim', False) then
+            if f > 2 then f := 2*f-2;
+          SetLength(PlayerModelsArray[ID].WeaponPoints[aa, bb, cc], f);
+        end;
+
+    if (config.ReadStr(AnimNames[b], 'resource2', '') <> '') and
+       (config.ReadStr(AnimNames[b], 'mask2', '') <> '') then
+    begin
+      g_Frames_CreateWAD(nil, s+'_LEFTANIM'+IntToStr(b),
+                         prefix+config.ReadStr(AnimNames[b], 'resource2', ''),
+                         64, 64, config.ReadInt(AnimNames[b], 'frames', 1),
+                         config.ReadBool(AnimNames[b], 'backanim', False));
+
+      g_Frames_CreateWAD(nil, s+'_LEFTANIM'+IntToStr(b)+'_MASK',
+                         prefix+config.ReadStr(AnimNames[b], 'mask2', ''),
+                         64, 64, config.ReadInt(AnimNames[b], 'frames', 1),
+                         config.ReadBool(AnimNames[b], 'backanim', False));
+    end;
+
+    PlayerModelsArray[ID].ModelSpeed[b] := Max(1, config.ReadInt(AnimNames[b], 'waitcount', 1) div 3);
+  end;
+
+  with PlayerModelsArray[ID], config do
+  begin
+    prefix := FileName+':SOUNDS\';
+
+    a := 1;
+    repeat
+      s := config.ReadStr('Sound', 'pain'+IntToStr(a), '');
+      if s <> '' then
+      begin
+        SetLength(PainSounds, Length(PainSounds)+1);
+        g_Sound_CreateWAD(PainSounds[High(PainSounds)].ID, prefix+s);
+        PainSounds[High(PainSounds)].Level := config.ReadInt('Sound', 'painlevel'+IntToStr(a), 1);
+      end;
+      a := a+1;
+    until s = '';
+
+    a := 1;
+    repeat
+      s := config.ReadStr('Sound', 'die'+IntToStr(a), '');
+      if s <> '' then
+      begin
+        SetLength(DieSounds, Length(DieSounds)+1);
+        g_Sound_CreateWAD(DieSounds[High(DieSounds)].ID, prefix+s);
+        DieSounds[High(DieSounds)].Level := config.ReadInt('Sound', 'dielevel'+IntToStr(a), 1);
+      end;
+      a := a+1;
+    until s = '';
+
+    SlopSound := Min(Max(config.ReadInt('Sound', 'slop', 0), 0), 2);
+
+    SetLength(Gibs, ReadInt('Gibs', 'count', 0));
+
+    if (Gibs <> nil) and
+       (WAD.GetResource('TEXTURES', config.ReadStr('Gibs', 'resource', 'GIBS'), pData, len)) and
+       (WAD.GetResource('TEXTURES', config.ReadStr('Gibs', 'mask', 'GIBSMASK'), pData2, len)) then
+    begin
+      for a := 0 to High(Gibs) do
+        if e_CreateTextureMemEx(pData, Gibs[a].ID, a*32, 0, 32, 32) and
+          e_CreateTextureMemEx(pData2, Gibs[a].MaskID, a*32, 0, 32, 32) then
+        begin
+          Gibs[a].Rect := e_GetTextureSize2(Gibs[a].ID);
+          with Gibs[a].Rect do
+            if Height > 3 then Height := Height-1-Random(2);
+          Gibs[a].OnlyOne := config.ReadInt('Gibs', 'once', -1) = a+1;
+        end;
+
+      FreeMem(pData);
+      FreeMem(pData2);
+    end;
+
+    ok := True;
+    for aa := WEAPON_SAW to WEAPON_SUPERPULEMET do
+      for bb := A_STAND to A_PAIN do
+        if not (bb in [A_DIE1, A_DIE2, A_PAIN]) then
+        begin
+          ok := ok and GetWeapPoints(config.ReadStr(AnimNames[bb], WeapNames[aa]+'_points', ''), aa, bb, D_RIGHT,
+                                     config.ReadInt(AnimNames[bb], 'frames', 0),
+                                     config.ReadBool(AnimNames[bb], 'backanim', False),
+                                     WeaponPoints);
+
+          if not GetWeapPoints(config.ReadStr(AnimNames[bb], WeapNames[aa]+'2_points', ''), aa, bb, D_LEFT,
+                               config.ReadInt(AnimNames[bb], 'frames', 0),
+                               config.ReadBool(AnimNames[bb], 'backanim', False),
+                               WeaponPoints) then
+            for f := 0 to High(WeaponPoints[aa, bb, D_RIGHT]) do
+            begin
+              WeaponPoints[aa, bb, D_LEFT, f].X := -WeaponPoints[aa, bb, D_RIGHT, f].X;
+              WeaponPoints[aa, bb, D_LEFT, f].Y := WeaponPoints[aa, bb, D_RIGHT, f].Y;
+            end;
+
+          if not ok then Break;
+        end;
+    {if ok then g_Console_Add(Info.Name+' weapon points ok')
+    else g_Console_Add(Info.Name+' weapon points fail');}
+    Info.HaveWeapon := ok;
+
+    s := config.ReadStr('Model', 'flag_point', '');
+    if not GetPoint(s, FlagPoint) then FlagPoint := FLAG_DEFPOINT;
+
+    FlagAngle := config.ReadInt('Model', 'flag_angle', FLAG_DEFANGLE);
+  end;
+
+  config.Free();
+  WAD.Free();
+
+  Result := True;
+end;
+
+function g_PlayerModel_Get(ModelName: String): TPlayerModel;
+var
+  a: Integer;
+  b: Byte;
+  ID, ID2: DWORD;
+begin
+  Result := nil;
+
+  if PlayerModelsArray = nil then Exit;
+
+  for a := 0 to High(PlayerModelsArray) do
+    if AnsiLowerCase(PlayerModelsArray[a].Info.Name) = AnsiLowerCase(ModelName) then
+    begin
+      Result := TPlayerModel.Create;
+
+      with PlayerModelsArray[a] do
+      begin
+        Result.FName := Info.Name;
+
+        for b := A_STAND to A_PAIN do
+        begin
+          if not (g_Frames_Get(ID, Info.Name+'_RIGHTANIM'+IntToStr(b)) and
+                  g_Frames_Get(ID2, Info.Name+'_RIGHTANIM'+IntToStr(b)+'_MASK')) then
+          begin
+            Result.Free();
+            Result := nil;
+          Exit;
+        end;
+
+        Result.FAnim[D_RIGHT][b] := TAnimation.Create(ID, b in [A_STAND, A_WALK], ModelSpeed[b]);
+
+        Result.FMaskAnim[D_RIGHT][b] := TAnimation.Create(ID2, b in [A_STAND, A_WALK], ModelSpeed[b]);
+
+        if g_Frames_Exists(Info.Name+'_LEFTANIM'+IntToStr(b)) and
+           g_Frames_Exists(Info.Name+'_LEFTANIM'+IntToStr(b)+'_MASK') then
+        if g_Frames_Get(ID, Info.Name+'_LEFTANIM'+IntToStr(b)) and
+           g_Frames_Get(ID2, Info.Name+'_LEFTANIM'+IntToStr(b)+'_MASK') then
+        begin
+          Result.FAnim[D_LEFT][b] := TAnimation.Create(ID, b in [A_STAND, A_WALK], ModelSpeed[b]);
+
+          Result.FMaskAnim[D_LEFT][b] := TAnimation.Create(ID2, b in [A_STAND, A_WALK], ModelSpeed[b]);
+        end;
+
+        Result.FPainSounds := PainSounds;
+        Result.FDieSounds := DieSounds;
+        Result.FSlopSound := SlopSound;
+      end;
+
+      Result.FDrawWeapon := Info.HaveWeapon;
+      Result.FWeaponPoints := WeaponPoints;
+
+      Result.FFlagPoint := FlagPoint;
+      Result.FFlagAngle := FlagAngle;
+
+      Break;
+    end;
+  end;
+end;
+
+function g_PlayerModel_GetAnim(ModelName: string; Anim: Byte; var _Anim, _Mask: TAnimation): Boolean;
+var
+  a: Integer;
+  c: Boolean;
+  ID: DWORD;
+begin
+  Result := False;
+
+  if PlayerModelsArray = nil then Exit;
+  for a := 0 to High(PlayerModelsArray) do
+    if PlayerModelsArray[a].Info.Name = ModelName then
+      with PlayerModelsArray[a] do
+      begin
+        if Anim in [A_STAND, A_WALK] then c := True else c := False;
+
+        if not g_Frames_Get(ID, Info.Name+'_RIGHTANIM'+IntToStr(Anim)) then
+          if not g_Frames_Get(ID, Info.Name+'_LEFTANIM'+IntToStr(Anim)) then Exit;
+
+        _Anim := TAnimation.Create(ID, c, ModelSpeed[Anim]);
+        _Anim.Speed := ModelSpeed[Anim];
+
+        if not g_Frames_Get(ID, Info.Name+'_RIGHTANIM'+IntToStr(Anim)+'_MASK') then
+          if not g_Frames_Get(ID, Info.Name+'_LEFTANIM'+IntToStr(Anim)+'_MASK') then Exit;
+
+        _Mask := TAnimation.Create(ID, c, ModelSpeed[Anim]);
+        _Mask.Speed := ModelSpeed[Anim];
+
+        Break;
+      end;
+
+  Result := True;
+end;
+
+function g_PlayerModel_GetGibs(ModelName: string; var Gibs: TGibsArray): Boolean;
+var
+  a, i, b: Integer;
+  c: Boolean;
+begin
+  Result := False;
+
+  if PlayerModelsArray = nil then Exit;
+  if gGibsCount = 0 then Exit;
+
+  c := False;
+
+  SetLength(Gibs, gGibsCount);
+
+  for a := 0 to High(PlayerModelsArray) do
+    if PlayerModelsArray[a].Info.Name = ModelName then
+    begin
+      for i := 0 to High(Gibs) do
+      begin
+        if c and (Length(PlayerModelsArray[a].Gibs) = 1) then
+        begin
+          SetLength(Gibs, i);
+          Break;
+        end;
+
+        repeat
+          b := Random(Length(PlayerModelsArray[a].Gibs));
+        until not (PlayerModelsArray[a].Gibs[b].OnlyOne and c);
+
+        Gibs[i] := PlayerModelsArray[a].Gibs[b];
+
+        if Gibs[i].OnlyOne then c := True;
+      end;
+
+      Result := True;
+      Break;
+    end;
+end;
+
+function g_PlayerModel_GetNames(): SArray;
+var
+  i: DWORD;
+begin
+  Result := nil;
+
+  if PlayerModelsArray = nil then Exit;
+
+  for i := 0 to High(PlayerModelsArray) do
+  begin
+    SetLength(Result, Length(Result)+1);
+    Result[High(Result)] := PlayerModelsArray[i].Info.Name;
+  end;
+end;
+
+function g_PlayerModel_GetInfo(ModelName: string): TModelInfo;
+var
+  a: Integer;
+begin
+  if PlayerModelsArray = nil then Exit;
+
+  for a := 0 to High(PlayerModelsArray) do
+    if PlayerModelsArray[a].Info.Name = ModelName then
+    begin
+      Result := PlayerModelsArray[a].Info;
+      Break;
+    end;
+end;
+
+procedure g_PlayerModel_FreeData();
+var
+  i: DWORD;
+  a, b, c: Integer;
+begin
+  for a := WEAPON_SAW to WEAPON_SUPERPULEMET do
+    for b := W_POS_NORMAL to W_POS_DOWN do
+      for c := W_ACT_NORMAL to W_ACT_FIRE do
+        e_DeleteTexture(WeaponID[a][b][c]); 
+
+  e_WriteLog('Releasing models...', MSG_NOTIFY);
+
+  if PlayerModelsArray = nil then Exit;
+
+  for i := 0 to High(PlayerModelsArray) do
+    with PlayerModelsArray[i] do
+    begin
+      for a := A_STAND to A_PAIN do
+      begin
+        g_Frames_DeleteByName(Info.Name+'_LEFTANIM'+IntToStr(a));
+        g_Frames_DeleteByName(Info.Name+'_LEFTANIM'+IntToStr(a)+'_MASK');
+        g_Frames_DeleteByName(Info.Name+'_RIGHTANIM'+IntToStr(a));
+        g_Frames_DeleteByName(Info.Name+'_RIGHTANIM'+IntToStr(a)+'_MASK');
+      end;
+
+      if PainSounds <> nil then
+        for b := 0 to High(PainSounds) do
+          e_DeleteSound(PainSounds[b].ID);
+
+      if DieSounds <> nil then
+        for b := 0 to High(DieSounds) do
+          e_DeleteSound(DieSounds[b].ID);
+
+      if Gibs <> nil then
+        for b := 0 to High(Gibs) do
+        begin
+          e_DeleteTexture(Gibs[b].ID);
+          e_DeleteTexture(Gibs[b].MaskID); 
+        end;
+    end;
+
+  PlayerModelsArray := nil;
+end;
+
+{ TPlayerModel }
+
+procedure TPlayerModel.ChangeAnimation(Animation: Byte; Force: Boolean = False);
+begin
+  if not Force then if FCurrentAnimation = Animation then Exit;
+
+  FCurrentAnimation := Animation;
+
+  if (FDirection = D_LEFT) and
+     (FAnim[D_LEFT][FCurrentAnimation] <> nil) and
+     (FMaskAnim[D_LEFT][FCurrentAnimation] <> nil) then
+  begin
+    FAnim[D_LEFT][FCurrentAnimation].Reset;
+    FMaskAnim[D_LEFT][FCurrentAnimation].Reset;
+  end
+  else
+  begin
+    FAnim[D_RIGHT][FCurrentAnimation].Reset;
+    FMaskAnim[D_RIGHT][FCurrentAnimation].Reset;
+  end;
+end;
+
+destructor TPlayerModel.Destroy();
+var
+  a: Byte;
+begin
+  for a := A_STAND to A_PAIN do
+  begin
+    FAnim[D_LEFT][a].Free();
+    FMaskAnim[D_LEFT][a].Free();
+    FAnim[D_RIGHT][a].Free();
+    FMaskAnim[D_RIGHT][a].Free();
+  end;
+
+  inherited;
+end;
+
+procedure TPlayerModel.Draw(X, Y: Integer; Alpha: Byte = 0);
+var
+  Mirror: TMirrorType;
+  pos, act: Byte;
+  p: TPoint;
+begin
+// Ôëàãè:
+  if Direction = D_LEFT then
+    Mirror := M_NONE
+  else
+    Mirror := M_HORIZONTAL;
+
+  if (FFlag <> FLAG_NONE) and (FFlagAnim <> nil) and
+     (not (FCurrentAnimation in [A_DIE1, A_DIE2])) then
+  begin
+    p.X := IfThen(Direction = D_LEFT,
+                  FLAG_BASEPOINT.X,
+                  64-FLAG_BASEPOINT.X);
+    p.Y := FLAG_BASEPOINT.Y;
+
+    FFlagAnim.DrawEx(X+IfThen(Direction = D_LEFT, FFlagPoint.X-1, 2*FLAG_BASEPOINT.X-FFlagPoint.X+1)-FLAG_BASEPOINT.X,
+                     Y+FFlagPoint.Y-FLAG_BASEPOINT.Y+1, Mirror, p,
+                     IfThen(FDirection = D_RIGHT, FFlagAngle, -FFlagAngle));
+  end;
+
+// Îðóæèå:
+  if Direction = D_RIGHT then
+    Mirror := M_NONE
+  else
+    Mirror := M_HORIZONTAL;
+
+  if FDrawWeapon and
+    (not (FCurrentAnimation in [A_DIE1, A_DIE2, A_PAIN])) and
+    (FCurrentWeapon in [WEAPON_SAW..WEAPON_SUPERPULEMET]) then
+  begin
+    if FCurrentAnimation in [A_SEEUP, A_ATTACKUP] then
+      pos := W_POS_UP
+    else
+      if FCurrentAnimation in [A_SEEDOWN, A_ATTACKDOWN] then
+        pos := W_POS_DOWN
+      else
+        pos := W_POS_NORMAL;
+
+    if (FCurrentAnimation in [A_ATTACK, A_ATTACKUP, A_ATTACKDOWN]) or
+       FFire then
+      act := W_ACT_FIRE
+    else
+      act := W_ACT_NORMAL;
+
+    if Alpha < 201 then
+      e_Draw(WeaponID[FCurrentWeapon][pos][act],
+             X+FWeaponPoints[FCurrentWeapon, FCurrentAnimation, FDirection,
+                             FAnim[D_RIGHT][FCurrentAnimation].CurrentFrame].X,
+             Y+FWeaponPoints[FCurrentWeapon, FCurrentAnimation, FDirection,
+                             FAnim[D_RIGHT][FCurrentAnimation].CurrentFrame].Y,
+             0, True, False, Mirror);
+  end;
+
+// Ìîäåëü:
+  if (FDirection = D_LEFT) and
+     (FAnim[D_LEFT][FCurrentAnimation] <> nil) then
+  begin
+    FAnim[D_LEFT][FCurrentAnimation].Alpha := Alpha;
+    FAnim[D_LEFT][FCurrentAnimation].Draw(X, Y, M_NONE);
+  end
+  else
+  begin
+    FAnim[D_RIGHT][FCurrentAnimation].Alpha := Alpha;
+    FAnim[D_RIGHT][FCurrentAnimation].Draw(X, Y, Mirror);
+  end;
+
+// Ìàñêà ìîäåëè:
+  e_Colors := FColor;
+
+  if (FDirection = D_LEFT) and
+     (FMaskAnim[D_LEFT][FCurrentAnimation] <> nil) then
+  begin
+    FMaskAnim[D_LEFT][FCurrentAnimation].Alpha := Alpha;
+    FMaskAnim[D_LEFT][FCurrentAnimation].Draw(X, Y, M_NONE);
+  end
+  else
+  begin
+    FMaskAnim[D_RIGHT][FCurrentAnimation].Alpha := Alpha;
+    FMaskAnim[D_RIGHT][FCurrentAnimation].Draw(X, Y, Mirror);
+  end;
+
+  e_Colors.R := 255;
+  e_Colors.G := 255;
+  e_Colors.B := 255;
+end;
+
+function TPlayerModel.GetCurrentAnimation: TAnimation;
+begin
+  if (FDirection = D_LEFT) and (FAnim[D_LEFT][FCurrentAnimation] <> nil) then
+    Result := FAnim[D_LEFT][FCurrentAnimation]
+  else
+    Result := FAnim[D_RIGHT][FCurrentAnimation];
+end;
+
+function TPlayerModel.GetCurrentAnimationMask: TAnimation;
+begin
+  if (FDirection = D_LEFT) and (FMaskAnim[D_LEFT][FCurrentAnimation] <> nil) then
+    Result := FMaskAnim[D_LEFT][FCurrentAnimation]
+  else
+    Result := FMaskAnim[D_RIGHT][FCurrentAnimation];
+end;
+
+function TPlayerModel.PlaySound(SoundType, Level: Byte; X, Y: Integer): Boolean;
+var
+  TempArray: array of DWORD;
+  a: Integer;
+begin
+  Result := False;
+  SetLength(TempArray, 0);
+
+  if SoundType = MODELSOUND_PAIN then
+  begin
+    if FPainSounds = nil then Exit;
+
+    for a := 0 to High(FPainSounds) do
+      if FPainSounds[a].Level = Level then
+      begin
+        SetLength(TempArray, Length(TempArray)+1);
+        TempArray[High(TempArray)] := FPainSounds[a].ID;
+      end;
+  end
+  else
+  begin
+    if (Level in [2, 3]) and (FSlopSound > 0) then
+    begin
+      g_Sound_PlayExAt('SOUND_MONSTER_SLOP', X, Y);
+      if FSlopSound = 1 then
+      begin
+        Result := True;
+        Exit;
+      end;
+    end;
+    if FDieSounds = nil then Exit;
+
+    for a := 0 to High(FDieSounds) do
+      if FDieSounds[a].Level = Level then
+      begin
+        SetLength(TempArray, Length(TempArray)+1);
+        TempArray[High(TempArray)] := FDieSounds[a].ID;
+      end;
+  end;
+
+  if TempArray = nil then Exit;
+
+  g_Sound_PlayAt(TempArray[Random(Length(TempArray))], X, Y);
+
+  Result := True;
+end;
+
+procedure TPlayerModel.SetColor(Red, Green, Blue: Byte);
+begin
+  FColor.R := Red;
+  FColor.G := Green;
+  FColor.B := Blue;
+end;
+
+procedure TPlayerModel.SetFire(Fire: Boolean);
+begin
+  FFire := Fire;
+
+  if FFire then FFireCounter := FAnim[D_RIGHT, A_ATTACK].Speed*FAnim[D_RIGHT, A_ATTACK].TotalFrames
+  else FFireCounter := 0;
+end;
+
+procedure TPlayerModel.SetFlag(Flag: Byte);
+var
+  id: DWORD;
+begin
+  FFlag := Flag;
+
+  FFlagAnim.Free();
+  FFlagAnim := nil;
+
+  case Flag of
+    FLAG_RED: g_Frames_Get(id, 'FRAMES_FLAG_RED');
+    FLAG_BLUE: g_Frames_Get(id, 'FRAMES_FLAG_BLUE');
+    else Exit;
+  end;
+
+  FFlagAnim := TAnimation.Create(id, True, 8);
+end;
+
+procedure TPlayerModel.SetWeapon(Weapon: Byte);
+begin
+  FCurrentWeapon := Weapon;
+end;
+
+procedure TPlayerModel.Update();
+begin
+  if (FDirection = D_LEFT) and (FAnim[D_LEFT][FCurrentAnimation] <> nil) then
+    FAnim[D_LEFT][FCurrentAnimation].Update else FAnim[D_RIGHT][FCurrentAnimation].Update;
+
+  if (FDirection = D_LEFT) and (FMaskAnim[D_LEFT][FCurrentAnimation] <> nil) then
+    FMaskAnim[D_LEFT][FCurrentAnimation].Update else FMaskAnim[D_RIGHT][FCurrentAnimation].Update;
+
+  if FFlagAnim <> nil then FFlagAnim.Update;
+
+  if FFireCounter > 0 then Dec(FFireCounter) else FFire := False;
+end;
+
+end.
diff --git a/src/game/g_res_downloader.pas b/src/game/g_res_downloader.pas
new file mode 100644 (file)
index 0000000..e4959b7
--- /dev/null
@@ -0,0 +1,151 @@
+unit g_res_downloader;
+
+interface
+
+uses sysutils, Classes, md5, g_net, g_netmsg, g_console, g_main, e_log;
+
+function g_Res_SearchSameWAD(const path, filename: string; const resMd5: TMD5Digest): string;
+function g_Res_DownloadWAD(const FileName: string): string;
+
+implementation
+
+uses g_language;
+
+const DOWNLOAD_DIR = 'downloads';
+
+procedure FindFiles(const dirName, filename: string; var files: TStringList);
+var
+  searchResult: TSearchRec;
+begin
+  if FindFirst(dirName+'/*', faAnyFile, searchResult) = 0 then
+  begin
+    try
+      repeat
+        if (searchResult.Attr and faDirectory) = 0 then
+        begin
+          if searchResult.Name = filename then
+          begin
+            files.Add(dirName+'/'+filename);
+            Exit;
+          end;
+        end
+        else if (searchResult.Name <> '.') and (searchResult.Name <> '..') then
+          FindFiles(IncludeTrailingPathDelimiter(dirName)+searchResult.Name,
+                    filename, files);
+      until FindNext(searchResult) <> 0;
+    finally
+      FindClose(searchResult);
+    end;
+  end;
+end;
+
+function CompareFileHash(const filename: string; const resMd5: TMD5Digest): Boolean;
+var
+  gResHash: TMD5Digest;
+begin
+  gResHash := MD5File(filename);
+  Result := MD5Match(gResHash, resMd5);
+end;
+
+function CheckFileHash(const path, filename: string; const resMd5: TMD5Digest): Boolean;
+begin
+  Result := FileExists(path + filename) and CompareFileHash(path + filename, resMd5);
+end;
+
+function g_Res_SearchSameWAD(const path, filename: string; const resMd5: TMD5Digest): string;
+var
+  res: string;
+  files: TStringList;
+  i: Integer;
+begin
+  Result := '';
+
+  if CheckFileHash(path, filename, resMd5) then
+  begin
+    Result := path + filename;
+    Exit;
+  end;
+
+  files := TStringList.Create;
+
+  FindFiles(path, filename, files);
+  for i := 0 to files.Count - 1 do
+  begin
+    res := files.Strings[i];
+    if CompareFileHash(res, resMd5) then
+    begin
+      Result := res;
+      Break;
+    end;
+  end;
+
+  files.Free;
+end;
+
+function SaveWAD(const path, filename: string; const data: array of Byte): string;
+var
+  resFile: TFileStream;
+begin
+  try
+    Result := path + DOWNLOAD_DIR + '/' + filename;
+    if not DirectoryExists(path + DOWNLOAD_DIR) then
+    begin
+      CreateDir(path + DOWNLOAD_DIR);
+    end;
+    resFile := TFileStream.Create(Result, fmCreate);
+    resFile.WriteBuffer(data[0], Length(data));
+    resFile.Free
+  except
+    Result := '';
+  end;
+end;
+
+function g_Res_DownloadWAD(const FileName: string): string;
+var
+  msgStream: TMemoryStream;
+  resStream: TFileStream;
+  mapData: TMapDataMsg;
+  i: Integer;
+  resData: TResDataMsg;
+begin
+  g_Console_Add(Format(_lc[I_NET_MAP_DL], [FileName]));
+  e_WriteLog('Downloading map `' + FileName + '` from server', MSG_NOTIFY);
+  MC_SEND_MapRequest();
+
+  msgStream := g_Net_Wait_Event(NET_MSG_MAP_RESPONSE);
+  if msgStream <> nil then
+  begin
+    mapData := MapDataFromMsgStream(msgStream);
+    msgStream.Free;
+  end;
+
+  for i := 0 to High(mapData.ExternalResources) do
+  begin
+    if not CheckFileHash(GameDir + '/wads/',
+                         mapData.ExternalResources[i].Name,
+                         mapData.ExternalResources[i].md5) then
+    begin
+      g_Console_Add(Format(_lc[I_NET_WAD_DL],
+                           [mapData.ExternalResources[i].Name]));
+      e_WriteLog('Downloading Wad `' + mapData.ExternalResources[i].Name +
+                 '` from server', MSG_NOTIFY);
+      MC_SEND_ResRequest(mapData.ExternalResources[i].Name);
+
+      msgStream := g_Net_Wait_Event(NET_MSG_RES_RESPONSE);
+      resData := ResDataFromMsgStream(msgStream);
+
+      resStream := TFileStream.Create(GameDir+'/wads/'+
+                                      mapData.ExternalResources[i].Name,
+                                      fmCreate);
+      resStream.WriteBuffer(resData.FileData[0], resData.FileSize);
+
+      resData.FileData := nil;
+      resStream.Free;
+      msgStream.Free;
+    end;
+  end;
+
+  Result := SaveWAD(MapsDir, ExtractFileName(FileName), mapData.FileData);
+end;
+
+end.
diff --git a/src/game/g_saveload.pas b/src/game/g_saveload.pas
new file mode 100644 (file)
index 0000000..36cf729
--- /dev/null
@@ -0,0 +1,577 @@
+unit g_saveload;
+
+interface
+
+uses
+  e_graphics, g_phys, g_textures, BinEditor;
+
+function g_GetSaveName(n: Integer): String;
+function g_SaveGame(n: Integer; Name: String): Boolean;
+function g_LoadGame(n: Integer): Boolean;
+procedure Obj_SaveState(o: PObj; var Mem: TBinMemoryWriter);
+procedure Obj_LoadState(o: PObj; var Mem: TBinMemoryReader);
+
+implementation
+
+uses
+  g_game, g_items, g_map, g_monsters, g_triggers,
+  g_basic, g_main, SysUtils, Math, WADEDITOR,
+  MAPSTRUCT, MAPDEF, g_weapons, g_player, g_console,
+  e_log, g_language;
+
+const
+  SAVE_SIGNATURE = $56534644; // 'DFSV'
+  SAVE_VERSION = $02;
+  END_MARKER_STRING = 'END';
+  PLAYER_VIEW_SIGNATURE = $57564C50; // 'PLVW'
+  OBJ_SIGNATURE = $4A424F5F; // '_OBJ'
+
+procedure Obj_SaveState(o: PObj; var Mem: TBinMemoryWriter);
+var
+  sig: DWORD;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà îáúåêòà:
+  sig := OBJ_SIGNATURE; // '_OBJ'
+  Mem.WriteDWORD(sig);
+// Ïîëîæåíèå ïî-ãîðèçîíòàëè:
+  Mem.WriteInt(o^.X);
+// Ïîëîæåíèå ïî-âåðòèêàëè:
+  Mem.WriteInt(o^.Y);
+// Îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê:
+  Mem.WriteInt(o^.Rect.X);
+  Mem.WriteInt(o^.Rect.Y);
+  Mem.WriteWord(o^.Rect.Width);
+  Mem.WriteWord(o^.Rect.Height);
+// Ñêîðîñòü:
+  Mem.WriteInt(o^.Vel.X);
+  Mem.WriteInt(o^.Vel.Y);
+// Ïðèáàâêà ê ñêîðîñòè:
+  Mem.WriteInt(o^.Accel.X);
+  Mem.WriteInt(o^.Accel.Y);
+end;
+
+procedure Obj_LoadState(o: PObj; var Mem: TBinMemoryReader);
+var
+  sig: DWORD;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà îáúåêòà:
+  Mem.ReadDWORD(sig);
+  if sig <> OBJ_SIGNATURE then // '_OBJ'
+  begin
+    raise EBinSizeError.Create('Obj_LoadState: Wrong Object Signature');
+  end;
+// Ïîëîæåíèå ïî-ãîðèçîíòàëè:
+  Mem.ReadInt(o^.X);
+// Ïîëîæåíèå ïî-âåðòèêàëè:
+  Mem.ReadInt(o^.Y);
+// Îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê:
+  Mem.ReadInt(o^.Rect.X);
+  Mem.ReadInt(o^.Rect.Y);
+  Mem.ReadWord(o^.Rect.Width);
+  Mem.ReadWord(o^.Rect.Height);
+// Ñêîðîñòü:
+  Mem.ReadInt(o^.Vel.X);
+  Mem.ReadInt(o^.Vel.Y);
+// Ïðèáàâêà ê ñêîðîñòè:
+  Mem.ReadInt(o^.Accel.X);
+  Mem.ReadInt(o^.Accel.Y);
+end;
+
+function g_GetSaveName(n: Integer): String;
+var
+  bFile: TBinFileReader;
+  bMem: TBinMemoryReader;
+  str: String;
+begin
+  Result := '';
+  str := '';
+  bMem := nil;
+  bFile := nil;
+
+  try
+  // Îòêðûâàåì ôàéë ñîõðàíåíèé:
+    bFile := TBinFileReader.Create();
+    if bFile.OpenFile(DataDir + 'SAVGAME' + IntToStr(n) + '.DAT',
+                      SAVE_SIGNATURE, SAVE_VERSION) then
+    begin
+    // ×èòàåì ïåðâûé áëîê - ñîñòîÿíèå èãðû:
+      bMem := TBinMemoryReader.Create();
+      bFile.ReadMemory(bMem);
+    // Èìÿ èãðû:
+      bMem.ReadString(str);
+
+    // Çàêðûâàåì ôàéë:
+      bFile.Close();
+    end;
+
+  except
+    on E1: EInOutError do
+      e_WriteLog('GetSaveName I/O Error: '+E1.Message, MSG_WARNING);
+    on E2: EBinSizeError do
+      e_WriteLog('GetSaveName Size Error: '+E2.Message, MSG_WARNING);
+  end;
+
+  bMem.Free();
+  bFile.Free();
+
+  Result := str;
+end;
+
+function g_SaveGame(n: Integer; Name: String): Boolean;
+var
+  bFile: TBinFileWriter;
+  bMem: TBinMemoryWriter;
+  sig: DWORD;
+  str: String;
+  nPlayers: Byte;
+  i, k: Integer;
+  PID1, PID2: Word;
+begin
+  Result := False;
+  bMem := nil;
+  bFile := nil;
+
+  try
+  // Ñîçäàåì ôàéë ñîõðàíåíèÿ:
+    bFile := TBinFileWriter.Create();
+    bFile.OpenFile(DataDir + 'SAVGAME' + IntToStr(n) + '.DAT',
+                   SAVE_SIGNATURE, SAVE_VERSION);
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå èãðû: /////
+    bMem := TBinMemoryWriter.Create(256);
+  // Èìÿ èãðû:
+    bMem.WriteString(Name, 32);
+  // Ïóòü ê êàðòå:
+    str := gGameSettings.WAD;
+    bMem.WriteString(str, 128);
+  // Èìÿ êàðòû:
+    g_ProcessResourceStr(gMapInfo.Map, nil, nil, @str);
+    bMem.WriteString(str, 32);
+  // Êîëè÷åñòâî èãðîêîâ:
+    nPlayers := g_Player_GetCount();
+    bMem.WriteByte(nPlayers);
+  // Èãðîâîå âðåìÿ:
+    bMem.WriteDWORD(gTime);
+  // Òèï èãðû:
+    bMem.WriteByte(gGameSettings.GameType);
+  // Ðåæèì èãðû:
+    bMem.WriteByte(gGameSettings.GameMode);
+  // Ëèìèò âðåìåíè:
+    bMem.WriteWord(gGameSettings.TimeLimit);
+  // Ëèìèò î÷êîâ:
+    bMem.WriteWord(gGameSettings.GoalLimit);
+  // Ëèìèò æèçíåé:
+    bMem.WriteByte(gGameSettings.MaxLives);
+  // Èãðîâûå îïöèè:
+    bMem.WriteDWORD(gGameSettings.Options);
+  // Äëÿ êîîïà:
+    bMem.WriteWord(gCoopMonstersKilled);
+    bMem.WriteWord(gCoopSecretsFound);
+    bMem.WriteWord(gCoopTotalMonstersKilled);
+    bMem.WriteWord(gCoopTotalSecretsFound);
+    bMem.WriteWord(gCoopTotalMonsters);
+    bMem.WriteWord(gCoopTotalSecrets);
+  // Ñîõðàíÿåì ñîñòîÿíèå èãðû:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ñîõðàíÿåì ñîñòîÿíèå îáëàñòåé ïðîñìîòðà: /////
+    bMem := TBinMemoryWriter.Create(8);
+    sig := PLAYER_VIEW_SIGNATURE;
+    bMem.WriteDWORD(sig); // 'PLVW'
+    PID1 := 0;
+    PID2 := 0;
+    if gPlayer1 <> nil then
+      PID1 := gPlayer1.UID;
+    if gPlayer2 <> nil then
+      PID2 := gPlayer2.UID;
+    bMem.WriteWord(PID1);
+    bMem.WriteWord(PID2);
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå êàðòû: /////
+    g_Map_SaveState(bMem);
+  // Ñîõðàíÿåì ñîñòîÿíèå êàðòû:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå ïðåäìåòîâ: /////
+    g_Items_SaveState(bMem);
+  // Ñîõðàíÿåì ñîñòîÿíèå ïðåäìåòîâ:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå òðèããåðîâ: /////
+    g_Triggers_SaveState(bMem);
+  // Ñîõðàíÿåì ñîñòîÿíèå òðèããåðîâ:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå îðóæèÿ: /////
+    g_Weapon_SaveState(bMem);
+  // Ñîõðàíÿåì ñîñòîÿíèå îðóæèÿ:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå ìîíñòðîâ: /////
+    g_Monsters_SaveState(bMem);
+  // Ñîõðàíÿåì ñîñòîÿíèå ìîíñòðîâ:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ïîëó÷àåì ñîñòîÿíèå òðóïîâ: /////
+    g_Player_Corpses_SaveState(bMem);
+  // Ñîõðàíÿåì ñîñòîÿíèå òðóïîâ:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Ñîõðàíÿåì èãðîêîâ (â òîì ÷èñëå áîòîâ): /////
+    if nPlayers > 0 then
+    begin
+      k := 0;
+      for i := 0 to High(gPlayers) do
+        if gPlayers[i] <> nil then
+        begin
+        // Ïîëó÷àåì ñîñòîÿíèå èãðîêà:
+          gPlayers[i].SaveState(bMem);
+        // Ñîõðàíÿåì ñîñòîÿíèå èãðîêà:
+          bFile.WriteMemory(bMem);
+          bMem.Free();
+          bMem := nil;
+          Inc(k);
+        end;
+
+    // Âñå ëè èãðîêè íà ìåñòå:
+      if k <> nPlayers then
+      begin
+        raise EInOutError.Create('g_SaveGame: Wrong Players Count');
+      end;
+    end;
+  ///// /////
+
+  ///// Ìàðêåð îêîí÷àíèÿ: /////
+    bMem := TBinMemoryWriter.Create(4);
+  // Ñòðîêà - îáîçíà÷åíèå êîíöà:
+    str := END_MARKER_STRING; // 'END'
+    bMem.WriteString(str, 3);
+  // Ñîõðàíÿåì ìàðêåð îêîí÷àíèÿ:
+    bFile.WriteMemory(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  // Çàêðûâàåì ôàéë ñîõðàíåíèÿ:
+    bFile.Close();
+    Result := True;
+
+  except
+    on E1: EInOutError do
+      begin
+        g_Console_Add(_lc[I_GAME_ERROR_SAVE]);
+        e_WriteLog('SaveState I/O Error: '+E1.Message, MSG_WARNING);
+      end;
+    on E2: EBinSizeError do
+      begin
+        g_Console_Add(_lc[I_GAME_ERROR_SAVE]);
+        e_WriteLog('SaveState Size Error: '+E2.Message, MSG_WARNING);
+      end;
+  end;
+
+  bMem.Free();
+  bFile.Free();
+end;
+
+function g_LoadGame(n: Integer): Boolean;
+var
+  bFile: TBinFileReader;
+  bMem: TBinMemoryReader;
+  sig: DWORD;
+  str, WAD_Path, Map_Name: String;
+  nPlayers, Game_Type, Game_Mode, Game_MaxLives: Byte;
+  Game_TimeLimit, Game_GoalLimit: Word;
+  Game_Time, Game_Options: Cardinal;
+  Game_CoopMonstersKilled,
+  Game_CoopSecretsFound,
+  Game_CoopTotalMonstersKilled,
+  Game_CoopTotalSecretsFound,
+  Game_CoopTotalMonsters,
+  Game_CoopTotalSecrets,
+  PID1, PID2: Word;
+  i: Integer;
+begin
+  Result := False;
+  bMem := nil;
+  bFile := nil;
+
+  try
+  // Îòêðûâàåì ôàéë ñ ñîõðàíåíèåì:
+    bFile := TBinFileReader.Create();
+    if not bFile.OpenFile(DataDir + 'SAVGAME' + IntToStr(n) + '.DAT',
+                          SAVE_SIGNATURE, SAVE_VERSION) then
+    begin
+      bFile.Free();
+      Exit;
+    end;
+
+    e_WriteLog('Loading saved game...', MSG_NOTIFY);
+    g_Game_Free();
+
+    g_Game_ClearLoading();
+    g_Game_SetLoadingText(_lc[I_LOAD_SAVE_FILE], 0, False);
+    gLoadGameMode := True;
+
+  ///// Çàãðóæàåì ñîñòîÿíèå èãðû: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Èìÿ èãðû:
+    bMem.ReadString(str);
+  // Ïóòü ê êàðòå:
+    bMem.ReadString(WAD_Path);
+  // Èìÿ êàðòû:
+    bMem.ReadString(Map_Name);
+  // Êîëè÷åñòâî èãðîêîâ:
+    bMem.ReadByte(nPlayers);
+  // Èãðîâîå âðåìÿ:
+    bMem.ReadDWORD(Game_Time);
+  // Òèï èãðû:
+    bMem.ReadByte(Game_Type);
+  // Ðåæèì èãðû:
+    bMem.ReadByte(Game_Mode);
+  // Ëèìèò âðåìåíè:
+    bMem.ReadWord(Game_TimeLimit);
+  // Ëèìèò î÷êîâ:
+    bMem.ReadWord(Game_GoalLimit);
+  // Ëèìèò æèçíåé:
+    bMem.ReadByte(Game_MaxLives);
+  // Èãðîâûå îïöèè:
+    bMem.ReadDWORD(Game_Options);
+  // Äëÿ êîîïà:
+    bMem.ReadWord(Game_CoopMonstersKilled);
+    bMem.ReadWord(Game_CoopSecretsFound);
+    bMem.ReadWord(Game_CoopTotalMonstersKilled);
+    bMem.ReadWord(Game_CoopTotalSecretsFound);
+    bMem.ReadWord(Game_CoopTotalMonsters);
+    bMem.ReadWord(Game_CoopTotalSecrets);
+  // Cîñòîÿíèå èãðû çàãðóæåíî:
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì ñîñòîÿíèå îáëàñòåé ïðîñìîòðà: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+    bMem.ReadDWORD(sig);
+    if sig <> PLAYER_VIEW_SIGNATURE then // 'PLVW'
+    begin
+      raise EInOutError.Create('g_LoadGame: Wrong Player View Signature');
+    end;
+    bMem.ReadWord(PID1);
+    bMem.ReadWord(PID2);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  // Çàãðóæàåì êàðòó:
+    ZeroMemory(@gGameSettings, SizeOf(TGameSettings));
+    gAimLine := False;
+    gShowMap := False;
+    if (Game_Type = GT_NONE) or (Game_Type = GT_SINGLE) then
+    begin
+    // Íàñòðîéêè èãðû:
+      gGameSettings.GameType := GT_SINGLE;
+      gGameSettings.MaxLives := 0;
+      gGameSettings.Options := gGameSettings.Options + GAME_OPTION_ALLOWEXIT;
+      gGameSettings.Options := gGameSettings.Options + GAME_OPTION_MONSTERS;
+      gGameSettings.Options := gGameSettings.Options + GAME_OPTION_BOTVSMONSTER;
+      gSwitchGameMode := GM_SINGLE;
+    end
+    else
+    begin
+    // Íàñòðîéêè èãðû:
+      gGameSettings.GameType := GT_CUSTOM;
+      gGameSettings.GameMode := Game_Mode;
+      gSwitchGameMode := Game_Mode;
+      gGameSettings.TimeLimit := Game_TimeLimit;
+      gGameSettings.GoalLimit := Game_GoalLimit;
+      gGameSettings.MaxLives := IfThen(Game_Mode = GM_CTF, 0, Game_MaxLives);
+      gGameSettings.Options := Game_Options;
+    end;
+    g_Game_ExecuteEvent('ongamestart');
+
+  // Óñòàíîâêà ðàçìåðîâ îêîí èãðîêîâ:
+    g_Game_SetupScreenSize();
+
+  // Çàãðóçêà è çàïóñê êàðòû:
+    if not g_Game_StartMap(WAD_Path + ':\' + Map_Name, True) then
+    begin
+      g_FatalError(Format(_lc[I_GAME_ERROR_MAP_LOAD], [WAD_Path + ':\' + Map_Name]));
+      Exit;
+    end;
+
+  // Íàñòðîéêè èãðîêîâ è áîòîâ:
+    g_Player_Init();
+
+  // Óñòàíàâëèâàåì âðåìÿ:
+    gTime := Game_Time;
+  // Âîçâðàùàåì ñòàòû:
+    gCoopMonstersKilled := Game_CoopMonstersKilled;
+    gCoopSecretsFound := Game_CoopSecretsFound;
+    gCoopTotalMonstersKilled := Game_CoopTotalMonstersKilled;
+    gCoopTotalSecretsFound := Game_CoopTotalSecretsFound;
+    gCoopTotalMonsters := Game_CoopTotalMonsters;
+    gCoopTotalSecrets := Game_CoopTotalSecrets;
+
+  ///// Çàãðóæàåì ñîñòîÿíèå êàðòû: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñîñòîÿíèå êàðòû:
+    g_Map_LoadState(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì ñîñòîÿíèå ïðåäìåòîâ: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñîñòîÿíèå ïðåäìåòîâ:
+    g_Items_LoadState(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì ñîñòîÿíèå òðèããåðîâ: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñîñòîÿíèå òðèããåðîâ:
+    g_Triggers_LoadState(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì ñîñòîÿíèå îðóæèÿ: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñîñòîÿíèå îðóæèÿ:
+    g_Weapon_LoadState(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì ñîñòîÿíèå ìîíñòðîâ: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñîñòîÿíèå ìîíñòðîâ:
+    g_Monsters_LoadState(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì ñîñòîÿíèå òðóïîâ: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñîñòîÿíèå òðóïîâ:
+    g_Player_Corpses_LoadState(bMem);
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  ///// Çàãðóæàåì èãðîêîâ (â òîì ÷èñëå áîòîâ): /////
+    if nPlayers > 0 then
+    begin
+    // Çàãðóæàåì:
+      for i := 0 to nPlayers-1 do
+      begin
+      // Çàãðóæàåì ñîñòîÿíèå èãðîêà:
+        bMem := TBinMemoryReader.Create();
+        bFile.ReadMemory(bMem);
+      // Ñîñòîÿíèå èãðîêà/áîòà:
+        g_Player_CreateFromState(bMem);
+        bMem.Free();
+        bMem := nil;
+      end;
+    end;
+  // Ïðèâÿçûâàåì îñíîâíûõ èãðîêîâ ê îáëàñòÿì ïðîñìîòðà:
+    gPlayer1 := g_Player_Get(PID1);
+    gPlayer2 := g_Player_Get(PID2);
+    if gPlayer1 <> nil then
+    begin
+      gPlayer1.Name := gPlayer1Settings.Name;
+      gPlayer1.FPreferredTeam := gPlayer1Settings.Team;
+      gPlayer1.FActualModelName := gPlayer1Settings.Model;
+      gPlayer1.SetModel(gPlayer1.FActualModelName);
+      gPlayer1.SetColor(gPlayer1Settings.Color);
+    end;
+    if gPlayer2 <> nil then
+    begin
+      gPlayer2.Name := gPlayer2Settings.Name;
+      gPlayer2.FPreferredTeam := gPlayer2Settings.Team;
+      gPlayer2.FActualModelName := gPlayer2Settings.Model;
+      gPlayer2.SetModel(gPlayer2.FActualModelName);
+      gPlayer2.SetColor(gPlayer2Settings.Color);
+    end;
+  ///// /////
+
+  ///// Ìàðêåð îêîí÷àíèÿ: /////
+    bMem := TBinMemoryReader.Create();
+    bFile.ReadMemory(bMem);
+  // Ñòðîêà - îáîçíà÷åíèå êîíöà:
+    bMem.ReadString(str);
+    if str <> END_MARKER_STRING then // 'END'
+    begin
+      raise EInOutError.Create('g_LoadGame: No END Marker');
+    end;
+  // Ìàðêåð îêîí÷àíèÿ çàãðóæåí:
+    bMem.Free();
+    bMem := nil;
+  ///// /////
+
+  // Èùåì òðèããåðû ñ óñëîâèåì ñìåðòè ìîíñòðîâ:
+    if (gMonsters <> nil) and (gTriggers <> nil) then
+      g_Map_ReAdd_DieTriggers();
+
+  // Çàêðûâàåì ôàéë çàãðóçêè:
+    bFile.Close();
+    gLoadGameMode := False;
+    Result := True;
+
+  except
+    on E1: EInOutError do
+      begin
+        g_Console_Add(_lc[I_GAME_ERROR_LOAD]);
+        e_WriteLog('LoadState I/O Error: '+E1.Message, MSG_WARNING);
+      end;
+    on E2: EBinSizeError do
+      begin
+        g_Console_Add(_lc[I_GAME_ERROR_LOAD]);
+        e_WriteLog('LoadState Size Error: '+E2.Message, MSG_WARNING);
+      end;
+  end;
+
+  bMem.Free();
+  bFile.Free();
+end;
+
+end.
diff --git a/src/game/g_sound.pas b/src/game/g_sound.pas
new file mode 100644 (file)
index 0000000..074d037
--- /dev/null
@@ -0,0 +1,632 @@
+unit g_sound;
+
+interface
+
+uses
+  e_sound;
+
+const
+  SOUND_MINDIST = 400;
+  SOUND_MAXDIST = 1000;
+
+type
+  TPlayableSound = class(TBasicSound)
+  private
+    FName: String;
+
+  public
+    constructor Create();
+    destructor Destroy(); override;
+    function Play(Force: Boolean = False): Boolean;
+    function PlayAt(X, Y: Integer): Boolean;
+    function PlayPanVolume(Pan, Volume: Single; Force: Boolean = False): Boolean;
+    function PlayVolumeAt(X, Y: Integer; Volume: Single): Boolean;
+    function SetByName(SN: String): Boolean;
+    function SetCoords(X, Y: Integer; Volume: Single): Boolean;
+
+    property Loop: Boolean read FLoop write FLoop;
+    property Name: String read FName;
+  end;
+
+  TMusic = class(TBasicSound)
+  private
+    FName: String;
+    FSpecPause: Boolean; // Ñïåö-ïàóçà. "Ñèëüíåå" îáû÷íîé
+    FNoMusic: Boolean;
+
+    procedure SetSpecPause(Enable: Boolean);
+
+  public
+    constructor Create();
+    destructor Destroy(); override;
+    function Play(Force: Boolean = False): Boolean;
+    function SetByName(SN: String): Boolean;
+    function IsPaused(): Boolean;
+    procedure Pause(Enable: Boolean);
+
+    property Name: String read FName;
+    property SpecPause: Boolean read FSpecPause write SetSpecPause;
+    property NoMusic: Boolean read FNoMusic;
+  end;
+
+function g_Sound_PlayEx(SoundName: ShortString): Boolean;
+function g_Sound_PlayExPanVolume(SoundName: ShortString; Pan: Single; Volume: Single): Boolean;
+function g_Sound_PlayAt(ID: DWORD; X, Y: Integer): Boolean;
+function g_Sound_PlayExAt(SoundName: ShortString; X, Y: Integer): Boolean;
+
+function g_Sound_CreateWAD(var ID: DWORD; Resource: string; isMusic: Boolean = False): Boolean;
+function g_Sound_CreateWADEx(SoundName: ShortString; Resource: string; isMusic: Boolean = False): Boolean;
+function g_Sound_CreateFile(var ID: DWORD; FileName: string; isMusic: Boolean = False): Boolean;
+function g_Sound_CreateFileEx(SoundName: ShortString; FileName: string; isMusic: Boolean = False): Boolean;
+
+procedure g_Sound_Delete(SoundName: ShortString);
+function g_Sound_Exists(SoundName: string): Boolean;
+function g_Sound_Get(var ID: DWORD; SoundName: ShortString): Boolean;
+
+procedure g_Sound_SetupAllVolumes(SoundVol, MusicVol: Byte);
+
+implementation
+
+uses
+  e_log, SysUtils, g_console, g_options, WADEDITOR,
+  g_game, g_basic, g_items, g_map, fmod, fmodtypes, Math,
+  g_language;
+
+type
+  TGameSound = record
+    Name:     ShortString;
+    ID:       DWORD;
+    IsMusic:  Boolean;
+  end;
+
+var
+  SoundArray: Array of TGameSound;
+  //SoundsMuted: Boolean = False;
+
+
+function FindSound(): DWORD;
+var
+  i: integer;
+begin
+  if SoundArray <> nil then
+    for i := 0 to High(SoundArray) do
+      if SoundArray[i].Name = '' then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if SoundArray = nil then
+    begin
+      SetLength(SoundArray, 8);
+      Result := 0;
+    end
+  else
+    begin
+      Result := High(SoundArray) + 1;
+      SetLength(SoundArray, Length(SoundArray) + 8);
+    end;
+end;
+
+function g_Sound_PlayEx(SoundName: ShortString): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+  if SoundArray = nil then
+    Exit;
+
+  for a := 0 to High(SoundArray) do
+    if SoundArray[a].Name = SoundName then
+    begin
+      Result := e_PlaySoundVolume(SoundArray[a].ID, gSoundLevel/255.0);
+      Exit;
+    end;
+
+  e_WriteLog(Format(_lc[I_GAME_ERROR_SOUND], [SoundName]), MSG_WARNING);
+end;
+
+function g_Sound_PlayExPanVolume(SoundName: ShortString; Pan: Single; Volume: Single): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+  if SoundArray = nil then
+    Exit;
+
+  for a := 0 to High(SoundArray) do
+    if SoundArray[a].Name = SoundName then
+    begin
+      Result := e_PlaySoundPanVolume(SoundArray[a].ID, Pan, Volume * (gSoundLevel/255.0));
+      Exit;
+    end;
+
+  e_WriteLog(Format(_lc[I_GAME_ERROR_SOUND], [SoundName]), MSG_WARNING);
+end;
+
+function PlaySoundAt(X, Y: Integer; var Pan: Single; var Volume: Single; InVolume: Single = 1.0): Boolean;
+var
+  l1, l2, lx, rx: Integer;
+  d1, d2, sMaxDist: Single;
+  c: Boolean;
+begin
+  l1 := gMaxDist;
+  l2 := gMaxDist;
+  sMaxDist := SOUND_MAXDIST * InVolume;
+
+  d1 := 0.0;
+
+  c := SOUND_MINDIST >= sMaxDist;
+
+  if X > gMapInfo.Width then
+    X := gMapInfo.Width
+  else
+    if X < 0 then
+      X := 0;
+
+  if Y > gMapInfo.Height then
+    Y := gMapInfo.Height
+  else
+    if Y < 0 then
+      Y := 0;
+
+  if gHearPoint1.Active then
+  begin
+    l1 := Round(Hypot(X - gHearPoint1.Coords.X, Y - gHearPoint1.Coords.Y));
+
+    lx := gHearPoint1.Coords.X - SOUND_MINDIST;
+    rx := gHearPoint1.Coords.X + SOUND_MINDIST;
+    if c then
+      d1 := 0.0
+    else if (X >= lx) and (X <= rx) then
+      d1 := 0.0
+    else if X < lx then
+      d1 := (X-lx)/sMaxDist
+    else
+      d1 := (X-rx)/sMaxDist;
+  end;
+
+  d2 := d1;
+
+  if gHearPoint2.Active then
+  begin
+    l2 := Round(Hypot(X - gHearPoint2.Coords.X, Y - gHearPoint2.Coords.Y));
+
+    lx := gHearPoint2.Coords.X - SOUND_MINDIST;
+    rx := gHearPoint2.Coords.X + SOUND_MINDIST;
+    if c then
+      d2 := 0.0
+    else if (X >= lx) and (X <= rx) then
+      d2 := 0.0
+    else if X < lx then
+      d2 := (X-lx)/sMaxDist
+    else
+      d2 := (X-rx)/sMaxDist;
+  end;
+
+  if l2 < l1 then
+  begin
+    l1 := l2;
+    d1 := d2;
+  end;
+
+  if l1 >= sMaxDist then
+    begin
+      Pan := 0.0;
+      Volume := 0.0;
+      Result := False;
+    end
+  else
+    begin
+      Pan := d1;
+      Volume := 1.0 - l1/sMaxDist;
+      Result := True;
+    end;
+end;
+
+function g_Sound_PlayAt(ID: DWORD; X, Y: Integer): Boolean;
+var
+  Pan, Vol: Single;
+begin
+  if PlaySoundAt(X, Y, Pan, Vol) then
+    Result := e_PlaySoundPanVolume(ID, Pan, Vol * (gSoundLevel/255.0))
+  else
+    Result := False;
+end;
+
+function g_Sound_PlayExAt(SoundName: ShortString; X, Y: Integer): Boolean;
+var
+  a: DWORD;
+  Pan, Vol: Single;
+begin
+  Result := False;
+
+  if SoundArray = nil then
+    Exit;
+
+  for a := 0 to High(SoundArray) do
+    if SoundArray[a].Name = SoundName then
+    begin
+      if PlaySoundAt(X, Y, Pan, Vol) then
+        Result := e_PlaySoundPanVolume(SoundArray[a].ID,
+                    Pan, Vol * (gSoundLevel/255.0));
+      Exit;
+    end;
+
+  e_WriteLog(Format(_lc[I_GAME_ERROR_SOUND], [SoundName]), MSG_WARNING);
+end;
+
+function g_Sound_CreateFile(var ID: DWORD; FileName: string; isMusic: Boolean = False): Boolean;
+begin
+  Result := e_LoadSound(FileName, ID, isMusic);
+end;
+
+function g_Sound_CreateFileEx(SoundName: ShortString; FileName: string; isMusic: Boolean = False): Boolean;
+var
+  find_id: DWORD;
+begin
+  Result := False;
+
+  find_id := FindSound();
+
+  if not e_LoadSound(FileName, SoundArray[find_id].ID, isMusic) then
+    Exit;
+
+  SoundArray[find_id].Name := SoundName;
+  SoundArray[find_id].IsMusic := isMusic;
+
+  Result := True;
+end;
+
+function g_Sound_CreateWAD(var ID: DWORD; Resource: string; isMusic: Boolean = False): Boolean;
+var
+  WAD: TWADEditor_1;
+  FileName,
+  SectionName,
+  ResourceName: string;
+  SoundData: Pointer;
+  ResLength: Integer;
+  ok: Boolean;
+begin
+  Result := False;
+  ok := False;
+
+  // e_WriteLog('Loading sound: ' + Resource, MSG_NOTIFY);
+  g_ProcessResourceStr(Resource, FileName, SectionName, ResourceName);
+
+  WAD := TWADEditor_1.Create();
+  WAD.ReadFile(FileName);
+
+  if WAD.GetResource(SectionName, ResourceName, SoundData, ResLength) then
+    begin
+      if e_LoadSoundMem(SoundData, ResLength, ID, isMusic) then
+        ok := True
+      else
+        FreeMem(SoundData);
+    end
+  else
+    e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+
+  WAD.Free();
+
+  if not ok then
+  begin
+    if isMusic then
+      e_WriteLog(Format('Error loading music %s', [Resource]), MSG_WARNING)
+    else
+      e_WriteLog(Format('Error loading sound %s', [Resource]), MSG_WARNING);
+    Exit;
+  end;
+
+  Result := True;
+end;
+
+function g_Sound_CreateWADEx(SoundName: ShortString; Resource: string; isMusic: Boolean = False): Boolean;
+var
+  WAD: TWADEditor_1;
+  FileName, SectionName, ResourceName: string;
+  SoundData: Pointer;
+  ResLength: Integer;
+  find_id: DWORD;
+  ok: Boolean;
+begin
+  Result := False;
+  ok := False;
+
+  // e_WriteLog('Loading sound: ' + Resource, MSG_NOTIFY);
+  g_ProcessResourceStr(Resource, FileName, SectionName, ResourceName);
+
+  find_id := FindSound();
+
+  WAD := TWADEditor_1.Create();
+  WAD.ReadFile(FileName);
+
+  if WAD.GetResource(SectionName, ResourceName, SoundData, ResLength) then
+    begin
+      if e_LoadSoundMem(SoundData, ResLength, SoundArray[find_id].ID, isMusic) then
+        begin
+          SoundArray[find_id].Name := SoundName;
+          SoundArray[find_id].IsMusic := isMusic;
+          ok := True;
+        end
+      else
+        FreeMem(SoundData);
+    end
+  else
+    e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+
+  WAD.Free();
+
+  if not ok then
+  begin
+    if isMusic then
+      e_WriteLog(Format('Error loading music %s', [Resource]), MSG_WARNING)
+    else
+      e_WriteLog(Format('Error loading sound %s', [Resource]), MSG_WARNING);
+    Exit;
+  end;
+
+  Result := True;
+end;
+
+procedure g_Sound_Delete(SoundName: ShortString);
+var
+  a: DWORD;
+begin
+  if (SoundArray = nil) or (SoundName = '') then
+    Exit;
+
+  for a := 0 to High(SoundArray) do
+    if SoundArray[a].Name = SoundName then
+    begin
+      e_DeleteSound(SoundArray[a].ID);
+      SoundArray[a].Name := '';
+      SoundArray[a].ID := 0;
+      SoundArray[a].IsMusic := False;
+    end;
+end;
+
+function g_Sound_Exists(SoundName: string): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+
+  if SoundName = '' then
+    Exit;
+
+  if SoundArray <> nil then
+    for a := 0 to High(SoundArray) do
+      if SoundArray[a].Name = SoundName then
+      begin
+        Result := True;
+        Break;
+      end;
+end;
+
+function g_Sound_Get(var ID: DWORD; SoundName: ShortString): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+
+  if SoundName = '' then
+    Exit;
+
+  if SoundArray <> nil then
+    for a := 0 to High(SoundArray) do
+      if SoundArray[a].Name = SoundName then
+      begin
+        ID := SoundArray[a].ID;
+        Result := True;
+        Break;
+      end;
+end;
+
+procedure g_Sound_SetupAllVolumes(SoundVol, MusicVol: Byte);
+var
+  Svol, Mvol: Single;
+  sm: Boolean;
+begin
+  Mvol := 0; // shut up, compiler
+  if (gSoundLevel = SoundVol) and (gMusicLevel = MusicVol) then
+    Exit;
+
+  if gSoundLevel > 0 then
+    begin
+      Svol := SoundVol / gSoundLevel;
+      sm := False;
+    end
+  else
+    begin
+      Svol := SoundVol / 255.0;
+      sm := True;
+    end;
+
+  if gMusic <> nil then
+    if gMusicLevel > 0 then
+      Mvol := gMusic.GetVolume() * MusicVol / gMusicLevel
+    else
+      Mvol := MusicVol / 255.0;
+
+  e_ModifyChannelsVolumes(Svol, sm);
+
+  if gMusic <> nil then
+    gMusic.SetVolume(Mvol);
+
+  gSoundLevel := SoundVol;
+  gMusicLevel := MusicVol;
+end;
+
+{ TPlayableSound: }
+
+constructor TPlayableSound.Create();
+begin
+  inherited;
+  FName := '';
+end;
+
+destructor TPlayableSound.Destroy();
+begin
+  inherited;
+end;
+
+function TPlayableSound.Play(Force: Boolean = False): Boolean;
+begin
+  if Force or not IsPlaying() then
+    begin
+      Stop();
+      Result := RawPlay(0.0, gSoundLevel/255.0, FPosition);
+    end
+  else
+    Result := False;
+end;
+
+function TPlayableSound.PlayAt(X, Y: Integer): Boolean;
+var
+  Pan, Vol: Single;
+begin
+  if PlaySoundAt(X, Y, Pan, Vol) then
+    begin
+      Stop();
+      Result := RawPlay(Pan, Vol * (gSoundLevel/255.0), FPosition);
+    end
+  else
+    Result := False;
+end;
+
+function TPlayableSound.PlayPanVolume(Pan, Volume: Single; Force: Boolean = False): Boolean;
+begin
+  if Force or not IsPlaying() then
+    begin
+      Stop();
+      Result := RawPlay(Pan, Volume * (gSoundLevel/255.0), FPosition);
+    end
+  else
+    Result := False;
+end;
+
+function TPlayableSound.PlayVolumeAt(X, Y: Integer; Volume: Single): Boolean;
+var
+  Pan, Vol: Single;
+begin
+  if PlaySoundAt(X, Y, Pan, Vol, Volume) then
+    begin
+      Stop();
+      Result := RawPlay(Pan, Volume * Vol * (gSoundLevel/255.0), FPosition);
+    end
+  else
+    Result := False;
+end;
+
+function TPlayableSound.SetCoords(X, Y: Integer; Volume: Single): Boolean;
+var
+  Pan, Vol: Single;
+begin
+  if PlaySoundAt(X, Y, Pan, Vol, Volume) then
+  begin
+    SetVolume(Volume * Vol * (gSoundLevel/255.0));
+    SetPan(Pan);
+    Result := True;
+  end
+  else
+  begin
+    SetVolume(0.0);
+    SetPan(0.0);
+    Result := False;
+  end;
+end;
+
+function TPlayableSound.SetByName(SN: String): Boolean;
+var
+  id: DWORD;
+begin
+  if g_Sound_Get(id, SN) then
+    begin
+      SetID(id);
+      FName := SN;
+      Result := True;
+    end
+  else
+    Result := False;
+end;
+
+{ TMusic: }
+
+constructor TMusic.Create();
+begin
+  inherited;
+  FName := '';
+  FSpecPause := False;
+  FNoMusic := True;
+end;
+
+destructor TMusic.Destroy();
+begin
+  inherited;
+end;
+
+function TMusic.Play(Force: Boolean = False): Boolean;
+begin
+  if FNoMusic then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+  if Force or not IsPlaying() then
+    begin
+      Stop();
+      Result := RawPlay(0.0, gMusicLevel/255.0, FPosition);
+      if Result then
+        SetPriority(0);
+      if Result and FSpecPause then
+        Pause(True);
+    end
+  else
+    Result := False;
+end;
+
+function TMusic.SetByName(SN: String): Boolean;
+var
+  id: DWORD;
+begin
+  if SN = '' then
+  begin
+    FNoMusic := True;
+    Result := True;
+    Exit;
+  end;
+
+  if g_Sound_Get(id, SN) then
+    begin
+      SetID(id);
+      FName := SN;
+      FNoMusic := False;
+      FSpecPause := False;
+      Result := True;
+    end
+  else
+    Result := False;
+end;
+
+function TMusic.IsPaused(): Boolean;
+begin
+  Result := inherited IsPaused();
+  Result := Result or FSpecPause;
+end;
+
+procedure TMusic.Pause(Enable: Boolean);
+begin
+// Îòêëþ÷àåì ïàóçó, òîëüêî åñëè íå áûëî ñïåö-ïàóçû:
+  if Enable or (not FSpecPause) then
+    inherited Pause(Enable);
+end;
+
+procedure TMusic.SetSpecPause(Enable: Boolean);
+begin
+  FSpecPause := Enable;
+  Pause(Enable);
+end;
+
+end.
diff --git a/src/game/g_textures.pas b/src/game/g_textures.pas
new file mode 100644 (file)
index 0000000..dcc91b7
--- /dev/null
@@ -0,0 +1,787 @@
+unit g_textures;
+
+interface
+
+uses
+  e_graphics, BinEditor;
+
+Type
+  TLevelTexture = record
+    TextureName: String;
+    Width,
+    Height:      Word;
+    case Anim: Boolean of
+      False: (TextureID:   DWORD;);
+      True:  (FramesID:    DWORD;
+              FramesCount: Byte;
+              Speed:       Byte);
+  end;
+
+  TLevelTextureArray = Array of TLevelTexture;
+
+  TAnimation = class(TObject)
+  private
+    ID:            DWORD;
+    FAlpha:        Byte;
+    FBlending:     Boolean;
+    FCounter:      Byte;    // Ñ÷åò÷èê îæèäàíèÿ ìåæäó êàäðàìè
+    FSpeed:        Byte;    // Âðåìÿ îæèäàíèÿ ìåæäó êàäðàìè
+    FCurrentFrame: Integer; // Òåêóùèé êàäð (íà÷èíàÿ ñ 0)
+    FLoop:         Boolean; // Ïåðåõîäèòü íà ïåðâûé êàäð ïîñëå ïîñëåäíåãî?
+    FEnabled:      Boolean; // Ðàáîòà ðàçðåøåíà?
+    FPlayed:       Boolean; // Ïðîèãðàíà âñÿ õîòÿ áû ðàç?
+    FHeight:       Word;
+    FWidth:        Word;
+    FMinLength:    Byte;    // Îæèäàíèå ïîñëå ïðîèãðûâàíèÿ
+    FRevert:       Boolean; // Ñìåíà êàäðîâ îáðàòíàÿ?
+    
+  public
+    constructor Create(FramesID: DWORD; Loop: Boolean; Speed: Byte);
+    destructor  Destroy(); override;
+    procedure   Draw(X, Y: Integer; Mirror: TMirrorType);
+    procedure   DrawEx(X, Y: Integer; Mirror: TMirrorType; RPoint: TPoint;
+                       Angle: SmallInt);
+    procedure   Reset();
+    procedure   Update();
+    procedure   Enable();
+    procedure   Disable();
+    procedure   Revert(r: Boolean);
+    procedure   SaveState(Var Mem: TBinMemoryWriter);
+    procedure   LoadState(Var Mem: TBinMemoryReader);
+    function    TotalFrames(): Integer;
+
+    property    Played: Boolean read FPlayed;
+    property    Enabled: Boolean read FEnabled;
+    property    IsReverse: Boolean read FRevert;
+    property    Loop: Boolean read FLoop write FLoop;
+    property    Speed: Byte read FSpeed write FSpeed;
+    property    MinLength: Byte read FMinLength write FMinLength;
+    property    CurrentFrame: Integer read FCurrentFrame write FCurrentFrame;
+    property    CurrentCounter: Byte read FCounter write FCounter;
+    property    Counter: Byte read FCounter;
+    property    Blending: Boolean read FBlending write FBlending;
+    property    Alpha: Byte read FAlpha write FAlpha;
+    property    FramesID: DWORD read ID;
+    property    Width: Word read FWidth;
+    property    Height: Word read FHeight;
+  end;
+
+function g_Texture_CreateWAD(var ID: DWORD; Resource: String): Boolean;
+function g_Texture_CreateFile(var ID: DWORD; FileName: String): Boolean;
+function g_Texture_CreateWADEx(TextureName: ShortString; Resource: String): Boolean;
+function g_Texture_CreateFileEx(TextureName: ShortString; FileName: String): Boolean;
+function g_Texture_Get(TextureName: ShortString; var ID: DWORD): Boolean;
+procedure g_Texture_Delete(TextureName: ShortString);
+procedure g_Texture_DeleteAll();
+
+function g_Frames_CreateWAD(ID: PDWORD; Name: ShortString; Resource: String;
+                            FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+function g_Frames_CreateFile(ID: PDWORD; Name: ShortString; FileName: String;
+                             FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+function g_Frames_CreateMemory(ID: PDWORD; Name: ShortString; pData: Pointer;
+                               FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+//function g_Frames_CreateRevert(ID: PDWORD; Name: ShortString; Frames: string): Boolean;
+function g_Frames_Get(var ID: DWORD; FramesName: ShortString): Boolean;
+function g_Frames_GetTexture(var ID: DWORD; FramesName: ShortString; Frame: Word): Boolean;
+function g_Frames_Exists(FramesName: String): Boolean;
+procedure g_Frames_DeleteByName(FramesName: ShortString);
+procedure g_Frames_DeleteByID(ID: DWORD);
+procedure g_Frames_DeleteAll();
+
+procedure DumpTextureNames();
+
+implementation
+
+uses
+  g_game, e_log, g_basic, SysUtils, g_console, WADEDITOR,
+  g_language;
+
+type
+  _TTexture = record
+    Name: ShortString;
+    ID: DWORD;
+    Width, Height: Word;
+  end;
+
+  TFrames = record
+    TexturesID: Array of DWORD;
+    Name: ShortString;
+    FrameWidth,
+    FrameHeight: Word;
+  end;
+
+var
+  TexturesArray: Array of _TTexture = nil;
+  FramesArray: Array of TFrames = nil;
+
+const
+  ANIM_SIGNATURE = $4D494E41; // 'ANIM'
+
+function FindTexture(): DWORD;
+var
+  i: integer;
+begin
+  if TexturesArray <> nil then
+  for i := 0 to High(TexturesArray) do
+    if TexturesArray[i].Name = '' then
+    begin
+      Result := i;
+      Exit;
+    end;
+
+  if TexturesArray = nil then
+  begin
+    SetLength(TexturesArray, 8);
+    Result := 0;
+  end
+  else
+  begin
+    Result := High(TexturesArray) + 1;
+    SetLength(TexturesArray, Length(TexturesArray) + 8);
+  end;
+end;
+
+function g_Texture_CreateWAD(var ID: DWORD; Resource: String): Boolean;
+var
+  WAD: TWADEditor_1;
+  FileName,
+  SectionName,
+  ResourceName: String;
+  TextureData: Pointer;
+  ResourceLength: Integer;
+begin
+  Result := False;
+  g_ProcessResourceStr(Resource, FileName, SectionName, ResourceName);
+
+  WAD := TWADEditor_1.Create;
+  WAD.ReadFile(FileName);
+
+  if WAD.GetResource(SectionName, ResourceName, TextureData, ResourceLength) then
+  begin
+    if e_CreateTextureMem(TextureData, ID) then 
+      Result := True
+    else
+      FreeMem(TextureData);
+  end
+  else
+  begin
+    e_WriteLog(Format('Error loading texture %s', [Resource]), MSG_WARNING);
+    e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+  end;
+  WAD.Free();
+end;
+
+function g_Texture_CreateFile(var ID: DWORD; FileName: String): Boolean;
+begin
+  Result := True;
+  if not e_CreateTexture(FileName, ID) then
+  begin
+    e_WriteLog(Format('Error loading texture %s', [FileName]), MSG_WARNING);
+    Result := False;
+  end;
+end;
+
+function g_Texture_CreateWADEx(TextureName: ShortString; Resource: String): Boolean;
+var
+  WAD: TWADEditor_1;
+  FileName,
+  SectionName,
+  ResourceName: String;
+  TextureData: Pointer;
+  find_id: DWORD;
+  ResourceLength: Integer;
+begin
+  g_ProcessResourceStr(Resource, FileName, SectionName, ResourceName);
+
+  find_id := FindTexture();
+
+  WAD := TWADEditor_1.Create;
+  WAD.ReadFile(FileName);
+
+  if WAD.GetResource(SectionName, ResourceName, TextureData, ResourceLength) then
+  begin
+    Result := e_CreateTextureMem(TextureData, TexturesArray[find_id].ID);
+    if Result then
+    begin
+      e_GetTextureSize(TexturesArray[find_id].ID, @TexturesArray[find_id].Width,
+                       @TexturesArray[find_id].Height);
+      TexturesArray[find_id].Name := LowerCase(TextureName);
+    end
+    else
+      FreeMem(TextureData);
+  end
+  else
+  begin
+    e_WriteLog(Format('Error loading texture %s', [Resource]), MSG_WARNING);
+    e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+    Result := False;
+  end;
+  WAD.Free();
+end;
+
+function g_Texture_CreateFileEx(TextureName: ShortString; FileName: String): Boolean;
+var
+  find_id: DWORD;
+begin
+  find_id := FindTexture;
+
+  Result := e_CreateTexture(FileName, TexturesArray[find_id].ID);
+  if Result then
+  begin
+    TexturesArray[find_id].Name := LowerCase(TextureName);
+    e_GetTextureSize(TexturesArray[find_id].ID, @TexturesArray[find_id].Width,
+                     @TexturesArray[find_id].Height);
+  end
+  else e_WriteLog(Format('Error loading texture %s', [FileName]), MSG_WARNING);
+end;
+
+function g_Texture_Get(TextureName: ShortString; var ID: DWORD): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+  if TexturesArray = nil then Exit;
+
+  if TextureName = '' then Exit;
+
+  TextureName := LowerCase(TextureName);
+
+  for a := 0 to High(TexturesArray) do
+    if TexturesArray[a].Name = TextureName then
+    begin
+      ID := TexturesArray[a].ID;
+      Result := True;
+      Break;
+    end;
+
+  //if not Result then g_ConsoleAdd('Texture '+TextureName+' not found');
+end;
+
+procedure g_Texture_Delete(TextureName: ShortString);
+var
+  a: DWORD;
+begin
+  if TexturesArray = nil then Exit;
+
+  TextureName := LowerCase(TextureName);
+
+  for a := 0 to High(TexturesArray) do
+    if TexturesArray[a].Name = TextureName then
+    begin
+      e_DeleteTexture(TexturesArray[a].ID);
+      TexturesArray[a].Name := '';
+      TexturesArray[a].ID := 0;
+      TexturesArray[a].Width := 0;
+      TexturesArray[a].Height := 0;
+    end;
+end;
+
+procedure g_Texture_DeleteAll();
+var
+  a: DWORD;
+begin
+  if TexturesArray = nil then Exit;
+
+  for a := 0 to High(TexturesArray) do
+    if TexturesArray[a].Name <> '' then
+      e_DeleteTexture(TexturesArray[a].ID);
+
+  TexturesArray := nil;
+end;
+
+function FindFrame(): DWORD;
+var
+  i: integer;
+begin
+  if FramesArray <> nil then
+    for i := 0 to High(FramesArray) do
+      if FramesArray[i].TexturesID = nil then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if FramesArray = nil then
+  begin
+    SetLength(FramesArray, 64);
+    Result := 0;
+  end
+  else
+  begin
+    Result := High(FramesArray) + 1;
+    SetLength(FramesArray, Length(FramesArray) + 64);
+  end;
+end;
+
+function g_Frames_CreateFile(ID: PDWORD; Name: ShortString; FileName: String;
+                             FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+var
+  a: Integer;
+  find_id: DWORD;
+begin
+  Result := False;
+
+  find_id := FindFrame;
+
+  if FCount <= 2 then BackAnimation := False;
+
+  if BackAnimation then SetLength(FramesArray[find_id].TexturesID, FCount+FCount-2)
+    else SetLength(FramesArray[find_id].TexturesID, FCount);
+
+  for a := 0 to FCount-1 do
+    if not e_CreateTextureEx(FileName, FramesArray[find_id].TexturesID[a],
+                             a*FWidth, 0, FWidth, FHeight) then Exit;
+
+  if BackAnimation then
+    for a := 1 to FCount-2 do
+      FramesArray[find_id].TexturesID[FCount+FCount-2-a] := FramesArray[find_id].TexturesID[a];
+
+  FramesArray[find_id].FrameWidth := FWidth;
+  FramesArray[find_id].FrameHeight := FHeight;
+  if Name <> '' then
+    FramesArray[find_id].Name := LowerCase(Name)
+  else
+    FramesArray[find_id].Name := '<noname>';
+
+  if ID <> nil then ID^ := find_id;
+
+  Result := True;
+end;
+
+function CreateFramesMem(pData: Pointer; ID: PDWORD; Name: ShortString;
+                         FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+var
+  find_id: DWORD;
+  a: Integer;
+begin
+  Result := False;
+
+  find_id := FindFrame();
+
+  if FCount <= 2 then BackAnimation := False;
+
+  if BackAnimation then SetLength(FramesArray[find_id].TexturesID, FCount+FCount-2)
+    else SetLength(FramesArray[find_id].TexturesID, FCount);
+
+  for a := 0 to FCount-1 do
+    if not e_CreateTextureMemEx(pData, FramesArray[find_id].TexturesID[a],
+                                a*FWidth, 0, FWidth, FHeight) then
+    begin
+      FreeMem(pData);
+      Exit;
+    end;
+
+  if BackAnimation then
+    for a := 1 to FCount-2 do
+      FramesArray[find_id].TexturesID[FCount+FCount-2-a] := FramesArray[find_id].TexturesID[a];
+
+  FramesArray[find_id].FrameWidth := FWidth;
+  FramesArray[find_id].FrameHeight := FHeight;
+  if Name <> '' then
+    FramesArray[find_id].Name := LowerCase(Name)
+  else
+    FramesArray[find_id].Name := '<noname>';
+
+  if ID <> nil then ID^ := find_id;
+
+  Result := True;
+end;
+
+function g_Frames_CreateWAD(ID: PDWORD; Name: ShortString; Resource: string;
+                            FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+var
+  WAD: TWADEditor_1;
+  FileName,
+  SectionName,
+  ResourceName: string;
+  TextureData: Pointer;
+  ResourceLength: Integer;
+begin
+  Result := False;
+
+  g_ProcessResourceStr(Resource, FileName, SectionName, ResourceName);
+
+  WAD := TWADEditor_1.Create();
+  WAD.ReadFile(FileName);
+
+  if not WAD.GetResource(SectionName, ResourceName, TextureData, ResourceLength) then
+  begin
+    WAD.Free();
+    e_WriteLog(Format('Error loading texture %s', [Resource]), MSG_WARNING);
+    e_WriteLog(Format('WAD Reader error: %s', [WAD.GetLastErrorStr]), MSG_WARNING);
+    Exit;
+  end;
+
+  if not CreateFramesMem(TextureData, ID, Name, FWidth, FHeight, FCount, BackAnimation) then
+  begin
+    WAD.Free();
+    Exit;
+  end;
+
+  WAD.Free();
+
+  Result := True;
+end;
+
+function g_Frames_CreateMemory(ID: PDWORD; Name: ShortString; pData: Pointer;
+                               FWidth, FHeight, FCount: Word; BackAnimation: Boolean = False): Boolean;
+begin
+  Result := CreateFramesMem(pData, ID, Name, FWidth, FHeight, FCount, BackAnimation);
+end;
+
+{function g_Frames_CreateRevert(ID: PDWORD; Name: ShortString; Frames: string): Boolean;
+var
+  find_id, b: DWORD;
+  a, c: Integer;
+begin
+ Result := False;
+
+ if not g_Frames_Get(b, Frames) then Exit;
+
+ find_id := FindFrame();
+
+ FramesArray[find_id].Name := Name;
+ FramesArray[find_id].FrameWidth := FramesArray[b].FrameWidth;
+ FramesArray[find_id].FrameHeight := FramesArray[b].FrameHeight;
+
+ c := High(FramesArray[find_id].TexturesID);
+
+ for a := 0 to c do
+  FramesArray[find_id].TexturesID[a] := FramesArray[b].TexturesID[c-a];
+
+ Result := True;
+end;}
+
+procedure g_Frames_DeleteByName(FramesName: ShortString);
+var
+  a: DWORD;
+  b: Integer;
+begin
+  if FramesArray = nil then Exit;
+
+  FramesName := LowerCase(FramesName);
+
+  for a := 0 to High(FramesArray) do
+    if FramesArray[a].Name = FramesName then
+    begin
+      if FramesArray[a].TexturesID <> nil then
+        for b := 0 to High(FramesArray[a].TexturesID) do
+          e_DeleteTexture(FramesArray[a].TexturesID[b]);
+      FramesArray[a].TexturesID := nil;
+      FramesArray[a].Name := '';
+      FramesArray[a].FrameWidth := 0;
+      FramesArray[a].FrameHeight := 0;
+    end;
+end;
+
+procedure g_Frames_DeleteByID(ID: DWORD);
+var
+  b: Integer;
+begin
+  if FramesArray = nil then Exit;
+
+  if FramesArray[ID].TexturesID <> nil then
+    for b := 0 to High(FramesArray[ID].TexturesID) do
+      e_DeleteTexture(FramesArray[ID].TexturesID[b]);
+  FramesArray[ID].TexturesID := nil;
+  FramesArray[ID].Name := '';
+  FramesArray[ID].FrameWidth := 0;
+  FramesArray[ID].FrameHeight := 0;
+end;
+
+procedure g_Frames_DeleteAll;
+var
+  a: DWORD;
+  b: DWORD;
+begin
+  if FramesArray = nil then Exit;
+
+  for a := 0 to High(FramesArray) do
+    if FramesArray[a].TexturesID <> nil then
+    begin
+      for b := 0 to High(FramesArray[a].TexturesID) do
+        e_DeleteTexture(FramesArray[a].TexturesID[b]);
+      FramesArray[a].TexturesID := nil;
+      FramesArray[a].Name := '';
+      FramesArray[a].FrameWidth := 0;
+      FramesArray[a].FrameHeight := 0;
+    end;
+
+  FramesArray := nil;
+end;
+
+function g_Frames_Get(var ID: DWORD; FramesName: ShortString): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+
+  if FramesArray = nil then
+    Exit;
+
+  FramesName := LowerCase(FramesName);
+
+  for a := 0 to High(FramesArray) do
+    if FramesArray[a].Name = FramesName then
+    begin
+      ID := a;
+      Result := True;
+      Break;
+    end;
+
+  if not Result then
+    g_FatalError(Format(_lc[I_GAME_ERROR_FRAMES], [FramesName]));
+end;
+
+function g_Frames_GetTexture(var ID: DWORD; FramesName: ShortString; Frame: Word): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+
+  if FramesArray = nil then
+    Exit;
+
+  FramesName := LowerCase(FramesName);
+
+  for a := 0 to High(FramesArray) do
+    if FramesArray[a].Name = FramesName then
+      if Frame <= High(FramesArray[a].TexturesID) then
+      begin
+        ID := FramesArray[a].TexturesID[Frame];
+        Result := True;
+        Break;
+      end;
+
+  if not Result then
+    g_FatalError(Format(_lc[I_GAME_ERROR_FRAMES], [FramesName]));
+end;
+
+function g_Frames_Exists(FramesName: string): Boolean;
+var
+  a: DWORD;
+begin
+  Result := False;
+
+  if FramesArray = nil then Exit;
+
+  FramesName := LowerCase(FramesName);
+
+  for a := 0 to High(FramesArray) do
+    if FramesArray[a].Name = FramesName then
+    begin
+      Result := True;
+      Exit;
+    end;
+end;
+
+procedure DumpTextureNames();
+var
+  i: Integer;
+begin
+  e_WriteLog('BEGIN Textures:', MSG_NOTIFY);
+  for i := 0 to High(TexturesArray) do
+    e_WriteLog('   '+IntToStr(i)+'. '+TexturesArray[i].Name, MSG_NOTIFY);
+  e_WriteLog('END Textures.', MSG_NOTIFY);
+
+  e_WriteLog('BEGIN Frames:', MSG_NOTIFY);
+  for i := 0 to High(FramesArray) do
+    e_WriteLog('   '+IntToStr(i)+'. '+FramesArray[i].Name, MSG_NOTIFY);
+  e_WriteLog('END Frames.', MSG_NOTIFY);
+end;
+
+{ TAnimation }
+
+constructor TAnimation.Create(FramesID: DWORD; Loop: Boolean; Speed: Byte);
+begin
+  ID := FramesID;
+
+  FMinLength := 0;
+  FLoop := Loop;
+  FSpeed := Speed;
+  FEnabled := True;
+  FCurrentFrame := 0;
+  FPlayed := False;
+  FAlpha := 0;
+  FWidth := FramesArray[ID].FrameWidth;
+  FHeight := FramesArray[ID].FrameHeight;
+end;
+
+destructor TAnimation.Destroy;
+begin
+  inherited;
+end;
+
+procedure TAnimation.Draw(X, Y: Integer; Mirror: TMirrorType);
+begin
+  if not FEnabled then
+    Exit;
+
+  e_DrawAdv(FramesArray[ID].TexturesID[FCurrentFrame], X, Y, FAlpha,
+            True, FBlending, 0, nil, Mirror);
+  //e_DrawQuad(X, Y, X+FramesArray[ID].FrameWidth-1, Y+FramesArray[ID].FrameHeight-1, 0, 255, 0);
+end;
+
+procedure TAnimation.Update();
+begin
+  if not FEnabled then
+    Exit;
+
+  FCounter := FCounter + 1;
+
+  if FCounter >= FSpeed then
+  begin // Îæèäàíèå ìåæäó êàäðàìè çàêîí÷èëîñü
+    if FRevert then
+      begin // Îáðàòíûé ïîðÿäîê êàäðîâ
+      // Äîøëè äî êîíöà àíèìàöèè. Âîçìîæíî, æäåì åùå:
+        if FCurrentFrame = 0 then
+          if Length(FramesArray[ID].TexturesID) * FSpeed +
+               FCounter < FMinLength then
+            Exit;
+
+        FCurrentFrame := FCurrentFrame - 1;
+        FPlayed := FCurrentFrame < 0;
+
+      // Ïîâòîðÿòü ëè àíèìàöèþ ïî êðóãó:
+        if FPlayed then
+          if FLoop then
+            FCurrentFrame := High(FramesArray[ID].TexturesID)
+          else
+            FCurrentFrame := FCurrentFrame + 1;
+
+        FCounter := 0;
+      end
+    else
+      begin // Ïðÿìîé ïîðÿäîê êàäðîâ
+      // Äîøëè äî êîíöà àíèìàöèè. Âîçìîæíî, æäåì åùå:
+        if FCurrentFrame = High(FramesArray[ID].TexturesID) then
+          if Length(FramesArray[ID].TexturesID) * FSpeed +
+               FCounter < FMinLength then
+            Exit;
+
+        FCurrentFrame := FCurrentFrame + 1;
+        FPlayed := (FCurrentFrame > High(FramesArray[ID].TexturesID));
+
+      // Ïîâòîðÿòü ëè àíèìàöèþ ïî êðóãó:
+        if FPlayed then
+          if FLoop then
+            FCurrentFrame := 0
+          else
+            FCurrentFrame := FCurrentFrame - 1;
+
+        FCounter := 0;
+      end;
+  end;
+end;
+
+procedure TAnimation.Reset();
+begin
+  if FRevert then
+    FCurrentFrame := High(FramesArray[ID].TexturesID)
+  else
+    FCurrentFrame := 0;
+
+  FCounter := 0;
+  FPlayed := False;
+end;
+
+procedure TAnimation.Disable;
+begin
+  FEnabled := False;
+end;
+
+procedure TAnimation.Enable;
+begin
+  FEnabled := True;
+end;
+
+procedure TAnimation.DrawEx(X, Y: Integer; Mirror: TMirrorType; RPoint: TPoint;
+                            Angle: SmallInt);
+begin
+  if not FEnabled then
+    Exit;
+
+  e_DrawAdv(FramesArray[ID].TexturesID[FCurrentFrame], X, Y, FAlpha,
+            True, FBlending, Angle, @RPoint, Mirror);
+end;
+
+function TAnimation.TotalFrames(): Integer;
+begin
+  Result := Length(FramesArray[ID].TexturesID);
+end;
+
+procedure TAnimation.Revert(r: Boolean);
+begin
+  FRevert := r;
+  Reset();
+end;
+
+procedure TAnimation.SaveState(Var Mem: TBinMemoryWriter);
+var
+  sig: DWORD;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà àíèìàöèè:
+  sig := ANIM_SIGNATURE; // 'ANIM'
+  Mem.WriteDWORD(sig);
+// Ñ÷åò÷èê îæèäàíèÿ ìåæäó êàäðàìè:
+  Mem.WriteByte(FCounter);
+// Òåêóùèé êàäð:
+  Mem.WriteInt(FCurrentFrame);
+// Ïðîèãðàíà ëè àíèìàöèÿ öåëèêîì:
+  Mem.WriteBoolean(FPlayed);
+// Alpha-êàíàë âñåé òåêñòóðû:
+  Mem.WriteByte(FAlpha);
+// Ðàçìûòèå òåêñòóðû:
+  Mem.WriteBoolean(FBlending);
+// Âðåìÿ îæèäàíèÿ ìåæäó êàäðàìè:
+  Mem.WriteByte(FSpeed);
+// Çàöèêëåíà ëè àíèìàöèÿ:
+  Mem.WriteBoolean(FLoop);
+// Âêëþ÷åíà ëè:
+  Mem.WriteBoolean(FEnabled);
+// Îæèäàíèå ïîñëå ïðîèãðûâàíèÿ:
+  Mem.WriteByte(FMinLength);
+// Îáðàòíûé ëè ïîðÿäîê êàäðîâ:
+  Mem.WriteBoolean(FRevert);
+end;
+
+procedure TAnimation.LoadState(Var Mem: TBinMemoryReader);
+var
+  sig: DWORD;
+begin
+  if Mem = nil then
+    Exit;
+
+// Ñèãíàòóðà àíèìàöèè:
+  Mem.ReadDWORD(sig);
+  if sig <> ANIM_SIGNATURE then // 'ANIM'
+  begin
+    raise EBinSizeError.Create('TAnimation.LoadState: Wrong Animation Signature');
+  end;
+// Ñ÷åò÷èê îæèäàíèÿ ìåæäó êàäðàìè:
+  Mem.ReadByte(FCounter);
+// Òåêóùèé êàäð:
+  Mem.ReadInt(FCurrentFrame);
+// Ïðîèãðàíà ëè àíèìàöèÿ öåëèêîì:
+  Mem.ReadBoolean(FPlayed);
+// Alpha-êàíàë âñåé òåêñòóðû:
+  Mem.ReadByte(FAlpha);
+// Ðàçìûòèå òåêñòóðû:
+  Mem.ReadBoolean(FBlending);
+// Âðåìÿ îæèäàíèÿ ìåæäó êàäðàìè:
+  Mem.ReadByte(FSpeed);
+// Çàöèêëåíà ëè àíèìàöèÿ:
+  Mem.ReadBoolean(FLoop);
+// Âêëþ÷åíà ëè:
+  Mem.ReadBoolean(FEnabled);
+// Îæèäàíèå ïîñëå ïðîèãðûâàíèÿ:
+  Mem.ReadByte(FMinLength);
+// Îáðàòíûé ëè ïîðÿäîê êàäðîâ:
+  Mem.ReadBoolean(FRevert);
+end;
+
+end.
diff --git a/src/game/g_triggers.pas b/src/game/g_triggers.pas
new file mode 100644 (file)
index 0000000..6748a00
--- /dev/null
@@ -0,0 +1,2711 @@
+unit g_triggers;
+
+interface
+
+uses
+  MAPSTRUCT, e_graphics, MAPDEF, g_basic, g_sound,
+  BinEditor;
+
+type
+  TActivator = record
+    UID:     Word;
+    TimeOut: Word;
+  end;
+  TTrigger = record
+    ID:               DWORD;
+    ClientID:         DWORD;
+    TriggerType:      Byte;
+    X, Y:             Integer;
+    Width, Height:    Word;
+    Enabled:          Boolean;
+    ActivateType:     Byte;
+    Keys:             Byte;
+    TexturePanel:     Integer;
+    TexturePanelType: Word;
+
+    TimeOut:          Word;
+    ActivateUID:      Word;
+    Activators:       array of TActivator;
+    PlayerCollide:    Boolean;
+    DoorTime:         Integer;
+    PressTime:        Integer;
+    PressCount:       Integer;
+    SoundPlayCount:   Integer;
+    Sound:            TPlayableSound;
+    AutoSpawn:        Boolean;
+    SpawnCooldown:    Integer;
+    SpawnedCount:     Integer;
+    ShotPanelType:    Word;
+    ShotPanelTime:    Integer;
+    ShotSightTime:    Integer;
+    ShotSightTimeout: Integer;
+    ShotSightTarget:  Word;
+    ShotSightTargetN: Word;
+    ShotAmmoCount:    Word;
+    ShotReloadTime:   Integer;
+
+    Data:             TTriggerData;
+  end;
+
+function g_Triggers_Create(Trigger: TTrigger): DWORD;
+procedure g_Triggers_Update();
+procedure g_Triggers_Press(ID: DWORD; ActivateType: Byte);
+function g_Triggers_PressR(X, Y: Integer; Width, Height: Word; UID: Word;
+                           ActivateType: Byte; IgnoreList: DWArray = nil): DWArray;
+procedure g_Triggers_PressL(X1, Y1, X2, Y2: Integer; UID: DWORD; ActivateType: Byte);
+procedure g_Triggers_PressC(CX, CY: Integer; Radius: Word; UID: Word; ActivateType: Byte);
+procedure g_Triggers_OpenAll();
+procedure g_Triggers_DecreaseSpawner(ID: DWORD);
+procedure g_Triggers_Free();
+procedure g_Triggers_SaveState(var Mem: TBinMemoryWriter);
+procedure g_Triggers_LoadState(var Mem: TBinMemoryReader);
+
+var
+  gTriggerClientID: Integer = 0;
+  gTriggers: array of TTrigger;
+  gSecretsCount: Integer = 0;
+  gMonstersSpawned: array of LongInt = nil;
+
+implementation
+
+uses
+  g_player, g_map, Math, g_gfx, g_game, g_textures,
+  g_console, g_monsters, g_items, g_phys, g_weapons,
+  WADEDITOR, g_main, SysUtils, e_log, g_language,
+  g_options, g_net, g_netmsg;
+
+const
+  TRIGGER_SIGNATURE = $52475254; // 'TRGR'
+  TRAP_DAMAGE = 1000;
+
+function FindTrigger(): DWORD;
+var
+  i: Integer;
+begin
+  if gTriggers <> nil then
+    for i := 0 to High(gTriggers) do
+      if gTriggers[i].TriggerType = TRIGGER_NONE then
+      begin
+        Result := i;
+        Exit;
+      end;
+
+  if gTriggers = nil then
+  begin
+    SetLength(gTriggers, 8);
+    Result := 0;
+  end
+  else
+  begin
+    Result := High(gTriggers) + 1;
+    SetLength(gTriggers, Length(gTriggers) + 8);
+  end;
+end;
+
+function CloseDoor(PanelID: Integer; NoSound: Boolean; d2d: Boolean): Boolean;
+var
+  a, b, c: Integer;
+begin
+  Result := False;
+
+  if PanelID = -1 then Exit;
+
+  if not d2d then
+  begin
+    with gWalls[PanelID] do
+    begin
+      if g_CollidePlayer(X, Y, Width, Height) or
+         g_CollideMonster(X, Y, Width, Height) then Exit;
+
+      if not Enabled then
+      begin
+        if not NoSound then
+        begin
+          g_Sound_PlayExAt('SOUND_GAME_DOORCLOSE', X, Y);
+          if g_Game_IsServer and g_Game_IsNet then
+            MH_SEND_Sound(X, Y, 'SOUND_GAME_DOORCLOSE');
+        end;
+        g_Map_EnableWall(PanelID);
+        Result := True;
+      end;
+    end;
+  end
+  else
+  begin
+    if gDoorMap = nil then Exit;
+
+    c := -1;
+    for a := 0 to High(gDoorMap) do
+    begin
+      for b := 0 to High(gDoorMap[a]) do
+        if gDoorMap[a, b] = DWORD(PanelID) then
+        begin
+          c := a;
+          Break;
+        end;
+
+      if c <> -1 then Break;
+    end;
+    if c = -1 then Exit;
+
+    for b := 0 to High(gDoorMap[c]) do
+      with gWalls[gDoorMap[c, b]] do
+      begin
+        if g_CollidePlayer(X, Y, Width, Height) or
+          g_CollideMonster(X, Y, Width, Height) then Exit;
+      end;
+
+    if not NoSound then
+      for b := 0 to High(gDoorMap[c]) do
+        if not gWalls[gDoorMap[c, b]].Enabled then
+        begin
+          with gWalls[PanelID] do
+          begin
+            g_Sound_PlayExAt('SOUND_GAME_DOORCLOSE', X, Y);
+            if g_Game_IsServer and g_Game_IsNet then
+              MH_SEND_Sound(X, Y, 'SOUND_GAME_DOORCLOSE');
+          end;
+          Break;
+        end;
+
+    for b := 0 to High(gDoorMap[c]) do
+      if not gWalls[gDoorMap[c, b]].Enabled then
+      begin
+        g_Map_EnableWall(gDoorMap[c, b]);
+        Result := True;
+      end;
+  end;
+end;
+
+procedure CloseTrap(PanelID: Integer; NoSound: Boolean; d2d: Boolean);
+var
+  a, b, c: Integer;
+begin
+  if PanelID = -1 then Exit;
+
+  if not d2d then
+  begin
+    with gWalls[PanelID] do
+      if (not NoSound) and (not Enabled) then
+      begin
+        g_Sound_PlayExAt('SOUND_GAME_SWITCH1', X, Y);
+        if g_Game_IsServer and g_Game_IsNet then
+          MH_SEND_Sound(X, Y, 'SOUND_GAME_SWITCH1');
+      end;
+
+
+    with gWalls[PanelID] do
+    begin
+      if gPlayers <> nil then
+        for a := 0 to High(gPlayers) do
+          if (gPlayers[a] <> nil) and gPlayers[a].Live and
+              gPlayers[a].Collide(X, Y, Width, Height) then
+            gPlayers[a].Damage(TRAP_DAMAGE, 0, 0, 0, HIT_TRAP);
+
+      if gMonsters <> nil then
+        for a := 0 to High(gMonsters) do
+          if (gMonsters[a] <> nil) and gMonsters[a].Live and
+          g_Obj_Collide(X, Y, Width, Height, @gMonsters[a].Obj) then
+            gMonsters[a].Damage(TRAP_DAMAGE, 0, 0, 0, HIT_TRAP);
+
+      if not Enabled then g_Map_EnableWall(PanelID);
+    end;
+  end
+  else
+  begin
+    if gDoorMap = nil then Exit;
+
+    c := -1;
+    for a := 0 to High(gDoorMap) do
+    begin
+      for b := 0 to High(gDoorMap[a]) do
+        if gDoorMap[a, b] = DWORD(PanelID) then
+        begin
+          c := a;
+          Break;
+        end;
+
+      if c <> -1 then Break;
+    end;
+    if c = -1 then Exit;
+
+    if not NoSound then
+      for b := 0 to High(gDoorMap[c]) do
+        if not gWalls[gDoorMap[c, b]].Enabled then
+        begin
+          with gWalls[PanelID] do
+          begin
+            g_Sound_PlayExAt('SOUND_GAME_SWITCH1', X, Y);
+            if g_Game_IsServer and g_Game_IsNet then
+              MH_SEND_Sound(X, Y, 'SOUND_GAME_SWITCH1');
+          end;
+          Break;
+        end;
+
+    for b := 0 to High(gDoorMap[c]) do
+      with gWalls[gDoorMap[c, b]] do
+      begin
+        if gPlayers <> nil then
+          for a := 0 to High(gPlayers) do
+            if (gPlayers[a] <> nil) and gPlayers[a].Live and
+            gPlayers[a].Collide(X, Y, Width, Height) then
+              gPlayers[a].Damage(TRAP_DAMAGE, 0, 0, 0, HIT_TRAP);
+
+        if gMonsters <> nil then
+          for a := 0 to High(gMonsters) do
+            if (gMonsters[a] <> nil) and gMonsters[a].Live and
+            g_Obj_Collide(X, Y, Width, Height, @gMonsters[a].Obj) then
+              gMonsters[a].Damage(TRAP_DAMAGE, 0, 0, 0, HIT_TRAP);
+
+        if not Enabled then g_Map_EnableWall(gDoorMap[c, b]);
+      end;
+  end;
+end;
+
+function OpenDoor(PanelID: Integer; NoSound: Boolean; d2d: Boolean): Boolean;
+var
+  a, b, c: Integer;
+begin
+  Result := False;
+
+  if PanelID = -1 then Exit;
+
+  if not d2d then
+  begin
+    with gWalls[PanelID] do
+      if Enabled then
+      begin
+        if not NoSound then
+        begin
+          g_Sound_PlayExAt('SOUND_GAME_DOOROPEN', X, Y);
+          if g_Game_IsServer and g_Game_IsNet then
+            MH_SEND_Sound(X, Y, 'SOUND_GAME_DOOROPEN');
+        end;
+        g_Map_DisableWall(PanelID);
+        Result := True;
+      end;
+  end
+  else
+  begin
+    if gDoorMap = nil then Exit;
+
+    c := -1;
+    for a := 0 to High(gDoorMap) do
+    begin
+      for b := 0 to High(gDoorMap[a]) do
+        if gDoorMap[a, b] = DWORD(PanelID) then
+        begin
+          c := a;
+          Break;
+        end;
+
+      if c <> -1 then Break;
+    end;
+    if c = -1 then Exit;
+
+    if not NoSound then
+      for b := 0 to High(gDoorMap[c]) do
+        if gWalls[gDoorMap[c, b]].Enabled then
+        begin
+          with gWalls[PanelID] do
+          begin
+            g_Sound_PlayExAt('SOUND_GAME_DOOROPEN', X, Y);
+            if g_Game_IsServer and g_Game_IsNet then
+              MH_SEND_Sound(X, Y, 'SOUND_GAME_DOOROPEN');
+          end;
+          Break;
+        end;
+
+    for b := 0 to High(gDoorMap[c]) do
+      if gWalls[gDoorMap[c, b]].Enabled then
+      begin
+        g_Map_DisableWall(gDoorMap[c, b]);
+        Result := True;
+      end;
+  end;
+end;
+
+function SetLift(PanelID: Integer; d: Integer; NoSound: Boolean; d2d: Boolean): Boolean;
+var
+  a, b, c, t: Integer;
+begin
+  t := 0;
+  Result := False;
+
+  if PanelID = -1 then Exit;
+
+  if (gLifts[PanelID].PanelType = PANEL_LIFTUP) or
+     (gLifts[PanelID].PanelType = PANEL_LIFTDOWN) then
+    case d of
+      0: t := 0;
+      1: t := 1;
+      else t := IfThen(gLifts[PanelID].LiftType = 1, 0, 1);
+    end
+  else if (gLifts[PanelID].PanelType = PANEL_LIFTLEFT) or
+          (gLifts[PanelID].PanelType = PANEL_LIFTRIGHT) then
+    case d of
+      0: t := 2;
+      1: t := 3;
+      else t := IfThen(gLifts[PanelID].LiftType = 2, 3, 2);
+    end;
+
+  if not d2d then
+  begin
+    with gLifts[PanelID] do
+      if LiftType <> t then
+      begin
+        g_Map_SetLift(PanelID, t);
+
+        {if not NoSound then
+          g_Sound_PlayExAt('SOUND_GAME_SWITCH0', X, Y);}
+        Result := True;
+      end;
+  end
+  else // Êàê â D2d
+  begin
+    if gLiftMap = nil then Exit;
+
+    c := -1;
+    for a := 0 to High(gLiftMap) do
+    begin
+      for b := 0 to High(gLiftMap[a]) do
+        if gLiftMap[a, b] = DWORD(PanelID) then
+        begin
+          c := a;
+          Break;
+        end;
+
+      if c <> -1 then Break;
+    end;
+    if c = -1 then Exit;
+
+    {if not NoSound then
+      for b := 0 to High(gLiftMap[c]) do
+        if gLifts[gLiftMap[c, b]].LiftType <> t then
+        begin
+          with gLifts[PanelID] do
+            g_Sound_PlayExAt('SOUND_GAME_SWITCH0', X, Y);
+          Break;
+        end;}
+
+    for b := 0 to High(gLiftMap[c]) do
+      with gLifts[gLiftMap[c, b]] do
+        if LiftType <> t then
+        begin
+          g_Map_SetLift(gLiftMap[c, b], t);
+
+          Result := True;
+        end;
+  end;
+end;
+
+procedure MakeShot(var Trigger: TTrigger; wx, wy, dx, dy: Integer; TargetUID: Word);
+var
+  Projectile: Boolean;
+  snd: string;
+  TextureID: DWORD;
+  Anim: TAnimation;
+begin
+  with Trigger do
+    if (Data.ShotAmmo = 0) or
+       ((Data.ShotAmmo > 0) and (ShotAmmoCount > 0)) then
+    begin
+      if (Data.ShotPanelID <> -1) and (ShotPanelTime = 0) then
+      begin
+        g_Map_SwitchTexture(ShotPanelType, Data.ShotPanelID);
+        ShotPanelTime := 4; // òèêîâ íà âñïûøêó âûñòðåëà
+      end;
+
+      if Data.ShotIntSight > 0 then
+        ShotSightTimeout := 180; // ~= 5 ñåêóíä
+
+      if ShotAmmoCount > 0 then Dec(ShotAmmoCount);
+      Projectile := True;
+      snd := 'SOUND_WEAPON_FIREROCKET';
+
+      dx := dx + Random(Data.ShotAccuracy) - Random(Data.ShotAccuracy);
+      dy := dy + Random(Data.ShotAccuracy) - Random(Data.ShotAccuracy);
+
+      case Data.ShotType of
+        TRIGGER_SHOT_PISTOL:
+          begin
+            g_Weapon_pistol(wx, wy, dx, dy, 0, True);
+            Projectile := False;
+            snd := 'SOUND_WEAPON_FIREPISTOL';
+            if Data.ShotSound then
+            begin
+              g_Player_CreateShell(wx, wy, 0, -2, SHELL_BULLET);
+              if g_Game_IsNet then
+                MH_SEND_Effect(wx, wy, 0, NET_GFX_SHELL1);
+            end;
+          end;
+
+        TRIGGER_SHOT_BULLET:
+          begin
+            g_Weapon_mgun(wx, wy, dx, dy, 0, True);
+            Projectile := False;
+            if gSoundEffectsDF then snd := 'SOUND_WEAPON_FIRECGUN'
+            else snd := 'SOUND_WEAPON_FIREPISTOL';
+            if Data.ShotSound then
+            begin
+              g_Player_CreateShell(wx, wy, 0, -2, SHELL_BULLET);
+              if g_Game_IsNet then
+                MH_SEND_Effect(wx, wy, 0, NET_GFX_SHELL1);
+            end;
+          end;
+
+        TRIGGER_SHOT_SHOTGUN:
+          begin
+            g_Weapon_Shotgun(wx, wy, dx, dy, 0, True);
+            Projectile := False;
+            snd := 'SOUND_WEAPON_FIRESHOTGUN';
+            if Data.ShotSound then
+            begin
+              g_Player_CreateShell(wx, wy, 0, -2, SHELL_SHELL);
+              if g_Game_IsNet then
+                MH_SEND_Effect(wx, wy, 0, NET_GFX_SHELL2);
+            end;
+          end;
+
+        TRIGGER_SHOT_SSG:
+          begin
+            g_Weapon_DShotgun(wx, wy, dx, dy, 0, True);
+            Projectile := False;
+            snd := 'SOUND_WEAPON_FIRESHOTGUN2';
+            if Data.ShotSound then
+            begin
+              g_Player_CreateShell(wx, wy, 0, -2, SHELL_SHELL);
+              g_Player_CreateShell(wx, wy, 0, -2, SHELL_SHELL);
+              if g_Game_IsNet then
+                MH_SEND_Effect(wx, wy, 0, NET_GFX_SHELL3);
+            end;
+          end;
+
+        TRIGGER_SHOT_IMP:
+          begin
+            g_Weapon_ball1(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREBALL';
+          end;
+
+        TRIGGER_SHOT_PLASMA:
+          begin
+            g_Weapon_Plasma(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREPLASMA';
+          end;
+
+        TRIGGER_SHOT_SPIDER:
+          begin
+            g_Weapon_aplasma(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREPLASMA';
+          end;
+
+        TRIGGER_SHOT_CACO:
+          begin
+            g_Weapon_ball2(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREBALL';
+          end;
+
+        TRIGGER_SHOT_BARON:
+          begin
+            g_Weapon_ball7(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREBALL';
+          end;
+
+        TRIGGER_SHOT_MANCUB:
+          begin
+            g_Weapon_manfire(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREBALL';
+          end;
+
+        TRIGGER_SHOT_REV:
+          begin
+            g_Weapon_revf(wx, wy, dx, dy, 0, TargetUID, -1, True);
+            snd := 'SOUND_WEAPON_FIREREV';
+          end;
+
+        TRIGGER_SHOT_ROCKET:
+          begin
+            g_Weapon_Rocket(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREROCKET';
+          end;
+
+        TRIGGER_SHOT_BFG:
+          begin
+            g_Weapon_BFGShot(wx, wy, dx, dy, 0, -1, True);
+            snd := 'SOUND_WEAPON_FIREBFG';
+          end;
+
+        TRIGGER_SHOT_EXPL:
+          begin
+            if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_ROCKET') then
+            begin
+              Anim := TAnimation.Create(TextureID, False, 6);
+              Anim.Blending := False;
+              g_GFX_OnceAnim(wx-64, wy-64, Anim);
+              Anim.Free();
+            end;
+            Projectile := False;
+            g_Weapon_Explode(wx, wy, 60, 0);
+            snd := 'SOUND_WEAPON_EXPLODEROCKET';
+          end;
+
+        TRIGGER_SHOT_BFGEXPL:
+          begin
+            if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_BFG') then
+            begin
+              Anim := TAnimation.Create(TextureID, False, 6);
+              Anim.Blending := False;
+              g_GFX_OnceAnim(wx-64, wy-64, Anim);
+              Anim.Free();
+            end;
+            Projectile := False;
+            g_Weapon_BFG9000(wx, wy, 0);
+            snd := 'SOUND_WEAPON_EXPLODEBFG';
+          end;
+      end;
+
+      if g_Game_IsNet and g_Game_IsServer then
+        case Data.ShotType of
+          TRIGGER_SHOT_EXPL:
+            MH_SEND_Effect(wx, wy, Byte(Data.ShotSound), NET_GFX_EXPLODE);
+          TRIGGER_SHOT_BFGEXPL:
+            MH_SEND_Effect(wx, wy, Byte(Data.ShotSound), NET_GFX_BFGEXPL);
+          else
+          begin
+            if Projectile then
+              MH_SEND_CreateShot(LastShotID);
+            if Data.ShotSound then
+              MH_SEND_Sound(wx, wy, snd);
+          end;
+        end;
+
+      if Data.ShotSound then
+        g_Sound_PlayExAt(snd, wx, wy);
+    end
+    else
+      if (Data.ShotIntReload > 0) and (ShotReloadTime = 0) then
+        ShotReloadTime := Data.ShotIntReload; // òèêîâ íà ïåðåçàðÿäêó ïóøêè
+end;
+
+procedure MakeEffect(X, Y, VX, VY: Integer; T, ST, CR, CG, CB: Byte; Silent, Send: Boolean);
+var
+  FramesID: DWORD;
+  Anim: TAnimation;
+begin
+  if T = TRIGGER_EFFECT_PARTICLE then
+    case ST of
+      TRIGGER_EFFECT_SLIQUID:
+      begin
+        if (CR = 255) and (CG = 0) and (CB = 0) then
+          g_GFX_SimpleWater(X, Y, 1, VX, VY, 1, 0, 0, 0)
+        else if (CR = 0) and (CG = 255) and (CB = 0) then
+          g_GFX_SimpleWater(X, Y, 1, VX, VY, 2, 0, 0, 0)
+        else if (CR = 0) and (CG = 0) and (CB = 255) then
+          g_GFX_SimpleWater(X, Y, 1, VX, VY, 3, 0, 0, 0)
+        else
+          g_GFX_SimpleWater(X, Y, 1, VX, VY, 0, 0, 0, 0);
+      end;
+      TRIGGER_EFFECT_LLIQUID:
+        g_GFX_SimpleWater(X, Y, 1, VX, VY, 4, CR, CG, CB);
+      TRIGGER_EFFECT_DLIQUID:
+        g_GFX_SimpleWater(X, Y, 1, VX, VY, 5, CR, CG, CB);
+      TRIGGER_EFFECT_BLOOD:
+        g_GFX_Blood(X, Y, 1, VX, VY, 0, 0, CR, CG, CB);
+      TRIGGER_EFFECT_SPARK:
+        g_GFX_Spark(X, Y, 1, GetAngle2(VX, VY), 0, 0);
+      TRIGGER_EFFECT_BUBBLE:
+        g_GFX_Bubbles(X, Y, 1, 0, 0);
+    end;
+  if T = TRIGGER_EFFECT_ANIMATION then
+    case ST of
+      EFFECT_TELEPORT: begin
+        if g_Frames_Get(FramesID, 'FRAMES_TELEPORT') then
+        begin
+          Anim := TAnimation.Create(FramesID, False, 3);
+          if not Silent then
+            g_Sound_PlayExAt('SOUND_GAME_TELEPORT', X, Y);
+          g_GFX_OnceAnim(X-32, Y-32, Anim);
+          Anim.Free();
+        end;
+        if Send and g_Game_IsServer and g_Game_IsNet then
+          MH_SEND_Effect(X, Y, Byte(not Silent), NET_GFX_TELE);
+      end;
+      EFFECT_RESPAWN: begin
+        if g_Frames_Get(FramesID, 'FRAMES_ITEM_RESPAWN') then
+        begin
+          Anim := TAnimation.Create(FramesID, False, 4);
+          if not Silent then
+            g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', X, Y);
+          g_GFX_OnceAnim(X-16, Y-16, Anim);
+          Anim.Free();
+        end;
+        if Send and g_Game_IsServer and g_Game_IsNet then
+          MH_SEND_Effect(X-16, Y-16, Byte(not Silent), NET_GFX_RESPAWN);
+      end;
+      EFFECT_FIRE: begin
+        if g_Frames_Get(FramesID, 'FRAMES_FIRE') then
+        begin
+          Anim := TAnimation.Create(FramesID, False, 4);
+          if not Silent then
+            g_Sound_PlayExAt('SOUND_FIRE', X, Y);
+          g_GFX_OnceAnim(X-32, Y-128, Anim);
+          Anim.Free();
+        end;
+        if Send and g_Game_IsServer and g_Game_IsNet then
+          MH_SEND_Effect(X-32, Y-128, Byte(not Silent), NET_GFX_FIRE);
+      end;
+    end;
+end;
+
+function ActivateTrigger(var Trigger: TTrigger; actType: Byte): Boolean;
+var
+  animonce: Boolean;
+  p: TPlayer;
+  m: TMonster;
+  i, k, wx, wy, xd, yd: Integer;
+  iid: LongWord;
+  coolDown: Boolean;
+  pAngle: Real;
+  FramesID: DWORD;
+  Anim: TAnimation;
+  UIDType: Byte;
+  TargetUID: Word;
+  msg: string;
+begin
+  Result := False;
+  if g_Game_IsClient then
+    Exit;
+
+  if not Trigger.Enabled then
+    Exit;
+  if (Trigger.TimeOut <> 0) and (actType <> ACTIVATE_CUSTOM) then
+    Exit;
+  if gLMSRespawn = LMS_RESPAWN_WARMUP then
+    Exit;
+
+  animonce := False;
+
+  coolDown := (actType <> 0);
+
+  with Trigger do
+  begin
+    case TriggerType of
+      TRIGGER_EXIT:
+        begin
+          g_Sound_PlayEx('SOUND_GAME_SWITCH0');
+          if g_Game_IsNet then MH_SEND_Sound(X, Y, 'SOUND_GAME_SWITCH0');
+          gExitByTrigger := True;
+          g_Game_ExitLevel(Data.MapName);
+          TimeOut := 18;
+          Result := True;
+
+          Exit;
+        end;
+
+      TRIGGER_TELEPORT:
+        begin
+          case g_GetUIDType(ActivateUID) of
+            UID_PLAYER:
+              begin
+                p := g_Player_Get(ActivateUID);
+                if p = nil then
+                  Exit;
+
+                if Data.d2d_teleport then
+                  begin
+                    if p.TeleportTo(Data.TargetPoint.X-(p.Obj.Rect.Width div 2),
+                                    Data.TargetPoint.Y-p.Obj.Rect.Height,
+                                    Data.silent_teleport,
+                                    Data.TlpDir) then
+                      Result := True;
+                  end
+                else
+                  if p.TeleportTo(Data.TargetPoint.X,
+                                  Data.TargetPoint.Y,
+                                  Data.silent_teleport,
+                                  Data.TlpDir) then
+                    Result := True;
+              end;
+
+            UID_MONSTER:
+              begin
+                m := g_Monsters_Get(ActivateUID);
+                if m = nil then
+                  Exit;
+
+                if Data.d2d_teleport then
+                  begin
+                    if m.TeleportTo(Data.TargetPoint.X-(m.Obj.Rect.Width div 2),
+                                    Data.TargetPoint.Y-m.Obj.Rect.Height,
+                                    Data.silent_teleport,
+                                    Data.TlpDir) then
+                      Result := True;
+                  end
+                else
+                  if m.TeleportTo(Data.TargetPoint.X,
+                                  Data.TargetPoint.Y,
+                                  Data.silent_teleport,
+                                  Data.TlpDir) then
+                    Result := True;
+              end;
+          end;
+
+          TimeOut := 0;
+        end;
+
+      TRIGGER_OPENDOOR:
+        begin
+          Result := OpenDoor(Data.PanelID, Data.NoSound, Data.d2d_doors);
+          TimeOut := 0;
+        end;
+
+      TRIGGER_CLOSEDOOR:
+        begin
+          Result := CloseDoor(Data.PanelID, Data.NoSound, Data.d2d_doors);
+          TimeOut := 0;
+        end;
+
+      TRIGGER_DOOR, TRIGGER_DOOR5:
+        begin
+          if Data.PanelID <> -1 then
+          begin
+            if gWalls[Data.PanelID].Enabled then
+              begin
+                Result := OpenDoor(Data.PanelID, Data.NoSound, Data.d2d_doors);
+
+                if TriggerType = TRIGGER_DOOR5 then
+                  DoorTime := 180;
+              end
+            else
+              Result := CloseDoor(Data.PanelID, Data.NoSound, Data.d2d_doors);
+
+            if Result then
+              TimeOut := 18;
+          end;
+        end;
+
+      TRIGGER_CLOSETRAP, TRIGGER_TRAP:
+        begin
+          CloseTrap(Data.PanelID, Data.NoSound, Data.d2d_doors);
+
+          if TriggerType = TRIGGER_TRAP then
+            begin
+              DoorTime := 40;
+              TimeOut := 76;
+            end
+          else
+            begin
+              DoorTime := -1;
+              TimeOut := 0;
+            end;
+
+          Result := True;
+        end;
+
+      TRIGGER_PRESS, TRIGGER_ON, TRIGGER_OFF, TRIGGER_ONOFF:
+        begin
+          PressCount := PressCount + 1;
+          
+          if PressTime = -1 then
+            PressTime := Data.Wait;
+
+          if coolDown then
+            TimeOut := 18
+          else
+            TimeOut := 0;
+          Result := True;
+        end;
+
+      TRIGGER_SECRET:
+        if g_GetUIDType(ActivateUID) = UID_PLAYER then
+        begin
+          Enabled := False;
+          Result := True;
+          if gLMSRespawn = LMS_RESPAWN_NONE then
+          begin
+            g_Player_Get(ActivateUID).GetSecret();
+            Inc(gCoopSecretsFound);
+            if g_Game_IsNet then MH_SEND_GameStats();
+          end;
+        end;
+
+      TRIGGER_LIFTUP:
+        begin
+          Result := SetLift(Data.PanelID, 0, Data.NoSound, Data.d2d_doors);
+          TimeOut := 0;
+
+          if (not Data.NoSound) and Result then begin
+            g_Sound_PlayExAt('SOUND_GAME_SWITCH0',
+                             X + (Width div 2),
+                             Y + (Height div 2));
+            if g_Game_IsServer and g_Game_IsNet then
+              MH_SEND_Sound(X + (Width div 2),
+                            Y + (Height div 2),
+                            'SOUND_GAME_SWITCH0');
+          end;
+        end;
+
+      TRIGGER_LIFTDOWN:
+        begin
+          Result := SetLift(Data.PanelID, 1, Data.NoSound, Data.d2d_doors);
+          TimeOut := 0;
+
+          if (not Data.NoSound) and Result then begin
+            g_Sound_PlayExAt('SOUND_GAME_SWITCH0',
+                             X + (Width div 2),
+                             Y + (Height div 2));
+            if g_Game_IsServer and g_Game_IsNet then
+              MH_SEND_Sound(X + (Width div 2),
+                            Y + (Height div 2),
+                            'SOUND_GAME_SWITCH0');
+          end;
+        end;
+
+      TRIGGER_LIFT:
+        begin
+          Result := SetLift(Data.PanelID, 3, Data.NoSound, Data.d2d_doors);
+
+          if Result then
+          begin
+            TimeOut := 18;
+
+            if (not Data.NoSound) and Result then begin
+              g_Sound_PlayExAt('SOUND_GAME_SWITCH0',
+                               X + (Width div 2),
+                               Y + (Height div 2));
+              if g_Game_IsServer and g_Game_IsNet then
+                MH_SEND_Sound(X + (Width div 2),
+                              Y + (Height div 2),
+                              'SOUND_GAME_SWITCH0');
+            end;
+          end;
+        end;
+
+      TRIGGER_TEXTURE:
+        begin
+          if ByteBool(Data.ActivateOnce) then
+            begin
+              Enabled := False;
+              TriggerType := TRIGGER_NONE;
+            end
+          else
+            if coolDown then
+              TimeOut := 6
+            else
+              TimeOut := 0;
+
+          animonce := Data.AnimOnce;
+          Result := True;
+        end;
+
+      TRIGGER_SOUND:
+        begin
+          if Sound <> nil then
+          begin
+            if Data.SoundSwitch and Sound.IsPlaying() then
+              begin // Íóæíî âûêëþ÷èòü, åñëè èãðàë
+                Sound.Stop();
+                SoundPlayCount := 0;
+                Result := True;
+              end
+            else // (not Data.SoundSwitch) or (not Sound.IsPlaying())
+              if (Data.PlayCount > 0) or (not Sound.IsPlaying()) then
+                begin
+                  if Data.PlayCount > 0 then
+                    SoundPlayCount := Data.PlayCount
+                  else // 0 - èãðàåì áåñêîíå÷íî
+                    SoundPlayCount := 1;
+                  Result := True;
+                end;
+            if g_Game_IsNet then MH_SEND_TriggerSound(Trigger);
+          end;
+        end;
+
+      TRIGGER_SPAWNMONSTER:
+        if (Data.MonType in [MONSTER_DEMON..MONSTER_MAN]) then
+        begin
+          Result := False;
+          if (Data.MonDelay > 0) and (actType <> ACTIVATE_CUSTOM) then
+          begin
+            AutoSpawn := not AutoSpawn;
+            SpawnCooldown := 0;
+            // Àâòîñïàâíåð ïåðåêëþ÷åí - ìåíÿåì òåêñòóðó
+            Result := True;
+          end;
+
+          if ((Data.MonDelay = 0) and (actType <> ACTIVATE_CUSTOM))
+          or ((Data.MonDelay > 0) and (actType = ACTIVATE_CUSTOM)) then
+            for k := 1 to Data.MonCount do
+            begin
+              if (actType = ACTIVATE_CUSTOM) and (Data.MonDelay > 0) then
+                SpawnCooldown := Data.MonDelay;
+              if (Data.MonMax > 0) and (SpawnedCount >= Data.MonMax) then
+                Break;
+
+              i := g_Monsters_Create(Data.MonType,
+                     Data.MonPos.X, Data.MonPos.Y,
+                     TDirection(Data.MonDir), True);
+
+              Result := True;
+
+            // Çäîðîâüå:
+              if (Data.MonHealth > 0) then
+                gMonsters[i].SetHealth(Data.MonHealth);
+            // Óñòàíàâëèâàåì ïîâåäåíèå:
+              gMonsters[i].MonsterBehaviour := Data.MonBehav;
+            // Èäåì èñêàòü öåëü, åñëè íàäî:
+              if Data.MonActive then
+                gMonsters[i].WakeUp();
+              gMonsters[i].FNoRespawn := True;
+
+              if Data.MonType <> MONSTER_BARREL then Inc(gTotalMonsters);
+
+              if g_Game_IsNet then
+              begin
+                SetLength(gMonstersSpawned, Length(gMonstersSpawned)+1);
+                gMonstersSpawned[High(gMonstersSpawned)] := gMonsters[i].UID;
+              end;
+
+              if Data.MonMax > 0 then
+              begin
+                gMonsters[i].SpawnTrigger := ID;
+                Inc(SpawnedCount);
+              end;
+
+              case Data.MonEffect of
+                EFFECT_TELEPORT: begin
+                  if g_Frames_Get(FramesID, 'FRAMES_TELEPORT') then
+                  begin
+                    Anim := TAnimation.Create(FramesID, False, 3);
+                    g_Sound_PlayExAt('SOUND_GAME_TELEPORT', Data.MonPos.X, Data.MonPos.Y);
+                    g_GFX_OnceAnim(gMonsters[i].Obj.X+gMonsters[i].Obj.Rect.X+(gMonsters[i].Obj.Rect.Width div 2)-32,
+                                   gMonsters[i].Obj.Y+gMonsters[i].Obj.Rect.Y+(gMonsters[i].Obj.Rect.Height div 2)-32, Anim);
+                    Anim.Free();
+                  end;
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_Effect(gMonsters[i].Obj.X+gMonsters[i].Obj.Rect.X+(gMonsters[i].Obj.Rect.Width div 2)-32,
+                                   gMonsters[i].Obj.Y+gMonsters[i].Obj.Rect.Y+(gMonsters[i].Obj.Rect.Height div 2)-32, 1,
+                                   NET_GFX_TELE);
+                end;
+                EFFECT_RESPAWN: begin
+                  if g_Frames_Get(FramesID, 'FRAMES_ITEM_RESPAWN') then
+                  begin
+                    Anim := TAnimation.Create(FramesID, False, 4);
+                    g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', Data.MonPos.X, Data.MonPos.Y);
+                    g_GFX_OnceAnim(gMonsters[i].Obj.X+gMonsters[i].Obj.Rect.X+(gMonsters[i].Obj.Rect.Width div 2)-16,
+                                   gMonsters[i].Obj.Y+gMonsters[i].Obj.Rect.Y+(gMonsters[i].Obj.Rect.Height div 2)-16, Anim);
+                    Anim.Free();
+                  end;
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_Effect(gMonsters[i].Obj.X+gMonsters[i].Obj.Rect.X+(gMonsters[i].Obj.Rect.Width div 2)-16,
+                                   gMonsters[i].Obj.Y+gMonsters[i].Obj.Rect.Y+(gMonsters[i].Obj.Rect.Height div 2)-16, 1,
+                                   NET_GFX_RESPAWN);
+                end;
+                EFFECT_FIRE: begin
+                  if g_Frames_Get(FramesID, 'FRAMES_FIRE') then
+                  begin
+                    Anim := TAnimation.Create(FramesID, False, 4);
+                    g_Sound_PlayExAt('SOUND_FIRE', Data.MonPos.X, Data.MonPos.Y);
+                    g_GFX_OnceAnim(gMonsters[i].Obj.X+gMonsters[i].Obj.Rect.X+(gMonsters[i].Obj.Rect.Width div 2)-32,
+                                   gMonsters[i].Obj.Y+gMonsters[i].Obj.Rect.Y+gMonsters[i].Obj.Rect.Height-128, Anim);
+                    Anim.Free();
+                  end;
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_Effect(gMonsters[i].Obj.X+gMonsters[i].Obj.Rect.X+(gMonsters[i].Obj.Rect.Width div 2)-32,
+                                   gMonsters[i].Obj.Y+gMonsters[i].Obj.Rect.Y+gMonsters[i].Obj.Rect.Height-128, 1,
+                                   NET_GFX_FIRE);
+                end;
+              end;
+
+              if g_Game_IsNet then
+                MH_SEND_MonsterSpawn(gMonsters[i].UID);
+            end;
+          if g_Game_IsNet then
+          begin
+            MH_SEND_GameStats();
+            MH_SEND_CoopStats();
+          end;
+
+          if coolDown then
+            TimeOut := 18
+          else
+            TimeOut := 0;
+          // Åñëè àêòèâèðîâàí àâòîñïàâíåðîì, íå ìåíÿåì òåêñòóðó
+          if actType = ACTIVATE_CUSTOM then
+            Result := False;
+        end;
+
+      TRIGGER_SPAWNITEM:
+        if (Data.ItemType in [ITEM_MEDKIT_SMALL..ITEM_MAX]) then
+        begin
+          Result := False;
+          if (Data.ItemDelay > 0) and (actType <> ACTIVATE_CUSTOM) then
+          begin
+            AutoSpawn := not AutoSpawn;
+            SpawnCooldown := 0;
+            // Àâòîñïàâíåð ïåðåêëþ÷åí - ìåíÿåì òåêñòóðó
+            Result := True;
+          end;
+
+          if ((Data.ItemDelay = 0) and (actType <> ACTIVATE_CUSTOM))
+          or ((Data.ItemDelay > 0) and (actType = ACTIVATE_CUSTOM)) then
+            if (not Data.ItemOnlyDM) or
+               (gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF]) then
+              for k := 1 to Data.ItemCount do
+              begin
+                if (actType = ACTIVATE_CUSTOM) and (Data.ItemDelay > 0) then
+                  SpawnCooldown := Data.ItemDelay;
+                if (Data.ItemMax > 0) and (SpawnedCount >= Data.ItemMax) then
+                  Break;
+
+                iid := g_Items_Create(Data.ItemPos.X, Data.ItemPos.Y,
+                  Data.ItemType, Data.ItemFalls, False, True);
+
+                Result := True;
+
+                if Data.ItemMax > 0 then
+                begin
+                  gItems[iid].SpawnTrigger := ID;
+                  Inc(SpawnedCount);
+                end;
+
+                case Data.ItemEffect of
+                  EFFECT_TELEPORT: begin
+                    if g_Frames_Get(FramesID, 'FRAMES_TELEPORT') then
+                    begin
+                      Anim := TAnimation.Create(FramesID, False, 3);
+                      g_Sound_PlayExAt('SOUND_GAME_TELEPORT', Data.ItemPos.X, Data.ItemPos.Y);
+                      g_GFX_OnceAnim(gItems[iid].Obj.X+gItems[iid].Obj.Rect.X+(gItems[iid].Obj.Rect.Width div 2)-32,
+                                     gItems[iid].Obj.Y+gItems[iid].Obj.Rect.Y+(gItems[iid].Obj.Rect.Height div 2)-32, Anim);
+                      Anim.Free();
+                    end;
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_Effect(gItems[iid].Obj.X+gItems[iid].Obj.Rect.X+(gItems[iid].Obj.Rect.Width div 2)-32,
+                                     gItems[iid].Obj.Y+gItems[iid].Obj.Rect.Y+(gItems[iid].Obj.Rect.Height div 2)-32, 1,
+                                     NET_GFX_TELE);
+                  end;
+                  EFFECT_RESPAWN: begin
+                    if g_Frames_Get(FramesID, 'FRAMES_ITEM_RESPAWN') then
+                    begin
+                      Anim := TAnimation.Create(FramesID, False, 4);
+                      g_Sound_PlayExAt('SOUND_ITEM_RESPAWNITEM', Data.ItemPos.X, Data.ItemPos.Y);
+                      g_GFX_OnceAnim(gItems[iid].Obj.X+gItems[iid].Obj.Rect.X+(gItems[iid].Obj.Rect.Width div 2)-16,
+                                     gItems[iid].Obj.Y+gItems[iid].Obj.Rect.Y+(gItems[iid].Obj.Rect.Height div 2)-16, Anim);
+                      Anim.Free();
+                    end;
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_Effect(gItems[iid].Obj.X+gItems[iid].Obj.Rect.X+(gItems[iid].Obj.Rect.Width div 2)-16,
+                                     gItems[iid].Obj.Y+gItems[iid].Obj.Rect.Y+(gItems[iid].Obj.Rect.Height div 2)-16, 1,
+                                     NET_GFX_RESPAWN);
+                  end;
+                  EFFECT_FIRE: begin
+                    if g_Frames_Get(FramesID, 'FRAMES_FIRE') then
+                    begin
+                      Anim := TAnimation.Create(FramesID, False, 4);
+                      g_Sound_PlayExAt('SOUND_FIRE', Data.ItemPos.X, Data.ItemPos.Y);
+                      g_GFX_OnceAnim(gItems[iid].Obj.X+gItems[iid].Obj.Rect.X+(gItems[iid].Obj.Rect.Width div 2)-32,
+                                     gItems[iid].Obj.Y+gItems[iid].Obj.Rect.Y+gItems[iid].Obj.Rect.Height-128, Anim);
+                      Anim.Free();
+                    end;
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_Effect(gItems[iid].Obj.X+gItems[iid].Obj.Rect.X+(gItems[iid].Obj.Rect.Width div 2)-32,
+                                     gItems[iid].Obj.Y+gItems[iid].Obj.Rect.Y+gItems[iid].Obj.Rect.Height-128, 1,
+                                     NET_GFX_FIRE);
+                  end;
+                end;
+
+                if g_Game_IsNet then
+                  MH_SEND_ItemSpawn(True, iid);
+              end;
+
+          if coolDown then
+            TimeOut := 18
+          else
+            TimeOut := 0;
+          // Åñëè àêòèâèðîâàí àâòîñïàâíåðîì, íå ìåíÿåì òåêñòóðó
+          if actType = ACTIVATE_CUSTOM then
+            Result := False;
+        end;
+
+      TRIGGER_MUSIC:
+        begin
+        // Ìåíÿåì ìóçûêó, åñëè åñòü íà ÷òî:
+          if (Trigger.Data.MusicName <> '') then
+          begin
+            gMusic.SetByName(Trigger.Data.MusicName);
+            gMusic.SpecPause := True;
+            gMusic.Play();
+          end;
+
+          if Trigger.Data.MusicAction = 1 then
+            begin // Âêëþ÷èòü
+              if gMusic.SpecPause then // Áûëà íà ïàóçå => èãðàòü
+                gMusic.SpecPause := False
+              else // Èãðàëà => ñíà÷àëà
+                gMusic.SetPosition(0);
+            end
+          else // Âûêëþ÷èòü
+            begin
+            // Ïàóçà:
+              gMusic.SpecPause := True;
+            end;
+
+          if coolDown then
+            TimeOut := 36
+          else
+            TimeOut := 0;
+          Result := True;
+          if g_Game_IsNet then MH_SEND_TriggerMusic;
+        end;
+
+      TRIGGER_PUSH:
+        begin
+          case g_GetUIDType(ActivateUID) of
+            UID_PLAYER:
+              begin
+                p := g_Player_Get(ActivateUID);
+                if p = nil then
+                  Exit;
+                  
+                if Data.ResetVel then
+                begin
+                  p.GameVelX := 0;
+                  p.GameVelY := 0;
+                  p.GameAccelX := 0;
+                  p.GameAccelY := 0;
+                end;
+
+                pAngle := -DegToRad(Data.PushAngle);
+                p.Push(Floor(Cos(pAngle)*Data.PushForce),
+                       Floor(Sin(pAngle)*Data.PushForce));
+              end;
+
+            UID_MONSTER:
+              begin
+                m := g_Monsters_Get(ActivateUID);
+                if m = nil then
+                  Exit;
+                if Data.ResetVel then
+                begin
+                  m.GameVelX := 0;
+                  m.GameVelY := 0;
+                  m.GameAccelX := 0;
+                  m.GameAccelY := 0;
+                end;
+
+                pAngle := -DegToRad(Data.PushAngle);
+                m.Push(Floor(Cos(pAngle)*Data.PushForce),
+                       Floor(Sin(pAngle)*Data.PushForce));
+              end;
+          end;
+
+          TimeOut := 0;
+          Result := True;
+        end;
+
+      TRIGGER_SCORE:
+        begin
+          Result := False;
+          // Ïðèáàâèòü èëè îòíÿòü î÷êî
+          if (Data.ScoreAction in [0..1]) and (Data.ScoreCount > 0) then
+          begin
+            // Ñâîåé èëè ÷óæîé êîìàíäå
+            if (Data.ScoreTeam in [0..1]) and (g_GetUIDType(ActivateUID) = UID_PLAYER) then
+            begin
+              p := g_Player_Get(ActivateUID);
+              if ((Data.ScoreAction = 0) and (Data.ScoreTeam = 0) and (p.Team = TEAM_RED))
+              or ((Data.ScoreAction = 0) and (Data.ScoreTeam = 1) and (p.Team = TEAM_BLUE)) then
+              begin
+                Inc(gTeamStat[TEAM_RED].Goals, Data.ScoreCount); // Red Scores
+
+                if Data.ScoreCon then
+                  if Data.ScoreTeam = 0 then
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_ADD_OWN], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '+r');
+                  end else
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_ADD_ENEMY], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '+re');
+                  end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_ADD], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, TEAM_RED);
+                end;
+              end;
+              if ((Data.ScoreAction = 1) and (Data.ScoreTeam = 0) and (p.Team = TEAM_RED))
+              or ((Data.ScoreAction = 1) and (Data.ScoreTeam = 1) and (p.Team = TEAM_BLUE)) then
+              begin
+                Dec(gTeamStat[TEAM_RED].Goals, Data.ScoreCount); // Red Fouls
+
+                if Data.ScoreCon then
+                  if Data.ScoreTeam = 0 then
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_SUB_OWN], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '-r');
+                  end else
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_SUB_ENEMY], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '-re');
+                  end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_SUB], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, -TEAM_RED);
+                end;
+              end;
+              if ((Data.ScoreAction = 0) and (Data.ScoreTeam = 0) and (p.Team = TEAM_BLUE))
+              or ((Data.ScoreAction = 0) and (Data.ScoreTeam = 1) and (p.Team = TEAM_RED)) then
+              begin
+                Inc(gTeamStat[TEAM_BLUE].Goals, Data.ScoreCount); // Blue Scores
+
+                if Data.ScoreCon then
+                  if Data.ScoreTeam = 0 then
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_ADD_OWN], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '+b');
+                  end else
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_ADD_ENEMY], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '+be');
+                  end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_ADD], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, TEAM_BLUE);
+                end;
+              end;
+              if ((Data.ScoreAction = 1) and (Data.ScoreTeam = 0) and (p.Team = TEAM_BLUE))
+              or ((Data.ScoreAction = 1) and (Data.ScoreTeam = 1) and (p.Team = TEAM_RED)) then
+              begin
+                Dec(gTeamStat[TEAM_BLUE].Goals, Data.ScoreCount); // Blue Fouls
+
+                if Data.ScoreCon then
+                  if Data.ScoreTeam = 0 then
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_SUB_OWN], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '-b');
+                  end else
+                  begin
+                    g_Console_Add(Format(_lc[I_PLAYER_SCORE_SUB_ENEMY], [p.Name, Data.ScoreCount, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                    if g_Game_IsServer and g_Game_IsNet then
+                      MH_SEND_GameEvent(NET_EV_SCORE, p.UID or (Data.ScoreCount shl 16), '-be');
+                  end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_SUB], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, -TEAM_BLUE);
+                end;
+              end;
+              Result := (p.Team = TEAM_RED) or (p.Team = TEAM_BLUE);
+            end;
+            // Êàêîé-òî êîíêðåòíîé êîìàíäå
+            if Data.ScoreTeam in [2..3] then
+            begin
+              if (Data.ScoreAction = 0) and (Data.ScoreTeam = 2) then
+              begin
+                Inc(gTeamStat[TEAM_RED].Goals, Data.ScoreCount); // Red Scores
+
+                if Data.ScoreCon then
+                begin
+                  g_Console_Add(Format(_lc[I_PLAYER_SCORE_ADD_TEAM], [_lc[I_PLAYER_SCORE_RED], Data.ScoreCount]), True);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE, Data.ScoreCount shl 16, '+tr');
+                end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_ADD], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, TEAM_RED);
+                end;
+              end;
+              if (Data.ScoreAction = 1) and (Data.ScoreTeam = 2) then
+              begin
+                Dec(gTeamStat[TEAM_RED].Goals, Data.ScoreCount); // Red Fouls
+
+                if Data.ScoreCon then
+                begin
+                  g_Console_Add(Format(_lc[I_PLAYER_SCORE_SUB_TEAM], [_lc[I_PLAYER_SCORE_RED], Data.ScoreCount]), True);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE, Data.ScoreCount shl 16, '-tr');
+                end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_SUB], [AnsiUpperCase(_lc[I_GAME_TEAM_RED])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, -TEAM_RED);
+                end;
+              end;
+              if (Data.ScoreAction = 0) and (Data.ScoreTeam = 3) then
+              begin
+                Inc(gTeamStat[TEAM_BLUE].Goals, Data.ScoreCount); // Blue Scores
+
+                if Data.ScoreCon then
+                begin
+                  g_Console_Add(Format(_lc[I_PLAYER_SCORE_ADD_TEAM], [_lc[I_PLAYER_SCORE_BLUE], Data.ScoreCount]), True);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE, Data.ScoreCount shl 16, '+tb');
+                end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_ADD], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, TEAM_BLUE);
+                end;
+              end;
+              if (Data.ScoreAction = 1) and (Data.ScoreTeam = 3) then
+              begin
+                Dec(gTeamStat[TEAM_BLUE].Goals, Data.ScoreCount); // Blue Fouls
+
+                if Data.ScoreCon then
+                begin
+                  g_Console_Add(Format(_lc[I_PLAYER_SCORE_SUB_TEAM], [_lc[I_PLAYER_SCORE_BLUE], Data.ScoreCount]), True);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE, Data.ScoreCount shl 16, '-tb');
+                end;
+
+                if Data.ScoreMsg then
+                begin
+                  g_Game_Message(Format(_lc[I_MESSAGE_SCORE_SUB], [AnsiUpperCase(_lc[I_GAME_TEAM_BLUE])]), 108);
+                  if g_Game_IsServer and g_Game_IsNet then
+                    MH_SEND_GameEvent(NET_EV_SCORE_MSG, -TEAM_BLUE);
+                end;
+              end;
+              Result := True;
+            end;
+          end;
+          // Âûèãðûø
+          if (Data.ScoreAction = 2) and (gGameSettings.GoalLimit > 0) then
+          begin
+            // Ñâîåé èëè ÷óæîé êîìàíäû
+            if (Data.ScoreTeam in [0..1]) and (g_GetUIDType(ActivateUID) = UID_PLAYER) then
+            begin
+              p := g_Player_Get(ActivateUID);
+              if ((Data.ScoreTeam = 0) and (p.Team = TEAM_RED)) // Red Wins
+              or ((Data.ScoreTeam = 1) and (p.Team = TEAM_BLUE)) then
+                if gTeamStat[TEAM_RED].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_RED].Goals := gGameSettings.GoalLimit;
+
+                  if Data.ScoreCon then
+                    if Data.ScoreTeam = 0 then
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_OWN], [p.Name, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wr');
+                    end else
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_ENEMY], [p.Name, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wre');
+                    end;
+
+                  Result := True;
+                end;
+              if ((Data.ScoreTeam = 0) and (p.Team = TEAM_BLUE)) // Blue Wins
+              or ((Data.ScoreTeam = 1) and (p.Team = TEAM_RED)) then
+                if gTeamStat[TEAM_BLUE].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_BLUE].Goals := gGameSettings.GoalLimit;
+
+                  if Data.ScoreCon then
+                    if Data.ScoreTeam = 0 then
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_OWN], [p.Name, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wb');
+                    end else
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_ENEMY], [p.Name, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wbe');
+                    end;
+
+                  Result := True;
+                end;
+            end;
+            // Êàêîé-òî êîíêðåòíîé êîìàíäû
+            if Data.ScoreTeam in [2..3] then
+            begin
+              if Data.ScoreTeam = 2 then // Red Wins
+                if gTeamStat[TEAM_RED].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_RED].Goals := gGameSettings.GoalLimit;
+                  Result := True;
+                end;
+              if Data.ScoreTeam = 3 then // Blue Wins
+                if gTeamStat[TEAM_BLUE].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_BLUE].Goals := gGameSettings.GoalLimit;
+                  Result := True;
+                end;
+            end;
+          end;
+          // Ïðîèãðûø
+          if (Data.ScoreAction = 3) and (gGameSettings.GoalLimit > 0) then
+          begin
+            // Ñâîåé èëè ÷óæîé êîìàíäû
+            if (Data.ScoreTeam in [0..1]) and (g_GetUIDType(ActivateUID) = UID_PLAYER) then
+            begin
+              p := g_Player_Get(ActivateUID);
+              if ((Data.ScoreTeam = 0) and (p.Team = TEAM_BLUE)) // Red Wins
+              or ((Data.ScoreTeam = 1) and (p.Team = TEAM_RED)) then
+                if gTeamStat[TEAM_RED].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_RED].Goals := gGameSettings.GoalLimit;
+
+                  if Data.ScoreCon then
+                    if Data.ScoreTeam = 0 then
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_ENEMY], [p.Name, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wre');
+                    end else
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_OWN], [p.Name, _lc[I_PLAYER_SCORE_TO_RED]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wr');
+                    end;
+
+                  Result := True;
+                end;
+              if ((Data.ScoreTeam = 0) and (p.Team = TEAM_RED)) // Blue Wins
+              or ((Data.ScoreTeam = 1) and (p.Team = TEAM_BLUE)) then
+                if gTeamStat[TEAM_BLUE].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_BLUE].Goals := gGameSettings.GoalLimit;
+
+                  if Data.ScoreCon then
+                    if Data.ScoreTeam = 0 then
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_ENEMY], [p.Name, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wbe');
+                    end else
+                    begin
+                      g_Console_Add(Format(_lc[I_PLAYER_SCORE_WIN_OWN], [p.Name, _lc[I_PLAYER_SCORE_TO_BLUE]]), True);
+                      if g_Game_IsServer and g_Game_IsNet then
+                        MH_SEND_GameEvent(NET_EV_SCORE, p.UID, 'wb');
+                    end;
+
+                  Result := True;
+                end;
+            end;
+            // Êàêîé-òî êîíêðåòíîé êîìàíäû
+            if Data.ScoreTeam in [2..3] then
+            begin
+              if Data.ScoreTeam = 3 then // Red Wins
+                if gTeamStat[TEAM_RED].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_RED].Goals := gGameSettings.GoalLimit;
+                  Result := True;
+                end;
+              if Data.ScoreTeam = 2 then // Blue Wins
+                if gTeamStat[TEAM_BLUE].Goals < SmallInt(gGameSettings.GoalLimit) then
+                begin
+                  gTeamStat[TEAM_BLUE].Goals := gGameSettings.GoalLimit;
+                  Result := True;
+                end;
+            end;
+          end;
+          if Result then begin
+            if coolDown then
+              TimeOut := 18
+            else
+              TimeOut := 0;
+            if g_Game_IsServer and g_Game_IsNet then
+              MH_SEND_GameStats;
+          end;
+        end;
+
+      TRIGGER_MESSAGE:
+        begin
+          msg := b_Text_Format(Data.MessageText);
+          case Data.MessageSendTo of
+            0: // activator
+              begin
+                if g_GetUIDType(ActivateUID) = UID_PLAYER then
+                begin
+                  if g_Game_IsWatchedPlayer(ActivateUID) then
+                  begin
+                    if Data.MessageKind = 0 then
+                      g_Console_Add(msg, True)
+                    else if Data.MessageKind = 1 then
+                      g_Game_Message(msg, Data.MessageTime);
+                  end
+                  else
+                  begin
+                    p := g_Player_Get(ActivateUID);
+                    if g_Game_IsNet and (p.FClientID >= 0) then
+                      if Data.MessageKind = 0 then
+                        MH_SEND_Chat(msg, NET_CHAT_SYSTEM, p.FClientID)
+                      else if Data.MessageKind = 1 then
+                        MH_SEND_GameEvent(NET_EV_BIGTEXT, Data.MessageTime, msg, p.FClientID);
+                  end;
+                end;
+              end;
+
+            1: // activator's team
+              begin
+                if g_GetUIDType(ActivateUID) = UID_PLAYER then
+                begin
+                  p := g_Player_Get(ActivateUID);
+                  if g_Game_IsWatchedTeam(p.Team) then
+                    if Data.MessageKind = 0 then
+                      g_Console_Add(msg, True)
+                    else if Data.MessageKind = 1 then
+                      g_Game_Message(msg, Data.MessageTime);
+
+                  if g_Game_IsNet then
+                  begin
+                    for i := Low(gPlayers) to High(gPlayers) do
+                      if (gPlayers[i].Team = p.Team) and (gPlayers[i].FClientID >= 0) then
+                        if Data.MessageKind = 0 then
+                          MH_SEND_Chat(msg, NET_CHAT_SYSTEM, gPlayers[i].FClientID)
+                        else if Data.MessageKind = 1 then
+                          MH_SEND_GameEvent(NET_EV_BIGTEXT, Data.MessageTime, msg, gPlayers[i].FClientID);
+                  end;
+                end;
+              end;
+
+            2: // activator's enemy team
+              begin
+                if g_GetUIDType(ActivateUID) = UID_PLAYER then
+                begin
+                  p := g_Player_Get(ActivateUID);
+                  if g_Game_IsWatchedTeam(p.Team) then
+                    if Data.MessageKind = 0 then
+                      g_Console_Add(msg, True)
+                    else if Data.MessageKind = 1 then
+                      g_Game_Message(msg, Data.MessageTime);
+
+                  if g_Game_IsNet then
+                  begin
+                    for i := Low(gPlayers) to High(gPlayers) do
+                      if (gPlayers[i].Team <> p.Team) and (gPlayers[i].FClientID >= 0) then
+                        if Data.MessageKind = 0 then
+                          MH_SEND_Chat(msg, NET_CHAT_SYSTEM, gPlayers[i].FClientID)
+                        else if Data.MessageKind = 1 then
+                          MH_SEND_GameEvent(NET_EV_BIGTEXT, Data.MessageTime, msg, gPlayers[i].FClientID);
+                  end;
+                end;
+              end;
+
+            3: // red team
+              begin
+                if g_Game_IsWatchedTeam(TEAM_RED) then
+                  if Data.MessageKind = 0 then
+                    g_Console_Add(msg, True)
+                  else if Data.MessageKind = 1 then
+                    g_Game_Message(msg, Data.MessageTime);
+
+                if g_Game_IsNet then
+                begin
+                  for i := Low(gPlayers) to High(gPlayers) do
+                    if (gPlayers[i].Team = TEAM_RED) and (gPlayers[i].FClientID >= 0) then
+                      if Data.MessageKind = 0 then
+                        MH_SEND_Chat(msg, NET_CHAT_SYSTEM, gPlayers[i].FClientID)
+                      else if Data.MessageKind = 1 then
+                        MH_SEND_GameEvent(NET_EV_BIGTEXT, Data.MessageTime, msg, gPlayers[i].FClientID);
+                end;
+              end;
+
+            4: // blue team
+              begin
+                if g_Game_IsWatchedTeam(TEAM_BLUE) then
+                  if Data.MessageKind = 0 then
+                    g_Console_Add(msg, True)
+                  else if Data.MessageKind = 1 then
+                    g_Game_Message(msg, Data.MessageTime);
+
+                if g_Game_IsNet then
+                begin
+                  for i := Low(gPlayers) to High(gPlayers) do
+                    if (gPlayers[i].Team = TEAM_BLUE) and (gPlayers[i].FClientID >= 0) then
+                      if Data.MessageKind = 0 then
+                        MH_SEND_Chat(msg, NET_CHAT_SYSTEM, gPlayers[i].FClientID)
+                      else if Data.MessageKind = 1 then
+                        MH_SEND_GameEvent(NET_EV_BIGTEXT, Data.MessageTime, msg, gPlayers[i].FClientID);
+                end;
+              end;
+
+            5: // everyone
+              begin
+                if Data.MessageKind = 0 then
+                  g_Console_Add(msg, True)
+                else if Data.MessageKind = 1 then
+                  g_Game_Message(msg, Data.MessageTime);
+
+                if g_Game_IsNet then
+                begin
+                  if Data.MessageKind = 0 then
+                    MH_SEND_Chat(msg, NET_CHAT_SYSTEM)
+                  else if Data.MessageKind = 1 then
+                    MH_SEND_GameEvent(NET_EV_BIGTEXT, Data.MessageTime, msg);
+                end;
+              end;
+          end;
+          TimeOut := 18;
+        end;
+
+      TRIGGER_DAMAGE, TRIGGER_HEALTH:
+        begin
+          Result := False;
+          UIDType := g_GetUIDType(ActivateUID);
+          if (UIDType = UID_PLAYER) or (UIDType = UID_MONSTER) then
+          begin
+            Result := True;
+            k := -1;
+            if coolDown then
+            begin
+              // Âñïîìèíàåì, àêòèâèðîâàë ëè îí ìåíÿ ðàíüøå
+              for i := 0 to High(Activators) do
+                if Activators[i].UID = ActivateUID then
+                begin
+                  k := i;
+                  Break;
+                end;
+              if k = -1 then
+              begin // Âèäèì åãî âïåðâûå
+                // Çàïîìèíàåì åãî
+                SetLength(Activators, Length(Activators) + 1);
+                k := High(Activators);
+                Activators[k].UID := ActivateUID;
+              end else
+              begin // Óæå âèäåëè åãî
+                // Åñëè èíòåðâàë îòêëþ÷¸í, íî îí âñ¸ åù¸ â çîíå ïîðàæåíèÿ, äà¸ì åìó âðåìÿ
+                if (Data.DamageInterval = 0) and (Activators[k].TimeOut > 0) then
+                  Activators[k].TimeOut := 65535;
+                // Òàéìàóò ïðîø¸ë - ðàáîòàåì
+                Result := Activators[k].TimeOut = 0;
+              end;
+            end;
+
+            if Result then
+            begin
+              case UIDType of
+                UID_PLAYER:
+                  begin
+                    p := g_Player_Get(ActivateUID);
+                    if p = nil then
+                      Exit;
+
+                    // Íàíîñèì óðîí èãðîêó
+                    if (TriggerType = TRIGGER_DAMAGE) and (Data.DamageValue > 0) then
+                      p.Damage(Data.DamageValue, 0, 0, 0, HIT_SOME);
+
+                    // Ëå÷èì èãðîêà
+                    if (TriggerType = TRIGGER_HEALTH) and (Data.HealValue > 0) then
+                      if p.Heal(Data.HealValue, not Data.HealMax) and (not Data.HealSilent) then
+                      begin
+                        g_Sound_PlayExAt('SOUND_ITEM_GETITEM', p.Obj.X, p.Obj.Y);
+                        if g_Game_IsServer and g_Game_IsNet then
+                          MH_SEND_Sound(p.Obj.X, p.Obj.Y, 'SOUND_ITEM_GETITEM');
+                      end;
+                  end;
+
+                UID_MONSTER:
+                  begin
+                    m := g_Monsters_Get(ActivateUID);
+                    if m = nil then
+                      Exit;
+
+                    // Íàíîñèì óðîí ìîíñòðó
+                    if (TriggerType = TRIGGER_DAMAGE) and (Data.DamageValue > 0) then
+                      m.Damage(Data.DamageValue, 0, 0, 0, HIT_SOME);
+
+                    // Ëå÷èì ìîíñòðà
+                    if (TriggerType = TRIGGER_HEALTH) and (Data.HealValue > 0) then
+                      if m.Heal(Data.HealValue) and (not Data.HealSilent) then
+                      begin
+                        g_Sound_PlayExAt('SOUND_ITEM_GETITEM', m.Obj.X, m.Obj.Y);
+                        if g_Game_IsServer and g_Game_IsNet then
+                          MH_SEND_Sound(m.Obj.X, m.Obj.Y, 'SOUND_ITEM_GETITEM');
+                      end;
+                  end;
+              end;
+              // Íàçíà÷àåì âðåìÿ ñëåäóþùåãî âîçäåéñòâèÿ
+              if TriggerType = TRIGGER_DAMAGE then
+                i := Data.DamageInterval
+              else
+                i := Data.HealInterval;
+              if coolDown then
+                if i > 0 then
+                  Activators[k].TimeOut := i
+                else
+                  Activators[k].TimeOut := 65535;
+            end;
+          end;
+          TimeOut := 0;
+        end;
+
+      TRIGGER_SHOT:
+        begin
+          if ShotSightTime > 0 then
+            Exit;
+
+          wx := Data.ShotPos.X;
+          wy := Data.ShotPos.Y;
+          pAngle := -DegToRad(Data.ShotAngle);
+          xd := wx + Round(Cos(pAngle) * 32.0);
+          yd := wy + Round(Sin(pAngle) * 32.0);
+          TargetUID := 0;
+
+          case Data.ShotTarget of
+            TRIGGER_SHOT_TARGET_MON: // monsters
+              if gMonsters <> nil then
+                for i := Low(gMonsters) to High(gMonsters) do
+                  if (gMonsters[i] <> nil) and gMonsters[i].Live and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gMonsters[i].Obj))) then
+                  begin
+                    xd := gMonsters[i].GameX + gMonsters[i].Obj.Rect.Width div 2;
+                    yd := gMonsters[i].GameY + gMonsters[i].Obj.Rect.Height div 2;
+                    TargetUID := gMonsters[i].UID;
+                    break;
+                  end;
+
+            TRIGGER_SHOT_TARGET_PLR: // players
+              if gPlayers <> nil then
+                for i := Low(gPlayers) to High(gPlayers) do
+                  if (gPlayers[i] <> nil) and gPlayers[i].Live and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gPlayers[i].Obj))) then
+                  begin
+                    xd := gPlayers[i].GameX + PLAYER_RECT_CX;
+                    yd := gPlayers[i].GameY + PLAYER_RECT_CY;
+                    TargetUID := gPlayers[i].UID;
+                    break;
+                  end;
+
+            TRIGGER_SHOT_TARGET_RED: // red team
+              if gPlayers <> nil then
+                for i := Low(gPlayers) to High(gPlayers) do
+                  if (gPlayers[i] <> nil) and gPlayers[i].Live and
+                     (gPlayers[i].Team = TEAM_RED) and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gPlayers[i].Obj))) then
+                  begin
+                    xd := gPlayers[i].GameX + PLAYER_RECT_CX;
+                    yd := gPlayers[i].GameY + PLAYER_RECT_CY;
+                    TargetUID := gPlayers[i].UID;
+                    break;
+                  end;
+
+            TRIGGER_SHOT_TARGET_BLUE: // blue team
+              if gPlayers <> nil then
+                for i := Low(gPlayers) to High(gPlayers) do
+                  if (gPlayers[i] <> nil) and gPlayers[i].Live and
+                     (gPlayers[i].Team = TEAM_BLUE) and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gPlayers[i].Obj))) then
+                  begin
+                    xd := gPlayers[i].GameX + PLAYER_RECT_CX;
+                    yd := gPlayers[i].GameY + PLAYER_RECT_CY;
+                    TargetUID := gPlayers[i].UID;
+                    break;
+                  end;
+
+            TRIGGER_SHOT_TARGET_MONPLR: // monsters then players
+            begin
+              if gMonsters <> nil then
+                for i := Low(gMonsters) to High(gMonsters) do
+                  if (gMonsters[i] <> nil) and gMonsters[i].Live and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gMonsters[i].Obj))) then
+                  begin
+                    xd := gMonsters[i].GameX + gMonsters[i].Obj.Rect.Width div 2;
+                    yd := gMonsters[i].GameY + gMonsters[i].Obj.Rect.Height div 2;
+                    TargetUID := gMonsters[i].UID;
+                    break;
+                  end;
+              if (TargetUID = 0) and (gPlayers <> nil) then
+                for i := Low(gPlayers) to High(gPlayers) do
+                  if (gPlayers[i] <> nil) and gPlayers[i].Live and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gPlayers[i].Obj))) then
+                  begin
+                    xd := gPlayers[i].GameX + PLAYER_RECT_CX;
+                    yd := gPlayers[i].GameY + PLAYER_RECT_CY;
+                    TargetUID := gPlayers[i].UID;
+                    break;
+                  end;
+            end;
+
+            TRIGGER_SHOT_TARGET_PLRMON: // players then monsters
+            begin
+              if gPlayers <> nil then
+                for i := Low(gPlayers) to High(gPlayers) do
+                  if (gPlayers[i] <> nil) and gPlayers[i].Live and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gPlayers[i].Obj))) then
+                  begin
+                    xd := gPlayers[i].GameX + PLAYER_RECT_CX;
+                    yd := gPlayers[i].GameY + PLAYER_RECT_CY;
+                    TargetUID := gPlayers[i].UID;
+                    break;
+                  end;
+              if (TargetUID = 0) and (gMonsters <> nil) then
+                for i := Low(gMonsters) to High(gMonsters) do
+                  if (gMonsters[i] <> nil) and gMonsters[i].Live and
+                     (Data.ShotAllMap or g_Obj_Collide(X, Y, Width, Height, @(gMonsters[i].Obj))) then
+                  begin
+                    xd := gMonsters[i].GameX + gMonsters[i].Obj.Rect.Width div 2;
+                    yd := gMonsters[i].GameY + gMonsters[i].Obj.Rect.Height div 2;
+                    TargetUID := gMonsters[i].UID;
+                    break;
+                  end;
+            end;
+
+            else TargetUID := ActivateUID;
+          end;
+
+          if (Data.ShotTarget = TRIGGER_SHOT_TARGET_NONE) or (TargetUID > 0) then
+          begin
+            Result := True;
+            if (Data.ShotIntSight = 0) or
+               (Data.ShotTarget = TRIGGER_SHOT_TARGET_NONE) or
+               (TargetUID = ShotSightTarget) then
+              MakeShot(Trigger, wx, wy, xd, yd, TargetUID)
+            else
+            begin
+              ShotSightTime := Data.ShotIntSight;
+              ShotSightTargetN := TargetUID;
+              if Data.ShotType = TRIGGER_SHOT_BFG then
+              begin
+                g_Sound_PlayExAt('SOUND_WEAPON_STARTFIREBFG', wx, wy);
+                if g_Game_IsNet and g_Game_IsServer then
+                  MH_SEND_Sound(wx, wy, 'SOUND_WEAPON_STARTFIREBFG');
+              end;
+            end;
+          end;
+
+          TimeOut := Data.ShotWait + 1;
+        end;
+
+      TRIGGER_EFFECT:
+        begin
+          i := Data.FXCount;
+
+          while i > 0 do
+          begin
+            case Data.FXPos of
+              TRIGGER_EFFECT_POS_CENTER:
+              begin
+                wx := X + Width div 2;
+                wy := Y + Height div 2;
+              end;
+              TRIGGER_EFFECT_POS_AREA:
+              begin
+                wx := X + Random(Width);
+                wy := Y + Random(Height);
+              end;
+              else begin
+                wx := X + Width div 2;
+                wy := Y + Height div 2;
+              end;
+            end;
+            xd := Data.FXVelX;
+            yd := Data.FXVelY;
+            if Data.FXSpreadL > 0 then xd := xd - Random(Data.FXSpreadL + 1);
+            if Data.FXSpreadR > 0 then xd := xd + Random(Data.FXSpreadR + 1);
+            if Data.FXSpreadU > 0 then yd := yd - Random(Data.FXSpreadU + 1);
+            if Data.FXSpreadD > 0 then yd := yd + Random(Data.FXSpreadD + 1);
+            MakeEffect(wx, wy, xd, yd,
+                       Data.FXType, Data.FXSubType,
+                       Data.FXColorR, Data.FXColorG, Data.FXColorB, True, False);
+            Dec(i);
+          end;
+          TimeOut := Data.FXWait;
+        end;
+    end;
+  end;
+
+  if Result and (Trigger.TexturePanel <> -1) then
+    g_Map_SwitchTexture(Trigger.TexturePanelType, Trigger.TexturePanel, IfThen(animonce, 2, 1));
+end;
+
+function g_Triggers_Create(Trigger: TTrigger): DWORD;
+var
+  find_id: DWORD;
+  fn, mapw: String;
+begin
+// Íå ñîçäàâàòü âûõîä, åñëè èãðà áåç âûõîäà:
+  if (Trigger.TriggerType = TRIGGER_EXIT) and
+     (not LongBool(gGameSettings.Options and GAME_OPTION_ALLOWEXIT)) then
+    Trigger.TriggerType := TRIGGER_NONE;
+
+// Åñëè ìîíñòðû çàïðåùåíû, îòìåíÿåì òðèããåð:
+  if (Trigger.TriggerType = TRIGGER_SPAWNMONSTER) and
+     (not LongBool(gGameSettings.Options and GAME_OPTION_MONSTERS)) and
+     (gGameSettings.GameType <> GT_SINGLE) then
+    Trigger.TriggerType := TRIGGER_NONE;
+
+// Ñ÷èòàåì êîëè÷åñòâî ñåêðåòîâ íà êàðòå:
+  if Trigger.TriggerType = TRIGGER_SECRET then
+    gSecretsCount := gSecretsCount + 1;
+
+  find_id := FindTrigger();
+  gTriggers[find_id] := Trigger;
+
+  with gTriggers[find_id] do
+  begin
+    ID := find_id;
+    // if this type of trigger exists both on the client and on the server
+    // use an uniform numeration
+    if Trigger.TriggerType = TRIGGER_SOUND then
+    begin
+      Inc(gTriggerClientID);
+      ClientID := gTriggerClientID;
+    end
+    else
+      ClientID := 0;
+    TimeOut := 0;
+    ActivateUID := 0;
+    PlayerCollide := False;
+    DoorTime := -1;
+    PressTime := -1;
+    PressCount := 0;
+    SoundPlayCount := 0;
+    Sound := nil;
+    AutoSpawn := False;
+    SpawnCooldown := 0;
+    SpawnedCount := 0;
+  end;
+
+// Çàãðóæàåì çâóê, åñëè ýòî òðèããåð "Çâóê":
+  if (Trigger.TriggerType = TRIGGER_SOUND) and
+     (Trigger.Data.SoundName <> '') then
+  begin
+  // Åùå íåò òàêîãî çâóêà:
+    if not g_Sound_Exists(Trigger.Data.SoundName) then
+    begin
+      g_ProcessResourceStr(Trigger.Data.SoundName, @fn, nil, nil);
+
+      if fn = '' then
+        begin // Çâóê â ôàéëå ñ êàðòîé
+          g_ProcessResourceStr(gMapInfo.Map, @mapw, nil, nil);
+          fn := mapw + Trigger.Data.SoundName;
+        end
+      else // Çâóê â îòäåëüíîì ôàéëå
+        fn := GameDir + '/wads/' + Trigger.Data.SoundName;
+
+      if not g_Sound_CreateWADEx(Trigger.Data.SoundName, fn) then
+        g_FatalError(Format(_lc[I_GAME_ERROR_TR_SOUND], [fn, Trigger.Data.SoundName]));
+    end;
+
+  // Ñîçäàåì îáúåêò çâóêà:
+    with gTriggers[find_id] do
+    begin
+      Sound := TPlayableSound.Create();
+      if not Sound.SetByName(Trigger.Data.SoundName) then
+      begin
+        Sound.Free();
+        Sound := nil;
+      end;
+    end;
+  end;
+
+// Çàãðóæàåì ìóçûêó, åñëè ýòî òðèããåð "Ìóçûêà":
+  if (Trigger.TriggerType = TRIGGER_MUSIC) and
+     (Trigger.Data.MusicName <> '') then
+  begin
+  // Åùå íåò òàêîé ìóçûêè:
+    if not g_Sound_Exists(Trigger.Data.MusicName) then
+    begin
+      g_ProcessResourceStr(Trigger.Data.MusicName, @fn, nil, nil);
+
+      if fn = '' then
+        begin // Ìóçûêà â ôàéëå ñ êàðòîé
+          g_ProcessResourceStr(gMapInfo.Map, @mapw, nil, nil);
+          fn := mapw + Trigger.Data.MusicName;
+        end
+      else // Ìóçûêà â ôàéëå ñ êàðòîé
+        fn := GameDir+'/wads/'+Trigger.Data.MusicName;
+
+      if not g_Sound_CreateWADEx(Trigger.Data.MusicName, fn, True) then
+        g_FatalError(Format(_lc[I_GAME_ERROR_TR_SOUND], [fn, Trigger.Data.MusicName]));
+    end;
+  end;
+
+// Çàãðóæàåì äàííûå òðèããåðà "Òóðåëü":
+  if Trigger.TriggerType = TRIGGER_SHOT then
+    with gTriggers[find_id] do
+    begin
+      ShotPanelTime := 0;
+      ShotSightTime := 0;
+      ShotSightTimeout := 0;
+      ShotSightTarget := 0;
+      ShotSightTargetN := 0;
+      ShotAmmoCount := Trigger.Data.ShotAmmo;
+      ShotReloadTime := 0;
+    end;
+
+  Result := find_id;
+end;
+
+procedure g_Triggers_Update();
+var
+  a, b, i: Integer;
+  Affected: array of Integer;
+begin
+  if gTriggers = nil then
+    Exit;
+  SetLength(Affected, 0);
+
+  for a := 0 to High(gTriggers) do
+    with gTriggers[a] do
+    // Åñòü òðèããåð:
+      if TriggerType <> TRIGGER_NONE then
+      begin
+      // Óìåíüøàåì âðåìÿ äî çàêðûòèÿ äâåðè (îòêðûòèÿ ëîâóøêè):
+        if DoorTime > 0 then
+          DoorTime := DoorTime - 1;
+      // Óìåíüøàåì âðåìÿ îæèäàíèÿ ïîñëå íàæàòèÿ:
+        if PressTime > 0 then
+          PressTime := PressTime - 1;
+      // Ïðîâåðÿåì èãðîêîâ è ìîíñòðîâ, êîòîðûõ ðàíåå çàïîìíèëè:
+        if (TriggerType = TRIGGER_DAMAGE) or (TriggerType = TRIGGER_HEALTH) then
+          for b := 0 to High(Activators) do
+          begin
+            // Óìåíüøàåì âðåìÿ äî ïîâòîðíîãî âîçäåéñòâèÿ:
+            if Activators[b].TimeOut > 0 then
+              Dec(Activators[b].TimeOut)
+            else
+              Continue;
+            // Ñ÷èòàåì, ÷òî îáúåêò ïîêèíóë çîíó äåéñòâèÿ òðèããåðà
+            if (Data.DamageInterval = 0) and (Activators[b].TimeOut < 65530) then
+              Activators[b].TimeOut := 0;
+          end;
+
+      // Îáðàáàòûâàåì ñïàâíåðû:
+        if Enabled and AutoSpawn then
+          if SpawnCooldown = 0 then
+          begin
+            // Åñëè ïðèøëî âðåìÿ, ñïàâíèì ìîíñòðà:
+            if (TriggerType = TRIGGER_SPAWNMONSTER) and (Data.MonDelay > 0)  then
+            begin
+              ActivateUID := 0;
+              ActivateTrigger(gTriggers[a], ACTIVATE_CUSTOM);
+            end;
+            // Åñëè ïðèøëî âðåìÿ, ñïàâíèì ïðåäìåò:
+            if (TriggerType = TRIGGER_SPAWNITEM) and (Data.ItemDelay > 0) then
+            begin
+              ActivateUID := 0;
+              ActivateTrigger(gTriggers[a], ACTIVATE_CUSTOM);
+            end;
+          end else // Óìåíüøàåì âðåìÿ îæèäàíèÿ:
+            Dec(SpawnCooldown);
+
+      // Îáðàáàòûâàåì ñîáûòèÿ òðèããåðà "Òóðåëü":
+        if TriggerType = TRIGGER_SHOT then
+        begin
+          if ShotPanelTime > 0 then
+          begin
+            Dec(ShotPanelTime);
+            if ShotPanelTime = 0 then
+              g_Map_SwitchTexture(ShotPanelType, Data.ShotPanelID);
+          end;
+          if ShotSightTime > 0 then
+          begin
+            Dec(ShotSightTime);
+            if ShotSightTime = 0 then
+              ShotSightTarget := ShotSightTargetN;
+          end;
+          if ShotSightTimeout > 0 then
+          begin
+            Dec(ShotSightTimeout);
+            if ShotSightTimeout = 0 then
+              ShotSightTarget := 0;
+          end;
+          if ShotReloadTime > 0 then
+          begin
+            Dec(ShotReloadTime);
+            if ShotReloadTime = 0 then
+              ShotAmmoCount := Data.ShotAmmo;
+          end;
+        end;
+
+      // Òðèããåð "Çâóê" óæå îòûãðàë, åñëè íóæíî åùå - ïåðåçàïóñêàåì:
+        if Enabled and (TriggerType = TRIGGER_SOUND) and (Sound <> nil) then
+          if (SoundPlayCount > 0) and (not Sound.IsPlaying()) then
+          begin
+            if Data.PlayCount > 0 then // Åñëè 0 - èãðàåì çâóê áåñêîíå÷íî
+              SoundPlayCount := SoundPlayCount - 1;
+            if Data.Local then
+              Sound.PlayVolumeAt(X+(Width div 2), Y+(Height div 2), Data.Volume/255.0)
+            else
+              Sound.PlayPanVolume((Data.Pan-127.0)/128.0, Data.Volume/255.0);
+            if Sound.IsPlaying() and g_Game_IsNet and g_Game_IsServer then
+              MH_SEND_TriggerSound(gTriggers[a]);
+          end;
+
+      // Òðèããåð "Ëîâóøêà" - ïîðà îòêðûâàòü:
+        if (TriggerType = TRIGGER_TRAP) and (DoorTime = 0) and (Data.PanelID <> -1) then
+        begin
+          OpenDoor(Data.PanelID, Data.NoSound, Data.d2d_doors);
+          DoorTime := -1;
+        end;
+
+      // Òðèããåð "Äâåðü 5 ñåê" - ïîðà çàêðûâàòü:
+        if (TriggerType = TRIGGER_DOOR5) and (DoorTime = 0) and (Data.PanelID <> -1) then
+        begin
+        // Óæå çàêðûòà:
+          if gWalls[Data.PanelID].Enabled then
+            DoorTime := -1
+          else // Ïîêà îòêðûòà - çàêðûâàåì
+            if CloseDoor(Data.PanelID, Data.NoSound, Data.d2d_doors) then
+              DoorTime := -1;
+        end;
+
+      // Òðèããåð - ðàñøèðèòåëü èëè ïåðåêëþ÷àòåëü, è ïðîøëà çàäåðæêà, è íàæàëè íóæíîå ÷èñëî ðàç:
+        if (TriggerType in [TRIGGER_PRESS, TRIGGER_ON, TRIGGER_OFF, TRIGGER_ONOFF]) and
+           (PressTime = 0) and (PressCount >= Data.Count) then
+        begin
+        // Ñáðàñûâàåì çàäåðæêó àêòèâàöèè:
+          PressTime := -1;
+        // Ñáðàñûâàåì ñ÷åò÷èê íàæàòèé:
+          if Data.Count > 0 then
+            PressCount := PressCount - Data.Count
+          else
+            PressCount := 0;
+
+        // Îïðåäåëÿåì èçìåíÿåìûå èì òðèããåðû:
+          for b := 0 to High(gTriggers) do
+            if g_Collide(Data.tX, Data.tY, Data.tWidth, Data.tHeight, gTriggers[b].X, gTriggers[b].Y,
+               gTriggers[b].Width, gTriggers[b].Height) and
+               ((b <> a) or (Data.Wait > 0)) then
+            begin // Can be self-activated, if there is Data.Wait
+              if (not Data.ExtRandom) or gTriggers[b].Enabled then
+              begin
+                SetLength(Affected, Length(Affected) + 1);
+                Affected[High(Affected)] := b;
+              end;
+            end;
+        // Âûáèðàåì îäèí èç òðèããåðîâ äëÿ ðàñøèðèòåëÿ, åñëè âêëþ÷åí ðàíäîì:
+          if (TriggerType = TRIGGER_PRESS) and Data.ExtRandom then
+          begin
+            if (Length(Affected) > 0) then
+            begin
+              b := Affected[Random(Length(Affected))];
+              gTriggers[b].ActivateUID := gTriggers[a].ActivateUID;
+              ActivateTrigger(gTriggers[b], 0);
+            end;
+          end
+          else // Â ïðîòèâíîì ñëó÷àå ðàáîòàåì êàê îáû÷íî:
+            for i := 0 to High(Affected) do
+            begin
+              b := Affected[i];
+              case TriggerType of
+                TRIGGER_PRESS:
+                  begin
+                    gTriggers[b].ActivateUID := gTriggers[a].ActivateUID;
+                    ActivateTrigger(gTriggers[b], 0);
+                  end;
+                TRIGGER_ON:
+                  begin
+                    gTriggers[b].Enabled := True;
+                  end;
+                TRIGGER_OFF:
+                  begin
+                    gTriggers[b].Enabled := False;
+                    gTriggers[b].TimeOut := 0;
+                    if gTriggers[b].AutoSpawn then
+                    begin
+                      gTriggers[b].AutoSpawn := False;
+                      gTriggers[b].SpawnCooldown := 0;
+                    end;
+                  end;
+                TRIGGER_ONOFF:
+                  begin
+                    gTriggers[b].Enabled := not gTriggers[b].Enabled;
+                    if not gTriggers[b].Enabled then
+                    begin
+                      gTriggers[b].TimeOut := 0;
+                      if gTriggers[b].AutoSpawn then
+                      begin
+                        gTriggers[b].AutoSpawn := False;
+                        gTriggers[b].SpawnCooldown := 0;
+                      end;
+                    end;
+                  end;
+              end;
+            end;
+          SetLength(Affected, 0);
+        end;
+
+      // Óìåíüøàåì âðåìÿ äî âîçìîæíîñòè ïîâòîðíîé àêòèâàöèè:
+        if TimeOut > 0 then
+        begin
+          TimeOut := TimeOut - 1;
+          Continue; // ×òîáû íå ïîòåðÿòü 1 åäèíèöó çàäåðæêè
+        end;
+
+      // Íèæå èäóò òèïû àêòèâàöèè, åñëè òðèããåð îòêëþ÷¸í - èä¸ì äàëüøå
+        if not Enabled then
+          Continue;
+
+      // "Èãðîê áëèçêî":
+        if ByteBool(ActivateType and ACTIVATE_PLAYERCOLLIDE) and
+           (TimeOut = 0) then
+          if gPlayers <> nil then
+            for b := 0 to High(gPlayers) do
+              if gPlayers[b] <> nil then
+                with gPlayers[b] do
+                // Æèâ, åñòü íóæíûå êëþ÷è è îí ðÿäîì:
+                  if Live and ((gTriggers[a].Keys and GetKeys) = gTriggers[a].Keys) and
+                     Collide(X, Y, Width, Height) then
+                  begin
+                    gTriggers[a].ActivateUID := UID;
+
+                    if (gTriggers[a].TriggerType in [TRIGGER_SOUND, TRIGGER_MUSIC]) and
+                       PlayerCollide then
+                      { Don't activate sound/music again if player is here }
+                    else
+                      ActivateTrigger(gTriggers[a], ACTIVATE_PLAYERCOLLIDE);
+                  end;
+
+        { TODO 5 : àêòèâàöèÿ ìîíñòðàìè òðèããåðîâ ñ êëþ÷àìè }
+
+        if ByteBool(ActivateType and ACTIVATE_MONSTERCOLLIDE) and
+           ByteBool(ActivateType and ACTIVATE_NOMONSTER) and
+           (TimeOut = 0) and (Keys = 0) then
+        begin
+        // Åñëè "Ìîíñòð áëèçêî" è "Ìîíñòðîâ íåò",
+        // çàïóñêàåì òðèããåð íà ñòàðòå êàðòû è ñíèìàåì îáà ôëàãà
+          ActivateType := ActivateType and not (ACTIVATE_MONSTERCOLLIDE or ACTIVATE_NOMONSTER);
+          gTriggers[a].ActivateUID := 0;
+          ActivateTrigger(gTriggers[a], 0);
+        end else
+        begin
+        // "Ìîíñòð áëèçêî":
+          if ByteBool(ActivateType and ACTIVATE_MONSTERCOLLIDE) and
+             (TimeOut = 0) and (Keys = 0) then // Åñëè íå íóæíû êëþ÷è
+            if gMonsters <> nil then
+              for b := 0 to High(gMonsters) do
+                if (gMonsters[b] <> nil) then
+                  with gMonsters[b] do
+                    if Collide(X, Y, Width, Height) then
+                    begin
+                      gTriggers[a].ActivateUID := UID;
+                      ActivateTrigger(gTriggers[a], ACTIVATE_MONSTERCOLLIDE);
+                    end;
+
+        // "Ìîíñòðîâ íåò":
+          if ByteBool(ActivateType and ACTIVATE_NOMONSTER) and
+             (TimeOut = 0) and (Keys = 0) then
+            if not g_CollideMonster(X, Y, Width, Height) then
+            begin
+              gTriggers[a].ActivateUID := 0;
+              ActivateTrigger(gTriggers[a], ACTIVATE_NOMONSTER);
+            end;
+        end;
+
+        PlayerCollide := g_CollidePlayer(X, Y, Width, Height);
+      end;
+end;
+
+procedure g_Triggers_Press(ID: DWORD; ActivateType: Byte);
+begin
+  gTriggers[ID].ActivateUID := 0;
+  ActivateTrigger(gTriggers[ID], ActivateType);
+end;
+
+function g_Triggers_PressR(X, Y: Integer; Width, Height: Word; UID: Word;
+                           ActivateType: Byte; IgnoreList: DWArray = nil): DWArray;
+var
+  a: Integer;
+  k: Byte;
+  p: TPlayer;
+begin
+  if gTriggers = nil then Exit;
+
+  case g_GetUIDType(UID) of
+    UID_GAME: k := 255;
+    UID_PLAYER:
+    begin
+      p := g_Player_Get(UID);
+      if p <> nil then
+        k := p.GetKeys
+      else
+        k := 0;
+    end;
+    else k := 0;
+  end;
+
+  Result := nil;
+
+  for a := 0 to High(gTriggers) do
+    if (gTriggers[a].TriggerType <> TRIGGER_NONE) and
+       (gTriggers[a].TimeOut = 0) and
+       (not InDWArray(a, IgnoreList)) and
+       ((gTriggers[a].Keys and k) = gTriggers[a].Keys) and
+       ByteBool(gTriggers[a].ActivateType and ActivateType) then
+      if g_Collide(X, Y, Width, Height,
+         gTriggers[a].X, gTriggers[a].Y,
+         gTriggers[a].Width, gTriggers[a].Height) then
+      begin
+        gTriggers[a].ActivateUID := UID;
+        if ActivateTrigger(gTriggers[a], ActivateType) then
+        begin
+          SetLength(Result, Length(Result)+1);
+          Result[High(Result)] := a;
+        end;
+      end;
+end;
+
+procedure g_Triggers_PressL(X1, Y1, X2, Y2: Integer; UID: DWORD; ActivateType: Byte);
+var
+  a: Integer;
+  k: Byte;
+  p: TPlayer;
+begin
+  if gTriggers = nil then Exit;
+
+  case g_GetUIDType(UID) of
+    UID_GAME: k := 255;
+    UID_PLAYER:
+    begin
+      p := g_Player_Get(UID);
+      if p <> nil then
+        k := p.GetKeys
+      else
+        k := 0;
+    end;
+    else k := 0;
+  end;
+
+  for a := 0 to High(gTriggers) do
+    if (gTriggers[a].TriggerType <> TRIGGER_NONE) and
+       (gTriggers[a].TimeOut = 0) and
+       ((gTriggers[a].Keys and k) = gTriggers[a].Keys) and
+       ByteBool(gTriggers[a].ActivateType and ActivateType) then
+      if g_CollideLine(x1, y1, x2, y2, gTriggers[a].X, gTriggers[a].Y,
+         gTriggers[a].Width, gTriggers[a].Height) then
+      begin
+        gTriggers[a].ActivateUID := UID;
+        ActivateTrigger(gTriggers[a], ActivateType);
+      end;
+end;
+
+procedure g_Triggers_PressC(CX, CY: Integer; Radius: Word; UID: Word; ActivateType: Byte);
+var
+  a: Integer;
+  k: Byte;
+  rsq: Word;
+  p: TPlayer;
+begin
+  if gTriggers = nil then
+    Exit;
+
+  case g_GetUIDType(UID) of
+    UID_GAME: k := 255;
+    UID_PLAYER:
+    begin
+     p := g_Player_Get(UID);
+     if p <> nil then
+      k := p.GetKeys
+     else
+      k := 0;
+    end;
+    else k := 0;
+  end;
+
+  rsq := Radius * Radius;
+
+  for a := 0 to High(gTriggers) do
+    if (gTriggers[a].TriggerType <> TRIGGER_NONE) and
+       (gTriggers[a].TimeOut = 0) and
+       ((gTriggers[a].Keys and k) = gTriggers[a].Keys) and
+       ByteBool(gTriggers[a].ActivateType and ActivateType) then
+      with gTriggers[a] do
+        if g_Collide(CX-Radius, CY-Radius, 2*Radius, 2*Radius,
+                     X, Y, Width, Height) then
+          if ((Sqr(CX-X)+Sqr(CY-Y)) < rsq) or // Öåíòð êðóãà áëèçîê ê âåðõíåìó ëåâîìó óãëó
+             ((Sqr(CX-X-Width)+Sqr(CY-Y)) < rsq) or // Öåíòð êðóãà áëèçîê ê âåðõíåìó ïðàâîìó óãëó
+             ((Sqr(CX-X-Width)+Sqr(CY-Y-Height)) < rsq) or // Öåíòð êðóãà áëèçîê ê íèæíåìó ïðàâîìó óãëó
+             ((Sqr(CX-X)+Sqr(CY-Y-Height)) < rsq) or // Öåíòð êðóãà áëèçîê ê íèæíåìó ëåâîìó óãëó
+             ( (CX > (X-Radius)) and (CX < (X+Width+Radius)) and
+               (CY > Y) and (CY < (Y+Height)) ) or // Öåíòð êðóãà íåäàëåêî îò âåðòèêàëüíûõ ãðàíèö ïðÿìîóãîëüíèêà
+             ( (CY > (Y-Radius)) and (CY < (Y+Height+Radius)) and
+               (CX > X) and (CX < (X+Width)) ) then // Öåíòð êðóãà íåäàëåêî îò ãîðèçîíòàëüíûõ ãðàíèö ïðÿìîóãîëüíèêà
+          begin
+            ActivateUID := UID;
+            ActivateTrigger(gTriggers[a], ActivateType);
+          end;
+end;
+
+procedure g_Triggers_OpenAll();
+var
+  a: Integer;
+  b: Boolean;
+begin
+  if gTriggers = nil then Exit;
+
+  b := False;
+  for a := 0 to High(gTriggers) do
+    with gTriggers[a] do
+      if (TriggerType = TRIGGER_OPENDOOR) or
+         (TriggerType = TRIGGER_DOOR5) or
+         (TriggerType = TRIGGER_DOOR) then
+      begin
+        OpenDoor(Data.PanelID, True, Data.d2d_doors);
+        if TriggerType = TRIGGER_DOOR5 then DoorTime := 180;
+        b := True;
+      end;
+
+  if b then g_Sound_PlayEx('SOUND_GAME_DOOROPEN');
+end;
+
+procedure g_Triggers_DecreaseSpawner(ID: DWORD);
+begin
+  if (gTriggers <> nil) then
+    if gTriggers[ID].SpawnedCount > 0 then
+      Dec(gTriggers[ID].SpawnedCount);
+end;
+
+procedure g_Triggers_Free();
+var
+  a: Integer;
+begin
+  if gTriggers <> nil then
+    for a := 0 to High(gTriggers) do
+    begin
+      if gTriggers[a].TriggerType = TRIGGER_SOUND then
+      begin
+        if g_Sound_Exists(gTriggers[a].Data.SoundName) then
+          g_Sound_Delete(gTriggers[a].Data.SoundName);
+
+        gTriggers[a].Sound.Free();
+      end;
+      if gTriggers[a].Activators <> nil then
+        SetLength(gTriggers[a].Activators, 0);
+    end;
+
+  gTriggers := nil;
+  gSecretsCount := 0;
+  SetLength(gMonstersSpawned, 0);
+end;
+
+procedure g_Triggers_SaveState(var Mem: TBinMemoryWriter);
+var
+  count, act_count, i, j: Integer;
+  dw: DWORD;
+  sg: Single;
+  b: Boolean;
+  p: Pointer;
+begin
+// Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ òðèããåðîâ:
+  count := 0;
+  if gTriggers <> nil then
+    for i := 0 to High(gTriggers) do
+      count := count + 1;
+
+  Mem := TBinMemoryWriter.Create((count+1) * 200);
+
+// Êîëè÷åñòâî òðèããåðîâ:
+  Mem.WriteInt(count);
+
+  if count = 0 then
+    Exit;
+
+  for i := 0 to High(gTriggers) do
+  begin
+  // Ñèãíàòóðà òðèããåðà:
+    dw := TRIGGER_SIGNATURE; // 'TRGR'
+    Mem.WriteDWORD(dw);
+  // Òèï òðèããåðà:
+    Mem.WriteByte(gTriggers[i].TriggerType);
+  // Ñïåöèàëüíûå äàííûå òðèããåðà:
+    p := @gTriggers[i].Data;
+    Mem.WriteMemory(p, SizeOf(TTriggerData));
+  // Êîîðäèíàòû ëåâîãî âåðõíåãî óãëà:
+    Mem.WriteInt(gTriggers[i].X);
+    Mem.WriteInt(gTriggers[i].Y);
+  // Ðàçìåðû:
+    Mem.WriteWord(gTriggers[i].Width);
+    Mem.WriteWord(gTriggers[i].Height);
+  // Âêëþ÷åí ëè òðèããåð:
+    Mem.WriteBoolean(gTriggers[i].Enabled);
+  // Òèï àêòèâàöèè òðèããåðà:
+    Mem.WriteByte(gTriggers[i].ActivateType);
+  // Êëþ÷è, íåîáõîäèìûå äëÿ àêòèâàöèè:
+    Mem.WriteByte(gTriggers[i].Keys);
+  // ID ïàíåëè, òåêñòóðà êîòîðîé èçìåíèòñÿ:
+    Mem.WriteInt(gTriggers[i].TexturePanel);
+  // Òèï ýòîé ïàíåëè:
+    Mem.WriteWord(gTriggers[i].TexturePanelType);
+  // Âðåìÿ äî âîçìîæíîñòè àêòèâàöèè:
+    Mem.WriteWord(gTriggers[i].TimeOut);
+  // UID òîãî, êòî àêòèâèðîâàë ýòîò òðèããåð:
+    Mem.WriteWord(gTriggers[i].ActivateUID);
+  // Ñïèñîê UID-îâ îáúåêòîâ, êîòîðûå íàõîäèëèñü ïîä âîçäåéñòâèåì:
+    act_count := Length(gTriggers[i].Activators);
+    Mem.WriteInt(act_count);
+    for j := 0 to act_count-1 do
+    begin
+      // UID îáúåêòà
+      Mem.WriteWord(gTriggers[i].Activators[j].UID);
+      // Âðåìÿ îæèäàíèÿ
+      Mem.WriteWord(gTriggers[i].Activators[j].TimeOut);
+    end;
+  // Ñòîèò ëè èãðîê â îáëàñòè òðèããåðà:
+    Mem.WriteBoolean(gTriggers[i].PlayerCollide);
+  // Âðåìÿ äî çàêðûòèÿ äâåðè:
+    Mem.WriteInt(gTriggers[i].DoorTime);
+  // Çàäåðæêà àêòèâàöèè:
+    Mem.WriteInt(gTriggers[i].PressTime);
+  // Ñ÷åò÷èê íàæàòèé:
+    Mem.WriteInt(gTriggers[i].PressCount);
+  // Ñïàâíåð àêòèâåí:
+    Mem.WriteBoolean(gTriggers[i].AutoSpawn);
+  // Çàäåðæêà ñïàâíåðà:
+    Mem.WriteInt(gTriggers[i].SpawnCooldown);
+  // Ñ÷åò÷èê ñîçäàíèÿ îáúåêòîâ:
+    Mem.WriteInt(gTriggers[i].SpawnedCount);
+  // Ñêîëüêî ðàç ïðîèãðàí çâóê:
+    Mem.WriteInt(gTriggers[i].SoundPlayCount);
+  // Ïðîèãðûâàåòñÿ ëè çâóê?
+    if gTriggers[i].Sound <> nil then
+      b := gTriggers[i].Sound.IsPlaying()
+    else
+      b := False;
+    Mem.WriteBoolean(b);
+    if b then
+    begin
+    // Ïîçèöèÿ ïðîèãðûâàíèÿ çâóêà:
+      dw := gTriggers[i].Sound.GetPosition();
+      Mem.WriteDWORD(dw);
+    // Ãðîìêîñòü çâóêà:
+      sg := gTriggers[i].Sound.GetVolume();
+      sg := sg / (gSoundLevel/255.0);
+      Mem.WriteSingle(sg);
+    // Ñòåðåî ñìåùåíèå çâóêà:
+      sg := gTriggers[i].Sound.GetPan();
+      Mem.WriteSingle(sg);
+    end;
+  end;
+end;
+
+procedure g_Triggers_LoadState(var Mem: TBinMemoryReader);
+var
+  count, act_count, i, j, a: Integer;
+  dw: DWORD;
+  vol, pan: Single;
+  b: Boolean;
+  p: Pointer;
+  Trig: TTrigger;
+begin
+  if Mem = nil then
+    Exit;
+
+  g_Triggers_Free();
+
+// Êîëè÷åñòâî òðèããåðîâ:
+  Mem.ReadInt(count);
+
+  if count = 0 then
+    Exit;
+
+  for a := 0 to count-1 do
+  begin
+  // Ñèãíàòóðà òðèããåðà:
+    Mem.ReadDWORD(dw);
+    if dw <> TRIGGER_SIGNATURE then // 'TRGR'
+    begin
+      raise EBinSizeError.Create('g_Triggers_LoadState: Wrong Trigger Signature');
+    end;
+  // Òèï òðèããåðà:
+    Mem.ReadByte(Trig.TriggerType);
+  // Ñïåöèàëüíûå äàííûå òðèããåðà:
+    Mem.ReadMemory(p, dw);
+    if dw <> SizeOf(TTriggerData) then
+    begin
+      raise EBinSizeError.Create('g_Triggers_LoadState: Wrong TriggerData Size');
+    end;
+    Trig.Data := TTriggerData(p^);
+  // Ñîçäàåì òðèããåð:
+    i := g_Triggers_Create(Trig);
+  // Êîîðäèíàòû ëåâîãî âåðõíåãî óãëà:
+    Mem.ReadInt(gTriggers[i].X);
+    Mem.ReadInt(gTriggers[i].Y);
+  // Ðàçìåðû:
+    Mem.ReadWord(gTriggers[i].Width);
+    Mem.ReadWord(gTriggers[i].Height);
+  // Âêëþ÷åí ëè òðèããåð:
+    Mem.ReadBoolean(gTriggers[i].Enabled);
+  // Òèï àêòèâàöèè òðèããåðà:
+    Mem.ReadByte(gTriggers[i].ActivateType);
+  // Êëþ÷è, íåîáõîäèìûå äëÿ àêòèâàöèè:
+    Mem.ReadByte(gTriggers[i].Keys);
+  // ID ïàíåëè, òåêñòóðà êîòîðîé èçìåíèòñÿ:
+    Mem.ReadInt(gTriggers[i].TexturePanel);
+  // Òèï ýòîé ïàíåëè:
+    Mem.ReadWord(gTriggers[i].TexturePanelType);
+  // Âðåìÿ äî âîçìîæíîñòè àêòèâàöèè:
+    Mem.ReadWord(gTriggers[i].TimeOut);
+  // UID òîãî, êòî àêòèâèðîâàë ýòîò òðèããåð:
+    Mem.ReadWord(gTriggers[i].ActivateUID);
+  // Ñïèñîê UID-îâ îáúåêòîâ, êîòîðûå íàõîäèëèñü ïîä âîçäåéñòâèåì:
+    Mem.ReadInt(act_count);
+    if act_count > 0 then
+    begin
+      SetLength(gTriggers[i].Activators, act_count);
+      for j := 0 to act_count-1 do
+      begin
+        // UID îáúåêòà
+        Mem.ReadWord(gTriggers[i].Activators[j].UID);
+        // Âðåìÿ îæèäàíèÿ
+        Mem.ReadWord(gTriggers[i].Activators[j].TimeOut);
+      end;
+    end;
+  // Ñòîèò ëè èãðîê â îáëàñòè òðèããåðà:
+    Mem.ReadBoolean(gTriggers[i].PlayerCollide);
+  // Âðåìÿ äî çàêðûòèÿ äâåðè:
+    Mem.ReadInt(gTriggers[i].DoorTime);
+  // Çàäåðæêà àêòèâàöèè:
+    Mem.ReadInt(gTriggers[i].PressTime);
+  // Ñ÷åò÷èê íàæàòèé:
+    Mem.ReadInt(gTriggers[i].PressCount);
+  // Ñïàâíåð àêòèâåí:
+    Mem.ReadBoolean(gTriggers[i].AutoSpawn);
+  // Çàäåðæêà ñïàâíåðà:
+    Mem.ReadInt(gTriggers[i].SpawnCooldown);
+  // Ñ÷åò÷èê ñîçäàíèÿ îáúåêòîâ:
+    Mem.ReadInt(gTriggers[i].SpawnedCount);
+  // Ñêîëüêî ðàç ïðîèãðàí çâóê:
+    Mem.ReadInt(gTriggers[i].SoundPlayCount);
+  // Ïðîèãðûâàåòñÿ ëè çâóê?
+    Mem.ReadBoolean(b);
+    if b then
+    begin
+    // Ïîçèöèÿ ïðîèãðûâàíèÿ çâóêà:
+      Mem.ReadDWORD(dw);
+    // Ãðîìêîñòü çâóêà:
+      Mem.ReadSingle(vol);
+    // Ñòåðåî ñìåùåíèå çâóêà:
+      Mem.ReadSingle(pan);
+    // Çàïóñêàåì çâóê, åñëè åñòü:
+      if gTriggers[i].Sound <> nil then
+      begin
+        gTriggers[i].Sound.PlayPanVolume(pan, vol);
+        gTriggers[i].Sound.Pause(True);
+        gTriggers[i].Sound.SetPosition(dw);
+      end
+    end;
+  end;
+end;
+
+end.
diff --git a/src/game/g_weapons.pas b/src/game/g_weapons.pas
new file mode 100644 (file)
index 0000000..6f6b60b
--- /dev/null
@@ -0,0 +1,2210 @@
+unit g_weapons;
+
+interface
+
+uses
+  g_textures, g_basic, e_graphics, g_phys, BinEditor;
+
+const
+  HIT_SOME    = 0;
+  HIT_ROCKET  = 1;
+  HIT_BFG     = 2;
+  HIT_TRAP    = 3;
+  HIT_FALL    = 4;
+  HIT_WATER   = 5;
+  HIT_ACID    = 6;
+  HIT_ELECTRO = 7;
+  HIT_FLAME   = 8;
+  HIT_SELF    = 9;
+  HIT_DISCON  = 10;
+
+type
+  TShot = record
+    ShotType: Byte;
+    Target: Word;
+    SpawnerUID: Word;
+    Triggers: DWArray;
+    Obj: TObj;
+    Animation: TAnimation;
+    TextureID: DWORD;
+    Timeout: DWORD;
+  end;
+
+var
+  Shots: array of TShot = nil;
+  LastShotID: Integer = 0;
+
+procedure g_Weapon_LoadData();
+procedure g_Weapon_FreeData();
+procedure g_Weapon_Init();
+procedure g_Weapon_Free();
+function g_Weapon_Hit(obj: PObj; d: Integer; SpawnerUID: Word; t: Byte; HitCorpses: Boolean = True): Byte;
+function g_Weapon_HitUID(UID: Word; d: Integer; SpawnerUID: Word; t: Byte): Boolean;
+function g_Weapon_CreateShot(I: Integer; ShotType: Byte; Spawner, TargetUID: Word; X, Y, XV, YV: Integer): LongWord;
+
+procedure g_Weapon_gun(x, y, xd, yd, v, dmg: Integer; SpawnerUID: Word; CheckTrigger: Boolean);
+procedure g_Weapon_punch(x, y: Integer; d, SpawnerUID: Word);
+function g_Weapon_chainsaw(x, y: Integer; d, SpawnerUID: Word): Integer;
+procedure g_Weapon_rocket(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_revf(x, y, xd, yd: Integer; SpawnerUID, TargetUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_plasma(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_ball1(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_ball2(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_ball7(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_aplasma(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_manfire(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_bfgshot(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1; Silent: Boolean = False);
+procedure g_Weapon_bfghit(x, y: Integer);
+procedure g_Weapon_pistol(x, y, xd, yd: Integer; SpawnerUID: Word; Silent: Boolean = False);
+procedure g_Weapon_mgun(x, y, xd, yd: Integer; SpawnerUID: Word; Silent: Boolean = False);
+procedure g_Weapon_shotgun(x, y, xd, yd: Integer; SpawnerUID: Word; Silent: Boolean = False);
+procedure g_Weapon_dshotgun(x, y, xd, yd: Integer; SpawnerUID: Word; Silent: Boolean = False);
+
+function g_Weapon_Explode(X, Y: Integer; rad: Integer; SpawnerUID: Word): Boolean;
+procedure g_Weapon_BFG9000(X, Y: Integer; SpawnerUID: Word);
+procedure g_Weapon_Update();
+procedure g_Weapon_Draw();
+function g_Weapon_Danger(UID: Word; X, Y: Integer; Width, Height: Word; Time: Byte): Boolean;
+procedure g_Weapon_DestroyShot(I: Integer; X, Y: Integer; Loud: Boolean = True);
+
+procedure g_Weapon_SaveState(var Mem: TBinMemoryWriter);
+procedure g_Weapon_LoadState(var Mem: TBinMemoryReader);
+
+const
+  WEAPON_KASTET         = 0;
+  WEAPON_SAW            = 1;
+  WEAPON_PISTOL         = 2;
+  WEAPON_SHOTGUN1       = 3;
+  WEAPON_SHOTGUN2       = 4;
+  WEAPON_CHAINGUN       = 5;
+  WEAPON_ROCKETLAUNCHER = 6;
+  WEAPON_PLASMA         = 7;
+  WEAPON_BFG            = 8;
+  WEAPON_SUPERPULEMET   = 9;
+  WEAPON_MEGAKASTET     = 10;
+  WEAPON_ZOMBY_PISTOL   = 20;
+  WEAPON_IMP_FIRE       = 21;
+  WEAPON_BSP_FIRE       = 22;
+  WEAPON_CACO_FIRE      = 23;
+  WEAPON_BARON_FIRE     = 24;
+  WEAPON_MANCUB_FIRE    = 25;
+  WEAPON_SKEL_FIRE      = 26;
+
+implementation
+
+uses
+  Math, g_map, g_player, g_gfx, g_sound, g_main,
+  g_console, SysUtils, g_options, g_game,
+  g_triggers, MAPDEF, e_log, g_monsters, g_saveload,
+  g_language, g_netmsg;
+
+type
+  TWaterPanel = record
+    X, Y: Integer;
+    Width, Height: Word;
+    Active: Boolean;
+  end;
+
+const
+  SHOT_ROCKETLAUNCHER_WIDTH = 27;
+  SHOT_ROCKETLAUNCHER_HEIGHT = 12;
+
+  SHOT_SKELFIRE_WIDTH = 32;
+  SHOT_SKELFIRE_HEIGHT = 16;
+
+  SHOT_PLASMA_WIDTH = 16;
+  SHOT_PLASMA_HEIGHT = 16;
+
+  SHOT_BFG_WIDTH = 32;
+  SHOT_BFG_HEIGHT = 32;
+  SHOT_BFG_DAMAGE = 100;
+  SHOT_BFG_RADIUS = 256;
+
+  SHOT_SIGNATURE = $544F4853; // 'SHOT'
+
+var
+  WaterMap: array of array of DWORD = nil;
+
+function FindShot(): DWORD;
+var
+  i: Integer;
+begin
+  if Shots <> nil then
+  for i := 0 to High(Shots) do
+    if Shots[i].ShotType = 0 then
+    begin
+      Result := i;
+      LastShotID := Result;
+      Exit;
+    end;
+
+  if Shots = nil then
+  begin
+    SetLength(Shots, 128);
+    Result := 0;
+  end
+  else
+  begin
+    Result := High(Shots) + 1;
+    SetLength(Shots, Length(Shots) + 128);
+  end;
+  LastShotID := Result;
+end;
+
+procedure CreateWaterMap();
+var
+  WaterArray: Array of TWaterPanel;
+  a, b, c, m: Integer;
+  ok: Boolean;
+begin
+  if gWater = nil then
+    Exit;
+
+  SetLength(WaterArray, Length(gWater));
+
+  for a := 0 to High(gWater) do
+  begin
+    WaterArray[a].X := gWater[a].X;
+    WaterArray[a].Y := gWater[a].Y;
+    WaterArray[a].Width := gWater[a].Width;
+    WaterArray[a].Height := gWater[a].Height;
+    WaterArray[a].Active := True;
+  end;
+
+  g_Game_SetLoadingText(_lc[I_LOAD_WATER_MAP], High(WaterArray), False);
+
+  for a := 0 to High(WaterArray) do
+    if WaterArray[a].Active then
+    begin
+      WaterArray[a].Active := False;
+      m := Length(WaterMap);
+      SetLength(WaterMap, m+1);
+      SetLength(WaterMap[m], 1);
+      WaterMap[m][0] := a;
+      ok := True;
+
+      while ok do
+      begin
+        ok := False;
+        for b := 0 to High(WaterArray) do
+          if WaterArray[b].Active then
+            for c := 0 to High(WaterMap[m]) do
+              if g_CollideAround(WaterArray[b].X,
+                                 WaterArray[b].Y,
+                                 WaterArray[b].Width,
+                                 WaterArray[b].Height,
+                                 WaterArray[WaterMap[m][c]].X,
+                                 WaterArray[WaterMap[m][c]].Y,
+                                 WaterArray[WaterMap[m][c]].Width,
+                                 WaterArray[WaterMap[m][c]].Height) then
+              begin
+                WaterArray[b].Active := False;
+                SetLength(WaterMap[m],
+                          Length(WaterMap[m])+1);
+                WaterMap[m][High(WaterMap[m])] := b;
+                ok := True;
+                Break;
+              end;
+      end;
+
+      g_Game_StepLoading();
+    end;
+
+  WaterArray := nil;
+end;
+
+procedure CheckTrap(ID: DWORD; dm: Integer; t: Byte);
+var
+  a, b, c, d, i1, i2: Integer;
+  pl, mn: WArray;
+begin
+  if (gWater = nil) or (WaterMap = nil) then Exit;
+
+  i1 := -1;
+  i2 := -1;
+
+  SetLength(pl, 1024);
+  SetLength(mn, 1024);
+  for d := 0 to 1023 do pl[d] := $FFFF;
+  for d := 0 to 1023 do mn[d] := $FFFF;
+
+  for a := 0 to High(WaterMap) do
+    for b := 0 to High(WaterMap[a]) do
+    begin
+      if not g_Obj_Collide(gWater[WaterMap[a][b]].X, gWater[WaterMap[a][b]].Y,
+                           gWater[WaterMap[a][b]].Width, gWater[WaterMap[a][b]].Height,
+                           @Shots[ID].Obj) then Continue;
+
+      for c := 0 to High(WaterMap[a]) do
+      begin
+        if gPlayers <> nil then
+        begin
+          for d := 0 to High(gPlayers) do
+            if (gPlayers[d] <> nil) and (gPlayers[d].Live) then
+              if gPlayers[d].Collide(gWater[WaterMap[a][c]]) then
+                if not InWArray(d, pl) then
+                  if i1 < 1023 then
+                  begin
+                    i1 := i1+1;
+                    pl[i1] := d;
+                  end;
+        end;
+
+        if gMonsters <> nil then
+        begin
+          for d := 0 to High(gMonsters) do
+            if (gMonsters[d] <> nil) and (gMonsters[d].Live) then
+              if gMonsters[d].Collide(gWater[WaterMap[a][c]]) then
+                if not InWArray(d, mn) then
+                  if i2 < 1023 then
+                  begin
+                    i2 := i2+1;
+                    mn[i2] := d;
+                  end;
+        end;
+      end;
+
+      if i1 <> -1 then
+        for d := 0 to i1 do
+          gPlayers[pl[d]].Damage(dm, Shots[ID].SpawnerUID, 0, 0, t);
+
+      if i2 <> -1 then
+        for d := 0 to i2 do
+          gMonsters[mn[d]].Damage(dm, 0, 0, Shots[ID].SpawnerUID, t);
+    end;
+
+  pl := nil;
+  mn := nil;
+end;
+
+function HitMonster(m: TMonster; d: Integer; vx, vy: Integer; SpawnerUID: Word; t: Byte): Boolean;
+var
+  tt, mt: Byte;
+  mon: TMonster;
+begin
+  Result := False;
+
+  tt := g_GetUIDType(SpawnerUID);
+  if tt = UID_MONSTER then
+  begin
+    mon := g_Monsters_Get(SpawnerUID);
+    if mon <> nil then
+      mt := g_Monsters_Get(SpawnerUID).MonsterType
+    else
+      mt := 0;
+  end
+  else
+    mt := 0;
+
+  if m = nil then Exit;
+  if m.UID = SpawnerUID then
+  begin
+  // Ñàì ñåáÿ ìîæåò ðàíèòü òîëüêî ðàêåòîé è òîêîì:
+    if (t <> HIT_ROCKET) and (t <> HIT_ELECTRO) then
+      Exit;
+  // Êèáåð äåìîí è áî÷êà âîîáùå íå ìîãóò ñåáÿ ðàíèòü:
+    if (m.MonsterType = MONSTER_CYBER) or
+       (m.MonsterType = MONSTER_BARREL) then
+    begin
+      Result := True;
+      Exit;
+    end;
+  end;
+
+  if tt = UID_MONSTER then
+  begin
+  // Lost_Soul íå ìîæåò ðàíèòü Pain_Elemental'à:
+    if (mt = MONSTER_SOUL) and (m.MonsterType = MONSTER_PAIN) then
+      Exit;
+
+  // Îáà ìîíñòðà îäíîãî âèäà:
+    if mt = m.MonsterType then
+      case mt of
+        MONSTER_IMP, MONSTER_DEMON, MONSTER_BARON, MONSTER_KNIGHT, MONSTER_CACO,
+        MONSTER_SOUL, MONSTER_MANCUB, MONSTER_SKEL, MONSTER_FISH:
+          Exit; // Ýòè íå áüþò ñâîèõ
+      end;
+  end;
+
+  if g_Game_IsServer then
+    Result := m.Damage(d, vx, vy, SpawnerUID, t)
+  else
+    Result := True;
+end;
+
+function HitPlayer(p: TPlayer; d: Integer; vx, vy: Integer; SpawnerUID: Word; t: Byte): Boolean;
+begin
+  Result := False;
+
+// Ñàì ñåáÿ ìîæåò ðàíèòü òîëüêî ðàêåòîé è òîêîì:
+  if (p.UID = SpawnerUID) and (t <> HIT_ROCKET) and (t <> HIT_ELECTRO) then
+    Exit;
+
+  if g_Game_IsServer then p.Damage(d, SpawnerUID, vx, vy, t);
+
+  Result := True;
+end;
+
+function GunHit(X, Y: Integer; vx, vy: Integer; dmg: Integer;
+  SpawnerUID: Word; AllowPush: Boolean): Byte;
+var
+  i, h: Integer;
+begin
+  Result := 0;
+
+  h := High(gPlayers);
+
+  if h <> -1 then
+    for i := 0 to h do
+      if (gPlayers[i] <> nil) and gPlayers[i].Live and gPlayers[i].Collide(X, Y) then
+        if HitPlayer(gPlayers[i], dmg, vx*10, vy*10-3, SpawnerUID, HIT_SOME) then
+        begin
+          if AllowPush then gPlayers[i].Push(vx, vy);
+          Result := 1;
+        end;
+
+  if Result <> 0 then Exit;
+
+  h := High(gMonsters);
+
+  if h <> -1 then
+    for i := 0 to h do
+      if (gMonsters[i] <> nil) and gMonsters[i].Live and gMonsters[i].Collide(X, Y) then
+        if HitMonster(gMonsters[i], dmg, vx*10, vy*10-3, SpawnerUID, HIT_SOME) then
+        begin
+          if AllowPush then gMonsters[i].Push(vx, vy);
+          Result := 2;
+          Exit;
+        end;
+end;
+
+procedure g_Weapon_BFG9000(X, Y: Integer; SpawnerUID: Word);
+var
+  i, h: Integer;
+  st: Byte;
+  pl: TPlayer;
+  b: Boolean;
+begin
+  //g_Sound_PlayEx('SOUND_WEAPON_EXPLODEBFG', 255);
+
+  h := High(gCorpses);
+
+  if gAdvCorpses and (h <> -1) then
+    for i := 0 to h do
+      if (gCorpses[i] <> nil) and (gCorpses[i].State <> CORPSE_STATE_REMOVEME) then
+        with gCorpses[i] do
+          if (g_PatchLength(X, Y, Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2),
+                            Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)) <= SHOT_BFG_RADIUS) and
+              g_TraceVector(X, Y, Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2),
+                            Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)) then
+          begin
+            Damage(50, 0, 0);
+            g_Weapon_BFGHit(Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2),
+                            Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2));
+          end;
+
+  st := TEAM_NONE;
+  pl := g_Player_Get(SpawnerUID);
+  if pl <> nil then
+    st := pl.Team;
+
+  h := High(gPlayers);
+
+  if h <> -1 then
+    for i := 0 to h do
+      if (gPlayers[i] <> nil) and (gPlayers[i].Live) and (gPlayers[i].UID <> SpawnerUID) then
+        with gPlayers[i] do
+          if (g_PatchLength(X, Y, GameX+PLAYER_RECT.X+(PLAYER_RECT.Width div 2),
+                            GameY+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)) <= SHOT_BFG_RADIUS) and
+              g_TraceVector(X, Y, GameX+PLAYER_RECT.X+(PLAYER_RECT.Width div 2),
+                            GameY+PLAYER_RECT.Y+(PLAYER_RECT.Height div 2)) then
+          begin
+            if (st = TEAM_NONE) or (st <> gPlayers[i].Team) then
+              b := HitPlayer(gPlayers[i], 50, 0, 0, SpawnerUID, HIT_SOME)
+            else
+              b := HitPlayer(gPlayers[i], 25, 0, 0, SpawnerUID, HIT_SOME);
+            if b then
+              gPlayers[i].BFGHit();
+          end;
+
+  h := High(gMonsters);
+
+  if h <> -1 then
+    for i := 0 to h do
+      if (gMonsters[i] <> nil) and (gMonsters[i].Live) and (gMonsters[i].UID <> SpawnerUID) then
+        with gMonsters[i] do
+          if (g_PatchLength(X, Y, Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2),
+                            Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)) <= SHOT_BFG_RADIUS) and
+              g_TraceVector(X, Y, Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2),
+                            Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)) then
+            if HitMonster(gMonsters[i], 50, 0, 0, SpawnerUID, HIT_SOME) then gMonsters[i].BFGHit();
+end;
+
+function g_Weapon_CreateShot(I: Integer; ShotType: Byte; Spawner, TargetUID: Word; X, Y, XV, YV: Integer): LongWord;
+var
+  find_id, FramesID: DWORD;
+begin
+  if I < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := I;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  case ShotType of
+    WEAPON_ROCKETLAUNCHER:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := SHOT_ROCKETLAUNCHER_WIDTH;
+        Obj.Rect.Height := SHOT_ROCKETLAUNCHER_HEIGHT;
+
+        Animation := nil;
+        Triggers := nil;
+        ShotType := WEAPON_ROCKETLAUNCHER;
+        g_Texture_Get('TEXTURE_WEAPON_ROCKET', TextureID);
+      end;
+    end;
+
+    WEAPON_PLASMA:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := SHOT_PLASMA_WIDTH;
+        Obj.Rect.Height := SHOT_PLASMA_HEIGHT;
+
+        Triggers := nil;
+        ShotType := WEAPON_PLASMA;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_PLASMA');
+        Animation := TAnimation.Create(FramesID, True, 5);
+      end;
+    end;
+
+    WEAPON_BFG:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := SHOT_BFG_WIDTH;
+        Obj.Rect.Height := SHOT_BFG_HEIGHT;
+
+        Triggers := nil;
+        ShotType := WEAPON_BFG;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_BFG');
+        Animation := TAnimation.Create(FramesID, True, 6);
+      end;
+    end;
+
+    WEAPON_IMP_FIRE:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := 16;
+        Obj.Rect.Height := 16;
+
+        Triggers := nil;
+        ShotType := WEAPON_IMP_FIRE;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_IMPFIRE');
+        Animation := TAnimation.Create(FramesID, True, 4);
+      end;
+    end;
+
+    WEAPON_CACO_FIRE:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := 16;
+        Obj.Rect.Height := 16;
+
+        Triggers := nil;
+        ShotType := WEAPON_CACO_FIRE;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_CACOFIRE');
+        Animation := TAnimation.Create(FramesID, True, 4);
+      end;
+    end;
+
+    WEAPON_MANCUB_FIRE:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := 32;
+        Obj.Rect.Height := 32;
+
+        Triggers := nil;
+        ShotType := WEAPON_MANCUB_FIRE;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_MANCUBFIRE');
+        Animation := TAnimation.Create(FramesID, True, 4);
+      end;
+    end;
+
+    WEAPON_BARON_FIRE:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := 32;
+        Obj.Rect.Height := 16;
+
+        Triggers := nil;
+        ShotType := WEAPON_BARON_FIRE;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_BARONFIRE');
+        Animation := TAnimation.Create(FramesID, True, 4);
+      end;
+    end;
+
+    WEAPON_BSP_FIRE:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := 16;
+        Obj.Rect.Height := 16;
+
+        Triggers := nil;
+        ShotType := WEAPON_BSP_FIRE;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_BSPFIRE');
+        Animation := TAnimation.Create(FramesID, True, 4);
+      end;
+    end;
+
+    WEAPON_SKEL_FIRE:
+    begin
+      with Shots[find_id] do
+      begin
+        g_Obj_Init(@Obj);
+
+        Obj.Rect.Width := SHOT_SKELFIRE_WIDTH;
+        Obj.Rect.Height := SHOT_SKELFIRE_HEIGHT;
+
+        Triggers := nil;
+        ShotType := WEAPON_SKEL_FIRE;
+        target := TargetUID;
+        g_Frames_Get(FramesID, 'FRAMES_WEAPON_SKELFIRE');
+        Animation := TAnimation.Create(FramesID, True, 5);
+      end;
+    end;
+  end;
+
+  Shots[find_id].Obj.X := X;
+  Shots[find_id].Obj.Y := Y;
+  Shots[find_id].Obj.Vel.X := XV;
+  Shots[find_id].Obj.Vel.Y := YV;
+  Shots[find_id].Obj.Accel.X := 0;
+  Shots[find_id].Obj.Accel.Y := 0;
+  Shots[find_id].SpawnerUID := Spawner;
+  Result := find_id;
+end;
+
+procedure throw(i, x, y, xd, yd, s: Integer);
+var
+  a: Integer;
+begin
+  yd := yd - y;
+  xd := xd - x;
+
+  a := Max(Abs(xd), Abs(yd));
+  if a = 0 then
+    a := 1;
+
+  Shots[i].Obj.X := x;
+  Shots[i].Obj.Y := y;
+  Shots[i].Obj.Vel.X := (xd*s) div a;
+  Shots[i].Obj.Vel.Y := (yd*s) div a;
+  Shots[i].Obj.Accel.X := 0;
+  Shots[i].Obj.Accel.Y := 0;
+  if Shots[i].ShotType in [WEAPON_ROCKETLAUNCHER, WEAPON_BFG] then
+    Shots[i].Timeout := 900 // ~25 sec
+  else
+    Shots[i].Timeout := 550 // ~15 sec
+end;
+
+function g_Weapon_Hit(obj: PObj; d: Integer; SpawnerUID: Word; t: Byte; HitCorpses: Boolean = True): Byte;
+var
+  i, h: Integer;
+
+  function PlayerHit(Team: Byte = 0): Boolean;
+  var
+    i: Integer;
+    ChkTeam: Boolean;
+    p: TPlayer;
+  begin
+    Result := False;
+    h := High(gPlayers);
+
+    if h <> -1 then
+      for i := 0 to h do
+        if (gPlayers[i] <> nil) and gPlayers[i].Live and g_Obj_Collide(obj, @gPlayers[i].Obj) then
+        begin
+          ChkTeam := True;
+          if (Team > 0) and (g_GetUIDType(SpawnerUID) = UID_PLAYER) then
+          begin
+            p := g_Player_Get(SpawnerUID);
+            if p <> nil then
+              ChkTeam := (p.Team = gPlayers[i].Team) xor (Team = 2);
+          end;
+          if ChkTeam then
+            if HitPlayer(gPlayers[i], d, obj^.Vel.X, obj^.Vel.Y, SpawnerUID, t) then
+            begin
+              gPlayers[i].Push((obj^.Vel.X+obj^.Accel.X)*IfThen(t = HIT_BFG, 8, 1) div 4,
+                               (obj^.Vel.Y+obj^.Accel.Y)*IfThen(t = HIT_BFG, 8, 1) div 4);
+              if t = HIT_BFG then
+                g_Game_DelayEvent(DE_BFGHIT, 1000, SpawnerUID);
+              Result := True;
+              break;
+            end;
+        end;
+  end;
+  function MonsterHit(): Boolean;
+  var
+    i: Integer;
+  begin
+    Result := False;
+    h := High(gMonsters);
+
+    if h <> -1 then
+      for i := 0 to h do
+        if (gMonsters[i] <> nil) and gMonsters[i].Live and g_Obj_Collide(obj, @gMonsters[i].Obj) then
+          if HitMonster(gMonsters[i], d, obj^.Vel.X, obj^.Vel.Y, SpawnerUID, t) then
+          begin
+            gMonsters[i].Push((obj^.Vel.X+obj^.Accel.X)*IfThen(t = HIT_BFG, 8, 1) div 4,
+                              (obj^.Vel.Y+obj^.Accel.Y)*IfThen(t = HIT_BFG, 8, 1) div 4);
+            Result := True;
+            break;
+          end;
+  end;
+begin
+  Result := 0;
+
+  if HitCorpses then
+  begin
+    h := High(gCorpses);
+
+    if gAdvCorpses and (h <> -1) then
+      for i := 0 to h do
+        if (gCorpses[i] <> nil) and (gCorpses[i].State <> CORPSE_STATE_REMOVEME) and
+           g_Obj_Collide(obj, @gCorpses[i].Obj) then
+        begin
+          // Ðàñïèëèâàåì òðóï:
+          gCorpses[i].Damage(d, (obj^.Vel.X+obj^.Accel.X) div 4,
+                                (obj^.Vel.Y+obj^.Accel.Y) div 4);
+          Result := 1;
+        end;
+  end;
+
+  case gGameSettings.GameMode of
+    // Êàìïàíèÿ:
+    GM_COOP, GM_SINGLE:
+    begin
+      // Ñíà÷àëà áü¸ì ìîíñòðîâ, åñëè åñòü, ïîòîì ïûòàåìñÿ áèòü èãðîêîâ
+      if MonsterHit() then
+      begin
+        Result := 2;
+        Exit;
+      end;
+
+      if PlayerHit() then
+      begin
+        Result := 1;
+        Exit;
+      end;
+    end;
+
+    // Äåçìàò÷:
+    GM_DM:
+    begin
+      // Ñíà÷àëà áü¸ì èãðîêîâ, åñëè åñòü, ïîòîì ïûòàåìñÿ áèòü ìîíñòðîâ
+      if PlayerHit() then
+      begin
+        Result := 1;
+        Exit;
+      end;
+
+      if MonsterHit() then
+      begin
+        Result := 2;
+        Exit;
+      end;
+    end;
+
+    // Êîìàíäíûå:
+    GM_TDM, GM_CTF:
+    begin
+      // Ñíà÷àëà áü¸ì èãðîêîâ êîìàíäû ñîïåðíèêà
+      if PlayerHit(2) then
+      begin
+        Result := 1;
+        Exit;
+      end;
+
+      // Ïîòîì ìîíñòðîâ
+      if MonsterHit() then
+      begin
+        Result := 2;
+        Exit;
+      end;
+
+      // È â êîíöå ñâîèõ èãðîêîâ
+      if PlayerHit(1) then
+      begin
+        Result := 1;
+        Exit;
+      end;
+    end;
+
+  end;
+end;
+
+function g_Weapon_HitUID(UID: Word; d: Integer; SpawnerUID: Word; t: Byte): Boolean;
+begin
+  Result := False;
+
+  case g_GetUIDType(UID) of
+    UID_PLAYER: Result := HitPlayer(g_Player_Get(UID), d, 0, 0, SpawnerUID, t);
+    UID_MONSTER: Result := HitMonster(g_Monsters_Get(UID), d, 0, 0, SpawnerUID, t);
+    else Exit;
+  end;
+end;
+
+function g_Weapon_Explode(X, Y: Integer; rad: Integer; SpawnerUID: Word): Boolean;
+var
+  i, h, r, dx, dy, m, mm: Integer;
+  _angle: SmallInt;
+begin
+  Result := False;
+
+  g_Triggers_PressC(X, Y, rad, SpawnerUID, ACTIVATE_SHOT);
+
+  r := rad*rad;
+
+  h := High(gPlayers);
+
+  if h <> -1 then
+    for i := 0 to h do
+      if (gPlayers[i] <> nil) and gPlayers[i].Live then
+        with gPlayers[i] do
+        begin
+          dx := Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2)-X;
+          dy := Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)-Y;
+
+          if dx > 1000 then dx := 1000;
+          if dy > 1000 then dy := 1000;
+
+          if dx*dx+dy*dy < r then
+          begin
+            //m := PointToRect(X, Y, GameX+PLAYER_RECT.X, GameY+PLAYER_RECT.Y,
+            //                 PLAYER_RECT.Width, PLAYER_RECT.Height);
+
+            mm := Max(abs(dx), abs(dy));
+            if mm = 0 then mm := 1;
+
+            HitPlayer(gPlayers[i], (100*(rad-mm)) div rad, (dx*10) div mm, (dy*10) div mm, SpawnerUID, HIT_ROCKET);
+            gPlayers[i].Push((dx*7) div mm, (dy*7) div mm);
+          end;
+        end;
+
+  h := High(gMonsters);
+
+  if h <> -1 then
+    for i := 0 to h do
+      if gMonsters[i] <> nil then
+        with gMonsters[i] do
+        begin
+          dx := Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2)-X;
+          dy := Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)-Y;
+
+          if dx > 1000 then dx := 1000;
+          if dy > 1000 then dy := 1000;
+
+          if dx*dx+dy*dy < r then
+          begin
+            //m := PointToRect(X, Y, Obj.X+Obj.Rect.X, Obj.Y+Obj.Rect.Y,
+            //                 Obj.Rect.Width, Obj.Rect.Height);
+
+            mm := Max(abs(dx), abs(dy));
+            if mm = 0 then mm := 1;
+
+            if gMonsters[i].Live then
+              HitMonster(gMonsters[i], ((gMonsters[i].Obj.Rect.Width div 4)*10*(rad-mm)) div rad,
+                         0, 0, SpawnerUID, HIT_ROCKET);
+
+            gMonsters[i].Push((dx*7) div mm, (dy*7) div mm);
+          end;
+        end;
+
+  h := High(gCorpses);
+
+  if gAdvCorpses and (h <> -1) then
+    for i := 0 to h do
+      if (gCorpses[i] <> nil) and (gCorpses[i].State <> CORPSE_STATE_REMOVEME) then
+        with gCorpses[i] do
+        begin
+          dx := Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2)-X;
+          dy := Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)-Y;
+
+          if dx > 1000 then dx := 1000;
+          if dy > 1000 then dy := 1000;
+
+          if dx*dx+dy*dy < r then
+          begin
+            m := PointToRect(X, Y, Obj.X+Obj.Rect.X, Obj.Y+Obj.Rect.Y,
+                             Obj.Rect.Width, Obj.Rect.Height);
+
+            mm := Max(abs(dx), abs(dy));
+            if mm = 0 then mm := 1;
+
+            Damage(Round(100*(rad-m)/rad), (dx*10) div mm, (dy*10) div mm);
+          end;
+        end;
+
+  h := High(gGibs);
+
+  if gAdvGibs and (h <> -1) then
+    for i := 0 to h do
+      if gGibs[i].Live then
+        with gGibs[i] do
+        begin
+          dx := Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2)-X;
+          dy := Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2)-Y;
+
+          if dx > 1000 then dx := 1000;
+          if dy > 1000 then dy := 1000;
+
+          if dx*dx+dy*dy < r then
+          begin
+            m := PointToRect(X, Y, Obj.X+Obj.Rect.X, Obj.Y+Obj.Rect.Y,
+                             Obj.Rect.Width, Obj.Rect.Height);
+            _angle := GetAngle(Obj.X+Obj.Rect.X+(Obj.Rect.Width div 2),
+                               Obj.Y+Obj.Rect.Y+(Obj.Rect.Height div 2), X, Y);
+
+            g_Obj_PushA(@Obj, Round(15*(rad-m)/rad), _angle);
+          end;
+        end;
+end;
+
+procedure g_Weapon_Init();
+begin
+  CreateWaterMap();
+end;
+
+procedure g_Weapon_Free();
+var
+  i: Integer;
+begin
+  if Shots <> nil then
+  begin
+    for i := 0 to High(Shots) do
+      if Shots[i].ShotType <> 0 then
+        Shots[i].Animation.Free();
+
+    Shots := nil;
+  end;
+
+  WaterMap := nil;
+end;
+
+procedure g_Weapon_LoadData();
+begin
+  e_WriteLog('Loading weapons data...', MSG_NOTIFY);
+
+  g_Sound_CreateWADEx('SOUND_WEAPON_HITPUNCH', GameWAD+':SOUNDS\HITPUNCH');
+  g_Sound_CreateWADEx('SOUND_WEAPON_MISSPUNCH', GameWAD+':SOUNDS\MISSPUNCH');
+  g_Sound_CreateWADEx('SOUND_WEAPON_HITBERSERK', GameWAD+':SOUNDS\HITBERSERK');
+  g_Sound_CreateWADEx('SOUND_WEAPON_MISSBERSERK', GameWAD+':SOUNDS\MISSBERSERK');
+  g_Sound_CreateWADEx('SOUND_WEAPON_SELECTSAW', GameWAD+':SOUNDS\SELECTSAW');
+  g_Sound_CreateWADEx('SOUND_WEAPON_IDLESAW', GameWAD+':SOUNDS\IDLESAW');
+  g_Sound_CreateWADEx('SOUND_WEAPON_HITSAW', GameWAD+':SOUNDS\HITSAW');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIRESHOTGUN2', GameWAD+':SOUNDS\FIRESHOTGUN2');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIRESHOTGUN', GameWAD+':SOUNDS\FIRESHOTGUN');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIRESAW', GameWAD+':SOUNDS\FIRESAW');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIREROCKET', GameWAD+':SOUNDS\FIREROCKET');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIREPLASMA', GameWAD+':SOUNDS\FIREPLASMA');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIREPISTOL', GameWAD+':SOUNDS\FIREPISTOL');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIRECGUN', GameWAD+':SOUNDS\FIRECGUN');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIREBFG', GameWAD+':SOUNDS\FIREBFG');
+  g_Sound_CreateWADEx('SOUND_FIRE', GameWAD+':SOUNDS\FIRE');
+  g_Sound_CreateWADEx('SOUND_WEAPON_STARTFIREBFG', GameWAD+':SOUNDS\STARTFIREBFG');
+  g_Sound_CreateWADEx('SOUND_WEAPON_EXPLODEROCKET', GameWAD+':SOUNDS\EXPLODEROCKET');
+  g_Sound_CreateWADEx('SOUND_WEAPON_EXPLODEBFG', GameWAD+':SOUNDS\EXPLODEBFG');
+  g_Sound_CreateWADEx('SOUND_WEAPON_BFGWATER', GameWAD+':SOUNDS\BFGWATER');
+  g_Sound_CreateWADEx('SOUND_WEAPON_EXPLODEPLASMA', GameWAD+':SOUNDS\EXPLODEPLASMA');
+  g_Sound_CreateWADEx('SOUND_WEAPON_PLASMAWATER', GameWAD+':SOUNDS\PLASMAWATER');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIREBALL', GameWAD+':SOUNDS\FIREBALL');
+  g_Sound_CreateWADEx('SOUND_WEAPON_EXPLODEBALL', GameWAD+':SOUNDS\EXPLODEBALL');
+  g_Sound_CreateWADEx('SOUND_WEAPON_FIREREV', GameWAD+':SOUNDS\FIREREV');
+  g_Sound_CreateWADEx('SOUND_PLAYER_JETFLY', GameWAD+':SOUNDS\WORKJETPACK');
+  g_Sound_CreateWADEx('SOUND_PLAYER_JETON', GameWAD+':SOUNDS\STARTJETPACK');
+  g_Sound_CreateWADEx('SOUND_PLAYER_JETOFF', GameWAD+':SOUNDS\STOPJETPACK');
+  g_Sound_CreateWADEx('SOUND_PLAYER_CASING1', GameWAD+':SOUNDS\CASING1');
+  g_Sound_CreateWADEx('SOUND_PLAYER_CASING2', GameWAD+':SOUNDS\CASING2');
+  g_Sound_CreateWADEx('SOUND_PLAYER_SHELL1', GameWAD+':SOUNDS\SHELL1');
+  g_Sound_CreateWADEx('SOUND_PLAYER_SHELL2', GameWAD+':SOUNDS\SHELL2');
+
+  g_Texture_CreateWADEx('TEXTURE_WEAPON_ROCKET', GameWAD+':TEXTURES\BROCKET');
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_SKELFIRE', GameWAD+':TEXTURES\BSKELFIRE', 64, 16, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_BFG', GameWAD+':TEXTURES\BBFG', 64, 64, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_PLASMA', GameWAD+':TEXTURES\BPLASMA', 16, 16, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_IMPFIRE', GameWAD+':TEXTURES\BIMPFIRE', 16, 16, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_BSPFIRE', GameWAD+':TEXTURES\BBSPFIRE', 16, 16, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_CACOFIRE', GameWAD+':TEXTURES\BCACOFIRE', 16, 16, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_BARONFIRE', GameWAD+':TEXTURES\BBARONFIRE', 64, 16, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_WEAPON_MANCUBFIRE', GameWAD+':TEXTURES\BMANCUBFIRE', 64, 32, 2);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_ROCKET', GameWAD+':TEXTURES\EROCKET', 128, 128, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_SKELFIRE', GameWAD+':TEXTURES\ESKELFIRE', 64, 64, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_BFG', GameWAD+':TEXTURES\EBFG', 128, 128, 6);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_IMPFIRE', GameWAD+':TEXTURES\EIMPFIRE', 64, 64, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_BFGHIT', GameWAD+':TEXTURES\BFGHIT', 64, 64, 4);
+  g_Frames_CreateWAD(nil, 'FRAMES_FIRE', GameWAD+':TEXTURES\FIRE', 64, 128, 8);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_PLASMA', GameWAD+':TEXTURES\EPLASMA', 32, 32, 4, True);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_BSPFIRE', GameWAD+':TEXTURES\EBSPFIRE', 32, 32, 5);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_CACOFIRE', GameWAD+':TEXTURES\ECACOFIRE', 64, 64, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_EXPLODE_BARONFIRE', GameWAD+':TEXTURES\EBARONFIRE', 64, 64, 3);
+  g_Frames_CreateWAD(nil, 'FRAMES_SMOKE', GameWAD+':TEXTURES\SMOKE', 32, 32, 10, False);
+
+  g_Texture_CreateWADEx('TEXTURE_SHELL_BULLET', GameWAD+':TEXTURES\EBULLET');
+  g_Texture_CreateWADEx('TEXTURE_SHELL_SHELL', GameWAD+':TEXTURES\ESHELL');
+end;
+
+procedure g_Weapon_FreeData();
+begin
+  e_WriteLog('Releasing weapons data...', MSG_NOTIFY);
+
+  g_Sound_Delete('SOUND_WEAPON_HITPUNCH');
+  g_Sound_Delete('SOUND_WEAPON_MISSPUNCH');
+  g_Sound_Delete('SOUND_WEAPON_HITBERSERK');
+  g_Sound_Delete('SOUND_WEAPON_MISSBERSERK');
+  g_Sound_Delete('SOUND_WEAPON_SELECTSAW');
+  g_Sound_Delete('SOUND_WEAPON_IDLESAW');
+  g_Sound_Delete('SOUND_WEAPON_HITSAW');
+  g_Sound_Delete('SOUND_WEAPON_FIRESHOTGUN2');
+  g_Sound_Delete('SOUND_WEAPON_FIRESHOTGUN');
+  g_Sound_Delete('SOUND_WEAPON_FIRESAW');
+  g_Sound_Delete('SOUND_WEAPON_FIREROCKET');
+  g_Sound_Delete('SOUND_WEAPON_FIREPLASMA');
+  g_Sound_Delete('SOUND_WEAPON_FIREPISTOL');
+  g_Sound_Delete('SOUND_WEAPON_FIRECGUN');
+  g_Sound_Delete('SOUND_WEAPON_FIREBFG');
+  g_Sound_Delete('SOUND_FIRE');
+  g_Sound_Delete('SOUND_WEAPON_STARTFIREBFG');
+  g_Sound_Delete('SOUND_WEAPON_EXPLODEROCKET');
+  g_Sound_Delete('SOUND_WEAPON_EXPLODEBFG');
+  g_Sound_Delete('SOUND_WEAPON_BFGWATER');
+  g_Sound_Delete('SOUND_WEAPON_EXPLODEPLASMA');
+  g_Sound_Delete('SOUND_WEAPON_PLASMAWATER');
+  g_Sound_Delete('SOUND_WEAPON_FIREBALL');
+  g_Sound_Delete('SOUND_WEAPON_EXPLODEBALL');
+  g_Sound_Delete('SOUND_WEAPON_FIREREV');
+  g_Sound_Delete('SOUND_PLAYER_JETFLY');
+  g_Sound_Delete('SOUND_PLAYER_JETON');
+  g_Sound_Delete('SOUND_PLAYER_JETOFF');
+  g_Sound_Delete('SOUND_PLAYER_CASING1');
+  g_Sound_Delete('SOUND_PLAYER_CASING2');
+  g_Sound_Delete('SOUND_PLAYER_SHELL1');
+  g_Sound_Delete('SOUND_PLAYER_SHELL2');
+
+  g_Texture_Delete('TEXTURE_WEAPON_ROCKET');
+  g_Frames_DeleteByName('FRAMES_WEAPON_BFG');
+  g_Frames_DeleteByName('FRAMES_WEAPON_PLASMA');
+  g_Frames_DeleteByName('FRAMES_WEAPON_IMPFIRE');
+  g_Frames_DeleteByName('FRAMES_WEAPON_BSPFIRE');
+  g_Frames_DeleteByName('FRAMES_WEAPON_CACOFIRE');
+  g_Frames_DeleteByName('FRAMES_WEAPON_MANCUBFIRE');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_ROCKET');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_BFG');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_IMPFIRE');
+  g_Frames_DeleteByName('FRAMES_BFGHIT');
+  g_Frames_DeleteByName('FRAMES_FIRE');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_PLASMA');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_BSPFIRE');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_CACOFIRE');
+  g_Frames_DeleteByName('FRAMES_SMOKE');
+  g_Frames_DeleteByName('FRAMES_WEAPON_BARONFIRE');
+  g_Frames_DeleteByName('FRAMES_EXPLODE_BARONFIRE');
+end;
+
+procedure g_Weapon_gun(x, y, xd, yd, v, dmg: Integer; SpawnerUID: Word; CheckTrigger: Boolean);
+var
+  a: Integer;
+  x2, y2: Integer;
+  dx, dy: Integer;
+  xe, ye: Integer;
+  xi, yi: Integer;
+  s, c: Extended;
+  //vx, vy: Integer;
+  xx, yy, d: Integer;
+
+  i: Integer;
+  t1, _collide: Boolean;
+  w, h: Word;
+begin
+  a := GetAngle(x, y, xd, yd)+180;
+
+  SinCos(DegToRad(-a), s, c);
+
+  if Abs(s) < 0.01 then s := 0;
+  if Abs(c) < 0.01 then c := 0;
+
+  x2 := x+Round(c*gMapInfo.Width);
+  y2 := y+Round(s*gMapInfo.Width);
+
+  t1 := gWalls <> nil;
+  _collide := False;
+  w := gMapInfo.Width;
+  h := gMapInfo.Height;
+
+  xe := 0;
+  ye := 0;
+  dx := x2-x;
+  dy := y2-y;
+
+  if (xd = 0) and (yd = 0) then Exit;
+
+  if dx > 0 then xi := 1 else if dx < 0 then xi := -1 else xi := 0;
+  if dy > 0 then yi := 1 else if dy < 0 then yi := -1 else yi := 0;
+
+  dx := Abs(dx);
+  dy := Abs(dy);
+
+  if dx > dy then d := dx else d := dy;
+
+  //blood vel, for Monster.Damage()
+  //vx := (dx*10 div d)*xi;
+  //vy := (dy*10 div d)*yi;
+
+  xx := x;
+  yy := y;
+
+  for i := 1 to d do
+  begin
+    xe := xe+dx;
+    ye := ye+dy;
+
+    if xe > d then
+    begin
+      xe := xe-d;
+      xx := xx+xi;
+    end;
+
+    if ye > d then
+    begin
+      ye := ye-d;
+      yy := yy+yi;
+    end;
+
+    if (yy > h) or (yy < 0) then Break;
+    if (xx > w) or (xx < 0) then Break;
+
+    if t1 then
+      if ByteBool(gCollideMap[yy, xx] and MARK_BLOCKED) then
+      begin
+        _collide := True;
+        g_GFX_Spark(xx-xi, yy-yi, 2+Random(2), 180+a, 0, 0);
+        if g_Game_IsServer and g_Game_IsNet then
+          MH_SEND_Effect(xx-xi, yy-yi, 180+a, NET_GFX_SPARK);
+      end;
+
+    if not _collide then
+      _collide := GunHit(xx, yy, xi*v, yi*v, dmg, SpawnerUID, v <> 0) <> 0;
+
+    if _collide then
+      Break;
+  end;
+
+  if CheckTrigger and g_Game_IsServer then
+    g_Triggers_PressL(X, Y, xx-xi, yy-yi, SpawnerUID, ACTIVATE_SHOT);
+end;
+
+procedure g_Weapon_punch(x, y: Integer; d, SpawnerUID: Word);
+var
+  obj: TObj;
+begin
+  obj.X := X;
+  obj.Y := Y;
+  obj.rect.X := 0;
+  obj.rect.Y := 0;
+  obj.rect.Width := 39;
+  obj.rect.Height := 52;
+  obj.Vel.X := 0;
+  obj.Vel.Y := 0;
+  obj.Accel.X := 0;
+  obj.Accel.Y := 0;
+
+  if g_Weapon_Hit(@obj, d, SpawnerUID, HIT_SOME) <> 0 then
+    g_Sound_PlayExAt('SOUND_WEAPON_HITPUNCH', x, y)
+  else
+    g_Sound_PlayExAt('SOUND_WEAPON_MISSPUNCH', x, y);
+end;
+
+function g_Weapon_chainsaw(x, y: Integer; d, SpawnerUID: Word): Integer;
+var
+  obj: TObj;
+begin
+  obj.X := X;
+  obj.Y := Y;
+  obj.rect.X := 0;
+  obj.rect.Y := 0;
+  obj.rect.Width := 32;
+  obj.rect.Height := 52;
+  obj.Vel.X := 0;
+  obj.Vel.Y := 0;
+  obj.Accel.X := 0;
+  obj.Accel.Y := 0;
+
+  Result := g_Weapon_Hit(@obj, d, SpawnerUID, HIT_SOME);
+end;
+
+procedure g_Weapon_rocket(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := SHOT_ROCKETLAUNCHER_WIDTH;
+    Obj.Rect.Height := SHOT_ROCKETLAUNCHER_HEIGHT;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 12);
+
+    Animation := nil;
+    triggers := nil;
+    ShotType := WEAPON_ROCKETLAUNCHER;
+    g_Texture_Get('TEXTURE_WEAPON_ROCKET', TextureID);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREROCKET', x, y);
+end;
+
+procedure g_Weapon_revf(x, y, xd, yd: Integer; SpawnerUID, TargetUID: Word;
+  WID: Integer = -1; Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := SHOT_SKELFIRE_WIDTH;
+    Obj.Rect.Height := SHOT_SKELFIRE_HEIGHT;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 12);
+
+    triggers := nil;
+    ShotType := WEAPON_SKEL_FIRE;
+    target := TargetUID;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_SKELFIRE');
+    Animation := TAnimation.Create(FramesID, True, 5);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREREV', x, y);
+end;
+
+procedure g_Weapon_plasma(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64);
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := SHOT_PLASMA_WIDTH;
+    Obj.Rect.Height := SHOT_PLASMA_HEIGHT;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_PLASMA;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_PLASMA');
+    Animation := TAnimation.Create(FramesID, True, 5);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREPLASMA', x, y);
+end;
+
+procedure g_Weapon_ball1(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := 16;
+    Obj.Rect.Height := 16;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_IMP_FIRE;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_IMPFIRE');
+    Animation := TAnimation.Create(FramesID, True, 4);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREBALL', x, y);
+end;
+
+procedure g_Weapon_ball2(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := 16;
+    Obj.Rect.Height := 16;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_CACO_FIRE;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_CACOFIRE');
+    Animation := TAnimation.Create(FramesID, True, 4);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREBALL', x, y);
+end;
+
+procedure g_Weapon_ball7(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := 32;
+    Obj.Rect.Height := 16;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_BARON_FIRE;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_BARONFIRE');
+    Animation := TAnimation.Create(FramesID, True, 4);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREBALL', x, y);
+end;
+
+procedure g_Weapon_aplasma(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := 16;
+    Obj.Rect.Height := 16;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_BSP_FIRE;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_BSPFIRE');
+    Animation := TAnimation.Create(FramesID, True, 4);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREPLASMA', x, y);
+end;
+
+procedure g_Weapon_manfire(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := 32;
+    Obj.Rect.Height := 32;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_MANCUB_FIRE;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_MANCUBFIRE'); 
+    Animation := TAnimation.Create(FramesID, True, 4);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREBALL', x, y);
+end;
+
+procedure g_Weapon_bfgshot(x, y, xd, yd: Integer; SpawnerUID: Word; WID: Integer = -1;
+  Silent: Boolean = False);
+var
+  find_id, FramesID: DWORD;
+  dx, dy: Integer;
+begin
+  if WID < 0 then
+    find_id := FindShot()
+  else
+  begin
+    find_id := WID;
+    if Integer(find_id) >= High(Shots) then
+      SetLength(Shots, find_id + 64)
+  end;
+  with Shots[find_id] do
+  begin
+    g_Obj_Init(@Obj);
+
+    Obj.Rect.Width := SHOT_BFG_WIDTH;
+    Obj.Rect.Height := SHOT_BFG_HEIGHT;
+
+    dx := IfThen(xd>x, -Obj.Rect.Width, 0);
+    dy := -(Obj.Rect.Height div 2);
+    throw(find_id, x+dx, y+dy, xd+dx, yd+dy, 16);
+
+    triggers := nil;
+    ShotType := WEAPON_BFG;
+    g_Frames_Get(FramesID, 'FRAMES_WEAPON_BFG');
+    Animation := TAnimation.Create(FramesID, True, 6);
+  end;
+
+  Shots[find_id].SpawnerUID := SpawnerUID;
+
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREBFG', x, y);
+end;
+
+procedure g_Weapon_bfghit(x, y: Integer);
+var
+  ID: DWORD;
+  Anim: TAnimation;
+begin
+  if g_Frames_Get(ID, 'FRAMES_BFGHIT') then
+  begin
+    Anim := TAnimation.Create(ID, False, 4);
+    g_GFX_OnceAnim(x-32, y-32, Anim);
+    Anim.Free();
+  end;
+end;
+
+procedure g_Weapon_pistol(x, y, xd, yd: Integer; SpawnerUID: Word;
+  Silent: Boolean = False);
+begin
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIREPISTOL', x, y);
+
+  g_Weapon_gun(x, y, xd, yd, 1, 3, SpawnerUID, True);
+  if gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF] then
+  begin
+    g_Weapon_gun(x, y+1, xd, yd+1, 1, 3, SpawnerUID, False);
+    g_Weapon_gun(x, y-1, xd, yd-1, 1, 2, SpawnerUID, False);
+  end;
+end;
+
+procedure g_Weapon_mgun(x, y, xd, yd: Integer; SpawnerUID: Word;
+  Silent: Boolean = False);
+begin
+  if not Silent then
+    if gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRECGUN', x, y);
+
+  g_Weapon_gun(x, y, xd, yd, 1, 3, SpawnerUID, True);
+  if (gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF]) and
+     (g_GetUIDType(SpawnerUID) = UID_PLAYER) then
+  begin
+    g_Weapon_gun(x, y+1, xd, yd+1, 1, 2, SpawnerUID, False);
+    g_Weapon_gun(x, y-1, xd, yd-1, 1, 2, SpawnerUID, False);
+  end;
+end;
+
+procedure g_Weapon_shotgun(x, y, xd, yd: Integer; SpawnerUID: Word;
+  Silent: Boolean = False);
+var
+  i, j: Integer;
+begin
+  if not Silent then
+    if gSoundEffectsDF then g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN', x, y);
+
+  for i := 0 to 9 do
+  begin
+    j := Random(17)-8; // -8 .. 8
+    g_Weapon_gun(x, y+j, xd, yd+j, IfThen(i mod 2 <> 0, 1, 0), 3, SpawnerUID, i=0);
+  end;
+end;
+
+procedure g_Weapon_dshotgun(x, y, xd, yd: Integer; SpawnerUID: Word;
+  Silent: Boolean = False);
+var
+  a, i, j: Integer;
+begin
+  if not Silent then
+    g_Sound_PlayExAt('SOUND_WEAPON_FIRESHOTGUN2', x, y);
+
+  if gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF] then a := 25 else a := 20;
+  for i := 0 to a do
+  begin
+    j := Random(41)-20; // -20 .. 20
+    g_Weapon_gun(x, y+j, xd, yd+j, IfThen(i mod 3 <> 0, 0, 1), 3, SpawnerUID, i=0);
+  end;
+end;
+
+procedure g_Weapon_Update();
+var
+  i, a, h, cx, cy, oldvx, oldvy: Integer;
+  _id: DWORD;
+  Anim: TAnimation;
+  t: DWArray;
+  st: Word;
+  s: String;
+  o: TObj;
+  spl: Boolean;
+  Loud: Boolean;
+begin
+  if Shots = nil then
+    Exit;
+
+  for i := 0 to High(Shots) do
+  begin
+    if Shots[i].ShotType = 0 then
+      Continue;
+
+    Loud := True;
+
+    with Shots[i] do
+    begin
+      Timeout := Timeout - 1;
+      oldvx := Obj.Vel.X;
+      oldvy := Obj.Vel.Y;
+    // Àêòèâèðîâàòü òðèããåðû ïî ïóòè (êðîìå óæå àêòèâèðîâàííûõ):
+      if g_Game_IsServer then
+        t := g_Triggers_PressR(Obj.X, Obj.Y, Obj.Rect.Width, Obj.Rect.Height,
+                               SpawnerUID, ACTIVATE_SHOT, triggers)
+      else
+        t := nil;
+
+      if t <> nil then
+      begin
+      // Ïîïîëíÿåì ñïèñîê àêòèâèðîâàííûõ òðèããåðîâ:
+        if triggers = nil then
+          triggers := t
+        else
+          begin
+            h := High(t);
+
+            for a := 0 to h do
+              if not InDWArray(t[a], triggers) then
+              begin
+                SetLength(triggers, Length(triggers)+1);
+                triggers[High(triggers)] := t[a];
+              end;
+          end;
+      end;
+
+    // Àíèìàöèÿ ñíàðÿäà:
+      if Animation <> nil then
+        Animation.Update();
+
+    // Äâèæåíèå:
+      spl := (ShotType <> WEAPON_PLASMA) and
+             (ShotType <> WEAPON_BFG) and
+             (ShotType <> WEAPON_BSP_FIRE);
+
+      st := g_Obj_Move(@Obj, False, spl);
+
+      if WordBool(st and MOVE_FALLOUT) or (Obj.X < -1000) or
+        (Obj.X > gMapInfo.Width+1000) or (Obj.Y < -1000) then
+      begin
+        // Íà êëèåíòå ñêîðåå âñåãî è òàê óæå âûïàë.
+        ShotType := 0;
+        Animation.Free();
+        Continue;
+      end;
+
+      cx := Obj.X + (Obj.Rect.Width div 2);
+      cy := Obj.Y + (Obj.Rect.Height div 2);
+
+      case ShotType of
+        WEAPON_ROCKETLAUNCHER, WEAPON_SKEL_FIRE: // Ðàêåòû è ñíàðÿäû Ñêåëåòà
+          begin
+          // Âûëåòåëà èç âîäû:
+            if WordBool(st and MOVE_HITAIR) then
+              g_Obj_SetSpeed(@Obj, 12);
+
+          // Â âîäå øëåéô - ïóçûðè, â âîçäóõå øëåéô - äûì:
+            if WordBool(st and MOVE_INWATER) then
+              g_GFX_Bubbles(Obj.X+(Obj.Rect.Width div 2),
+                            Obj.Y+(Obj.Rect.Height div 2),
+                            1+Random(3), 16, 16)
+            else
+              if g_Frames_Get(_id, 'FRAMES_SMOKE') then
+              begin
+                Anim := TAnimation.Create(_id, False, 3);
+                Anim.Alpha := 150;
+                g_GFX_OnceAnim(Obj.X-8+Random(9),
+                               Obj.Y+(Obj.Rect.Height div 2)-20+Random(9),
+                               Anim, ONCEANIM_SMOKE);
+                Anim.Free();
+              end;
+
+          // Ïîïàëè â êîãî-òî èëè â ñòåíó:
+            if WordBool(st and (MOVE_HITWALL or MOVE_HITLAND or MOVE_HITCEIL)) or
+               (g_Weapon_Hit(@Obj, 10, SpawnerUID, HIT_SOME, False) <> 0) or
+               (Timeout < 1) then
+            begin
+              Obj.Vel.X := 0;
+              Obj.Vel.Y := 0;
+
+              g_Weapon_Explode(cx, cy, 60, SpawnerUID);
+
+              if ShotType = WEAPON_SKEL_FIRE then
+                begin // Âçðûâ ñíàðÿäà Ñêåëåòà
+                  if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_SKELFIRE') then
+                  begin
+                    Anim := TAnimation.Create(TextureID, False, 8);
+                    Anim.Blending := False;
+                    g_GFX_OnceAnim((Obj.X+32)-32, (Obj.Y+8)-32, Anim);
+                    Anim.Free();
+                  end;
+                end
+              else
+                begin // Âçðûâ Ðàêåòû
+                  if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_ROCKET') then
+                  begin
+                    Anim := TAnimation.Create(TextureID, False, 6);
+                    Anim.Blending := False;
+                    g_GFX_OnceAnim(cx-64, cy-64, Anim);
+                    Anim.Free();
+                  end;
+                end;
+
+              g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEROCKET', Obj.X, Obj.Y);
+
+              ShotType := 0;
+            end;
+
+            if ShotType = WEAPON_SKEL_FIRE then
+            begin // Ñàìîíàâîäêà ñíàðÿäà Ñêåëåòà:
+              if GetPos(target, @o) then
+                throw(i, Obj.X, Obj.Y,
+                      o.X+o.Rect.X+(o.Rect.Width div 2)+o.Vel.X+o.Accel.X,
+                      o.Y+o.Rect.Y+(o.Rect.Height div 2)+o.Vel.Y+o.Accel.Y,
+                      12);
+            end;
+          end;
+
+        WEAPON_PLASMA, WEAPON_BSP_FIRE: // Ïëàçìà, ïëàçìà Àðàõíàòðîíà
+          begin
+          // Ïîïàëà â âîäó - ýëåêòðîøîê ïî âîäå:
+            if WordBool(st and (MOVE_INWATER or MOVE_HITWATER)) then
+            begin
+              g_Sound_PlayExAt('SOUND_WEAPON_PLASMAWATER', Obj.X, Obj.Y);
+              if g_Game_IsServer then CheckTrap(i, 10, HIT_ELECTRO);
+              ShotType := 0;
+              Continue;
+            end;
+
+          // Âåëè÷èíà óðîíà:
+            if (ShotType = WEAPON_PLASMA) and
+               (gGameSettings.GameMode in [GM_DM, GM_TDM, GM_CTF]) then
+              a := 10
+            else
+              a := 5;
+
+            if ShotType = WEAPON_BSP_FIRE then
+              a := 10;
+
+          // Ïîïàëè â êîãî-òî èëè â ñòåíó:
+            if WordBool(st and (MOVE_HITWALL or MOVE_HITLAND or MOVE_HITCEIL)) or
+               (g_Weapon_Hit(@Obj, a, SpawnerUID, HIT_SOME, False) <> 0) or
+               (Timeout < 1) then
+            begin
+              if ShotType = WEAPON_PLASMA then
+                s := 'FRAMES_EXPLODE_PLASMA'
+              else
+                s := 'FRAMES_EXPLODE_BSPFIRE';
+
+            // Âçðûâ Ïëàçìû:
+              if g_Frames_Get(TextureID, s) then
+              begin
+                Anim := TAnimation.Create(TextureID, False, 3);
+                Anim.Blending := False;
+                g_GFX_OnceAnim(cx-16, cy-16, Anim);
+                Anim.Free();
+              end;
+
+              g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEPLASMA', Obj.X, Obj.Y);
+
+              ShotType := 0;
+            end;
+          end;
+
+        WEAPON_BFG: // BFG
+          begin
+          // Ïîïàëà â âîäó - ýëåêòðîøîê ïî âîäå:
+            if WordBool(st and (MOVE_INWATER or MOVE_HITWATER)) then
+            begin
+              g_Sound_PlayExAt('SOUND_WEAPON_BFGWATER', Obj.X, Obj.Y);
+              if g_Game_IsServer then CheckTrap(i, 1000, HIT_ELECTRO);
+              ShotType := 0;
+              Continue;
+            end;
+
+          // Ïîïàëè â êîãî-òî èëè â ñòåíó:
+            if WordBool(st and (MOVE_HITWALL or MOVE_HITLAND or MOVE_HITCEIL)) or
+               (g_Weapon_Hit(@Obj, SHOT_BFG_DAMAGE, SpawnerUID, HIT_BFG, False) <> 0) or
+               (Timeout < 1) then
+            begin
+            // Ëó÷è BFG:
+              if g_Game_IsServer then g_Weapon_BFG9000(cx, cy, SpawnerUID);
+
+            // Âçðûâ BFG:
+              if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_BFG') then
+              begin
+                Anim := TAnimation.Create(TextureID, False, 6);
+                Anim.Blending := False;
+                g_GFX_OnceAnim(cx-64, cy-64, Anim);
+                Anim.Free();
+              end;
+
+              g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBFG', Obj.X, Obj.Y);
+
+              ShotType := 0;
+            end;
+          end;
+
+        WEAPON_IMP_FIRE, WEAPON_CACO_FIRE, WEAPON_BARON_FIRE: // Âûñòðåëû Áåñà, Êàêîäåìîíà Ðûöàðÿ/Áàðîíà àäà
+          begin
+          // Âûëåòåë èç âîäû:
+            if WordBool(st and MOVE_HITAIR) then
+              g_Obj_SetSpeed(@Obj, 16);
+
+          // Âåëè÷èíà óðîíà:
+            if ShotType = WEAPON_IMP_FIRE then
+              a := 5
+            else
+              if ShotType = WEAPON_CACO_FIRE then
+                a := 20
+              else
+                a := 40;
+
+          // Ïîïàëè â êîãî-òî èëè â ñòåíó:
+            if WordBool(st and (MOVE_HITWALL or MOVE_HITLAND or MOVE_HITCEIL)) or
+               (g_Weapon_Hit(@Obj, a, SpawnerUID, HIT_SOME) <> 0) or
+               (Timeout < 1) then
+            begin
+              if ShotType = WEAPON_IMP_FIRE then
+                s := 'FRAMES_EXPLODE_IMPFIRE'
+              else
+                if ShotType = WEAPON_CACO_FIRE then
+                  s := 'FRAMES_EXPLODE_CACOFIRE'
+                else
+                  s := 'FRAMES_EXPLODE_BARONFIRE';
+
+            // Âçðûâ:
+              if g_Frames_Get(TextureID, s) then
+              begin
+                Anim := TAnimation.Create(TextureID, False, 6);
+                Anim.Blending := False;
+                g_GFX_OnceAnim(cx-32, cy-32, Anim);
+                Anim.Free();
+              end;
+
+              g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBALL', Obj.X, Obj.Y);
+
+              ShotType := 0;
+            end;
+          end;
+
+        WEAPON_MANCUB_FIRE: // Âûñòðåë Ìàíêóáóñà
+          begin
+          // Âûëåòåë èç âîäû:
+            if WordBool(st and MOVE_HITAIR) then
+              g_Obj_SetSpeed(@Obj, 16);
+
+          // Ïîïàëè â êîãî-òî èëè â ñòåíó:
+            if WordBool(st and (MOVE_HITWALL or MOVE_HITLAND or MOVE_HITCEIL)) or
+               (g_Weapon_Hit(@Obj, 40, SpawnerUID, HIT_SOME, False) <> 0) or
+               (Timeout < 1) then
+            begin
+            // Âçðûâ:
+              if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_ROCKET') then
+              begin
+                Anim := TAnimation.Create(TextureID, False, 6);
+                Anim.Blending := False;
+                g_GFX_OnceAnim(cx-64, cy-64, Anim);
+                Anim.Free();
+              end;
+
+              g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBALL', Obj.X, Obj.Y);
+
+              ShotType := 0;
+            end;
+          end;
+      end; // case ShotType of...
+
+    // Åñëè ñíàðÿäà óæå íåò, óäàëÿåì àíèìàöèþ:
+      if (ShotType = 0) then
+      begin
+        if gGameSettings.GameType = GT_SERVER then
+          MH_SEND_DeleteShot(i, Obj.X, Obj.Y, Loud);
+        Animation.Free();
+        Animation := nil;
+      end
+      else if (oldvx <> Obj.Vel.X) or (oldvy <> Obj.Vel.Y) then
+        if gGameSettings.GameType = GT_SERVER then
+          MH_SEND_UpdateShot(i);
+    end;
+  end;
+end;
+
+procedure g_Weapon_Draw();
+var
+  i: Integer;
+  a: SmallInt;
+  p: TPoint;
+begin
+  if Shots = nil then
+    Exit;
+
+  for i := 0 to High(Shots) do
+    if Shots[i].ShotType <> 0 then
+      with Shots[i] do
+      begin
+        if (Shots[i].ShotType = WEAPON_ROCKETLAUNCHER) or
+           (Shots[i].ShotType = WEAPON_BARON_FIRE) or
+           (Shots[i].ShotType = WEAPON_MANCUB_FIRE) or
+           (Shots[i].ShotType = WEAPON_SKEL_FIRE) then
+          a := -GetAngle2(Obj.Vel.X, Obj.Vel.Y)
+        else
+          a := 0;
+
+        p.X := Obj.Rect.Width div 2;
+        p.Y := Obj.Rect.Height div 2;
+
+        if Animation <> nil then
+          begin
+            if (Shots[i].ShotType = WEAPON_BARON_FIRE) or
+               (Shots[i].ShotType = WEAPON_MANCUB_FIRE) or
+               (Shots[i].ShotType = WEAPON_SKEL_FIRE) then
+              Animation.DrawEx(Obj.X, Obj.Y, M_NONE, p, a)
+            else
+              Animation.Draw(Obj.X, Obj.Y, M_NONE);
+          end
+        else
+          begin
+            if (Shots[i].ShotType = WEAPON_ROCKETLAUNCHER) then
+              e_DrawAdv(TextureID, Obj.X, Obj.Y, 0, True, False, a, @p, M_NONE)
+            else
+              e_Draw(TextureID, Obj.X, Obj.Y, 0, True, False);
+          end;
+      end;
+end;
+
+function g_Weapon_Danger(UID: Word; X, Y: Integer; Width, Height: Word; Time: Byte): Boolean;
+var
+  a: Integer;
+begin
+  Result := False;
+
+  if Shots = nil then
+    Exit;
+
+  for a := 0 to High(Shots) do
+    if (Shots[a].ShotType <> 0) and (Shots[a].SpawnerUID <> UID) then
+      if ((Shots[a].Obj.Vel.Y = 0) and (Shots[a].Obj.Vel.X > 0) and (Shots[a].Obj.X < X)) or
+          (Shots[a].Obj.Vel.Y = 0) and (Shots[a].Obj.Vel.X < 0) and (Shots[a].Obj.X > X) then
+        if (Abs(X-Shots[a].Obj.X) < Abs(Shots[a].Obj.Vel.X*Time)) and
+            g_Collide(X, Y, Width, Height, X, Shots[a].Obj.Y,
+                      Shots[a].Obj.Rect.Width, Shots[a].Obj.Rect.Height) and
+            g_TraceVector(X, Y, Shots[a].Obj.X, Shots[a].Obj.Y) then
+        begin
+          Result := True;
+          Exit;
+        end;
+end;
+
+procedure g_Weapon_SaveState(var Mem: TBinMemoryWriter);
+var
+  count, i, j: Integer;
+  dw: DWORD;
+begin
+// Ñ÷èòàåì êîëè÷åñòâî ñóùåñòâóþùèõ ñíàðÿäîâ:
+  count := 0;
+  if Shots <> nil then
+    for i := 0 to High(Shots) do
+      if Shots[i].ShotType <> 0 then
+        count := count + 1;
+
+  Mem := TBinMemoryWriter.Create((count+1) * 80);
+
+// Êîëè÷åñòâî ñíàðÿäîâ:
+  Mem.WriteInt(count);
+
+  if count = 0 then
+    Exit;
+
+  for i := 0 to High(Shots) do
+    if Shots[i].ShotType <> 0 then
+    begin
+    // Ñèãíàòóðà ñíàðÿäà:
+      dw := SHOT_SIGNATURE; // 'SHOT'
+      Mem.WriteDWORD(dw);
+    // Òèï ñíàðÿäà:
+      Mem.WriteByte(Shots[i].ShotType);
+    // Öåëü:
+      Mem.WriteWord(Shots[i].Target);
+    // UID ñòðåëÿâøåãî:
+      Mem.WriteWord(Shots[i].SpawnerUID);
+    // Ðàçìåð ïîëÿ Triggers:
+      dw := Length(Shots[i].Triggers);
+      Mem.WriteDWORD(dw);
+    // Òðèããåðû, àêòèâèðîâàííûå âûñòðåëîì:
+      for j := 0 to Integer(dw)-1 do
+        Mem.WriteDWORD(Shots[i].Triggers[j]);
+    // Îáúåêò ñíàðÿäà:
+      Obj_SaveState(@Shots[i].Obj, Mem);
+    end;
+end;
+
+procedure g_Weapon_LoadState(var Mem: TBinMemoryReader);
+var
+  count, i, j: Integer;
+  dw: DWORD;
+begin
+  if Mem = nil then
+    Exit;
+
+// Êîëè÷åñòâî ñíàðÿäîâ:
+  Mem.ReadInt(count);
+
+  SetLength(Shots, count);
+
+  if count = 0 then
+    Exit;
+
+  for i := 0 to count-1 do
+  begin
+  // Ñèãíàòóðà ñíàðÿäà:
+    Mem.ReadDWORD(dw);
+    if dw <> SHOT_SIGNATURE then // 'SHOT'
+    begin
+      raise EBinSizeError.Create('g_Weapons_LoadState: Wrong Shot Signature');
+    end;
+  // Òèï ñíàðÿäà:
+    Mem.ReadByte(Shots[i].ShotType);
+  // Öåëü:
+    Mem.ReadWord(Shots[i].Target);
+  // UID ñòðåëÿâøåãî:
+    Mem.ReadWord(Shots[i].SpawnerUID);
+  // Ðàçìåð ïîëÿ Triggers:
+    Mem.ReadDWORD(dw);
+    SetLength(Shots[i].Triggers, dw);
+  // Òðèããåðû, àêòèâèðîâàííûå âûñòðåëîì:
+    for j := 0 to Integer(dw)-1 do
+      Mem.ReadDWORD(Shots[i].Triggers[j]);
+  // Îáúåêò ïðåäìåòà:
+    Obj_LoadState(@Shots[i].Obj, Mem);
+
+  // Óñòàíîâêà òåêñòóðû èëè àíèìàöèè:
+    Shots[i].TextureID := DWORD(-1);
+    Shots[i].Animation := nil;
+
+    case Shots[i].ShotType of
+      WEAPON_ROCKETLAUNCHER, WEAPON_SKEL_FIRE:
+        begin
+          g_Texture_Get('TEXTURE_WEAPON_ROCKET', Shots[i].TextureID);
+        end;
+      WEAPON_PLASMA:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_PLASMA');
+          Shots[i].Animation := TAnimation.Create(dw, True, 5);
+        end;
+      WEAPON_BFG:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_BFG');
+          Shots[i].Animation := TAnimation.Create(dw, True, 6);
+        end;
+      WEAPON_IMP_FIRE:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_IMPFIRE');
+          Shots[i].Animation := TAnimation.Create(dw, True, 4);
+        end;
+      WEAPON_BSP_FIRE:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_BSPFIRE');
+          Shots[i].Animation := TAnimation.Create(dw, True, 4);
+        end;
+      WEAPON_CACO_FIRE:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_CACOFIRE');
+          Shots[i].Animation := TAnimation.Create(dw, True, 4);
+        end;
+      WEAPON_BARON_FIRE:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_BARONFIRE');
+          Shots[i].Animation := TAnimation.Create(dw, True, 4);
+        end;
+      WEAPON_MANCUB_FIRE:
+        begin
+          g_Frames_Get(dw, 'FRAMES_WEAPON_MANCUBFIRE');
+          Shots[i].Animation := TAnimation.Create(dw, True, 4);
+        end;
+    end;
+  end;
+end;
+
+procedure g_Weapon_DestroyShot(I: Integer; X, Y: Integer; Loud: Boolean = True);
+var
+  cx, cy: Integer;
+  Anim: TAnimation;
+  s: string;
+begin
+  if Shots = nil then
+    Exit;
+  if (I > High(Shots)) or (I < 0) then Exit;
+  
+  with Shots[I] do
+  begin
+    if ShotType = 0 then Exit;
+    Obj.X := X;
+    Obj.Y := Y;
+    cx := Obj.X + (Obj.Rect.Width div 2);
+    cy := Obj.Y + (Obj.Rect.Height div 2);
+
+    case ShotType of
+      WEAPON_ROCKETLAUNCHER, WEAPON_SKEL_FIRE: // Ðàêåòû è ñíàðÿäû Ñêåëåòà
+      begin
+        if Loud then
+        begin
+          if ShotType = WEAPON_SKEL_FIRE then
+          begin // Âçðûâ ñíàðÿäà Ñêåëåòà
+            if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_SKELFIRE') then
+            begin
+              Anim := TAnimation.Create(TextureID, False, 8);
+              Anim.Blending := False;
+              g_GFX_OnceAnim((Obj.X+32)-32, (Obj.Y+8)-32, Anim);
+              Anim.Free();
+            end;
+          end
+          else
+          begin // Âçðûâ Ðàêåòû
+            if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_ROCKET') then
+            begin
+              Anim := TAnimation.Create(TextureID, False, 6);
+              Anim.Blending := False;
+              g_GFX_OnceAnim(cx-64, cy-64, Anim);
+              Anim.Free();
+            end;
+          end;
+          g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEROCKET', Obj.X, Obj.Y);
+        end;
+      end;
+
+      WEAPON_PLASMA, WEAPON_BSP_FIRE: // Ïëàçìà, ïëàçìà Àðàõíàòðîíà
+      begin
+        if ShotType = WEAPON_PLASMA then
+          s := 'FRAMES_EXPLODE_PLASMA'
+        else
+          s := 'FRAMES_EXPLODE_BSPFIRE';
+        
+        if g_Frames_Get(TextureID, s) and loud then
+        begin
+          Anim := TAnimation.Create(TextureID, False, 3);
+          Anim.Blending := False;
+          g_GFX_OnceAnim(cx-16, cy-16, Anim);
+          Anim.Free();
+
+          g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEPLASMA', Obj.X, Obj.Y);
+        end;
+      end;
+
+      WEAPON_BFG: // BFG
+      begin
+        // Âçðûâ BFG:
+        if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_BFG') and Loud then
+        begin
+          Anim := TAnimation.Create(TextureID, False, 6);
+          Anim.Blending := False;
+          g_GFX_OnceAnim(cx-64, cy-64, Anim);
+          Anim.Free();
+
+          g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBFG', Obj.X, Obj.Y);
+        end;
+      end;
+
+      WEAPON_IMP_FIRE, WEAPON_CACO_FIRE, WEAPON_BARON_FIRE: // Âûñòðåëû Áåñà, Êàêîäåìîíà Ðûöàðÿ/Áàðîíà àäà
+      begin
+        if ShotType = WEAPON_IMP_FIRE then
+          s := 'FRAMES_EXPLODE_IMPFIRE'
+        else
+          if ShotType = WEAPON_CACO_FIRE then
+            s := 'FRAMES_EXPLODE_CACOFIRE'
+          else
+            s := 'FRAMES_EXPLODE_BARONFIRE';
+
+        if g_Frames_Get(TextureID, s) and Loud then
+        begin
+          Anim := TAnimation.Create(TextureID, False, 6);
+          Anim.Blending := False;
+          g_GFX_OnceAnim(cx-32, cy-32, Anim);
+          Anim.Free();
+
+          g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBALL', Obj.X, Obj.Y);
+        end;
+      end;
+
+      WEAPON_MANCUB_FIRE: // Âûñòðåë Ìàíêóáóñà
+      begin
+        if g_Frames_Get(TextureID, 'FRAMES_EXPLODE_ROCKET') and Loud then
+        begin
+          Anim := TAnimation.Create(TextureID, False, 6);
+          Anim.Blending := False;
+          g_GFX_OnceAnim(cx-64, cy-64, Anim);
+          Anim.Free();
+
+          g_Sound_PlayExAt('SOUND_WEAPON_EXPLODEBALL', Obj.X, Obj.Y);
+        end;
+      end;
+    end; // case ShotType of...
+
+    ShotType := 0;
+    Animation.Free();
+  end;
+end;
+
+end.
diff --git a/src/game/g_window.pas b/src/game/g_window.pas
new file mode 100644 (file)
index 0000000..bb20c47
--- /dev/null
@@ -0,0 +1,520 @@
+unit g_window;
+
+interface
+
+uses
+  WADEDITOR;
+
+function  SDLMain(): Integer;
+function  GetTimer(): Int64;
+procedure ResetTimer();
+function  CreateGLWindow(Title: PChar): Boolean;
+procedure KillGLWindow();
+procedure PushExitEvent();
+function  ProcessMessage(): Boolean;
+procedure ProcessLoading();
+procedure ReDrawWindow();
+procedure SwapBuffers();
+procedure Sleep(ms: LongWord);
+function  GetDisplayModes(dBPP: DWORD; var SelRes: DWORD): SArray;
+function  g_Window_SetDisplay(PreserveGL: Boolean = False): Boolean;
+function  g_Window_SetSize(W, H: Word; FScreen: Boolean): Boolean;
+
+implementation
+
+uses
+  SDL, GL, GLExt, e_graphics, e_log, g_main,
+  g_console, SysUtils, e_input, g_options, g_game,
+  g_basic, g_textures, e_sound, g_sound, g_menu, ENet, g_net;
+
+var
+  h_Wnd: PSDL_Surface;
+  wFlags: LongWord = 0;
+  Time, Time_Delta, Time_Old: Int64;
+  flag: Boolean;
+  wNeedTimeReset: Boolean = False;
+  wWindowCreated: Boolean = False;
+  //wCursorShown: Boolean = False;
+  wMinimized: Boolean = False;
+  //wNeedFree: Boolean = True;
+  wLoadingProgress: Boolean = False;
+  wLoadingQuit: Boolean = False;
+  {wWinPause: Byte = 0;}
+
+const
+  // TODO: move this to a separate file
+  CP1251: array [0..127] of Word = (
+    $0402,$0403,$201A,$0453,$201E,$2026,$2020,$2021,$20AC,$2030,$0409,$2039,$040A,$040C,$040B,$040F,
+    $0452,$2018,$2019,$201C,$201D,$2022,$2013,$2014,$003F,$2122,$0459,$203A,$045A,$045C,$045B,$045F,
+    $00A0,$040E,$045E,$0408,$00A4,$0490,$00A6,$00A7,$0401,$00A9,$0404,$00AB,$00AC,$00AD,$00AE,$0407,
+    $00B0,$00B1,$0406,$0456,$0491,$00B5,$00B6,$00B7,$0451,$2116,$0454,$00BB,$0458,$0405,$0455,$0457,
+    $0410,$0411,$0412,$0413,$0414,$0415,$0416,$0417,$0418,$0419,$041A,$041B,$041C,$041D,$041E,$041F,
+    $0420,$0421,$0422,$0423,$0424,$0425,$0426,$0427,$0428,$0429,$042A,$042B,$042C,$042D,$042E,$042F,
+    $0430,$0431,$0432,$0433,$0434,$0435,$0436,$0437,$0438,$0439,$043A,$043B,$043C,$043D,$043E,$043F,
+    $0440,$0441,$0442,$0443,$0444,$0445,$0446,$0447,$0448,$0449,$044A,$044B,$044C,$044D,$044E,$044F
+  );
+
+// TODO: make a transition table or something
+function WCharToCP1251(wc: Word): Word;
+begin
+  for Result := 0 to 127 do
+    if CP1251[Result] = wc then
+      break;
+  Result := Result + 128;
+end;
+
+function g_Window_SetDisplay(PreserveGL: Boolean = False): Boolean;
+begin
+  Result := False;
+
+  e_WriteLog('Setting display mode...', MSG_NOTIFY);
+
+  if wWindowCreated and PreserveGL then
+    e_SaveGLContext(); // we need this and restore because of a bug in SDL1.2, apparently
+
+  wFlags := SDL_RESIZABLE or SDL_OPENGL;
+  if gFullscreen then wFlags := wFlags or SDL_FULLSCREEN;
+
+  h_Wnd := SDL_SetVideoMode(gScreenWidth, gScreenHeight, gBPP, wFlags);
+  SDL_EnableUNICODE(SDL_ENABLE);
+  SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
+  SDL_ShowCursor(SDL_DISABLE);
+
+  if wWindowCreated and PreserveGL then
+    e_RestoreGLContext();
+
+  Result := h_Wnd <> nil;
+end;
+
+procedure ReShowCursor();
+begin
+  // TODO: what was this for?
+end;
+
+function GetDisplayModes(dBPP: DWORD; var SelRes: DWORD): SArray;
+var
+  modesp: PPSDL_Rect;
+  tmpp: PSDL_Rect;
+  tmpr: SDL_Rect;
+  i: Integer;
+begin
+  SetLength(Result, 0);
+  modesp := SDL_ListModes(nil, SDL_FULLSCREEN or SDL_HWSURFACE);
+  if modesp = nil then exit;
+  if Pointer(-1) = modesp then exit;
+
+  tmpp := modesp^;
+  i := 0;
+  while tmpp <> nil do
+  begin
+    tmpr := tmpp^;
+    if (tmpr.w = gScreenWidth) and (tmpr.h = gScreenHeight) then
+      SelRes := i;
+    SetLength(Result, Length(Result) + 1);
+    Result[i] := IntToStr(tmpr.w) + 'x' + IntToStr(tmpr.h);
+
+    modesp := Pointer(Cardinal(modesp) + SizeOf(PSDL_Rect));
+    tmpp := modesp^;
+    Inc(i);
+  end;
+
+  e_WriteLog('SDL: Got ' + IntToStr(Length(Result)) + ' resolutions.', MSG_NOTIFY);
+end;
+
+procedure Sleep(ms: LongWord);
+begin
+  SDL_Delay(ms);
+end;
+
+procedure ChangeWindowSize();
+begin
+  gWinSizeX := gScreenWidth;
+  gWinSizeY := gScreenHeight;
+  e_ResizeWindow(gScreenWidth, gScreenHeight);
+  g_Game_SetupScreenSize();
+  g_Menu_Reset();
+  g_Game_ClearLoading();
+end;
+
+function g_Window_SetSize(W, H: Word; FScreen: Boolean): Boolean;
+var
+  Preserve: Boolean;
+begin
+  Result := False;
+  Preserve := False;
+
+  if (gScreenWidth <> W) or (gScreenHeight <> H) then
+  begin
+    Result := True;
+    gScreenWidth := W;
+    gScreenHeight := H;
+  end;
+
+  if gFullscreen <> FScreen then
+  begin
+    Result := True;
+    gFullscreen := FScreen;
+    Preserve := True;
+  end;
+
+  if Result then
+  begin
+    g_Window_SetDisplay(Preserve);
+    ChangeWindowSize();
+  end;
+end;
+
+function EventHandler(ev: TSDL_Event): Boolean;
+var
+  key, keychr: Word;
+  //joy: Integer;
+begin
+  Result := False;
+  case ev.type_ of
+    SDL_VIDEORESIZE:
+    begin
+      g_Window_SetSize(ev.resize.w, ev.resize.h, gFullscreen);
+      e_Clear();
+    end;
+
+    SDL_ACTIVEEVENT:
+    begin
+      if (ev.active.gain = 0) then
+      begin
+        if g_debug_WinMsgs then
+        begin
+          g_Console_Add('Inactive');
+          e_WriteLog('[DEBUG] WinMsgs: Inactive', MSG_NOTIFY);
+        end;
+
+        if LongBool(ev.active.state and SDL_APPINPUTFOCUS) and gWinActive then
+        begin
+          e_EnableInput := False;
+          e_ClearInputBuffer();
+
+          if gMuteWhenInactive then
+            e_MuteChannels(True);
+
+          if g_debug_WinMsgs then
+          begin
+            g_Console_Add('Inactive indeed');
+            e_WriteLog('[DEBUG] WinMsgs: Inactive indeed', MSG_NOTIFY);
+          end;
+
+          gWinActive := False;
+        end;
+
+        if LongBool(ev.active.state and SDL_APPACTIVE) and (not wMinimized) then
+        begin
+          e_ResizeWindow(0, 0);
+          wMinimized := True;
+
+          if g_debug_WinMsgs then
+          begin
+            g_Console_Add('Minimized indeed');
+            e_WriteLog('[DEBUG] WinMsgs: Minimized indeed', MSG_NOTIFY);
+          end;
+        end;
+      end
+      else
+      begin
+        if g_debug_WinMsgs then
+        begin
+          g_Console_Add('Active');
+          e_WriteLog('[DEBUG] WinMsgs: Active', MSG_NOTIFY);
+        end;
+
+        // Åñëè îêíî áûëî íåàêòèâíûì:
+        if LongBool(ev.active.state and SDL_APPINPUTFOCUS) and (not gWinActive) then
+        begin
+          e_EnableInput := True;
+
+          if gMuteWhenInactive then
+            e_MuteChannels(False);
+
+          if g_debug_WinMsgs then
+          begin
+            g_Console_Add('Active indeed');
+            e_WriteLog('[DEBUG] WinMsgs: Active indeed', MSG_NOTIFY);
+          end;
+
+          gWinActive := True;
+        end;
+
+        if LongBool(ev.active.state and SDL_APPACTIVE) and wMinimized then
+        begin
+          e_ResizeWindow(gScreenWidth, gScreenHeight);
+
+          wMinimized := False;
+
+          if g_debug_WinMsgs then
+          begin
+            g_Console_Add('Restored indeed');
+            e_WriteLog('[DEBUG] WinMsgs: Restored indeed', MSG_NOTIFY);
+          end;
+        end;
+      end;
+    end;
+
+    SDL_VIDEOEXPOSE:
+    begin
+      // TODO: the fuck is this event?
+      // Draw();
+    end;
+
+    SDL_QUITEV:
+    begin
+      if gExit <> EXIT_QUIT then
+      begin
+        if not wLoadingProgress then
+        begin
+          g_Game_Free();
+          g_Game_Quit();
+        end
+        else
+          wLoadingQuit := True;
+      end;
+      Result := True;
+    end;
+
+    SDL_KEYDOWN:
+    begin
+      key := ev.key.keysym.sym;
+      keychr := ev.key.keysym.unicode;
+      KeyPress(key);
+      if (keychr > 7) and (key <> IK_BACKSPACE) then
+      begin
+        if (keychr >= 128) then
+          keychr := WCharToCP1251(keychr);
+        CharPress(Chr(keychr));
+      end;
+    end;
+
+    // key presses and joysticks are handled in e_input
+  end;
+end;
+
+procedure SwapBuffers();
+begin
+  SDL_GL_SwapBuffers();
+end;
+
+procedure KillGLWindow();
+begin
+  wWindowCreated := False;
+end;
+
+function CreateGLWindow(Title: PChar): Boolean;
+//var
+//  flags: LongWord;
+begin
+  Result := False;
+
+  gWinSizeX := gScreenWidth;
+  gWinSizeY := gScreenHeight;
+
+  e_WriteLog('Creating window', MSG_NOTIFY);
+
+  if not g_Window_SetDisplay() then
+  begin
+    KillGLWindow();
+    e_WriteLog('Window creation error (resolution not supported?)', MSG_FATALERROR);
+    exit;
+  end;
+
+  SDL_WM_SetCaption(Title, Title);
+  wWindowCreated := True;
+
+  e_ResizeWindow(gScreenWidth, gScreenHeight);
+  e_InitGL();
+
+  Result := True;
+end;
+
+function GetTimer(): Int64;
+begin
+  Result := SDL_GetTicks() * 1000; // TODO: do we really need microseconds here?
+end;
+
+procedure ResetTimer();
+begin
+  wNeedTimeReset := True;
+end;
+
+procedure PushExitEvent();
+var
+  ev: TSDL_Event;
+begin
+  ev.type_ := SDL_QUITEV;
+  SDL_PushEvent(@ev);
+end;
+
+procedure ProcessLoading();
+var
+  ev: TSDL_Event;
+  ID: DWORD;
+begin
+  //wNeedFree := False;
+  wLoadingProgress := True;
+  while SDL_PollEvent(@ev) > 0 do
+  begin
+    if (ev.type_ = SDL_QUITEV) then
+      break;
+  end;
+  //wNeedFree := True;
+
+  if (ev.type_ = SDL_QUITEV) or (gExit = EXIT_QUIT) then
+  begin
+    wLoadingProgress := False;
+    exit;
+  end;
+
+  if not wMinimized then
+  begin
+    if g_Texture_Get('INTER', ID) then
+      e_DrawSize(ID, 0, 0, 0, False, False, gScreenWidth, gScreenHeight)
+    else
+      e_Clear(GL_COLOR_BUFFER_BIT, 0, 0, 0);
+
+    DrawLoadingStat();
+    SwapBuffers();
+
+    ReShowCursor();
+  end;
+
+  e_SoundUpdate();
+
+  if NetMode = NET_SERVER then
+    g_Net_Host_Update
+  else
+    if (NetMode = NET_CLIENT) and (NetState <> NET_STATE_AUTH) then
+      g_Net_Client_UpdateWhileLoading;
+  wLoadingProgress := False;
+end;
+
+function ProcessMessage(): Boolean;
+var
+  i, t: Integer;
+  ev: TSDL_Event;
+begin
+  Result := False;
+
+  while SDL_PollEvent(@ev) > 0 do
+  begin
+    Result := EventHandler(ev);
+    if ev.type_ = SDL_QUITEV then exit;
+  end;
+
+  Time := GetTimer();
+  Time_Delta := Time - Time_Old;
+
+  flag := False;
+
+  if wNeedTimeReset then
+  begin
+    Time_Delta := 27777;
+    wNeedTimeReset := False;
+  end;
+
+  t := Time_Delta div 27777;
+  if t > 0 then
+  begin
+    flag := True;
+    for i := 1 to t do
+    begin
+      if NetMode = NET_SERVER then g_Net_Host_Update()
+      else if NetMode = NET_CLIENT then g_Net_Client_Update();
+      Update();
+    end;
+  end
+  else
+  begin
+    if NetMode = NET_SERVER then g_Net_Host_Update()
+    else if NetMode = NET_CLIENT then g_Net_Client_Update();
+  end;
+
+  if wLoadingQuit then
+  begin
+    g_Game_Free();
+    g_Game_Quit();
+  end;
+
+  if gExit = EXIT_QUIT then
+  begin
+    Result := True;
+    Exit;
+  end;
+
+// Âðåìÿ ïðåäûäóùåãî îáíîâëåíèÿ:
+  if flag then
+  begin
+    Time_Old := Time - (Time_Delta mod 27777);
+    if (not wMinimized) then
+    begin
+      Draw();
+      SwapBuffers();
+      ReShowCursor();
+    end;
+  end
+  else
+    Sleep(1);
+
+  e_SoundUpdate();
+end;
+
+procedure ReDrawWindow;
+begin
+  SwapBuffers();
+  ReShowCursor();
+end;
+
+procedure InitOpenGL(VSync: Boolean);
+var
+  v: Byte;
+begin
+  if VSync then v := 1 else v := 0;
+  SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
+  SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
+  SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
+  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
+  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+  SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, v);
+end;
+
+function SDLMain(): Integer;
+begin
+  e_WriteLog('Creating GL window', MSG_NOTIFY);
+  if not CreateGLWindow(PChar(Format('Doom 2D: Forever %s', [GAME_VERSION]))) then
+  begin
+    Result := 0;
+    exit;
+  end;
+
+  e_WriteLog('Initializing OpenGL', MSG_NOTIFY);
+  InitOpenGL(gVSync);
+
+  {EnumDisplayModes();}
+
+  Init();
+  Time_Old := GetTimer();
+
+// Êîìàíäíàÿ ñòðîêà:
+  if ParamCount > 0 then
+    g_Game_Process_Params();
+
+// Çàïðîñ ÿçûêà:
+  if (not gGameOn) and gAskLanguage then
+    g_Menu_AskLanguage();
+
+  e_WriteLog('Entering the main loop', MSG_NOTIFY);
+
+  while not ProcessMessage() do
+    { Main Loop } ;
+
+  Release();
+  KillGLWindow();
+
+  Result := 0;
+end;
+
+end.
diff --git a/src/lib/enet/enet.pp b/src/lib/enet/enet.pp
new file mode 100644 (file)
index 0000000..bfc7bef
--- /dev/null
@@ -0,0 +1,407 @@
+{$mode objfpc}
+unit enet;
+
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enet.pp
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original file: enet.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+
+uses
+  enetplatform, enettypes, enetprotocol, enetlist, enetcallbacks;
+
+const
+  ENET_VERSION_MAJOR = 1;
+  ENET_VERSION_MINOR = 3;
+  ENET_VERSION_PATCH = 12;
+
+  ENET_HOST_ANY       = 0;
+  ENET_HOST_BROADCAST : LongWord = $FFFFFFFF;
+  ENET_PORT_ANY       = 0;
+
+  ENET_BUFFER_MAXIMUM = 1 + 2 * ENET_PROTOCOL_MAXIMUM_PACKET_COMMANDS;
+
+  ENET_HOST_RECEIVE_BUFFER_SIZE          = 256 * 1024;
+  ENET_HOST_SEND_BUFFER_SIZE             = 256 * 1024;
+  ENET_HOST_BANDWIDTH_THROTTLE_INTERVAL  = 1000;
+  ENET_HOST_DEFAULT_MTU                  = 1400;
+  ENET_HOST_DEFAULT_MAXIMUM_PACKET_SIZE  = 32 * 1024 * 1024;
+  ENET_HOST_DEFAULT_MAXIMUM_WAITING_DATA = 32 * 1024 * 1024;
+
+  ENET_PEER_DEFAULT_ROUND_TRIP_TIME      = 500;
+  ENET_PEER_DEFAULT_PACKET_THROTTLE      = 32;
+  ENET_PEER_PACKET_THROTTLE_SCALE        = 32;
+  ENET_PEER_PACKET_THROTTLE_COUNTER      = 7;
+  ENET_PEER_PACKET_THROTTLE_ACCELERATION = 2;
+  ENET_PEER_PACKET_THROTTLE_DECELERATION = 2;
+  ENET_PEER_PACKET_THROTTLE_INTERVAL     = 5000;
+  ENET_PEER_PACKET_LOSS_SCALE            = 1 shl 16;
+  ENET_PEER_PACKET_LOSS_INTERVAL         = 10000;
+  ENET_PEER_WINDOW_SIZE_SCALE            = 64 * 1024;
+  ENET_PEER_TIMEOUT_LIMIT                = 32;
+  ENET_PEER_TIMEOUT_MINIMUM              = 5000;
+  ENET_PEER_TIMEOUT_MAXIMUM              = 30000;
+  ENET_PEER_PING_INTERVAL                = 500;
+  ENET_PEER_UNSEQUENCED_WINDOWS          = 64;
+  ENET_PEER_UNSEQUENCED_WINDOW_SIZE      = 1024;
+  ENET_PEER_FREE_UNSEQUENCED_WINDOWS     = 32;
+  ENET_PEER_RELIABLE_WINDOWS             = 16;
+  ENET_PEER_RELIABLE_WINDOW_SIZE         = $1000;
+  ENET_PEER_FREE_RELIABLE_WINDOWS        = 8;
+
+type
+  //enums
+  ENetSocketType      = ( ENET_SOCKET_TYPE_STREAM     = 1,
+                          ENET_SOCKET_TYPE_DATAGRAM   = 2 );
+  ENetSocketWait      = ( ENET_SOCKET_WAIT_NONE       = 0,
+                          ENET_SOCKET_WAIT_SEND       = 1,
+                          ENET_SOCKET_WAIT_RECEIVE    = 2,
+                          ENET_SOCKET_WAIT_INTERRUPT  = 4 );
+  ENetSocketOption    = ( ENET_SOCKOPT_NONBLOCK       = 1,
+                          ENET_SOCKOPT_BROADCAST      = 2,
+                          ENET_SOCKOPT_RCVBUF         = 3,
+                          ENET_SOCKOPT_SNDBUF         = 4,
+                          ENET_SOCKOPT_REUSEADDR      = 5,
+                          ENET_SOCKOPT_RCVTIMEO       = 6,
+                          ENET_SOCKOPT_SNDTIMEO       = 7,
+                          ENET_SOCKOPT_ERROR          = 8,
+                          ENET_SOCKOPT_NODELAY        = 9 );
+  ENetSocketShutdown  = ( ENET_SOCKET_SHUTDOWN_READ,
+                          ENET_SOCKET_SHUTDOWN_WRITE,
+                          ENET_SOCKET_SHUTDOWN_READ_WRITE );
+
+Const
+  ENET_PACKET_FLAG_RELIABLE            = 1;
+  ENET_PACKET_FLAG_UNSEQUENCED         = 2;
+  ENET_PACKET_FLAG_NO_ALLOCATE         = 4;
+  ENET_PACKET_FLAG_UNRELIABLE_FRAGMENT = 8;
+  ENET_PACKET_FLAG_SENT                = 256; 
+
+Type
+  ENetPeerState       = ( ENET_PEER_STATE_DISCONNECTED,
+                          ENET_PEER_STATE_CONNECTING,
+                          ENET_PEER_STATE_ACKNOWLEDGING_CONNECT,
+                          ENET_PEER_STATE_CONNECTION_PENDING,
+                          ENET_PEER_STATE_CONNECTION_SUCCEEDED,
+                          ENET_PEER_STATE_CONNECTED,
+                          ENET_PEER_STATE_DISCONNECT_LATER,
+                          ENET_PEER_STATE_DISCONNECTING,
+                          ENET_PEER_STATE_ACKNOWLEDGING_DISCONNECT,
+                          ENET_PEER_STATE_ZOMBIE                    );
+
+  ENetEventType       = ( ENET_EVENT_TYPE_NONE,
+                          ENET_EVENT_TYPE_CONNECT,
+                          ENET_EVENT_TYPE_DISCONNECT,
+                          ENET_EVENT_TYPE_RECEIVE     );
+
+  //definitions
+  ENetVersion = enet_uint32;
+
+  //structs pointers
+  pENetAddress         = ^ENetAddress;
+  pENetPacket          = ^ENetPacket;
+  pENetAcknowledgement = ^ENetAcknowledgement;
+  pENetOutgoingCommand = ^ENetOutgoingCommand;
+  pENetIncomingCommand = ^ENetIncomingCommand;
+  pENetChannel         = ^ENetChannel;
+  pENetPeer            = ^ENetPeer;
+  pENetCompressor      = ^ENetCompressor;
+  pENetHost            = ^ENetHost;
+  pENetEvent           = ^ENetEvent;
+
+  //callbacks
+  ENetPacketFreeCallback = procedure( packet: pENetPacket ); cdecl;
+  ENetChecksumCallback = function( const buffers: pENetBuffer; bufferCount: enet_size_t ): enet_uint32; cdecl;
+  ENetInterceptCallback = function( host: pENetHost; event: pENetEvent ): enet_int; cdecl;
+
+{$PACKRECORDS C}
+
+  //structs
+  ENetAddress = record
+    host : enet_uint32;
+    port : enet_uint16;
+  end;
+  ENetPacket = record
+    referenceCount : enet_size_t;
+    flags          : enet_uint32;
+    data           : penet_uint8;
+    dataLength     : enet_size_t;
+    freeCallback   : ENetPacketFreeCallback;
+    userData       : Pointer;
+  end;
+  ENetAcknowledgement = record
+    acknowledgementList : ENetListNode;
+    sentTime            : enet_uint32;
+    command             : TENetProtocol;
+  end;
+  ENetOutgoingCommand = record
+    outgoingCommandList      : ENetListNode;
+    reliableSequenceNumber   : enet_uint16;
+    unreliableSequenceNumber : enet_uint16;
+    sentTime                 : enet_uint32;
+    roundTripTimeout         : enet_uint32;
+    roundTripTimeoutLimit    : enet_uint32;
+    fragmentOffset           : enet_uint32;
+    fragmentLength           : enet_uint16;
+    sendAttempts             : enet_uint16;
+    command                  : TENetProtocol;
+    packet                   : pENetPacket;
+  end;
+  ENetIncomingCommand = record
+    incomingCommandList      : ENetListNode;
+    reliableSequenceNumber   : enet_uint16;
+    unreliableSequenceNumber : enet_uint16;
+    command                  : TENetProtocol;
+    fragmentCount            : enet_uint32;
+    fragmentsRemaining       : enet_uint32;
+    fragments                : penet_uint32;
+    packet                   : pENetPacket;
+  end;
+  ENetChannel = record
+    outgoingReliableSequenceNumber   : enet_uint16;
+    outgoingUnreliableSequenceNumber : enet_uint16;
+    usedReliableWindows              : enet_uint16;
+    reliableWindows                  : array[ 0..ENET_PEER_RELIABLE_WINDOWS-1 ] of enet_uint16;
+    incomingReliableSequenceNumber   : enet_uint16;
+    incomingUnreliableSequenceNumber : enet_uint16;
+    incomingReliableCommands         : TENetList;
+    incomingUnreliableCommands       : TENetList;
+  end;
+  ENetPeer = record
+    dispatchList                   : ENetListNode;
+    host                           : pENetHost;
+    outgoingPeerID                 : enet_uint16;
+    incomingPeerID                 : enet_uint16;
+    connectID                      : enet_uint32;
+    outgoingSessionID              : enet_uint8;
+    incomingSessionID              : enet_uint8;
+    address                        : ENetAddress;
+    data                           : Pointer;
+    state                          : ENetPeerState;
+    channels                       : pENetChannel;
+    channelCount                   : enet_size_t;
+    incomingBandwidth              : enet_uint32;
+    outgoingBandwidth              : enet_uint32;
+    incomingBandwidthThrottleEpoch : enet_uint32;
+    outgoingBandwidthThrottleEpoch : enet_uint32;
+    incomingDataTotal              : enet_uint32;
+    outgoingDataTotal              : enet_uint32;
+    lastSendTime                   : enet_uint32;
+    lastReceiveTime                : enet_uint32;
+    nextTimeout                    : enet_uint32;
+    earliestTimeout                : enet_uint32;
+    packetLossEpoch                : enet_uint32;
+    packetsSent                    : enet_uint32;
+    packetsLost                    : enet_uint32;
+    packetLoss                     : enet_uint32;
+    packetLossVariance             : enet_uint32;
+    packetThrottle                 : enet_uint32;
+    packetThrottleLimit            : enet_uint32;
+    packetThrottleCounter          : enet_uint32;
+    packetThrottleEpoch            : enet_uint32;
+    packetThrottleAcceleration     : enet_uint32;
+    packetThrottleDeceleration     : enet_uint32;
+    packetThrottleInterval         : enet_uint32;
+    pingInterval                   : enet_uint32;
+    timeoutLimit                   : enet_uint32;
+    timeoutMinimum                 : enet_uint32;
+    timeoutMaximum                 : enet_uint32;
+    lastRoundTripTime              : enet_uint32;
+    lowestRoundTripTime            : enet_uint32;
+    lastRoundTripTimeVariance      : enet_uint32;
+    highestRoundTripTimeVariance   : enet_uint32;
+    roundTripTime                  : enet_uint32;
+    roundTripTimeVariance          : enet_uint32;
+    mtu                            : enet_uint32;
+    windowSize                     : enet_uint32;
+    reliableDataInTransit          : enet_uint32;
+    outgoingReliableSequenceNumber : enet_uint16;
+    acknowledgements               : TENetList;
+    sentReliableCommands           : TENetList;
+    sentUnreliableCommands         : TENetList;
+    outgoingReliableCommands       : TENetList;
+    outgoingUnreliableCommands     : TENetList;
+    dispatchedCommands             : TENetList;
+    needsDispatch                  : enet_int;
+    incomingUnsequencedGroup       : enet_uint16;
+    outgoingUnsequencedGroup       : enet_uint16;
+    unsequencedWindow              : array[ 0..(ENET_PEER_UNSEQUENCED_WINDOW_SIZE div 32)-1 ] of enet_uint32;
+    eventData                      : enet_uint32;
+    totalWaitingData               : enet_size_t;
+  end;
+  ENetCompressor = record
+    context    : Pointer;
+    compress   : function( context: Pointer; const inBuffers: pENetBuffer; inBufferCount, inLimit: enet_size_t; outData: penet_uint8; outLimit: enet_size_t ): enet_size_t; cdecl;
+    decompress : function( context: Pointer; const inData: penet_uint8; inLimit: enet_size_t; outData: penet_uint8; outLimit: enet_size_t ): enet_size_t; cdecl;
+    destroy    : procedure( context: Pointer ); cdecl;
+  end;
+  ENetHost = record
+    socket                     : ENetSocket;
+    address                    : ENetAddress;
+    incomingBandwidth          : enet_uint32;
+    outgoingBandwidth          : enet_uint32;
+    bandwidthThrottleEpoch     : enet_uint32;
+    mtu                        : enet_uint32;
+    randomSeed                 : enet_uint32;
+    recalculateBandwidthLimits : enet_int;
+    peers                      : pENetPeer;
+    peerCount                  : enet_size_t;
+    channelLimit               : enet_size_t;
+    serviceTime                : enet_uint32;
+    dispatchQueue              : TENetList;
+    continueSending            : enet_int;
+    packetSize                 : enet_size_t;
+    headerFlags                : enet_uint16;
+    commands                   : array[ 0..ENET_PROTOCOL_MAXIMUM_PACKET_COMMANDS-1 ] of TENetProtocol;
+    commandCount               : enet_size_t;
+    buffers                    : array[ 0..ENET_BUFFER_MAXIMUM-1 ] of ENetBuffer;
+    bufferCount                : enet_size_t;
+    checksum                   : ENetChecksumCallback;
+    compressor                 : ENetCompressor;
+    packetData                 : array[ 0..1, 0..ENET_PROTOCOL_MAXIMUM_MTU-1 ] of enet_uint8;
+    receivedAddress            : ENetAddress;
+    receivedData               : penet_uint8;
+    receivedDataLength         : enet_size_t;
+    totalSentData              : enet_uint32;
+    totalSentPackets           : enet_uint32;
+    totalReceivedData          : enet_uint32;
+    totalReceivedPackets       : enet_uint32;
+    intercept                  : ENetInterceptCallback;
+    connectedPeers             : enet_size_t;
+    bandwidthLimitedPeers      : enet_size_t;
+    duplicatePeers             : enet_size_t;
+    maximumPacketSize          : enet_size_t;
+    maximumWaitingData         : enet_size_t;
+  end;
+  ENetEvent = record
+    kind      : ENetEventType; //originally "type", which conflicts
+    peer      : pENetPeer;
+    channelID : enet_uint8;
+    data      : enet_uint32;
+    packet    : pENetPacket;
+  end;
+
+{$PACKRECORDS DEFAULT}
+
+//inline macros
+function ENET_VERSION_CREATE( const major, minor, patch: LongInt ): ENetVersion; inline;
+function ENET_VERSION_GET_MAJOR( const version: ENetVersion ): LongInt; inline;
+function ENET_VERSION_GET_MINOR( const version: ENetVersion ): LongInt; inline;
+function ENET_VERSION_GET_PATCH( const version: ENetVersion ): LongInt; inline;
+function ENET_VERSION(): ENetVersion; inline;
+
+//external
+{$MACRO ON}
+{$DEFINE libraryENet := cdecl; external 'enet'}
+
+function enet_initialize(): enet_int; libraryENet;
+function enet_initialize_with_callbacks( version: ENetVersion; const inits: pENetCallbacks ): enet_int; libraryENet;
+procedure enet_deinitialize(); libraryENet;
+function enet_linked_version(): ENetVersion; libraryENet;
+
+function enet_time_get(): enet_uint32; libraryENet;
+procedure enet_time_set( newTimeBase: enet_uint32 ); libraryENet;
+
+function enet_socket_create( kind: ENetSocketType ): ENetSocket; libraryENet;
+function enet_socket_bind( socket: ENetSocket; const address: pENetAddress ): enet_int; libraryENet;
+function enet_socket_get_address( socket: ENetSocket; address: pENetAddress ): enet_int; libraryENet;
+function enet_socket_listen( socket: ENetSocket; backlog: enet_int ): enet_int; libraryENet;
+function enet_socket_accept( socket: ENetSocket; address: pENetAddress ): ENetSocket; libraryENet;
+function enet_socket_connect( socket: ENetSocket; const address: pENetAddress ): enet_int; libraryENet;
+function enet_socket_send( socket: ENetSocket; const address: pENetAddress; const buffers: pENetBuffer; bufferCount: enet_size_t ): enet_int; libraryENet;
+function enet_socket_receive( socket: ENetSocket; address: pENetAddress; buffers: pENetBuffer; bufferCount: enet_size_t ): enet_int; libraryENet;
+function enet_socket_wait( socket: ENetSocket; condition: penet_uint32; timeout: enet_uint32 ): enet_int; libraryENet;
+function enet_socket_set_option( socket: ENetSocket; option: ENetSocketOption; value: enet_int ): enet_int; libraryENet;
+function enet_socket_get_option( socket: ENetSocket; option: ENetSocketOption; value: penet_int ): enet_int; libraryENet;
+function enet_socket_shutdown( socket: ENetSocket; how: ENetSocketShutdown ): enet_int; libraryENet;
+procedure enet_socket_destroy( socket: ENetSocket ); libraryENet;
+function enet_socketset_select( maxSocket: ENetSocket; readSet: pENetSocketSet; writeSet: pENetSocketSet; timeout: enet_uint32 ): enet_int; libraryENet;
+
+function enet_address_set_host( address: pENetAddress; const hostName: PChar ): enet_int; libraryENet;
+function enet_address_get_host_ip( const address: pENetAddress; hostName: PChar; nameLength: enet_size_t ): enet_int; libraryENet;
+function enet_address_get_host( const address: pENetAddress; hostName: PChar; nameLength: enet_size_t ): enet_int; libraryENet;
+
+function enet_packet_create( const data: Pointer; dataLength: enet_size_t; flags: enet_uint32 ): pENetPacket; libraryENet;
+procedure enet_packet_destroy( packet: pENetPacket ); libraryENet;
+function enet_packet_resize( packet: pENetPacket; dataLength: enet_size_t ): enet_int; libraryENet;
+function enet_crc32( const buffers: pENetBuffer; bufferCount: enet_size_t ): enet_uint32; libraryENet;
+
+function enet_host_create( const address: pENetAddress; peerCount, channelLimit: enet_size_t; incomingBandwidth, outgoingBandwidth: enet_uint32 ): pENetHost; libraryENet;
+procedure enet_host_destroy( host: pENetHost ); libraryENet;
+function enet_host_connect( host: pENetHost; const address: pENetAddress; channelCount: enet_size_t; data: enet_uint32 ): pENetPeer; libraryENet;
+function enet_host_check_events( host: pENetHost; event: pENetEvent ): enet_int; libraryENet;
+function enet_host_service( host: pENetHost; event: pENetEvent; timeout: enet_uint32 ): enet_int; libraryENet;
+procedure enet_host_flush( host: pENetHost ); libraryENet;
+procedure enet_host_widecast( host: pENetHost; channelID: enet_uint8; packet: pENetPacket ); libraryENet name 'enet_host_broadcast'; //renamed due to names conflict
+procedure enet_host_compress( host: pENetHost; const compressor: pENetCompressor ); libraryENet;
+function enet_host_compress_with_range_coder( host: pENetHost ): enet_int; libraryENet;
+procedure enet_host_channel_limit( host: pENetHost; channelLimit: enet_size_t ); libraryENet;
+procedure enet_host_bandwidth_limit( host: pENetHost; incomingBandwidth, outgoingBandwidth: enet_uint32 ); libraryENet;
+
+function enet_peer_send( peer: pENetPeer; channelID: enet_uint8; packet: pENetPacket ): enet_int; libraryENet;
+function enet_peer_receive( peer: pENetPeer; channelID: penet_uint8 ): pENetPacket; libraryENet;
+procedure enet_peer_ping( peer: pENetPeer ); libraryENet;
+procedure enet_peer_ping_frequency( peer: pENetPeer; pingInterval: enet_uint32 ); libraryENet name 'enet_peer_ping_interval'; //renamed due to names conflict
+procedure enet_peer_timeout( peer: pENetPeer; timeoutLimit, timeoutMinimum, timeoutMaximum: enet_uint32 ); libraryENet;
+procedure enet_peer_reset( peer: pENetPeer ); libraryENet;
+procedure enet_peer_disconnect( peer: pENetPeer; data: enet_uint32 ); libraryENet;
+procedure enet_peer_disconnect_now( peer: pENetPeer; data: enet_uint32 ); libraryENet;
+procedure enet_peer_disconnect_later( peer: pENetPeer; data: enet_uint32 ); libraryENet;
+procedure enet_peer_throttle_configure( peer: pENetPeer; interval, acceleration, deceleration: enet_uint32 ); libraryENet;
+
+function enet_range_coder_create(): Pointer; libraryENet;
+procedure enet_range_coder_destroy( context: Pointer ); libraryENet;
+function enet_range_coder_compress( context: Pointer; const inBuffers: pENetBuffer; inBufferCount, inLiit: enet_size_t; outData: penet_uint8; outLimit: enet_size_t ): enet_size_t; libraryENet;
+function enet_range_coder_decompress( context: Pointer; const inData: penet_uint8; inLimit: enet_size_t; outData: penet_uint8; outLimit: enet_size_t ): enet_size_t; libraryENet;
+
+implementation
+
+function ENET_VERSION_CREATE( const major, minor, patch: LongInt ): ENetVersion; inline;
+   begin Result := (major shl 16) or (minor shl 8) or patch;
+     end;
+
+function ENET_VERSION_GET_MAJOR( const version: ENetVersion ): LongInt; inline;
+   begin Result := (version shr 16) and $FF;
+     end;
+
+function ENET_VERSION_GET_MINOR( const version: ENetVersion ): LongInt; inline;
+   begin Result := (version shr 8) and $FF;
+     end;
+
+function ENET_VERSION_GET_PATCH( const version: ENetVersion ): LongInt; inline;
+   begin Result := version and $FF;
+     end;
+
+function ENET_VERSION(): ENetVersion; inline;
+   begin Result := ENET_VERSION_CREATE( ENET_VERSION_MAJOR, ENET_VERSION_MINOR, ENET_VERSION_PATCH );
+     end;
+
+end.
+
diff --git a/src/lib/enet/enetcallbacks.pp b/src/lib/enet/enetcallbacks.pp
new file mode 100644 (file)
index 0000000..fafd66e
--- /dev/null
@@ -0,0 +1,54 @@
+{$mode objfpc}{$H+}
+unit enetcallbacks;
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enetcallbacks.pp
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original file: callbacks.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+
+uses enettypes; //only for size_t
+
+type
+
+{$PACKRECORDS C}
+
+  pENetCallbacks = ^TENetCallbacks;
+  TENetCallbacks = record
+    malloc    : function( size: enet_size_t ): Pointer; cdecl;
+    free      : procedure( memory: Pointer ); cdecl;
+    no_memory : procedure(); cdecl;
+  end;
+
+{$PACKRECORDS DEFAULT}
+
+implementation
+
+end.
+
diff --git a/src/lib/enet/enetlist.pp b/src/lib/enet/enetlist.pp
new file mode 100644 (file)
index 0000000..854a1b7
--- /dev/null
@@ -0,0 +1,95 @@
+{$mode objfpc}{$H+}
+unit enetlist;
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enetlist.pp
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original file: list.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+                                  
+type
+
+{$PACKRECORDS C}
+
+  pENetListNode = ^ENetListNode;
+  ENetListNode = record
+    next     : pENetListNode;
+    previous : pENetListNode;
+  end;
+
+  ENetListIterator = pENetListNode;
+
+  pENetList = ^TENetList;
+  TENetList = record
+    sentinel : ENetListNode;
+  end;
+
+{$PACKRECORDS DEFAULT}
+
+//inline macros
+function enet_list_begin( list: pENetList ): ENetListIterator; inline;
+function enet_list_end( list: pENetList ): ENetListIterator; inline;
+
+function enet_list_empty( list: pENetList ): Boolean; inline;
+
+function enet_list_next( iterator: ENetListIterator ): ENetListIterator; inline;
+function enet_list_previous( iterator: ENetListIterator ): ENetListIterator; inline;
+
+function enet_list_front( list: pENetList ): Pointer; inline;
+function enet_list_back( list: pENetList ): Pointer; inline;
+
+implementation
+
+function enet_list_begin( list: pENetList ): ENetListIterator; inline;
+   begin Result := list^.sentinel.next;
+     end;
+function enet_list_end( list: pENetList ): ENetListIterator; inline;
+   begin Result := @( list^.sentinel );
+     end;
+
+function enet_list_empty( list: pENetList ): Boolean; inline;
+   begin Result := enet_list_begin(list) = enet_list_end(list);
+     end;
+
+function enet_list_next( iterator: ENetListIterator ): ENetListIterator; inline;
+   begin Result := iterator^.next;
+     end;
+function enet_list_previous( iterator: ENetListIterator ): ENetListIterator; inline;
+   begin Result := iterator^.previous;
+     end;
+
+function enet_list_front( list: pENetList ): Pointer; inline;
+   begin Result := Pointer( list^.sentinel.next );
+     end;
+function enet_list_back( list: pENetList ): Pointer; inline;
+   begin Result := Pointer( list^.sentinel.previous );
+     end;
+
+end.
+
diff --git a/src/lib/enet/enetplatform.pp b/src/lib/enet/enetplatform.pp
new file mode 100644 (file)
index 0000000..88c908a
--- /dev/null
@@ -0,0 +1,108 @@
+{$mode objfpc}{$H+}
+unit enetplatform;
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enetplatform
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original files: win32.h & unix.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+
+uses enettypes, // used only for size_t
+     {$IFDEF WINDOWS} WinSock2 {$ELSE} BaseUnix, Sockets {$ENDIF};
+
+const
+  ENET_SOCKET_NULL = {$IFDEF WINDOWS} INVALID_SOCKET; {$ELSE} -1; {$ENDIF}
+//  ENET_BUFFER_MAXIMUM = MSG_MAXIOVLEN; //is it forgotten in win32.h ?
+
+type
+  ENetSocket = {$IFDEF WINDOWS} TSocket {$ELSE} enet_int {$ENDIF};
+
+  ENetSocketSet = TFDSet;
+  pENetSocketSet = ^ENetSocketSet;
+
+{$PACKRECORDS C}
+
+  pENetBuffer = ^ENetBuffer;
+  ENetBuffer = record
+    {$IFDEF WINDOWS}
+    dataLength : enet_size_t;
+    data       : Pointer;
+    {$ELSE}
+    data       : Pointer;
+    dataLength : enet_size_t;
+    {$ENDIF}
+  end;
+
+{$PACKRECORDS DEFAULT}
+
+//inline macros
+function ENET_HOST_TO_NET_16( const value: Word ): Word; inline;
+function ENET_HOST_TO_NET_32( const value: LongWord ): LongWord; inline;
+
+function ENET_NET_TO_HOST_16( const value: Word ): Word; inline;
+function ENET_NET_TO_HOST_32( const value: LongWord ): LongWord; inline;
+
+procedure ENET_SOCKETSET_EMPTY( var sockset: ENetSocketSet ); inline;
+procedure ENET_SOCKETSET_ADD( var sockset: ENetSocketSet; socket: ENetSocket ); inline;
+procedure ENET_SOCKETSET_REMOVE( var sockset: ENetSocketSet; socket: ENetSocket ); inline;
+function  ENET_SOCKETSET_CHECK( var sockset: ENetSocketSet; socket: ENetSocket ): Boolean; inline;
+
+implementation
+
+function ENET_HOST_TO_NET_16( const value: Word ): Word; inline;
+   begin Result := htons(value);
+     end;
+function ENET_HOST_TO_NET_32( const value: LongWord ): LongWord; inline;
+   begin Result := htonl(value);
+     end;
+
+function ENET_NET_TO_HOST_16( const value: Word ): Word; inline;
+   begin Result := ntohs(value);
+     end;
+function ENET_NET_TO_HOST_32( const value: LongWord ): LongWord; inline;
+   begin Result := ntohl(value);
+     end;
+
+procedure ENET_SOCKETSET_EMPTY( var sockset: ENetSocketSet ); inline;
+    begin {$IFNDEF WINDOWS}fpFD_ZERO{$ELSE}FD_ZERO{$ENDIF}( sockset );
+      end;
+procedure ENET_SOCKETSET_ADD( var sockset: ENetSocketSet; socket: ENetSocket ); inline;
+    begin {$IFNDEF WINDOWS}fpFD_SET{$ELSE}FD_SET{$ENDIF}( socket, sockset );
+      end;
+procedure ENET_SOCKETSET_REMOVE( var sockset: ENetSocketSet; socket: ENetSocket ); inline;
+    begin {$IFNDEF WINDOWS}fpFD_CLR{$ELSE}FD_CLR{$ENDIF}( socket, sockset );
+      end;
+
+function ENET_SOCKETSET_CHECK( var sockset: ENetSocketSet; socket: ENetSocket ): Boolean; inline;
+   begin Result := {$IFNDEF WINDOWS} fpFD_ISSET( socket, sockset ) <> 0
+                               {$ELSE}   FD_ISSET( socket, sockset ) {$ENDIF};
+     end;
+
+end.
+
diff --git a/src/lib/enet/enetprotocol.pp b/src/lib/enet/enetprotocol.pp
new file mode 100644 (file)
index 0000000..520600e
--- /dev/null
@@ -0,0 +1,214 @@
+{$mode objfpc}
+unit enetprotocol;
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enetprotocol.pp
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original file: protocol.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+
+uses enettypes;
+
+const
+   ENET_PROTOCOL_MINIMUM_MTU             = 576;
+   ENET_PROTOCOL_MAXIMUM_MTU             = 4096;
+   ENET_PROTOCOL_MAXIMUM_PACKET_COMMANDS = 32;
+   ENET_PROTOCOL_MINIMUM_WINDOW_SIZE     = 4096;
+   ENET_PROTOCOL_MAXIMUM_WINDOW_SIZE     = 65536;
+   ENET_PROTOCOL_MINIMUM_CHANNEL_COUNT   = 1;
+   ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT   = 255;
+   ENET_PROTOCOL_MAXIMUM_PEER_ID         = $FFF;
+   ENET_PROTOCOL_MAXIMUM_FRAGMENT_COUNT  = 1024 * 1024;
+
+   // ENetProtocolFlag
+   ENET_PROTOCOL_COMMAND_FLAG_ACKNOWLEDGE = 1 shl 7;
+   ENET_PROTOCOL_COMMAND_FLAG_UNSEQUENCED = 1 shl 6;
+   ENET_PROTOCOL_HEADER_FLAG_COMPRESSED   = 1 shl 14;
+   ENET_PROTOCOL_HEADER_FLAG_SENT_TIME    = 1 shl 15;
+   ENET_PROTOCOL_HEADER_FLAG_MASK         = ENET_PROTOCOL_HEADER_FLAG_COMPRESSED or
+                                            ENET_PROTOCOL_HEADER_FLAG_SENT_TIME;
+   ENET_PROTOCOL_HEADER_SESSION_MASK      = 3 shl 12;
+   ENET_PROTOCOL_HEADER_SESSION_SHIFT     = 12;
+
+type
+  ENetProtocolCommand = ( ENET_PROTOCOL_COMMAND_NONE,
+                          ENET_PROTOCOL_COMMAND_ACKNOWLEDGE,
+                          ENET_PROTOCOL_COMMAND_CONNECT,
+                          ENET_PROTOCOL_COMMAND_VERIFY_CONNECT,
+                          ENET_PROTOCOL_COMMAND_DISCONNECT,
+                          ENET_PROTOCOL_COMMAND_PING,
+                          ENET_PROTOCOL_COMMAND_SEND_RELIABLE,
+                          ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE,
+                          ENET_PROTOCOL_COMMAND_SEND_FRAGMENT,
+                          ENET_PROTOCOL_COMMAND_SEND_UNSEQUENCED,
+                          ENET_PROTOCOL_COMMAND_BANDWIDTH_LIMIT,
+                          ENET_PROTOCOL_COMMAND_THROTTLE_CONFIGURE,
+                          ENET_PROTOCOL_COMMAND_SEND_UNRELIABLE_FRAGMENT,
+                          ENET_PROTOCOL_COMMAND_COUNT,
+                          ENET_PROTOCOL_COMMAND_MASK = $0F                );
+
+  ENetProtocolFlag = Integer; //alias for FPC-uncompatible enum, placed in const
+
+{$PACKRECORDS 1}
+
+  pENetProtocolHeader = ^ENetProtocolHeader;
+  ENetProtocolHeader = record
+    peerID   : enet_uint16;
+    sentTime : enet_uint16;
+  end;
+
+  pENetProtocolCommandHeader = ^ENetProtocolCommandHeader;
+  ENetProtocolCommandHeader = record
+    command                : enet_uint8;
+    channelID              : enet_uint8;
+    reliableSequenceNumber : enet_uint16;
+  end;
+
+  pENetProtocolAcknowledge = ^ENetProtocolAcknowledge;
+  ENetProtocolAcknowledge = record
+    header                         : ENetProtocolCommandHeader;
+    receivedReliableSequenceNumber : enet_uint16;
+    receivedSentTime               : enet_uint16;
+  end;
+
+  pENetProtocolConnect = ^ENetProtocolConnect;
+  ENetProtocolConnect = record
+    header                     : ENetProtocolCommandHeader;
+    outgoingPeerID             : enet_uint16;
+    incomingSessionID          : enet_uint8;
+    outgoingSessionID          : enet_uint8;
+    mtu                        : enet_uint32;
+    windowSize                 : enet_uint32;
+    channelCount               : enet_uint32;
+    incomingBandwidth          : enet_uint32;
+    outgoingBandwidth          : enet_uint32;
+    packetThrottleInterval     : enet_uint32;
+    packetThrottleAcceleration : enet_uint32;
+    packetThrottleDeceleration : enet_uint32;
+    connectID                  : enet_uint32;
+    data                       : enet_uint32;
+  end;
+
+  pENetProtocolVerifyConnect = ^ENetProtocolVerifyConnect;
+  ENetProtocolVerifyConnect = record
+    header                     : ENetProtocolCommandHeader;
+    outgoingPeerID             : enet_uint16;
+    incomingSessionID          : enet_uint8;
+    outgoingSessionID          : enet_uint8;
+    mtu                        : enet_uint32;
+    windowSize                 : enet_uint32;
+    channelCount               : enet_uint32;
+    incomingBandwidth          : enet_uint32;
+    outgoingBandwidth          : enet_uint32;
+    packetThrottleInterval     : enet_uint32;
+    packetThrottleAcceleration : enet_uint32;
+    packetThrottleDeceleration : enet_uint32;
+    connectID                  : enet_uint32;
+  end;
+
+  pENetProtocolBandwidthLimit = ^ENetProtocolBandwidthLimit;
+  ENetProtocolBandwidthLimit = record
+    header            : ENetProtocolCommandHeader;
+    incomingBandwidth : enet_uint32;
+    outgoingBandwidth : enet_uint32;
+  end;
+
+  pENetProtocolThrottleConfigure = ^ENetProtocolThrottleConfigure;
+  ENetProtocolThrottleConfigure = record
+    header                     : ENetProtocolCommandHeader;
+    packetThrottleInterval     : enet_uint32;
+    packetThrottleAcceleration : enet_uint32;
+    packetThrottleDeceleration : enet_uint32;
+  end;
+
+  pENetProtocolDisconnect = ^ENetProtocolDisconnect;
+  ENetProtocolDisconnect = record
+    header : ENetProtocolCommandHeader;
+    data   : enet_uint32;
+  end;
+
+  pENetProtocolPing = ^ENetProtocolPing;
+  ENetProtocolPing = record
+    header : ENetProtocolCommandHeader;
+  end;
+
+  pENetProtocolSendReliable = ^ENetProtocolSendReliable;
+  ENetProtocolSendReliable = record
+    header     : ENetProtocolCommandHeader;
+    dataLength : enet_uint16;
+  end;
+
+  pENetProtocolSendUnreliable = ^ENetProtocolSendUnreliable;
+  ENetProtocolSendUnreliable = record
+    header                   : ENetProtocolCommandHeader;
+    unreliableSequenceNumber : enet_uint16;
+    dataLength               : enet_uint16;
+  end;
+
+  pENetProtocolSendUnsequenced = ^ENetProtocolSendUnsequenced;
+  ENetProtocolSendUnsequenced = record
+    header           : ENetProtocolCommandHeader;
+    unsequencedGroup : enet_uint16;
+    dataLength       : enet_uint16;
+  end;
+
+  pENetProtocolSendFragment = ^ENetProtocolSendFragment;
+  ENetProtocolSendFragment = record
+    header              : ENetProtocolCommandHeader;
+    startSequenceNumber : enet_uint16;
+    dataLength          : enet_uint16;
+    fragmentCount       : enet_uint32;
+    fragmentNumber      : enet_uint32;
+    totalLength         : enet_uint32;
+    fragmentOffset      : enet_uint32;
+  end;
+
+  pENetProtocol = ^TENetProtocol;
+  TENetProtocol = record //union
+  case Byte of
+  0 : (header            : ENetProtocolCommandHeader);
+  1 : (acknowledge       : ENetProtocolAcknowledge);
+  2 : (connect           : ENetProtocolConnect);
+  3 : (verifyConnect     : ENetProtocolVerifyConnect);
+  4 : (disconnect        : ENetProtocolDisconnect);
+  5 : (ping              : ENetProtocolPing);
+  6 : (sendReliable      : ENetProtocolSendReliable);
+  7 : (sendUnreliable    : ENetProtocolSendUnreliable);
+  8 : (sendUnsequenced   : ENetProtocolSendUnsequenced);
+  9 : (sendFragment      : ENetProtocolSendFragment);
+  10: (bandwidthLimit    : ENetProtocolBandwidthLimit);
+  11: (throttleConfigure : ENetProtocolThrottleConfigure);
+  end;
+
+{$PACKRECORDS DEFAULT}
+
+implementation
+
+end.
+
diff --git a/src/lib/enet/enettime.pp b/src/lib/enet/enettime.pp
new file mode 100644 (file)
index 0000000..eb1be53
--- /dev/null
@@ -0,0 +1,69 @@
+{$mode objfpc}
+unit enettime;
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enettime.pp
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original file: time.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+
+const
+  ENET_TIME_OVERFLOW = 86400000;
+
+//inline macros
+function ENET_TIME_LESS( const a, b: LongInt ): Boolean; inline;
+function ENET_TIME_GREATER( const a, b: LongInt ): Boolean; inline;
+
+function ENET_TIME_LESS_EQUAL( const a, b: LongInt ): Boolean; inline;
+function ENET_TIME_GREATER_EQUAL( const a, b: LongInt ): Boolean; inline;
+
+function ENET_TIME_DIFFERENCE( const a, b: LongInt ): LongInt; inline;
+
+implementation
+
+function ENET_TIME_LESS( const a, b: LongInt ): Boolean; inline;
+   begin Result := a - b >= ENET_TIME_OVERFLOW;
+     end;
+function ENET_TIME_GREATER( const a, b: LongInt ): Boolean; inline;
+   begin Result := b - a >= ENET_TIME_OVERFLOW;
+     end;
+
+function ENET_TIME_LESS_EQUAL( const a, b: LongInt ): Boolean; inline;
+   begin Result := not ENET_TIME_GREATER( a, b );
+     end;
+function ENET_TIME_GREATER_EQUAL( const a, b: LongInt ): Boolean; inline;
+   begin Result := not ENET_TIME_LESS( a, b );
+     end;
+
+function ENET_TIME_DIFFERENCE( const a, b: LongInt ): LongInt; inline;
+   begin if a - b >= ENET_TIME_OVERFLOW then Result := b - a else Result := a - b;
+     end;
+
+end.
+
diff --git a/src/lib/enet/enettypes.pp b/src/lib/enet/enettypes.pp
new file mode 100644 (file)
index 0000000..53790ce
--- /dev/null
@@ -0,0 +1,55 @@
+{$mode objfpc}
+unit enettypes;
+
+{
+  ENet - Reliable UDP networking library
+
+  FreePascal DLL header: enettypes.pp
+  Copyright (c) 2015 Dmitry D. Chernov aka Black Doomer
+
+  Original file: types.h
+  Copyright (c) 2002-2014 Lee Salzman
+
+  Version 1 for 1.3.12: 25.02.2015
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+}
+
+interface
+
+type
+  enet_size_t = SizeUInt; //alias for C size_t
+  penet_size_t = ^enet_size_t;
+
+  enet_int = LongInt; //alias for C int
+  penet_int = ^enet_int;
+
+  enet_uint8 = Byte;
+  penet_uint8 = ^enet_uint8;
+
+  enet_uint16 = Word;
+  penet_uint16 = ^enet_uint16;
+
+  enet_uint32 = LongWord;
+  penet_uint32 = ^enet_uint32;
+
+implementation
+
+end.
+
diff --git a/src/lib/fmod/fmod.inc b/src/lib/fmod/fmod.inc
new file mode 100644 (file)
index 0000000..b764206
--- /dev/null
@@ -0,0 +1,23 @@
+{$IFDEF WIN32}
+{$DEFINE MSWINDOWS}
+{$ENDIF}
+
+{$IFDEF FPC}
+  {$MODE DELPHI}
+  {$PACKRECORDS C}
+{$ENDIF}
+
+{$IFDEF VER110}
+  {$DEFINE DELPHI_5_OR_LOWER}
+{$ELSE}
+  {$IFDEF VER120}
+    {$DEFINE DELPHI_5_OR_LOWER}
+  {$ELSE}
+    {$IFDEF VER130}
+      {$DEFINE DELPHI_5_OR_LOWER}
+    {$ENDIF}
+  {$ENDIF}
+{$ENDIF}
+
+(* Force four-byte enums *)
+{$Z4}
\ No newline at end of file
diff --git a/src/lib/fmod/fmod.pas b/src/lib/fmod/fmod.pas
new file mode 100644 (file)
index 0000000..038ecad
--- /dev/null
@@ -0,0 +1,1004 @@
+(* ============================================================================================= *)
+(* FMOD Ex - Main Pascal header file. Copyright (c), Firelight Technologies Pty, Ltd. 2004-2008. *)
+(*                                                                                               *)
+(* This header is for statically linking to the FMOD library.                                    *)
+(*                                                                                               *)
+(* ============================================================================================= *)
+
+unit fmod;
+
+{$I fmod.inc}
+
+interface
+
+uses
+  fmodtypes;
+
+(* ========================================================================================== *)
+(* FUNCTION PROTOTYPES                                                                        *)
+(* ========================================================================================== *)
+
+(*
+    FMOD System memory functions (optional).
+*)
+
+function FMOD_Memory_Initialize           (poolmem: Pointer; poollen: Integer; useralloc: FMOD_MEMORY_ALLOCCALLBACK; userrealloc: FMOD_MEMORY_REALLOCCALLBACK; userfree: FMOD_MEMORY_FREECALLBACK): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Memory_GetStats             (var currentalloced: Integer; var maxalloced: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    FMOD System factory functions.  Use this to create an FMOD System Instance.  below you will see FMOD_System_Init/Close to get started.
+*)
+
+function FMOD_System_Create               (var system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Release              (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    'System' API
+*)
+
+(*
+     Pre-init functions.
+*)
+
+function FMOD_System_SetOutput              (system: FMOD_SYSTEM; output: FMOD_OUTPUTTYPE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetOutput              (system: FMOD_SYSTEM; var output: FMOD_OUTPUTTYPE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetNumDrivers          (system: FMOD_SYSTEM; var numdrivers: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_GetDriverName          (system: FMOD_SYSTEM; id: Integer; name: PChar; namelen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetDriverCaps          (system: FMOD_SYSTEM; id: Integer; var caps: Cardinal; var minfrequency, maxfrequency: Integer; var controlpanelspeakermode: FMOD_SPEAKERMODE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetDriver              (system: FMOD_SYSTEM; driver: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetDriver              (system: FMOD_SYSTEM; var driver: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetHardwareChannels    (system: FMOD_SYSTEM; min2d: Integer; max2d: Integer; min3d: Integer; max3d: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetHardwareChannels    (system: FMOD_SYSTEM; var numhw2d: Integer; var numhw3d: Integer; var total: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetSoftwareChannels    (system: FMOD_SYSTEM; numsoftwarechannels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetSoftwareChannels    (system: FMOD_SYSTEM; var numsoftwarechannels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetSoftwareFormat      (system: FMOD_SYSTEM; samplerate: Integer; format: FMOD_SOUND_FORMAT; numoutputchannels: Integer; maxinputchannels: Integer; resamplemethod: FMOD_DSP_RESAMPLER): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetSoftwareFormat      (system: FMOD_SYSTEM; var samplerate: Integer; var format: FMOD_SOUND_FORMAT; var numoutputchannels: Integer; var maxinputchannels: Integer; var bits: Integer; var resamplemethod: FMOD_DSP_RESAMPLER): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetDSPBufferSize       (system: FMOD_SYSTEM; bufferlength: Cardinal; numbuffers: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetDSPBufferSize       (system: FMOD_SYSTEM; var bufferlength: Cardinal; var numbuffers: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetFileSystem          (system: FMOD_SYSTEM; useropen: FMOD_FILE_OPENCALLBACK; userclose: FMOD_FILE_CLOSECALLBACK; userread: FMOD_FILE_READCALLBACK; userseek: FMOD_FILE_SEEKCALLBACK): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_AttachFileSystem       (system: FMOD_SYSTEM; useropen: FMOD_FILE_OPENCALLBACK; userclose: FMOD_FILE_CLOSECALLBACK; userread: FMOD_FILE_READCALLBACK; userseek: FMOD_FILE_SEEKCALLBACK): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetAdvancedSettings    (system: FMOD_SYSTEM; var settings:FMOD_ADVANCEDSETTINGS): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetAdvancedSettings    (system: FMOD_SYSTEM; var settings:FMOD_ADVANCEDSETTINGS): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetSpeakerMode         (system: FMOD_SYSTEM; speakermode: FMOD_SPEAKERMODE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetSpeakerMode         (system: FMOD_SYSTEM; var speakermode: FMOD_SPEAKERMODE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Plug-in support
+*)
+
+function FMOD_System_SetPluginPath          (system: FMOD_SYSTEM; const path: PChar): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_LoadPlugin             (system: FMOD_SYSTEM; const filename: PChar; var plugintype: FMOD_PLUGINTYPE; var index: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetNumPlugins          (system: FMOD_SYSTEM; plugintype: FMOD_PLUGINTYPE; var numplugins: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetPluginInfo          (system: FMOD_SYSTEM; plugintype: FMOD_PLUGINTYPE; index: Integer; name: PChar; namelen: Integer; version: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_UnloadPlugin           (system: FMOD_SYSTEM; plugintype: FMOD_PLUGINTYPE; index: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetOutputByPlugin      (system: FMOD_SYSTEM; index: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetOutputByPlugin      (system: FMOD_SYSTEM; var index: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_CreateCodec            (system: FMOD_SYSTEM; description:FMOD_CODEC_DESCRIPTION): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Init/Close
+*)
+
+function FMOD_System_Init                   (system: FMOD_SYSTEM; maxchannels: Integer; flags: Cardinal; const extradriverdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Close                  (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     General post-init system functions
+*)
+
+function FMOD_System_Update                 (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_UpdateFinished         (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_System_Set3DSettings          (system: FMOD_SYSTEM; dopplerscale, distancefactor, rolloffscale: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Get3DSettings          (system: FMOD_SYSTEM; var dopplerscale: Single; var distancefactor: Single; var rolloffscale: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Set3DNumListeners      (system: FMOD_SYSTEM; numlisteners: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Get3DNumListeners      (system: FMOD_SYSTEM; var numlisteners: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Set3DListenerAttributes(system: FMOD_SYSTEM; listener: Integer; const pos: FMOD_VECTOR; const vel: FMOD_VECTOR; const forward_: FMOD_VECTOR; const up: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_Get3DListenerAttributes(system: FMOD_SYSTEM; listener: Integer; var pos: FMOD_VECTOR; var vel: FMOD_VECTOR; var forward_: FMOD_VECTOR; var up: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+// function FMOD_System_SetSpeakerPosition     (system: FMOD_SYSTEM; speaker: FMOD_SPEAKER; x:Single; y:Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_GetSpeakerPosition     (system: FMOD_SYSTEM; speaker: FMOD_SPEAKER; var x:Single; var y:Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetStreamBufferSize    (system: FMOD_SYSTEM; filebuffersize: Cardinal; filebuffersizetype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetStreamBufferSize    (system: FMOD_SYSTEM; var filebuffersize: Cardinal; filebuffersizetype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     System information functions.
+*)
+
+function FMOD_System_GetVersion             (system: FMOD_SYSTEM; var version: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetOutputHandle        (system: FMOD_SYSTEM; var handle: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetChannelsPlaying     (system: FMOD_SYSTEM; var channels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetCPUUsage            (system: FMOD_SYSTEM; var dsp, stream, update, total: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetSoundRAM            (system: FMOD_SYSTEM; var currentalloced: Integer; var maxalloced: Integer; var total: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetNumCDROMDrives      (system: FMOD_SYSTEM; var num: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetCDROMDriveName      (system: FMOD_SYSTEM; drivenum: Integer; name: PChar; namelen: Integer; scsiaddr: PChar; scsiaddrlen: Integer; devicename: PChar; devicenamelen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetSpectrum            (system: FMOD_SYSTEM; var spectrumarray: Single; numvalues: Integer; channeloffset: Integer; windowtype: FMOD_DSP_FFT_WINDOW): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetWaveData            (system: FMOD_SYSTEM; var wavearray: Single; numvalues: Integer; channeloffset: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Sound/DSP/Channel/FX creation and retrieval.
+*)
+
+function FMOD_System_CreateSound            (system: FMOD_SYSTEM; const name_or_data: PChar; mode: Cardinal; exinfo: PFMOD_CREATESOUNDEXINFO; var sound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_CreateStream           (system: FMOD_SYSTEM; const name_or_data: PChar; mode: Cardinal; exinfo: PFMOD_CREATESOUNDEXINFO; var sound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_CreateDSP              (system: FMOD_SYSTEM; const description: FMOD_DSP_DESCRIPTION; var dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_CreateDSPByType        (system: FMOD_SYSTEM; type_: FMOD_DSP_TYPE; var dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_CreateDSPByIndex       (system: FMOD_SYSTEM; index: Integer; var dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_CreateChannelGroup     (system: FMOD_SYSTEM; const name: PChar; var channelgroup: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_System_PlaySound              (system: FMOD_SYSTEM; channelid: Integer; sound: FMOD_SOUND; paused: FMOD_BOOL; var channel: FMOD_CHANNEL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_PlayDSP                (system: FMOD_SYSTEM; channelid: Integer; dsp: FMOD_DSP; paused: FMOD_BOOL; var channel: FMOD_CHANNEL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetChannel             (system: FMOD_SYSTEM; channelid: Integer; var channel: FMOD_CHANNEL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetMasterChannelGroup  (system: FMOD_SYSTEM; var channelgroup: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Reverb API
+*)
+
+function FMOD_System_SetReverbProperties    (system: FMOD_SYSTEM; const prop: FMOD_REVERB_PROPERTIES): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetReverbProperties    (system: FMOD_SYSTEM; var prop: FMOD_REVERB_PROPERTIES): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+Function FMOD_System_SetReverbAmbientProperties(system:FMOD_SYSTEM; var prop:FMOD_REVERB_PROPERTIES):FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     System level DSP access.
+*)
+
+function FMOD_System_GetDSPHead             (system: FMOD_SYSTEM; var dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_AddDSP                 (system: FMOD_SYSTEM; dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_LockDSP                (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_UnlockDSP              (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Recording API
+*)
+
+// function FMOD_System_SetRecordDriver        (system: FMOD_SYSTEM; driver: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_GetRecordDriver        (system: FMOD_SYSTEM; var driver: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetRecordNumDrivers    (system: FMOD_SYSTEM; var numdrivers: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_System_GetRecordDriverName    (system: FMOD_SYSTEM; id: Integer; name: PChar; namelen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetRecordPosition      (system: FMOD_SYSTEM; var position: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_System_RecordStart            (system: FMOD_SYSTEM; sound: FMOD_SOUND; loop: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_RecordStop             (system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_IsRecording            (system: FMOD_SYSTEM; var recording: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Geometry API.
+*)
+
+function FMOD_System_CreateGeometry         (system: FMOD_SYSTEM; maxPolygons: Integer; maxVertices: Integer; var geometry: FMOD_GEOMETRY): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetGeometrySettings    (system: FMOD_SYSTEM; maxWorldSize: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetGeometrySettings    (system: FMOD_SYSTEM; var maxWorldSize: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_LoadGeometry           (system: FMOD_SYSTEM; data: Pointer; dataSize: Integer; var geometry: FMOD_GEOMETRY): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Network functions.
+*)
+
+function FMOD_System_SetNetworkProxy        (system: FMOD_SYSTEM; const proxy: PChar): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetNetworkProxy        (system: FMOD_SYSTEM; proxy: PChar; proxylen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_SetNetworkTimeout      (system: FMOD_SYSTEM; timeout: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetNetworkTimeout      (system: FMOD_SYSTEM; var timeout: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_System_SetUserData            (system: FMOD_SYSTEM; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_System_GetUserData            (system: FMOD_SYSTEM; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    'Sound' API
+*)
+
+function FMOD_Sound_Release                 (sound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetSystemObject         (sound: FMOD_SOUND; var system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Standard sound manipulation functions.
+*)
+
+function FMOD_Sound_Lock                    (sound: FMOD_SOUND; offset, length: Cardinal; var ptr1: Pointer; var ptr2: Pointer; var len1: Cardinal; var len2: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Unlock                  (sound: FMOD_SOUND; ptr1, ptr2: Pointer; len1, len2: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SetDefaults             (sound: FMOD_SOUND; frequency, volume, pan: Single; const levels: Single; priority: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetDefaults             (sound: FMOD_SOUND; var frequency: Single; var volume: Single; var pan: Single; var priority: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SetVariations           (sound: FMOD_SOUND; frequency, volume, pan: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetVariations           (sound: FMOD_SOUND; var frequency: Single; var volume: Single; var pan: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Set3DMinMaxDistance     (sound: FMOD_SOUND; min, max: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Get3DMinMaxDistance     (sound: FMOD_SOUND; var min: Single; var max: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Set3DConeSettings       (sound: FMOD_SOUND; insideconeangle: Single; outsideconeangle: Single; outsidevolume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Get3DConeSettings       (sound: FMOD_SOUND; var insideconeangle: Single; var outsideconeangle: Single; var outsidevolume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Set3DCustomRolloff      (sound: FMOD_SOUND; points: FMOD_VECTOR; numpoints: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_Get3DCustomRolloff      (sound: FMOD_SOUND; var points: FMOD_VECTOR; var numpoints: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SetSubSound             (sound: FMOD_SOUND; index: Integer; subsound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetSubSound             (sound: FMOD_SOUND; index: Integer; var subsound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SetSubSoundSentence     (sound: FMOD_SOUND; var subsoundlist: Integer; numsubsounds: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetName                 (sound: FMOD_SOUND; name: PChar; namelen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetLength               (sound: FMOD_SOUND; var length: Cardinal; lengthtype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetFormat               (sound: FMOD_SOUND; var type_: FMOD_SOUND_TYPE; var format: FMOD_SOUND_FORMAT; var channels: Integer; var bits: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetNumSubSounds         (sound: FMOD_SOUND; var numsubsounds: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetNumTags              (sound: FMOD_SOUND; var numtags: Integer; var numtagsupdated: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetTag                  (sound: FMOD_SOUND; const name: PChar; index: Integer; var tag: FMOD_TAG): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetOpenState            (sound: FMOD_SOUND; var openstate: FMOD_OPENSTATE; var percentbuffered: Cardinal; var starving: Boolean): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_ReadData                (sound: FMOD_SOUND; buffer: Pointer; lenbytes: Cardinal; var read: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SeekData                (sound: FMOD_SOUND; pcm: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Synchronization point API.  These points can come from markers embedded in wav files, and can also generate channel callbacks.
+*)
+
+function FMOD_Sound_GetNumSyncPoints        (sound: FMOD_SOUND; var numsyncpoints: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetSyncPoint            (sound: FMOD_SOUND; index: Integer; var point: FMOD_SYNCPOINT): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetSyncPointInfo        (sound: FMOD_SOUND; point: FMOD_SYNCPOINT; name: PChar; namelen: Integer; offset: Cardinal; offsettype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_AddSyncPoint            (sound: FMOD_SOUND; offset: Cardinal; offsettype: Cardinal; name: PChar; var point: FMOD_SYNCPOINT): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_DeleteSyncPoint         (sound: FMOD_SOUND; point: FMOD_SYNCPOINT): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Functions also in Channel class but here they are the 'default' to save having to change it in Channel all the time.
+*)
+
+function FMOD_Sound_SetMode                 (sound: FMOD_SOUND; mode: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetMode                 (sound: FMOD_SOUND; var mode: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SetLoopCount            (sound: FMOD_SOUND; loopcount: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetLoopCount            (sound: FMOD_SOUND; var loopcount: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_SetLoopPoints           (sound: FMOD_SOUND; loopstart: Cardinal; loopstarttype: Cardinal; loopend: Cardinal; loopendtype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetLoopPoints           (sound: FMOD_SOUND; var loopstart: Cardinal; loopstarttype: Cardinal; var loopend: Cardinal; loopendtype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_Sound_SetUserData             (sound: FMOD_SOUND; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Sound_GetUserData             (sound: FMOD_SOUND; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    'Channel' API
+*)
+
+function FMOD_Channel_GetSystemObject       (channel: FMOD_CHANNEL; var system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_Channel_Stop                  (channel: FMOD_CHANNEL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetPaused             (channel: FMOD_CHANNEL; paused: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetPaused             (channel: FMOD_CHANNEL; var paused: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetVolume             (channel: FMOD_CHANNEL; volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetVolume             (channel: FMOD_CHANNEL; var volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetFrequency          (channel: FMOD_CHANNEL; frequency: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetFrequency          (channel: FMOD_CHANNEL; var frequency: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetPan                (channel: FMOD_CHANNEL; pan: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetPan                (channel: FMOD_CHANNEL; var pan: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetDelay              (channel: FMOD_CHANNEL; startdelay: Cardinal; enddelay: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetDelay              (channel: FMOD_CHANNEL; var startdelay: Cardinal; var enddelay: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetSpeakerMix         (channel: FMOD_CHANNEL; frontleft, frontright, center, lfe, backleft, backright, sideleft, sideright: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetSpeakerMix         (channel: FMOD_CHANNEL; var frontleft: Single; var frontright: Single; var center: Single; var lfe: Single; var backleft: Single; var backright: Single; var sideleft: Single; var sideright: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetSpeakerLevels      (channel: FMOD_CHANNEL; speaker: FMOD_SPEAKER; const levels: Single; numlevels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetSpeakerLevels      (channel: FMOD_CHANNEL; speaker: FMOD_SPEAKER; var levels: Single; var numlevels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetMute               (channel: FMOD_CHANNEL; mute: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetMute               (channel: FMOD_CHANNEL; var mute: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetPriority           (channel: FMOD_CHANNEL; priority: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetPriority           (channel: FMOD_CHANNEL; var priority: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetPosition           (channel: FMOD_CHANNEL; position: Cardinal; postype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetPosition           (channel: FMOD_CHANNEL; var position: Cardinal; postype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetReverbProperties   (channel: FMOD_CHANNEL; const prop: FMOD_REVERB_CHANNELPROPERTIES): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetReverbProperties   (channel: FMOD_CHANNEL; var prop: FMOD_REVERB_CHANNELPROPERTIES): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetChannelGroup       (channel: FMOD_CHANNEL; channelgroup: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetChannelGroup       (channel: FMOD_CHANNEL; var channelgroup: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetCallback           (channel: FMOD_CHANNEL; callback: FMOD_CHANNEL_CALLBACK): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     3D functionality.
+*)
+
+function FMOD_Channel_Set3DAttributes       (channel: FMOD_CHANNEL; const pos: FMOD_VECTOR; const vel: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DAttributes       (channel: FMOD_CHANNEL; var pos: FMOD_VECTOR; var vel: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DMinMaxDistance   (channel: FMOD_CHANNEL; mindistance, maxdistance: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DMinMaxDistance   (channel: FMOD_CHANNEL; var mindistance: Single; var maxdistance: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DConeSettings     (channel: FMOD_CHANNEL; insideconeangle: Single; outsideconeangle: Single; outsidevolume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DConeSettings     (channel: FMOD_CHANNEL; var insideconeangle: Single; var outsideconeangle: Single; var outsidevolume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DConeOrientation  (channel: FMOD_CHANNEL; var orientation: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DConeOrientation  (channel: FMOD_CHANNEL; var orientation: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DCustomRolloff    (channel: FMOD_CHANNEL; points: FMOD_VECTOR; numpoints: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DCustomRolloff    (channel: FMOD_CHANNEL; var points: FMOD_VECTOR; var numpoints: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DOcclusion        (channel: FMOD_CHANNEL; directOcclusion: Single; reverbOcclusion: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DOcclusion        (channel: FMOD_CHANNEL; var directOcclusion: Single; var reverbOcclusion: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DSpread           (channel: FMOD_CHANNEL; angle: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DSpread           (channel: FMOD_CHANNEL; var angle: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Set3DPanLevel         (channel: FMOD_CHANNEL; level: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_Get3DPanLevel         (channel: FMOD_CHANNEL; var level: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     DSP functionality only for channels playing sounds created with FMOD_SOFTWARE.
+*)
+
+function FMOD_Channel_GetDSPHead            (channel: FMOD_CHANNEL; var dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_AddDSP                (channel: FMOD_CHANNEL; dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Information only functions.
+*)
+
+function FMOD_Channel_IsPlaying             (channel: FMOD_CHANNEL; var isplaying: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_IsVirtual             (channel: FMOD_CHANNEL; var isvirtual: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetAudibility         (channel: FMOD_CHANNEL; var audibility: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetCurrentSound       (channel: FMOD_CHANNEL; var sound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetSpectrum           (channel: FMOD_CHANNEL; spectrumarray: PSingle; numvalues: Integer; channeloffset: Integer; windowtype: FMOD_DSP_FFT_WINDOW): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetWaveData           (channel: FMOD_CHANNEL; var wavearray: Single; numvalues: Integer; channeloffset: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetIndex              (channel: FMOD_CHANNEL; var index: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Functions also found in Sound class but here they can be set per channel.
+*)
+
+function FMOD_Channel_SetMode               (channel: FMOD_CHANNEL; mode: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetMode               (channel: FMOD_CHANNEL; var mode: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetLoopCount          (channel: FMOD_CHANNEL; loopcount: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetLoopCount          (channel: FMOD_CHANNEL; var loopcount: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_SetLoopPoints         (channel: FMOD_CHANNEL; loopstart: Cardinal; loopstarttype: Cardinal; loopend: Cardinal; loopendtype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetLoopPoints         (channel: FMOD_CHANNEL; var loopstart: Cardinal; loopstarttype: Cardinal; var loopend: Cardinal; loopendtype: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_Channel_SetUserData           (channel: FMOD_CHANNEL; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Channel_GetUserData           (channel: FMOD_CHANNEL; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    'ChannelGroup' API
+*)
+
+function FMOD_ChannelGroup_Release          (channelgroup: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetSystemObject  (channelgroup: FMOD_CHANNELGROUP; var system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Channelgroup scale values.  (scales the current volume or pitch of all channels and channel groups, DOESN'T overwrite)
+*)
+
+function FMOD_ChannelGroup_SetVolume        (channelgroup: FMOD_CHANNELGROUP; volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetVolume        (channelgroup: FMOD_CHANNELGROUP; var volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_SetPitch         (channelgroup: FMOD_CHANNELGROUP; pitch: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetPitch         (channelgroup: FMOD_CHANNELGROUP; var pitch: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Channelgroup override values.  (recursively overwrites whatever settings the channels had)
+*)
+
+function FMOD_ChannelGroup_Stop                    (channelgroup: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_ChannelGroup_OverridePaused          (channelgroup: FMOD_CHANNELGROUP; paused: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_OverrideVolume          (channelgroup: FMOD_CHANNELGROUP; volume : Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_OverrideFrequency       (channelgroup: FMOD_CHANNELGROUP; frequency: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_OverridePan             (channelgroup: FMOD_CHANNELGROUP; pan: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_ChannelGroup_OverrideMute            (channelgroup: FMOD_CHANNELGROUP; mute: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_OverrideReverbProperties(channelgroup: FMOD_CHANNELGROUP; const prop: FMOD_REVERB_CHANNELPROPERTIES): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_Override3DAttributes    (channelgroup: FMOD_CHANNELGROUP; const pos: FMOD_VECTOR; const vel: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_OverrideSpeakerMix      (channelgroup: FMOD_CHANNELGROUP; frontleft, frontright, center, lfe, backleft, backright, sideleft, sideright: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Nested channel groups.
+*)
+
+function FMOD_ChannelGroup_AddGroup         (channelgroup: FMOD_CHANNELGROUP; group: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetNumGroups     (channelgroup: FMOD_CHANNELGROUP; var numgroups: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetGroup         (channelgroup: FMOD_CHANNELGROUP; index: Integer; var group: FMOD_CHANNELGROUP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     DSP functionality only for channel groups playing sounds created with FMOD_SOFTWARE.
+*)
+
+function FMOD_ChannelGroup_GetDSPHead       (channelgroup: FMOD_CHANNELGROUP; var dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_AddDSP           (channelgroup: FMOD_CHANNELGROUP; dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Information only functions.
+*)
+
+function FMOD_ChannelGroup_GetName          (channelgroup: FMOD_CHANNELGROUP; name: PChar; namelen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetNumChannels   (channelgroup: FMOD_CHANNELGROUP; var numchannels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetChannel       (channelgroup: FMOD_CHANNELGROUP; index: Integer; var channel: FMOD_CHANNEL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetSpectrum      (channelgroup: FMOD_CHANNELGROUP; var spectrumarray: Single; numvalues: Integer; channeloffset: Integer; windowtype: FMOD_DSP_FFT_WINDOW ): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetWaveData      (channelgroup: FMOD_CHANNELGROUP; var wavearray: Single; numvalues: Integer; channeloffset: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_ChannelGroup_SetUserData      (channelgroup: FMOD_CHANNELGROUP; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_ChannelGroup_GetUserData      (channelgroup: FMOD_CHANNELGROUP; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    'DSP' API
+*)
+
+function FMOD_DSP_Release                   (dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetSystemObject           (dsp: FMOD_DSP; var system: FMOD_SYSTEM): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Connection / disconnection / input and output enumeration.
+*)
+
+function FMOD_DSP_AddInput                  (dsp, target: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_DisconnectFrom            (dsp, target: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_Remove                    (dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetNumInputs              (dsp: FMOD_DSP; var numinputs: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetNumOutputs             (dsp: FMOD_DSP; var numoutputs: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetInput                  (dsp: FMOD_DSP; index: Integer; var input: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetOutput                 (dsp: FMOD_DSP; index: Integer; var output: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_SetInputMix               (dsp: FMOD_DSP; index: Integer; volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_GetInputMix               (dsp: FMOD_DSP; index: Integer; var volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_SetInputLevels            (dsp: FMOD_DSP; index: Integer; speaker: FMOD_SPEAKER; const levels: Single; numlevels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_GetInputLevels            (dsp: FMOD_DSP; index: Integer; speaker: FMOD_SPEAKER; const levels: Single; numlevels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_SetOutputMix              (dsp: FMOD_DSP; index: Integer; volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_GetOutputMix              (dsp: FMOD_DSP; index: Integer; var volume: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_SetOutputLevels           (dsp: FMOD_DSP; index: Integer; speaker: FMOD_SPEAKER; const levels: Single; numlevels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+// function FMOD_DSP_GetOutputLevels           (dsp: FMOD_DSP; index: Integer; speaker: FMOD_SPEAKER; const levels: Single; numlevels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     DSP unit control
+*)
+
+function FMOD_DSP_SetActive                 (dsp: FMOD_DSP; active: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetActive                 (dsp: FMOD_DSP; var active: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_SetBypass                 (dsp: FMOD_DSP; bypass: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetBypass                 (dsp: FMOD_DSP; var bypass: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_Reset                     (dsp: FMOD_DSP): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     DSP parameter control
+*)
+
+function FMOD_DSP_SetParameter              (dsp: FMOD_DSP; index: Integer; value: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetParameter              (dsp: FMOD_DSP; index: Integer; var value: Single; valuestr: PChar; valuestrlen: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetNumParameters          (dsp: FMOD_DSP; var numparams: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetParameterInfo          (dsp: FMOD_DSP; index: Integer; name: PChar; plabel: PChar; description: PChar; descriptionlen: Integer; var min: Single; var max: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_ShowConfigDialog          (dsp: FMOD_DSP; hwnd: Pointer; show: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     DSP attributes.
+*)
+
+function FMOD_DSP_GetInfo                   (dsp: FMOD_DSP; name:PChar; var version: cardinal; var channels:integer; var configwidth:integer; var configheight:integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_SetDefaults               (dsp: FMOD_DSP; frequency: Single; volume: Single; pan: Single; var levels: Single; priority: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetDefaults               (dsp: FMOD_DSP; var frequency: Single; var volume: Single; var pan: Single; var priority: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_DSP_SetUserData               (dsp: FMOD_DSP; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_DSP_GetUserData               (dsp: FMOD_DSP; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+    'Geometry' API
+*)
+
+function FMOD_Geometry_Release              (geometry: FMOD_GEOMETRY): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_Geometry_AddPolygon           (geometry: FMOD_GEOMETRY; directOcclusion: Single; reverbOcclusion: Single; doubleSided: FMOD_BOOL; numVertices: Integer; vertices: PFMOD_VECTOR; var polygonIndex: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetNumPolygons       (geometry: FMOD_GEOMETRY; var numPolygons: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetMaxPolygons       (geometry: FMOD_GEOMETRY; var maxPolygons: Integer; maxVertices: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetPolygonNumVertices(geometry: FMOD_GEOMETRY; polygonIndex: Integer; var numVertices: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_SetPolygonVertex     (geometry: FMOD_GEOMETRY; polygonIndex: Integer; vertexIndex: Integer; var vertex: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetPolygonVertex     (geometry: FMOD_GEOMETRY; polygonIndex: Integer; vertexIndex: Integer; var vertex: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_SetPolygonAttributes (geometry: FMOD_GEOMETRY; polygonIndex: Integer; directOcclusion: Single; reverbOcclusion: Single; doubleSided: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetPolygonAttributes (geometry: FMOD_GEOMETRY; polygonIndex: Integer; var directOcclusion: Single; var reverbOcclusion: Single; var doubleSided: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_Geometry_SetActive            (geometry: FMOD_GEOMETRY; active: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetActive            (geometry: FMOD_GEOMETRY; var active: FMOD_BOOL): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_SetRotation          (geometry: FMOD_GEOMETRY; var forward: FMOD_VECTOR; var up: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetRotation          (geometry: FMOD_GEOMETRY; var forward: FMOD_VECTOR; var up: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_SetPosition          (geometry: FMOD_GEOMETRY; var position: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetPosition          (geometry: FMOD_GEOMETRY; var position: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_SetScale             (geometry: FMOD_GEOMETRY; var scale: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetScale             (geometry: FMOD_GEOMETRY; var scale: FMOD_VECTOR): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_Save                 (geometry: FMOD_GEOMETRY; data: Pointer; var datasize: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_Geometry_SetUserData          (geometry: FMOD_GEOMETRY; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Geometry_GetUserData          (geometry: FMOD_GEOMETRY; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+function FMOD_Load                          (const libname: PChar): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+function FMOD_Unload                        (): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+implementation
+
+const
+{$IFDEF MSWINDOWS}
+  FMOD_DLL = 'fmodex.dll';
+{$ELSE}
+{$IFDEF LINUX}
+  FMOD_DLL = 'libfmodex.so';
+{$ENDIF}
+{$ENDIF}
+
+function FMOD_Load(const libname: PChar): FMOD_RESULT;
+begin
+  FMOD_Load := FMOD_OK;
+end;
+
+function FMOD_Unload(): FMOD_RESULT;
+begin
+  FMOD_Unload := FMOD_OK;
+end;
+
+(*
+    FMOD System memory functions (optional).
+*)
+
+function FMOD_Memory_Initialize             ; external FMOD_DLL;
+function FMOD_Memory_GetStats               ; external FMOD_DLL;
+
+(*
+    FMOD System factory functions.  Use this to create an FMOD System Instance.  below you will see FMOD_System_Init/Close to get started.
+*)
+
+function FMOD_System_Create                 ; external FMOD_DLL;
+function FMOD_System_Release                ; external FMOD_DLL;
+
+(*
+    'System' API
+*)
+
+(*
+     Pre-init functions.
+*)
+
+function FMOD_System_SetOutput              ; external FMOD_DLL;
+function FMOD_System_GetOutput              ; external FMOD_DLL;
+function FMOD_System_GetNumDrivers          ; external FMOD_DLL;
+// function FMOD_System_GetDriverName          ; external FMOD_DLL;
+function FMOD_System_GetDriverCaps          ; external FMOD_DLL;
+function FMOD_System_SetDriver              ; external FMOD_DLL;
+function FMOD_System_GetDriver              ; external FMOD_DLL;
+function FMOD_System_SetHardwareChannels    ; external FMOD_DLL;
+function FMOD_System_GetHardwareChannels    ; external FMOD_DLL;
+function FMOD_System_SetSoftwareChannels    ; external FMOD_DLL;
+function FMOD_System_GetSoftwareChannels    ; external FMOD_DLL;
+function FMOD_System_SetSoftwareFormat      ; external FMOD_DLL;
+function FMOD_System_GetSoftwareFormat      ; external FMOD_DLL;
+function FMOD_System_SetDSPBufferSize       ; external FMOD_DLL;
+function FMOD_System_GetDSPBufferSize       ; external FMOD_DLL;
+function FMOD_System_SetFileSystem          ; external FMOD_DLL;
+function FMOD_System_AttachFileSystem       ; external FMOD_DLL;
+function FMOD_System_SetAdvancedSettings    ; external FMOD_DLL;
+function FMOD_System_GetAdvancedSettings    ; external FMOD_DLL;
+function FMOD_System_SetSpeakerMode         ; external FMOD_DLL;
+function FMOD_System_GetSpeakerMode         ; external FMOD_DLL;
+
+(*
+     Plug-in support
+*)
+
+function FMOD_System_SetPluginPath          ; external FMOD_DLL;
+function FMOD_System_LoadPlugin             ; external FMOD_DLL;
+function FMOD_System_GetNumPlugins          ; external FMOD_DLL;
+function FMOD_System_GetPluginInfo          ; external FMOD_DLL;
+function FMOD_System_UnloadPlugin           ; external FMOD_DLL;
+function FMOD_System_SetOutputByPlugin      ; external FMOD_DLL;
+function FMOD_System_GetOutputByPlugin      ; external FMOD_DLL;
+// function FMOD_System_CreateCodec            ; external FMOD_DLL;
+
+(*
+     Init/Close
+*)
+
+function FMOD_System_Init                   ; external FMOD_DLL;
+function FMOD_System_Close                  ; external FMOD_DLL;
+
+(*
+     General post-init system functions
+*)
+
+function FMOD_System_Update                 ; external FMOD_DLL;
+// function FMOD_System_UpdateFinished         ; external FMOD_DLL;
+
+function FMOD_System_Set3DSettings          ; external FMOD_DLL;
+function FMOD_System_Get3DSettings          ; external FMOD_DLL;
+function FMOD_System_Set3DNumListeners      ; external FMOD_DLL;
+function FMOD_System_Get3DNumListeners      ; external FMOD_DLL;
+function FMOD_System_Set3DListenerAttributes; external FMOD_DLL;
+function FMOD_System_Get3DListenerAttributes; external FMOD_DLL;
+
+// function FMOD_System_SetSpeakerPosition     ; external FMOD_DLL;
+// function FMOD_System_GetSpeakerPosition     ; external FMOD_DLL;
+function FMOD_System_SetStreamBufferSize    ; external FMOD_DLL;
+function FMOD_System_GetStreamBufferSize    ; external FMOD_DLL;
+
+(*
+     System information functions.
+*)
+
+function FMOD_System_GetVersion             ; external FMOD_DLL;
+function FMOD_System_GetOutputHandle        ; external FMOD_DLL;
+function FMOD_System_GetChannelsPlaying     ; external FMOD_DLL;
+function FMOD_System_GetCPUUsage            ; external FMOD_DLL;
+function FMOD_System_GetSoundRAM            ; external FMOD_DLL;
+function FMOD_System_GetNumCDROMDrives      ; external FMOD_DLL;
+function FMOD_System_GetCDROMDriveName      ; external FMOD_DLL;
+function FMOD_System_GetSpectrum            ; external FMOD_DLL;
+function FMOD_System_GetWaveData            ; external FMOD_DLL;
+
+(*
+     Sound/DSP/Channel/FX creation and retrieval.
+*)
+
+function FMOD_System_CreateSound            ; external FMOD_DLL;
+function FMOD_System_CreateStream           ; external FMOD_DLL;
+function FMOD_System_CreateDSP              ; external FMOD_DLL;
+function FMOD_System_CreateDSPByType        ; external FMOD_DLL;
+// function FMOD_System_CreateDSPByIndex       ; external FMOD_DLL;
+function FMOD_System_CreateChannelGroup     ; external FMOD_DLL;
+
+function FMOD_System_PlaySound              ; external FMOD_DLL;
+function FMOD_System_PlayDSP                ; external FMOD_DLL;
+function FMOD_System_GetChannel             ; external FMOD_DLL;
+function FMOD_System_GetMasterChannelGroup  ; external FMOD_DLL;
+
+(*
+     Reverb API
+*)
+
+function FMOD_System_SetReverbProperties    ; external FMOD_DLL;
+function FMOD_System_GetReverbProperties    ; external FMOD_DLL;
+Function FMOD_System_SetReverbAmbientProperties; external fmod_dll;
+(*
+     System level DSP access.
+*)
+
+function FMOD_System_GetDSPHead             ; external FMOD_DLL;
+function FMOD_System_AddDSP                 ; external FMOD_DLL;
+function FMOD_System_LockDSP                ; external FMOD_DLL;
+function FMOD_System_UnlockDSP              ; external FMOD_DLL;
+
+(*
+     Recording API
+*)
+
+// function FMOD_System_SetRecordDriver        ; external FMOD_DLL;
+// function FMOD_System_GetRecordDriver        ; external FMOD_DLL;
+function FMOD_System_GetRecordNumDrivers    ; external FMOD_DLL;
+// function FMOD_System_GetRecordDriverName    ; external FMOD_DLL;
+function FMOD_System_GetRecordPosition      ; external FMOD_DLL;
+
+function FMOD_System_RecordStart            ; external FMOD_DLL;
+function FMOD_System_RecordStop             ; external FMOD_DLL;
+function FMOD_System_IsRecording            ; external FMOD_DLL;
+
+(*
+     Geometry API.
+*)
+
+function FMOD_System_CreateGeometry         ; external FMOD_DLL;
+function FMOD_System_SetGeometrySettings    ; external FMOD_DLL;
+function FMOD_System_GetGeometrySettings    ; external FMOD_DLL;
+function FMOD_System_LoadGeometry           ; external FMOD_DLL;
+
+(*
+     Network functions
+*)
+
+function FMOD_System_SetNetworkProxy        ; external FMOD_DLL;
+function FMOD_System_GetNetworkProxy        ; external FMOD_DLL;
+function FMOD_System_SetNetworkTimeout      ; external FMOD_DLL;
+function FMOD_System_GetNetworkTimeout      ; external FMOD_DLL;
+
+(*
+     Userdata set/get
+*)
+
+function FMOD_System_SetUserData            ; external FMOD_DLL;
+function FMOD_System_GetUserData            ; external FMOD_DLL;
+
+(*
+    'Sound' API
+*)
+
+function FMOD_Sound_Release                 ; external FMOD_DLL;
+function FMOD_Sound_GetSystemObject         ; external FMOD_DLL;
+
+(*
+     Standard sound manipulation functions.
+*)
+
+function FMOD_Sound_Lock                    ; external FMOD_DLL;
+function FMOD_Sound_Unlock                  ; external FMOD_DLL;
+function FMOD_Sound_SetDefaults             ; external FMOD_DLL;
+function FMOD_Sound_GetDefaults             ; external FMOD_DLL;
+function FMOD_Sound_SetVariations           ; external FMOD_DLL;
+function FMOD_Sound_GetVariations           ; external FMOD_DLL;
+function FMOD_Sound_Set3DMinMaxDistance     ; external FMOD_DLL;
+function FMOD_Sound_Get3DMinMaxDistance     ; external FMOD_DLL;
+function FMOD_Sound_Set3DConeSettings       ; external FMOD_DLL;
+function FMOD_Sound_Get3DConeSettings       ; external FMOD_DLL;
+function FMOD_Sound_Set3DCustomRolloff      ; external FMOD_DLL;
+function FMOD_Sound_Get3DCustomRolloff      ; external FMOD_DLL;
+function FMOD_Sound_SetSubSound             ; external FMOD_DLL;
+function FMOD_Sound_GetSubSound             ; external FMOD_DLL;
+function FMOD_Sound_SetSubSoundSentence     ; external FMOD_DLL;
+function FMOD_Sound_GetName                 ; external FMOD_DLL;
+function FMOD_Sound_GetLength               ; external FMOD_DLL;
+function FMOD_Sound_GetFormat               ; external FMOD_DLL;
+function FMOD_Sound_GetNumSubSounds         ; external FMOD_DLL;
+function FMOD_Sound_GetNumTags              ; external FMOD_DLL;
+function FMOD_Sound_GetTag                  ; external FMOD_DLL;
+function FMOD_Sound_GetOpenState            ; external FMOD_DLL;
+function FMOD_Sound_ReadData                ; external FMOD_DLL;
+function FMOD_Sound_SeekData                ; external FMOD_DLL;
+
+(*
+     Synchronization point API.  These points can come from markers embedded in wav files, and can also generate channel callbacks.
+*)
+
+function FMOD_Sound_GetNumSyncPoints        ; external FMOD_DLL;
+function FMOD_Sound_GetSyncPoint            ; external FMOD_DLL;
+function FMOD_Sound_GetSyncPointInfo        ; external FMOD_DLL;
+function FMOD_Sound_AddSyncPoint            ; external FMOD_DLL;
+function FMOD_Sound_DeleteSyncPoint         ; external FMOD_DLL;
+
+(*
+     Functions also in Channel class but here they are the 'default' to save having to change it in Channel all the time.
+*)
+
+function FMOD_Sound_SetMode                 ; external FMOD_DLL;
+function FMOD_Sound_GetMode                 ; external FMOD_DLL;
+function FMOD_Sound_SetLoopCount            ; external FMOD_DLL;
+function FMOD_Sound_GetLoopCount            ; external FMOD_DLL;
+function FMOD_Sound_SetLoopPoints           ; external FMOD_DLL;
+function FMOD_Sound_GetLoopPoints           ; external FMOD_DLL;
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_Sound_SetUserData             ; external FMOD_DLL;
+function FMOD_Sound_GetUserData             ; external FMOD_DLL;
+
+(*
+    'Channel' API
+*)
+
+function FMOD_Channel_GetSystemObject       ; external FMOD_DLL;
+
+function FMOD_Channel_Stop                  ; external FMOD_DLL;
+function FMOD_Channel_SetPaused             ; external FMOD_DLL;
+function FMOD_Channel_GetPaused             ; external FMOD_DLL;
+function FMOD_Channel_SetVolume             ; external FMOD_DLL;
+function FMOD_Channel_GetVolume             ; external FMOD_DLL;
+function FMOD_Channel_SetFrequency          ; external FMOD_DLL;
+function FMOD_Channel_GetFrequency          ; external FMOD_DLL;
+function FMOD_Channel_SetPan                ; external FMOD_DLL;
+function FMOD_Channel_GetPan                ; external FMOD_DLL;
+function FMOD_Channel_SetDelay              ; external FMOD_DLL;
+function FMOD_Channel_GetDelay              ; external FMOD_DLL;
+function FMOD_Channel_SetSpeakerMix         ; external FMOD_DLL;
+function FMOD_Channel_GetSpeakerMix         ; external FMOD_DLL;
+function FMOD_Channel_SetSpeakerLevels      ; external FMOD_DLL;
+function FMOD_Channel_GetSpeakerLevels      ; external FMOD_DLL;
+function FMOD_Channel_SetMute               ; external FMOD_DLL;
+function FMOD_Channel_GetMute               ; external FMOD_DLL;
+function FMOD_Channel_SetPriority           ; external FMOD_DLL;
+function FMOD_Channel_GetPriority           ; external FMOD_DLL;
+function FMOD_Channel_SetPosition           ; external FMOD_DLL;
+function FMOD_Channel_GetPosition           ; external FMOD_DLL;
+function FMOD_Channel_SetReverbProperties   ; external FMOD_DLL;
+function FMOD_Channel_GetReverbProperties   ; external FMOD_DLL;
+function FMOD_Channel_SetChannelGroup       ; external FMOD_DLL;
+function FMOD_Channel_GetChannelGroup       ; external FMOD_DLL;
+function FMOD_Channel_SetCallback           ; external FMOD_DLL;
+
+(*
+     3D functionality.
+*)
+
+function FMOD_Channel_Set3DAttributes       ; external FMOD_DLL;
+function FMOD_Channel_Get3DAttributes       ; external FMOD_DLL;
+function FMOD_Channel_Set3DMinMaxDistance   ; external FMOD_DLL;
+function FMOD_Channel_Get3DMinMaxDistance   ; external FMOD_DLL;
+function FMOD_Channel_Set3DConeSettings     ; external FMOD_DLL;
+function FMOD_Channel_Get3DConeSettings     ; external FMOD_DLL;
+function FMOD_Channel_Set3DConeOrientation  ; external FMOD_DLL;
+function FMOD_Channel_Get3DConeOrientation  ; external FMOD_DLL;
+function FMOD_Channel_Set3DCustomRolloff    ; external FMOD_DLL;
+function FMOD_Channel_Get3DCustomRolloff    ; external FMOD_DLL;
+function FMOD_Channel_Set3DOcclusion        ; external FMOD_DLL;
+function FMOD_Channel_Get3DOcclusion        ; external FMOD_DLL;
+function FMOD_Channel_Set3DSpread           ; external FMOD_DLL;
+function FMOD_Channel_Get3DSpread           ; external FMOD_DLL;
+function FMOD_Channel_Set3DPanLevel         ; external FMOD_DLL;
+function FMOD_Channel_Get3DPanLevel         ; external FMOD_DLL;
+
+(*
+     DSP functionality only for channels playing sounds created with FMOD_SOFTWARE.
+*)
+
+function FMOD_Channel_GetDSPHead            ; external FMOD_DLL;
+function FMOD_Channel_AddDSP                ; external FMOD_DLL;
+
+(*
+     Information only functions.
+*)
+
+function FMOD_Channel_IsPlaying             ; external FMOD_DLL;
+function FMOD_Channel_IsVirtual             ; external FMOD_DLL;
+function FMOD_Channel_GetAudibility         ; external FMOD_DLL;
+function FMOD_Channel_GetCurrentSound       ; external FMOD_DLL;
+function FMOD_Channel_GetSpectrum           ; external FMOD_DLL;
+function FMOD_Channel_GetWaveData           ; external FMOD_DLL;
+function FMOD_Channel_GetIndex              ; external FMOD_DLL;
+
+(*
+     Functions also found in Sound class but here they can be set per channel.
+*)
+
+function FMOD_Channel_SetMode               ; external FMOD_DLL;
+function FMOD_Channel_GetMode               ; external FMOD_DLL;
+function FMOD_Channel_SetLoopCount          ; external FMOD_DLL;
+function FMOD_Channel_GetLoopCount          ; external FMOD_DLL;
+function FMOD_Channel_SetLoopPoints         ; external FMOD_DLL;
+function FMOD_Channel_GetLoopPoints         ; external FMOD_DLL;
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_Channel_SetUserData           ; external FMOD_DLL;
+function FMOD_Channel_GetUserData           ; external FMOD_DLL;
+
+(*
+    'ChannelGroup' API
+*)
+
+function FMOD_ChannelGroup_Release          ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetSystemObject  ; external FMOD_DLL;
+
+(*
+     Channelgroup scale values.  (scales the current volume or pitch of all channels and channel groups, DOESN'T overwrite)
+*)
+
+function FMOD_ChannelGroup_SetVolume        ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetVolume        ; external FMOD_DLL;
+function FMOD_ChannelGroup_SetPitch         ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetPitch         ; external FMOD_DLL;
+
+(*
+     Channelgroup override values.  (recursively overwrites whatever settings the channels had)
+*)
+
+function FMOD_ChannelGroup_Stop                    ; external FMOD_DLL;
+// function FMOD_ChannelGroup_OverridePaused          ; external FMOD_DLL;
+function FMOD_ChannelGroup_OverrideVolume          ; external FMOD_DLL;
+function FMOD_ChannelGroup_OverrideFrequency       ; external FMOD_DLL;
+function FMOD_ChannelGroup_OverridePan             ; external FMOD_DLL;
+// function FMOD_ChannelGroup_OverrideMute            ; external FMOD_DLL;
+function FMOD_ChannelGroup_OverrideReverbProperties; external FMOD_DLL;
+function FMOD_ChannelGroup_Override3DAttributes    ; external FMOD_DLL;
+function FMOD_ChannelGroup_OverrideSpeakerMix      ; external FMOD_DLL;
+
+(*
+     Nested channel groups.
+*)
+
+function FMOD_ChannelGroup_AddGroup         ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetNumGroups     ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetGroup         ; external FMOD_DLL;
+
+(*
+     DSP functionality only for channel groups playing sounds created with FMOD_SOFTWARE.
+*)
+
+function FMOD_ChannelGroup_GetDSPHead       ; external FMOD_DLL;
+function FMOD_ChannelGroup_AddDSP           ; external FMOD_DLL;
+
+(*
+     Information only functions.
+*)
+
+function FMOD_ChannelGroup_GetName          ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetNumChannels   ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetChannel       ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetSpectrum      ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetWaveData      ; external FMOD_DLL;
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_ChannelGroup_SetUserData      ; external FMOD_DLL;
+function FMOD_ChannelGroup_GetUserData      ; external FMOD_DLL;
+
+(*
+    'DSP' API
+*)
+
+function FMOD_DSP_Release                   ; external FMOD_DLL;
+function FMOD_DSP_GetSystemObject           ; external FMOD_DLL;
+
+(*
+     Connection / disconnection / input and output enumeration.
+*)
+
+function FMOD_DSP_AddInput                  ; external FMOD_DLL;
+function FMOD_DSP_DisconnectFrom            ; external FMOD_DLL;
+function FMOD_DSP_Remove                    ; external FMOD_DLL;
+function FMOD_DSP_GetNumInputs              ; external FMOD_DLL;
+function FMOD_DSP_GetNumOutputs             ; external FMOD_DLL;
+function FMOD_DSP_GetInput                  ; external FMOD_DLL;
+function FMOD_DSP_GetOutput                 ; external FMOD_DLL;
+// function FMOD_DSP_SetInputMix               ; external FMOD_DLL;
+// function FMOD_DSP_GetInputMix               ; external FMOD_DLL;
+// function FMOD_DSP_SetInputLevels            ; external FMOD_DLL;
+// function FMOD_DSP_GetInputLevels            ; external FMOD_DLL;
+// function FMOD_DSP_SetOutputMix              ; external FMOD_DLL;
+// function FMOD_DSP_GetOutputMix              ; external FMOD_DLL;
+// function FMOD_DSP_SetOutputLevels           ; external FMOD_DLL;
+// function FMOD_DSP_GetOutputLevels           ; external FMOD_DLL;
+
+(*
+     DSP unit control
+*)
+
+function FMOD_DSP_SetActive                 ; external FMOD_DLL;
+function FMOD_DSP_GetActive                 ; external FMOD_DLL;
+function FMOD_DSP_SetBypass                 ; external FMOD_DLL;
+function FMOD_DSP_GetBypass                 ; external FMOD_DLL;
+function FMOD_DSP_Reset                     ; external FMOD_DLL;
+
+(*
+     DSP parameter control
+*)
+
+function FMOD_DSP_SetParameter              ; external FMOD_DLL;
+function FMOD_DSP_GetParameter              ; external FMOD_DLL;
+function FMOD_DSP_GetNumParameters          ; external FMOD_DLL;
+function FMOD_DSP_GetParameterInfo          ; external FMOD_DLL;
+function FMOD_DSP_ShowConfigDialog          ; external FMOD_DLL;
+
+(*
+     DSP attributes.
+*)
+
+function FMOD_DSP_GetInfo                   ; external FMOD_DLL;
+function FMOD_DSP_SetDefaults               ; external FMOD_DLL;
+function FMOD_DSP_GetDefaults               ; external FMOD_DLL;
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_DSP_SetUserData               ; external FMOD_DLL;
+function FMOD_DSP_GetUserData               ; external FMOD_DLL;
+
+(*
+    'Geometry' API
+*)
+
+function FMOD_Geometry_Release              ; external FMOD_DLL;
+
+function FMOD_Geometry_AddPolygon           ; external FMOD_DLL;
+function FMOD_Geometry_GetNumPolygons       ; external FMOD_DLL;
+function FMOD_Geometry_GetMaxPolygons       ; external FMOD_DLL;
+function FMOD_Geometry_GetPolygonNumVertices; external FMOD_DLL;
+function FMOD_Geometry_SetPolygonVertex     ; external FMOD_DLL;
+function FMOD_Geometry_GetPolygonVertex     ; external FMOD_DLL;
+function FMOD_Geometry_SetPolygonAttributes ; external FMOD_DLL;
+function FMOD_Geometry_GetPolygonAttributes ; external FMOD_DLL;
+
+function FMOD_Geometry_SetActive            ; external FMOD_DLL;
+function FMOD_Geometry_GetActive            ; external FMOD_DLL;
+function FMOD_Geometry_SetRotation          ; external FMOD_DLL;
+function FMOD_Geometry_GetRotation          ; external FMOD_DLL;
+function FMOD_Geometry_SetPosition          ; external FMOD_DLL;
+function FMOD_Geometry_GetPosition          ; external FMOD_DLL;
+function FMOD_Geometry_SetScale             ; external FMOD_DLL;
+function FMOD_Geometry_GetScale             ; external FMOD_DLL;
+function FMOD_Geometry_Save                 ; external FMOD_DLL;
+
+(*
+     Userdata set/get.
+*)
+
+function FMOD_Geometry_SetUserData          ; external FMOD_DLL;
+function FMOD_Geometry_GetUserData          ; external FMOD_DLL;
+
+end.
diff --git a/src/lib/fmod/fmoderrors.pas b/src/lib/fmod/fmoderrors.pas
new file mode 100644 (file)
index 0000000..2b497bc
--- /dev/null
@@ -0,0 +1,125 @@
+(* ============================================================================================== *)
+(* FMOD Ex - Error string header file. Copyright (c), Firelight Technologies Pty, Ltd. 2004-2008. *)
+(*                                                                                                *)
+(* Use this header if you want to store or display a string version / english explanation of      *)
+(* the FMOD error codes.                                                                          *)
+(*                                                                                                *)
+(* ============================================================================================== *)
+
+unit fmoderrors;
+
+{$I fmod.inc}
+
+interface
+
+uses
+  fmodtypes;
+
+function FMOD_ErrorString(errcode: FMOD_RESULT): PChar;
+
+implementation
+
+function FMOD_ErrorString(errcode: FMOD_RESULT): PChar;
+begin
+  case errcode of
+    FMOD_ERR_ALREADYLOCKED:          Result := 'Tried to call lock a second time before unlock was called. ';
+    FMOD_ERR_BADCOMMAND:             Result := 'Tried to call a function on a data type that does not allow this type of functionality (ie calling Sound::lock on a streaming sound). ';
+    FMOD_ERR_CDDA_DRIVERS:           Result := 'Neither NTSCSI nor ASPI could be initialised. ';
+    FMOD_ERR_CDDA_INIT:              Result := 'An error occurred while initialising the CDDA subsystem. ';
+    FMOD_ERR_CDDA_INVALID_DEVICE:    Result := 'Couldnt find the specified device. ';
+    FMOD_ERR_CDDA_NOAUDIO:           Result := 'No audio tracks on the specified disc. ';
+    FMOD_ERR_CDDA_NODEVICES:         Result := 'No CD/DVD devices were found. ';
+    FMOD_ERR_CDDA_NODISC:            Result := 'No disc present in the specified drive. ';
+    FMOD_ERR_CDDA_READ:              Result := 'A CDDA read error occurred. ';
+    FMOD_ERR_CHANNEL_ALLOC:          Result := 'Error trying to allocate a channel. ';
+    FMOD_ERR_CHANNEL_STOLEN:         Result := 'The specified channel has been reused to play another sound. ';
+    FMOD_ERR_COM:                    Result := 'A Win32 COM related error occured. COM failed to initialize or a QueryInterface failed meaning a Windows codec or driver was not installed properly. ';
+    FMOD_ERR_DMA:                    Result := 'DMA Failure.  See debug output for more information. ';
+    FMOD_ERR_DSP_CONNECTION:         Result := 'DSP connection error.  Connection possibly caused a cyclic dependancy.  Or tried to connect a tree too many units deep (more than 128). ';
+    FMOD_ERR_DSP_FORMAT:             Result := 'DSP Format error.  A DSP unit may have attempted to connect to this network with the wrong format. ';
+    FMOD_ERR_DSP_NOTFOUND:           Result := 'DSP connection error.  Couldnt find the DSP unit specified. ';
+    FMOD_ERR_DSP_RUNNING:            Result := 'DSP error.  Cannot perform this operation while the network is in the middle of running.  This will most likely happen if a connection or disconnection is attempted in a DSP callback. ';
+    FMOD_ERR_DSP_TOOMANYCONNECTIONS: Result := 'DSP connection error.  The unit being connected to or disconnected should only have 1 input or output. ';
+    FMOD_ERR_EVENT_ALREADY_LOADED:   Result := 'The specified project has already been loaded. Having multiple copies of the same project loaded simultaneously is forbidden. ';
+    FMOD_ERR_EVENT_FAILED:           Result := 'An Event failed to be retrieved, most likely due to just fail being specified as the max playbacks behavior. ';
+    FMOD_ERR_EVENT_GUIDCONFLICT:     Result := 'An event with the same GUID already exists. ';
+    FMOD_ERR_EVENT_INFOONLY:         Result := 'Cant execute this command on an EVENT_INFOONLY event. ';
+    FMOD_ERR_EVENT_INTERNAL:         Result := 'An error occured that wasnt supposed to.  See debug log for reason. ';
+    FMOD_ERR_EVENT_MAXSTREAMS:       Result := 'Event failed because Max streams was hit when FMOD_EVENT_INIT_FAIL_ON_MAXSTREAMS was specified. ';
+    FMOD_ERR_EVENT_MISMATCH:         Result := 'FSB mismatches the FEV it was compiled with, the stream/sample mode it was meant to be created with was different, or the FEV was built for a different platform. ';
+    FMOD_ERR_EVENT_NAMECONFLICT:     Result := 'A category with the same name already exists. ';
+    FMOD_ERR_EVENT_NEEDSSIMPLE:      Result := 'Tried to call a function on a complex event thats only supported by simple events. ';
+    FMOD_ERR_EVENT_NOTFOUND:         Result := 'The requested event, event group, event category or event property could not be found. ';
+    FMOD_ERR_FILE_BAD:               Result := 'Error loading file. ';
+    FMOD_ERR_FILE_COULDNOTSEEK:      Result := 'Couldnt perform seek operation.  This is a limitation of the medium (ie netstreams) or the file format. ';
+    FMOD_ERR_FILE_DISKEJECTED:       Result := 'Media was ejected while reading. ';
+    FMOD_ERR_FILE_EOF:               Result := 'End of file unexpectedly reached while trying to read essential data (truncated data?). ';
+    FMOD_ERR_FILE_NOTFOUND:          Result := 'File not found. ';
+    FMOD_ERR_FILE_UNWANTED:          Result := 'Unwanted file access occured. ';
+    FMOD_ERR_FORMAT:                 Result := 'Unsupported file or audio format. ';
+    FMOD_ERR_HTTP:                   Result := 'A HTTP error occurred. This is a catch-all for HTTP errors not listed elsewhere. ';
+    FMOD_ERR_HTTP_ACCESS:            Result := 'The specified resource requires authentication or is forbidden. ';
+    FMOD_ERR_HTTP_PROXY_AUTH:        Result := 'Proxy authentication is required to access the specified resource. ';
+    FMOD_ERR_HTTP_SERVER_ERROR:      Result := 'A HTTP server error occurred. ';
+    FMOD_ERR_HTTP_TIMEOUT:           Result := 'The HTTP request timed out. ';
+    FMOD_ERR_INITIALIZATION:         Result := 'FMOD was not initialized correctly to support this function. ';
+    FMOD_ERR_INITIALIZED:            Result := 'Cannot call this command after System::init. ';
+    FMOD_ERR_INTERNAL:               Result := 'An error occured that wasnt supposed to.  Contact support. ';
+    FMOD_ERR_INVALID_ADDRESS:        Result := 'On Xbox 360, this memory address passed to FMOD must be physical, (ie allocated with XPhysicalAlloc.) ';
+    FMOD_ERR_INVALID_FLOAT:          Result := 'Value passed in was a NaN, Inf or denormalized float. ';
+    FMOD_ERR_INVALID_HANDLE:         Result := 'An invalid object handle was used. ';
+    FMOD_ERR_INVALID_PARAM:          Result := 'An invalid parameter was passed to this function. ';
+    FMOD_ERR_INVALID_POSITION:       Result := 'An invalid seek position was passed to this function. ';
+    FMOD_ERR_INVALID_SPEAKER:        Result := 'An invalid speaker was passed to this function based on the current speaker mode. ';
+    FMOD_ERR_INVALID_SYNCPOINT:      Result := 'The syncpoint did not come from this sound handle. ';
+    FMOD_ERR_INVALID_VECTOR:         Result := 'The vectors passed in are not unit length, or perpendicular. ';
+    FMOD_ERR_IRX:                    Result := 'PS2 only.  fmodex.irx failed to initialize.  This is most likely because you forgot to load it. ';
+    FMOD_ERR_MAXAUDIBLE:             Result := 'Reached maximum audible playback count for this sounds soundgroup. ';
+    FMOD_ERR_MEMORY:                 Result := 'Not enough memory or resources. ';
+    FMOD_ERR_MEMORY_CANTPOINT:       Result := 'Cant use FMOD_OPENMEMORY_POINT on non PCM source data, or non mp3/xma/adpcm data if FMOD_CREATECOMPRESSEDSAMPLE was used. ';
+    FMOD_ERR_MEMORY_IOP:             Result := 'PS2 only.  Not enough memory or resources on PlayStation 2 IOP ram. ';
+    FMOD_ERR_MEMORY_SRAM:            Result := 'Not enough memory or resources on console sound ram. ';
+    FMOD_ERR_MUSIC_UNINITIALIZED:    Result := 'Music system is not initialized probably because no music data is loaded. ';
+    FMOD_ERR_NEEDS2D:                Result := 'Tried to call a command on a 3d sound when the command was meant for 2d sound. ';
+    FMOD_ERR_NEEDS3D:                Result := 'Tried to call a command on a 2d sound when the command was meant for 3d sound. ';
+    FMOD_ERR_NEEDSHARDWARE:          Result := 'Tried to use a feature that requires hardware support.  (ie trying to play a VAG compressed sound in software on PS2). ';
+    FMOD_ERR_NEEDSSOFTWARE:          Result := 'Tried to use a feature that requires the software engine.  Software engine has either been turned off, or command was executed on a hardware channel which does not support this feature. ';
+    FMOD_ERR_NET_CONNECT:            Result := 'Couldnt connect to the specified host. ';
+    FMOD_ERR_NET_SOCKET_ERROR:       Result := 'A socket error occurred.  This is a catch-all for socket-related errors not listed elsewhere. ';
+    FMOD_ERR_NET_URL:                Result := 'The specified URL couldnt be resolved. ';
+    FMOD_ERR_NET_WOULD_BLOCK:        Result := 'Operation on a non-blocking socket could not complete immediately. ';
+    FMOD_ERR_NOTREADY:               Result := 'Operation could not be performed because specified sound/DSP connection is not ready. ';
+    FMOD_ERR_OUTPUT_ALLOCATED:       Result := 'Error initializing output device, but more specifically, the output device is already in use and cannot be reused. ';
+    FMOD_ERR_OUTPUT_CREATEBUFFER:    Result := 'Error creating hardware sound buffer. ';
+    FMOD_ERR_OUTPUT_DRIVERCALL:      Result := 'A call to a standard soundcard driver failed, which could possibly mean a bug in the driver or resources were missing or exhausted. ';
+    FMOD_ERR_OUTPUT_ENUMERATION:     Result := 'Error enumerating the available driver list. List may be inconsistent due to a recent device addition or removal. ';
+    FMOD_ERR_OUTPUT_FORMAT:          Result := 'Soundcard does not support the minimum features needed for this soundsystem (16bit stereo output). ';
+    FMOD_ERR_OUTPUT_INIT:            Result := 'Error initializing output device. ';
+    FMOD_ERR_OUTPUT_NOHARDWARE:      Result := 'FMOD_HARDWARE was specified but the sound card does not have the resources necessary to play it. ';
+    FMOD_ERR_OUTPUT_NOSOFTWARE:      Result := 'Attempted to create a software sound but no software channels were specified in System::init. ';
+    FMOD_ERR_PAN:                    Result := 'Panning only works with mono or stereo sound sources. ';
+    FMOD_ERR_PLUGIN:                 Result := 'An unspecified error has been returned from a 3rd party plugin. ';
+    FMOD_ERR_PLUGIN_INSTANCES:       Result := 'The number of allowed instances of a plugin has been exceeded. ';
+    FMOD_ERR_PLUGIN_MISSING:         Result := 'A requested output, dsp unit type or codec was not available. ';
+    FMOD_ERR_PLUGIN_RESOURCE:        Result := 'A resource that the plugin requires cannot be found. (ie the DLS file for MIDI playback) ';
+    FMOD_ERR_PRELOADED:              Result := 'The specified sound is still in use by the event system, call EventSystem::unloadFSB before trying to release it. ';
+    FMOD_ERR_RECORD:                 Result := 'An error occured trying to initialize the recording device. ';
+    FMOD_ERR_REVERB_INSTANCE:        Result := 'Specified Instance in FMOD_REVERB_PROPERTIES couldnt be set. Most likely because it is an invalid instance number, or another application has locked the EAX4 FX slot. ';
+    FMOD_ERR_SUBSOUNDS:              Result := 'The error occured because the sound referenced contains subsounds.  The operation cannot be performed on a parent sound, or a parent sound was played without setting up a sentence first. ';
+    FMOD_ERR_SUBSOUND_ALLOCATED:     Result := 'This subsound is already being used by another sound, you cannot have more than one parent to a sound.  Null out the other parents entry first. ';
+    FMOD_ERR_SUBSOUND_CANTMOVE:      Result := 'Shared subsounds cannot be replaced or moved from their parent stream, such as when the parent stream is an FSB file. ';
+    FMOD_ERR_SUBSOUND_MODE:          Result := 'The subsounds mode bits do not match with the parent sounds mode bits.  See documentation for function that it was called with. ';
+    FMOD_ERR_TAGNOTFOUND:            Result := 'The specified tag could not be found or there are no tags. ';
+    FMOD_ERR_TOOMANYCHANNELS:        Result := 'The sound created exceeds the allowable input channel count.  This can be increased using the maxinputchannels parameter in System::setSoftwareFormat. ';
+    FMOD_ERR_UNIMPLEMENTED:          Result := 'Something in FMOD hasnt been implemented when it should be! contact support! ';
+    FMOD_ERR_UNINITIALIZED:          Result := 'This command failed because System::init or System::setDriver was not called. ';
+    FMOD_ERR_UNSUPPORTED:            Result := 'A command issued was not supported by this object.  Possibly a plugin without certain callbacks specified. ';
+    FMOD_ERR_UPDATE:                 Result := 'An error caused by System::update occured. ';
+    FMOD_ERR_VERSION:                Result := 'The version number of this file format is not supported. ';
+    FMOD_OK:                         Result := 'No errors.';
+    else
+      Result := 'Unknown error.';
+  end;
+end;
+
+end.
diff --git a/src/lib/fmod/fmodpresets.pas b/src/lib/fmod/fmodpresets.pas
new file mode 100644 (file)
index 0000000..a27d59f
--- /dev/null
@@ -0,0 +1,81 @@
+(* ========================================================================================== *)
+(* FMOD Ex - Presets header file. Copyright (c), Firelight Technologies Pty, Ltd. 2004-2008.  *)
+(*                                                                                            *)
+(* A set of predefined environment PARAMETERS, created by Creative Labs.                      *)
+(* These are used to initialize an FMOD_REVERB_PROPERTIES structure statically.               *)
+(*                                                                                            *)
+(* ========================================================================================== *)
+
+unit fmodpresets;
+
+{$I fmod.inc}
+
+interface
+
+uses
+  fmodtypes;
+
+(*
+[DEFINE] 
+[
+    [NAME] 
+    FMOD_REVERB_PRESETS
+
+    [DESCRIPTION]   
+    A set of predefined environment PARAMETERS, created by Creative Labs
+    These are used to initialize an FMOD_REVERB_PROPERTIES structure statically.
+    ie 
+    FMOD_REVERB_PROPERTIES prop = FMOD_PRESET_GENERIC;
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable
+
+    [SEE_ALSO]
+    FMOD_System_SetReverbProperties
+]
+*)
+
+{$IFDEF COMPILER6_UP}{$J+}{$ENDIF}
+(*                                                           Instance     Env              Size            Diffuse               Room          RoomHF          RmLF        DecTm             DecHF               DecLF               Refl                RefDel                    RefPan                            Revb          RevDel              ReverbPan                   EchoTm            EchDp             ModTm                 ModDp                   AirAbs                  HFRef                 LFRef               RRlOff                  Diffus            Densty          FLAGS *)
+const
+  FSOUND_PRESET_OFF:              FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 0;  EnvSize: 7.5;   EnvDiffusion: 1.00;   Room: -10000; RoomHF: -10000; RoomLF: 0;  DecayTime: 1.00;  DecayHFRatio: 1.00; DecayLFRatio: 1.0;  Reflections: -2602; ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 200;  ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 0.0;   Density: 0.0;   Flags: $3f);
+  FSOUND_PRESET_GENERIC:          FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 0;  EnvSize: 7.5;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -100;   RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.83; DecayLFRatio: 1.0;  Reflections: -2602; ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 200;  ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_PADDEDCELL:       FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 1;  EnvSize: 1.4;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -6000;  RoomLF: 0;  DecayTime: 0.17;  DecayHFRatio: 0.10; DecayLFRatio: 1.0;  Reflections: -1204; ReflectionsDelay: 0.001;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 207;  ReverbDelay: 0.002; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_ROOM:             FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 2;  EnvSize: 1.9;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -454;   RoomLF: 0;  DecayTime: 0.40;  DecayHFRatio: 0.83; DecayLFRatio: 1.0;  Reflections: -1646; ReflectionsDelay: 0.002;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 53;   ReverbDelay: 0.003; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_BATHROOM:         FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 3;  EnvSize: 1.4;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -1200;  RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.54; DecayLFRatio: 1.0;  Reflections: -370;  ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 1030; ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 60.0;  Flags: $3f);
+  FSOUND_PRESET_LIVINGROOM:       FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 4;  EnvSize: 2.5;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -6000;  RoomLF: 0;  DecayTime: 0.50;  DecayHFRatio: 0.10; DecayLFRatio: 1.0;  Reflections: -1376; ReflectionsDelay: 0.003;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-1104; ReverbDelay: 0.004; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_STONEROOM:        FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 5;  EnvSize: 11.6;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -300;   RoomLF: 0;  DecayTime: 2.31;  DecayHFRatio: 0.64; DecayLFRatio: 1.0;  Reflections: -711;  ReflectionsDelay: 0.012;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 83;   ReverbDelay: 0.017; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_AUDITORIUM:       FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 6;  EnvSize: 21.6;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -476;   RoomLF: 0;  DecayTime: 4.32;  DecayHFRatio: 0.59; DecayLFRatio: 1.0;  Reflections: -789;  ReflectionsDelay: 0.020;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-289;  ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_CONCERTHALL:      FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 7;  EnvSize: 19.6;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -500;   RoomLF: 0;  DecayTime: 3.92;  DecayHFRatio: 0.70; DecayLFRatio: 1.0;  Reflections: -1230; ReflectionsDelay: 0.020;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-2;    ReverbDelay: 0.029; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_CAVE:             FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 8;  EnvSize: 14.6;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: 0;      RoomLF: 0;  DecayTime: 2.91;  DecayHFRatio: 1.30; DecayLFRatio: 1.0;  Reflections: -602;  ReflectionsDelay: 0.015;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-302;  ReverbDelay: 0.022; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $1f);
+  FSOUND_PRESET_ARENA:            FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 9;  EnvSize: 36.2;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -698;   RoomLF: 0;  DecayTime: 7.24;  DecayHFRatio: 0.33; DecayLFRatio: 1.0;  Reflections: -1166; ReflectionsDelay: 0.020;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 16;   ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_HANGAR:           FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 10; EnvSize: 50.3;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -1000;  RoomLF: 0;  DecayTime: 10.05; DecayHFRatio: 0.23; DecayLFRatio: 1.0;  Reflections: -602;  ReflectionsDelay: 0.020;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 198;  ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_CARPETTEDHALLWAY: FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 11; EnvSize: 1.9;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -4000;  RoomLF: 0;  DecayTime: 0.30;  DecayHFRatio: 0.10; DecayLFRatio: 1.0;  Reflections: -1831; ReflectionsDelay: 0.002;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-1630; ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_HALLWAY:          FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 12; EnvSize: 1.8;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -300;   RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.59; DecayLFRatio: 1.0;  Reflections: -1219; ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 441;  ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_STONECORRIDOR:    FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 13; EnvSize: 13.5;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -237;   RoomLF: 0;  DecayTime: 2.70;  DecayHFRatio: 0.79; DecayLFRatio: 1.0;  Reflections: -1214; ReflectionsDelay: 0.013;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 395;  ReverbDelay: 0.020; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_ALLEY:            FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 14; EnvSize: 7.5;   EnvDiffusion: 0.30;   Room: -1000;  RoomHF: -270;   RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.86; DecayLFRatio: 1.0;  Reflections: -1204; ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-4;    ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.125;  EchoDepth: 0.95;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_FOREST:           FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 15; EnvSize: 38.0;  EnvDiffusion: 0.30;   Room: -1000;  RoomHF: -3300;  RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.54; DecayLFRatio: 1.0;  Reflections: -2560; ReflectionsDelay: 0.162;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-229;  ReverbDelay: 0.088; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.125;  EchoDepth: 1.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 79.0;  Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_CITY:             FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 16; EnvSize: 7.5;   EnvDiffusion: 0.50;   Room: -1000;  RoomHF: -800;   RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.67; DecayLFRatio: 1.0;  Reflections: -2273; ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-1691; ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 50.0;  Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_MOUNTAINS:        FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 17; EnvSize: 100.0; EnvDiffusion: 0.27;   Room: -1000;  RoomHF: -2500;  RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.21; DecayLFRatio: 1.0;  Reflections: -2780; ReflectionsDelay: 0.300;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-1434; ReverbDelay: 0.100; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 1.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 27.0;  Density: 100.0; Flags: $1f);
+  FSOUND_PRESET_QUARRY:           FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 18; EnvSize: 17.5;  EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -1000;  RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.83; DecayLFRatio: 1.0;  Reflections: -10000;ReflectionsDelay: 0.061;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 500;  ReverbDelay: 0.025; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.125;  EchoDepth: 0.70;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_PLAIN:            FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 19; EnvSize: 42.5;  EnvDiffusion: 0.21;   Room: -1000;  RoomHF: -2000;  RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.50; DecayLFRatio: 1.0;  Reflections: -2466; ReflectionsDelay: 0.179;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-1926; ReverbDelay: 0.100; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 1.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 21.0;  Density: 100.0; Flags: $3f);
+  FSOUND_PRESET_PARKINGLOT:       FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 20; EnvSize: 8.3;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: 0;      RoomLF: 0;  DecayTime: 1.65;  DecayHFRatio: 1.50; DecayLFRatio: 1.0;  Reflections: -1363; ReflectionsDelay: 0.008;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-1153; ReverbDelay: 0.012; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $1f);
+  FSOUND_PRESET_SEWERPIPE:        FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 21; EnvSize: 1.7;   EnvDiffusion: 0.80;   Room: -1000;  RoomHF: -1000;  RoomLF: 0;  DecayTime: 2.81;  DecayHFRatio: 0.14; DecayLFRatio: 1.0;  Reflections:  429;  ReflectionsDelay: 0.014;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 1023; ReverbDelay: 0.021; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 0.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 80.0;  Density: 60.0;  Flags: $3f);
+  FSOUND_PRESET_UNDERWATER:       FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 22; EnvSize: 1.8;   EnvDiffusion: 1.00;   Room: -1000;  RoomHF: -4000;  RoomLF: 0;  DecayTime: 1.49;  DecayHFRatio: 0.10; DecayLFRatio: 1.0;  Reflections: -449;  ReflectionsDelay: 0.007;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 1700; ReverbDelay: 0.011; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 1.18; ModulationDepth: 0.348; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $3f);
+
+(* Non I3DL2 presets *)
+
+  FSOUND_PRESET_DRUGGED:          FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 23; EnvSize: 1.9;   EnvDiffusion: 0.50;   Room: -1000;  RoomHF: 0;      RoomLF: 0;  DecayTime: 8.39;  DecayHFRatio: 1.39; DecayLFRatio: 1.0;  Reflections: -115;  ReflectionsDelay: 0.002;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 985;  ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 0.25; ModulationDepth: 1.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $1f);
+  FSOUND_PRESET_DIZZY:            FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 24; EnvSize: 1.8;   EnvDiffusion: 0.60;   Room: -1000;  RoomHF: -400;   RoomLF: 0;  DecayTime: 17.23; DecayHFRatio: 0.56; DecayLFRatio: 1.0;  Reflections: -1713; ReflectionsDelay: 0.020;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb:-613;  ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 1.00;  ModulationTime: 0.81; ModulationDepth: 0.310; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $1f);
+  FSOUND_PRESET_PSYCHOTIC:        FMOD_REVERB_PROPERTIES = (Instance: 0; Environment: 25; EnvSize: 1.0;   EnvDiffusion: 0.50;   Room: -1000;  RoomHF: -151;   RoomLF: 0;  DecayTime: 7.56;  DecayHFRatio: 0.91; DecayLFRatio: 1.0;  Reflections: -626;  ReflectionsDelay: 0.020;  ReflectionsPan: (0.0, 0.0, 0.0);  Reverb: 774;  ReverbDelay: 0.030; ReverbPan: (0.0, 0.0, 0.0); EchoTime: 0.250;  EchoDepth: 0.00;  ModulationTime: 4.00; ModulationDepth: 1.000; AirAbsorptionHF: -5.0;  HFReference: 5000.0;  LFReference: 250.0; RoomRolloffFactor: 0.0; Diffusion: 100.0; Density: 100.0; Flags: $1f);
+
+(* PlayStation 2 Only presets *)
+(* Delphi/Kylix cannot create PlayStation 2 executables, so there is no need to
+   convert the PlayStation 2 presets. *)
+{$IFDEF COMPILER6_UP}{$J-}{$ENDIF}
+
+(* [DEFINE_END] *)
+
+implementation
+
+end.
diff --git a/src/lib/fmod/fmodtypes.pas b/src/lib/fmod/fmodtypes.pas
new file mode 100644 (file)
index 0000000..8402041
--- /dev/null
@@ -0,0 +1,2181 @@
+(* ============================================================================================= *)
+(* FMOD Ex - Main C/C++ header file. Copyright (c), Firelight Technologies Pty, Ltd. 2004-2008.  *)
+(*                                                                                               *)
+(* This header is the base header for all other FMOD headers.  If you are programming in C       *)
+(* use this exclusively, or if you are programming C++ use this in conjunction with FMOD.HPP     *)
+(*                                                                                               *)
+(* ============================================================================================= *)
+
+unit fmodtypes;
+
+{$I fmod.inc}
+
+interface
+
+(*
+    FMOD version number.  Check this against FMOD_System_GetVersion
+*)
+
+const
+  FMOD_VERSION = $00041500;
+
+(*
+    FMOD types.
+*)
+
+type
+  FMOD_SYSTEM       = Pointer;
+  FMOD_SOUND        = Pointer;
+  FMOD_CHANNEL      = Pointer;
+  FMOD_CHANNELGROUP = Pointer;
+  FMOD_DSP          = Pointer;
+  FMOD_BOOL         = LongBool;
+  FMOD_POLYGON         = Pointer;
+  FMOD_GEOMETRY     = Pointer;
+  FMOD_SYNCPOINT       = Pointer;
+  FMOD_TIMEUNIT     = Cardinal;
+            fmod_mode=cardinal;
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Structure describing a point in 3D space.
+
+    [REMARKS]
+    FMOD uses a left handed co-ordinate system by default.
+    To use a right handed co-ordinate system specify FMOD_INIT_3D_RIGHTHANDED from FMOD_INITFLAGS in FMOD_System_Init.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_Set3DListenerAttributes
+    FMOD_System_Get3DListenerAttributes
+    FMOD_Channel_Set3DAttributes
+    FMOD_Channel_Get3DAttributes
+    FMOD_Geometry_AddPolygon
+    FMOD_Geometry_SetPolygonVertex
+    FMOD_Geometry_GetPolygonVertex
+    FMOD_Geometry_SetRotation
+    FMOD_Geometry_GetRotation
+    FMOD_Geometry_SetPosition
+    FMOD_Geometry_GetPosition
+    FMOD_Geometry_SetScale
+    FMOD_Geometry_GetScale
+    FMOD_INITFLAGS
+]
+*)
+type PFMOD_VECTOR = ^FMOD_VECTOR;
+  FMOD_VECTOR = record
+         x, y, z: Single;
+  end;
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    error codes.  Returned from every function.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+]
+*)
+type
+  FMOD_RESULT =
+  (
+    FMOD_OK,                        {  No errors.  }
+    FMOD_ERR_ALREADYLOCKED,         {  Tried to call lock a second time before unlock was called.  }
+    FMOD_ERR_BADCOMMAND,            {  Tried to call a function on a data type that does not allow this type of functionality (ie calling Sound::lock on a streaming sound).  }
+    FMOD_ERR_CDDA_DRIVERS,          {  Neither NTSCSI nor ASPI could be initialised.  }
+    FMOD_ERR_CDDA_INIT,             {  An error occurred while initialising the CDDA subsystem.  }
+    FMOD_ERR_CDDA_INVALID_DEVICE,   {  Couldn't find the specified device.  }
+    FMOD_ERR_CDDA_NOAUDIO,          {  No audio tracks on the specified disc.  }
+    FMOD_ERR_CDDA_NODEVICES,        {  No CD/DVD devices were found.  }
+    FMOD_ERR_CDDA_NODISC,           {  No disc present in the specified drive.  }
+    FMOD_ERR_CDDA_READ,             {  A CDDA read error occurred.  }
+    FMOD_ERR_CHANNEL_ALLOC,         {  Error trying to allocate a channel.  }
+    FMOD_ERR_CHANNEL_STOLEN,        {  The specified channel has been reused to play another sound.  }
+    FMOD_ERR_COM,                   {  A Win32 COM related error occured. COM failed to initialize or a QueryInterface failed meaning a Windows codec or driver was not installed properly.  }
+    FMOD_ERR_DMA,                   {  DMA Failure.  See debug output for more information.  }
+    FMOD_ERR_DSP_CONNECTION,        {  DSP connection error.  Connection possibly caused a cyclic dependancy.  Or tried to connect a tree too many units deep (more than 128).  }
+    FMOD_ERR_DSP_FORMAT,            {  DSP Format error.  A DSP unit may have attempted to connect to this network with the wrong format.  }
+    FMOD_ERR_DSP_NOTFOUND,          {  DSP connection error.  Couldn't find the DSP unit specified.  }
+    FMOD_ERR_DSP_RUNNING,           {  DSP error.  Cannot perform this operation while the network is in the middle of running.  This will most likely happen if a connection or disconnection is attempted in a DSP callback.  }
+    FMOD_ERR_DSP_TOOMANYCONNECTIONS,{  DSP connection error.  The unit being connected to or disconnected should only have 1 input or output.  }
+    FMOD_ERR_FILE_BAD,              {  Error loading file.  }
+    FMOD_ERR_FILE_COULDNOTSEEK,     {  Couldn't perform seek operation.  This is a limitation of the medium (ie netstreams) or the file format.  }
+    FMOD_ERR_FILE_DISKEJECTED,      {  Media was ejected while reading.  }
+    FMOD_ERR_FILE_EOF,              {  End of file unexpectedly reached while trying to read essential data (truncated data?).  }
+    FMOD_ERR_FILE_NOTFOUND,         {  File not found.  }
+    FMOD_ERR_FILE_UNWANTED,         {  Unwanted file access occured.  }
+    FMOD_ERR_FORMAT,                {  Unsupported file or audio format.  }
+    FMOD_ERR_HTTP,                  {  A HTTP error occurred. This is a catch-all for HTTP errors not listed elsewhere.  }
+    FMOD_ERR_HTTP_ACCESS,           {  The specified resource requires authentication or is forbidden.  }
+    FMOD_ERR_HTTP_PROXY_AUTH,       {  Proxy authentication is required to access the specified resource.  }
+    FMOD_ERR_HTTP_SERVER_ERROR,     {  A HTTP server error occurred.  }
+    FMOD_ERR_HTTP_TIMEOUT,          {  The HTTP request timed out.  }
+    FMOD_ERR_INITIALIZATION,        {  FMOD was not initialized correctly to support this function.  }
+    FMOD_ERR_INITIALIZED,           {  Cannot call this command after System::init.  }
+    FMOD_ERR_INTERNAL,              {  An error occured that wasn't supposed to.  Contact support.  }
+    FMOD_ERR_INVALID_ADDRESS,       {  On Xbox 360, this memory address passed to FMOD must be physical, (ie allocated with XPhysicalAlloc.)  }
+    FMOD_ERR_INVALID_FLOAT,         {  Value passed in was a NaN, Inf or denormalized float.  }
+    FMOD_ERR_INVALID_HANDLE,        {  An invalid object handle was used.  }
+    FMOD_ERR_INVALID_PARAM,         {  An invalid parameter was passed to this function.  }
+    FMOD_ERR_INVALID_POSITION,      {  An invalid seek position was passed to this function.  }
+    FMOD_ERR_INVALID_SPEAKER,       {  An invalid speaker was passed to this function based on the current speaker mode.  }
+    FMOD_ERR_INVALID_SYNCPOINT,     {  The syncpoint did not come from this sound handle.  }
+    FMOD_ERR_INVALID_VECTOR,        {  The vectors passed in are not unit length, or perpendicular.  }
+    FMOD_ERR_IRX,                   {  PS2 only.  fmodex.irx failed to initialize.  This is most likely because you forgot to load it.  }
+    FMOD_ERR_MAXAUDIBLE,            {  Reached maximum audible playback count for this sound's soundgroup.  }
+    FMOD_ERR_MEMORY,                {  Not enough memory or resources.  }
+    FMOD_ERR_MEMORY_CANTPOINT,      {  Can't use FMOD_OPENMEMORY_POINT on non PCM source data, or non mp3/xma/adpcm data if FMOD_CREATECOMPRESSEDSAMPLE was used.  }
+    FMOD_ERR_MEMORY_IOP,            {  PS2 only.  Not enough memory or resources on PlayStation 2 IOP ram.  }
+    FMOD_ERR_MEMORY_SRAM,           {  Not enough memory or resources on console sound ram.  }
+    FMOD_ERR_NEEDS2D,               {  Tried to call a command on a 3d sound when the command was meant for 2d sound.  }
+    FMOD_ERR_NEEDS3D,               {  Tried to call a command on a 2d sound when the command was meant for 3d sound.  }
+    FMOD_ERR_NEEDSHARDWARE,         {  Tried to use a feature that requires hardware support.  (ie trying to play a VAG compressed sound in software on PS2).  }
+    FMOD_ERR_NEEDSSOFTWARE,         {  Tried to use a feature that requires the software engine.  Software engine has either been turned off, or command was executed on a hardware channel which does not support this feature.  }
+    FMOD_ERR_NET_CONNECT,           {  Couldn't connect to the specified host.  }
+    FMOD_ERR_NET_SOCKET_ERROR,      {  A socket error occurred.  This is a catch-all for socket-related errors not listed elsewhere.  }
+    FMOD_ERR_NET_URL,               {  The specified URL couldn't be resolved.  }
+    FMOD_ERR_NET_WOULD_BLOCK,       {  Operation on a non-blocking socket could not complete immediately.  }
+    FMOD_ERR_NOTREADY,              {  Operation could not be performed because specified sound/DSP connection is not ready.  }
+    FMOD_ERR_OUTPUT_ALLOCATED,      {  Error initializing output device, but more specifically, the output device is already in use and cannot be reused.  }
+    FMOD_ERR_OUTPUT_CREATEBUFFER,   {  Error creating hardware sound buffer.  }
+    FMOD_ERR_OUTPUT_DRIVERCALL,     {  A call to a standard soundcard driver failed, which could possibly mean a bug in the driver or resources were missing or exhausted.  }
+    FMOD_ERR_OUTPUT_ENUMERATION,    {  Error enumerating the available driver list. List may be inconsistent due to a recent device addition or removal.  }
+    FMOD_ERR_OUTPUT_FORMAT,         {  Soundcard does not support the minimum features needed for this soundsystem (16bit stereo output).  }
+    FMOD_ERR_OUTPUT_INIT,           {  Error initializing output device.  }
+    FMOD_ERR_OUTPUT_NOHARDWARE,     {  FMOD_HARDWARE was specified but the sound card does not have the resources necessary to play it.  }
+    FMOD_ERR_OUTPUT_NOSOFTWARE,     {  Attempted to create a software sound but no software channels were specified in System::init.  }
+    FMOD_ERR_PAN,                   {  Panning only works with mono or stereo sound sources.  }
+    FMOD_ERR_PLUGIN,                {  An unspecified error has been returned from a 3rd party plugin.  }
+    FMOD_ERR_PLUGIN_INSTANCES,      {  The number of allowed instances of a plugin has been exceeded.  }
+    FMOD_ERR_PLUGIN_MISSING,        {  A requested output, dsp unit type or codec was not available.  }
+    FMOD_ERR_PLUGIN_RESOURCE,       {  A resource that the plugin requires cannot be found. (ie the DLS file for MIDI playback)  }
+    FMOD_ERR_RECORD,                {  An error occured trying to initialize the recording device.  }
+    FMOD_ERR_REVERB_INSTANCE,       {  Specified Instance in FMOD_REVERB_PROPERTIES couldn't be set. Most likely because it is an invalid instance number, or another application has locked the EAX4 FX slot.  }
+    FMOD_ERR_SUBSOUND_ALLOCATED,    {  This subsound is already being used by another sound, you cannot have more than one parent to a sound.  Null out the other parent's entry first.  }
+    FMOD_ERR_SUBSOUND_CANTMOVE,     {  Shared subsounds cannot be replaced or moved from their parent stream, such as when the parent stream is an FSB file.  }
+    FMOD_ERR_SUBSOUND_MODE,         {  The subsound's mode bits do not match with the parent sound's mode bits.  See documentation for function that it was called with.  }
+    FMOD_ERR_SUBSOUNDS,             {  The error occured because the sound referenced contains subsounds.  The operation cannot be performed on a parent sound, or a parent sound was played without setting up a sentence first.  }
+    FMOD_ERR_TAGNOTFOUND,           {  The specified tag could not be found or there are no tags.  }
+    FMOD_ERR_TOOMANYCHANNELS,       {  The sound created exceeds the allowable input channel count.  This can be increased using the maxinputchannels parameter in System::setSoftwareFormat.  }
+    FMOD_ERR_UNIMPLEMENTED,         {  Something in FMOD hasn't been implemented when it should be! contact support!  }
+    FMOD_ERR_UNINITIALIZED,         {  This command failed because System::init or System::setDriver was not called.  }
+    FMOD_ERR_UNSUPPORTED,           {  A command issued was not supported by this object.  Possibly a plugin without certain callbacks specified.  }
+    FMOD_ERR_UPDATE,                {  An error caused by System::update occured.  }
+    FMOD_ERR_VERSION,               {  The version number of this file format is not supported.  }
+    FMOD_ERR_PRELOADED,             {  The specified sound is still in use by the event system, call EventSystem::unloadFSB before trying to release it.  }
+
+    FMOD_ERR_EVENT_FAILED,          {  An Event failed to be retrieved, most likely due to 'just fail' being specified as the max playbacks behavior.  }
+    FMOD_ERR_EVENT_INFOONLY,        {  Can't execute this command on an EVENT_INFOONLY event.  }
+    FMOD_ERR_EVENT_INTERNAL,        {  An error occured that wasn't supposed to.  See debug log for reason.  }
+    FMOD_ERR_EVENT_MAXSTREAMS,      {  Event failed because 'Max streams' was hit when FMOD_EVENT_INIT_FAIL_ON_MAXSTREAMS was specified.  }
+    FMOD_ERR_EVENT_MISMATCH,        {  FSB mismatches the FEV it was compiled with, the stream/sample mode it was meant to be created with was different, or the FEV was built for a different platform.  }
+    FMOD_ERR_EVENT_NAMECONFLICT,    {  A category with the same name already exists.  }
+    FMOD_ERR_EVENT_NOTFOUND,        {  The requested event, event group, event category or event property could not be found.  }
+    FMOD_ERR_EVENT_NEEDSSIMPLE,     {  Tried to call a function on a complex event that's only supported by simple events.  }
+    FMOD_ERR_EVENT_GUIDCONFLICT,    {  An event with the same GUID already exists.  }
+    FMOD_ERR_EVENT_ALREADY_LOADED,  {  The specified project has already been loaded. Having multiple copies of the same project loaded simultaneously is forbidden.  }
+
+    FMOD_ERR_MUSIC_UNINITIALIZED,   {  Music system is not initialized probably because no music data is loaded.  }
+
+    FMOD_RESULT_FORCEINT = 65536    {  Makes sure this enum is signed 32bit.  }
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These output types are used with FMOD_System_SetOutput/FMOD_System_GetOutput, to choose which output method to use.
+
+    [REMARKS]
+    To pass information to the driver when initializing fmod use the extradriverdata parameter in System::init for the following reasons.
+    - FMOD_OUTPUTTYPE_WAVWRITER - extradriverdata is a pointer to a char * filename that the wav writer will output to.
+    - FMOD_OUTPUTTYPE_WAVWRITER_NRT - extradriverdata is a pointer to a char * filename that the wav writer will output to.
+    - FMOD_OUTPUTTYPE_DSOUND - extradriverdata is a pointer to a HWND so that FMOD can set the focus on the audio for a particular window.
+    - FMOD_OUTPUTTYPE_GC - extradriverdata is a pointer to a FMOD_ARAMBLOCK_INFO struct. This can be found in fmodgc.h.
+    - FMOD_OUTPUTTYPE_ALSA - extradriverdata is a pointer to a char * argument if required by the chosen ALSA driver.
+    Currently these are the only FMOD drivers that take extra information.  Other unknown plugins may have different requirements.
+
+    Note! If FMOD_OUTPUTTYPE_NOSOUND_NRT or FMOD_OUTPUTTYPE_NOSOUND_NRT are used, and if the System::update function is being called
+    very quickly (ie for a non realtime decode) it may be being called too quickly for the FMOD streamer thread to respond to.
+    The result will be a skipping/stuttering output in the captured audio.
+    To remedy this, disable the FMOD Ex streamer thread, and use FMOD_INIT_STREAM_FROM_UPDATE can be used to avoid skipping in
+    the output stream, as it will lock the mixer and the streamer together in the same thread.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_SetOutput
+    FMOD_System_GetOutput
+    FMOD_System_SetSoftwareFormat
+    FMOD_System_GetSoftwareFormat
+    FMOD_System_Init
+    FMOD_System_Update
+    FMOD_INITFLAGS
+]
+*)
+type
+  FMOD_OUTPUTTYPE =
+  (
+    FMOD_OUTPUTTYPE_AUTODETECT,      (* Picks the best output mode for the platform.  This is the default. *)
+
+    FMOD_OUTPUTTYPE_UNKNOWN,         (* All         - 3rd party plugin, unknown.  This is for use with System::getOutput only. *)
+    FMOD_OUTPUTTYPE_NOSOUND,         (* All         - All calls in this mode succeed but make no sound. *)
+    FMOD_OUTPUTTYPE_WAVWRITER,       (* All         - Writes output to fmodoutput.wav by default.  Use the 'extradriverdata' parameter in System::init, by simply passing the filename as a string, to set the wav filename. *)
+    FMOD_OUTPUTTYPE_NOSOUND_NRT,     (* All         - Non-realtime version of FMOD_OUTPUTTYPE_NOSOUND.  User can drive mixer with System::update at whatever rate they want. *)
+    FMOD_OUTPUTTYPE_WAVWRITER_NRT,   (* All         - Non-realtime version of FMOD_OUTPUTTYPE_WAVWRITER.  User can drive mixer with System::update at whatever rate they want. *)
+
+    FMOD_OUTPUTTYPE_DSOUND,          (* Win32/Win64 - DirectSound output.  Use this to get hardware accelerated 3d audio and EAX Reverb support. (Default on Windows) *)
+    FMOD_OUTPUTTYPE_WINMM,           (* Win32/Win64 - Windows Multimedia output. *)
+    FMOD_OUTPUTTYPE_OPENAL,          (* Win32/Win64 - OpenAL 1.1 output. Use this for lower CPU overhead than FMOD_OUTPUTTYPE_DSOUND, and also Vista H/W support with Creative Labs cards. *)
+    FMOD_OUTPUTTYPE_WASAPI,          (* Win32       - Windows Audio Session API. (Default on Windows Vista) *)
+    FMOD_OUTPUTTYPE_ASIO,            (* Win32       - Low latency ASIO driver. *)
+    FMOD_OUTPUTTYPE_OSS,             (* Linux       - Open Sound System output. (Default on Linux) *)
+    FMOD_OUTPUTTYPE_ALSA,            (* Linux       - Advanced Linux Sound Architecture output. *)
+    FMOD_OUTPUTTYPE_ESD,             (* Linux       - Enlightment Sound Daemon output. *)
+    FMOD_OUTPUTTYPE_SOUNDMANAGER,    (* Mac         - Macintosh SoundManager output.  (Default on Mac carbon library)*)
+    FMOD_OUTPUTTYPE_COREAUDIO,       (* Mac         - Macintosh CoreAudio output.  (Default on Mac OSX library) *)
+    FMOD_OUTPUTTYPE_XBOX,            (* Xbox        - Native hardware output. (Default on Xbox) *)
+    FMOD_OUTPUTTYPE_PS2,             (* PS2         - Native hardware output. (Default on PS2) *)
+    FMOD_OUTPUTTYPE_PS3,             (* PS3         - Native hardware output. (Default on PS3) *)
+    FMOD_OUTPUTTYPE_GC,              (* GameCube    - Native hardware output. (Default on GameCube) *)
+    FMOD_OUTPUTTYPE_XBOX360,         (* Xbox 360    - Native hardware output. (Default on Xbox 360) *)
+    FMOD_OUTPUTTYPE_PSP,             (* PSP         - Native hardware output. (Default on PSP) *)
+    FMOD_OUTPUTTYPE_WII,                        (* Wii                 - Native hardware output. (Default on Wii) *)
+
+    FMOD_OUTPUTTYPE_MAX              (* Maximum number of output types supported. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Bit fields to use with FMOD_System_GetDriverCaps to determine the capabilities of a card / output device.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_GetDriverCaps
+]
+*)
+const
+    FMOD_CAPS_NONE                    = $00000000;    (* Device has no special capabilities *)
+    FMOD_CAPS_HARDWARE                = $00000001;    (* Device supports hardware mixing. *)
+    FMOD_CAPS_HARDWARE_EMULATED       = $00000002;    (* User has device set to 'Hardware acceleration = off' in control panel, and now extra 200ms latency is incurred.*)
+    FMOD_CAPS_OUTPUT_MULTICHANNEL     = $00000004;    (* Device can do multichannel output, ie greater than 2 channels *)
+    FMOD_CAPS_OUTPUT_FORMAT_PCM8      = $00000008;    (* Device can output to 8bit integer PCM *)
+    FMOD_CAPS_OUTPUT_FORMAT_PCM16     = $00000010;    (* Device can output to 16bit integer PCM *)
+    FMOD_CAPS_OUTPUT_FORMAT_PCM24     = $00000020;    (* Device can output to 24bit integer PCM *)
+    FMOD_CAPS_OUTPUT_FORMAT_PCM32     = $00000040;    (* Device can output to 32bit integer PCM *)
+    FMOD_CAPS_OUTPUT_FORMAT_PCMFLOAT  = $00000080;    (* Device can output to 32bit floating point PCM *)
+    FMOD_CAPS_REVERB_EAX2             = $00000100;    (* Device supports EAX2 reverb. *)
+    FMOD_CAPS_REVERB_EAX3             = $00000200;    (* Device supports EAX3 reverb. *)
+    FMOD_CAPS_REVERB_EAX4             = $00000400;    (* Device supports EAX4 reverb  *)
+    FMOD_CAPS_REVERB_EAX5                        = $00000800;    (* Device supports EAX5 reverb  *)
+    FMOD_CAPS_REVERB_I3DL2            = $00001000;    (* Device supports I3DL2 reverb. *)
+    FMOD_CAPS_REVERB_LIMITED          = $00002000;    (* Device supports some form of limited hardware reverb, maybe parameterless and only selectable by environment. *)
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These are speaker types defined for use with the FMOD_System_SetSpeakerMode or FMOD_System_GetSpeakerMode command.
+
+    [REMARKS]
+    These are important notes on speaker modes in regards to sounds created with FMOD_SOFTWARE.
+    Note below the phrase 'sound channels' is used.  These are the subchannels inside a sound, they are not related and
+    have nothing to do with the FMOD class "FMOD_CHANNEL".
+    For example a mono sound has 1 sound channel, a stereo sound has 2 sound channels, and an AC3 or 6 channel wav file have 6 "sound channels".
+
+    FMOD_SPEAKERMODE_NONE
+    ---------------------
+    This mode is for output devices that are not specifically mono/stereo/quad/surround/5.1 or 7.1, but are multichannel.
+    Sound channels map to speakers sequentially, so a mono sound maps to output speaker 0, stereo sound maps to output speaker 0 & 1.
+    The user assumes knowledge of the speaker order.  FMOD_SPEAKER enumerations may not apply, so raw channel indicies should be used.
+    Multichannel sounds map input channels to output channels 1:1.
+    Channel::setPan and Channel::setSpeakerMix do not work.
+    Speaker levels must be manually set with Channel::setSpeakerLevels.
+
+    FMOD_SPEAKERMODE_MONO
+    ---------------------
+    This mode is for a 1 speaker arrangement.
+    Panning does not work in this speaker mode.
+    Mono, stereo and multichannel sounds have each sound channel played on the one speaker unity.
+    Mix behaviour for multichannel sounds can be set with Channel::setSpeakerLevels.
+    Channel::setSpeakerMix does not work.
+
+    FMOD_SPEAKERMODE_STEREO
+    -----------------------
+    This mode is for 2 speaker arrangements that have a left and right speaker.
+    - Mono sounds default to an even distribution between left and right.  They can be panned with Channel::setPan.
+    - Stereo sounds default to the middle, or full left in the left speaker and full right in the right speaker.
+    - They can be cross faded with Channel::setPan.
+    - Multichannel sounds have each sound channel played on each speaker at unity.
+    - Mix behaviour for multichannel sounds can be set with Channel::setSpeakerLevels.
+    - Channel::setSpeakerMix works but only front left and right parameters are used, the rest are ignored.
+
+    FMOD_SPEAKERMODE_QUAD
+    ------------------------
+    This mode is for 4 speaker arrangements that have a front left, front right, rear left and a rear right speaker.
+    - Mono sounds default to an even distribution between front left and front right.  They can be panned with Channel::setPan.
+    - Stereo sounds default to the left sound channel played on the front left, and the right sound channel played on the front right.
+    - They can be cross faded with Channel::setPan.
+    - Multichannel sounds default to all of their sound channels being played on each speaker in order of input.
+    - Mix behaviour for multichannel sounds can be set with Channel::setSpeakerLevels.
+    - Channel::setSpeakerMix works but side left, side right, center and lfe are ignored.
+
+    FMOD_SPEAKERMODE_SURROUND
+    ------------------------
+    This mode is for 4 speaker arrangements that have a front left, front right, front center and a rear center.
+    - Mono sounds default to the center speaker.  They can be panned with Channel::setPan.
+    - Stereo sounds default to the left sound channel played on the front left, and the right sound channel played on the front right.
+    - They can be cross faded with Channel::setPan.
+    - Multichannel sounds default to all of their sound channels being played on each speaker in order of input.
+    - Mix behaviour for multichannel sounds can be set with Channel::setSpeakerLevels.
+    - Channel::setSpeakerMix works but side left, side right and lfe are ignored, and rear left / rear right are averaged into the rear speaker.
+
+    FMOD_SPEAKERMODE_5POINT1
+    ------------------------
+    This mode is for 5.1 speaker arrangements that have a left/right/center/rear left/rear right and a subwoofer speaker.
+    - Mono sounds default to the center speaker.  They can be panned with Channel::setPan.
+    - Stereo sounds default to the left sound channel played on the front left, and the right sound channel played on the front right.
+    - They can be cross faded with Channel::setPan.
+    - Multichannel sounds default to all of their sound channels being played on each speaker in order of input.
+    - Mix behaviour for multichannel sounds can be set with Channel::setSpeakerLevels.
+    - Channel::setSpeakerMix works but side left / side right are ignored.
+
+    FMOD_SPEAKERMODE_7POINT1
+    ------------------------
+    This mode is for 7.1 speaker arrangements that have a left/right/center/rear left/rear right/side left/side right
+    and a subwoofer speaker.
+    - Mono sounds default to the center speaker.  They can be panned with Channel::setPan.
+    - Stereo sounds default to the left sound channel played on the front left, and the right sound channel played on the front right.
+    - They can be cross faded with Channel::setPan.
+    - Multichannel sounds default to all of their sound channels being played on each speaker in order of input.
+    - Mix behaviour for multichannel sounds can be set with Channel::setSpeakerLevels.
+    - Channel::setSpeakerMix works and every parameter is used to set the balance of a sound in any speaker.
+
+    FMOD_SPEAKERMODE_PROLOGIC
+    ------------------------------------------------------
+    This mode is for mono, stereo, 5.1 and 7.1 speaker arrangements, as it is backwards and forwards compatible with stereo,
+    but to get a surround effect a Dolby Prologic or Prologic 2 hardware decoder / amplifier is needed.
+    Pan behaviour is the same as FMOD_SPEAKERMODE_5POINT1.
+
+    If this function is called the numoutputchannels setting in System::setSoftwareFormat is overwritten.
+
+    For 3D sounds, panning is determined at runtime by the 3D subsystem based on the speaker mode to determine which speaker the
+    sound should be placed in.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_SetSpeakerMode
+    FMOD_System_GetSpeakerMode
+    FMOD_System_GetDriverCaps
+    FMOD_Channel_SetSpeakerLevels
+]
+*)
+type
+  FMOD_SPEAKERMODE =
+  (
+    FMOD_SPEAKERMODE_RAW,              (* There is no specific speakermode.  Sound channels are mapped in order of input to output.  See remarks for more information. *)
+    FMOD_SPEAKERMODE_MONO,             (* The speakers are monaural. *)
+    FMOD_SPEAKERMODE_STEREO,           (* The speakers are stereo (DEFAULT). *)
+    FMOD_SPEAKERMODE_QUAD,             (* 4 speaker setup.  This includes front left, front right, rear left, rear right.  *)
+    FMOD_SPEAKERMODE_SURROUND,         (* 4 speaker setup.  This includes front left, front right, center, rear center (rear left/rear right are averaged). *)
+    FMOD_SPEAKERMODE_5POINT1,          (* 5.1 speaker setup.  This includes front left, front right, center, rear left, rear right and a subwoofer. *)
+    FMOD_SPEAKERMODE_7POINT1,          (* 7.1 speaker setup.  This includes front left, front right, center, rear left, rear right, side left, side right and a subwoofer. *)
+    FMOD_SPEAKERMODE_PROLOGIC          (* Stereo output, but data is encoded in a way that is picked up by a Prologic/Prologic2 decoder and split into a 5.1 speaker setup. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These are speaker types defined for use with the FMOD_Channel_SetSpeakerLevels command.
+    It can also be used for speaker placement in the FMOD_System_Set3DSpeakerPosition command.
+
+    [REMARKS]
+    If you are using FMOD_SPEAKERMODE_NONE and speaker assignments are meaningless, just cast a raw integer value to this type.
+    For example (FMOD_SPEAKER)7 would use the 7th speaker (also the same as FMOD_SPEAKER_SIDE_RIGHT).
+    Values higher than this can be used if an output system has more than 8 speaker types / output channels.  15 is the current maximum.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_SPEAKERMODE
+    FMOD_Channel_SetSpeakerLevels
+    FMOD_Channel_GetSpeakerLevels
+    FMOD_System_Set3DSpeakerPosition
+    FMOD_System_Get3DSpeakerPosition
+]
+*)
+type
+  FMOD_SPEAKER =
+  (
+    FMOD_SPEAKER_FRONT_LEFT,
+    FMOD_SPEAKER_FRONT_RIGHT,
+    FMOD_SPEAKER_FRONT_CENTER,
+    FMOD_SPEAKER_LOW_FREQUENCY,
+    FMOD_SPEAKER_BACK_LEFT,
+    FMOD_SPEAKER_BACK_RIGHT,
+    FMOD_SPEAKER_SIDE_LEFT,
+    FMOD_SPEAKER_SIDE_RIGHT,
+    FMOD_SPEAKER_MAX
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These are plugin types defined for use with the FMOD_System_GetNumPlugins,
+    FMOD_System_GetPluginInfo and FMOD_System_UnloadPlugin functions.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_GetNumPlugins
+    FMOD_System_GetPluginInfo
+    FMOD_System_UnloadPlugin
+]
+*)
+type
+  FMOD_PLUGINTYPE =
+  (
+    FMOD_PLUGINTYPE_OUTPUT,          (* The plugin type is an output module.  FMOD mixed audio will play through one of these devices *)
+    FMOD_PLUGINTYPE_CODEC,           (* The plugin type is a file format codec.  FMOD will use these codecs to load file formats for playback. *)
+    FMOD_PLUGINTYPE_DSP              (* The plugin type is a DSP unit.  FMOD will use these plugins as part of its DSP network to apply effects to output or generate sound in realtime. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Initialization flags.  Use them with FMOD_System_Init in the flags parameter to change various behaviour.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_Init
+]
+*)
+const
+    FMOD_INIT_NORMAL                   = $00000000;   (* All platforms - Initialize normally *)
+    FMOD_INIT_STREAM_FROM_UPDATE       = $00000001;   (* All platforms - No stream thread is created internally.  Streams are driven from System::update.  Mainly used with non-realtime outputs. *)
+    FMOD_INIT_3D_RIGHTHANDED           = $00000002;   (* All platforms - FMOD will treat +X as left, +Y as up and +Z as forwards. *)
+    FMOD_INIT_SOFTWARE_DISABLE         = $00000004;   (* All platforms - Disable software mixer to save memory.  Anything created with FMOD_SOFTWARE will fail and DSP will not work. *)
+    FMOD_INIT_SOFTWARE_OCCLUSION       = $00000008;   (* All platforms - All FMOD_SOFTWARE with FMOD_3D based voices will add a software lowpass filter effect into the DSP chain which is automatically used when Channel::set3DOcclusion is used or the geometry API. *)
+    FMOD_INIT_SOFTWARE_HRTF            = $00000010;   (* All platforms - All FMOD_SOFTWARE with FMOD_3D based voices will add a software lowpass filter effect into the DSP chain which causes sounds to sound duller when the sound goes behind the listener. *)
+    FMOD_INIT_VOL0_BECOMES_VIRTUAL     = $00000080;   (* All platforms - Any sounds that are 0 volume will go virtual and not be processed except for having their positions updated virtually.  Use System::setAdvancedSettings to adjust what volume besides zero to switch to virtual at. *)
+    FMOD_INIT_WASAPI_EXCLUSIVE         = $00000100;   (* Win32 Vista only - for WASAPI output - Enable exclusive access to hardware, lower latency at the expense of excluding other applications from accessing the audio hardware. *)
+    FMOD_INIT_DSOUND_HRTFNONE          = $00000200;   (* Win32 only - for DirectSound output - FMOD_HARDWARE | FMOD_3D buffers use simple stereo panning/doppler/attenuation when 3D hardware acceleration is not present. *)
+    FMOD_INIT_DSOUND_HRTFLIGHT         = $00000400;   (* Win32 only - for DirectSound output - FMOD_HARDWARE | FMOD_3D buffers use a slightly higher quality algorithm when 3D hardware acceleration is not present. *)
+    FMOD_INIT_DSOUND_HRTFFULL          = $00000800;   (* Win32 only - for DirectSound output - FMOD_HARDWARE | FMOD_3D buffers use full quality 3D playback when 3d hardware acceleration is not present. *)
+    FMOD_INIT_PS2_DISABLECORE0REVERB   = $00010000;   (* PS2 only - Disable reverb on CORE 0 to regain SRAM. *)
+    FMOD_INIT_PS2_DISABLECORE1REVERB   = $00020000;   (* PS2 only - Disable reverb on CORE 1 to regain SRAM. *)
+    FMOD_INIT_PS2_DONTUSESCRATCHPAD    = $00040000;   (* PS2 only - FMOD's usage of the scratchpad between System::update and System::updateFinished. *)
+    FMOD_INIT_PS2_SWAPDMACHANNELS      = $00080000;   (* PS2 only - Changes FMOD from using SPU DMA channel 0 for software mixing, and 1 for sound data upload/file streaming, to 1 and 0 respectively. *)
+    FMOD_INIT_XBOX_REMOVEHEADROOM      = $00100000;   (* Xbox only - By default DirectSound attenuates all sound by 6db to avoid clipping/distortion.  CAUTION.  If you use this flag you are responsible for the final mix to make sure clipping / distortion doesn't happen. *)
+    FMOD_INIT_360_MUSICMUTENOTPAUSE    = $00200000;   (* Xbox 360 only - The "music" channelgroup which by default pauses when custom 360 dashboard music is played, can be changed to mute (therefore continues playing) instead of pausing, by using this flag. *)
+
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These definitions describe the type of song being played.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetFormat
+]
+*)
+type
+  FMOD_SOUND_TYPE =
+  (
+    FMOD_SOUND_TYPE_UNKNOWN,         {  3rd party / unknown plugin format. }
+    FMOD_SOUND_TYPE_AAC,             {  AAC.  Currently unsupported. }
+    FMOD_SOUND_TYPE_AIFF,            {  AIFF. }
+    FMOD_SOUND_TYPE_ASF,             {  Microsoft Advanced Systems Format (ie WMA/ASF/WMV). }
+    FMOD_SOUND_TYPE_AT3,             {  Sony ATRAC 3 format }
+    FMOD_SOUND_TYPE_CDDA,            {  Digital CD audio. }
+    FMOD_SOUND_TYPE_DLS,             {  Sound font / downloadable sound bank. }
+    FMOD_SOUND_TYPE_FLAC,            {  FLAC lossless codec. }
+    FMOD_SOUND_TYPE_FSB,             {  FMOD Sample Bank. }
+    FMOD_SOUND_TYPE_GCADPCM,         {  GameCube ADPCM }
+    FMOD_SOUND_TYPE_IT,              {  Impulse Tracker. }
+    FMOD_SOUND_TYPE_MIDI,            {  MIDI. }
+    FMOD_SOUND_TYPE_MOD,             {  Protracker / Fasttracker MOD. }
+    FMOD_SOUND_TYPE_MPEG,            {  MP2/MP3 MPEG. }
+    FMOD_SOUND_TYPE_OGGVORBIS,       {  Ogg vorbis. }
+    FMOD_SOUND_TYPE_PLAYLIST,        {  Information only from ASX/PLS/M3U/WAX playlists }
+    FMOD_SOUND_TYPE_RAW,             {  Raw PCM data. }
+    FMOD_SOUND_TYPE_S3M,             {  ScreamTracker 3. }
+    FMOD_SOUND_TYPE_SF2,             {  Sound font 2 format. }
+    FMOD_SOUND_TYPE_USER,            {  User created sound. }
+    FMOD_SOUND_TYPE_WAV,             {  Microsoft WAV. }
+    FMOD_SOUND_TYPE_XM,              {  FastTracker 2 XM. }
+    FMOD_SOUND_TYPE_XMA,             {  Xbox360 XMA }
+    FMOD_SOUND_TYPE_VAG,             {  PlayStation 2 / PlayStation Portable adpcm VAG format. }
+
+    FMOD_SOUND_TYPE_MAX,             {  Maximum number of sound types supported. }
+    FMOD_SOUND_TYPE_FORCEINT = 65536 {  Makes sure this enum is signed 32bit. }
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These definitions describe the native format of the hardware or software buffer that will be used.
+
+    [REMARKS]
+    This is the format the native hardware or software buffer will be or is created in.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_CreateSound
+    FMOD_Sound_GetFormat
+]
+*)
+type
+  FMOD_SOUND_FORMAT =
+  (
+    FMOD_SOUND_FORMAT_NONE,             {  Unitialized / unknown.  }
+    FMOD_SOUND_FORMAT_PCM8,             {  8bit integer PCM data.  }
+    FMOD_SOUND_FORMAT_PCM16,            {  16bit integer PCM data.   }
+    FMOD_SOUND_FORMAT_PCM24,            {  24bit integer PCM data.   }
+    FMOD_SOUND_FORMAT_PCM32,            {  32bit integer PCM data.   }
+    FMOD_SOUND_FORMAT_PCMFLOAT,         {  32bit floating point PCM data.   }
+    FMOD_SOUND_FORMAT_GCADPCM,          {  Compressed GameCube DSP data.  }
+    FMOD_SOUND_FORMAT_IMAADPCM,         {  Compressed IMA ADPCM / Xbox ADPCM data.  }
+    FMOD_SOUND_FORMAT_VAG,              {  Compressed PlayStation 2 / PlayStation Portable ADPCM data.  }
+    FMOD_SOUND_FORMAT_XMA,              {  Compressed Xbox360 data.  }
+    FMOD_SOUND_FORMAT_MPEG,             {  Compressed MPEG layer 2 or 3 data.  }
+
+    FMOD_SOUND_FORMAT_MAX,              {  Maximum number of sound formats supported.  }
+    FMOD_SOUND_FORMAT_FORCEINT = 65536  {  Makes sure this enum is signed 32bit.  }
+  );
+  PFMOD_SOUND_FORMAT = ^FMOD_SOUND_FORMAT;
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Sound description bitfields, bitwise OR them together for loading and describing sounds.
+
+    [REMARKS]
+    By default a sound will open as a static sound that is decompressed fully into memory.  To have a sound stream instead, use FMOD_CREATESTREAM.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_CreateSound
+    FMOD_System_CreateStream
+    FMOD_Sound_GetMode
+    FMOD_Sound_GetMode
+    FMOD_Channel_SetMode
+    FMOD_Channel_GetMode
+]
+*)
+const
+  FMOD_DEFAULT                 = $00000000;  {  FMOD_DEFAULT is a default sound type.  Equivalent to all the defaults listed below.  FMOD_LOOP_OFF, FMOD_2D, FMOD_HARDWARE.  (Note - only Windows with a high spec soundcard, PS2, PSP, and Wii support FMOD_HARDWARE)  }
+  FMOD_LOOP_OFF                = $00000001;  {  For non looping sounds. (DEFAULT).  Overrides FMOD_LOOP_NORMAL / FMOD_LOOP_BIDI.  }
+  FMOD_LOOP_NORMAL             = $00000002;  {  For forward looping sounds.  }
+  FMOD_LOOP_BIDI               = $00000004;  {  For bidirectional looping sounds. (only works on software mixed static sounds).  }
+  FMOD_2D                      = $00000008;  {  Ignores any 3d processing. (DEFAULT).  }
+  FMOD_3D                      = $00000010;  {  Makes the sound positionable in 3D.  Overrides FMOD_2D.  }
+  FMOD_HARDWARE                = $00000020;  {  Attempts to make sounds use hardware acceleration. (DEFAULT).  Note on platforms that don't support FMOD_HARDWARE (only Windows with a high spec soundcard, PS2, PSP, and Wii support FMOD_HARDWARE), this will be internally treated as FMOD_SOFTWARE.  }
+  FMOD_SOFTWARE                = $00000040;  {  Makes the sound be mixed by the FMOD CPU based software mixer.  Overrides FMOD_HARDWARE.  Use this for FFT, DSP, compressed sample support, 2D multi-speaker support and other software related features.  }
+  FMOD_CREATESTREAM            = $00000080;  {  Decompress at runtime, streaming from the source provided (ie from disk).  Overrides FMOD_CREATESAMPLE and FMOD_CREATECOMPRESSEDSAMPLE.  Note a stream can only be played once at a time due to a stream only having 1 stream buffer and file handle.  Open multiple streams to have them play concurrently.  }
+  FMOD_CREATESAMPLE            = $00000100;  {  Decompress at loadtime, decompressing or decoding whole file into memory as the target sample format (ie PCM).  Fastest for FMOD_SOFTWARE based playback and most flexible.   }
+  FMOD_CREATECOMPRESSEDSAMPLE  = $00000200;  {  Load MP2, MP3, IMAADPCM or XMA into memory and leave it compressed.  During playback the FMOD software mixer will decode it in realtime as a 'compressed sample'.  Can only be used in combination with FMOD_SOFTWARE.  Overrides FMOD_CREATESAMPLE.  If the sound data is not ADPCM, MPEG or XMA it will behave as if it was created with FMOD_CREATESAMPLE and decode the sound into PCM.  }
+  FMOD_OPENUSER                = $00000400;  {  Opens a user created static sample or stream. Use FMOD_CREATESOUNDEXINFO to specify format and/or read callbacks.  If a user created 'sample' is created with no read callback, the sample will be empty.  Use Sound::lock and Sound::unlock to place sound data into the sound if this is the case.  }
+  FMOD_OPENMEMORY              = $00000800;  {  "name_or_data" will be interpreted as a pointer to memory instead of filename for creating sounds.  Use FMOD_CREATESOUNDEXINFO to specify length.  If used with FMOD_CREATESAMPLE or FMOD_CREATECOMPRESSEDSAMPLE, FMOD duplicates the memory into its own buffers.  Your own buffer can be freed after open.  If used with FMOD_CREATESTREAM, FMOD will stream out of the buffer whose pointer you passed in.  In this case, your own buffer should not be freed until you have finished with and released the stream. }
+  FMOD_OPENMEMORY_POINT        = $10000000;  {  "name_or_data" will be interpreted as a pointer to memory instead of filename for creating sounds.  Use FMOD_CREATESOUNDEXINFO to specify length.  This differs to FMOD_OPENMEMORY in that it uses the memory as is, without duplicating the memory into its own buffers.  For Wii/PSP FMOD_HARDWARE supports this flag for the GCADPCM/VAG formats.  On other platforms FMOD_SOFTWARE must be used, as sound hardware on the other platforms (ie PC) cannot access main ram.  Cannot be freed after open, only after Sound::release.   Will not work if the data is compressed and FMOD_CREATECOMPRESSEDSAMPLE is not used.  }
+  FMOD_OPENRAW                 = $00001000;  {  Will ignore file format and treat as raw pcm.  Use FMOD_CREATESOUNDEXINFO to specify format.  Requires at least defaultfrequency, numchannels and format to be specified before it will open.  Must be little endian data.  }
+  FMOD_OPENONLY                = $00002000;  {  Just open the file, dont prebuffer or read.  Good for fast opens for info, or when sound::readData is to be used.  }
+  FMOD_ACCURATETIME            = $00004000;  {  For System::createSound - for accurate Sound::getLength/Channel::setPosition on VBR MP3, and MOD/S3M/XM/IT/MIDI files.  Scans file first, so takes longer to open. FMOD_OPENONLY does not affect this.  }
+  FMOD_MPEGSEARCH              = $00008000;  {  For corrupted / bad MP3 files.  This will search all the way through the file until it hits a valid MPEG header.  Normally only searches for 4k.  }
+  FMOD_NONBLOCKING             = $00010000;  {  For opening sounds and getting streamed subsounds (seeking) asyncronously.  Use Sound::getOpenState to poll the state of the sound as it opens or retrieves the subsound in the background.  }
+  FMOD_UNIQUE                  = $00020000;  {  Unique sound, can only be played one at a time  }
+  FMOD_3D_HEADRELATIVE         = $00040000;  {  Make the sound's position, velocity and orientation relative to the listener.  }
+  FMOD_3D_WORLDRELATIVE        = $00080000;  {  Make the sound's position, velocity and orientation absolute (relative to the world). (DEFAULT)  }
+  FMOD_3D_LOGROLLOFF           = $00100000;  {  This sound will follow the standard logarithmic rolloff model where mindistance = full volume, maxdistance = where sound stops attenuating, and rolloff is fixed according to the global rolloff factor.  (DEFAULT)  }
+  FMOD_3D_LINEARROLLOFF        = $00200000;  {  This sound will follow a linear rolloff model where mindistance = full volume, maxdistance = silence.  Rolloffscale is ignored.  }
+  FMOD_3D_CUSTOMROLLOFF        = $04000000;  {  This sound will follow a rolloff model defined by Sound::set3DCustomRolloff / Channel::set3DCustomRolloff.   }
+  FMOD_3D_IGNOREGEOMETRY       = $40000000;  {  Is not affect by geometry occlusion.  If not specified in Sound::setMode, or Channel::setMode, the flag is cleared and it is affected by geometry again.  }
+  FMOD_CDDA_FORCEASPI          = $00400000;  {  For CDDA sounds only - use ASPI instead of NTSCSI to access the specified CD/DVD device.  }
+  FMOD_CDDA_JITTERCORRECT      = $00800000;  {  For CDDA sounds only - perform jitter correction. Jitter correction helps produce a more accurate CDDA stream at the cost of more CPU time.  }
+  FMOD_UNICODE                 = $01000000;  {  Filename is double-byte unicode.  }
+  FMOD_IGNORETAGS              = $02000000;  {  Skips id3v2/asf/etc tag checks when opening a sound, to reduce seek/read overhead when opening files (helps with CD performance).  }
+  FMOD_LOWMEM                  = $08000000;  {  Removes some features from samples to give a lower memory overhead, like Sound::getName.  See remarks.  }
+  FMOD_LOADSECONDARYRAM        = $20000000;  {  Load sound into the secondary RAM of supported platform. On PS3, sounds will be loaded into RSX/VRAM.  }
+  FMOD_VIRTUAL_PLAYFROMSTART   = $80000000;  {  For sounds that start virtual (due to being quiet or low importance), instead of swapping back to audible, and playing at the correct offset according to time, this flag makes the sound play from the start.  }
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These values describe what state a sound is in after FMOD_NONBLOCKING has been used to open it.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetOpenState
+    FMOD_MODE
+]
+*)
+type
+  FMOD_OPENSTATE =
+  (
+    FMOD_OPENSTATE_READY = 0,       {  Opened and ready to play.  }
+    FMOD_OPENSTATE_LOADING,         {  Initial load in progress.  }
+    FMOD_OPENSTATE_ERROR,           {  Failed to open - file not found, out of memory etc.  See return value of Sound::getOpenState for what happened.  }
+    FMOD_OPENSTATE_CONNECTING,      {  Connecting to remote host (internet sounds only).  }
+    FMOD_OPENSTATE_BUFFERING,       {  Buffering data.  }
+    FMOD_OPENSTATE_SEEKING,         {  Seeking to subsound and re-flushing stream buffer.  }
+    FMOD_OPENSTATE_STREAMING,       {  Ready and playing, but not possible to release at this time without stalling the main thread.  }
+    FMOD_OPENSTATE_SETPOSITION,     {  Seeking within a stream to a different position.  }
+
+    FMOD_OPENSTATE_MAX,             {  Maximum number of open state types.  }
+    FMOD_OPENSTATE_FORCEINT = 65536 {  Makes sure this enum is signed 32bit.  }
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These callback types are used with FMOD_Channel_SetCallback.
+
+    [REMARKS]
+    Each callback has commanddata parameters passed int unique to the type of callback.
+    See reference to FMOD_CHANNEL_CALLBACK to determine what they might mean for each type of callback.
+
+    Note!  Currently the user must call System::update for these callbacks to trigger!
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Channel_SetCallback
+    FMOD_CHANNEL_CALLBACK
+    FMOD_System_Update
+]
+*)
+type
+  FMOD_CHANNEL_CALLBACKTYPE =
+  (
+    FMOD_CHANNEL_CALLBACKTYPE_END,                                 (* Called when a sound ends. *)
+    FMOD_CHANNEL_CALLBACKTYPE_VIRTUALVOICE,      (* Called when a voice is swapped out or swapped in. *)
+    FMOD_CHANNEL_CALLBACKTYPE_SYNCPOINT,                       (* Called when a syncpoint is encountered.  Can be from wav file markers. *)
+    FMOD_CHANNEL_CALLBACKTYPE_OCCLUSION,      (* Called when the channel has its geometry occlusion value calculated.  Can be used to clamp or change the value. *)
+
+    FMOD_CHANNEL_CALLBACKTYPE_MAX ,           (* Maximum number of callback types supported. *)
+    FMOD_CHANNEL_CALLBACKTYPE_FORCEINT = 65536
+  );
+
+
+(*
+    FMOD Callbacks
+*)
+type
+  FMOD_CHANNEL_CALLBACK       = function (channel: FMOD_CHANNEL; callbacktype: FMOD_CHANNEL_CALLBACKTYPE; commanddata1: Pointer; commanddata2: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+  FMOD_SOUND_NONBLOCKCALLBACK = function (sound: FMOD_SOUND; result: FMOD_RESULT): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_SOUND_PCMREADCALLBACK  = function (sound: FMOD_SOUND; data: Pointer; datalen: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_SOUND_PCMSEEKCALLBACK  = function (sound: FMOD_SOUND; subsound: Integer; pcmoffset: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+  FMOD_FILE_OPENCALLBACK      = function (const name: PChar; unicode: Integer; filesize: Cardinal; handle: Pointer; var userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_FILE_CLOSECALLBACK     = function (handle: Pointer; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_FILE_READCALLBACK      = function (handle, buffer: Pointer; sizebytes: Cardinal; var bytesread: Cardinal; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_FILE_SEEKCALLBACK      = function (handle: Pointer; pos, type_: Integer; userdata: Pointer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+  FMOD_MEMORY_ALLOCCALLBACK   = function (size: Cardinal): Pointer; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_MEMORY_REALLOCCALLBACK = function (ptr: Pointer; size: Cardinal): Pointer; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_MEMORY_FREECALLBACK    = procedure (ptr: Pointer); {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    List of windowing methods used in spectrum analysis to reduce leakage / transient signals intefering with the analysis.
+    This is a problem with analysis of continuous signals that only have a small portion of the signal sample (the fft window size).
+    Windowing the signal with a curve or triangle tapers the sides of the fft window to help alleviate this problem.
+
+    [REMARKS]
+    Cyclic signals such as a sine wave that repeat their cycle in a multiple of the window size do not need windowing.
+    I.e. If the sine wave repeats every 1024, 512, 256 etc samples and the FMOD fft window is 1024, then the signal would not need windowing.
+    Not windowing is the same as FMOD_DSP_FFT_WINDOW_RECT, which is the default.
+    If the cycle of the signal (ie the sine wave) is not a multiple of the window size, it will cause frequency abnormalities, so a different windowing method is needed.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_GetSpectrum
+    FMOD_Channel_GetSpectrum
+]
+*)
+type
+  FMOD_DSP_FFT_WINDOW =
+  (
+    FMOD_DSP_FFT_WINDOW_RECT,           (* w[n] = 1.0                                                                                            *)
+    FMOD_DSP_FFT_WINDOW_TRIANGLE,       (* w[n] = TRI(2n/N)                                                                                      *)
+    FMOD_DSP_FFT_WINDOW_HAMMING,        (* w[n] = 0.54 - (0.46 * COS(n/N) )                                                                      *)
+    FMOD_DSP_FFT_WINDOW_HANNING,        (* w[n] = 0.5 *  (1.0  - COS(n/N) )                                                                      *)
+    FMOD_DSP_FFT_WINDOW_BLACKMAN,       (* w[n] = 0.42 - (0.5  * COS(n/N) ) + (0.08 * COS(2.0 * n/N) )                                           *)
+    FMOD_DSP_FFT_WINDOW_BLACKMANHARRIS, (* w[n] = 0.35875 - (0.48829 * COS(1.0 * n/N)) + (0.14128 * COS(2.0 * n/N)) - (0.01168 * COS(3.0 * n/N)) *)
+    FMOD_DSP_FFT_WINDOW_MAX
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    List of interpolation types that the FMOD Ex software mixer supports.
+
+    [REMARKS]
+    The default resampler type is FMOD_DSP_RESAMPLER_LINEAR.
+    Use System::setSoftwareFormat to tell FMOD the resampling quality you require for FMOD_SOFTWARE based sounds.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    System::setSoftwareFormat
+    System::getSoftwareFormat
+]
+*)
+type
+  FMOD_DSP_RESAMPLER =
+  (
+    FMOD_DSP_RESAMPLER_NOINTERP,        (* No interpolation.  High frequency aliasing hiss will be audible depending on the sample rate of the sound. *)
+    FMOD_DSP_RESAMPLER_LINEAR,          (* Linear interpolation (default method).  Fast and good quality, causes very slight lowpass effect on low frequency sounds. *)
+    FMOD_DSP_RESAMPLER_CUBIC,           (* Cubic interoplation.  Slower than linear interpolation but better quality. *)
+    FMOD_DSP_RESAMPLER_SPLINE,          (* 5 point spline interoplation.  Slowest resampling method but best quality. *)
+    FMOD_DSP_RESAMPLER_MAX
+  );
+
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    List of tag types that could be stored within a sound.  These include id3 tags, metadata from netstreams and vorbis/asf data.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetTag
+]
+*)
+type
+  FMOD_TAGTYPE =
+  (
+    FMOD_TAGTYPE_UNKNOWN,
+    FMOD_TAGTYPE_ID3V1,
+    FMOD_TAGTYPE_ID3V2,
+    FMOD_TAGTYPE_VORBISCOMMENT,
+    FMOD_TAGTYPE_SHOUTCAST,
+    FMOD_TAGTYPE_ICECAST,
+    FMOD_TAGTYPE_ASF,
+    FMOD_TAGTYPE_MIDI,
+    FMOD_TAGTYPE_PLAYLIST,
+    FMOD_TAGTYPE_FMOD,
+    FMOD_TAGTYPE_USER,
+    FMOD_TAGTYPE_MAX
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    List of data types that can be returned by FMOD_Sound_GetTag
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetTag
+]
+*)
+type
+  FMOD_TAGDATATYPE =
+  (
+    FMOD_TAGDATATYPE_BINARY,
+    FMOD_TAGDATATYPE_INT,
+    FMOD_TAGDATATYPE_FLOAT,
+    FMOD_TAGDATATYPE_STRING,
+    FMOD_TAGDATATYPE_STRING_UTF16,
+    FMOD_TAGDATATYPE_STRING_UTF16BE,
+    FMOD_TAGDATATYPE_STRING_UTF8,
+    FMOD_TAGDATATYPE_CDTOC
+  );
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Structure describing a piece of tag data.
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetTag
+    FMOD_TAGTYPE
+    FMOD_TAGDATATYPE
+]
+*)
+type
+  FMOD_TAG = record
+    tagtype: FMOD_TAGTYPE;      (* [out] The type of this tag. *)
+    datatype: FMOD_TAGDATATYPE; (* [out] The type of data that this tag contains *)
+    name: PChar;                (* [out] The name of this tag i.e. "TITLE", "ARTIST" etc. *)
+    data: Pointer;              (* [out] Pointer to the tag data - its format is determined by the datatype member *)
+    datalen: Cardinal;          (* [out] Length of the data contained in this tag *)
+    updated: FMOD_BOOL;         (* [out] True if this tag has been updated since last being accessed with FMOD_Sound_GetTag *)
+  end;
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Structure describing a CD/DVD table of contents
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetTag
+]
+*)
+type
+  FMOD_CDTOC = record
+    numtracks: Integer;                 (* [out] The number of tracks on the CD *)
+    min: array [0..99] of Integer;      (* [out] The start offset of each track in minutes *)
+    sec: array [0..99] of Integer;      (* [out] The start offset of each track in seconds *)
+    frame: array [0..99] of Integer;    (* [out] The start offset of each track in frames *)
+  end;
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    List of time types that can be returned by FMOD_Sound_GetLength and used with FMOD_Channel_SetPosition or FMOD_Channel_GetPosition.
+
+    [REMARKS]
+    FMOD_TIMEUNIT_SUBSOUND_MS, FMOD_TIMEUNIT_SUBSOUND_PCM, FMOD_TIMEUNIT_SUBSOUND_PCMBYTES, FMOD_TIMEUNIT_SUBSOUND and FMOD_TIMEUNIT_SUBSOUND_BUFFERED are only supported by Channel::getPosition.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Sound_GetLength
+    FMOD_Channel_SetPosition
+    FMOD_Channel_GetPosition
+]
+*)
+const
+    FMOD_TIMEUNIT_MS                 = $00000001;  (* Milliseconds. *)
+    FMOD_TIMEUNIT_PCM                = $00000002;  (* PCM Samples, related to milliseconds * samplerate / 1000. *)
+    FMOD_TIMEUNIT_PCMBYTES           = $00000004;  (* Bytes, related to PCM samples * channels * datawidth (ie 16bit = 2 bytes). *)
+    FMOD_TIMEUNIT_RAWBYTES           = $00000008;  (* Raw file bytes of (compressed) sound data (does not include headers).  Only used by FMOD_Sound_GetLength and FMOD_Channel_GetPosition. *)
+    FMOD_TIMEUNIT_MODORDER           = $00000100;  (* MOD/S3M/XM/IT.  Order in a sequenced module format.  Use FMOD_Sound_GetFormat to determine the format. *)
+    FMOD_TIMEUNIT_MODROW             = $00000200;  (* MOD/S3M/XM/IT.  Current row in a sequenced module format.  FMOD_Sound_GetLength will return the number if rows in the currently playing or seeked to pattern. *)
+    FMOD_TIMEUNIT_MODPATTERN         = $00000400;  (* MOD/S3M/XM/IT.  Current pattern in a sequenced module format.  Sound::getLength will return the number of patterns in the song and Channel::getPosition will return the currently playing pattern. *)
+    FMOD_TIMEUNIT_SENTENCE_MS        = $00010000;  (* Currently playing subsound in a sentence time in milliseconds. *)
+    FMOD_TIMEUNIT_SENTENCE_PCM       = $00020000;  (* Currently playing subsound in a sentence time in PCM Samples, related to milliseconds * samplerate / 1000. *)
+    FMOD_TIMEUNIT_SENTENCE_PCMBYTES  = $00040000;  (* Currently playing subsound in a sentence time in bytes, related to PCM samples * channels * datawidth (ie 16bit = 2 bytes). *)
+    FMOD_TIMEUNIT_SENTENCE           = $00080000;  (* Currently playing subsound in a sentence according to the channel. For display.  *)
+    FMOD_TIMEUNIT_SENTENCE_SUBSOUND  = $00100000;  (* Currently playing subsound in a sentence according to the buffered ahead of time sound.  For sentence processing ahead of time. *)
+    FMOD_TIMEUNIT_BUFFERED           = $10000000;  (* Time value as seen by buffered stream.  This is always ahead of audible time, and is only used for processing. *)
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    When creating a multichannel sound, FMOD will pan them to their default speaker locations, for example a 6 channel sound will default to one channel per 5.1 output speaker.
+    Another example is a stereo sound.  It will default to left = front left, right = front right.
+
+    This is for sounds that are not 'default'.  For example you might have a sound that is 6 channels but actually made up of 3 stereo pairs, that should all be located in front left, front right only.
+
+    [REMARKS]
+    For full flexibility of speaker assignments, use Channel::setSpeakerLevels.  This functionality is cheaper, uses less memory and easier to use.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3, Wii
+
+    [SEE_ALSO]
+    FMOD_CREATESOUNDEXINFO
+    FMOD_Channel_SetSpeakerLevels
+]
+*)
+type
+  FMOD_SPEAKERMAPTYPE =
+  (
+    FMOD_SPEAKERMAPTYPE_DEFAULT,     (* This is the default, and just means FMOD decides which speakers it puts the source channels. *)
+    FMOD_SPEAKERMAPTYPE_ALLMONO,     (* This means the sound is made up of all mono sounds.  All voices will be panned to the front center by default in this case.  *)
+    FMOD_SPEAKERMAPTYPE_ALLSTEREO    (* This means the sound is made up of all stereo sounds.  All voices will be panned to front left and front right alternating every second channel.  *)
+  );
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Use this structure with FMOD_System_CreateSound when more control is needed over loading.
+    The possible reasons to use this with FMOD_System_CreateSound are:
+    - Loading a file from memory.
+    - Loading a file from within another file, giving an offset and length.
+    - To create a user created / non file based sound.
+    - To specify a starting subsound to seek to within a multi-sample sounds (ie FSB/DLS/SF2) when created as a stream.
+    - To specify which subsounds to load for multi-sample sounds (ie FSB/DLS/SF2) so that memory is saved and only a subset is actually loaded/read from disk.
+    - To specify 'piggyback' read and seek callbacks for capture of sound data as fmod reads and decodes it.  Useful for ripping decoded PCM data from sounds as they are loaded / played.
+    - To specify a MIDI DLS/SF2 sample set file to load when opening a MIDI file.
+    See below on what members to fill for each of the above types of sound you want to create.
+
+    [REMARKS]
+    This structure is optional!  Specify 0 or NULL in FMOD_System_CreateSound if you don't need it!
+
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    Loading a file from memory.
+    ---------------------------
+    - Create the sound using the FMOD_OPENMEMORY flag.
+    - Mandatory.  Specify 'length' for the size of the memory block in bytes.
+    - Other flags are optional.
+
+    Loading a file from within another larger (possibly wad/pak) file, by giving the loader an offset and length.
+    -------------------------------------------------------------------------------------------------------------
+    - Mandatory.  Specify 'fileoffset' and 'length'.
+    - Other flags are optional.
+
+    To create a user created / non file based sound.
+    ------------------------------------------------
+    - Create the sound using the FMOD_OPENUSER flag.
+    - Mandatory.  Specify 'defaultfrequency, 'numchannels' and 'format'.
+    - Other flags are optional.
+
+    To specify a starting subsound to seek to and flush with, within a multi-sample stream (ie FSB/DLS/SF2).
+    --------------------------------------------------------------------------------------------------------
+    - Mandatory.  Specify 'initialsubsound'.
+
+    To specify which subsounds to load for multi-sample sounds (ie FSB/DLS/SF2) so that memory is saved and only a subset is actually loaded/read from disk.
+    --------------------------------------------------------------------------------------------------------------------------------------------------------
+    - Mandatory.  Specify 'inclusionlist' and 'inclusionlistnum'.
+
+    To specify 'piggyback' read and seek callbacks for capture of sound data as fmod reads and decodes it.  Useful for ripping decoded PCM data from sounds as they are loaded / played.
+    ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+    - Mandatory.  Specify 'pcmreadcallback' and 'pcmseekcallback'.
+
+    To specify a MIDI DLS/SF2 sample set file to load when opening a MIDI file.
+    ---------------------------------------------------------------------------
+    - Mandatory.  Specify 'dlsname'.
+
+    Setting the 'decodebuffersize' is for cpu intensive codecs that may be causing stuttering, not file intensive codecs (ie those from CD or netstreams) which are normally altered with System::setStreamBufferSize.  As an example of cpu intensive codecs, an mp3 file will take more cpu to decode than a PCM wav file.
+    If you have a stuttering effect, then it is using more cpu than the decode buffer playback rate can keep up with.  Increasing the decode buffersize will most likely solve this problem.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_CreateSound
+]
+*)
+type
+  FMOD_CREATESOUNDEXINFO = record
+    cbsize: Integer;                                   (* [in] Size of this structure.  This is used so the structure can be expanded in the future and still work on older versions of FMOD Ex. *)
+    length: Cardinal;                                  (* [in] Optional. Specify 0 to ignore. Size in bytes of file to load, or sound to create (in this case only if FMOD_OPENUSER is used).  Required if loading from memory.  If 0 is specified, then it will use the size of the file (unless loading from memory then an error will be returned). *)
+    fileoffset: Cardinal;                              (* [in] Optional. Specify 0 to ignore. Offset from start of the file to start loading from.  This is useful for loading files from inside big data files. *)
+    numchannels: Integer;                              (* [in] Optional. Specify 0 to ignore. Number of channels in a sound specified only if FMOD_OPENUSER is used. *)
+    defaultfrequency: Integer;                         (* [in] Optional. Specify 0 to ignore. Default frequency of sound in a sound specified only if FMOD_OPENUSER is used.  Other formats use the frequency determined by the file format. *)
+    format: FMOD_SOUND_FORMAT;                         (* [in] Optional. Specify 0 or FMOD_SOUND_FORMAT_NONE to ignore. Format of the sound specified only if FMOD_OPENUSER is used.  Other formats use the format determined by the file format.   *)
+    decodebuffersize: Integer;                         (* [in] Optional. Specify 0 to ignore. For streams.  This determines the size of the double buffer (in PCM samples) that a stream uses.  Use this for user created streams if you want to determine the size of the callback buffer passed to you.  Specify 0 to use FMOD's default size which is currently equivalent to 400ms of the sound format created/loaded. *)
+    initialsubsound: Integer;                          (* [in] Optional. Specify 0 to ignore. In a multi-sample file format such as .FSB/.DLS/.SF2, specify the initial subsound to seek to, only if FMOD_CREATESTREAM is used. *)
+    numsubsounds: Integer;                             (* [in] Optional. Specify 0 to ignore or have no subsounds.  In a user created multi-sample sound, specify the number of subsounds within the sound that are accessable with FMOD_SoundGetSubSound. *)
+    inclusionlist: ^Integer;                           (* [in] Optional. Specify 0 to ignore. In a multi-sample format such as .FSB/.DLS/.SF2 it may be desirable to specify only a subset of sounds to be loaded out of the whole file.  This is an array of subsound indicies to load into memory when created. *)
+    inclusionlistnum: Integer;                         (* [in] Optional. Specify 0 to ignore. This is the number of integers contained within the *)
+    pcmreadcallback: FMOD_SOUND_PCMREADCALLBACK;       (* [in] Optional. Specify 0 to ignore. Callback to 'piggyback' on FMOD's read functions and accept or even write PCM data while FMOD is opening the sound.  Used for user sounds created with FMOD_OPENUSER or for capturing decoded data as FMOD reads it. *)
+    pcmseekcallback: FMOD_SOUND_PCMSEEKCALLBACK;       (* [in] Optional. Specify 0 to ignore. Callback for when the user calls a seeking function such as FMOD_Channel_SetPosition within a multi-sample sound, and for when it is opened.*)
+    nonblockcallback: FMOD_SOUND_NONBLOCKCALLBACK;     (* [in] Optional. Specify 0 to ignore. Callback for successful completion, or error while loading a sound that used the FMOD_NONBLOCKING flag.*)
+    dlsname: PChar;                                    (* [in] Optional. Specify 0 to ignore. Filename for a DLS or SF2 sample set when loading a MIDI file.   If not specified, on windows it will attempt to open /windows/system32/drivers/gm.dls, otherwise the MIDI will fail to open.  *)
+    encryptionkey: PChar;                              (* [in] Optional. Specify 0 to ignore. Key for encrypted FSB file.  Without this key an encrypted FSB file will not load. *)
+    maxpolyphony: Integer;                             (* [in] Optional. Specify 0 to ignore. For sequenced formats with dynamic channel allocation such as .MID and .IT, this specifies the maximum voice count allowed while playing.  .IT defaults to 64.  .MID defaults to 32. *)
+    userdata: Pointer;                                 (* [in] Optional. Specify 0 to ignore. This is user data to be attached to the sound during creation.  Access via Sound::getUserData. *)
+    suggestedsoundtype: FMOD_SOUND_TYPE;               (* [in] Optional. Specify 0 or FMOD_SOUND_TYPE_UNKNOWN to ignore.  Instead of scanning all codec types, use this to speed up loading by making it jump straight to this codec. *)
+    useropen:  FMOD_FILE_OPENCALLBACK;                 (* [in] Optional. Specify 0 to ignore. Callback for opening this file. *)
+    userclose: FMOD_FILE_CLOSECALLBACK;                (* [in] Optional. Specify 0 to ignore. Callback for closing this file. *)
+    userread: FMOD_FILE_READCALLBACK;                  (* [in] Optional. Specify 0 to ignore. Callback for reading from this file. *)
+    userseek: FMOD_FILE_SEEKCALLBACK;                  (* [in] Optional. Specify 0 to ignore. Callback for seeking within this file. *)
+    speakermap: FMOD_SPEAKERMAPTYPE;                   (* [in] Optional. Specify 0 to ignore. Use this to differ the way fmod maps multichannel sounds to speakers.  See FMOD_SPEAKERMAPTYPE for more. *)
+    initialsoundgroup: Integer;                        (* [in] Optional. Specify 0 to ignore. Specify a sound group if required, to put sound in as it is created. *)
+    initialseekposition: Integer;                      (* [in] Optional. Specify 0 to ignore. For streams. Specify an initial position to seek the stream to. *)
+    initialseekpostype: FMOD_TIMEUNIT;                 (* [in] Optional. Specify 0 to ignore. For streams. Specify the time unit for the position set in initialseekposition. *)
+    ignoresetfilesystem: Integer;                      (* [in] Optional. Specify 0 to ignore. Set to 1 to use fmod's built in file system. Ignores setFileSystem callbacks and also FMOD_CREATESOUNEXINFO file callbacks.  Useful for specific cases where you don't want to use your own file system but want to use fmod's file system (ie net streaming). *)
+    // myVersion2013: Array [0..27] of Byte;
+  end;
+  PFMOD_CREATESOUNDEXINFO = ^FMOD_CREATESOUNDEXINFO;
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Structure defining a reverb environment.
+
+    For more indepth descriptions of the reverb properties under win32, please see the EAX2 and EAX3
+    documentation at http://developer.creative.com/ under the 'downloads' section.
+    If they do not have the EAX3 documentation, then most information can be attained from
+    the EAX2 documentation, as EAX3 only adds some more parameters and functionality on top of
+    EAX2.
+
+    [REMARKS]
+    Note the default reverb properties are the same as the FMOD_PRESET_GENERIC preset.
+    Note that integer values that typically range from -10,000 to 1000 are represented in
+    decibels, and are of a logarithmic scale, not linear, wheras float values are always linear.
+    PORTABILITY: Each member has the platform it supports in braces ie (win32/xbox).
+    Some reverb parameters are only supported in win32 and some only on xbox. If all parameters are set then
+    the reverb should product a similar effect on either platform.
+    Win32/Win64 - This is only supported with FMOD_OUTPUTTYPE_DSOUND and EAX compatible sound cards.
+    Macintosh - Currently unsupported.
+    Linux - Currently unsupported.
+    Xbox - Only a subset of parameters are supported.
+    PlayStation 2 - Only the Environment and Flags paramenters are supported.
+    GameCube - Only a subset of parameters are supported.
+
+    The numerical values listed below are the maximum, minimum and default values for each variable respectively.
+
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    [PLATFORMS]
+    Win32, Win64, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable
+
+    [SEE_ALSO]
+    FMOD_System_SetReverbProperties
+    FMOD_System_GetReverbProperties
+    FMOD_REVERB_PRESETS
+    FMOD_REVERB_FLAGS
+]
+*)
+type
+  FMOD_REVERB_PROPERTIES = record           (*          MIN     MAX     DEFAULT  DESCRIPTION *)
+    Instance: Integer;                      (* [in/out] 0     , 2     , 0      , EAX4 only. Environment Instance. 3 seperate reverbs simultaneously are possible. This specifies which one to set. (win32 only) *)
+    Environment: Cardinal;                  (* [in/out] 0     , 25    , 0      , sets all listener properties (win32/ps2) *)
+    EnvSize: Single;                        (* [in/out] 1.0   , 100.0 , 7.5    , environment size in meters (win32 only) *)
+    EnvDiffusion: Single;                   (* [in/out] 0.0   , 1.0   , 1.0    , environment diffusion (win32/xbox) *)
+    Room: Integer;                          (* [in/out] -10000, 0     , -1000  , room effect level (at mid frequencies) (win32/xbox) *)
+    RoomHF: Integer;                        (* [in/out] -10000, 0     , -100   , relative room effect level at high frequencies (win32/xbox) *)
+    RoomLF: Integer;                        (* [in/out] -10000, 0     , 0      , relative room effect level at low frequencies (win32 only) *)
+    DecayTime: Single;                      (* [in/out] 0.1   , 20.0  , 1.49   , reverberation decay time at mid frequencies (win32/xbox) *)
+    DecayHFRatio: Single;                   (* [in/out] 0.1   , 2.0   , 0.83   , high-frequency to mid-frequency decay time ratio (win32/xbox) *)
+    DecayLFRatio: Single;                   (* [in/out] 0.1   , 2.0   , 1.0    , low-frequency to mid-frequency decay time ratio (win32 only) *)
+    Reflections: Integer;                   (* [in/out] -10000, 1000  , -2602  , early reflections level relative to room effect (win32/xbox) *)
+    ReflectionsDelay: Single;               (* [in/out] 0.0   , 0.3   , 0.007  , initial reflection delay time (win32/xbox) *)
+    ReflectionsPan: array [0..2] of Single; (* [in/out]       ,       , [0,0,0], early reflections panning vector (win32 only) *)
+    Reverb: Integer;                        (* [in/out] -10000, 2000  , 200    , late reverberation level relative to room effect (win32/xbox) *)
+    ReverbDelay: Single;                    (* [in/out] 0.0   , 0.1   , 0.011  , late reverberation delay time relative to initial reflection (win32/xbox) *)
+    ReverbPan: array [0..2] of Single;      (* [in/out]       ,       , [0,0,0], late reverberation panning vector (win32 only) *)
+    EchoTime: Single;                       (* [in/out] .075  , 0.25  , 0.25   , echo time (win32 only) *)
+    EchoDepth: Single;                      (* [in/out] 0.0   , 1.0   , 0.0    , echo depth (win32 only) *)
+    ModulationTime: Single;                 (* [in/out] 0.04  , 4.0   , 0.25   , modulation time (win32 only) *)
+    ModulationDepth: Single;                (* [in/out] 0.0   , 1.0   , 0.0    , modulation depth (win32 only) *)
+    AirAbsorptionHF: Single;                (* [in/out] -100  , 0.0   , -5.0   , change in level per meter at high frequencies (win32 only) *)
+    HFReference: Single;                    (* [in/out] 1000.0, 20000 , 5000.0 , reference high frequency (hz) (win32/xbox) *)
+    LFReference: Single;                    (* [in/out] 20.0  , 1000.0, 250.0  , reference low frequency (hz) (win32 only) *)
+    RoomRolloffFactor: Single;              (* [in/out] 0.0   , 10.0  , 0.0    , like FMOD_3D_Listener_SetRolloffFactor but for room effect (win32/xbox) *)
+    Diffusion: Single;                      (* [in/out] 0.0   , 100.0 , 100.0  , Value that controls the echo density in the late reverberation decay. (xbox only) *)
+    Density: Single;                        (* [in/out] 0.0   , 100.0 , 100.0  , Value that controls the modal density in the late reverberation decay (xbox only) *)
+    Flags: Cardinal;                        (* [in/out] FMOD_REVERB_FLAGS - modifies the behavior of above properties (win32/ps2) *)
+  end;
+
+
+(*
+[DEFINE]
+[
+    [NAME]
+    FMOD_REVERB_FLAGS
+
+    [DESCRIPTION]
+    Values for the Flags member of the FMOD_REVERB_PROPERTIES structure.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_REVERB_PROPERTIES
+]
+*)
+const
+  FMOD_REVERB_FLAGS_DECAYTIMESCALE        = $00000001; (* 'EnvSize' affects reverberation decay time *)
+  FMOD_REVERB_FLAGS_REFLECTIONSSCALE      = $00000002; (* 'EnvSize' affects reflection level *)
+  FMOD_REVERB_FLAGS_REFLECTIONSDELAYSCALE = $00000004; (* 'EnvSize' affects initial reflection delay time *)
+  FMOD_REVERB_FLAGS_REVERBSCALE           = $00000008; (* 'EnvSize' affects reflections level *)
+  FMOD_REVERB_FLAGS_REVERBDELAYSCALE      = $00000010; (* 'EnvSize' affects late reverberation delay time *)
+  FMOD_REVERB_FLAGS_DECAYHFLIMIT          = $00000020; (* AirAbsorptionHF affects DecayHFRatio *)
+  FMOD_REVERB_FLAGS_ECHOTIMESCALE         = $00000040; (* 'EnvSize' affects echo time *)
+  FMOD_REVERB_FLAGS_MODULATIONTIMESCALE   = $00000080; (* 'EnvSize' affects modulation time *)
+  FMOD_REVERB_FLAGS_DEFAULT               = (FMOD_REVERB_FLAGS_DECAYTIMESCALE or
+                                                FMOD_REVERB_FLAGS_REFLECTIONSSCALE or
+                                                FMOD_REVERB_FLAGS_REFLECTIONSDELAYSCALE or
+                                                FMOD_REVERB_FLAGS_REVERBSCALE or
+                                                FMOD_REVERB_FLAGS_REVERBDELAYSCALE or
+                                                FMOD_REVERB_FLAGS_DECAYHFLIMIT);
+(* [DEFINE_END] *)
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Structure defining the properties for a reverb source, related to a FMOD channel.
+
+    For more indepth descriptions of the reverb properties under win32, please see the EAX3
+    documentation at http://developer.creative.com/ under the 'downloads' section.
+    If they do not have the EAX3 documentation, then most information can be attained from
+    the EAX2 documentation, as EAX3 only adds some more parameters and functionality on top of
+    EAX2.
+
+    Note the default reverb properties are the same as the FMOD_PRESET_GENERIC preset.
+    Note that integer values that typically range from -10,000 to 1000 are represented in
+    decibels, and are of a logarithmic scale, not linear, wheras float values are typically linear.
+    PORTABILITY: Each member has the platform it supports in braces ie (win32/xbox).
+    Some reverb parameters are only supported in win32 and some only on xbox. If all parameters are set then
+    the reverb should product a similar effect on either platform.
+    Linux and FMODCE do not support the reverb api.
+
+    The numerical values listed below are the maximum, minimum and default values for each variable respectively.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_Channel_SetReverbProperties
+    FMOD_Channel_GetReverbProperties
+    FMOD_REVERB_CHANNELFLAGS
+]
+*)
+type
+  FMOD_REVERB_CHANNELPROPERTIES = record  (* MIN     MAX    DEFAULT *)
+    Direct: Integer;                      (* -10000, 1000,  0,    direct path level (at low and mid frequencies) (win32/xbox) *)
+    DirectHF: Integer;                    (* -10000, 0,     0,    relative direct path level at high frequencies (win32/xbox) *)
+    Room: Integer;                        (* -10000, 1000,  0,    room effect level (at low and mid frequencies) (win32/xbox) *)
+    RoomHF: Integer;                      (* -10000, 0,     0,    relative room effect level at high frequencies (win32/xbox) *)
+    Obstruction: Integer;                 (* -10000, 0,     0,    main obstruction control (attenuation at high frequencies)  (win32/xbox) *)
+    ObstructionLFRatio: Single;           (* 0.0,    1.0,   0.0,  obstruction low-frequency level re. main control (win32/xbox) *)
+    Occlusion: Integer;                   (* -10000, 0,     0,    main occlusion control (attenuation at high frequencies) (win32/xbox) *)
+    OcclusionLFRatio: Single;             (* 0.0,    1.0,   0.25, occlusion low-frequency level re. main control (win32/xbox) *)
+    OcclusionRoomRatio: Single;           (* 0.0,    10.0,  1.5,  relative occlusion control for room effect (win32) *)
+    OcclusionDirectRatio: Single;         (* 0.0,    10.0,  1.0,  relative occlusion control for direct path (win32) *)
+    Exclusion: Integer;                   (* -10000, 0,     0,    main exlusion control (attenuation at high frequencies) (win32) *)
+    ExclusionLFRatio: Single;             (* 0.0,    1.0,   1.0,  exclusion low-frequency level re. main control (win32) *)
+    OutsideVolumeHF: Integer;             (* -10000, 0,     0,    outside sound cone level at high frequencies (win32) *)
+    DopplerFactor: Single;                (* 0.0,    10.0,  0.0,  like DS3D flDopplerFactor but per source (win32) *)
+    RolloffFactor: Single;                (* 0.0,    10.0,  0.0,  like DS3D flRolloffFactor but per source (win32) *)
+    RoomRolloffFactor: Single;            (* 0.0,    10.0,  0.0,  like DS3D flRolloffFactor but for room effect (win32/xbox) *)
+    AirAbsorptionFactor: Single;          (* 0.0,    10.0,  1.0,  multiplies AirAbsorptionHF member of FMOD_REVERB_PROPERTIES (win32) *)
+    Flags: Integer;                       (* FMOD_REVERB_CHANNELFLAGS - modifies the behavior of properties (win32) *)
+  end;
+
+
+(*
+[DEFINE]
+[
+    [NAME]
+    FMOD_REVERB_CHANNELFLAGS
+
+    [DESCRIPTION]
+    Values for the Flags member of the FMOD_REVERB_CHANNELPROPERTIES structure.
+
+    [REMARKS]
+    For EAX4 support with multiple reverb environments, set FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT0,
+    FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT1 or/and FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT2 in the flags member
+    of FMOD_REVERB_CHANNELPROPERTIES to specify which environment instance(s) to target.
+    Only up to 2 environments to target can be specified at once. Specifying three will result in an error.
+    If the sound card does not support EAX4, the environment flag is ignored.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_REVERB_CHANNELPROPERTIES
+]
+*)
+const
+  FMOD_REVERB_CHANNELFLAGS_DIRECTHFAUTO  = $00000001; (* Automatic setting of 'Direct'  due to distance from listener *)
+  FMOD_REVERB_CHANNELFLAGS_ROOMAUTO      = $00000002; (* Automatic setting of 'Room'  due to distance from listener *)
+  FMOD_REVERB_CHANNELFLAGS_ROOMHFAUTO    = $00000004; (* Automatic setting of 'RoomHF' due to distance from listener *)
+  FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT0  = $00000008; (* EAX4 only. Specify channel to target reverb instance 0. *)
+  FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT1  = $00000010; (* EAX4 only. Specify channel to target reverb instance 1. *)
+  FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT2  = $00000020; (* EAX4 only. Specify channel to target reverb instance 2. *)
+  FMOD_REVERB_CHANNELFLAGS_DEFAULT       = (FMOD_REVERB_CHANNELFLAGS_DIRECTHFAUTO or
+                                                FMOD_REVERB_CHANNELFLAGS_ROOMAUTO or
+                                                FMOD_REVERB_CHANNELFLAGS_ROOMHFAUTO or
+                                                FMOD_REVERB_CHANNELFLAGS_ENVIRONMENT0);
+(* [DEFINE_END] *)
+
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Settings for advanced features like configuring memory and cpu usage for the FMOD_CREATECOMPRESSEDSAMPLE feature.
+
+    [REMARKS]
+    maxMPEGcodecs / maxADPCMcodecs / maxXMAcodecs will determine the maximum cpu usage of playing realtime samples.  Use this to lower potential excess cpu usage and also control memory usage.<br>
+
+    Memory will be allocated for codecs 'up front' (during System::init) if these values are specified as non zero.  If any are zero, it allocates memory for the codec whenever a file of the type in question is loaded.  So if maxMPEGcodecs is 0 for example, it will allocate memory for the mpeg codecs the first time an mp3 is loaded or an mp3 based .FSB file is loaded.
+
+    Due to inefficient encoding techniques on certain .wav based ADPCM files, FMOD can can need an extra 29720 bytes per codec.  This means for lowest memory consumption.  Use FSB as it uses an optimal/small ADPCM block size.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_SetAdvancedSettings
+    FMOD_System_GetAdvancedSettings
+    FMOD_System_Init
+    FMOD_MODE
+]
+*)
+type
+  FMOD_ADVANCEDSETTINGS = record
+    cbsize: Integer;                (* Size of structure.  Use sizeof(FMOD_ADVANCEDSETTINGS) *)
+    maxMPEGcodecs: Integer;         (* For use with FMOD_CREATECOMPRESSEDSAMPLE only.  Mpeg  codecs consume 29.076 bytes per instance and this number will determine how many mpeg channels can be played simultaneously.  Default = 16. *)
+    maxADPCMcodecs: Integer;        (* For use with FMOD_CREATECOMPRESSEDSAMPLE only.  ADPCM codecs consume ?? bytes per instance and this number will determine how many ADPCM channels can be played simultaneously.  Default = 32. *)
+    maxXMAcodecs: Integer;          (* For use with FMOD_CREATECOMPRESSEDSAMPLE only.  XMA   codecs consume ?? bytes per instance and this number will determine how many XMA channels can be played simultaneously.  Default = 32.  *)
+    ASIONumChannels: Integer;       (* [in/out] *)
+    ASIOChannelList: Pointer;       (* [in/out] *)
+    max3DReverbDSPs: Integer;       (* [in/out] The max number of 3d reverb DSP's in the system. *)
+    HRTFMinAngle: Single;           (* [in/out] For use with FMOD_INIT_SOFTWARE_HRTF.  The angle (0-360) of a 3D sound from the listener's forward vector at which the HRTF function begins to have an effect.  Default = 180.0. *)
+    HRTFMaxAngle: Single;           (* [in/out] For use with FMOD_INIT_SOFTWARE_HRTF.  The angle (0-360) of a 3D sound from the listener's forward vector at which the HRTF function begins to have maximum effect.  Default = 360.0.  *)
+    HRTFFreq: Single;               (* [in/out] For use with FMOD_INIT_SOFTWARE_HRTF.  The cutoff frequency of the HRTF's lowpass filter function when at maximum effect. (i.e. at HRTFMaxAngle).  Default = 4000.0. *)
+  end;
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Special channel index values for FMOD functions.
+
+    [REMARKS]
+    To get 'all' of the channels, use System::getChannelGroup.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    System::playSound
+    System::playDSP
+    System::getChannel
+]
+*)
+const
+  FMOD_CHANNEL_FREE           = -1;      (* Value to play on any free channel. *)
+  FMOD_CHANNEL_REUSE          = -2;      (* For a channel index, re-use the channel handle that was passed in. *)
+
+
+(* =============================================================================================== *)
+(* FMOD Ex - codec development header file. Copyright (c), Firelight Technologies Pty, Ltd. 2004.  *)
+(*                                                                                                 *)
+(* Use this header if you are wanting to develop your own file format plugin to use with           *)
+(* FMOD's codec system.  With this header you can make your own fileformat plugin that FMOD        *)
+(* can register and use.  See the documentation and examples on how to make a working plugin.      *)
+(*                                                                                                 *)
+(* =============================================================================================== *)
+
+type
+  FMOD_CODEC_STATE = ^tagFMOD_CODEC_STATE;
+
+
+(*
+    Codec callbacks
+*)
+(* type- Cannot use the type keyword here because the declaration of FMOD_CODEC_STATE requires tagFMOD_CODEC_STATE in the same type block *)
+  FMOD_CODEC_OPENCALLBACK        = function (codec: FMOD_CODEC_STATE; usermode: Cardinal; userfrequency: Integer; userformat: FMOD_SOUND_FORMAT): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF} {$ENDIF}
+  FMOD_CODEC_CLOSECALLBACK       = function (codec: FMOD_CODEC_STATE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_CODEC_READCALLBACK        = function (codec: FMOD_CODEC_STATE; buffer: Pointer; sizebytes: Cardinal; var bytesread: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_CODEC_GETLENGTHCALLBACK   = function (codec: FMOD_CODEC_STATE; length: Cardinal; lengthtype: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_CODEC_SETPOSITIONCALLBACK = function (codec: FMOD_CODEC_STATE; subsound: Integer; pcm: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_CODEC_GETPOSITIONCALLBACK = function (codec: FMOD_CODEC_STATE; var subsound: Integer; var position: Cardinal; postype: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_CODEC_SOUNDCREATECALLBACK = function (codec: FMOD_CODEC_STATE; subsound: Integer; sound: FMOD_SOUND): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_CODEC_METADATACALLBACK    = function (codec: FMOD_CODEC_STATE; tagtype: FMOD_TAGTYPE; name: PChar; data: Pointer; datalen: Cardinal; datatype: FMOD_TAGDATATYPE; unique: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    When creating a codec, declare one of these and provide the relevant callbacks and name for FMOD to use when it opens and reads a file.
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+]
+*)
+(* type- Cannot use the type keyword here because the declaration of FMOD_CODEC_STATE requires tagFMOD_CODEC_STATE in the same type block *)
+  FMOD_CODEC_DESCRIPTION = record
+    name: PChar;                                    (* [in] Name of the codec. *)
+    version: Cardinal;                              (* [in] Plugin writer's version number. *)
+    timeunits: Integer;                             (* [in] When setposition codec is called, only these time formats will be passed to the codec. Use bitwise OR to accumulate different types. *)
+    open: FMOD_CODEC_OPENCALLBACK;                  (* [in] Open callback for the codec for when FMOD tries to open a sound using this codec. *)
+    close: FMOD_CODEC_CLOSECALLBACK;                (* [in] Close callback for the codec for when FMOD tries to close a sound using this codec.  *)
+    read: FMOD_CODEC_READCALLBACK;                  (* [in] Read callback for the codec for when FMOD tries to read some data from the file to the destination format (specified in the open callback). *)
+    getlength: FMOD_CODEC_GETLENGTHCALLBACK;        (* [in] Callback to return the length of the song in whatever format required when Sound::getLength is called. *)
+    setposition: FMOD_CODEC_SETPOSITIONCALLBACK;    (* [in] Seek callback for the codec for when FMOD tries to seek within the file with Sound::setTime or Sound::setPosition. *)
+    getposition: FMOD_CODEC_GETPOSITIONCALLBACK;    (* [in] Tell callback for the codec for when FMOD tries to get the current position within the with Sound::getTime or Sound::getPosition. *)
+    soundcreate: FMOD_CODEC_SOUNDCREATECALLBACK;    (* [in] Sound creation callback for the codec when FMOD finishes creating the sound.  (So the codec can set more parameters for the related created sound, ie loop points/mode or 3D attributes etc). *)
+  end;
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Set these values marked 'in' to tell fmod what sort of sound to create.
+    The format, channels and frequency tell FMOD what sort of hardware buffer to create when you initialize your code.  So if you wrote an MP3 codec that decoded to stereo 16bit integer PCM, you would specify FMOD_SOUND_FORMAT_PCM16, and channels would be equal to 2.
+    Members marked as 'out' are set by fmod.  Do not modify these.  Simply specify 0 for these values when declaring the structure, FMOD will fill in the values for you after creation with the correct function pointers.
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    An FMOD file might be from disk, memory or network, however the file may be opened by the user.
+
+    'numsubsounds' should be 0 if the file is a normal single sound stream or sound.  Examples of this would be .WAV, .WMA, .MP3, .AIFF.
+    'numsubsounds' should be 1+ if the file is a container format, and does not contain wav data itself.  Examples of these types would be CDDA (multiple CD tracks), FSB (contains multiple sounds), MIDI/MOD/S3M/XM/IT (contain instruments).
+    The arrays of format, channel, frequency, length and blockalign should point to arrays of information based on how many subsounds are in the format.  If the number of subsounds is 0 then it should point to 1 of each attribute, the same as if the number of subsounds was 1.  If subsounds was 100 for example, each pointer should point to an array of 100 of each attribute.
+    When a sound has 1 or more subsounds, you must play the individual sounds specified by first obtaining the subsound with FMOD_Sound_GetSubSound.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_SOUND_FORMAT
+    FMOD_FILE_READCALLBACK
+    FMOD_FILE_SEEKCALLBACK
+    FMOD_CODEC_METADATACALLBACK
+    FMOD_Sound_GetSubSound
+    FMOD_Sound_GetNumSubSounds
+]
+*)
+(* type- Cannot use the type keyword here because the declaration of FMOD_CODEC_STATE requires tagFMOD_CODEC_STATE in the same type block *)
+  FMOD_CODEC_WAVEFORMAT = record
+    name:  array [0..255] of Char;    (* [in] Name of sound. *)
+    format: FMOD_SOUND_FORMAT;        (* [in] format for codec output, ie FMOD_SOUND_FORMAT_PCM8, FMOD_SOUND_FORMAT_PCM16.*)
+    channels: Integer;                (* [in] number of channels used by codec, ie mono = 1, stereo = 2. *)
+    frequency: Integer;               (* [in] default frequency in hz of the codec, ie 44100. *)
+    lengthpcm: Cardinal;              (* [in] length in decompressed, PCM samples of the file, ie length in seconds * frequency. *)
+    blockalign: Integer;              (* [in] blockalign in decompressed, PCM samples of the optimal decode chunk size for this format.  The codec read callback will be called in multiples of this value. *)
+    loopstart: Integer;               (* [in] loopstart in decompressed, PCM samples of file. *)
+    loopend: Integer;                 (* [in] loopend in decompressed, PCM samples of file. *)
+    mode: Cardinal;                   (* [in] mode to determine whether the sound should by default load as looping, non looping, 2d or 3d. *)
+    channelmask: Cardinal;            (* [in] Microsoft speaker channel mask, as defined for WAVEFORMATEXTENSIBLE and is found in ksmedia.h.  Leave at 0 to play in natural speaker order. *)
+  end;
+  PFMOD_CODEC_WAVEFORMAT = ^FMOD_CODEC_WAVEFORMAT;
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Codec plugin structure that is passed into each callback.
+
+    Set these numsubsounds and waveformat members when called in FMOD_CODEC_OPENCALLBACK to tell fmod what sort of sound to create.
+
+    The format, channels and frequency tell FMOD what sort of hardware buffer to create when you initialize your code.  So if you wrote an MP3 codec that decoded to stereo 16bit integer PCM, you would specify FMOD_SOUND_FORMAT_PCM16, and channels would be equal to 2.
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+
+    An FMOD file might be from disk, memory or network, however the file may be opened by the user.
+
+    'numsubsounds' should be 0 if the file is a normal single sound stream or sound.  Examples of this would be .WAV, .WMA, .MP3, .AIFF.
+    'numsubsounds' should be 1+ if the file is a container format, and does not contain wav data itself.  Examples of these types would be CDDA (multiple CD tracks), FSB (contains multiple sounds), MIDI/MOD/S3M/XM/IT (contain instruments).
+    The arrays of format, channel, frequency, length and blockalign should point to arrays of information based on how many subsounds are in the format.  If the number of subsounds is 0 then it should point to 1 of each attribute, the same as if the number of subsounds was 1.  If subsounds was 100 for example, each pointer should point to an array of 100 of each attribute.
+    When a sound has 1 or more subsounds, you must play the individual sounds specified by first obtaining the subsound with FMOD_Sound_GetSubSound.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_SOUND_FORMAT
+    FMOD_FILE_READCALLBACK
+    FMOD_FILE_SEEKCALLBACK
+    FMOD_CODEC_METADATACALLBACK
+    FMOD_Sound_GetSubSound
+    FMOD_Sound_GetNumSubSounds
+]
+*)
+(* type- Cannot use the type keyword here because the declaration of FMOD_CODEC_STATE requires tagFMOD_CODEC_STATE in the same type block *)
+  tagFMOD_CODEC_STATE = record
+    numsubsounds: Integer;                  (* [in] Number of 'subsounds' in this sound.  Anything other than 0 makes it a 'container' format (ie CDDA/FSB/MIDI/MOD/etc which contain 1 or more subsounds).  For most normal, single sound codec such as WAV/AIFF/MP3, this should be 0 as they are not a container for subsounds, they are the sound itself. *)
+    waveformat: PFMOD_CODEC_WAVEFORMAT;     (* [in] Pointer to an array of format structures containing information about each sample.  The number of entries here must equal the number of subsounds defined in the subsound parameter. If numsubsounds = 0 then there should be 1 instance of this structure. *)
+    plugindata: Pointer;                    (* [in/out] User created data the codec plugin writer wants to attach to this object. *)
+
+    filehandle: Pointer;                    (* [out] - Do not modify.  This will return an internal FMOD file handle to use with the callbacks provided.  *)
+    filesize: Cardinal;                     (* [out] - Do not modify.  This will return an internal FMOD file handle to use with the callbacks provided.  *)
+    fileread: FMOD_FILE_READCALLBACK;       (* [out] - Do not modify.  This will return a callable FMOD file function to use from codec. *)
+    fileseek: FMOD_FILE_SEEKCALLBACK;       (* [out] - Do not modify.  This will return a callable FMOD file function to use from codec.  *)
+    metadata: FMOD_CODEC_METADATACALLBACK;  (* [out] - Do not modify.  This will return a callable FMOD metadata function to use from codec.  *)
+  end;
+
+
+(* ========================================================================================== *)
+(* FMOD Ex - DSP header file. Copyright (c), Firelight Technologies Pty, Ltd. 2004-2005.      *)
+(*                                                                                            *)
+(* Use this header if you are interested in delving deeper into the FMOD software mixing /    *)
+(* DSP engine.  In this header you can find parameter structures for FMOD system reigstered   *)
+(* DSP effects and generators.                                                                *)
+(* Also use this header if you are wanting to develop your own DSP plugin to use with FMOD's  *)
+(* dsp system.  With this header you can make your own DSP plugin that FMOD can               *)
+(* register and use.  See the documentation and examples on how to make a working plugin.     *)
+(*                                                                                            *)
+(* ========================================================================================== *)
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    DSP plugin structure that is passed into each callback.
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable
+
+    [SEE_ALSO]
+    FMOD_DSP_DESCRIPTION
+]
+*)
+  tagFMOD_DSP_STATE = record
+    instance: FMOD_DSP;     (* [out] Handle to the DSP hand the user created.  Not to be modified.  C++ users cast to FMOD::DSP to use.  *)
+    plugindata: Pointer;    (* [in] Plugin writer created data the output author wants to attach to this object. *)
+  end;
+
+type
+  FMOD_DSP_STATE = ^tagFMOD_DSP_STATE;
+
+(*
+    DSP callbacks
+*)
+(* type- Cannot use the type keyword here because the declaration of FMOD_CODEC_STATE requires tagFMOD_CODEC_STATE in the same type block *)
+  FMOD_DSP_CREATECALLBACK       = function (dsp: FMOD_DSP_STATE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_RELEASECALLBACK      = function (dsp: FMOD_DSP_STATE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_RESETCALLBACK        = function (dsp: FMOD_DSP_STATE): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_READCALLBACK         = function (dsp: FMOD_DSP_STATE; var inbuffer: Single; var outbuffer: Single; length: Cardinal; inchannels: Integer; outchannels: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_SETPOSITIONCALLBACK  = function (dsp: FMOD_DSP_STATE; position: Cardinal): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_SETPARAMCALLBACK     = function (dsp: FMOD_DSP_STATE; index: Integer; value: Single): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_GETPARAMCALLBACK     = function (dsp: FMOD_DSP_STATE; index: Integer; var value: Single; valuestr: PString): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+  FMOD_DSP_DIALOGCALLBACK       = function (dsp: FMOD_DSP_STATE; hwnd: Pointer; show: Integer): FMOD_RESULT; {$IFDEF WIN32} stdcall; {$ELSE} cdecl; {$ENDIF}
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    These definitions can be used for creating FMOD defined special effects or DSP units.
+
+    [REMARKS]
+    To get them to be active, first create the unit, then add it somewhere into the DSP network, either at the front of the network near the soundcard unit to affect the global output (by using FMOD_System_GetDSPHead, or on a single channel (using FMOD_Channel_GetDSPHead).
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_CreateDSPByType
+]
+*)
+type
+  FMOD_DSP_TYPE =
+  (
+    FMOD_DSP_TYPE_UNKNOWN,            (* This unit was created via a non FMOD plugin so has an unknown purpose. *)
+    FMOD_DSP_TYPE_MIXER,              (* This unit does nothing but take inputs and mix them together then feed the result to the soundcard unit *)
+    FMOD_DSP_TYPE_OSCILLATOR,         (* This unit generates sine/square/saw/triangle or noise tones. *)
+    FMOD_DSP_TYPE_LOWPASS,            (* This unit filters sound using a high quality, resonant lowpass filter algorithm but consumes more CPU time. *)
+    FMOD_DSP_TYPE_ITLOWPASS,          (* This unit filters sound using a resonant lowpass filter algorithm that is used in Impulse Tracker. *)
+    FMOD_DSP_TYPE_HIGHPASS,           (* This unit filters sound using a resonant highpass filter algorithm. *)
+    FMOD_DSP_TYPE_ECHO,               (* This unit produces an echo on the sound and fades out at the desired rate. *)
+    FMOD_DSP_TYPE_FLANGE,             (* This unit produces a flange effect on the sound. *)
+    FMOD_DSP_TYPE_DISTORTION,         (* This unit distorts the sound. *)
+    FMOD_DSP_TYPE_NORMALIZE,          (* This unit normalizes or amplifies the sound to a certain level. *)
+    FMOD_DSP_TYPE_PARAMEQ,            (* This unit attenuates or amplifies a selected frequency range. *)
+    FMOD_DSP_TYPE_PITCHSHIFT,         (* This unit bends the pitch of a sound without changing the speed of playback. *)
+    FMOD_DSP_TYPE_CHORUS,             (* This unit produces a chorus effect on the sound. *)
+    FMOD_DSP_TYPE_REVERB,             (* This unit produces a reverb effect on the sound. *)
+    FMOD_DSP_TYPE_VSTPLUGIN,          (* This unit allows the use of Steinberg VST plugins *)
+    FMOD_DSP_TYPE_WINAMPPLUGIN,       (* This unit allows the use of Nullsoft Winamp plugins *)
+    FMOD_DSP_TYPE_ITECHO,             (* This unit produces an echo on the sound and fades out at the desired rate as is used in Impulse Tracker. *)
+    FMOD_DSP_TYPE_COMPRESSOR,         (* This unit implements dynamic compression (linked multichannel, wideband) *)
+    FMOD_DSP_TYPE_SFXREVERB,          (* This unit implements SFX reverb *)
+    FMOD_DSP_TYPE_LOWPASS_SIMPLE      (* This unit filters sound using a simple lowpass with no resonance, but has flexible cutoff and is fast. *)
+  );
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    The step parameter tells the gui or application that the parameter has a certain granularity.
+    For example in the example of cutoff frequency with a range from 100.0 to 22050.0 you might only want the selection to be in 10hz increments.  For this you would simply use 10.0 as the step value.
+    For a boolean, you can use min = 0.0, max = 1.0, step = 1.0.  This way the only possible values are 0.0 and 1.0.
+    Some applications may detect min = 0.0, max = 1.0, step = 1.0 and replace a graphical slider bar with a checkbox instead.
+    A step value of 1.0 would simulate integer values only.
+    A step value of 0.0 would mean the full floating point range is accessable.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_CreateDSP
+    FMOD_DSP_SetParameter
+]
+*)
+type
+  FMOD_DSP_PARAMETERDESC = record
+    min: Single;                     (* [in] Minimum value of the parameter (ie 100.0). *)
+    max: Single;                     (* [in] Maximum value of the parameter (ie 22050.0). *)
+    defaultval: Single;              (* [in] Default value of parameter. *)
+    name:  array [0..15] of Char;    (* [in] Name of the parameter to be displayed (ie "Cutoff frequency"). *)
+    plabel:  array [0..15] of Char;   (* [in] Short string to be put next to value to denote the unit type (ie "hz"). *)
+    description: PChar;              (* [in] Description of the parameter to be displayed as a help item / tooltip for this parameter. *)
+  end;
+  PFMOD_DSP_PARAMETERDESC = ^FMOD_DSP_PARAMETERDESC;
+
+
+(*
+[STRUCTURE]
+[
+    [DESCRIPTION]
+    Strcture to define the parameters for a DSP unit.
+
+    [REMARKS]
+    Members marked with [in] mean the variable can be written to.  The user can set the value.
+    Members marked with [out] mean the variable is modified by FMOD and is for reading purposes only.  Do not change this value.
+
+    There are 2 different ways to change a parameter in this architecture.
+    One is to use FMOD_DSP_SetParameter / FMOD_DSP_GetParameter.  This is platform independant and is dynamic, so new unknown plugins can have their parameters enumerated and used.
+    The other is to use FMOD_DSP_ShowConfigDialog.  This is platform specific and requires a GUI, and will display a dialog box to configure the plugin.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_System_CreateDSP
+    FMOD_System_GetDSP
+]
+*)
+type
+  FMOD_DSP_DESCRIPTION = record
+    name: array [0..31] of Char;                    (* [in] Name of the unit to be displayed in the network. *)
+    version: Cardinal;                              (* [in] Plugin writer's version number. *)
+    channels: Integer;                              (* [in] Number of channels.  Use 0 to process whatever number of channels is currently in the network.  >0 would be mostly used if the unit is a fixed format generator and not a filter. *)
+    create: FMOD_DSP_CREATECALLBACK;                (* [in] Create callback.  This is called when DSP unit is created.  Can be null. *)
+    release: FMOD_DSP_RELEASECALLBACK;              (* [in] Release callback.  This is called just before the unit is freed so the user can do any cleanup needed for the unit.  Can be null. *)
+    reset: FMOD_DSP_RESETCALLBACK;                  (* [in] Reset callback.  This is called by the user to reset any history buffers that may need resetting for a filter, when it is to be used or re-used for the first time to its initial clean state.  Use to avoid clicks or artifacts. *)
+    read: FMOD_DSP_READCALLBACK;                    (* [in] Read callback.  Processing is done here.  Can be null. *)
+    setposition: FMOD_DSP_SETPOSITIONCALLBACK;      (* [in] Set position callback.  This is called if the unit wants to update its position info but not process data, or reset a cursor position internally if it is reading data from a certain source.  Can be null. *)
+    numparameters: Integer;                         (* [in] Number of parameters used in this filter.  The user finds this with FMOD_DSP_GetNumParameters *)
+    paramdesc: PFMOD_DSP_PARAMETERDESC;             (* [in] Variable number of parameter structures. *)
+    setparameter: FMOD_DSP_SETPARAMCALLBACK;        (* [in] This is called when the user calls FMOD_DSP_SetParameter.  Can be null. *)
+    getparameter: FMOD_DSP_GETPARAMCALLBACK;        (* [in] This is called when the user calls FMOD_DSP_GetParameter.  Can be null. *)
+    config: FMOD_DSP_DIALOGCALLBACK;                (* [in] This is called when the user calls FMOD_DSP_ShowConfigDialog.  Can be used to display a dialog to configure the filter.  Can be null. *)
+    configwidth: Integer;                           (* [in] Width of config dialog graphic if there is one.  0 otherwise. *)
+    configheight: Integer;                          (* [in] Height of config dialog graphic if there is one.  0 otherwise. *)
+  end;
+
+
+(*
+    ==============================================================================================================
+
+    FMOD built in effect parameters.
+    Use FMOD_DSP_SetParameter with these enums for the 'index' parameter.
+
+    ==============================================================================================================
+*)
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_OSCILLATOR filter.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_OSCILLATOR =
+  (
+    FMOD_DSP_OSCILLATOR_TYPE,   (* Waveform type.  0 = sine.  1 = square. 2 = sawup. 3 = sawdown. 4 = triangle. 5 = noise.  *)
+    FMOD_DSP_OSCILLATOR_RATE    (* Frequency of the sinewave in hz.  1.0 to 22000.0.  Default = 220.0. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_LOWPASS filter.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_LOWPASS =
+  (
+    FMOD_DSP_LOWPASS_CUTOFF,    (* Lowpass cutoff frequency in hz.   1.0 to 22000.0.  Default = 5000.0. *)
+    FMOD_DSP_LOWPASS_RESONANCE  (* Lowpass resonance Q value. 1.0 to 10.0.  Default = 1.0. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_LOWPASS2 filter.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_LOWPASS2 =
+  (
+    FMOD_DSP_LOWPASS2_CUTOFF,    (* Lowpass cutoff frequency in hz.  1.0 to 22000.0.  Default = 5000.0/ *)
+    FMOD_DSP_LOWPASS2_RESONANCE  (* Lowpass resonance Q value.  0.0 to 127.0.  Default = 1.0. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_HIGHPASS filter.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_HIGHPASS =
+  (
+    FMOD_DSP_HIGHPASS_CUTOFF,    (* Highpass cutoff frequency in hz.  10.0 to output 22000.0.  Default = 5000.0. *)
+    FMOD_DSP_HIGHPASS_RESONANCE  (* Highpass resonance Q value.  1.0 to 10.0.  Default = 1.0. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_ECHO filter.
+
+    [REMARKS]
+    Note.  Every time the delay is changed, the plugin re-allocates the echo buffer.  This means the echo will dissapear at that time while it refills its new buffer.
+    Larger echo delays result in larger amounts of memory allocated.
+
+    '<i>maxchannels</i>' also dictates the amount of memory allocated.  By default, the maxchannels value is 0.  If FMOD is set to stereo, the echo unit will allocate enough memory for 2 channels.  If it is 5.1, it will allocate enough memory for a 6 channel echo, etc.
+    If the echo effect is only ever applied to the global mix (ie it was added with FMOD_System_AddDSP), then 0 is the value to set as it will be enough to handle all speaker modes.
+    When the echo is added to a channel (ie FMOD_Channel_AddDSP) then the channel count that comes in could be anything from 1 to 8 possibly.  It is only in this case where you might want to increase the channel count above the output's channel count.
+    If a channel echo is set to a lower number than the sound's channel count that is coming in, it will not echo the sound.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_ECHO =
+  (
+    FMOD_DSP_ECHO_DELAY,       (* Echo delay in ms.  10  to 5000.  Default = 500. *)
+    FMOD_DSP_ECHO_DECAYRATIO,  (* Echo decay per delay.  0 to 1.  1.0 = No decay, 0.0 = total decay.  Default = 0.5. *)
+    FMOD_DSP_ECHO_FEEDBACK,    (* 0 = Unit is a simple 1 line delay.  1 = Unit echos what has already been processed by echo. Default = 1. *)
+    FMOD_DSP_ECHO_MAXCHANNELS  (* Maximum channels supported.  0 to 16.  0 = same as fmod's default output polyphony, 1 = mono, 2 = stereo etc.  See remarks for more.  Default = 0.  It is suggested to leave at 0! *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_FLANGE filter.
+
+    [REMARKS]
+    Flange is an effect where the signal is played twice at the same time, and one copy slides back and forth creating a whooshing or flanging effect.
+    As there are 2 copies of the same signal, by default each signal is given 50% mix, so that the total is not louder than the original unaffected signal.
+
+    Flange depth is a percentage of a 10ms shift from the original signal.  Anything above 10ms is not considered flange because to the ear it begins to 'echo' so 10ms is the highest value possible.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_FLANGE =
+  (
+    FMOD_DSP_FLANGE_DRYMIX,      (* Volume of original signal to pass to output.  0.0 to 1.0. Default = 0.45. *)
+    FMOD_DSP_FLANGE_WETMIX,      (* Volume of flange signal to pass to output.  0.0 to 1.0. Default = 0.55. *)
+    FMOD_DSP_FLANGE_DEPTH,       (* Flange depth.  0.01 to 1.0.  Default = 1.0. *)
+    FMOD_DSP_FLANGE_RATE         (* Flange speed in hz.  0.0 to 20.0.  Default = 0.1. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_DISTORTION filter.
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_DISTORTION =
+  (
+    FMOD_DSP_DISTORTION_LEVEL    (* Distortion value.  0.0 to 1.0.  Default = 0.5. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_NORMALIZE filter.
+
+    [REMARKS]
+    Normalize amplifies the sound based on the maximum peaks within the signal.
+    For example if the maximum peaks in the signal were 50% of the bandwidth, it would scale the whole sound by 2.
+    The lower threshold value makes the normalizer ignores peaks below a certain point, to avoid over-amplification if a loud signal suddenly came in, and also to avoid amplifying to maximum things like background hiss.
+
+    Because FMOD is a realtime audio processor, it doesn't have the luxury of knowing the peak for the whole sound (ie it can't see into the future), so it has to process data as it comes in.
+    To avoid very sudden changes in volume level based on small samples of new data, fmod fades towards the desired amplification which makes for smooth gain control.  The fadetime parameter can control this.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_NORMALIZE =
+  (
+    FMOD_DSP_NORMALIZE_FADETIME,    (* Time to ramp the silence to full in ms.  0.0 to 20000.0. Default = 5000.0. *)
+    FMOD_DSP_NORMALIZE_THRESHHOLD,  (* Lower volume range threshold to ignore.  0.0 to 1.0.  Default = 0.1.  Raise higher to stop amplification of very quiet signals. *)
+    FMOD_DSP_NORMALIZE_MAXAMP       (* Maximum amplification allowed.  1.0 to 100000.0.  Default = 20.0.  1.0 = no amplifaction, higher values allow more boost. *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_PARAMEQ filter.
+
+    [REMARKS]
+    Parametric EQ is a bandpass filter that attenuates or amplifies a selected frequency and its neighbouring frequencies.
+
+    To create a multi-band EQ create multiple FMOD_DSP_TYPE_PARAMEQ units and set each unit to different frequencies, for example 1000hz, 2000hz, 4000hz, 8000hz, 16000hz with a range of 1 octave each.
+
+    When a frequency has its gain set to 1.0, the sound will be unaffected and represents the original signal exactly.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_PARAMEQ =
+  (
+    FMOD_DSP_PARAMEQ_CENTER,     (* Frequency center.  20.0 to 22000.0.  Default = 8000.0. *)
+    FMOD_DSP_PARAMEQ_BANDWIDTH,  (* Octave range around the center frequency to filter.  0.2 to 5.0.  Default = 1.0. *)
+    FMOD_DSP_PARAMEQ_GAIN        (* Frequency Gain.  0.05 to 3.0.  Default = 1.0.  *)
+  );
+
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_PITCHSHIFT filter.
+
+    [REMARKS]
+    This pitch shifting unit can be used to change the pitch of a sound without speeding it up or slowing it down.
+    It can also be used for time stretching or scaling, for example if the pitch was doubled, and the frequency of the sound was halved, the pitch of the sound would sound correct but it would be twice as slow.
+
+    <b>Warning!</b> This filter is very computationally expensive!  Similar to a vocoder, it requires several overlapping FFT and IFFT's to produce smooth output, and can require around 440mhz for 1 stereo 48khz signal using the default settings.
+    Reducing the signal to mono will half the cpu usage, as will the overlap count.
+    Reducing this will lower audio quality, but what settings to use are largely dependant on the sound being played.  A noisy polyphonic signal will need higher overlap and fft size compared to a speaking voice for example.
+
+    This pitch shifter is based on the pitch shifter code at http://www.dspdimension.com, written by Stephan M. Bernsee.
+    The original code is COPYRIGHT 1999-2003 Stephan M. Bernsee <smb@dspdimension.com>.
+
+    '<i>maxchannels</i>' dictates the amount of memory allocated.  By default, the maxchannels value is 0.  If FMOD is set to stereo, the pitch shift unit will allocate enough memory for 2 channels.  If it is 5.1, it will allocate enough memory for a 6 channel pitch shift, etc.
+    If the pitch shift effect is only ever applied to the global mix (ie it was added with FMOD_System_AddDSP), then 0 is the value to set as it will be enough to handle all speaker modes.
+    When the pitch shift is added to a channel (ie FMOD_Channel_AddDSP) then the channel count that comes in could be anything from 1 to 8 possibly.  It is only in this case where you might want to increase the channel count above the output's channel count.
+    If a channel pitch shift is set to a lower number than the sound's channel count that is coming in, it will not pitch shift the sound.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_PITCHSHIFT =
+  (
+    FMOD_DSP_PITCHSHIFT_PITCH,       (* Pitch value.  0.5 to 2.0.  Default = 1.0. 0.5 = one octave down, 2.0 = one octave up.  1.0 does not change the pitch. *)
+    FMOD_DSP_PITCHSHIFT_FFTSIZE,     (* FFT window size.  256, 512, 1024, 2048, 4096.  Default = 1024.  Increase this to reduce 'smearing'.  This effect is a warbling sound similar to when an mp3 is encoded at very low bitrates. *)
+    FMOD_DSP_PITCHSHIFT_OVERLAP,     (* Window overlap.  1 to 32.  Default = 4.  Increase this to reduce 'tremolo' effect.  Increasing it by a factor of 2 doubles the CPU usage. *)
+    FMOD_DSP_PITCHSHIFT_MAXCHANNELS  (* Maximum channels supported.  0 to 16.  0 = same as fmod's default output polyphony, 1 = mono, 2 = stereo etc.  See remarks for more.  Default = 0.  It is suggested to leave at 0! *)
+  );
+
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_CHORUS filter.
+
+    [REMARKS]
+    Chrous is an effect where the sound is more 'spacious' due to 1 to 3 versions of the sound being played along side the original signal but with the pitch of each copy modulating on a sine wave.
+    This is a highly configurable chorus unit.  It supports 3 taps, small and large delay times and also feedback.
+    This unit also could be used to do a simple echo, or a flange effect.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_CHORUS =
+  (
+    FMOD_DSP_CHORUS_DRYMIX,   (* Volume of original signal to pass to output.  0.0 to 1.0. Default = 0.5. *)
+    FMOD_DSP_CHORUS_WETMIX1,  (* Volume of 1st chorus tap.  0.0 to 1.0.  Default = 0.5. *)
+    FMOD_DSP_CHORUS_WETMIX2,  (* Volume of 2nd chorus tap. This tap is 90 degrees out of phase of the first tap.  0.0 to 1.0.  Default = 0.5. *)
+    FMOD_DSP_CHORUS_WETMIX3,  (* Volume of 3rd chorus tap. This tap is 90 degrees out of phase of the second tap.  0.0 to 1.0.  Default = 0.5. *)
+    FMOD_DSP_CHORUS_DELAY,    (* Chorus delay in ms.  0.1 to 100.0.  Default = 40.0 ms. *)
+    FMOD_DSP_CHORUS_RATE,     (* Chorus modulation rate in hz.  0.0 to 20.0.  Default = 0.8 hz. *)
+    FMOD_DSP_CHORUS_DEPTH,    (* Chorus modulation depth.  0.0 to 1.0.  Default = 0.03. *)
+    FMOD_DSP_CHORUS_FEEDBACK  (* Chorus feedback.  Controls how much of the wet signal gets fed back into the chorus buffer.  0.0 to 1.0.  Default = 0.0. *)
+  );
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_REVERB filter.
+
+    [REMARKS]
+    Based on freeverb by Jezar at Dreampoint - http://www.dreampoint.co.uk.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_REVERB =
+  (
+    FMOD_DSP_REVERB_ROOMSIZE, (* Roomsize. 0.0 to 1.0.  Default = 0.5 *)
+    FMOD_DSP_REVERB_DAMP,     (* Damp.     0.0 to 1.0.  Default = 0.5 *)
+    FMOD_DSP_REVERB_WETMIX,   (* Wet mix.  0.0 to 1.0.  Default = 0.33 *)
+    FMOD_DSP_REVERB_DRYMIX,   (* Dry mix.  0.0 to 1.0.  Default = 0.66 *)
+    FMOD_DSP_REVERB_WIDTH,    (* Width.    0.0 to 1.0.  Default = 1.0 *)
+    FMOD_DSP_REVERB_MODE      (* Mode.     0 (normal), 1 (freeze).  Default = 0 *)
+  );
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_ITECHO filter.
+    This is effectively a software based echo filter that emulates the DirectX DMO echo effect.  Impulse tracker files can support this, and FMOD will produce the effect on ANY platform, not just those that support DirectX effects!
+
+    [REMARKS]
+    Note.  Every time the delay is changed, the plugin re-allocates the echo buffer.  This means the echo will dissapear at that time while it refills its new buffer.
+    Larger echo delays result in larger amounts of memory allocated.
+
+    As this is a stereo filter made mainly for IT playback, it is targeted for stereo signals.
+    With mono signals only the FMOD_DSP_ITECHO_LEFTDELAY is used.
+    For multichannel signals (>2) there will be no echo on those channels.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3
+
+    [SEE_ALSO]
+    FMOD_DSP_SetParameter
+    FMOD_DSP_GetParameter
+    FMOD_DSP_TYPE
+    FMOD_System_AddDSP
+]
+*)
+type
+  FMOD_DSP_ITECHO =
+  (
+    FMOD_DSP_ITECHO_WETDRYMIX,      (* Ratio of wet (processed) signal to dry (unprocessed) signal. Must be in the range from 0.0 through 100.0 (all wet). The default value is 50. *)
+    FMOD_DSP_ITECHO_FEEDBACK,       (* Percentage of output fed back into input, in the range from 0.0 through 100.0. The default value is 50. *)
+    FMOD_DSP_ITECHO_LEFTDELAY,      (* Delay for left channel, in milliseconds, in the range from 1.0 through 2000.0. The default value is 500 ms. *)
+    FMOD_DSP_ITECHO_RIGHTDELAY,     (* Delay for right channel, in milliseconds, in the range from 1.0 through 2000.0. The default value is 500 ms. *)
+    FMOD_DSP_ITECHO_PANDELAY        (* Value that specifies whether to swap left and right delays with each successive echo. The default value is zero, meaning no swap. Possible values are defined as 0.0 (equivalent to FALSE) and 1.0 (equivalent to TRUE).  CURRENTLY NOT SUPPORTED. *)
+  );
+
+
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_COMPRESSOR unit.
+    This is a simple linked multichannel software limiter that is uniform across the whole spectrum.<br>
+
+    [REMARKS]
+    The limiter is not guaranteed to catch every peak above the threshold level,
+    because it cannot apply gain reduction instantaneously - the time delay is
+    determined by the attack time. However setting the attack time too short will
+    distort the sound, so it is a compromise. High level peaks can be avoided by
+    using a short attack time - but not too short, and setting the threshold a few
+    decibels below the critical level.
+    <br>
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3, Wii
+
+    [SEE_ALSO]
+    DSP::SetParameter
+    DSP::GetParameter
+    FMOD_DSP_TYPE
+    System::addDSP
+]
+*)
+type
+  FMOD_DSP_COMPRESSOR =
+  (
+    FMOD_DSP_COMPRESSOR_THRESHOLD,  (* Threshold level (dB) in the range from -60 through 0. The default value is 0. *)
+    FMOD_DSP_COMPRESSOR_ATTACK,     (* Gain reduction attack time (milliseconds), in the range from 10 through 200. The default value is 50. *)
+    FMOD_DSP_COMPRESSOR_RELEASE,    (* Gain reduction release time (milliseconds), in the range from 20 through 1000. The default value is 50. *)
+    FMOD_DSP_COMPRESSOR_GAINMAKEUP  (* Make-up gain (dB) applied after limiting, in the range from 0 through 30. The default value is 0. *)
+  );
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_SFXREVERB unit.<br>
+
+    [REMARKS]
+    This is a high quality I3DL2 based reverb which improves greatly on FMOD_DSP_REVERB.<br>
+    On top of the I3DL2 property set, "Dry Level" is also included to allow the dry mix to be changed.<br>
+    <br>
+    Currently FMOD_DSP_SFXREVERB_REFLECTIONSLEVEL, FMOD_DSP_SFXREVERB_REFLECTIONSDELAY and FMOD_DSP_SFXREVERB_REVERBDELAY are not enabled but will come in future versions.<br>
+    <br>
+    These properties can be set with presets in FMOD_REVERB_PRESETS.
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3, Wii
+
+    [SEE_ALSO]
+    DSP::SetParameter
+    DSP::GetParameter
+    FMOD_DSP_TYPE
+    System::addDSP
+    FMOD_REVERB_PRESETS
+]
+*)
+type
+  FMOD_DSP_SFXREVERB =
+  (
+    FMOD_DSP_SFXREVERB_DRYLEVEL,            (* Dry Level      : Mix level of dry signal in output in mB.  Ranges from -10000.0 to 0.0.  Default is 0. *)
+    FMOD_DSP_SFXREVERB_ROOM,                (* Room           : Room effect level at low frequencies in mB.  Ranges from -10000.0 to 0.0.  Default is 0.0. *)
+    FMOD_DSP_SFXREVERB_ROOMHF,              (* Room HF        : Room effect high-frequency level re. low frequency level in mB.  Ranges from -10000.0 to 0.0.  Default is 0.0. *)
+    FMOD_DSP_SFXREVERB_ROOMROLLOFFFACTOR,   (* Room Rolloff   : Like DS3D flRolloffFactor but for room effect.  Ranges from 0.0 to 10.0. Default is 10.0 *)
+    FMOD_DSP_SFXREVERB_DECAYTIME,           (* Decay Time     : Reverberation decay time at low-frequencies in seconds.  Ranges from 0.1 to 20.0. Default is 1.0. *)
+    FMOD_DSP_SFXREVERB_DECAYHFRATIO,        (* Decay HF Ratio : High-frequency to low-frequency decay time ratio.  Ranges from 0.1 to 2.0. Default is 0.5. *)
+    FMOD_DSP_SFXREVERB_REFLECTIONSLEVEL,    (* Reflections    : Early reflections level relative to room effect in mB.  Ranges from -10000.0 to 1000.0.  Default is -10000.0. *)
+    FMOD_DSP_SFXREVERB_REFLECTIONSDELAY,    (* Reflect Delay  : Delay time of first reflection in seconds.  Ranges from 0.0 to 0.3.  Default is 0.02. *)
+    FMOD_DSP_SFXREVERB_REVERBLEVEL,         (* Reverb         : Late reverberation level relative to room effect in mB.  Ranges from -10000.0 to 2000.0.  Default is 0.0. *)
+    FMOD_DSP_SFXREVERB_REVERBDELAY,         (* Reverb Delay   : Late reverberation delay time relative to first reflection in seconds.  Ranges from 0.0 to 0.1.  Default is 0.04. *)
+    FMOD_DSP_SFXREVERB_DIFFUSION,           (* Diffusion      : Reverberation diffusion (echo density) in percent.  Ranges from 0.0 to 100.0.  Default is 100.0. *)
+    FMOD_DSP_SFXREVERB_DENSITY,             (* Density        : Reverberation density (modal density) in percent.  Ranges from 0.0 to 100.0.  Default is 100.0. *)
+    FMOD_DSP_SFXREVERB_HFREFERENCE          (* HF Reference   : Reference high frequency in Hz.  Ranges from 20.0 to 20000.0. Default is 5000.0. *)
+  );
+
+(*
+[ENUM]
+[
+    [DESCRIPTION]
+    Parameter types for the FMOD_DSP_TYPE_LOWPASS_SIMPLE filter.<br>
+    This is a very simple low pass filter, based on two single-pole RC time-constant modules.
+    The emphasis is on speed rather than accuracy, so this should not be used for task requiring critical filtering.<br>
+
+    [REMARKS]
+
+    [PLATFORMS]
+    Win32, Win64, Linux, Linux64, Macintosh, Xbox, Xbox360, PlayStation 2, GameCube, PlayStation Portable, PlayStation 3, Wii
+
+    [SEE_ALSO]
+    DSP::setParameter
+    DSP::getParameter
+    FMOD_DSP_TYPE
+]
+*)
+type
+  FMOD_DSP_LOWPASS_SIMPLE =
+  (
+    FMOD_DSP_LOWPASS_SIMPLE_CUTOFF     (* Lowpass cutoff frequency in hz.  10.0 to 22000.0.  Default = 5000.0 *)
+  );
+
+
+
+
+implementation
+
+end.
diff --git a/src/shared/BinEditor.pas b/src/shared/BinEditor.pas
new file mode 100644 (file)
index 0000000..f59d4f3
--- /dev/null
@@ -0,0 +1,664 @@
+Unit BinEditor;
+
+Interface
+
+Uses
+  SysUtils;
+
+Type
+  EBinSizeError = Class (Exception);
+
+  TBinMemoryWriter = Class (TObject)
+  Private
+    FSize: Cardinal;
+    FData: Pointer;
+    FPosition: Cardinal;
+
+    Procedure   WriteVar(Var x; varSize: Cardinal);
+    Procedure   ExtendMemory(addLen: Cardinal);
+
+  Public
+    Constructor Create(aSize: Cardinal);
+    Destructor  Destroy(); Override;
+    Procedure   WriteByte(Var x: Byte);
+    Procedure   WriteWord(Var x: Word);
+    Procedure   WriteDWORD(Var x: DWORD);
+    Procedure   WriteShortInt(Var x: ShortInt);
+    Procedure   WriteSmallInt(Var x: SmallInt);
+    Procedure   WriteInt(Var x: Integer);
+    Procedure   WriteSingle(Var x: Single);
+    Procedure   WriteBoolean(Var x: Boolean);
+    Procedure   WriteString(Var x: String; aMaxLen: Byte = 255);
+    Procedure   WriteMemory(Var x: Pointer; memSize: Cardinal);
+    Procedure   Fill(aLen: Cardinal; aFillSym: Byte);
+    Procedure   SaveToFile(Var aFile: File);
+    Procedure   SaveToMemory(Var aMem: TBinMemoryWriter);
+  End;
+
+  TBinMemoryReader = Class (TObject)
+  Private
+    FSize: Cardinal;
+    FData: Pointer;
+    FPosition: Cardinal;
+
+    Procedure   ReadVar(Var x; varSize: Cardinal);
+
+  Public
+    Constructor Create();
+    Destructor  Destroy(); Override;
+    Procedure   ReadByte(Var x: Byte);
+    Procedure   ReadWord(Var x: Word);
+    Procedure   ReadDWORD(Var x: DWORD);
+    Procedure   ReadShortInt(Var x: ShortInt);
+    Procedure   ReadSmallInt(Var x: SmallInt);
+    Procedure   ReadInt(Var x: Integer);
+    Procedure   ReadSingle(Var x: Single);
+    Procedure   ReadBoolean(Var x: Boolean);
+    Procedure   ReadString(Var x: String);
+    Procedure   ReadMemory(Var x: Pointer; Var memSize: Cardinal);
+    Procedure   Skip(aLen: Cardinal);
+    Procedure   LoadFromFile(Var aFile: File);
+    Procedure   LoadFromMemory(Var aMem: TBinMemoryReader);
+  End;
+
+  TBinFileWriter = Class (TObject)
+  Private
+    FHandle: File;
+    FOpened: Boolean;
+
+  Public
+    Constructor Create();
+    Destructor  Destroy(); Override;
+    Procedure   OpenFile(Const aFileName: String;
+                         aFileSig: Cardinal;
+                         aFileVer: Byte;
+                         aOverWrite: Boolean = True);
+    Procedure   Close();
+    Procedure   WriteMemory(Var aMemory: TBinMemoryWriter);
+  End;
+
+  TBinFileReader = Class (TObject)
+  Private
+    FHandle: File;
+    FOpened: Boolean;
+
+  Public
+    Constructor Create();
+    Destructor  Destroy(); Override;
+    Function    OpenFile(Const aFileName: String;
+                         aFileSig: Cardinal;
+                         aFileVer: Byte): Boolean;
+    Procedure   Close();
+    Procedure   ReadMemory(Var aMemory: TBinMemoryReader);
+  End;
+
+procedure FillMemory(Dest: Pointer; Len: Cardinal; Ch: Byte);
+procedure CopyMemory(Dest: Pointer; Src: Pointer; Len: Cardinal);
+procedure ZeroMemory(Dest: Pointer; Len: Cardinal);
+
+Implementation
+
+Uses
+  Math, e_log;
+
+Const
+  MAX_BIN_SIZE = 4 * 1024 * 1024; // 4 MB
+
+procedure CopyMemory(Dest: Pointer; Src: Pointer; Len: Cardinal);
+begin
+  Move(Src^, Dest^, Len);
+end;
+
+procedure FillMemory(Dest: Pointer; Len: Cardinal; Ch: Byte);
+begin
+  FillChar(Dest^, Len, Ch);
+end;
+
+procedure ZeroMemory(Dest: Pointer; Len: Cardinal);
+begin
+  FillChar(Dest^, Len, 0);
+end;
+  
+{ T B i n M e m o r y W r i t e r : }
+
+Constructor TBinMemoryWriter.Create(aSize: Cardinal);
+begin
+  if aSize <= 0 then
+    FSize := 1
+  else
+    FSize := aSize;
+  if FSize > MAX_BIN_SIZE then
+    FSize := MAX_BIN_SIZE;
+
+  GetMem(FData, FSize);
+  FPosition := 0;
+end;
+
+Destructor TBinMemoryWriter.Destroy();
+begin
+  if FData <> nil then
+  begin
+    FreeMem(FData);
+    FData := nil;
+  end;
+  
+  Inherited;
+end;
+
+Procedure TBinMemoryWriter.WriteVar(Var x; varSize: Cardinal);
+begin
+  if (FPosition + varSize) > FSize then
+    ExtendMemory(varSize);
+
+  CopyMemory(Pointer(Cardinal(FData) + FPosition),
+             @x, varSize);
+  FPosition := FPosition + varSize;
+end;
+
+Procedure TBinMemoryWriter.ExtendMemory(addLen: Cardinal);
+var
+  tmp: Pointer;
+
+begin
+  while ((FPosition + addLen) > FSize) and
+        (FSize <= MAX_BIN_SIZE) do
+    FSize := FSize * 2;
+
+  if FSize > MAX_BIN_SIZE then
+  begin
+    raise EBinSizeError.Create('TBinMemoryWriter.ExtendMemory: Tried to allocete more than 4 MB');
+    Exit;
+  end;
+
+  GetMem(tmp, FSize);
+
+  if FPosition > 0 then
+    CopyMemory(tmp, FData, FPosition);
+
+  FreeMem(FData);
+  FData := tmp;
+
+  e_WriteLog('Save Memory Extended: '+IntToStr(FSize), MSG_NOTIFY);
+end;
+
+Procedure TBinMemoryWriter.WriteByte(Var x: Byte);
+begin
+  WriteVar(x, SizeOf(Byte));
+end;
+
+Procedure TBinMemoryWriter.WriteWord(Var x: Word);
+begin
+  WriteVar(x, SizeOf(Word));
+end;
+
+Procedure TBinMemoryWriter.WriteDWORD(Var x: DWORD);
+begin
+  WriteVar(x, SizeOf(DWORD));
+end;
+
+Procedure TBinMemoryWriter.WriteShortInt(Var x: ShortInt);
+begin
+  WriteVar(x, SizeOf(ShortInt));
+end;
+
+Procedure TBinMemoryWriter.WriteSmallInt(Var x: SmallInt);
+begin
+  WriteVar(x, SizeOf(SmallInt));
+end;
+
+Procedure TBinMemoryWriter.WriteInt(Var x: Integer);
+begin
+  WriteVar(x, SizeOf(Integer));
+end;
+
+Procedure TBinMemoryWriter.WriteSingle(Var x: Single);
+begin
+  WriteVar(x, SizeOf(Single));
+end;
+
+Procedure TBinMemoryWriter.WriteBoolean(Var x: Boolean);
+var
+  y: Byte;
+
+begin
+  if x then
+    y := 1
+  else
+    y := 0;
+
+  WriteVar(y, SizeOf(Byte));
+end;
+
+Procedure TBinMemoryWriter.WriteString(Var x: String; aMaxLen: Byte = 255);
+var
+  len: Byte;
+
+begin
+  len := Min(Length(x), aMaxLen);
+
+  if (FPosition + SizeOf(Byte) + len) > FSize then
+    ExtendMemory(SizeOf(Byte) + len);
+
+// Äëèíà ñòðîêè:
+  CopyMemory(Pointer(Cardinal(FData) + FPosition),
+             @len, SizeOf(Byte));
+  FPosition := FPosition + SizeOf(Byte);
+// Ñòðîêà:
+  if len > 0 then
+  begin
+    CopyMemory(Pointer(Cardinal(FData) + FPosition),
+               @x[1], len);
+    FPosition := FPosition + len;
+  end;
+end;
+
+Procedure TBinMemoryWriter.WriteMemory(Var x: Pointer; memSize: Cardinal);
+begin
+  if (FPosition + SizeOf(Cardinal) + memSize) > FSize then
+    ExtendMemory(SizeOf(Cardinal) + memSize);
+
+// Äëèíà áëîêà ïàìÿòè:
+  CopyMemory(Pointer(Cardinal(FData) + FPosition),
+             @memSize, SizeOf(Cardinal));
+  FPosition := FPosition + SizeOf(Cardinal);
+// Áëîê ïàìÿòè:
+  if memSize > 0 then
+  begin
+    CopyMemory(Pointer(Cardinal(FData) + FPosition),
+               x, memSize);
+    FPosition := FPosition + memSize;
+  end;
+end;
+
+Procedure TBinMemoryWriter.Fill(aLen: Cardinal; aFillSym: Byte);
+begin
+  if (FPosition + aLen) > FSize then
+    ExtendMemory(aLen);
+
+  if aLen > 0 then
+  begin
+    FillMemory(Pointer(Cardinal(FData) + FPosition),
+               aLen, aFillSym);
+    FPosition := FPosition + aLen;
+  end;
+end;
+
+Procedure TBinMemoryWriter.SaveToFile(Var aFile: File);
+var
+  nw: Cardinal;
+
+begin
+// Ðàçìåð áëîêà:
+  BlockWrite(aFile, FPosition, SizeOf(Cardinal), nw);
+  if nw <> SizeOf(Cardinal) then
+    begin
+      raise EInOutError.Create('TBinMemoryWriter.SaveToFile: Writing Length');
+    end
+  else
+    begin
+    // Äàííûå áëîêà:
+      BlockWrite(aFile, FData^, FPosition, nw);
+      if nw <> FPosition then
+      begin
+        raise EInOutError.Create('TBinMemoryWriter.SaveToFile: Writing Data');
+      end
+    end;
+end;
+
+Procedure TBinMemoryWriter.SaveToMemory(Var aMem: TBinMemoryWriter);
+begin
+  if aMem <> nil then
+  begin
+    aMem.WriteMemory(FData, FPosition);
+  end;
+end;
+
+{ T B i n M e m o r y R e a d e r : }
+
+Constructor TBinMemoryReader.Create();
+begin
+  FSize := 0;
+  FData := nil;
+  FPosition := 1;
+end;
+
+Destructor TBinMemoryReader.Destroy();
+begin
+  if FData <> nil then
+  begin
+    FreeMem(FData);
+    FData := nil;
+  end;
+
+  Inherited;
+end;
+
+Procedure TBinMemoryReader.ReadVar(Var x; varSize: Cardinal);
+begin
+  if (FPosition + varSize) <= FSize then
+    begin
+      CopyMemory(@x,
+                 Pointer(Cardinal(FData) + FPosition),
+                 varSize);
+      FPosition := FPosition + varSize;
+    end
+  else
+    raise EBinSizeError.Create('TBinMemoryReader.ReadVar: End of Memory');
+end;
+
+Procedure TBinMemoryReader.ReadByte(Var x: Byte);
+begin
+  ReadVar(x, SizeOf(Byte));
+end;
+
+Procedure TBinMemoryReader.ReadWord(Var x: Word);
+begin
+  ReadVar(x, SizeOf(Word));
+end;
+
+Procedure TBinMemoryReader.ReadDWORD(Var x: DWORD);
+begin
+  ReadVar(x, SizeOf(DWORD));
+end;
+
+Procedure TBinMemoryReader.ReadShortInt(Var x: ShortInt);
+begin
+  ReadVar(x, SizeOf(ShortInt));
+end;
+
+Procedure TBinMemoryReader.ReadSmallInt(Var x: SmallInt);
+begin
+  ReadVar(x, SizeOf(SmallInt));
+end;
+
+Procedure TBinMemoryReader.ReadInt(Var x: Integer);
+begin
+  ReadVar(x, SizeOf(Integer));
+end;
+
+Procedure TBinMemoryReader.ReadSingle(Var x: Single);
+begin
+  ReadVar(x, SizeOf(Single));
+end;
+
+Procedure TBinMemoryReader.ReadBoolean(Var x: Boolean);
+var
+  y: Byte;
+
+begin
+  ReadVar(y, SizeOf(Byte));
+
+  if y > 0 then
+    x := True
+  else
+    x := False;
+end;
+
+Procedure TBinMemoryReader.ReadString(Var x: String);
+var
+  len: Byte;
+
+begin
+  if (FPosition + SizeOf(Byte)) <= FSize then
+    begin
+    // Äëèíà ñòðîêè:
+      CopyMemory(@len,
+                 Pointer(Cardinal(FData) + FPosition),
+                 SizeOf(Byte));
+         
+      if (FPosition + SizeOf(Byte) + len) <= FSize then
+        begin
+          FPosition := FPosition + SizeOf(Byte);
+        // Ñòðîêà:
+          SetLength(x, len);
+          if len > 0 then
+            begin
+              CopyMemory(@x[1],
+                         Pointer(Cardinal(FData) + FPosition),
+                         len);
+              FPosition := FPosition + len;
+            end
+          else
+            x := '';
+        end
+      else
+        raise EBinSizeError.Create('TBinMemoryReader.ReadString: Too Long String');
+    end
+  else
+    raise EBinSizeError.Create('TBinMemoryReader.ReadString: End of Memory');
+end;
+
+Procedure TBinMemoryReader.ReadMemory(Var x: Pointer; Var memSize: Cardinal);
+begin
+  if (FPosition + SizeOf(Cardinal)) <= FSize then
+    begin
+    // Äëèíà áëîêà ïàìÿòè:
+      CopyMemory(@memSize,
+                 Pointer(Cardinal(FData) + FPosition),
+                 SizeOf(Cardinal));
+
+      if (FPosition + SizeOf(Cardinal) + memSize) <= FSize then
+        begin
+          FPosition := FPosition + SizeOf(Cardinal);
+        // Áëîê ïàìÿòè:
+          if memSize > 0 then
+            begin
+              GetMem(x, memSize);
+              CopyMemory(x,
+                         Pointer(Cardinal(FData) + FPosition),
+                         memSize);
+              FPosition := FPosition + memSize;
+            end
+          else
+            x := nil;
+        end
+      else
+        raise EBinSizeError.Create('TBinMemoryReader.ReadMemory: Too Long Memory');
+    end
+  else
+    raise EBinSizeError.Create('TBinMemoryReader.ReadMemory: End of Memory');
+end;
+
+Procedure TBinMemoryReader.Skip(aLen: Cardinal);
+begin
+  if (FPosition + aLen) <= FSize then
+    begin
+      FPosition := FPosition + aLen;
+    end
+  else
+    raise EBinSizeError.Create('TBinMemoryReader.Skip: End of Memory');
+end;
+
+Procedure TBinMemoryReader.LoadFromFile(Var aFile: File);
+var
+  nr: Cardinal;
+  aSize: Cardinal;
+
+begin
+  if FData <> nil then
+    FreeMem(FData);
+
+// Ðàçìåð áëîêà:
+  BlockRead(aFile, aSize, SizeOf(Cardinal), nr);
+  if nr <> SizeOf(Cardinal) then
+    begin
+      raise EInOutError.Create('TBinMemoryReader.LoadFromFile: Reading Length');
+    end
+  else
+    begin
+      FSize := aSize;
+      GetMem(FData, FSize);
+      FPosition := 0;
+    // Äàííûå áëîêà:
+      BlockRead(aFile, FData^, FSize, nr);
+      if nr <> FSize then
+      begin
+        raise EInOutError.Create('TBinMemoryReader.LoadFromFile: Reading Data');
+      end
+    end;
+end;
+
+Procedure TBinMemoryReader.LoadFromMemory(Var aMem: TBinMemoryReader);
+begin
+  if FData <> nil then
+    FreeMem(FData);
+
+  if aMem <> nil then
+  begin
+    aMem.ReadMemory(FData, FSize);
+    FPosition := 0;
+  end;
+end;
+
+{ T B i n F i l e W r i t e r : }
+
+Constructor TBinFileWriter.Create();
+begin
+  FOpened := False;
+end;
+
+Destructor TBinFileWriter.Destroy();
+begin
+  Close();
+
+  Inherited;
+end;
+
+Procedure TBinFileWriter.OpenFile(Const aFileName: String;
+                                  aFileSig: Cardinal;
+                                  aFileVer: Byte;
+                                  aOverWrite: Boolean = True);
+var
+  nw: Integer;
+
+begin
+  Close();
+
+  if (not FileExists(aFileName)) or (aOverWrite) then
+  begin
+    AssignFile(FHandle, aFileName);
+    ReWrite(FHandle, 1);
+
+  // Ñèãíàòóðà:
+    BlockWrite(FHandle, aFileSig, SizeOf(Cardinal), nw);
+    if nw <> SizeOf(Cardinal) then
+      begin
+        raise EInOutError.Create('TBinFileWriter.OpenFile: Writing File Signature');
+      end
+    else
+      begin
+      // Âåðñèÿ:
+        BlockWrite(FHandle, aFileVer, SizeOf(Byte), nw);
+        if nw <> SizeOf(Byte) then
+          begin
+            raise EInOutError.Create('TBinFileWriter.OpenFile: Writing File Version');
+          end
+        else
+          begin
+            FOpened := True;
+          end;
+      end;
+  end;
+end;
+
+Procedure TBinFileWriter.Close();
+begin
+  if FOpened then
+  begin
+    System.Close(FHandle);
+    FOpened := False;
+  end;
+end;
+
+Procedure TBinFileWriter.WriteMemory(Var aMemory: TBinMemoryWriter);
+begin
+  if (FOpened) and (aMemory <> nil) then
+  begin
+    aMemory.SaveToFile(FHandle);
+  end;
+end;
+
+{ T B i n F i l e R e a d e r : }
+
+Constructor TBinFileReader.Create();
+begin
+  FOpened := False;
+end;
+
+Destructor TBinFileReader.Destroy();
+begin
+  Close();
+
+  Inherited;
+end;
+
+Function TBinFileReader.OpenFile(Const aFileName: String;
+                                 aFileSig: Cardinal;
+                                 aFileVer: Byte): Boolean;
+var
+  nr: Integer;
+  sig: Cardinal;
+  ver: Byte;
+
+begin
+  Result := False;
+
+  Close();
+
+  if FileExists(aFileName) then
+  begin
+    AssignFile(FHandle, aFileName);
+    ReSet(FHandle, 1);
+
+  // Ñèãíàòóðà:
+    BlockRead(FHandle, sig, SizeOf(Cardinal), nr);
+    if (nr <> SizeOf(Cardinal)) then
+      begin
+        raise EInOutError.Create('TBinFileReader.OpenFile: Reading File Signature');
+      end
+    else
+      if (sig <> aFileSig) then
+        begin
+          raise EInOutError.Create('TBinFileReader.OpenFile: Wrong File Signature');
+        end
+      else
+        begin
+        // Âåðñèÿ:
+          BlockRead(FHandle, ver, SizeOf(Byte), nr);
+          if (nr <> SizeOf(Byte)) then
+            begin
+              raise EInOutError.Create('TBinFileReader.OpenFile: Reading File Version');
+            end
+          else
+            if (ver <> aFileVer) then
+              begin
+                raise EInOutError.Create('TBinFileReader.OpenFile: Wrong File Version');
+              end
+            else
+              begin
+                FOpened := True;
+                Result := True;
+              end;
+        end;
+  end;
+end;
+
+Procedure TBinFileReader.Close();
+begin
+  if FOpened then
+  begin
+    System.Close(FHandle);
+    FOpened := False;
+  end;
+end;
+
+Procedure TBinFileReader.ReadMemory(Var aMemory: TBinMemoryReader);
+begin
+  if (FOpened) and (aMemory <> nil) then
+  begin
+    aMemory.LoadFromFile(FHandle);
+  end;
+end;
+
+
+End.
diff --git a/src/shared/CONFIG.pas b/src/shared/CONFIG.pas
new file mode 100644 (file)
index 0000000..00129f3
--- /dev/null
@@ -0,0 +1,305 @@
+unit CONFIG;
+
+{
+-----------------------------------
+CONFIG.PAS ÂÅÐÑÈß ÎÒ 24.09.06
+-----------------------------------
+}
+
+interface
+
+type
+  TParam = record
+   Param: ShortString;
+   Value: ShortString;
+   Section: Word;
+  end;
+
+  TConfig = class(TObject)
+   private
+    FParams: array of TParam;
+    FSections: array of ShortString;
+    FCurrentSection: Word;
+    function ReadParam(Section, Param, Default: string): string;
+    procedure WriteParam(Section, Param, Value: string);
+    procedure ProcessStr(Str: string);
+   public
+    constructor Create();
+    constructor CreateFile(FileName: string);
+    constructor CreateMem(pData: Pointer; _Length: LongWord);
+    destructor Destroy(); override;
+    procedure FreeConfig();
+    procedure SaveFile(FileName: string);
+    function ReadInt(Section, Param: string; Default: Integer): Integer;
+    function ReadStr(Section, Param: string; Default: String): string;
+    function ReadBool(Section, Param: string; Default: Boolean): Boolean;
+    function SectionExists(Section: string): Boolean;
+    procedure WriteInt(Section, Param: string; Value: Integer);
+    procedure WriteStr(Section, Param, Value: string);
+    procedure WriteBool(Section, Param: string; Value: Boolean);
+   end;
+
+implementation
+
+uses
+  SysUtils, BinEditor;
+
+{ TConfig }
+
+constructor TConfig.Create();
+begin
+ inherited Create;
+
+ FreeConfig();
+end;
+
+constructor TConfig.CreateFile(FileName: string);
+var
+  f: TextFile;
+  a: string;
+begin
+ FreeConfig();
+
+ if not FileExists(FileName) then Exit;
+
+ AssignFile(f, FileName);
+ Reset(f);
+ while not EOF(f) do
+ begin
+  Readln(f, a);
+  ProcessStr(a);
+ end;
+ CloseFile(f);
+end;
+
+constructor TConfig.CreateMem(pData: Pointer; _Length: LongWord);
+var
+  a: Integer;
+  str: string;
+begin
+ FreeConfig();
+
+ if _Length = 0 then Exit;
+ if pData = nil then Exit;
+
+ SetLength(str, _Length);
+
+ CopyMemory(@str[1], pData, _Length);
+
+ while Str <> '' do
+ begin
+  for a := 2 to Length(Str) do
+   if (Str[a-1]+Str[a] = #13#10) or (a = Length(Str)) then
+   begin
+    if a <> Length(Str) then ProcessStr(Copy(Str, 1, a-2)) else ProcessStr(Str);
+    Delete(Str, 1, a);
+    Str := Trim(Str);
+    Break;
+   end;
+ end;
+end;
+
+destructor TConfig.Destroy();
+begin
+ FParams := nil;
+ FSections := nil;
+
+ inherited;
+end;
+
+procedure TConfig.FreeConfig();
+begin
+ FParams := nil;
+ FSections := nil;
+
+ SetLength(FSections, 1);
+ FSections[0] := '';
+ FCurrentSection := 0;
+end;
+
+procedure TConfig.ProcessStr(Str: string);
+var
+  a, l: Integer;
+begin
+ Str := Trim(Str);
+
+ if (Str <> '') and (Length(Str) > 2) and (Str[1] <> ';') then
+ begin
+  l := Length(Str);
+
+  if Pos('=', Str) > 0  then
+  begin
+   SetLength(FParams, Length(FParams)+1);
+
+   with FParams[High(FParams)] do
+   begin
+    a := Pos('=', Str);
+    Param := Trim(Copy(Str, 1, a-1));
+    Value := Trim(Copy(Str, a+1, l));
+    Section := FCurrentSection;
+   end;
+  end
+   else if (Str[1] = '[') and (Str[l] = ']') then
+  begin
+   SetLength(FSections, Length(FSections)+1);
+   FCurrentSection := High(FSections);
+   FSections[FCurrentSection] := Trim(Copy(Str, 2, l-2));
+  end;
+ end;
+end;
+
+function TConfig.ReadBool(Section, Param: string; Default: Boolean): Boolean;
+var
+  a: Integer;
+begin
+ if Default then a := 1 else a := 0;
+
+ Result := StrToIntDef(ReadParam(Section, Param, IntToStr(a)), a) <> 0;
+end;
+
+function TConfig.ReadInt(Section, Param: string; Default: Integer): Integer;
+begin
+ Result := StrToIntDef(ReadParam(Section, Param, IntToStr(Default)), Default);
+end;
+
+function TConfig.ReadParam(Section, Param, Default: string): string;
+var
+  a: Integer;
+  s: Word;
+  ok: Boolean;
+  p: string;
+begin
+ Result := default;
+
+ if FParams = nil then Exit;
+ if FSections = nil then Exit;
+
+ ok := False;
+ s := 0;
+ for a := 0 to High(FSections) do
+  if LowerCase(FSections[a]) = LowerCase(Section) then
+  begin
+   s := a;
+   ok := True;
+  end;
+
+ if not ok then Exit;
+
+ p := LowerCase(Param);
+
+ for a := 0 to High(FParams) do
+  if (FParams[a].Section = s) and (LowerCase(FParams[a].Param) = p) then
+  begin
+   Result := FParams[a].Value;
+   Break;
+  end;
+end;
+
+function TConfig.ReadStr(Section, Param, Default: string): string;
+begin
+ Result := ReadParam(Section, Param, Default);
+end;
+
+procedure TConfig.SaveFile(FileName: string);
+var
+  f: TextFile;
+  a: Integer;
+  b: Integer;
+begin
+ AssignFile(f, FileName);
+ Rewrite(f);
+ if (FSections <> nil) or (FParams <> nil) then
+ begin
+  if FSections <> nil then
+   for a := 0 to High(FSections) do
+   begin
+    if FSections[a] <> '' then Writeln(f, '['+FSections[a]+']');
+
+    if FParams <> nil then
+     for b := 0 to High(FParams) do
+      if FParams[b].Section = a then WriteLn(f, FParams[b].Param+'='+FParams[b].Value);
+
+    if (a <> High(FSections)) and (FSections[a] <> '') then WriteLn(f, '');
+   end;
+ end;
+ CloseFile(f);
+end;
+
+function TConfig.SectionExists(Section: string): Boolean;
+var
+  a: Integer;
+begin
+ Result := False;
+
+ if FSections = nil then Exit;
+
+ Section := LowerCase(Section);
+
+ for a := 0 to High(FSections) do
+  if Section = LowerCase(FSections[a]) then
+  begin
+   Result := True;
+   Exit;
+  end;
+end;
+
+procedure TConfig.WriteBool(Section, Param: string; Value: Boolean);
+begin
+ WriteParam(Section, Param, BoolToStr(Value)); 
+end;
+
+procedure TConfig.WriteInt(Section, Param: string; Value: Integer);
+begin
+ WriteParam(Section, Param, IntToStr(Value));
+end;
+
+procedure TConfig.WriteParam(Section, Param, Value: string);
+var
+  a, b: Integer;
+  ok: Boolean;
+begin
+ a := 0;
+ b := 0;
+
+ ok := False;
+
+ if FSections <> nil then
+  for a := 0 to High(FSections) do
+   if FSections[a] = Section then
+   begin
+    ok := True;
+    Break;
+   end;
+
+ if not ok then
+ begin
+  SetLength(FSections, Length(FSections)+1);
+  a := High(FSections);
+  FSections[a] := Section;
+ end;
+
+ ok := False;
+ if FParams <> nil then
+  for b := 0 to High(FParams) do
+   if (LowerCase(FParams[b].Param) = LowerCase(Param)) and (FParams[b].Section = a) then
+   begin
+    ok := True;
+    Break;
+   end;
+
+ if ok then FParams[b].Value := Value
+  else
+ begin
+  SetLength(FParams, Length(FParams)+1);
+  FParams[High(FParams)].Param := Param;
+  FParams[High(FParams)].Value := Value;
+  FParams[High(FParams)].Section := a;
+ end;
+end;
+
+procedure TConfig.WriteStr(Section, Param, Value: string);
+begin
+ WriteParam(Section, Param, Value);
+end;
+
+end.
diff --git a/src/shared/CONFIGSIMPLE.pas b/src/shared/CONFIGSIMPLE.pas
new file mode 100644 (file)
index 0000000..05960de
--- /dev/null
@@ -0,0 +1,142 @@
+unit CONFIGSIMPLE;
+
+interface
+
+function config_open(FileName: string): Boolean;
+function config_read_int(param: string; def: Integer): Integer;
+function config_read_str(param: string; def: string): string;
+function config_read_bool(param: string; def: Boolean): Boolean;
+procedure config_close();
+
+implementation
+
+uses windows;
+
+var
+  cfg_data: array of ShortString = nil;
+
+function tostr(i: Integer): string;
+begin
+ Str(i, Result);
+end;
+
+function toint(s: string; var i: Integer): Boolean;
+var
+  code: Integer;
+begin
+ Val(s, i, code);
+
+ Result := code = 0;
+end;
+
+function readparam(param: string; var s: string): Boolean;
+var
+  a, b, len, d_len: Integer;
+begin
+ Result := False;
+
+ if cfg_data = nil then Exit;
+
+ d_len := Length(cfg_data);
+ for a := 0 to d_len do
+ begin
+  len := Length(cfg_data[a]);
+  if len = 0 then Exit;
+
+  for b := 1 to len do
+   if cfg_data[a][b] = '=' then
+    if Copy(cfg_data[a], 1, b-1) = param then
+    begin
+     s := Copy(cfg_data[a], b+1, len);
+     Result := True;
+     Exit;
+    end;
+ end;
+end;
+
+function config_open(FileName: string): Boolean;
+var
+  f: TextFile;
+  str: ShortString;
+  len, d_len, line: Integer;
+begin
+ Result := False;
+
+ if cfg_data <> nil then config_close();
+
+ AssignFile(f, FileName);
+
+ {$I-}
+ Reset(f);
+ {$I+}
+
+ if IOResult <> 0 then Exit;
+
+ d_len := 32;
+ SetLength(cfg_data, d_len);
+ line := 0;
+
+ while not EOF(f) do
+ begin
+  Readln(f, str);
+
+  len := Length(str);
+  if len < 3 then Continue;
+  if str[1] = ';' then Continue;
+
+  if line >= d_len then
+  begin
+   d_len := d_len+32;
+   SetLength(cfg_data, d_len);
+  end;
+
+  cfg_data[line] := str;
+  line := line+1;
+ end;
+
+ CloseFile(f);
+
+ Result := True;
+end;
+
+function config_read_int(param: string; def: Integer): Integer;
+var
+  s: string;
+begin
+ Result := def;
+
+ if not readparam(param, s) then Exit;
+
+ if not toint(s, Result) then Result := def;
+end;
+
+function config_read_str(param: string; def: string): string;
+var
+  s: string;
+begin
+ Result := def;
+
+ if not readparam(param, s) then Exit;
+
+ Result := s;
+end;
+
+function config_read_bool(param: string; def: Boolean): Boolean;
+var
+  s: string;
+begin
+ Result := def;
+
+ if not readparam(param, s) then Exit;
+
+ Result := s <> '0';
+end;
+
+procedure config_close();
+begin
+ if cfg_data <> nil then cfg_data := nil;
+end;
+
+
+end.
diff --git a/src/shared/MAPDEF.pas b/src/shared/MAPDEF.pas
new file mode 100644 (file)
index 0000000..697d253
--- /dev/null
@@ -0,0 +1,321 @@
+unit MAPDEF;
+
+{
+-----------------------------------
+MAPDEF.PAS ÂÅÐÑÈß ÎÒ 22.03.09
+
+Ïîääåðæêà êàðò âåðñèè 1
+-----------------------------------
+}
+
+interface
+
+uses
+  MAPSTRUCT;
+
+const
+  PANEL_NONE      = 0;
+  PANEL_WALL      = 1;
+  PANEL_BACK      = 2;
+  PANEL_FORE      = 4;
+  PANEL_WATER     = 8;
+  PANEL_ACID1     = 16;
+  PANEL_ACID2     = 32;
+  PANEL_STEP      = 64;
+  PANEL_LIFTUP    = 128;
+  PANEL_LIFTDOWN  = 256;
+  PANEL_OPENDOOR  = 512;
+  PANEL_CLOSEDOOR = 1024;
+  PANEL_BLOCKMON  = 2048;
+  PANEL_LIFTLEFT  = 4096;
+  PANEL_LIFTRIGHT = 8192;
+
+  PANEL_FLAG_BLENDING      = 1;
+  PANEL_FLAG_HIDE          = 2;
+  PANEL_FLAG_WATERTEXTURES = 4;
+
+  EFFECT_NONE     = 0;
+  EFFECT_TELEPORT = 1;
+  EFFECT_RESPAWN  = 2;
+  EFFECT_FIRE     = 3;
+
+  ITEM_NONE                  = 0;
+  ITEM_MEDKIT_SMALL          = 1;
+  ITEM_MEDKIT_LARGE          = 2;
+  ITEM_MEDKIT_BLACK          = 3;
+  ITEM_ARMOR_GREEN           = 4;
+  ITEM_ARMOR_BLUE            = 5;
+  ITEM_SPHERE_BLUE           = 6;
+  ITEM_SPHERE_WHITE          = 7;
+  ITEM_SUIT                  = 8;
+  ITEM_OXYGEN                = 9;
+  ITEM_INVUL                 = 10;
+  ITEM_WEAPON_SAW            = 11;
+  ITEM_WEAPON_SHOTGUN1       = 12;
+  ITEM_WEAPON_SHOTGUN2       = 13;
+  ITEM_WEAPON_CHAINGUN       = 14;
+  ITEM_WEAPON_ROCKETLAUNCHER = 15;
+  ITEM_WEAPON_PLASMA         = 16;
+  ITEM_WEAPON_BFG            = 17;
+  ITEM_WEAPON_SUPERPULEMET   = 18;
+  ITEM_AMMO_BULLETS          = 19;
+  ITEM_AMMO_BULLETS_BOX      = 20;
+  ITEM_AMMO_SHELLS           = 21;
+  ITEM_AMMO_SHELLS_BOX       = 22;
+  ITEM_AMMO_ROCKET           = 23;
+  ITEM_AMMO_ROCKET_BOX       = 24;
+  ITEM_AMMO_CELL             = 25;
+  ITEM_AMMO_CELL_BIG         = 26;
+  ITEM_AMMO_BACKPACK         = 27;
+  ITEM_KEY_RED               = 28;
+  ITEM_KEY_GREEN             = 29;
+  ITEM_KEY_BLUE              = 30;
+  ITEM_WEAPON_KASTET         = 31;
+  ITEM_WEAPON_PISTOL         = 32;
+  ITEM_BOTTLE                = 33;
+  ITEM_HELMET                = 34;
+  ITEM_JETPACK               = 35;
+  ITEM_INVIS                 = 36;
+
+  ITEM_MAX                   = 36; // store the last item's id in here
+                                   // use this in for loops
+
+  ITEM_OPTION_ONLYDM = 1;
+  ITEM_OPTION_FALL   = 2;
+
+  AREA_NONE          = 0;
+  AREA_PLAYERPOINT1  = 1;
+  AREA_PLAYERPOINT2  = 2;
+  AREA_DMPOINT       = 3;
+  AREA_REDFLAG       = 4;
+  AREA_BLUEFLAG      = 5;
+  AREA_DOMFLAG       = 6;
+  AREA_REDTEAMPOINT  = 7;
+  AREA_BLUETEAMPOINT = 8;
+
+  MONSTER_NONE   = 0;
+  MONSTER_DEMON  = 1;
+  MONSTER_IMP    = 2;
+  MONSTER_ZOMBY  = 3;
+  MONSTER_SERG   = 4;
+  MONSTER_CYBER  = 5;
+  MONSTER_CGUN   = 6;
+  MONSTER_BARON  = 7;
+  MONSTER_KNIGHT = 8;
+  MONSTER_CACO   = 9;
+  MONSTER_SOUL   = 10;
+  MONSTER_PAIN   = 11;
+  MONSTER_SPIDER = 12;
+  MONSTER_BSP    = 13;
+  MONSTER_MANCUB = 14;
+  MONSTER_SKEL   = 15;
+  MONSTER_VILE   = 16;
+  MONSTER_FISH   = 17;
+  MONSTER_BARREL = 18;
+  MONSTER_ROBO   = 19;
+  MONSTER_MAN    = 20;
+
+  TRIGGER_NONE            = 0;
+  TRIGGER_EXIT            = 1;
+  TRIGGER_TELEPORT        = 2;
+  TRIGGER_OPENDOOR        = 3;
+  TRIGGER_CLOSEDOOR       = 4;
+  TRIGGER_DOOR            = 5;
+  TRIGGER_DOOR5           = 6;
+  TRIGGER_CLOSETRAP       = 7;
+  TRIGGER_TRAP            = 8;
+  TRIGGER_PRESS           = 9;
+  TRIGGER_SECRET          = 10;
+  TRIGGER_LIFTUP          = 11;
+  TRIGGER_LIFTDOWN        = 12;
+  TRIGGER_LIFT            = 13;
+  TRIGGER_TEXTURE         = 14;
+  TRIGGER_ON              = 15;
+  TRIGGER_OFF             = 16;
+  TRIGGER_ONOFF           = 17;
+  TRIGGER_SOUND           = 18;
+  TRIGGER_SPAWNMONSTER    = 19;
+  TRIGGER_SPAWNITEM       = 20;
+  TRIGGER_MUSIC           = 21;
+  TRIGGER_PUSH            = 22;
+  TRIGGER_SCORE           = 23;
+  TRIGGER_MESSAGE         = 24;
+  TRIGGER_DAMAGE          = 25;
+  TRIGGER_HEALTH          = 26;
+  TRIGGER_SHOT            = 27;
+  TRIGGER_EFFECT          = 28;
+  TRIGGER_MAX             = 28;
+
+  TRIGGER_SHOT_PISTOL  = 0;
+  TRIGGER_SHOT_BULLET  = 1;
+  TRIGGER_SHOT_SHOTGUN = 2;
+  TRIGGER_SHOT_SSG     = 3;
+  TRIGGER_SHOT_IMP     = 4;
+  TRIGGER_SHOT_PLASMA  = 5;
+  TRIGGER_SHOT_SPIDER  = 6;
+  TRIGGER_SHOT_CACO    = 7;
+  TRIGGER_SHOT_BARON   = 8;
+  TRIGGER_SHOT_MANCUB  = 9;
+  TRIGGER_SHOT_REV     = 10;
+  TRIGGER_SHOT_ROCKET  = 11;
+  TRIGGER_SHOT_BFG     = 12;
+  TRIGGER_SHOT_EXPL    = 13;
+  TRIGGER_SHOT_BFGEXPL = 14;
+  TRIGGER_SHOT_MAX     = 14;
+
+  TRIGGER_SHOT_TARGET_NONE   = 0;
+  TRIGGER_SHOT_TARGET_MON    = 1;
+  TRIGGER_SHOT_TARGET_PLR    = 2;
+  TRIGGER_SHOT_TARGET_RED    = 3;
+  TRIGGER_SHOT_TARGET_BLUE   = 4;
+  TRIGGER_SHOT_TARGET_MONPLR = 5;
+  TRIGGER_SHOT_TARGET_PLRMON = 6;
+
+  TRIGGER_EFFECT_PARTICLE  = 0;
+  TRIGGER_EFFECT_ANIMATION = 1;
+
+  TRIGGER_EFFECT_SLIQUID = 0;
+  TRIGGER_EFFECT_LLIQUID = 1;
+  TRIGGER_EFFECT_DLIQUID = 2;
+  TRIGGER_EFFECT_BLOOD   = 3;
+  TRIGGER_EFFECT_SPARK   = 4;
+  TRIGGER_EFFECT_BUBBLE  = 5;
+  TRIGGER_EFFECT_MAX     = 5;
+
+  TRIGGER_EFFECT_POS_CENTER = 0;
+  TRIGGER_EFFECT_POS_AREA   = 1;
+
+  ACTIVATE_PLAYERCOLLIDE  = 1;
+  ACTIVATE_MONSTERCOLLIDE = 2;
+  ACTIVATE_PLAYERPRESS    = 4;
+  ACTIVATE_MONSTERPRESS   = 8;
+  ACTIVATE_SHOT           = 16;
+  ACTIVATE_NOMONSTER      = 32;
+  ACTIVATE_CUSTOM         = 255;
+
+  KEY_RED      = 1;
+  KEY_GREEN    = 2;
+  KEY_BLUE     = 4;
+  KEY_REDTEAM  = 8;
+  KEY_BLUETEAM = 16;
+
+  TEXTURE_NAME_WATER = '_water_0';
+  TEXTURE_NAME_ACID1 = '_water_1';
+  TEXTURE_NAME_ACID2 = '_water_2';
+
+  TEXTURE_SPECIAL_WATER = DWORD(-1);
+  TEXTURE_SPECIAL_ACID1 = DWORD(-2);
+  TEXTURE_SPECIAL_ACID2 = DWORD(-3);
+  TEXTURE_NONE = DWORD(-4);
+
+Type
+  TPoint = packed record
+    X, Y: LongInt;
+  end;
+
+  TTriggerData = record
+    case Byte of
+      0: (Default: Byte128);
+      TRIGGER_EXIT:         (MapName: Char16);
+      TRIGGER_TELEPORT:     (TargetPoint: TPoint;
+                             d2d_teleport: Boolean;
+                             silent_teleport: Boolean;
+                             TlpDir: Byte);
+      TRIGGER_OPENDOOR,
+      TRIGGER_CLOSEDOOR,
+      TRIGGER_DOOR,
+      TRIGGER_DOOR5,
+      TRIGGER_CLOSETRAP,
+      TRIGGER_TRAP,
+      TRIGGER_LIFTUP,
+      TRIGGER_LIFTDOWN,
+      TRIGGER_LIFT:         (PanelID: Integer;
+                             NoSound: Boolean;
+                             d2d_doors: Boolean);
+      TRIGGER_PRESS,
+      TRIGGER_ON,
+      TRIGGER_OFF,
+      TRIGGER_ONOFF:        (tX, tY: Integer;
+                             tWidth, tHeight: Word;
+                             Wait: Word;
+                             Count: Word;
+                             MonsterID: Integer;
+                             ExtRandom: Boolean);
+      TRIGGER_SECRET:       ();
+      TRIGGER_TEXTURE:      (ActivateOnce: Boolean;
+                             AnimOnce: Boolean);
+      TRIGGER_SOUND:        (SoundName: Char64;
+                             Volume: Byte;
+                             Pan: Byte;
+                             Local: Boolean;
+                             PlayCount: Byte;
+                             SoundSwitch: Boolean);
+      TRIGGER_SPAWNMONSTER: (MonPos: TPoint;
+                             MonType: Byte;
+                             MonHealth: Integer;
+                             MonDir: Byte;
+                             MonActive: Boolean;
+                             MonCount: Integer;
+                             MonEffect: Byte;
+                             MonMax: Word;
+                             MonDelay: Word;
+                             MonBehav: Byte);
+      TRIGGER_SPAWNITEM:    (ItemPos: TPoint;
+                             ItemType: Byte;
+                             ItemFalls: Boolean;
+                             ItemOnlyDM: Boolean;
+                             ItemCount: Integer;
+                             ItemEffect: Byte;
+                             ItemMax: Word;
+                             ItemDelay: Word);
+      TRIGGER_MUSIC:        (MusicName: Char64;
+                             MusicAction: Byte);
+      TRIGGER_PUSH:         (PushAngle: Word;
+                             PushForce: Byte;
+                             ResetVel: Boolean);
+      TRIGGER_SCORE:        (ScoreAction: Byte;
+                             ScoreCount: Byte;
+                             ScoreTeam: Byte;
+                             ScoreCon,
+                             ScoreMsg: Boolean);
+      TRIGGER_MESSAGE:      (MessageKind: Byte;
+                             MessageSendTo: Byte;
+                             MessageText: Char100;
+                             MessageTime: Word);
+      TRIGGER_DAMAGE:       (DamageValue: Word;
+                             DamageInterval: Word);
+      TRIGGER_HEALTH:       (HealValue: Word;
+                             HealInterval: Word;
+                             HealMax: Boolean;
+                             HealSilent: Boolean);
+      TRIGGER_SHOT:         (ShotType: Byte;
+                             ShotSound: Boolean;
+                             ShotPanelID: Integer;
+                             ShotTarget: Byte;
+                             ShotIntSight: Word;
+                             ShotAllMap: Boolean;
+                             ShotPos: TPoint;
+                             ShotAngle: Word;
+                             ShotWait: Word;
+                             ShotAccuracy: Word;
+                             ShotAmmo: Word;
+                             ShotIntReload: Word);
+      TRIGGER_EFFECT:       (FXCount: Byte;
+                             FXType: Byte;
+                             FXSubType: Byte;
+                             FXColorR: Byte;
+                             FXColorG: Byte;
+                             FXColorB: Byte;
+                             FXPos: Byte;
+                             FXWait: Word;
+                             FXVelX: ShortInt;
+                             FXVelY: ShortInt;
+                             FXSpreadL: Byte;
+                             FXSpreadR: Byte;
+                             FXSpreadU: Byte;
+                             FXSpreadD: Byte);
+  end;
+
+implementation
+
+end.
diff --git a/src/shared/MAPREADER.pas b/src/shared/MAPREADER.pas
new file mode 100644 (file)
index 0000000..868c1b0
--- /dev/null
@@ -0,0 +1,392 @@
+unit MAPREADER;
+
+{
+-----------------------------------
+MAPREADER.PAS ÂÅÐÑÈß ÎÒ 13.11.07
+
+Ïîääåðæêà êàðò âåðñèè 1
+-----------------------------------
+}
+
+interface
+
+uses
+  MAPSTRUCT;
+
+type
+  TDataBlock = packed record
+   Block: TBlock;
+   Data:  Pointer;
+  end;
+
+  TDataBlocksArray = packed array of TDataBlock;
+
+  TMapReader = class(TObject)
+   private
+    FError: Byte;
+    FVersion: Byte;
+    FDataBlocks: TDataBlocksArray;
+    function GetBlocks(BlocksType: Byte): TDataBlocksArray;
+   public
+    constructor Create();
+    destructor Destroy(); override;
+    function LoadMap(Data: Pointer): Boolean;
+    procedure FreeMap();
+    function HandledVersion(): Byte; virtual;
+    
+    property GetError: Byte read FError;
+    property GetVersion: Byte read FVersion;
+  end;
+
+  TMapReader_1 = class(TMapReader)
+   private
+   public
+    function GetMapHeader(): TMapHeaderRec_1;
+    function GetTextures(): TTexturesRec1Array;
+    function GetPanels(): TPanelsRec1Array;
+    function GetItems(): TItemsRec1Array;
+    function GetAreas(): TAreasRec1Array;
+    function GetMonsters(): TMonsterRec1Array;
+    function GetTriggers(): TTriggersRec1Array;
+    function HandledVersion(): Byte; override;
+  end;
+
+const
+  MAP_ERROR_NONE      = $00;
+  MAP_ERROR_SIGNATURE = $01;
+  MAP_ERROR_VERSION   = $02;
+
+  NNF_NO_NAME         = 0;
+  NNF_NAME_BEFORE     = 1;
+  NNF_NAME_EQUALS     = 2;
+  NNF_NAME_AFTER      = 3;
+
+function g_Texture_NumNameFindStart(name: String): Boolean;
+function g_Texture_NumNameFindNext(var newName: String): Byte;
+
+implementation
+
+uses
+  SysUtils, BinEditor;
+
+var
+  NNF_PureName: String; // Èìÿ òåêñòóðû áåç öèôð â êîíöå
+  NNF_FirstNum: Integer; // ×èñëî ó íà÷àëüíîé òåêñòóðû
+  NNF_CurrentNum: Integer; // Ñëåäóþùåå ÷èñëî ó òåêñòóðû
+
+function g_Texture_NumNameFindStart(name: String): Boolean;
+var
+  i: Integer;
+
+begin
+  Result := False;
+  NNF_PureName := '';
+  NNF_FirstNum := -1;
+  NNF_CurrentNum := -1;
+
+  for i := Length(name) downto 1 do
+    if (name[i] = '_') then // "_" - ñèìâîë íà÷àëà íîìåðíîãî ïîñòôèêñà
+    begin
+      if i = Length(name) then
+        begin // Íåò öèôð â êîíöå ñòðîêè
+          Exit;
+        end
+      else
+        begin
+          NNF_PureName := Copy(name, 1, i);
+          Delete(name, 1, i);
+          Break;
+        end;
+    end;
+
+// Íå ïåðåâåñòè â ÷èñëî:
+  if not TryStrToInt(name, NNF_FirstNum) then
+    Exit;
+
+  NNF_CurrentNum := 0;
+
+  Result := True;
+end;
+
+function g_Texture_NumNameFindNext(var newName: String): Byte;
+begin
+  if (NNF_PureName = '') or (NNF_CurrentNum < 0) then
+  begin
+    newName := '';
+    Result := NNF_NO_NAME;
+    Exit;
+  end;
+
+  newName := NNF_PureName + IntToStr(NNF_CurrentNum);
+
+  if NNF_CurrentNum < NNF_FirstNum then
+    Result := NNF_NAME_BEFORE
+  else
+    if NNF_CurrentNum > NNF_FirstNum then
+      Result := NNF_NAME_AFTER
+    else
+      Result := NNF_NAME_EQUALS;
+
+  Inc(NNF_CurrentNum);
+end;
+
+{ T M a p R e a d e r _ 1 : }
+
+function TMapReader_1.GetAreas(): TAreasRec1Array;
+var
+  TempDataBlocks: TDataBlocksArray;
+  a: Integer;
+  b, Size: LongWord;
+begin
+ Result := nil;
+
+ TempDataBlocks := GetBlocks(BLOCK_AREAS);
+
+ if TempDataBlocks = nil then Exit;
+
+ size := SizeOf(TAreaRec_1);
+
+ for a := 0 to High(TempDataBlocks) do
+  for b := 0 to (TempDataBlocks[a].Block.BlockSize div size)-1 do
+  begin
+   SetLength(Result, Length(Result)+1);
+   CopyMemory(@Result[High(Result)], Pointer(LongWord(TempDataBlocks[a].Data)+b*size), size);
+  end;
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.GetItems(): TItemsRec1Array;
+var
+  TempDataBlocks: TDataBlocksArray;
+  a: Integer;
+  b, Size: LongWord;
+begin
+ Result := nil;
+
+ TempDataBlocks := GetBlocks(BLOCK_ITEMS);
+
+ if TempDataBlocks = nil then Exit;
+
+ size := SizeOf(TItemRec_1);
+
+ for a := 0 to High(TempDataBlocks) do
+  for b := 0 to (TempDataBlocks[a].Block.BlockSize div size)-1 do
+  begin
+   SetLength(Result, Length(Result)+1);
+   CopyMemory(@Result[High(Result)], Pointer(LongWord(TempDataBlocks[a].Data)+b*size), size);
+  end;
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.GetMapHeader(): TMapHeaderRec_1;
+var
+  TempDataBlocks: TDataBlocksArray;
+begin
+ ZeroMemory(@Result, SizeOf(TMapHeaderRec_1));
+
+ TempDataBlocks := GetBlocks(BLOCK_HEADER);
+
+ if TempDataBlocks = nil then Exit;
+
+ CopyMemory(@Result, TempDataBlocks[0].Data, SizeOf(TMapHeaderRec_1));
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.GetMonsters(): TMonsterRec1Array;
+var
+  TempDataBlocks: TDataBlocksArray;
+  a: Integer;
+  b, Size: LongWord;
+begin
+ Result := nil;
+
+ TempDataBlocks := GetBlocks(BLOCK_MONSTERS);
+
+ if TempDataBlocks = nil then Exit;
+
+ size := SizeOf(TMonsterRec_1);
+
+ for a := 0 to High(TempDataBlocks) do
+  for b := 0 to (TempDataBlocks[a].Block.BlockSize div size)-1 do
+  begin
+   SetLength(Result, Length(Result)+1);
+   CopyMemory(@Result[High(Result)], Pointer(LongWord(TempDataBlocks[a].Data)+b*size), size);
+  end;
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.GetPanels(): TPanelsRec1Array;
+var
+  TempDataBlocks: TDataBlocksArray;
+  a: Integer;
+  b, Size: LongWord;
+begin
+ Result := nil;
+
+ TempDataBlocks := GetBlocks(BLOCK_PANELS);
+
+ if TempDataBlocks = nil then Exit;
+
+ size := SizeOf(TPanelRec_1);
+
+ for a := 0 to High(TempDataBlocks) do
+  for b := 0 to (TempDataBlocks[a].Block.BlockSize div size)-1 do
+  begin
+   SetLength(Result, Length(Result)+1);
+   CopyMemory(@Result[High(Result)], Pointer(LongWord(TempDataBlocks[a].Data)+b*size), size);
+  end;
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.GetTextures(): TTexturesRec1Array;
+var
+  TempDataBlocks: TDataBlocksArray;
+  a: Integer;
+  b, Size: LongWord;
+begin
+ Result := nil;
+
+ TempDataBlocks := GetBlocks(BLOCK_TEXTURES);
+
+ if TempDataBlocks = nil then Exit;
+
+ size := SizeOf(TTextureRec_1);
+
+ for a := 0 to High(TempDataBlocks) do
+  for b := 0 to (TempDataBlocks[a].Block.BlockSize div size)-1 do
+  begin
+   SetLength(Result, Length(Result)+1);
+   CopyMemory(@Result[High(Result)], Pointer(LongWord(TempDataBlocks[a].Data)+b*size), size);
+  end;
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.GetTriggers(): TTriggersRec1Array;
+var
+  TempDataBlocks: TDataBlocksArray;
+  a: Integer;
+  b, Size: LongWord;
+begin
+ Result := nil;
+
+ TempDataBlocks := GetBlocks(BLOCK_TRIGGERS);
+
+ if TempDataBlocks = nil then Exit;
+
+ size := SizeOf(TTriggerRec_1);
+
+ for a := 0 to High(TempDataBlocks) do
+  for b := 0 to (TempDataBlocks[a].Block.BlockSize div size)-1 do
+  begin
+   SetLength(Result, Length(Result)+1);
+   CopyMemory(@Result[High(Result)], Pointer(LongWord(TempDataBlocks[a].Data)+b*size), size);
+  end;
+
+ TempDataBlocks := nil;
+end;
+
+function TMapReader_1.HandledVersion: Byte;
+begin
+ Result := $01;
+end;
+
+{ T M a p R e a d e r : }
+
+constructor TMapReader.Create();
+begin
+ FDataBlocks := nil;
+ FError := MAP_ERROR_NONE;
+ FVersion := $00;
+end;
+
+destructor TMapReader.Destroy();
+begin
+ FreeMap();
+
+ inherited;
+end;
+
+procedure TMapReader.FreeMap();
+var
+  a: Integer;
+begin
+ if FDataBlocks <> nil then
+  for a := 0 to High(FDataBlocks) do
+   if FDataBlocks[a].Data <> nil then FreeMem(FDataBlocks[a].Data);
+
+ FDataBlocks := nil;
+ FVersion := $00;
+ FError := MAP_ERROR_NONE;
+end;
+
+function TMapReader.GetBlocks(BlocksType: Byte): TDataBlocksArray;
+var
+  a: Integer;
+begin
+ Result := nil;
+
+ if FDataBlocks = nil then Exit;
+
+ for a := 0 to High(FDataBlocks) do
+  if FDataBlocks[a].Block.BlockType = BlocksType then
+  begin
+   SetLength(Result, Length(Result)+1);
+   Result[High(Result)] := FDataBlocks[a];
+  end;
+end;
+
+function TMapReader.HandledVersion(): Byte;
+begin
+ Result := $00;
+end;
+
+function TMapReader.LoadMap(Data: Pointer): Boolean;
+var
+  adr: LongWord;
+  _id: Integer;
+  Sign: array[0..2] of Char;
+  Ver: Byte;
+begin
+ Result := False;
+
+ CopyMemory(@Sign[0], Data, 3);
+ if Sign <> MAP_SIGNATURE then
+ begin
+  FError := MAP_ERROR_SIGNATURE;
+  Exit;
+ end;
+ adr := 3;
+
+ CopyMemory(@Ver, Pointer(LongWord(Data)+adr), 1);
+ FVersion := Ver;
+ if Ver > HandledVersion() then
+ begin
+  FError := MAP_ERROR_VERSION;
+  Exit;
+ end;
+ adr := adr+1;
+
+ repeat
+  SetLength(FDataBlocks, Length(FDataBlocks)+1);
+  _id := High(FDataBlocks);
+
+  CopyMemory(@FDataBlocks[_id].Block, Pointer(LongWord(Data)+adr), SizeOf(TBlock));
+  adr := adr+SizeOf(TBlock);
+
+  FDataBlocks[_id].Data := GetMemory(FDataBlocks[_id].Block.BlockSize);
+
+  CopyMemory(FDataBlocks[_id].Data, Pointer(LongWord(Data)+adr), FDataBlocks[_id].Block.BlockSize);
+
+  adr := adr+FDataBlocks[_id].Block.BlockSize;
+ until FDataBlocks[_id].Block.BlockType = BLOCK_NONE;
+
+ Result := True;
+end;
+
+end.
diff --git a/src/shared/MAPSTRUCT.pas b/src/shared/MAPSTRUCT.pas
new file mode 100644 (file)
index 0000000..5b26f65
--- /dev/null
@@ -0,0 +1,127 @@
+unit MAPSTRUCT;
+
+{
+-----------------------------------
+MAPSTRUCT.PAS ÂÅÐÑÈß ÎÒ 13.11.07
+
+Ïîääåðæêà êàðò âåðñèè 1
+-----------------------------------
+}
+
+{
+ Êàðòà ïðåäñòàâëÿåò ñîáîþ WAD, â êîòîðîì ðåñóðñû â êîðíå - ñîáñòâåííî ñàìè êàðòû
+ (MAP01, MAP02 è ò.ä.).
+
+ Áëîêè çàêàí÷èâàþòñÿ íóëåâûì áëîêîì (BlockType=BLOCK_NONE)
+
+ Ñòðóêòóðà êàðòû (MAP01, MAP02...):
+ --------------------------------------
+ SIGNATURE    | Byte[3]         | 'MAP'
+ VERSION      | Byte            | $01
+ BLOCK1       | TBlock          |
+ BLOCK1DATA   | RAW             |
+ ...          | ......          |
+ BLOCKN       | TBlock          |
+ BLOCKNDATA   | RAW             |
+ --------------------------------------
+
+ Ñòðóêòóðà áëîêà:
+ --------------------------------------
+ BLOCKTYPE    | Byte     | (BLOCK_TEXTURES, BLOCK_PANELS,...)
+ RESERVED     | LongWord | $00000000
+ BLOCKSIZE    | LongWord | Ñêîëüêî ýòîò áëîê â ðàçìåðå (áàéò ïîñëå record'à)
+ --------------------------------------
+}
+
+interface
+
+const
+  MAP_SIGNATURE = 'MAP';
+  BLOCK_NONE      = 0;
+  BLOCK_TEXTURES  = 1;
+  BLOCK_PANELS    = 2;
+  BLOCK_ITEMS     = 3;
+  BLOCK_AREAS     = 4;
+  BLOCK_MONSTERS  = 5;
+  BLOCK_TRIGGERS  = 6;
+  BLOCK_HEADER    = 7;
+
+type
+  Char16     = packed array[0..15] of Char;
+  Char32     = packed array[0..31] of Char;
+  Char64     = packed array[0..63] of Char;
+  Char100    = packed array[0..99] of Char;
+  Char256    = packed array[0..255] of Char;
+  Byte128    = packed array[0..127] of Byte;
+
+  TMapHeaderRec_1 = packed record
+   MapName:        Char32;
+   MapAuthor:      Char32;
+   MapDescription: Char256;
+   MusicName:      Char64;
+   SkyName:        Char64;
+   Width:          Word;
+   Height:         Word;
+  end;
+
+  TTextureRec_1 = packed record
+   Resource: Char64;
+   Anim:     Byte;
+  end;
+
+  TPanelRec_1 = packed record
+   X, Y:       Integer;
+   Width,
+   Height:     Word;
+   TextureNum: Word;
+   PanelType:  Word;
+   Alpha:      Byte;
+   Flags:      Byte;
+  end;
+
+  TItemRec_1 = packed record
+   X, Y:     Integer;
+   ItemType: Byte;
+   Options:  Byte;
+  end;
+
+  TMonsterRec_1 = packed record
+   X, Y:        Integer;
+   MonsterType: Byte;
+   Direction:   Byte;
+  end;
+
+  TAreaRec_1 = packed record
+   X, Y:      Integer;
+   AreaType:  Byte;
+   Direction: Byte;
+  end;
+
+  TTriggerRec_1 = packed record
+   X, Y:         Integer;
+   Width,
+   Height:       Word;
+   Enabled:      Byte;
+   TexturePanel: Integer;
+   TriggerType:  Byte;
+   ActivateType: Byte;
+   Keys:         Byte; 
+   DATA:         Byte128;
+  end;
+
+  TBlock = packed record
+   BlockType: Byte;
+   Reserved:  LongWord;
+   BlockSize: LongWord;
+  end;
+
+  TTexturesRec1Array = array of TTextureRec_1;
+  TPanelsRec1Array = array of TPanelRec_1;
+  TItemsRec1Array = array of TItemRec_1;
+  TMonsterRec1Array = array of TMonsterRec_1;
+  TAreasRec1Array = array of TAreaRec_1;
+  TTriggersRec1Array = array of TTriggerRec_1;
+
+implementation
+
+end.
diff --git a/src/shared/MAPWRITER.pas b/src/shared/MAPWRITER.pas
new file mode 100644 (file)
index 0000000..39ca417
--- /dev/null
@@ -0,0 +1,323 @@
+unit MAPWRITER;
+
+{
+-----------------------------------
+MAPWRITER.PAS ÂÅÐÑÈß ÎÒ 24.09.06
+
+Ïîääåðæêà êàðò âåðñèè 1
+-----------------------------------
+}
+
+interface
+
+uses
+  MAPSTRUCT;
+
+type
+  TDataBlock = packed record
+   Block: TBlock;
+   Data:  Pointer;
+  end;
+
+  TDataBlocksArray = packed array of TDataBlock;
+
+  TMapWriter = class(TObject)
+   private
+    FDataBlocks: TDataBlocksArray;
+   public
+    constructor Create();
+    destructor Destroy(); override;
+    procedure FreeMap();
+    function SaveMap(var Data: Pointer): LongWord;
+    function HandledVersion(): Byte; virtual;
+  end;
+
+  TMapWriter_1 = class(TMapWriter)
+   public
+    function AddTextures(Textures: TTexturesRec1Array): Boolean;
+    function AddPanels(Panels: TPanelsRec1Array): Boolean;
+    function AddItems(Items: TItemsRec1Array): Boolean;
+    function AddMonsters(Monsters: TMonsterRec1Array): Boolean;
+    function AddAreas(Areas: TAreasRec1Array): Boolean;
+    function AddTriggers(Triggers: TTriggersRec1Array): Boolean;
+    function AddHeader(MapHeader: TMapHeaderRec_1): Boolean;
+    function HandledVersion(): Byte; override;
+  end;
+
+
+implementation
+
+uses
+  BinEditor, SysUtils;
+
+{ TMapWriter }
+
+constructor TMapWriter.Create();
+begin
+ FDataBlocks := nil;
+end;
+
+destructor TMapWriter.Destroy();
+begin
+ FreeMap();
+
+ inherited;
+end;
+
+procedure TMapWriter.FreeMap();
+var
+  a: Integer;
+begin
+ if FDataBlocks <> nil then
+  for a := 0 to High(FDataBlocks) do
+   if FDataBlocks[a].Data <> nil then FreeMem(FDataBlocks[a].Data);
+
+ FDataBlocks := nil;
+end;
+
+function TMapWriter.SaveMap(var Data: Pointer): LongWord;
+var
+  a: Integer;
+  b, c: LongWord;
+  Sign: array[0..2] of Char;
+  Ver: Byte;
+begin
+ b := 3+1+SizeOf(TBlock)*(Length(FDataBlocks)+1);
+
+ if FDataBlocks <> nil then
+  for a := 0 to High(FDataBlocks) do
+   b := b+FDataBlocks[a].Block.BlockSize;
+
+ Result := b;
+
+ GetMem(Data, b);
+
+ Sign := MAP_SIGNATURE;
+ CopyMemory(Data, @Sign[0], 3);
+ c := 3;
+
+ Ver := HandledVersion();
+ CopyMemory(Pointer(LongWord(Data)+c), @Ver, 1);
+ c := c+1;
+
+ if FDataBlocks <> nil then
+  for a := 0 to High(FDataBlocks) do
+  begin
+   CopyMemory(Pointer(LongWord(Data)+c), @FDataBlocks[a].Block, SizeOf(TBlock));
+   c := c+SizeOf(TBlock);
+   CopyMemory(Pointer(LongWord(Data)+c), FDataBlocks[a].Data, FDataBlocks[a].Block.BlockSize);
+   c := c+FDataBlocks[a].Block.BlockSize;
+  end;
+
+ ZeroMemory(Pointer(LongWord(Data)+c), SizeOf(TBlock));
+end;
+
+function TMapWriter.HandledVersion(): Byte;
+begin
+ Result := $00;
+end;
+
+{ TMapWriter_1 }
+
+function TMapWriter_1.AddAreas(Areas: TAreasRec1Array): Boolean;
+var
+  a, size: LongWord;
+begin
+ if Areas = nil then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TAreaRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_AREAS;
+  Block.Reserved := $00000000;
+  Block.BlockSize := LongWord(Length(Areas))*size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  for a := 0 to High(Areas) do
+   CopyMemory(Pointer(LongWord(Data)+a*Size), @Areas[a], size);
+ end;
+ Result := True;
+end;
+
+function TMapWriter_1.AddItems(Items: TItemsRec1Array): Boolean;
+var
+  a, size: LongWord;
+begin
+ if Items = nil then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TItemRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_ITEMS;
+  Block.Reserved := $00000000;
+  Block.BlockSize := LongWord(Length(Items))*size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  for a := 0 to High(Items) do
+   CopyMemory(Pointer(LongWord(Data)+a*size), @Items[a], size);
+ end;
+ Result := True;
+end;
+
+function TMapWriter_1.AddMonsters(Monsters: TMonsterRec1Array): Boolean;
+var
+  a, size: LongWord;
+begin
+ if Monsters = nil then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TMonsterRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_MONSTERS;
+  Block.Reserved := $00000000;
+  Block.BlockSize := LongWord(Length(Monsters))*size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  for a := 0 to High(Monsters) do
+   CopyMemory(Pointer(LongWord(Data)+a*Size), @Monsters[a], size);
+ end;
+ Result := True;
+end;
+
+function TMapWriter_1.AddPanels(Panels: TPanelsRec1Array): Boolean;
+var
+  a, size: LongWord;
+begin
+ if Panels = nil then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TPanelRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_PANELS;
+  Block.Reserved := $00000000;
+  Block.BlockSize := LongWord(Length(Panels))*size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  for a := 0 to High(Panels) do
+   CopyMemory(Pointer(LongWord(Data)+a*size), @Panels[a], size);
+ end;
+ Result := True;
+end;
+
+function TMapWriter_1.AddTextures(Textures: TTexturesRec1Array): Boolean;
+var
+  a, size: LongWord;
+begin
+ if Textures = nil then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TTextureRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_TEXTURES;
+  Block.Reserved := $00000000;
+  Block.BlockSize := LongWord(Length(Textures))*size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  for a := 0 to High(Textures) do
+   CopyMemory(Pointer(LongWord(Data)+a*size), @Textures[a], size);
+ end;
+
+ Result := True;
+end;
+
+function TMapWriter_1.AddTriggers(Triggers: TTriggersRec1Array): Boolean;
+var
+  a, size: LongWord;
+begin
+ if Triggers = nil then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TTriggerRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_TRIGGERS;
+  Block.Reserved := $00000000;
+  Block.BlockSize := LongWord(Length(Triggers))*size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  for a := 0 to High(Triggers) do
+   CopyMemory(Pointer(LongWord(Data)+a*size), @Triggers[a], size);
+ end;
+
+ Result := True;
+end;
+
+function TMapWriter_1.AddHeader(MapHeader: TMapHeaderRec_1): Boolean;
+var
+  size: LongWord;
+begin
+ SetLength(FDataBlocks, Length(FDataBlocks)+1);
+
+ size := SizeOf(TMapHeaderRec_1);
+
+ with FDataBlocks[High(FDataBlocks)] do
+ begin
+  Block.BlockType := BLOCK_HEADER;
+  Block.Reserved := $00000000;
+  Block.BlockSize := size;
+
+  Data := GetMemory(Block.BlockSize);
+
+  CopyMemory(Pointer(LongWord(Data)), @MapHeader, size);
+ end;
+ Result := True;
+end;
+
+function TMapWriter_1.HandledVersion(): Byte;
+begin
+ Result := $01;
+end;
+
+end.
diff --git a/src/shared/WADEDITOR.pas b/src/shared/WADEDITOR.pas
new file mode 100644 (file)
index 0000000..3c6c1a3
--- /dev/null
@@ -0,0 +1,879 @@
+unit WADEDITOR;
+
+{
+-----------------------------------
+WADEDITOR.PAS ÂÅÐÑÈß ÎÒ 26.08.08
+
+Ïîääåðæêà âàäîâ âåðñèè 1
+-----------------------------------
+}
+
+interface
+
+uses WADSTRUCT;
+
+type
+  SArray = array of ShortString;
+
+  TWADEditor_1 = class(TObject)
+   private
+    FResData:   Pointer;
+    FResTable:  packed array of TResourceTableRec_1;
+    FHeader:    TWADHeaderRec_1;
+    FDataSize:  LongWord;
+    FOffset:    LongWord;
+    FFileName:  string;
+    FWADOpened: Byte;
+    FLastError: Integer;
+    FVersion:   Byte;
+    function LastErrorString(): string;
+    function GetResName(ResName: string): Char16;
+   public
+    constructor Create();
+    destructor Destroy(); override;
+    procedure FreeWAD();
+    function  ReadFile(FileName: string): Boolean;
+    function  ReadMemory(Data: Pointer; Len: LongWord): Boolean;
+    procedure CreateImage();
+    function AddResource(Data: Pointer; Len: LongWord; Name: string;
+                         Section: string): Boolean; overload;
+    function AddResource(FileName, Name, Section: string): Boolean; overload;
+    function AddAlias(Res, Alias: string): Boolean;
+    procedure AddSection(Name: string);
+    procedure RemoveResource(Section, Resource: string);
+    procedure SaveTo(FileName: string);
+    function HaveResource(Section, Resource: string): Boolean;
+    function HaveSection(Section: string): Boolean;
+    function GetResource(Section, Resource: string; var pData: Pointer;
+                         var Len: Integer): Boolean;
+    function GetSectionList(): SArray;
+    function GetResourcesList(Section: string): SArray;
+
+    property GetLastError: Integer read FLastError;
+    property GetLastErrorStr: string read LastErrorString;
+    property GetResourcesCount: Word read FHeader.RecordsCount;
+    property GetVersion: Byte read FVersion;
+  end;
+
+const
+  DFWAD_NOERROR                = 0;
+  DFWAD_ERROR_WADNOTFOUND      = -1;
+  DFWAD_ERROR_CANTOPENWAD      = -2;
+  DFWAD_ERROR_RESOURCENOTFOUND = -3;
+  DFWAD_ERROR_FILENOTWAD       = -4;
+  DFWAD_ERROR_WADNOTLOADED     = -5;
+  DFWAD_ERROR_READRESOURCE     = -6;
+  DFWAD_ERROR_READWAD          = -7;
+  DFWAD_ERROR_WRONGVERSION     = -8;
+
+
+ procedure g_ProcessResourceStr(ResourceStr: String; var FileName,
+                                SectionName, ResourceName: String); overload;
+ procedure g_ProcessResourceStr(ResourceStr: String; FileName,
+                                SectionName, ResourceName: PString); overload;
+
+implementation
+
+uses
+  SysUtils, BinEditor, ZLib;
+
+const
+  DFWAD_OPENED_NONE   = 0;
+  DFWAD_OPENED_FILE   = 1;
+  DFWAD_OPENED_MEMORY = 2;
+
+procedure DecompressBuf(const InBuf: Pointer; InBytes: Integer;
+  OutEstimate: Integer; out OutBuf: Pointer; out OutBytes: Integer);
+var
+  strm: TZStreamRec;
+  P: Pointer;
+  BufInc: Integer;
+begin
+  FillChar(strm, sizeof(strm), 0);
+  BufInc := (InBytes + 255) and not 255;
+  if OutEstimate = 0 then
+    OutBytes := BufInc
+  else
+    OutBytes := OutEstimate;
+  GetMem(OutBuf, OutBytes);
+  try
+    strm.next_in := InBuf;
+    strm.avail_in := InBytes;
+    strm.next_out := OutBuf;
+    strm.avail_out := OutBytes;
+    inflateInit_(strm, zlib_version, sizeof(strm));
+    try
+      while inflate(strm, Z_FINISH) <> Z_STREAM_END do
+      begin
+        P := OutBuf;
+        Inc(OutBytes, BufInc);
+        ReallocMem(OutBuf, OutBytes);
+        strm.next_out := PByteF(PChar(OutBuf) + (PChar(strm.next_out) - PChar(P)));
+        strm.avail_out := BufInc;
+      end;
+    finally
+      inflateEnd(strm);
+    end;
+    ReallocMem(OutBuf, strm.total_out);
+    OutBytes := strm.total_out;
+  except
+    FreeMem(OutBuf);
+    raise
+  end;
+end;
+
+procedure g_ProcessResourceStr(ResourceStr: String; var FileName,
+                               SectionName, ResourceName: String);
+var
+  a, i: Integer;
+
+begin
+  for i := Length(ResourceStr) downto 1 do
+    if ResourceStr[i] = ':' then
+      Break;
+
+  FileName := Copy(ResourceStr, 1, i-1);
+
+  for a := i+1 to Length(ResourceStr) do
+    if (ResourceStr[a] = '\') or (ResourceStr[a] = '/') then Break;
+
+  ResourceName := Copy(ResourceStr, a+1, Length(ResourceStr)-Abs(a));
+  SectionName := Copy(ResourceStr, i+1, Length(ResourceStr)-Length(ResourceName)-Length(FileName)-2);
+end;
+
+procedure g_ProcessResourceStr(ResourceStr: AnsiString; FileName,
+                               SectionName, ResourceName: PAnsiString);
+var
+  a, i, l1, l2: Integer;
+
+begin
+  for i := Length(ResourceStr) downto 1 do
+    if ResourceStr[i] = ':' then
+      Break;
+
+  if FileName <> nil then
+    begin
+      FileName^ := Copy(ResourceStr, 1, i-1);
+      l1 := Length(FileName^);
+    end
+  else
+    l1 := 0;
+
+  for a := i+1 to Length(ResourceStr) do
+    if (ResourceStr[a] = '\') or (ResourceStr[a] = '/') then Break;
+
+  if ResourceName <> nil then
+    begin
+      ResourceName^ := Copy(ResourceStr, a+1, Length(ResourceStr)-Abs(a));
+      l2 := Length(ResourceName^);
+    end
+  else
+    l2 := 0;
+
+  if SectionName <> nil then
+    SectionName^ := Copy(ResourceStr, i+1, Length(ResourceStr)-l2-l1-2);
+end;
+
+{ TWADEditor_1 }
+
+function TWADEditor_1.AddResource(Data: Pointer; Len: LongWord; Name: string;
+                                  Section: string): Boolean;
+var
+  ResCompressed: Pointer;
+  ResCompressedSize: Integer;
+  a, b: Integer;
+begin
+ Result := False;
+
+ SetLength(FResTable, Length(FResTable)+1);
+
+ if Section = '' then
+ begin
+  if Length(FResTable) > 1 then
+   for a := High(FResTable) downto 1 do
+    FResTable[a] := FResTable[a-1];
+
+  a := 0;
+ end
+  else
+ begin
+  Section := AnsiUpperCase(Section);
+  b := -1;
+
+  for a := 0 to High(FResTable) do
+   if (FResTable[a].Length = 0) and (FResTable[a].ResourceName = Section) then
+   begin
+    for b := High(FResTable) downto a+2 do
+     FResTable[b] := FResTable[b-1];
+
+    b := a+1;
+    Break;
+   end;
+
+  if b = -1 then
+  begin
+   SetLength(FResTable, Length(FResTable)-1);
+   Exit;
+  end;
+  a := b;
+ end;
+
+ ResCompressed := nil;
+ ResCompressedSize := 0;
+ Compress(Data, @Len, ResCompressed, ResCompressedSize);
+ if ResCompressed = nil then Exit;
+
+ if FResData = nil then FResData := AllocMem(ResCompressedSize)
+  else ReallocMem(FResData, FDataSize+Cardinal(ResCompressedSize));
+
+ FDataSize := FDataSize+LongWord(ResCompressedSize);
+
+ CopyMemory(Pointer(PChar(FResData)+FDataSize-PChar(ResCompressedSize)),
+            ResCompressed, ResCompressedSize);
+ FreeMemory(ResCompressed);
+
+ Inc(FHeader.RecordsCount);
+
+ with FResTable[a] do
+ begin
+  ResourceName := GetResName(Name);
+  Address := FOffset;
+  Length := ResCompressedSize;
+ end;
+
+ FOffset := FOffset+Cardinal(ResCompressedSize);
+
+ Result := True;
+end;
+
+function TWADEditor_1.AddAlias(Res, Alias: string): Boolean;
+var
+  a, b: Integer;
+  ares: Char16;
+begin
+ Result := False;
+
+ if FResTable = nil then Exit;
+
+ b := -1;
+ ares := GetResName(Alias);
+ for a := 0 to High(FResTable) do
+  if FResTable[a].ResourceName = Res then
+  begin
+   b := a;
+   Break;
+  end;
+
+ if b = -1 then Exit;
+
+ Inc(FHeader.RecordsCount);
+
+ SetLength(FResTable, Length(FResTable)+1);
+
+ with FResTable[High(FResTable)] do
+ begin
+  ResourceName := ares;
+  Address := FResTable[b].Address;
+  Length := FResTable[b].Length;
+ end;
+
+ Result := True;
+end;
+
+function TWADEditor_1.AddResource(FileName, Name, Section: string): Boolean;
+var
+  ResCompressed: Pointer;
+  ResCompressedSize: Integer;
+  ResourceFile: File;
+  TempResource: Pointer;
+  OriginalSize: Integer;
+  a, b: Integer;
+begin
+ Result := False;
+
+ AssignFile(ResourceFile, FileName);
+
+ try
+  Reset(ResourceFile, 1);
+ except
+  FLastError := DFWAD_ERROR_CANTOPENWAD;
+  Exit;
+ end;
+
+ OriginalSize := FileSize(ResourceFile);
+ GetMem(TempResource, OriginalSize);
+
+ try
+  BlockRead(ResourceFile, TempResource^, OriginalSize);
+ except
+  FLastError := DFWAD_ERROR_READWAD;
+  FreeMemory(TempResource);
+  CloseFile(ResourceFile);
+  Exit;
+ end;
+
+ CloseFile(ResourceFile);
+
+ ResCompressed := nil;
+ ResCompressedSize := 0;
+ Compress(TempResource, @OriginalSize, ResCompressed, ResCompressedSize);
+ FreeMemory(TempResource);
+ if ResCompressed = nil then Exit;
+
+ SetLength(FResTable, Length(FResTable)+1);
+
+ if Section = '' then
+ begin
+  if Length(FResTable) > 1 then
+   for a := High(FResTable) downto 1 do
+    FResTable[a] := FResTable[a-1];
+
+  a := 0;
+ end
+  else
+ begin
+  Section := AnsiUpperCase(Section);
+  b := -1;
+
+  for a := 0 to High(FResTable) do
+   if (FResTable[a].Length = 0) and (FResTable[a].ResourceName = Section) then
+   begin
+    for b := High(FResTable) downto a+2 do
+     FResTable[b] := FResTable[b-1];
+
+    b := a+1;
+    Break;
+   end;
+
+  if b = -1 then
+  begin
+   FreeMemory(ResCompressed);
+   SetLength(FResTable, Length(FResTable)-1);
+   Exit;
+  end;
+
+  a := b;
+ end;
+
+ if FResData = nil then FResData := AllocMem(ResCompressedSize)
+  else ReallocMem(FResData, FDataSize+Cardinal(ResCompressedSize));
+
+ FDataSize := FDataSize+LongWord(ResCompressedSize);
+ CopyMemory(Pointer(PChar(FResData)+FDataSize-PChar(ResCompressedSize)),
+            ResCompressed, ResCompressedSize);
+ FreeMemory(ResCompressed);
+
+ Inc(FHeader.RecordsCount);
+
+ with FResTable[a] do
+ begin
+  ResourceName := GetResName(Name);
+  Address := FOffset;
+  Length := ResCompressedSize;
+ end;
+
+ FOffset := FOffset+Cardinal(ResCompressedSize);
+
+ Result := True;
+end;
+
+procedure TWADEditor_1.AddSection(Name: string);
+begin
+ if Name = '' then Exit;
+
+ Inc(FHeader.RecordsCount);
+
+ SetLength(FResTable, Length(FResTable)+1);
+ with FResTable[High(FResTable)] do
+ begin
+  ResourceName := GetResName(Name);
+  Address := $00000000;
+  Length := $00000000;
+ end;
+end;
+
+constructor TWADEditor_1.Create();
+begin
+ FResData := nil;
+ FResTable := nil;
+ FDataSize := 0;
+ FOffset := 0;
+ FHeader.RecordsCount := 0;
+ FFileName := '';
+ FWADOpened := DFWAD_OPENED_NONE;
+ FLastError := DFWAD_NOERROR;
+ FVersion := DFWAD_VERSION;
+end;
+
+procedure TWADEditor_1.CreateImage();
+var
+  WADFile: File;
+  b: LongWord;
+begin
+ if FWADOpened = DFWAD_OPENED_NONE then
+ begin
+  FLastError := DFWAD_ERROR_WADNOTLOADED;
+  Exit;
+ end;
+
+ if FWADOpened = DFWAD_OPENED_MEMORY then Exit;
+
+ if FResData <> nil then FreeMem(FResData);
+
+ try
+  AssignFile(WADFile, FFileName);
+  Reset(WADFile, 1);
+
+  b := 6+SizeOf(TWADHeaderRec_1)+SizeOf(TResourceTableRec_1)*Length(FResTable);
+
+  FDataSize := LongWord(FileSize(WADFile))-b;
+
+  GetMem(FResData, FDataSize);
+
+  Seek(WADFile, b);
+  BlockRead(WADFile, FResData^, FDataSize);
+
+  CloseFile(WADFile);
+
+  FOffset := FDataSize;
+ except
+  FLastError := DFWAD_ERROR_CANTOPENWAD;
+  CloseFile(WADFile);
+  Exit;
+ end;
+
+ FLastError := DFWAD_NOERROR;
+end;
+
+destructor TWADEditor_1.Destroy();
+begin
+ FreeWAD();
+
+ inherited;
+end;
+
+procedure TWADEditor_1.FreeWAD();
+begin
+ if FResData <> nil then FreeMem(FResData);
+ FResTable := nil;
+ FDataSize := 0;
+ FOffset := 0;
+ FHeader.RecordsCount := 0;
+ FFileName := '';
+ FWADOpened := DFWAD_OPENED_NONE;
+ FLastError := DFWAD_NOERROR;
+ FVersion := DFWAD_VERSION;
+end;
+
+function TWADEditor_1.GetResName(ResName: string): Char16;
+begin
+ ZeroMemory(@Result[0], 16);
+ if ResName = '' then Exit;
+
+ ResName := Trim(UpperCase(ResName));
+ if Length(ResName) > 16 then SetLength(ResName, 16);
+
+ CopyMemory(@Result[0], @ResName[1], Length(ResName));
+end;
+
+function TWADEditor_1.HaveResource(Section, Resource: string): Boolean;
+var
+  a: Integer;
+  CurrentSection: string;
+begin
+ Result := False;
+
+ if FResTable = nil then Exit;
+
+ CurrentSection := '';
+ Section := AnsiUpperCase(Section);
+ Resource := AnsiUpperCase(Resource);
+
+ for a := 0 to High(FResTable) do
+ begin
+  if FResTable[a].Length = 0 then
+  begin
+   CurrentSection := FResTable[a].ResourceName;
+   Continue;
+  end;
+
+  if (FResTable[a].ResourceName = Resource) and
+     (CurrentSection = Section) then
+  begin
+   Result := True;
+   Break;
+  end;
+ end;
+end;
+
+function TWADEditor_1.HaveSection(Section: string): Boolean;
+var
+  a: Integer;
+begin
+ Result := False;
+
+ if FResTable = nil then Exit;
+ if Section = '' then
+ begin
+  Result := True;
+  Exit;
+ end;
+
+ Section := AnsiUpperCase(Section);
+
+ for a := 0 to High(FResTable) do
+  if (FResTable[a].Length = 0) and (FResTable[a].ResourceName = Section) then
+  begin
+   Result := True;
+   Exit;
+  end;
+end;
+
+function TWADEditor_1.GetResource(Section, Resource: string;
+  var pData: Pointer; var Len: Integer): Boolean;
+var
+  a: LongWord;
+  i: Integer;
+  WADFile: File;
+  CurrentSection: string;
+  TempData: Pointer;
+  OutBytes: Integer;
+begin
+ Result := False;
+
+ CurrentSection := '';
+
+ if FWADOpened = DFWAD_OPENED_NONE then
+ begin
+  FLastError := DFWAD_ERROR_WADNOTLOADED;
+  Exit;
+ end;
+
+ Section := UpperCase(Section);
+ Resource := UpperCase(Resource);
+
+ i := -1;
+ for a := 0 to High(FResTable) do
+ begin
+  if FResTable[a].Length = 0 then
+  begin
+   CurrentSection := FResTable[a].ResourceName;
+   Continue;
+  end;
+
+  if (FResTable[a].ResourceName = Resource) and
+     (CurrentSection = Section) then
+  begin
+   i := a;
+   Break;
+  end;
+ end;
+
+ if i = -1 then
+ begin
+  FLastError := DFWAD_ERROR_RESOURCENOTFOUND;
+  Exit;
+ end;
+
+ if FWADOpened = DFWAD_OPENED_FILE then
+ begin
+  try
+   AssignFile(WADFile, FFileName);
+   Reset(WADFile, 1);
+
+   Seek(WADFile, FResTable[i].Address+6+
+        LongWord(SizeOf(TWADHeaderRec_1)+SizeOf(TResourceTableRec_1)*Length(FResTable)));
+   TempData := GetMemory(FResTable[i].Length);
+   BlockRead(WADFile, TempData^, FResTable[i].Length);
+   DecompressBuf(TempData, FResTable[i].Length, 0, pData, OutBytes);
+   FreeMem(TempData);
+
+   Len := OutBytes;
+
+   CloseFile(WADFile);
+  except
+   FLastError := DFWAD_ERROR_CANTOPENWAD;
+   CloseFile(WADFile);
+   Exit;
+  end;
+ end
+  else
+ begin
+  TempData := GetMemory(FResTable[i].Length);
+  CopyMemory(TempData, Pointer(LongWord(FResData)+FResTable[i].Address+6+
+             LongWord(SizeOf(TWADHeaderRec_1)+SizeOf(TResourceTableRec_1)*Length(FResTable))),
+             FResTable[i].Length);
+  DecompressBuf(TempData, FResTable[i].Length, 0, pData, OutBytes);
+  FreeMem(TempData);
+
+  Len := OutBytes;
+ end;
+
+ FLastError := DFWAD_NOERROR;
+ Result := True;
+end;
+
+function TWADEditor_1.GetResourcesList(Section: string): SArray;
+var
+  a: Integer;
+  CurrentSection: Char16;
+begin
+ Result := nil;
+
+ if FResTable = nil then Exit;
+ if Length(Section) > 16 then Exit;
+
+ CurrentSection := '';
+
+ for a := 0 to High(FResTable) do
+ begin
+  if FResTable[a].Length = 0 then
+  begin
+   CurrentSection := FResTable[a].ResourceName;
+   Continue;
+  end;
+
+  if CurrentSection = Section then
+  begin
+   SetLength(Result, Length(Result)+1);
+   Result[High(Result)] := FResTable[a].ResourceName;
+  end;
+ end;
+end;
+
+function TWADEditor_1.GetSectionList(): SArray;
+var
+  i: DWORD;
+begin
+ Result := nil;
+
+ if FResTable = nil then Exit;
+
+ if FResTable[0].Length <> 0 then
+ begin
+  SetLength(Result, 1);
+  Result[0] := '';
+ end;
+
+ for i := 0 to High(FResTable) do
+  if FResTable[i].Length = 0 then
+  begin
+   SetLength(Result, Length(Result)+1);
+   Result[High(Result)] := FResTable[i].ResourceName;
+  end;
+end;
+
+function TWADEditor_1.LastErrorString(): string;
+begin
+ case FLastError of
+  DFWAD_NOERROR: Result := '';
+  DFWAD_ERROR_WADNOTFOUND: Result := 'DFWAD file not found';
+  DFWAD_ERROR_CANTOPENWAD: Result := 'Can''t open DFWAD file';
+  DFWAD_ERROR_RESOURCENOTFOUND: Result := 'Resource not found';
+  DFWAD_ERROR_FILENOTWAD: Result := 'File is not DFWAD';
+  DFWAD_ERROR_WADNOTLOADED: Result := 'DFWAD file is not loaded';
+  DFWAD_ERROR_READRESOURCE: Result := 'Read resource error';
+  DFWAD_ERROR_READWAD: Result := 'Read DFWAD error';
+ end;
+end;
+
+function TWADEditor_1.ReadFile(FileName: string): Boolean;
+var
+  WADFile: File;
+  Signature: array[0..4] of Char;
+  a: Integer;
+begin
+ FreeWAD();
+
+ Result := False;
+
+ if not FileExists(FileName) then
+ begin
+  FLastError := DFWAD_ERROR_WADNOTFOUND;
+  Exit;
+ end;
+
+ FFileName := FileName;
+
+ AssignFile(WADFile, FFileName);
+
+ try
+  Reset(WADFile, 1);
+ except
+  FLastError := DFWAD_ERROR_CANTOPENWAD;
+  Exit;
+ end;
+
+ try
+  BlockRead(WADFile, Signature, 5);
+  if Signature <> DFWAD_SIGNATURE then
+  begin
+   FLastError := DFWAD_ERROR_FILENOTWAD;
+   CloseFile(WADFile);
+   Exit;
+  end;
+
+  BlockRead(WADFile, FVersion, 1);
+  if FVersion <> DFWAD_VERSION then
+  begin
+    FLastError := DFWAD_ERROR_WRONGVERSION;
+    CloseFile(WADFile);
+    Exit;
+  end;
+
+  BlockRead(WADFile, FHeader, SizeOf(TWADHeaderRec_1));
+  SetLength(FResTable, FHeader.RecordsCount);
+  if FResTable <> nil then
+  begin
+   BlockRead(WADFile, FResTable[0], SizeOf(TResourceTableRec_1)*FHeader.RecordsCount);
+
+   for a := 0 to High(FResTable) do
+    if FResTable[a].Length <> 0 then
+     FResTable[a].Address := FResTable[a].Address-6-(LongWord(SizeOf(TWADHeaderRec_1)+
+                             SizeOf(TResourceTableRec_1)*Length(FResTable)));
+  end;
+
+  CloseFile(WADFile);
+ except
+  FLastError := DFWAD_ERROR_READWAD;
+  CloseFile(WADFile);
+  Exit;
+ end;
+
+ FWADOpened := DFWAD_OPENED_FILE;
+ FLastError := DFWAD_NOERROR;
+ Result := True;
+end;
+
+function TWADEditor_1.ReadMemory(Data: Pointer; Len: LongWord): Boolean;
+var
+  Signature: array[0..4] of Char;
+  a: Integer;
+begin
+ FreeWAD();
+
+ Result := False;
+
+ CopyMemory(@Signature[0], Data, 5);
+ if Signature <> DFWAD_SIGNATURE then
+ begin
+  FLastError := DFWAD_ERROR_FILENOTWAD;
+  Exit;
+ end;
+
+ CopyMemory(@FVersion, Pointer(LongWord(Data)+5), 1);
+ if FVersion <> DFWAD_VERSION then
+ begin
+   FLastError := DFWAD_ERROR_WRONGVERSION;
+   Exit;
+ end;
+
+ CopyMemory(@FHeader, Pointer(LongWord(Data)+6), SizeOf(TWADHeaderRec_1));
+
+ SetLength(FResTable, FHeader.RecordsCount);
+ if FResTable <> nil then
+ begin
+  CopyMemory(@FResTable[0], Pointer(LongWord(Data)+6+SizeOf(TWADHeaderRec_1)),
+             SizeOf(TResourceTableRec_1)*FHeader.RecordsCount);
+
+  for a := 0 to High(FResTable) do
+   if FResTable[a].Length <> 0 then
+    FResTable[a].Address := FResTable[a].Address-6-(LongWord(SizeOf(TWADHeaderRec_1)+
+                            SizeOf(TResourceTableRec_1)*Length(FResTable)));
+ end;
+
+ GetMem(FResData, Len);
+ CopyMemory(FResData, Data, Len);
+
+ FWADOpened := DFWAD_OPENED_MEMORY;
+ FLastError := DFWAD_NOERROR;
+
+ Result := True;
+end;
+
+procedure TWADEditor_1.RemoveResource(Section, Resource: string);
+var
+  a, i: Integer;
+  CurrentSection: Char16;
+  b, c, d: LongWord;
+begin
+ if FResTable = nil then Exit;
+
+ i := -1;
+ b := 0;
+ c := 0;
+ CurrentSection := '';
+
+ for a := 0 to High(FResTable) do
+ begin
+  if FResTable[a].Length = 0 then
+  begin
+   CurrentSection := FResTable[a].ResourceName;
+   Continue;
+  end;
+
+  if (FResTable[a].ResourceName = Resource) and
+     (CurrentSection = Section) then
+  begin
+   i := a;
+   b := FResTable[a].Length;
+   c := FResTable[a].Address;
+   Break;
+  end;
+ end;
+
+ if i = -1 then Exit;
+
+ for a := i to High(FResTable)-1 do
+  FResTable[a] := FResTable[a+1];
+
+ SetLength(FResTable, Length(FResTable)-1);
+
+ d := 0;
+ for a := 0 to High(FResTable) do
+  if (FResTable[a].Length <> 0) and (FResTable[a].Address > c) then
+  begin
+   FResTable[a].Address := FResTable[a].Address-b;
+   d := d+FResTable[a].Length;
+  end;
+
+ CopyMemory(Pointer(LongWord(FResData)+c), Pointer(LongWord(FResData)+c+b), d);
+
+ FDataSize := FDataSize-b;
+ FOffset := FOffset-b;
+ ReallocMem(FResData, FDataSize);
+
+ FHeader.RecordsCount := FHeader.RecordsCount-1;
+end;
+
+procedure TWADEditor_1.SaveTo(FileName: string);
+var
+  WADFile: File;
+  sign: string;
+  ver: Byte;
+  Header: TWADHeaderRec_1;
+  i: Integer;
+begin
+ sign := DFWAD_SIGNATURE;
+ ver := DFWAD_VERSION;
+
+ Header.RecordsCount := Length(FResTable);
+
+ if FResTable <> nil then
+  for i := 0 to High(FResTable) do
+   if FResTable[i].Length <> 0 then
+    FResTable[i].Address := FResTable[i].Address+6+SizeOf(TWADHeaderRec_1)+
+                            SizeOf(TResourceTableRec_1)*Header.RecordsCount;
+
+ AssignFile(WADFile, FileName);
+ Rewrite(WADFile, 1);
+  BlockWrite(WADFile, sign[1], 5);
+  BlockWrite(WADFile, ver, 1);
+  BlockWrite(WADFile, Header, SizeOf(TWADHeaderRec_1));
+  if FResTable <> nil then BlockWrite(WADFile, FResTable[0],
+                                      SizeOf(TResourceTableRec_1)*Header.RecordsCount);
+  if FResData <> nil then BlockWrite(WADFile, FResData^, FDataSize);
+ CloseFile(WADFile);
+end;
+
+end.
diff --git a/src/shared/WADSTRUCT.pas b/src/shared/WADSTRUCT.pas
new file mode 100644 (file)
index 0000000..d7d7b06
--- /dev/null
@@ -0,0 +1,43 @@
+unit WADSTRUCT;
+
+{
+-----------------------------------
+WADSTRUCT.PAS ÂÅÐÑÈß ÎÒ 24.09.06
+
+Ïîääåðæêà âàäîâ âåðñèè 1
+-----------------------------------
+
+Ñòðóêòóðà DFWAD-ôàéëà âåðñèè 1:
+ ------------------------------------------
+ SIGNATURE  | Byte[5]             | 'DFWAD'
+ VERSION    | Byte                | $01
+ HEADER     | TWADHeaderRec_1     |
+ RESRECORD1 | TResourceTableRec_1 |
+ ...        | ................... |
+ RESRECORDN | TResourceTableRec_1 |
+ DATA       | RAW                 |
+ ------------------------------------------
+}
+
+interface
+
+type
+  Char16 = packed array[0..15] of Char;
+    
+  TWADHeaderRec_1 = packed record
+   RecordsCount: Word;
+  end;
+
+  TResourceTableRec_1 = packed record
+   ResourceName: Char16;
+   Address:      LongWord;
+   Length:       LongWord;
+  end;
+
+const
+  DFWAD_SIGNATURE = 'DFWAD';
+  DFWAD_VERSION   = $01;
+
+implementation
+
+end.
diff --git a/tmp/.gitignore b/tmp/.gitignore
new file mode 100644 (file)
index 0000000..c96a04f
--- /dev/null
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file