Link to home
Start Free TrialLog in
Avatar of werehamster-
werehamster-

asked on

Properly Coded Indy Reader Threads - Part 2

This is basically a continuation of a privious question...
https://www.experts-exchange.com/questions/21116285/Properly-Formatted-and-Stable-Indy-TCP-Client-Thread-w-Reader-Thread.html

I've got a decent system of reading the packets and using methods of handling the data, but now I need a good way of handling disconnects from the server end. (abnormal or whatever)

I cannot modify the server in anyway, but every packet begins with...

Record
  StartPacket : Byte; //always $FF
  PacketID : Byte;  //function
  PacketLength : Word; // including header
  PacketData : array[PacketLength-4] of byte

I am now, with good suggestions for jacco, using a consts file that contains all the packet ID's and a variable that holds the names of methods that can be searched searched for and run...

const
  SID_NULL = $00;
  SID_CLIENTID = $05;
[...]
published
[...]
  procedure Process_SID_NULL;
  procedure Process_SID_CLIENTID;
[...]

var
  BNET_HeaderNames: array[Byte] of string;

initialization
  BNET_HeaderNames[SID_NULL] := 'SID_NULL';
  BNET_HeaderNames[SID_CLIENTID] := 'SID_CLIENTID';

It currently has threadsafe methods of writing packets from outside the thread.  I am basically looking for more ideas and suggestions.  

What I really need is a good way to terminate the connection.  Should I just .free the reader thread and indy client or is there a better way?

PS, I don't like just links to commonly known sites.  I like examples and detailed suggestions.

I will be awarding points appropriately.  Some points are owed to a contributor in "Part 1", but I will still give to others who contribute.  Soon as there is enough useful information in this topic, I will make a "Part 3" and continue on from there.
ASKER CERTIFIED SOLUTION
Avatar of Jacco
Jacco
Flag of Netherlands image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Now terminating the thread would normally be just:

fReader.Terminate and then fReader.Free. The Terminate would brake from the while loop in Execute this is done after one buffer read and one message write (if available).

But as you can see from the destructor of TThread Terminate is also called automatically:

destructor TThread.Destroy;
begin
  if (FThreadID <> 0) and not FFinished then
  begin
    Terminate;
    if FCreateSuspended then
      Resume;
    WaitFor;
  end;
  if FHandle <> 0 then CloseHandle(FHandle);
  inherited Destroy;
  FFatalException.Free;
  RemoveThread;
end;

Now one pitfall is when you use fClient.Read(....) because there the thread will wait for IP input infinitly. That is why I used ReadLn with a timeout in the past and this time I found an even better way of reading using the ReadFromStack/Extract where there is no wait.

Regards Jacco
Avatar of werehamster-
werehamster-

ASKER

