![]() |
AW: Query an Gameserver
Zitat:
Da bei einer Port-Angabe es sinnlos wäre negative Werte zu ermöglichen ist der Wert als "unsigned" vorgegeben. Dass die Bytes vertaucht sind, hätte Dir allerdings die Zuhilfename von Windows-Bordmitteln, wie z.B. dem Taschenrechner gezeigt. Dort hättest Du einfach einmal die Zahl, welche Du erhalten hast eingeben können und in Hex umwandeln lassen. Dann eifach die Hex-Werte vertauchen und zurück nach Dezimal umwandeln. Wenn Du dann die erhaltene Zahl als Portadresse in Deinem Programm benutzt hättest. dann hättest Du gesehen, dass Daten empfangen werden. |
AW: Query an Gameserver
Liste der Anhänge anzeigen (Anzahl: 1)
Das die Daten vertauscht sind steht doch lang und breit in der Dokumentation:
Zitat:
Da ich mir dieses Elend mit dem Auslesen und Schreiben nicht mehr ansehen kann, hier mal eine Unit, womit man dieses Pakete sehr einfach zusammenbauen und auch wieder auseinander nehmen kann. Kleine Demo vorweg (alles was einem unbekannt vorkommt ist in der Unit
Delphi-Quellcode:
definiert):
SourceQuery
Delphi-Quellcode:
Ausgabe von
program SimpleTests;
{$APPTYPE CONSOLE} {$R *.res} uses IdGlobal, IdUDPClient, StrUtils, SysUtils, SourceQuery in 'SourceQuery.pas'; procedure ParseInfoResponse( AResponse: TSourceQueryBytes ); var LID: SQUShort; LEDF: SQByte; begin Writeln( 'Parse-Data:' ); Writeln; Writeln( 'Protocol-Header ', AResponse.ReadLong ); Writeln( 'Packet-Header ', AResponse.ReadChar ); Writeln( 'Protocol ', AResponse.ReadByte ); Writeln( 'Name ', AResponse.ReadString ); Writeln( 'Map ', AResponse.ReadString ); Writeln( 'Folder ', AResponse.ReadString ); Writeln( 'Game ', AResponse.ReadString ); LID := AResponse.ReadUShort; Writeln( 'ID ', LID ); Writeln( 'Players ', AResponse.ReadByte ); Writeln( 'Max. Players ', AResponse.ReadByte ); Writeln( 'Bots ', AResponse.ReadByte ); Writeln( 'Server Type ', AResponse.ReadChar ); Writeln( 'Environment ', AResponse.ReadChar ); Writeln( 'Visibility ', AResponse.ReadByte ); Writeln( 'VAC ', AResponse.ReadByte ); // Testen auf "The Ship" if ( LID >= 2400 ) and ( LID <= 2499 ) then begin Writeln( 'Mode ', AResponse.ReadByte ); Writeln( 'Witnesses ', AResponse.ReadByte ); Writeln( 'Duration ', AResponse.ReadByte ); end; Writeln( 'Version ', AResponse.ReadString ); if not AResponse.Eof then begin LEDF := AResponse.ReadByte; Writeln( 'EDF ', LEDF ); if LEDF and $80 = $80 then begin Writeln( 'Port ', AResponse.ReadShort ); end; if LEDF and $10 = $10 then begin Writeln( 'SteamID ', AResponse.ReadLongLong ); end; if LEDF and $40 = $40 then begin Writeln( 'Port ', AResponse.ReadShort ); Writeln( 'Name ', AResponse.ReadString ); end; if LEDF and $20 = $20 then begin Writeln( 'Keywords ', AResponse.ReadString ); end; if LEDF and $01 = $01 then begin Writeln( 'GameID ', AResponse.ReadLongLong ); end; end; end; procedure TestInfoResponse; var LResponse: TSourceQueryBytes; begin Writeln( 'Example response for Counter Strike: Source:' ); Writeln; LResponse.SetData( {AData} HexStrToBytes( {AHexStr} StringReplace( {} 'FF FF FF FF 49 02 67 61 6D 65 32 78 73 2E 63 6F' + {} '6D 20 43 6F 75 6E 74 65 72 2D 53 74 72 69 6B 65' + {} '20 53 6F 75 72 63 65 20 23 31 00 64 65 5F 64 75' + {} '73 74 00 63 73 74 72 69 6B 65 00 43 6F 75 6E 74' + {} '65 72 2D 53 74 72 69 6B 65 3A 20 53 6F 75 72 63' + {} '65 00 F0 00 05 10 04 64 6C 00 00 31 2E 30 2E 30' + {} '2E 32 32 00', {OldPattern} ' ', {NewPattern} '', {Flags} [rfReplaceAll] ) ) ); ParseInfoResponse( LResponse ); end; procedure RealInfoRequest; var LRequest, LResponse: TSourceQueryBytes; LUdp: TIdUDPClient; LBuffer: TIdBytes; LResponseSize: Integer; begin // Prepare Request LRequest.WriteLong( SQ_SIMPLEPACKET_PROTOCOL_HEADER ); LRequest.WriteByte( SQ_INFO_REQUEST_HEADER ); LRequest.WriteString( 'Source Engine Query' ); Writeln( 'REQUEST-DATA:' ); Writeln; Writeln( LRequest.ToString ); Writeln; LBuffer := LRequest.GetData; LUdp := TIdUDPClient.Create( nil ); try // Send Request Write( 'Sending ... ' ); LUdp.SendBuffer( '5.45.97.44', 2301, LBuffer ); // Receive Response SetLength( LBuffer, SQ_PACKET_MAXSIZE ); Write( 'Receiving ... ' ); LResponseSize := LUdp.ReceiveBuffer( LBuffer ); Writeln( LResponseSize, ' Bytes' ); SetLength( LBuffer, LResponseSize ); Writeln; finally LUdp.Free; end; Writeln( 'RESPONSE-DATA:' ); Writeln; Writeln( DumpBuffer( LBuffer ) ); Writeln; // Parse Response LResponse.SetData( LBuffer ); ParseInfoResponse( LResponse ); end; procedure RealMasterQueryRequest; var LRequest, LResponse: TSourceQueryBytes; LBuffer: TIdBytes; LBufferSize: Integer; LUdp: TIdUDPClient; LServerAddress: TSQServerAddressBlock; LCount, LRetries: Integer; begin LServerAddress.IP1 := 0; LServerAddress.IP2 := 0; LServerAddress.IP3 := 0; LServerAddress.IP4 := 0; LServerAddress.Port := 0; LUdp := TIdUDPClient.Create( nil ); try LCount := 0; repeat // Daten abrufen ... LRequest.Clear; LRequest.WriteByte( $31 ); // Message Type LRequest.WriteByte( SQ_REGIONCODE_RESTOFTHEWORLD ); // Region Code LRequest.WriteString( LServerAddress.ToString ); // IP-Address LRequest.WriteString( '\gamedir\arma2arrowpc' ); // Filter LBuffer := LRequest.GetData; // Send Request LUdp.SendBuffer( 'hl2master.steampowered.com', 27011, LBuffer ); LRetries := 0; repeat if LRetries >= 3 then raise Exception.Create( 'Too many retries' ); SetLength( LBuffer, SQ_PACKET_MAXSIZE ); LBufferSize := LUdp.ReceiveBuffer( LBuffer, 2000 ); Inc( LRetries ); until LBufferSize > 0; SetLength( LBuffer, LBufferSize ); LResponse.SetData( LBuffer ); Assert( LResponse.ReadLong = SQ_SIMPLEPACKET_PROTOCOL_HEADER ); Assert( LResponse.ReadByte = $66 ); Assert( LResponse.ReadByte = $0A ); // Parse the Server-Addresses while not LResponse.Eof do begin Inc( LCount ); LServerAddress.IP1 := LResponse.ReadByte; LServerAddress.IP2 := LResponse.ReadByte; LServerAddress.IP3 := LResponse.ReadByte; LServerAddress.IP4 := LResponse.ReadByte; LServerAddress.Port := swap( LResponse.ReadUShort ); // swap weil die Doku das sagt Writeln( LCount:7, '. ', LServerAddress.ToString ); end; until ( LServerAddress.ToString = '0.0.0.0:0' ); // ... bis die leere Adresse zurückkommt finally LUdp.Free; end; end; begin try TestInfoResponse; RealInfoRequest; RealMasterQueryRequest; except on E: Exception do Writeln( E.ClassName, ': ', E.Message ); end; ReadLn; end.
Delphi-Quellcode:
:
RealInfoRequest
Code:
In der ganz kurzen Form (ohne Dump und Kommentare)) sieht ein Request dann wie folgt aus
REQUEST-DATA:
FF FF FF FF 54 53 6F 75 72 63 65 20 45 6E 67 69 ÿÿÿÿTSource Engi 6E 65 20 51 75 65 72 79 00 ne Query. Sending ... Receiving ... 196 Bytes RESPONSE-DATA: FF FF FF FF 49 11 5B 4C 2D 54 2D 53 5D 20 45 70 ÿÿÿÿI.[L-T-S] Ep 6F 63 68 20 4F 72 69 67 69 6E 73 20 28 31 2E 30 och Origins (1.0 2E 35 2E 31 2F 31 32 35 35 34 38 29 00 54 61 76 .5.1/125548).Tav 69 00 61 72 6D 61 32 61 72 72 6F 77 70 63 00 44 i.arma2arrowpc.D 61 79 5A 20 45 70 6F 63 68 20 4F 72 69 67 69 6E ayZ Epoch Origin 73 00 8A 84 01 19 00 64 77 00 00 31 2E 36 33 2E s.??...dw..1.63. 31 32 35 35 34 38 00 B1 FE 08 01 CC 2C AF 75 14 125548.±þ..Ì,¯u. 40 01 62 74 2C 72 31 36 33 2C 6E 31 32 35 35 34 @.bt,r163,n12554 38 2C 73 37 2C 69 31 2C 6D 66 2C 6C 66 2C 76 66 8,s7,i1,mf,lf,vf 2C 64 74 2C 74 63 6F 6F 70 2C 67 36 35 35 34 35 ,dt,tcoop,g65545 2C 63 32 31 34 37 34 38 33 36 34 37 2D 32 31 34 ,c2147483647-214 37 34 38 33 36 34 37 2C 70 77 2C 00 8A 84 00 00 7483647,pw,.??.. 00 00 00 00 .... Parse-Data: Protocol-Header -1 Packet-Header I Protocol 17 Name [L-T-S] Epoch Origins (1.0.5.1/125548) Map Tavi Folder arma2arrowpc Game DayZ Epoch Origins ID 33930 Players 1 Max. Players 25 Bots 0 Server Type d Environment w Visibility 0 VAC 0 Version 1.63.125548 EDF 177 Port 2302 SteamID 90094488230087681 Keywords bt,r163,n125548,s7,i1,mf,lf,vf,dt,tcoop,g65545,c2147483647-21474 83647,pw, GameID 33930
Delphi-Quellcode:
Der komplette Source im Anhang
procedure InfoRequest;
var LRequest, LResponse: TSourceQueryBytes; LUdp: TIdUDPClient; LBuffer: TIdBytes; LResponseSize: Integer; begin LRequest.WriteLong( SQ_SIMPLEPACKET_PROTOCOL_HEADER ); LRequest.WriteByte( SQ_INFO_REQUEST_HEADER ); LRequest.WriteString( 'Source Engine Query' ); LBuffer := LRequest.GetData; LUdp := TIdUDPClient.Create( nil ); try LUdp.SendBuffer( '5.45.97.44', 2301, LBuffer ); SetLength( LBuffer, SQ_PACKET_MAXSIZE ); LResponseSize := LUdp.ReceiveBuffer( LBuffer ); SetLength( LBuffer, LResponseSize ); finally LUdp.Free; end; LResponse.SetData( LBuffer ); ParseInfoResponse( LResponse ); end; |
AW: Query an Gameserver
Zitat:
Da wundert es mich nicht mehr, dass viele Programme, welche nur ein Fenster haben zum Teil und das ohne Daten geladen zu haben, bereits 50kb im Hauptspeicher belegen, statt den eigentlich erforderlichen 4kb. |
AW: Query an Gameserver
Jeder wie er will von Spaghetticode bis ordentlich strukturiert. Irgendwo dazwischen sortiert man sich ein. Wo siehst du dich da (rhetorische Frage, es reicht wenn du dir die selber beantwortest).
|
AW: Query an Gameserver
...
Ich häng immernoch an der Kompletten Serverliste... Ich bekomme stets nur den ersten Teil, danch bekomme ich nichts brauchbares... Noch einer Lust mir zu helfen ? MFG Flo |
AW: Query an Gameserver
Irgendwie hab ich meinen aktuellen Code vergessen:
Delphi-Quellcode:
irgendwie bekomme ich 4 mal die selbe liste.
procedure Get_SERVER_LIST(a1: byte; a2: byte; a3: byte; a4: byte; a5: Word);
var // request : TA2S_InfoRequest; request : TServerListRequest; response : TServerListResponse; buffer : TBufferArray; i : Integer; y : Integer; endoflist : Boolean; endofpartlist : Boolean; new_a1 : byte; new_a2 : byte; new_a3 : byte; new_a4 : byte; new_a5 : word; old_a1 : byte; old_a2 : byte; old_a3 : byte; old_a4 : byte; old_a5 : word; old_seed : String; new_seed : String; newport_plain: Integer; oldport_plain: Integer; begin old_a1 := $30; old_a2 := $30; old_a3 := $30; old_a4 := $30; old_a5 := $3030; // \gamedir\arma2arrowpc // 31 FF 30 2e 30 2e 30 2e 30 3a 30 00 5c 67 61 6d 65 64 69 72 5c 61 72 6d 61 32 61 72 72 6f 77 70 63 00 // \gamedir\arma3 // 31 ff 30 2e 30 2e 30 2e 30 3a 30 00 5c 67 61 6d 65 64 69 72 5c 61 72 6d 61 33 request.RequestHeader := $31; request.PacketHeader := $FF; request.First := $30; request.First_dot := $2E; request.Second := $30; request.Second_dot := $2E; request.Third := $30; request.Third_dot := $2E; request.Fourth := $30; request.double_dot := $3A; request.Port := $3030; request.Payload[0] := $00; request.Payload[1] := $5C; request.Payload[2] := $67; request.Payload[3] := $61; request.Payload[4] := $6D; request.Payload[5] := $65; request.Payload[6] := $64; request.Payload[7] := $69; request.Payload[8] := $72; request.Payload[9] := $5C; request.Payload[10] := $61; request.Payload[11] := $72; request.Payload[12] := $6D; request.Payload[13] := $61; request.Payload[14] := $33; request.Payload[15] := $00; Form1.udp1.BlockMode := bmNonBlocking; Form1.udp1.RemoteHost := 'hl2master.steampowered.com'; Form1.udp1.RemotePort := '27011'; if Form1.udp1.Active = false then begin form1.udp1.Active := true; form1.ListBox1.Clear; endoflist := false; end; if (Form1.udp1.Connected) then begin y := 0; repeat i := 0; form1.udp1.CleanupInstance; Form1.Udp1.SendBuf(request, SizeOf(request)); Form1.udp1.WaitForData(250); Form1.Udp1.ReceiveBuf(buffer, SizeOf(buffer)); Move(buffer[i], response.ResponseHeader, SizeOf(response.ResponseHeader)); inc(i, SizeOf(response.ResponseHeader)); Move(buffer[i], response.PacketHeader, SizeOf(response.PacketHeader)); inc(i, SizeOf(response.PacketHeader)); // form1.Caption := chr(response.ResponseHeader) + chr(response.PacketHeader) + IntToStr(i); form1.Memo1.Text := bintostr((buffer)); repeat Move(buffer[i], response.First, SizeOf(response.First)); inc(i, SizeOf(response.First)); Move(buffer[i], response.Second, SizeOf(response.Second)); inc(i, SizeOf(response.Second)); Move(buffer[i], response.Third, SizeOf(response.Third)); inc(i, SizeOf(response.Third)); Move(buffer[i], response.Fourth, SizeOf(response.Fourth)); inc(i, SizeOf(response.Fourth)); // Response.Port := Buffer[i] * 256 + Buffer[i+1]; Inc(i, SizeOf(Response.Port)); // Weil die Bytes vertauscht sind, müssen sie umgedreht werden für ein richtiges newport_plain := Buffer[i] * 256 + Buffer[i+1]; Inc(i, SizeOf(Response.Port)); // Weil die Bytes vertauscht sind, müssen sie umgedreht werden für ein richtiges new_a1 := response.First; new_a2 := response.Second; new_a3 := response.Third; new_a4 := response.Fourth; new_a5 := response.Port; new_seed := IntToStr(new_a1) + IntToStr(new_a2) + IntToStr(new_a3) + IntToStr(new_a4) + IntToStr(new_a5); if (response.First <> 00) AND (response.Second <> 00) AND (response.Third <> 00) AND (response.Fourth <> 00) then begin form1.duplicatcounter := 0; Form1.ListBox1.Items.Add ((IntToStr(new_a1)) + '.' + (IntToStr(new_a2)) + '.' + (IntToStr(new_a3)) + '.' + (IntToStr(new_a4)) + ':' + IntToStr((newport_plain)) ); old_a1 := response.First; old_a2 := response.Second; old_a3 := response.Third; old_a4 := response.Fourth; old_a5 := newport_plain; old_seed := IntToStr(old_a1) + IntToStr(old_a2) + IntToStr(old_a3) + IntToStr(old_a4) + IntToStr(newport_plain); Application.ProcessMessages; end else begin Form1.duplicatcounter := form1.duplicatcounter + 1; request.First := strtoint( StringToHex(inttostr(old_a1))); request.Second := strtoint( StringToHex(inttostr(old_a2))); request.Third := strtoint( StringToHex(inttostr(old_a3))); request.Fourth := strtoint( StringToHex(inttostr(old_a4))); request.Port := strtoint( StringToHex(inttostr(old_a5))); Form1.Edit3.Text := StringToHex(inttostr(old_a1) + inttostr(old_a2) + inttostr(old_a3) + inttostr(old_a4) + inttostr(old_a5)) ; end; y := y + 1; form1.Caption := inttostr(y); // until (IntToStr(response.First)) + '.' + (IntToStr(response.Second)) + '.' + (IntToStr(response.Third)) + '.' + (IntToStr(response.Fourth)) + ':' + IntToStr((response.Port)) = '0.0.0.0:0'; until form1.duplicatcounter = 2; Form1.ListBox1.Items.Add ('---------------'); Form1.ListBox1.Items.Add ('---------------'); Form1.ListBox1.Items.Add ('NewSeed: ' + new_seed); Form1.ListBox1.Items.Add ('OldSeed: ' + old_seed); Form1.ListBox1.Items.Add ('---------------'); Form1.ListBox1.Items.Add ('---------------'); until y > 500; end; Form1.udp1.Active := false; form1.Caption := 'Servers: ' + inttostr(Form1.ListBox1.Items.Count -1); end; |
AW: Query an Gameserver
Zitat:
Darum würde ich zunächst einmal den Buffer vor versenden des Request nullen
Delphi-Quellcode:
wenn beim zweiten lesen nur Nullen kommen, scheint die Gegenstelle keine Daten geliefert zu haben.
fillchar
Delphi-Quellcode:
Und wofür ist dieses seltsame Konstrukt gut?
request.First := strtoint( StringToHex(inttostr(old_a1)));
Gruß K-H |
AW: Query an Gameserver
Danke für den Hinweis mit dem Leeren des Buffers. Leider hat dies noch nicht zum Erfolg geführt.
Delphi-Quellcode:
Nachdem ich das ganze ein wenig überarbeitet habe, komme ich nun wenigstens schonmal zu einem sicheren Ergebnis. Ich bekomme immer exakt 2 Mal die selbe Liste (geplant -> y - Vaiable) obwohl ich die IP Adresse im "response" ändere.
FillChar(buffer, SizeOf(buffer), 0);
Hier erstmal der Code:
Delphi-Quellcode:
Mir scheint ich hab immernoch ein Problem damit die empfangenen Bytes für die IP und das Word für den Port richtig zu übergeben.
procedure Get_SERVER_LIST();
var request : TServerListRequest; response : TServerListResponse; buffer : TBufferArray; i : Integer; y : Integer; new_a1 : byte; new_a2 : byte; new_a3 : byte; new_a4 : byte; new_a5 : word; old_a1 : byte; old_a2 : byte; old_a3 : byte; old_a4 : byte; old_a5 : word; old_seed : String; new_seed : String; begin old_a1 := 30; old_a2 := 30; old_a3 := 30; old_a4 := 30; old_a5 := 3030; // \gamedir\arma2arrowpc // 31 FF 30 2e 30 2e 30 2e 30 3a 30 00 5c 67 61 6d 65 64 69 72 5c 61 72 6d 61 32 61 72 72 6f 77 70 63 00 // \gamedir\arma3 // 31 ff 30 2e 30 2e 30 2e 30 3a 30 00 5c 67 61 6d 65 64 69 72 5c 61 72 6d 61 33 Form1.udp1.BlockMode := bmNonBlocking; Form1.udp1.RemoteHost := 'hl2master.steampowered.com'; Form1.udp1.RemotePort := '27011'; form1.udp1.Open; y := 0; form1.ListBox1.Clear; if (Form1.udp1.Connected) then begin repeat request.RequestHeader := $31; request.PacketHeader := $FF; request.First := old_a1; request.First_dot := $2E; request.Second := old_a2; request.Second_dot := $2E; request.Third := old_a3; request.Third_dot := $2E; request.Fourth := old_a4; request.double_dot := $3A; request.Port := old_a5; request.Payload[0] := $00; request.Payload[1] := $5C; request.Payload[2] := $67; request.Payload[3] := $61; request.Payload[4] := $6D; request.Payload[5] := $65; request.Payload[6] := $64; request.Payload[7] := $69; request.Payload[8] := $72; request.Payload[9] := $5C; request.Payload[10] := $61; request.Payload[11] := $72; request.Payload[12] := $6D; request.Payload[13] := $61; request.Payload[14] := $33; request.Payload[15] := $00; Form1.Udp1.SendBuf(request, SizeOf(request)); Form1.udp1.WaitForData(250); Form1.Udp1.ReceiveBuf(buffer, SizeOf(buffer)); i := 0; Move(buffer[i], response.ResponseHeader, SizeOf(response.ResponseHeader)); inc(i, SizeOf(response.ResponseHeader)); Move(buffer[i], response.PacketHeader, SizeOf(response.PacketHeader)); inc(i, SizeOf(response.PacketHeader)); repeat Move(buffer[i], response.First, SizeOf(response.First)); inc(i, SizeOf(response.First)); Move(buffer[i], response.Second, SizeOf(response.Second)); inc(i, SizeOf(response.Second)); Move(buffer[i], response.Third, SizeOf(response.Third)); inc(i, SizeOf(response.Third)); Move(buffer[i], response.Fourth, SizeOf(response.Fourth)); inc(i, SizeOf(response.Fourth)); Response.Port := Buffer[i] * 256 + Buffer[i+1]; Inc(i, SizeOf(Response.Port)); // Weil die Bytes vertauscht sind, müssen sie umgedreht werden für ein richtiges new_a1 := response.First; new_a2 := response.Second; new_a3 := response.Third; new_a4 := response.Fourth; new_a5 := response.Port; new_seed := IntToStr(new_a1) + IntToStr(new_a2) + IntToStr(new_a3) + IntToStr(new_a4) + IntToStr(new_a5); if (new_seed <> '00000') then begin form1.duplicatcounter := 0; Form1.ListBox1.Items.Add ((IntToStr(new_a1)) + '.' + (IntToStr(new_a2)) + '.' + (IntToStr(new_a3)) + '.' + (IntToStr(new_a4)) + ':' + IntToStr((new_a5)) ); old_a1 := new_a1; old_a2 := new_a2; old_a3 := new_a3; old_a4 := new_a4; old_a5 := new_a5; old_seed := IntToStr(old_a1) + IntToStr(old_a2) + IntToStr(old_a3) + IntToStr(old_a4) + IntToStr(new_a5); Application.ProcessMessages; end else begin Form1.duplicatcounter := form1.duplicatcounter + 1; end; until form1.duplicatcounter = 1; y := y + 1; Form1.ListBox1.Items.Add ('---------------'); Form1.ListBox1.Items.Add ('---------------'); Form1.ListBox1.Items.Add ('NewSeed: ' + new_seed); Form1.ListBox1.Items.Add ('OldSeed: ' + old_seed); Form1.ListBox1.Items.Add ('---------------'); Form1.ListBox1.Items.Add ('---------------'); FillChar(Buffer, SizeOf(buffer), 0); // Buffer leeren until y = 2; end; Form1.udp1.Close; form1.Caption := 'Servers: ' + inttostr(Form1.ListBox1.Items.Count -1); end; :oops::cry: |
Alle Zeitangaben in WEZ +1. Es ist jetzt 16:12 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz