{*********************************************************

 SlavaNap source code.

 Copyright 2001,2002 by SlavaNap development team
 Released under GNU General Public License

 Latest version is available at
 http://www.slavanap.org

**********************************************************

 Unit: servers

 Class for server-server connections

*********************************************************}
unit servers;

interface

{$I defines.pas}

uses
 SysUtils, Classes, Classes2, graphics, zlibex, winsock, windows, constants,
 stypes, blcksock, synsock, class_cmdlist;

type
 TAllowLinkType = (linkAll, linkList, linkNone, linkCustom);
 TConnectionState = (conNotConnected, conConnecting, conConnected);
 TServerAuthentication = (authResolve, authPassword);
 TServer = class(TObject)
  host          : String;
  alias         : String;
  port          : Integer;
  hub           : TServer;
  connected     : TConnectionState;
  thread        : TThread;
  logged        : Boolean;
  version       : String;
  console       : String;
  num_users, num_files, max_users: Integer;
  num_bytes     : Int64;
  server_handle : Integer;
  mypassword    : String;
  remotepassword: String;
  authentication: TServerAuthentication;
  compress      : TZCompressionLevel;
  relink        : Cardinal;
  comments      : String;
  forced_ip     : String;
  socket        : HSocket;
  out_list      : TNapCmdList;
  out_buf       : TNapCmdList;
  recv_buf      : String;
  recv_len      : Integer;
  login_start   : Cardinal;
  incoming      : Boolean;
  lag           : Boolean;
  reg_user      : String;
  redirects     : Integer;
  truestats     : Boolean;
  constructor Create;
  destructor Destroy; override;
  procedure Exec(id: Integer; cmd: String; log_cmd: Boolean=true);
  procedure Relay(id: Integer; cmd: String; dst,src: Integer);
  procedure Receive(n: Integer);
  procedure Connect;
  procedure ResetData;
  procedure Flush;
  procedure Compile;
  function  CountLag: Integer;
  procedure AddToOutput(str: string);
 end;
 TServerThread = class(TThread)
  srv: TServer;
  ip: String;
  err_msg: String;
  constructor Create(server: TServer);
  procedure Execute; override;
  destructor Destroy; override;
  procedure ResolveIP;
  procedure SyncComplete;
  procedure Error(str: String);
  procedure SyncError;
 end;

function GetServerName(srv: TServer): String;
function GetServerAlias(srv: TServer): String;
function GetServerLink(srv: TServer): TServer;
function GetServerHandle(srv: TServer): Integer;
function FindServer(host: String; allow_alias: Boolean): TServer; overload;
function FindServer(handle: Integer): TServer; overload;
function CheckLag(srv: TServer): Boolean;
function CountSubServers(srv: TServer): Integer;
function CountLinkedServers(ignored: TServer): Integer;
procedure LoadServers;
procedure SaveServers;

implementation

uses
 vars, lang, handler, thread, memory_manager;

var
 console_list, handle_list: String;

function GetServerName(srv: TServer): String;
begin
 if srv=nil then Result:=servername_t
 else result:=srv.host;
end;

function GetServerAlias(srv: TServer): String;  
begin  
 if srv=nil then Result:=serveralias  
 else if srv.alias<>'' then result:=srv.alias
 else result:=srv.host;
end;

function GetServerLink(srv: TServer): TServer;
begin
  Result:=nil;
  if srv=nil then exit;
  while srv.hub<>nil do
   srv:=srv.hub;
  Result:=srv;
end;

function CountSubServers(srv: TServer): Integer;
var
 i,j: Integer;
 srv2: TServer;
begin
 j:=1;
 for i:=0 to db_servers.count-1 do
 begin
  srv2:=db_servers.Items[i];
  if srv2.hub=srv then
   inc(j,CountSubServers(srv2));
 end;
 Result:=j;
end;

function CountLinkedServers(ignored: TServer): Integer;
var
 i,j: Integer;
 srv: TServer;
begin
 j:=1;
 for i:=0 to db_servers.count-1 do
 begin
  srv:=db_servers.Items[i];
  if srv<>ignored then
   if srv.logged then
    if srv.hub=nil then
     inc(j,CountSubServers(srv));
 end;
 Result:=j;
end;

function CheckLag(srv: TServer): Boolean;
begin
 Result:=false;
 if srv=nil then exit;
 srv:=GetServerLink(srv);
 if srv=nil then exit;
 Result:=true;
 if srv.connected<>conConnected then exit;
 if (current_time-srv.login_start)<SERVER_SYNCH_TIMEOUT then exit;
 Result:=srv.lag;
end;

function GetServerHandle(srv: TServer): Integer;
begin
 if srv=nil then Result:=myserverhandle
 else Result:=srv.server_handle;
end;

function FindServer(host: String; allow_alias: Boolean): TServer;
var
 i: Integer;
 srv: TServer;
begin
 host:=lowercase(host);
 if db_servers<>nil then
 for i:=0 to db_servers.Count-1 do
 begin
   srv:=db_servers.Items[i];
   if srv.host=host then
   begin
     Result:=srv;
     exit;
   end;
   if allow_alias and (srv.alias=host) then
   begin
     Result:=srv;
     exit;
   end;
 end;
 Result:=nil;
end;

function FindServer(handle: Integer): TServer;
var
 i: Integer;
 srv: TServer;
begin
 if db_servers<>nil then
 for i:=0 to db_servers.Count-1 do
 begin
   srv:=db_servers.Items[i];
   if srv.server_handle=handle then
   begin
     Result:=srv;
     exit;
   end;
 end;
 Result:=nil;
end;

procedure LoadServers;
var
 list,lst: TMyStringList;
 i: Integer;
 server: TServer;
 num: Integer;
begin
 list:=CreateStringList;
 try
  list.LoadFromFile(ApplicationDir+'servers');
  except
   on E:Exception do
   begin
    DebugLog('LoadServers : exception '+E.Message);
    FreeStringList(list);
    exit;
   end;
 end;
 lst:=CreateStringList;
 num:=2;
 for i:=0 to list.Count-1 do
 begin
  if list.Strings[i]='# Version 2' then num:=5;
  SplitString(list.Strings[i],lst);
  if lst.Count>=num then
  if Length(list.Strings[i])>0 then
  if list.Strings[i][1]<>'#' then
  if FindServer(lst.Strings[0],false)=nil then
  if lst.Strings[0]<>servername_t then
  begin
     server:=TServer.Create;
     server.connected:=conNotConnected;
     lst.Strings[0]:=lowercase(lst.Strings[0]);
     server.host:=lst.Strings[0];
     server.port:=StrToIntDef(lst.Strings[1],8888);
     server.server_handle:=0;
     if lst.count>4 then
     begin
       server.mypassword:=lst.Strings[2];
       server.remotepassword:=lst.Strings[3];
       server.authentication:=TServerAuthentication(StrToIntDef(lst.Strings[4],Ord(server.authentication)));
     end;
     if lst.count>5 then server.compress:=TZCompressionLevel(StrToIntDef(lst.Strings[5],3));
     if lst.count>6 then server.relink:=StrToIntDef(lst.Strings[6],0)*60000;
     if lst.count>7 then server.comments:=lst.Strings[7];
     if lst.count>8 then server.forced_ip:=lst.Strings[8];
     if lst.count>9 then server.alias:=lst.Strings[9];
     server.truestats:=false;
     if lst.count>10 then
      if lst.strings[10]='1' then
       server.truestats:=true;
     server.redirects:=0;
     if server.relink=1000 then server.relink:=900000;
     if server.relink<60000 then server.relink:=0;
     if server.relink>3600000 then server.relink:=server.relink div 60;
     db_servers.Add(server);
  end;
 end;
 FreeStringList(lst);
 FreeStringList(list);
end;

procedure SaveServers;
var
 str, str2: String;
 list: TMyStringList;
 i: Integer;
 server: TServer;
begin
 list:=CreateStringList;
 for i:=0 to db_servers.Count-1 do
 begin
   server:=db_servers.Items[i];
   if server.host<>servername_t then
   begin
     if server.truestats=false then
      str2:='0'
     else
      str2:='1';
     str:=server.host+' '+IntToStr(server.port)+' '+AddStr(server.mypassword)+' '+AddStr(server.remotepassword)+' '+IntToStr(Ord(server.authentication))+' '+IntToStr(Ord(server.compress))+' '+IntToStr(server.relink div 60000)+' '+AddStr(server.comments)+' '+AddStr(server.forced_ip)+' '+AddStr(server.alias)+' '+AddStr(str2);
     list.Add(str);
   end;
 end;
 list.Sort;
 list.Insert(0,'# Version 2');
 list.Insert(1,'#');
 list.Insert(2,'# ! このファイルは次回サーバーがシャットダウンしたとき上書きされます。');
 list.Insert(3,'# 編集するときは、サーバーが稼動していないか確認してください。');
 list.Insert(4,'#');
 list.Insert(5,'# 形式: host_name port mypassword remotepassword authentication_type compression relink_time "comments" forced_ip alias truestats');
 list.Insert(6,'#');
 list.Insert(7,'# 認証形式: 0 - ホスト名を解決, 1 - パスワードをチェック');
 list.Insert(8,'# 圧縮: 0 - 無圧縮, 1 - 最高速, 2 - 標準, 3 - 最大');
 list.Insert(9,'# 再リンクまでの時間: 0 - リンクしない, その他 - 起動時にリンクし、切断されたらこの数字の時間(分)後に再リンクする');
 list.Insert(10,'# IP: "" - 自動認識, その他 - 強制IP (サーバーがLANを通してリンクしているときに使えます)');
 list.Insert(11,'#');
 try
  list.SaveToFile(ApplicationDir+'servers');
  except
   on E:Exception do
    DebugLog('SaveServers : exception '+E.Message);
 end;
 FreeStringList(list);
end;

procedure CreateConsoleList;
var
 i: Integer;
 srv: TServer;
begin
 console_list:='';
 handle_list:='';
 for i:=0 to db_servers.count-1 do
 begin
   srv:=db_servers.Items[i];
   if srv.logged then
   begin
    console_list:=console_list+srv.console+' ';
    handle_list:=handle_list+IntToStr(srv.server_handle)+' ';
   end;
 end;
 console_list:=console_list+cons.nick;
 handle_list:=handle_list+IntToStr(myserverhandle);
end;

constructor TServer.Create;
begin
 inherited Create;
 reg_user:='';
 host:='';
 port:=8888;
 thread:=nil;
 mypassword:='password';
 remotepassword:='password';
 compress:=zcMax;
 relink:=0;
 comments:='';
 forced_ip:='';
 alias:='';
 socket:=INVALID_SOCKET;
 out_list:=nil;
 out_buf:=nil;
 recv_buf:='';
 ResetData;
end;

destructor TServer.Destroy;
begin
 if thread<>nil then
 try
  thread.Terminate;
  thread:=nil;
  except
 end;
 if socket<>INVALID_SOCKET then
 begin
  DoCloseSocket(socket);
  socket:=INVALID_SOCKET;
 end;
 if out_list<>nil then
 try
  FreeCmdList(out_list);
  out_list:=nil;
  except
 end;
 if out_buf<>nil then
 try
  FreeCmdList(out_buf);
  out_buf:=nil;
  except
 end; 
 SetLength(recv_buf,0);
 SetLength(host,0);
 SetLength(version,0);
 SetLength(console,0);
 SetLength(mypassword,0);
 SetLength(remotepassword,0);
 SetLength(alias,0);
 inherited Destroy;
end;

procedure TServer.ResetData;
begin
 SetLength(recv_buf,0);
 recv_len:=0;
 num_users:=0;
 num_files:=0;
 max_users:=0;
 num_bytes:=0;
 console:='';
 hub:=nil;
 connected:=conNotConnected;
 version:='';
 server_handle:=0;
 logged:=false;
 login_start:=GetTickCount;
 lag:=false;
 incoming:=false;
end;

procedure TServer.Relay(id: Integer; cmd: String; dst,src: Integer);
begin
 if connected=conNotConnected then exit;
 if id<>MSG_SRV_RELAY then
 begin
  cmd:=IntToStr(dst)+' '+IntToStr(src)+' '+IntToStr(id)+' '+cmd;
  id:=MSG_SRV_RELAY;
 end;
 if hub<>nil then hub.Relay(id,cmd,dst,src)
 else Exec(id,cmd,false);
end;

procedure TServer.Exec(id: Integer; cmd: String; log_cmd: Boolean=true);
var
 str: String;
begin
 if connected=conNotConnected then exit;
 if hub<>nil then
 begin
   hub.Relay(id,cmd,server_handle,myserverhandle);
   exit;
 end;
 if out_list=nil then out_list:=CreateCmdList;
 if log_servercommands and log_cmd then
 begin
   if id<>MSG_SRV_COMPRESSED then
     str:='Sending server command ['+IntToStr(id)+'] "'+cmd+'" ('+host+')'
   else
     str:='Sending compressed server data ('+host+')';  
   Log(0,str,true);
 end;
 str:='    '+cmd;
 str[1]:=Chr(Length(cmd) div 256);
 str[2]:=Chr(Length(cmd) and 255);
 str[3]:=Chr(id div 256);
 str[4]:=Chr(id and 255);
 if (out_list.GetLength+Length(str))>MAX_SERVER_COMMAND then Compile;
 out_list.AddCmd(GetTickCount,str);
end;

procedure TServer.Connect;
begin
 connected:=conConnecting;
 CreateConsoleList;
 thread:=TServerThread.Create(self);
end;

procedure TServer.AddToOutput(str: string);
var
 i: Integer;
 str1: String;
begin
 if out_buf=nil then out_buf:=CreateCmdList;
{ if Length(str)<1 then exit;
 while Length(str)>768 do
 begin
   str1:=Copy(str,1,512);
   Delete(str,1,512);
   out_buf.AddCmd(GetTickCount,str1);
 end;}
 out_buf.AddCmd(GetTickCount,str);
end;

procedure TServer.Compile;
var
 i: Integer;
 cmd: TNapCmd;
 str: String;
 old_mem, new_mem: Integer;
 adding: Boolean;
begin
 {$IFDEF CHECK_LEAK}
 old_mem:=AllocMemSize;
 {$ENDIF}
 if out_buf=nil then out_buf:=CreateCmdList;
 if (out_list=nil) or (out_list.count<1) then exit;
 while out_list.count>0 do
 begin
   if logged and (compress<>zcNone) then
   begin // try to compress
     str:='';
     i:=0;
     adding:=true;
     while (out_list.count>0) and (i<MAX_SERVER_COMMAND) and adding do
     begin
       cmd:=out_list.Cmd(0);
       if (i+Length(cmd.cmd))<MAX_SERVER_COMMAND then
       begin
         out_list.Delete(0);
         str:=str+cmd.cmd;
         i:=Length(str);
       end
       else
         adding:=false;
     end;
     {$I checksync.pas}
     cmd.cmd:='    '+ZCompressStr(str,compress);
     {$I checksync.pas}
     i:=Length(cmd.cmd)-4;
     cmd.cmd[1]:=Chr(i div 256);
     cmd.cmd[2]:=Chr(i and 255);
     cmd.cmd[3]:=Chr(MSG_SRV_COMPRESSED div 256);
     cmd.cmd[4]:=Chr(MSG_SRV_COMPRESSED and 255);
     if i>0 then AddToOutput(cmd.cmd);
     SetLength(cmd.cmd,0);
     SetLength(str,0);
     {$IFDEF CHECK_LEAK}
     new_mem:=AllocMemSize;
     if (new_mem-old_mem)>POSSIBLE_LEAK then
      DebugLog('Possible leak in TServer.Compile (1): '+IntToStr(new_mem-old_mem)+' bytes allocated');
     {$ENDIF}
   end
   else
   begin
     // uncompressed
     while out_list.count>0 do
     begin
      AddToOutput(out_list.Str(0));
      out_list.Delete(0);
     end;
     {$IFDEF CHECK_LEAK}
     new_mem:=AllocMemSize;
     if (new_mem-old_mem)>POSSIBLE_LEAK then
      DebugLog('Possible leak in TServer.Compile (2): '+IntToStr(new_mem-old_mem)+' bytes allocated');
     {$ENDIF}
     exit;
   end;
 end;
end;

procedure TServer.Flush;
var
 cmd: TNapCmd;
 old_mem, new_mem: Integer;
 last_error: Integer;
begin
 if connected<>conConnected then exit;
 if hub<>nil then exit;
 {$IFDEF CHECK_LEAK}
 old_mem:=AllocMemSize;
 {$ENDIF}
 if (out_buf=nil) or (out_buf.count=0) then
  Compile;
 lag:=false;
 if out_buf=nil then exit;
 if out_buf.count<1 then exit;
 if socket=INVALID_SOCKET then exit;
 if not CanSend(true) then exit;
 last_error:=0;
 {$I checksync.pas}
 while (last_error=0) and (out_buf.count>0) do
 begin
   cmd:=out_buf.cmd(0);
   TCPSocket_SendString(socket,cmd.cmd,last_error);
   if last_error=WSAEWOULDBLOCK then
   begin
     lag:=true;
     {$IFDEF CHECK_LEAK}
     new_mem:=AllocMemSize;
     if (new_mem-old_mem)>POSSIBLE_LEAK then
      DebugLog('Possible leak in TServer.Flush (1): '+IntToStr(new_mem-old_mem)+' bytes allocated');
     {$ENDIF}
     exit;
   end;
   if last_error<>0 then
   begin
     Wallop(MSG_SERVER_NOSUCH,wallopServer,GetLangT(LNG_DELINK2,host,IntToStr(last_error),GetErrorDesc(last_error),'Flush'),true);
     DisconnectServer(self,true,false,'Flush');
     {$IFDEF CHECK_LEAK}
     new_mem:=AllocMemSize;
     if (new_mem-old_mem)>POSSIBLE_LEAK then
      DebugLog('Possible leak in TServer.Flush (2): '+IntToStr(new_mem-old_mem)+' bytes allocated');
     {$ENDIF}
     exit;
   end;
   out_buf.Delete(0);
   inc(bytes_out,Length(cmd.cmd));
   inc(bandwidth_up,Length(cmd.cmd));
   if not CanSend(true) then exit;
   if out_buf.count<1 then
   begin
    {$IFDEF CHECK_LEAK}
    new_mem:=AllocMemSize;
    if (new_mem-old_mem)>POSSIBLE_LEAK then
     DebugLog('Possible leak in TServer.Flush (3): '+IntToStr(new_mem-old_mem)+' bytes allocated');
    {$ENDIF}
    exit;
   end;
 end;
end;

procedure TServer.Receive(n: Integer);
var
 i,j: Integer;
 old_mem, new_mem: Integer;
 last_error: Integer;
begin
 if not running then exit;
 if connected<>conConnected then exit;
 if hub<>nil then exit;
 if socket=INVALID_SOCKET then exit;
 if not CanReceive(true) then exit;
 if Length(recv_buf)<4 then SetLength(recv_buf,RECV_BUF_SIZE_SERVER);
 if not TCPSocket_CanRead(socket,0,last_error) then
 begin
   if last_error=WSAEWOULDBLOCK then exit;
   if last_error<>0 then
   begin
     Wallop(MSG_SERVER_NOSUCH,wallopServer,GetLangT(LNG_DELINK2,host,IntToStr(last_error),GetErrorDesc(last_error),'Receive0'),true);
     DisconnectServer(self,true,false,'Receive0');
   end;
   exit;
 end;
 i:=RECV_BUF_SIZE_SERVER-recv_len;
 i:=TCPSocket_RecvBuffer(socket,@recv_buf[recv_len+1],i,last_error);
 if last_error=WSAEWOULDBLOCK then exit;
 if last_error<>0 then
 begin
   Wallop(MSG_SERVER_NOSUCH,wallopServer,GetLangT(LNG_DELINK2,host,IntToStr(last_error),GetErrorDesc(last_error),'Receive'),true);
   DisconnectServer(self,true,false,'Receive');
   exit;
 end;
 {$I checksync.pas}
 if i<1 then exit;
 inc(bytes_in,i);
 inc(recv_len,i);
 inc(bandwidth_down,i);
 if recv_len<4 then exit;
 i:=Ord(recv_buf[2])+256*Ord(recv_buf[1]);
 SetLength(gcmd.cmd,0);
 while i<(recv_len-3) do // processing received buffer
 begin
   gcmd.id:=Ord(recv_buf[4])+256*Ord(recv_buf[3]);
   SetLength(gcmd.cmd,i);
   if i>0 then
     Move(recv_buf[5],gcmd.cmd[1],i);
   Move(recv_buf[i+5],recv_buf[1],recv_len-i-4);
   dec(recv_len,i+4);
   {$IFDEF CHECK_LEAK}
   old_mem:=AllocMemSize;
   {$ENDIF}
   ProcessServerCommand(self);
   {$IFDEF CHECK_LEAK}
   new_mem:=AllocMemSize;
   if (new_mem-old_mem)>POSSIBLE_LEAK then
    DebugLog('Possible leak in ProcessServerCommand ('+IntToStr(gcmd.id)+','+gcmd.cmd+'): '+IntToStr(new_mem-old_mem)+' bytes allocated');
   {$ENDIF}
   if connected<>conConnected then exit;
   if num_processed>max_server_commands then exit;
   if recv_len>3 then
     i:=Ord(recv_buf[2])+256*Ord(recv_buf[1])
   else
     i:=recv_len;
 end;
 if socket<>INVALID_SOCKET then
 begin
  inc(n);
  if n>=SERVER_MAX_RECEIVE_RECURSE then exit;
  Receive(n);
 end;