Ok.  So what if I want to disconnect from the client side manually instead of waiting for the server to disconnect?  Do I just use MyReaderThread.free?
>> String variables can contain nulls (#0 characters as well) the only thing you have to take care of is when using string functions that expect a PChar with these strings

I don't get what you mean.  Do you mean function that convert strings to PChar?

Anyway, do I have to change any of the sample code you origonally gave me at all?

This is what I have re-written so far...

unit BotFrm;

interface

uses
  SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs, IdTCPClient, SyncObjs,
  QStdCtrls, QExtCtrls, QMenus, QTypes, QComCtrls, MainFrm, inifiles, PacketConsts;

type
  TBNETReader = class;

  TBotForm = class(TForm)
    OutputMemo: TMemo;
    ListBox1: TListBox;
    InputMemo: TMemo;
    Splitter1: TSplitter;
    fReader : TBNETReader;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  public
    INI : TIniFile;


  end;

  TClientState = (csDisconnected, csConnected);
  TMessageHandler = procedure of object;

  TBNETReader = class(TThread)
    fClient: TIdTCPClient;
    fForm : TBotForm;
    fState: TClientState;
    fInPacket : TBNETPacket;
    fOutPacket : TBNETPacket;
    fQueueOutSection: TCriticalSection;
    fReadPos : Word;
    constructor Create(AForm : TBotForm);
    destructor Destroy; override;
    procedure Execute; override;
    procedure DoMessage;
    procedure DoCaption;
  published
    //procedure Process_SID_PING;
    procedure Process_SID_OTHER;
  end;

implementation

{$R *.xfm}

constructor TBNETReader.Create(AForm : TBotForm);
begin
  inherited Create(False);
  fClient := TIdTCPClient.Create(nil);
  fForm := AForm;
  fReadPos := 0;
  fQueueOutSection := TCriticalSection.Create;
  synchronize(DoCaption);
end;

destructor TBNETReader.Destroy;
begin
  fClient.Free;
  fQueueOutSection.Free;
  inherited Destroy;
end;

procedure TBNETReader.Execute;
begin
  while not Terminated do
  begin
//    fClient.ReadFromStack(False,0,False);
    Sleep(10);
  end;
end;

procedure TBNETReader.DoMessage;
begin
end;

procedure TBNETREader.DoCaption;
const
  cConnected : array[Boolean] of string = ('Not Connected','Connected');
begin
  FForm.Caption := FForm.INI.ReadString('Main','Uername','Werebot')
    +' - '+cConnected[fClient.Connected];

end;

procedure TBNETReader.Process_SID_OTHER;
begin
end;

procedure TBotForm.FormCreate(Sender: TObject);
begin
  If not assigned(INI) then
    INI := TIniFile.Create(ExtractFilePath(Application.ExeName)+'config.ini');
  If INI.ReadString('Main','ConnectOnStartup','N') = 'Y' then
    Begin
      OutputMemo.Lines.Add('Connecting...');
    End;
end;

procedure TBotForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

end.

----------------------------------------------------------------

unit MainFrm;

interface

uses
  SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
  QExtCtrls, QComCtrls, QTypes, QMenus, QStdCtrls, QImgList, QActnList,
  QStdActns,inifiles;

type
  TMainForm = class(TForm)
    StatusBar1: TStatusBar;
    MainMenu: TMainMenu;
    SaveDialog: TSaveDialog;
    OpenDialog: TOpenDialog;
    ActionList: TActionList;
    actnWinClose: TWindowClose;
    actnWinCascade: TWindowCascade;
    actnWinTile: TWindowTile;
    actnWinMinimizeAll: TWindowMinimizeAll;
    actnExit: TAction;
    actnConnect: TAction;
    ImageList: TImageList;
    Connect1: TMenuItem;
    Timer1: TTimer;
    procedure actnConnectExecute(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  public
    INI : TIniFile;

    procedure CreateMDIChild(const Name: string);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.xfm}

uses
   BotFrm;

resourcestring
  sUntitled = 'Untitled';

procedure TMainForm.CreateMDIChild(const Name: string);
var
  Child: TBotForm;
begin
  { create a new MDI child window }
  Child := TBotForm.Create(Application);
  Child.Caption := Name;
  Child.INI := TIniFile.Create(Name)
end;


procedure TMainForm.actnConnectExecute(Sender: TObject);
var
  ABotForm : TBotForm;
begin
  if not Assigned(ActiveMDIChild) then
    Begin
      ABotForm := TBotForm.Create(Self);
      ABotForm.WindowState := wsMaximized;
      ABotForm.INI := TIniFile.Create(ExtractFilePath(Application.ExeName)+'config.ini');
      ABotForm.Caption := ABotForm.INI.ReadString('Main','Username','Werebot');
    End
  else if ActiveMDIChild is TBotForm then
    Begin
      TBotForm(ActiveMDIChild).OutPutMemo.Lines.Add('Connect Click')
    End;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Ini     := TIniFile.Create( ExtractFilePath(Application.ExeName)+'config.ini' );

  Top     := Ini.ReadInteger( 'Position', 'Top',    50  );
  Left    := Ini.ReadInteger( 'Position', 'Left',   100 );
  Height  := Ini.ReadInteger( 'Position', 'Height', 400 );
  Width   := Ini.ReadInteger( 'Position', 'Width',  540 );

  Timer1.Interval := 100;
  Timer1.Enabled := True;

end;


procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ini.WriteInteger('Position','Top',Top);
  ini.WriteInteger('Position','Left',Left);
  ini.WriteInteger('Position','Height',Height);
  ini.WriteInteger('Position','Width',Width);
  ini.Free;
end;


procedure TMainForm.Timer1Timer(Sender: TObject);
begin
  If Ini.ReadString('Main','ConnectOnStartup','N') = 'Y' then
    Begin
      MainForm.actnConnectExecute(Self);
    End;
  Timer1.Enabled := False;
end;

end.

---------------------------------------------------

unit PacketConsts;

interface

const
  SID_NULL                             = $00;
  SID_CLIENTID                         = $05;
  SID_STARTVERSIONING           = $06;
  SID_REPORTVERSION                 = $07;
  SID_GETADVLISTEX                 = $09;
  SID_ENTERCHAT                         = $0A;
  SID_GETCHANNELLIST           = $0B;
  SID_JOINCHANNEL                   = $0C;
  SID_CHATCOMMAND                   = $0E;
  SID_CHATEVENT                         = $0F;
  SID_FLOODDETECTED                 = $13;
  SID_UDPPINGRESPONSE           = $14;
  SID_MESSAGEBOX                   = $19;
  SID_PING                             = $25;
  SID_READUSERDATA                 = $26;
  SID_WRITEUSERDATA                 = $27;
  SID_LOGONCHALLENGE           = $28;
  SID_LOGONRESPONSE                 = $29;
  SID_CREATEACCOUNT                 = $2A;
  SID_CHANGEPASSWORD           = $31;
  SID_CDKEY2                       = $36;
  SID_LOGONRESPONSE2           = $3A;
  SID_CREATEACCOUNT2           = $3D;
  SID_LOGONREALMEX                 = $3E;
  SID_AUTH_INFO                         = $50;
  SID_AUTH_CHECK                   = $51;
  SID_FRIENDLIST               = $65;
  SID_FRIENDUPDATE                 = $66;
  SID_FRIENDADDED                   = $67;
  SID_FRIENDREMOVED                 = $68;
  SID_FRIENDMOVED                   = $69;
  SID_FINDCLANCANDIDATES   = $70;
  SID_INVITEMULTIPLEUSERS  = $71;
  SID_DISBANDCLAN                   = $73;
  SID_CLANINFO                         = $75;
  SID_CLANREQUEST                   = $77;
  SID_CLANINVITE                   = $79;
  SID_CLANMOTD                         = $7C;
  SID_CLANMEMBERLIST           = $7D;
  SID_CLANMEMBERUPDATE         = $7F;
  SID_CLANPROMOTION                 = $81;
var
  SID_HeaderNames: array[Byte] of string;

const
  MCP_STARTUP                           = $01;
  MCP_CHARCREATE                   = $02;
  MCP_CHARLOGON                         = $07;
  MCP_CHARDELETE                   = $0A;
  MCP_MOTD                             = $12;
  MCP_CHARLIST                         = $17;
  MCP_CHARUPGRADE                   = $18;

const
  PACKET_IDLE                           = $00;
  PACKET_LOGON                         = $01;
  PACKET_STATSUPDATE               = $02;
  PACKET_DATABASE                   = $03;
  PACKET_MESSAGE                   = $04;
  PACKET_CYCLE                         = $05;
  PACKET_USERINFO                   = $06;
  PACKET_USERLOGGINGOFF             = $07;
  PACKET_COMMAND                   = $08;
  PACKET_CHANGEDBPASSWORD       = $09;
  PACKET_BOTNETVERSION             = $0A;
  PACKET_BOTNETCHAT                 = $0B;
  PACKET_ACCOUNT                   = $0D;

const
  PKT_SERVERPING                    = $05;
  PKT_KEEPALIVE                         = $07;
  PKT_CONNTEST2                         = $09;


const
  EID_SHOWUSER             = $01;
  EID_JOIN                 = $02;
  EID_LEAVE                = $03;
  EID_WHISPER              = $04;
  EID_TALK                 = $05;
  EID_BROADCAST            = $06;
  EID_CHANNEL              = $07;
  EID_USERFLAGS                    = $09;
  EID_WHISPERSENT          = $0A;
  EID_CHANNELFULL          = $0D;
  EID_CHANNELDOESNOTEXIST  = $0E;
  EID_ERROR                = $13;
  EID_INFO                 = $12;
  EID_EMOTE                = $17;
  MAX_EVENT                                 = $18;

const
  BNLS_NULL                             = $00;
  BNLS_CDKEY                           = $01;
  BNLS_LOGONCHALLENGE               = $02;
  BNLS_LOGONPROOF                   = $03;
  BNLS_CREATEACCOUNT               = $04;
  BNLS_CHANGECHALLENGE             = $05;
  BNLS_CHANGEPROOF                 = $06;
  BNLS_UPGRADECHALLENGE             = $07;
  BNLS_UPGRADEPROOF                 = $08;
  BNLS_VERSIONCHECK                 = $09;
  BNLS_CONFIRMLOGON                 = $0A;
  BNLS_HASHDATA                         = $0B;
  BNLS_CDKEY_EX                         = $0C;
  BNLS_CHOOSENLSREVISION       = $0D;
  BNLS_AUTHORIZE                   = $0E;
  BNLS_AUTHORIZEPROOF               = $0F;
  BNLS_REQUESTVERSIONBYTE       = $10;
  BNLS_VERIFYSERVER                 = $11;
  BNLS_RESERVESERVERSLOTS       = $12;
  BNLS_SERVERLOGONCHALLENGE= $13;
  BNLS_SERVERLOGONPROOF             = $14;

const
  PRODUCT_STARCRAFT        = $01;
  PRODUCT_BROODWAR         = $02;
  PRODUCT_WAR2BNE          = $03;
  PRODUCT_DIABLO2          = $04;
  PRODUCT_LORDOFDESTRUCTION= $05;
  PRODUCT_JAPANSTARCRAFT   = $06;
  PRODUCT_WARCRAFT3        = $07;
  PRODUCT_THEFROZENTHRONE  = $08;

const
  SID_AUTH_INFO_Data : array[$00..$39] of byte = (
$FF,$50,$3A,$00,$00,$00,$00,$00,$36,$38,$58,$49,$50,$58,$33,$57, // .P:.....68XIPX3W
$10,$00,$00,$00,$53,$55,$6E,$65,$18,$BA,$00,$A6,$F0,$00,$00,$00, // ....SUne........
$09,$04,$00,$00,$09,$04,$00,$00,$55,$53,$41,$00,$55,$6E,$69,$74, // ........USA.Unit
$65,$64,$20,$53,$74,$61,$74,$65,$73,$00                          // ed States.
);

type
  TByteArray = array[Word] of Byte;
  TBNETPacket = Record
    StartPacket  : Byte; // Always $FF
    PacketID     : Byte;
    PacketLength : Word; // Including Header
    PacketData   : TByteArray;
  End;


implementation

initialization
  SID_HeaderNames[SID_NULL]                := 'SID_NULL';
  SID_HeaderNames[SID_CLIENTID]            := 'SID_CLIENTID';
  SID_HeaderNames[SID_STARTVERSIONING]     := 'SID_STARTVERSIONING';
  SID_HeaderNames[SID_REPORTVERSION]       := 'SID_REPORTVERSION';
  SID_HeaderNames[SID_GETADVLISTEX]        := 'SID_GETADVLISTEX';
  SID_HeaderNames[SID_ENTERCHAT]           := 'SID_ENTERCHAT';
  SID_HeaderNames[SID_GETCHANNELLIST]      := 'SID_GETCHANNELLIST';
  SID_HeaderNames[SID_JOINCHANNEL]         := 'SID_JOINCHANNEL';
  SID_HeaderNames[SID_CHATCOMMAND]         := 'SID_CHATCOMMAND';
  SID_HeaderNames[SID_CHATEVENT]           := 'SID_CHATEVENT';
  SID_HeaderNames[SID_FLOODDETECTED]       := 'SID_FLOODDETECTED';
  SID_HeaderNames[SID_UDPPINGRESPONSE]     := 'SID_UDPPINGRESPONSE';
  SID_HeaderNames[SID_MESSAGEBOX]          := 'SID_MESSAGEBOX';
  SID_HeaderNames[SID_PING]                := 'SID_PING';
  SID_HeaderNames[SID_READUSERDATA]        := 'SID_READUSERDATA';
  SID_HeaderNames[SID_WRITEUSERDATA]       := 'SID_WRITEUSERDATA';
  SID_HeaderNames[SID_LOGONCHALLENGE]      := 'SID_LOGONCHALLENGE';
  SID_HeaderNames[SID_LOGONRESPONSE]       := 'SID_LOGONRESPONSE';
  SID_HeaderNames[SID_CREATEACCOUNT]       := 'SID_CREATEACCOUNT';
  SID_HeaderNames[SID_CHANGEPASSWORD]      := 'SID_CHANGEPASSWORD';
  SID_HeaderNames[SID_CDKEY2]              := 'SID_CDKEY2';
  SID_HeaderNames[SID_LOGONRESPONSE2]      := 'SID_LOGONRESPONSE2';
  SID_HeaderNames[SID_CREATEACCOUNT2]      := 'SID_CREATEACCOUNT2';
  SID_HeaderNames[SID_LOGONREALMEX]        := 'SID_LOGONREALMEX';
  SID_HeaderNames[SID_AUTH_INFO]           := 'SID_AUTH_INFO';
  SID_HeaderNames[SID_AUTH_CHECK]          := 'SID_AUTH_CHECK';
  SID_HeaderNames[SID_FRIENDLIST]          := 'SID_FRIENDLIST';
  SID_HeaderNames[SID_FRIENDUPDATE]        := 'SID_FRIENDUPDATE';
  SID_HeaderNames[SID_FRIENDADDED]         := 'SID_FRIENDADDED';
  SID_HeaderNames[SID_FRIENDREMOVED]       := 'SID_FRIENDREMOVED';
  SID_HeaderNames[SID_FRIENDMOVED]         := 'SID_FRIENDMOVED';
  SID_HeaderNames[SID_FINDCLANCANDIDATES]  := 'SID_FINDCLANCANDIDATES';
  SID_HeaderNames[SID_INVITEMULTIPLEUSERS] := 'SID_INVITEMULTIPLEUSERS';
  SID_HeaderNames[SID_DISBANDCLAN]         := 'SID_DISBANDCLAN';
  SID_HeaderNames[SID_CLANINFO]            := 'SID_CLANINFO';
  SID_HeaderNames[SID_CLANREQUEST]         := 'SID_CLANREQUEST';
  SID_HeaderNames[SID_CLANINVITE]          := 'SID_CLANINVITE';
  SID_HeaderNames[SID_CLANMOTD]            := 'SID_CLANMOTD';
  SID_HeaderNames[SID_CLANMEMBERLIST]      := 'SID_CLANMEMBERLIST';
  SID_HeaderNames[SID_CLANMEMBERUPDATE]    := 'SID_CLANMEMBERUPDATE';
  SID_HeaderNames[SID_CLANPROMOTION]       := 'SID_CLANPROMOTION';
end.

------------------------------------------------------
You can download my project folder via...
www.datazap.net/ftp/werehamster/MyBot/MyBot.zip

also contains 2 semi-functional prototypes called project1 and project2.  current one is werebot.
Anyway, I don't have a need for any other process to read the packets outside the thread so I took out the critical section for the reading part.

Basically what I want to do is that if I don't have a process for it, to call process_other which will display a packet dump of the data to the screen.  I have some code to this already (though a little buggy).

Unhandled Packet: Header = 0000, Length = 0020, Name = Unknown
0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Would put Name = SID_Something if there is a const for it, but no process handler.  Anyway, no real question involved here, just explaining my next goal so you have a better understanding what I am doing.

Heres a couple of questions...

If I call each process_xxx, do I have to pass parameters for any particular reason?  I figured I would just use the fData and fDataLength and stuff instead.  Calling these process procedures holds up the rest of the thread anyway right?

Also, I've been messing with something in the past unsuccessfully.  Can I use something like a TMemoryStream on memory that has already been allocated without writing my own TCustomStream?  I suppose I could make fData a TMemoryStream itlsef and work with it from there, but would it be safe to treat String(fData.memory) as a string?

Actually, if there was a component that allowed using an existing pointer/string, to be allowed to do things like ReadInt, ReadByte, ReadBoolean, etc, it would be extremely useful for this.  I've written my own one in the past for a customized TCP client I made about 5 years ago that I could dig up I guess if I can find it.  Any thoughts on this?  Basically in each process_XXX I'd like to ideally just do something like this...

procedure TBNETReader.Process_SID_XXX
var
  X : Integer;
  B : Byte;
begin

//X := fInPacket.ReadInteger;
//B := fInPacket.ReadByte;
X := ReadInteger;
B := ReadByte;
//fOutPacket.Header := SID_Something
//fOutPacket.Data.WriteByte(B+1)
//fOutPacket.Send;
PacketStart(SID_Something)
WriteByte(B+1);
PacketSend;
end;

Which could be simplified via WriteByte(ReadByte+1) if I wanted to.

Any suggestions?  I looked up and found TReader and TWriter and stuff, but looks too confusing for me.
Here is a link to a hex dump question I put up asking for source incase you have any.

https://www.experts-exchange.com/questions/21134147/Simple-Hex-Dump-Example-0000-00-49-50-51-123.html
>>I don't get what you mean.  Do you mean function that convert strings to PChar?

Yep, they exist. Here is a sample:

function AnsiPos(const Substr, S: string): Integer;
var
  P: PChar;
begin
  Result := 0;
  P := AnsiStrPos(PChar(S), PChar(SubStr));
  if P <> nil then
    Result := Integer(P) - Integer(PChar(S)) + 1;
end;

AnsiPos is also used by:

function StringReplace(const S, OldPattern, NewPattern: string; Flags: TReplaceFlags): string;

Which I use a lot. That why I said you have to take care :)

>>Ok.  So what if I want to disconnect from the client side manually instead of waiting for the server to disconnect?  Do I just use MyReaderThread.free?

You could. Free while call Terminate (set fTerminate to True) Execute will break from the while loop (in a short time if you use the ReadFromStack) and if you want you can that call fClient.Disconnect before exiting the Execute method (would be clean).

The thing is, this scheme will not work if you wait in any of the ReadXXX functions (except ReadFromStack).

>>If I call each process_xxx, do I have to pass parameters for any particular reason?  I figured I would just use the fData and fDataLength and stuff instead.  Calling these process procedures holds up the rest of the thread anyway right?

If you use private packet members you do not need to pass anything. And yep, calling the procedures hold up the thread.

>>Also, I've been messing with something in the past unsuccessfully.  Can I use something like a TMemoryStream on memory that has already been allocated without writing my own TCustomStream?  I suppose I could make fData a TMemoryStream itlsef and work with it from there, but would it be safe to treat String(fData.memory) as a string?

What should work is the following:

Add this empty decalration:

type
  TAllocatedMemoryStream = class(TMemoryStream);

With it you can call protected functions so in you code you can create and use an ordinary TMemoryStream and assign it you memory just by using a typecast:

...
  TAllocatedMemoryStream(MS).SetPointer(@fData[0], 65536);