end;

function TServer.CountLag: Integer;
begin
 Result:=0;
 if out_buf<>nil then
  if out_buf.count>0 then
  begin
    Result:=GetTickCount-out_buf.Id(0);
    exit;
  end;
 if out_list<>nil then
  if out_list.count>0 then
  begin
    Result:=GetTickCount-out_list.Id(0);
    exit;
  end;
end;

constructor TServerThread.Create(server: TServer);
begin
 srv:=server;
 ip:=server.forced_ip;
 inherited Create(false);
end;

procedure TServerThread.ResolveIP;
var
 list: TMyStringList;
begin
 if ip<>'' then exit;
 list:=CreateStringList;
 ResolveNameToIP(srv.host,list);
 if list.count>0 then ip:=list.Strings[0];
 FreeStringList(list);
end;

procedure TServerThread.Execute;
var
 last_error: Integer;
 alias_str: String;
begin
 linking:=true;
 FreeOnTerminate:=true;
 priority:=tpLower;
 if srv.socket<>INVALID_SOCKET then
 begin
  DoCloseSocket(srv.socket);
  srv.socket:=INVALID_SOCKET;
 end;
 ResolveIP;
 if not running then Terminate;
 if terminated then exit;
 if ip='' then
 begin
   Error(GetLangE(LNG_LINKERRRESOLVE,srv.host));
   srv.thread:=nil;
   srv.connected:=conNotConnected;
   linking:=false;
   exit;
 end;
 if not running then Terminate;
 if terminated then exit;
 srv.socket:=synsock.socket(PF_INET,integer(SOCK_STREAM),IPPROTO_TCP);
 srv.login_start:=current_time;
 inc(sockets_count);
 TCPSocket_Connect(srv.socket,ip,IntToStr(srv.port),last_error);
 if not running then Terminate;
 if terminated then exit;
 srv.login_start:=current_time;
 if last_error<>0 then
 try
   Error(GetLangE(LNG_LINKERRCONNECT,srv.host,GetErrorDesc(last_error)));
   DoCloseSocket(srv.socket);
   srv.socket:=INVALID_SOCKET;
   srv.thread:=nil;
   srv.connected:=conNotConnected;
   linking:=false;
   exit;
  except
   exit;
 end;
 TCPSocket_Block(srv.socket,false); // make it non-blocking
 TCPSocket_KeepAlive(srv.socket,true);
 if not sockets_servers_default then
 begin
   TCPSocket_SetSizeRecvBuffer(srv.socket,sockets_servers_recv);
   TCPSocket_SetSizeSendBuffer(srv.socket,sockets_servers_send);
 end;
 srv.Exec(920,'00');
 srv.Exec(MSG_SRV_LOGIN,AddStr(servername_t)+' '+IntToStr(server_port[0])+' '+AddStr(SLAVANAP_FULL)+' '+AddStr(NET_BUILD)+' '+IntToStr(myserverhandle)+' '+AddStr(cons.nick)+' '+AddStr(cons_reg_user)+' '+AddStr(console_list)+' '+AddStr(handle_list));
 Synchronize(SyncComplete);
 srv.thread:=nil;
 srv.connected:=conConnected;
 srv.incoming:=false;
 srv.logged:=false;
 linking:=true;
end;

destructor TServerThread.Destroy;
begin
 inherited Destroy;
end;

procedure TServerThread.SyncComplete;
begin
 if not running then exit;
 cmd_list.AddDoubleCmd(MSG_CMD_WALLOP,0,GetLangE(LNG_LINKSRVLOGGED,srv.host),'');
end;

procedure TServerThread.Error(str: String);
begin
 err_msg:=str;
 Synchronize(SyncError);
end;

procedure TServerThread.SyncError;
begin
 if not running then exit;
 cmd_list.AddDoubleCmd(MSG_CMD_WALLOP,0,err_msg,'');
end;

end.