...

I am not sure about the [0] maybe leave it away. And you have to make sure you don't call Write, change the Size or anything, just read.

I still think it would be easier to have fData as a string, you could use is as a stream as simple as a call to TStringStream.Create(fData).

>>Actually, if there was a component that allowed using an existing pointer/string, to be allowed to do things like ReadInt, ReadByte, ReadBoolean, etc, it would be extremely useful for this.  I've written my own one in the past for a customized TCP client I made about 5 years ago that I could dig up I guess if I can find it.  Any thoughts on this?  Basically in each process_XXX I'd like to ideally just do something like this...

When I can determine the format myself I always use TReader/TWriter from the classes unit for that purpose. You can hook them to a stream and read all kinds of data types from it.
 
You can easily make a descendant of TStringStream that supports reading Integers/Bytes from it:

type
  TMyStringStream = class(TStringStream)
  public
    function ReadByte: Byte;
  end;

function TMyStringStream.ReadByte: Byte;
begin
  Read(@Result, 1);
end;

I have downloaded the project, can look at it tonight.

Regards Jacco
I suppose I could just use it as a string.  Just going to require a little reconditioning on my part.  I'm still in the mentality of getting the length of a string from checking astring[0] and stuff from back in turbo pascal.  Never really developed my string manipulating skills, but now isn't the worst time to force myself into it.

Actually, what I am thinking of is making a couple of helper functions that do something like...

function getByteAt(var String; Pos: Word) : Byte;
function getWordAt(var String; Pos: Word) : Word;

...and of course checking the string length and stuff so it isn't out of bounds and stuff.
As far as the packet dump thing, I got that pretty much figured out with the help of a couple other fellows.  Nothing I coudn't do on my own, just like to see how others do things and very frequently find better ways of doing things.  The second guy gave me the idea of converting to a string instead of directly to a memo file which would let me reused the doMessage procedure.  Anyway, enough with that.  :)
Anyway, I did some work and got something functional now...

www.datazap.net/ftp/werehamster/MyBot/MyBot2.zip

I just can't get my disconnect button working without getting errors.
Reverse the destructor order:

destructor TBNETReader.Destroy;
begin
  inherited Destroy;
  fClient.Free;
  fQueueOutSection.Free;
  fQueueOut.Free;
end;

This way the thread will first be terminated and then you resources will be freed.

I have no server so for me it stuck at the Connect with does much longer that 500 milliseconds...

I have IdSocketError added in Tools\Debugger Options\Language Exceptions to ignore it. This measure might be to extreme for you but Indy developers indicate in the sources to at least add EIdSilentException to the ignored ones.

Regards Jacco
well if you have config.ini in the folder, you have a server to connect to.  It is a server that is up now.  Just have SID_PING coded right now.  I'll give it a shot when I get home.
I have tested you app here and I never get an error (even without my change).

I changed a one small thing:

procedure TMainForm.actnConnectExecute(Sender: TObject);
var
  ABotForm : TBotForm;
begin
  if not Assigned(ActiveMDIChild) then Begin
    ABotForm := TBotForm.Create(Self);
    ABotForm.WindowState := wsMaximized;
    ABotForm.INI := TIniFile.Create(ExtractFilePath(Application.ExeName)+'config.ini');
    ABotForm.Caption := ABotForm.INI.ReadString('Main','Username','Werebot');
    ABotForm.fReader := TBNETReader.Create(TBotForm(ActiveMDIChild));
  End
  else if ActiveMDIChild is TBotForm then
    if not Assigned(TBotForm(ActiveMDIChild).fReader) then begin
      TBotForm(ActiveMDIChild).fReader := TBNETReader.Create(TBotForm(ActiveMDIChild));
      MainForm.actnConnect.Caption := 'Disconnect';
    end
    else begin
      FreeAndNil(TBotForm(ActiveMDIChild).fReader);
      MainForm.actnConnect.Caption := 'Connect';
    end;
end;

FreeAndNil the thread in stead of terminating. This action is in balance the construction fo a thread when connecting.

Regards Jacco
www.datazap.net/ftp/werehamster/MyBot/MyBot3.zip

has been expanded to include a 2nd thread to a second server that has a different header.  I used the same techniques.

Only thing is this, how do I stop the thing from contantly trying to reconnect a million times if the server refuses the connection (temp IP ban) ?

If your curious what I am doing, goto...
http://bnetdocs.valhallalegends.com/

Anyway, I am going to close this topic soon and give ya these points and start a new topic again.  

So far things seem to work fine, but like to implement a reconnection timer or somehow informing the main form that one of the clients disconnected.

Also, I need to slow down the outgoing packets some how so that it transmits packets at a delay between them.  The server I am connecting to has flood control that IP bans the client if it detects spam.  Maybe some way of guaging the chars per second for the last 10 seconds and adjust the delay between packets accordingly or something.  Is this something I would use a timer for and have the timer insert a string into the queue on an interval?

Anyway, I'm just babbling.  It's 4am right now and I should be in bed.  I always lose track of time when I am writing a program.  Small flaw in me I guess.

Thanks for the help so far.
-Steve
Maybe you can see in the Exception that occurs in the connect what the reason is. The code in the EIdSocketError might be of interest.

In the exception handler of the connect part you could check for a code in the exception:

on E: EIdSocketError do
begin
  case LastError of
    10041: ...
    else raise;
  end;
end;

You could for example add an extra state to the state machine csConnectionTimeOut which just sleeps a timeout and goes to csDisconnected again.

>>Also, I need to slow down the outgoing packets some how so that it transmits packets at a delay between them.

Maybe you can use one of Indy IOHandlers which does a speed limit: TIdIOHandlerThrottle. You can limit the speed to a number of bytes/bits per second for a connection. You could create one in your thread. I haven't experimented with them but I think you can just connect is to the TIdTCPClient.IOHandler before opening it.

>>Anyway, I am going to close this topic soon and give ya these points and start a new topic again.  

Thanks! I think this one will push me over the 100.000 points overall. Do they still give out T-Shirts?

>>Anyway, I'm just babbling.  It's 4am right now and I should be in bed.  I always lose track of time when I am writing a program.  Small flaw in me I guess.

I know the problem. Come with the hobby I guess.

Regards Jacco
Anyway, I did a re-write again, getting away from CLX as I think it is quite buggy and I don't have any immediate plans to use Kylix and I wanted to use a TRichEdit component to get some color.