sync: restore source from Google Drive
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s
This commit is contained in:
1 parent
e2d8247e91
commit
7063d1b1c9
993 files changed
+949976
No files matched your search
@@ -0,0 +1,41 @@
|
||||
unit KM_NetAuthUnsecure;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, KM_CommonClasses;
|
||||
|
||||
type
|
||||
TKMNetSecurity = class
|
||||
public
|
||||
class procedure GenerateChallenge(M: TKMemoryStream; aSender: Integer);
|
||||
class function SolveChallenge(M: TKMemoryStream; aSender: Integer): TKMemoryStream;
|
||||
class function ValidateSolution(M: TKMemoryStream; aSender: Integer): Boolean;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
{ TKMNetSecurity }
|
||||
class procedure TKMNetSecurity.GenerateChallenge(M: TKMemoryStream; aSender: Integer);
|
||||
begin
|
||||
//Leave M unchanged
|
||||
end;
|
||||
|
||||
|
||||
class function TKMNetSecurity.SolveChallenge(M: TKMemoryStream; aSender: Integer): TKMemoryStream;
|
||||
begin
|
||||
Result := TKMemoryStreamBinary.Create;
|
||||
//Result.Write(Integer(GAME_BETA_REVISION));
|
||||
end;
|
||||
|
||||
|
||||
class function TKMNetSecurity.ValidateSolution(M: TKMemoryStream; aSender: Integer): Boolean;
|
||||
//var
|
||||
// BetaVersion: Integer;
|
||||
begin
|
||||
// M.Read(BetaVersion);
|
||||
// Result := BetaVersion = GAME_BETA_REVISION;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
end.
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
unit KM_NetClient;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, KM_NetTypes
|
||||
{$IFDEF WDC} ,KM_NetClientOverbyte {$ENDIF}
|
||||
{$IFDEF FPC} ,KM_NetClientLNet {$ENDIF}
|
||||
{,KM_Log};
|
||||
|
||||
{ Contains basic items we need for smooth Net experience:
|
||||
|
||||
- connect to server
|
||||
- signal if we have successfully connected to server
|
||||
- signal if we could not connect to server
|
||||
|
||||
- disconnect from server (always successful)
|
||||
- signal if we were forcefully disconnected by server
|
||||
|
||||
- send binary data to other server clients
|
||||
- recieve binary data from other server clients
|
||||
|
||||
- optionaly report non-important status messages }
|
||||
|
||||
type
|
||||
{$IFDEF WDC}
|
||||
TKMNetClientImplementation = class(TKMNetClientOverbyte);
|
||||
{$ENDIF}
|
||||
{$IFDEF FPC}
|
||||
TKMNetClientImplementation = class(TKMNetClientLNet);
|
||||
{$ENDIF}
|
||||
|
||||
TKMNetClient = class;
|
||||
TNotifySenderDataEvent = procedure (aNetClient: TKMNetClient; aSenderIndex: SmallInt; aData: Pointer; aLength: Cardinal) of object;
|
||||
|
||||
TKMNetClient = class
|
||||
private
|
||||
fClient: TKMNetClientImplementation;
|
||||
fConnected: Boolean;
|
||||
|
||||
fTotalSize: Cardinal;
|
||||
fBufferSize: Cardinal;
|
||||
fBuffer: array of Byte;
|
||||
|
||||
fOnConnectSucceed: TNotifyEvent;
|
||||
fOnConnectFailed: TGetStrProc;
|
||||
fOnForcedDisconnect: TNotifyEvent;
|
||||
fOnRecieveData: TNotifySenderDataEvent;
|
||||
fOnStatusMessage: TGetStrProc;
|
||||
procedure Error(const aText: string);
|
||||
procedure ConnectSucceed(Sender: TObject);
|
||||
procedure ConnectFailed(const aText: string);
|
||||
procedure ForcedDisconnect(Sender: TObject);
|
||||
procedure RecieveData(aData: Pointer; aLength: Cardinal);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
property Connected: Boolean read fConnected;
|
||||
function MyIPString: string;
|
||||
function SendBufferEmpty: Boolean;
|
||||
|
||||
procedure ConnectTo(const aAddress: string; const aPort: Word); //Try to connect to server
|
||||
property OnConnectSucceed: TNotifyEvent write fOnConnectSucceed; //Signal success
|
||||
property OnConnectFailed: TGetStrProc write fOnConnectFailed; //Signal fail and text description
|
||||
|
||||
procedure Disconnect; //Disconnect from server
|
||||
property OnForcedDisconnect: TNotifyEvent write fOnForcedDisconnect; //Signal we were forcelly disconnected
|
||||
|
||||
property OnRecieveData: TNotifySenderDataEvent write fOnRecieveData;
|
||||
procedure SetHandleBackgrounException;
|
||||
procedure SendData(aSender, aRecepient: TKMNetHandleIndex; aData: Pointer; aLength: Cardinal);
|
||||
procedure UpdateStateIdle;
|
||||
|
||||
property OnStatusMessage: TGetStrProc write fOnStatusMessage;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
{ TKMNetClient }
|
||||
constructor TKMNetClient.Create;
|
||||
begin
|
||||
inherited;
|
||||
|
||||
fClient := TKMNetClientImplementation.Create;
|
||||
fConnected := False;
|
||||
SetLength(fBuffer, 0);
|
||||
fBufferSize := 0;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetClient.Destroy;
|
||||
begin
|
||||
fClient.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetClient.MyIPString:string;
|
||||
begin
|
||||
Result := fClient.MyIPString;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetClient.SendBufferEmpty: Boolean;
|
||||
begin
|
||||
Result := fClient.SendBufferEmpty;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.Error(const aText: string);
|
||||
begin
|
||||
if Assigned(fOnStatusMessage) then fOnStatusMessage('Client: Error ' + aText);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.SetHandleBackgrounException;
|
||||
begin
|
||||
fClient.SetHandleBackgrounException;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.ConnectTo(const aAddress: string; const aPort: Word);
|
||||
begin
|
||||
SetLength(fBuffer, 0);
|
||||
fBufferSize := 0;
|
||||
fClient.OnError := Error;
|
||||
fClient.OnConnectSucceed := ConnectSucceed;
|
||||
fClient.OnConnectFailed := ConnectFailed;
|
||||
fClient.OnSessionDisconnected := ForcedDisconnect;
|
||||
fClient.OnRecieveData := RecieveData;
|
||||
fClient.ConnectTo(aAddress, aPort);
|
||||
if Assigned(fOnStatusMessage) then fOnStatusMessage('Client: Connecting..');
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.ConnectSucceed(Sender: TObject);
|
||||
begin
|
||||
fConnected := True;
|
||||
if Assigned(fOnStatusMessage) then fOnStatusMessage('Client: Connected');
|
||||
if Assigned(fOnConnectSucceed) then fOnConnectSucceed(Self);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.ConnectFailed(const aText: string);
|
||||
begin
|
||||
fConnected := False;
|
||||
if Assigned(fOnStatusMessage) then fOnStatusMessage('Client: Connection failed. ' + aText);
|
||||
if Assigned(fOnConnectFailed) then fOnConnectFailed(aText);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.Disconnect;
|
||||
begin
|
||||
fOnConnectSucceed := nil;
|
||||
fOnConnectFailed := nil;
|
||||
fOnForcedDisconnect := nil;
|
||||
fOnRecieveData := nil;
|
||||
fOnStatusMessage := nil;
|
||||
|
||||
SetLength(fBuffer,0);
|
||||
fBufferSize := 0;
|
||||
|
||||
fConnected := False;
|
||||
fClient.Disconnect;
|
||||
end;
|
||||
|
||||
|
||||
//Happens in following cases:
|
||||
// - when we deliberately disconnect
|
||||
// - when connection failed
|
||||
// - when server disconnects us
|
||||
procedure TKMNetClient.ForcedDisconnect(Sender: TObject);
|
||||
begin
|
||||
if fConnected then
|
||||
begin
|
||||
if Assigned(fOnStatusMessage) then
|
||||
fOnStatusMessage('Client: Forced disconnect');
|
||||
if Assigned(fOnForcedDisconnect) then
|
||||
fOnForcedDisconnect(Self);
|
||||
end;
|
||||
fConnected := False;
|
||||
end;
|
||||
|
||||
|
||||
// Assemble the packet as [Sender.Recepient.Length.Data]
|
||||
// We can pack/clean the header later on (if we hit bandwidth limits)
|
||||
procedure TKMNetClient.SendData(aSender, aRecepient: TKMNetHandleIndex; aData: Pointer; aLength: Cardinal);
|
||||
var
|
||||
P: Pointer;
|
||||
begin
|
||||
// We use fSocket.Shutdown(1) for disconnection in Overbyte implementation,
|
||||
// then we have to check if we actually connected to server before sending any data
|
||||
// Otherwise we could get "ESocketException: Can't send after socket shutdown"
|
||||
if not fConnected then Exit;
|
||||
|
||||
Assert(aLength <= MAX_PACKET_SIZE, 'Packet over size limit');
|
||||
|
||||
// Sender 2bytes
|
||||
// Recepient 2bytes
|
||||
// Length 2bytes
|
||||
//.Data MAX_PACKET_SIZE
|
||||
GetMem(P, aLength + 6);
|
||||
try
|
||||
PKMNetHandleIndex(P)^ := aSender;
|
||||
PKMNetHandleIndex(NativeUInt(P) + 2)^ := aRecepient;
|
||||
PWord(NativeUInt(P) + 4)^ := aLength;
|
||||
Move(aData^, Pointer(NativeUInt(P) + 6)^, aLength);
|
||||
fClient.SendData(P, aLength + 6);
|
||||
finally
|
||||
FreeMem(P);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
//Split recieved data into single packets
|
||||
procedure TKMNetClient.RecieveData(aData: Pointer; aLength: Cardinal);
|
||||
var
|
||||
packetSender: TKMNetHandleIndex;
|
||||
packetLength: Word;
|
||||
// HeaderSize: Byte;
|
||||
begin
|
||||
//Append new data to buffer
|
||||
SetLength(fBuffer, fBufferSize + aLength);
|
||||
Move(aData^, fBuffer[fBufferSize], aLength);
|
||||
fBufferSize := fBufferSize + aLength;
|
||||
//gLog.AddTime('############### recieve data: Length = ' + IntToStr(aLength) + ' fBufferSize = ' + IntTOStr(fBufferSize));
|
||||
|
||||
//HeaderSize := 2*SizeOf(TKMNetHandleIndex) +
|
||||
|
||||
while fBufferSize >= 1 do
|
||||
begin
|
||||
//Try to read data packet from buffer
|
||||
// PacksCnt := PNativeUInt(@fBuffer[0])^;
|
||||
//gLog.AddTime('%%%%% receive cumulative packet: packs Cnt = ' + IntToStr(PacksCnt));
|
||||
while (fBufferSize >= 7) and (PByte(@fBuffer[0])^ > 0) do
|
||||
begin
|
||||
packetSender := PKMNetHandleIndex(@fBuffer[1])^;
|
||||
//We skip PacketRecipient because thats us
|
||||
packetLength := PWord(@fBuffer[5])^;
|
||||
|
||||
//gLog.AddTime('pack %d: sender = %s length = %d' , [PacksCnt - PNativeUInt(@fBuffer[0])^ + 1, GetNetAddressStr(PacketSender), PacketLength]);
|
||||
//Buffer is lengthy enough to contain full packet, process it
|
||||
if packetLength <= fBufferSize - 7 then
|
||||
begin
|
||||
Inc(fTotalSize, packetLength);
|
||||
//Skip packet header
|
||||
// Check if fOnReceiveData is assigned (TKMServerQuery could disconnect client but some messages can still arrive and we are in a different thread at this moment)
|
||||
if Assigned(fOnRecieveData) then
|
||||
fOnRecieveData(Self, packetSender, @fBuffer[7], packetLength);
|
||||
|
||||
//Check if Network was stopped by processing above packet (e.g. version mismatch)
|
||||
if not Assigned(fOnRecieveData) then
|
||||
Exit;
|
||||
|
||||
//Trim received packet from buffer
|
||||
if packetLength < fBufferSize - 7 then //Check range
|
||||
Move(fBuffer[7 + packetLength], fBuffer[1], fBufferSize-packetLength-7);
|
||||
fBufferSize := fBufferSize - packetLength - 6;
|
||||
PByte(@fBuffer[0])^ := PByte(@fBuffer[0])^ - 1;
|
||||
end else
|
||||
begin
|
||||
//gLog.AddTime('---xxx break: PacketLength = %d < %d = BufferSize-16', [PacketLength, fBufferSize-16]);
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
if PByte(@fBuffer[0])^ = 0 then
|
||||
begin
|
||||
Move(fBuffer[1], fBuffer[0], fBufferSize - 1);
|
||||
fBufferSize := fBufferSize - 1;
|
||||
//gLog.AddTime('---%%%% End of cumulative packet. TotalSize = ' + IntToStr(fTotalSize));
|
||||
fTotalSize := 0;
|
||||
end else
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClient.UpdateStateIdle;
|
||||
begin
|
||||
{$IFDEF FPC} fClient.UpdateStateIdle; {$ENDIF}
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
unit KM_NetClientLNet;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, Math, SysUtils, LNet, dialogs;
|
||||
|
||||
|
||||
{ This unit knows nothing about KaM, it's just a puppet in hands of KM_ClientControl,
|
||||
doing all the low level work on TCP. So we can replace this unit with other TCP client
|
||||
without KaM even noticing. }
|
||||
const
|
||||
MAX_SEND_BUFFER = 1048576; //1 MB
|
||||
|
||||
type
|
||||
TNotifyDataEvent = procedure(aData: Pointer; aLength: Cardinal) of object;
|
||||
|
||||
TKMNetClientLNet = class
|
||||
private
|
||||
fSocket: TLTCP;
|
||||
fBuffer: array of byte;
|
||||
fBufferLen: Cardinal;
|
||||
|
||||
fOnError: TGetStrProc;
|
||||
fOnConnectSucceed: TNotifyEvent;
|
||||
fOnConnectFailed: TGetStrProc;
|
||||
fOnSessionDisconnected: TNotifyEvent;
|
||||
fOnRecieveData: TNotifyDataEvent;
|
||||
procedure Connected(aSocket: TLSocket);
|
||||
procedure Disconnected(aSocket: TLSocket);
|
||||
procedure Receive(aSocket: TLSocket);
|
||||
procedure Error(const aMsg: string; aSocket: TLSocket);
|
||||
procedure CanSend(aSocket: TLSocket);
|
||||
|
||||
procedure PutInBuffer(aData: Pointer; aLength: Cardinal);
|
||||
procedure AttemptSend;
|
||||
function BufferFull: Boolean;
|
||||
public
|
||||
destructor Destroy; override;
|
||||
function MyIPString: string;
|
||||
function SendBufferEmpty: Boolean;
|
||||
procedure ConnectTo(const aAddress: string; const aPort: Word);
|
||||
procedure Disconnect;
|
||||
procedure SendData(aData: Pointer; aLength: Cardinal);
|
||||
procedure SetHandleBackgrounException;
|
||||
procedure UpdateStateIdle;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnConnectSucceed: TNotifyEvent write fOnConnectSucceed;
|
||||
property OnConnectFailed: TGetStrProc write fOnConnectFailed;
|
||||
property OnSessionDisconnected: TNotifyEvent write fOnSessionDisconnected;
|
||||
property OnRecieveData: TNotifyDataEvent write fOnRecieveData;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
{ TKMNetClientLNet }
|
||||
destructor TKMNetClientLNet.Destroy;
|
||||
begin
|
||||
fSocket.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetClientLNet.MyIPString: string;
|
||||
begin
|
||||
Result := 'Not implemented yet';
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetClientLNet.SendBufferEmpty: Boolean;
|
||||
begin
|
||||
Result := (fSocket = nil) or (fBufferLen = 0);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.ConnectTo(const aAddress: string; const aPort: Word);
|
||||
begin
|
||||
FreeAndNil(fSocket);
|
||||
fSocket := TLTCP.Create(nil);
|
||||
fSocket.OnDisconnect := Disconnected;
|
||||
fSocket.OnConnect := Connected;
|
||||
fSocket.OnReceive := Receive;
|
||||
fSocket.OnError := Error;
|
||||
fSocket.Timeout := 0;
|
||||
try
|
||||
fSocket.Connect(aAddress, aPort);
|
||||
fSocket.CallAction;
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
//Trap the exception and tell the user. Note: While debugging, Delphi will still stop execution for the exception, but normally the dialouge won't show.
|
||||
fOnConnectFailed(E.Message);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.Disconnect;
|
||||
begin
|
||||
if fSocket <> nil then fSocket.Disconnect(True);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.PutInBuffer(aData: Pointer; aLength: Cardinal);
|
||||
begin
|
||||
SetLength(fBuffer, fBufferLen + aLength);
|
||||
Move(aData^, fBuffer[fBufferLen], aLength);
|
||||
fBufferLen := fBufferLen + aLength;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.AttemptSend;
|
||||
var
|
||||
LenSent: Integer;
|
||||
begin
|
||||
if fBufferLen <= 0 then Exit;
|
||||
|
||||
LenSent := fSocket.Send(fBuffer[0], fBufferLen);
|
||||
|
||||
if LenSent > 0 then
|
||||
begin
|
||||
fBufferLen := fBufferLen - LenSent;
|
||||
if fBufferLen > 0 then
|
||||
Move(fBuffer[LenSent], fBuffer[0], fBufferLen);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TKMNetClientLNet.BufferFull: Boolean;
|
||||
begin
|
||||
Result := fBufferLen >= MAX_SEND_BUFFER;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.SendData(aData: Pointer; aLength: Cardinal);
|
||||
begin
|
||||
if fSocket.Connected then
|
||||
begin
|
||||
if BufferFull then
|
||||
begin
|
||||
fOnError('LNet Client Error: Send buffer full');
|
||||
Exit;
|
||||
end;
|
||||
PutInBuffer(aData, aLength);
|
||||
AttemptSend;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.SetHandleBackgrounException;
|
||||
begin
|
||||
// Do nothing for now
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.CanSend(aSocket: TLSocket);
|
||||
begin
|
||||
AttemptSend;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.Connected(aSocket: TLSocket);
|
||||
begin
|
||||
fOnConnectSucceed(Self);
|
||||
aSocket.SetState(ssNoDelay, True); //Send packets ASAP (disables Nagle's algorithm)
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.Disconnected(aSocket: TLSocket);
|
||||
begin
|
||||
fOnSessionDisconnected(Self);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.Receive(aSocket: TLSocket);
|
||||
const
|
||||
BUFFER_SIZE = 10240; //10kb
|
||||
var
|
||||
P: Pointer;
|
||||
L: Integer; //L could be -1 when no data is available
|
||||
begin
|
||||
GetMem(P, BUFFER_SIZE+1); //+1 to avoid RangeCheckError when L = BufferSize
|
||||
L := aSocket.Get(P^, BUFFER_SIZE);
|
||||
|
||||
if L > 0 then //if L=0 then exit;
|
||||
fOnRecieveData(P, L);
|
||||
|
||||
FreeMem(P);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.Error(const aMsg: string; aSocket: TLSocket);
|
||||
begin
|
||||
fOnError('LNet Client Error: ' + aMsg);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientLNet.UpdateStateIdle;
|
||||
begin
|
||||
if fSocket <> nil then fSocket.CallAction; //Process network events
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
unit KM_NetClientOverbyte;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, OverbyteIcsWSocket, OverbyteIcsWndControl, WinSock;
|
||||
|
||||
|
||||
{ This unit knows nothing about KaM, it's just a puppet in hands of KM_ClientControl,
|
||||
doing all the low level work on TCP. So we can replace this unit with other TCP client
|
||||
without KaM even noticing. }
|
||||
type
|
||||
TNotifyDataEvent = procedure(aData: Pointer; aLength: Cardinal) of object;
|
||||
|
||||
TKMNetClientOverbyte = class
|
||||
private
|
||||
fSocket: TWSocket;
|
||||
fOnError: TGetStrProc;
|
||||
fOnConnectSucceed: TNotifyEvent;
|
||||
fOnConnectFailed: TGetStrProc;
|
||||
fOnSessionDisconnected: TNotifyEvent;
|
||||
fOnRecieveData: TNotifyDataEvent;
|
||||
procedure Connected(Sender: TObject; aError: Word);
|
||||
procedure Disconnected(Sender: TObject; aError: Word);
|
||||
procedure DataAvailable(Sender: TObject; aError: Word);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
function MyIPString: string;
|
||||
function SendBufferEmpty: Boolean;
|
||||
procedure ConnectTo(const aAddress: string; const aPort: Word);
|
||||
procedure Disconnect;
|
||||
procedure SendData(aData: Pointer; aLength: Cardinal);
|
||||
procedure SetHandleBackgrounException;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnConnectSucceed: TNotifyEvent write fOnConnectSucceed;
|
||||
property OnConnectFailed: TGetStrProc write fOnConnectFailed;
|
||||
property OnSessionDisconnected: TNotifyEvent write fOnSessionDisconnected;
|
||||
property OnRecieveData: TNotifyDataEvent write fOnRecieveData;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
{ TKMNetClientOverbyte }
|
||||
constructor TKMNetClientOverbyte.Create;
|
||||
var
|
||||
wsaData: TWSAData;
|
||||
begin
|
||||
inherited Create;
|
||||
Assert(WSAStartup($101, wsaData) = 0, 'Error in Network');
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetClientOverbyte.Destroy;
|
||||
begin
|
||||
fSocket.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetClientOverbyte.MyIPString: string;
|
||||
begin
|
||||
if LocalIPList.Count >= 1 then
|
||||
Result := LocalIPList[0] //First address should be ours
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientOverbyte.ConnectTo(const aAddress: string; const aPort: Word);
|
||||
begin
|
||||
FreeAndNil(fSocket);
|
||||
fSocket := TWSocket.Create(nil);
|
||||
fSocket.ComponentOptions := [wsoTcpNoDelay]; //Send packets ASAP (disables Nagle's algorithm)
|
||||
fSocket.Proto := 'tcp';
|
||||
fSocket.Addr := aAddress;
|
||||
fSocket.Port := IntToStr(aPort);
|
||||
fSocket.OnSessionClosed := Disconnected;
|
||||
fSocket.OnSessionConnected := Connected;
|
||||
fSocket.OnDataAvailable := DataAvailable;
|
||||
try
|
||||
// Failure to connect could be caused by Firewall on DedicatedServer side
|
||||
fSocket.Connect;
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
// Trap the exception and tell the user.
|
||||
// Note: While debugging, Delphi will still stop execution for the exception, but normally the dialouge won't show.
|
||||
fOnConnectFailed(E.Message);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientOverbyte.Disconnect;
|
||||
begin
|
||||
if fSocket <> nil then
|
||||
begin
|
||||
// ShutDown(1) Works better than Close or CloseDelayed
|
||||
// With Close or CloseDelayed some data that was sent just before disconnection could not be delivered to server.
|
||||
// E.g. mkDisconnect packet
|
||||
// But we can't send data into ShutDown'ed socket (we could try into Closed one, since it will have State wsClosed)
|
||||
fSocket.ShutDown(1);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientOverbyte.SendData(aData: Pointer; aLength: Cardinal);
|
||||
begin
|
||||
if fSocket.State = wsConnected then //Sometimes this occurs just before disconnect/reconnect
|
||||
fSocket.Send(aData, aLength);
|
||||
end;
|
||||
|
||||
|
||||
// Handle all 'background (unhandled)' exceptions, so we will be able to intercept them with madExcept
|
||||
procedure TKMNetClientOverbyte.SetHandleBackgrounException;
|
||||
begin
|
||||
// From OverbyteIcsWndControl unit comment section:
|
||||
|
||||
// Call setter function
|
||||
// SetIcsThreadLocalFinalBgExceptionHandling() to enable one of
|
||||
// the following options in current thread context. With
|
||||
// "fehAppHandleException" unhandled exceptions are passed to the
|
||||
// Application exception handler if available, with
|
||||
// "fehShowException" unhandled exceptions are displayed either
|
||||
// in the console or through Windows MessageBox API (owner HWND = 0).
|
||||
// Both options allow tools like MadExcept to catch and display
|
||||
// the exception, with "fehNone" (default) unhandled exceptions
|
||||
// are thrown away silently.
|
||||
|
||||
// Both options fehAppHandleException and fehShowException will show madExcept dialog
|
||||
SetIcsThreadLocalFinalBgExceptionHandling(fehAppHandleException);
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetClientOverbyte.SendBufferEmpty: Boolean;
|
||||
begin
|
||||
if (fSocket <> nil) and (fSocket.State = wsConnected) then
|
||||
Result := fSocket.AllSent
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientOverbyte.Connected(Sender: TObject; aError: Word);
|
||||
begin
|
||||
if aError <> 0 then
|
||||
fOnConnectFailed('Error: '+WSocketErrorDesc(aError)+' (#' + IntToStr(aError)+')')
|
||||
else
|
||||
begin
|
||||
fOnConnectSucceed(Self);
|
||||
fSocket.SetTcpNoDelayOption; //Send packets ASAP (disables Nagle's algorithm)
|
||||
fSocket.SocketSndBufSize := 65535; //WinSock buffer should be bigger than internal buffer
|
||||
fSocket.BufSize := 32768;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientOverbyte.Disconnected(Sender: TObject; aError: Word);
|
||||
begin
|
||||
//Do not exit on error, because when a disconnect error occurs, the client has still disconnected
|
||||
if aError <> 0 then
|
||||
fOnError('Client: Disconnection error: '+WSocketErrorDesc(aError)+' (#' + IntToStr(aError)+')');
|
||||
|
||||
fOnSessionDisconnected(Self);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetClientOverbyte.DataAvailable(Sender: TObject; aError: Word);
|
||||
const
|
||||
BUFFER_SIZE = 10240; //10kb
|
||||
var
|
||||
P: Pointer;
|
||||
L: Integer; //L could be -1 when no data is available
|
||||
begin
|
||||
if aError <> 0 then
|
||||
begin
|
||||
fOnError('DataAvailable. Error '+WSocketErrorDesc(aError)+' (#' + IntToStr(aError)+')');
|
||||
Exit;
|
||||
end;
|
||||
|
||||
GetMem(P, BUFFER_SIZE + 1); //+1 to avoid RangeCheckError when L = BufferSize
|
||||
L := TWSocket(Sender).Receive(P, BUFFER_SIZE);
|
||||
|
||||
if L > 0 then //if L=0 then exit;
|
||||
fOnRecieveData(P, L);
|
||||
|
||||
FreeMem(P);
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
unit KM_NetConsts;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
KM_NetTypes;
|
||||
|
||||
|
||||
const
|
||||
NET_MP_GAME_STATE: array [TKMNetGameState] of TMPGameState = (mgsNone, mgsNone, mgsNone, mgsLobby, mgsLoading, mgsGame, mgsGame);
|
||||
NET_ALLOWED_PACKETS_SET: array [TKMNetGameState] of set of TKMNetMessageKind = (
|
||||
//lgsNone
|
||||
[],
|
||||
//lgsConnecting
|
||||
[mkRefuseToJoin, mkIndexOnServer, mkNetProtocolVersion, mkWelcomeMessage, mkPing,
|
||||
mkConnectedToRoom, mkPingFpsInfo, mkKicked, mkServerName, mkReqPassword],
|
||||
//lgsQuery
|
||||
[mkAllowToJoin, mkRefuseToJoin, mkAuthChallenge, mkPing, mkPingFpsInfo, mkKicked],
|
||||
//lgsLobby
|
||||
[mkAskForAuth, mkAskToJoin, mkClientLost, mkReassignHost, mkDisconnect, mkPing, mkPingFpsInfo, mkPlayersList,
|
||||
mkRequestHand, mkRequestTeam, mkRequestFlagColor, mkResetMap, mkMapSelect, mkSaveSelect,
|
||||
mkReadyToStart, mkStart, mkTextChat, mkKicked, mkLangCode, mkGameOptions, mkServerName,
|
||||
mkFileRequest, mkFileSendStarted, mkFileChunk, mkFileEnd, mkFileAck, mkFileProgress,
|
||||
mkTextTranslated, mkHasMapOrSave, mkSetPassword],
|
||||
//lgsLoading
|
||||
[mkAskForAuth, mkClientLost, mkReassignHost, mkDisconnect, mkPing, mkPingFpsInfo, mkPlayersList,
|
||||
mkReadyToPlay, mkPlay, mkTextChat, mkKicked, mkTextTranslated, mkVote],
|
||||
//lgsGame
|
||||
[mkAskForAuth, mkClientLost, mkReassignHost, mkDisconnect, mkPing, mkPingFpsInfo, {mkFPS,} mkPlayersList, mkReadyToReturnToLobby,
|
||||
mkCommands, mkTextChat, mkResyncFromTick, mkAskToReconnect, mkKicked, mkClientReconnected, mkTextTranslated, mkVote,
|
||||
mkAskToSendCrashreport],
|
||||
//lgsReconnecting
|
||||
[mkIndexOnServer, mkNetProtocolVersion, mkWelcomeMessage, mkPing, {mkFPS, }mkConnectedToRoom,
|
||||
mkPingFpsInfo, mkPlayersList, mkReconnectionAccepted, mkRefuseReconnect, mkKicked]
|
||||
);
|
||||
|
||||
JOIN_TIMEOUT = 8000; //8 sec. Timeout for join queries
|
||||
RECONNECT_PAUSE = 3000; //Time in ms which we wait before attempting to reconnect (stops the socket from becoming overloaded)
|
||||
VOTE_TIMEOUT = 60000; //60 sec. Timeout for votes
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
unit KM_NetDedicatedServer;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
SysUtils, Classes, Math,
|
||||
{$IFDEF MSWindows}Windows,{$ENDIF}
|
||||
KM_NetServer, KM_NetServerLocator, KM_NetUDP, KM_CommonTypes;
|
||||
|
||||
|
||||
type
|
||||
TKMNetDedicatedServer = class
|
||||
private
|
||||
fLastPing, fLastAnnounce: cardinal;
|
||||
fNetServer: TKMNetServer;
|
||||
fServerLocator: TKMNetServerLocator;
|
||||
fUDPAnnounce: TKMNetUDPAnnounce;
|
||||
fOnMessage: TUnicodeStringEvent;
|
||||
fPublishServer: Boolean;
|
||||
fAnnounceInterval: Word;
|
||||
fPingInterval: Word;
|
||||
fServerName: AnsiString;
|
||||
fPort: Word;
|
||||
fAnnounceUDP: Boolean;
|
||||
procedure StatusMessage(const aData: string);
|
||||
procedure MasterServerError(const aData: string);
|
||||
public
|
||||
constructor Create(aMaxRooms, aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort: Word;
|
||||
const aMasterServerAddress: String; const aHTMLStatusFile: String;
|
||||
const aWelcomeMessage: UnicodeString; const aPacketsAccDelay: Integer; aDedicated: Boolean);
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure Start(const aServerName: AnsiString; const aPort: Word; aPublishServer, aAnnounceUDP: Boolean);
|
||||
procedure Stop;
|
||||
procedure UpdateState;
|
||||
procedure UpdateSettings(const aServerName: AnsiString; aPublishServer, aAnnounceUDP: Boolean;
|
||||
aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort: Word;
|
||||
const aMasterServerAddress: string; const aHTMLStatusFile: string;
|
||||
const aWelcomeMessage: UnicodeString; const aServerPacketsAccDelay: Integer);
|
||||
property OnMessage: TUnicodeStringEvent write fOnMessage;
|
||||
|
||||
procedure GetServerInfo(aList: TList);
|
||||
function IsListening: Boolean;
|
||||
|
||||
property Server: TKMNetServer read fNetServer;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
KM_CommonUtils;
|
||||
|
||||
const
|
||||
// Enforce a minimum so our master server doesn't get spammed
|
||||
MINIMUM_ANNOUNCE_INTERVAL = 180;
|
||||
|
||||
|
||||
{ TKMDedicatedServer }
|
||||
constructor TKMNetDedicatedServer.Create(aMaxRooms, aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort: Word;
|
||||
const aMasterServerAddress: String; const aHTMLStatusFile: String;
|
||||
const aWelcomeMessage: UnicodeString; const aPacketsAccDelay: Integer; aDedicated: Boolean);
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fNetServer := TKMNetServer.Create(aMaxRooms, aKickTimeout, aHTMLStatusFile, aWelcomeMessage, aPacketsAccDelay);
|
||||
fServerLocator := TKMNetServerLocator.Create(aMasterServerAddress, aDedicated);
|
||||
fServerLocator.OnError := MasterServerError;
|
||||
fUDPAnnounce := TKMNetUDPAnnounce.Create(aServerUDPScanPort);
|
||||
fUDPAnnounce.OnError := StatusMessage;
|
||||
|
||||
fAnnounceInterval := Max(MINIMUM_ANNOUNCE_INTERVAL, aAnnounceInterval);
|
||||
fPingInterval := aPingInterval;
|
||||
fLastPing := 0;
|
||||
fLastAnnounce := 0;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetDedicatedServer.Destroy;
|
||||
begin
|
||||
FreeAndNil(fNetServer);
|
||||
FreeAndNil(fServerLocator);
|
||||
FreeAndNil(fUDPAnnounce);
|
||||
StatusMessage('Server destroyed');
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.Start(const aServerName: AnsiString; const aPort: Word; aPublishServer, aAnnounceUDP: Boolean);
|
||||
begin
|
||||
fPort := aPort;
|
||||
fServerName := aServerName;
|
||||
fPublishServer := aPublishServer;
|
||||
fAnnounceUDP := aAnnounceUDP;
|
||||
fNetServer.OnStatusMessage := StatusMessage;
|
||||
fNetServer.StartListening(fPort, fServerName);
|
||||
fUDPAnnounce.StartAnnouncing(fPort, fServerName, fAnnounceUDP);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.Stop;
|
||||
begin
|
||||
fNetServer.StopListening;
|
||||
fNetServer.ClearClients;
|
||||
fUDPAnnounce.StopAnnouncing;
|
||||
StatusMessage('Stopped listening');
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.UpdateState;
|
||||
var
|
||||
tickCount:Cardinal;
|
||||
begin
|
||||
fNetServer.UpdateStateIdle;
|
||||
fServerLocator.UpdateStateIdle;
|
||||
fUDPAnnounce.UpdateStateIdle;
|
||||
|
||||
if not fNetServer.Listening then Exit; //Do not measure pings or announce the server if we are not listening
|
||||
|
||||
tickCount := TimeGet;
|
||||
if TimeSince(fLastPing) >= fPingInterval then
|
||||
begin
|
||||
fNetServer.MeasurePings;
|
||||
fLastPing := tickCount;
|
||||
end;
|
||||
|
||||
if fPublishServer and (TimeSince(fLastAnnounce) >= fAnnounceInterval*1000) then
|
||||
begin
|
||||
fServerLocator.AnnounceServer(UnicodeString(fServerName), fPort, fNetServer.GetPlayerCount, fAnnounceInterval + 20);
|
||||
fLastAnnounce := tickCount;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.UpdateSettings(const aServerName: AnsiString; aPublishServer, aAnnounceUDP: Boolean;
|
||||
aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort: Word;
|
||||
const aMasterServerAddress: String; const aHTMLStatusFile: String;
|
||||
const aWelcomeMessage: UnicodeString;
|
||||
const aServerPacketsAccDelay: Integer);
|
||||
begin
|
||||
fAnnounceInterval := Max(MINIMUM_ANNOUNCE_INTERVAL, aAnnounceInterval);
|
||||
fPingInterval := aPingInterval;
|
||||
fServerLocator.MasterServerAddress := aMasterServerAddress;
|
||||
fServerName := aServerName;
|
||||
fPublishServer := aPublishServer;
|
||||
fAnnounceUDP := aAnnounceUDP;
|
||||
|
||||
fNetServer.UpdateSettings(aKickTimeout, aHTMLStatusFile, aWelcomeMessage, aServerName, aServerPacketsAccDelay);
|
||||
fUDPAnnounce.UpdateSettings(aServerName, aServerUDPScanPort);
|
||||
|
||||
fLastAnnounce := 0; //Make the server announce itself next update so the changes are sent to the master server ASAP
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.StatusMessage(const aData: string);
|
||||
begin
|
||||
if Assigned(fOnMessage) then fOnMessage(aData);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.MasterServerError(const aData: string);
|
||||
begin
|
||||
StatusMessage('HTTP Master Server: ' + aData);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetDedicatedServer.GetServerInfo(aList: TList);
|
||||
begin
|
||||
fNetServer.GetServerInfo(aList);
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetDedicatedServer.IsListening: Boolean;
|
||||
begin
|
||||
Result := fNetServer.Listening;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
unit KM_NetFileTransfer;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
SysUtils, Math, KM_Defaults, KM_CommonClasses, KM_NetTypes, KM_MapTypes
|
||||
{$IFDEF FPC}, zstream {$ENDIF}
|
||||
{$IFDEF WDC}, ZLib {$ENDIF};
|
||||
|
||||
const
|
||||
MAX_TRANSFERS = MAX_LOBBY_SLOTS - 1; //One for each player and spectator
|
||||
|
||||
type
|
||||
TKMTransferEvent = procedure(aClientIndex: TKMNetHandleIndex) of object;
|
||||
TKMTransferPacketEvent = procedure(aClientIndex: TKMNetHandleIndex; aStream: TKMemoryStream; out aSendBufferEmpty: Boolean) of object;
|
||||
TKMTransferProgressEvent = procedure(Total, Progress: Cardinal) of object;
|
||||
TKMTransferProgressPlayerEvent = procedure(aNetPlayerIndex: Integer; Total, Progress: Cardinal) of object;
|
||||
TKMTransferType = (kttMap, kttSave);
|
||||
|
||||
TKMFileSender = class
|
||||
private
|
||||
fReceiverIndex: TKMNetHandleIndex;
|
||||
fChunksInFlight: Byte;
|
||||
fSendStream: TKMemoryStream;
|
||||
procedure AddFileToStream(const aFileName, aPostFix, aExt: UnicodeString);
|
||||
public
|
||||
constructor Create(aType: TKMTransferType; const aBundleName: UnicodeString; aMapKind: TKMMapKind; aReceiverIndex: TKMNetHandleIndex);
|
||||
destructor Destroy; override;
|
||||
procedure WriteChunk(aStream: TKMemoryStream; aLength: Cardinal);
|
||||
procedure AckReceived;
|
||||
function StreamEnd: Boolean;
|
||||
property ReceiverIndex: TKMNetHandleIndex read fReceiverIndex;
|
||||
end;
|
||||
|
||||
TKMFileReceiver = class
|
||||
private
|
||||
fReceiveStream: TKMemoryStream;
|
||||
fType: TKMTransferType;
|
||||
fBundleName: UnicodeString;
|
||||
fBundleCRC: Cardinal;
|
||||
fTotalSize: Cardinal;
|
||||
fReceivedSize: Cardinal;
|
||||
procedure ClearExistingFiles;
|
||||
function ValidExtension(const aExt: UnicodeString): Boolean;
|
||||
public
|
||||
constructor Create(aType: TKMTransferType; const aBundleName: UnicodeString; aBundleCRC: Cardinal = 0);
|
||||
destructor Destroy; override;
|
||||
procedure DataReceived(aStream: TKMemoryStream);
|
||||
property TotalSize: Cardinal read fTotalSize;
|
||||
property ReceivedSize: Cardinal read fReceivedSize;
|
||||
function ProcessTransfer: Boolean;
|
||||
end;
|
||||
|
||||
TKMFileSenderManager = class
|
||||
private
|
||||
fSenders: array[1..MAX_TRANSFERS] of TKMFileSender;
|
||||
fOnTransferCompleted: TKMTransferEvent;
|
||||
fOnTransferPacket: TKMTransferPacketEvent;
|
||||
function ActiveTransferCount: Byte;
|
||||
public
|
||||
destructor Destroy; override;
|
||||
function StartNewSend(aType: TKMTransferType; const aBundleName: String; aMapKind: TKMMapKind;
|
||||
aReceiverIndex: TKMNetHandleIndex): Boolean;
|
||||
procedure AbortAllTransfers;
|
||||
procedure AckReceived(aReceiverIndex: TKMNetHandleIndex);
|
||||
procedure ClientDisconnected(aReceiverIndex: TKMNetHandleIndex);
|
||||
procedure UpdateStateIdle(aSendBufferEmpty: Boolean);
|
||||
property OnTransferCompleted: TKMTransferEvent write fOnTransferCompleted;
|
||||
property OnTransferPacket: TKMTransferPacketEvent write fOnTransferPacket;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
KromUtils, KM_Maps, KM_Saves,
|
||||
KM_ScriptPreProcessor, KM_ScriptFilesCollection,
|
||||
KM_Log, KM_FileIO;
|
||||
|
||||
|
||||
const
|
||||
VALID_MAP_EXTENSIONS: array [1..5] of UnicodeString = ('map', 'dat', EXT_FILE_SCRIPT, 'txt', 'pdf');
|
||||
VALID_MAP_EXTENSIONS_POSTFIX: array[1..5] of UnicodeString = ('libx', 'wav', 'ogg', 'pdf', 'tiles');
|
||||
VALID_SAVE_EXTENSIONS: array[1..3] of UnicodeString = (EXT_SAVE_MAIN, EXT_SAVE_BASE, EXT_SAVE_REPLAY);
|
||||
|
||||
|
||||
function GetFullSourceFileName(aType: TKMTransferType; const aName: String; aMapKind: TKMMapKind; const aPostfix, aExt: UnicodeString): String;
|
||||
begin
|
||||
case aType of
|
||||
kttMap: Result := TKMapsCollection.FullPath(aName, aPostfix + '.' + aExt, aMapKind);
|
||||
kttSave: Result := TKMSavesCollection.FullPath(aName, aExt, True);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function GetFullDestFileName(aType: TKMTransferType; const aName, Postfix, aExt: String; const aCustomFileName: UnicodeString = ''): String;
|
||||
begin
|
||||
case aType of
|
||||
kttMap: if aCustomFileName = '' then
|
||||
Result := TKMapsCollection.FullPath(aName, Postfix + '.' + aExt, mkDL)
|
||||
else
|
||||
Result := TKMapsCollection.FullPath(aName, aCustomFileName, Postfix + '.' + aExt, mkDL);
|
||||
kttSave: Result := TKMSavesCollection.Path(DOWNLOADED_LOBBY_SAVE, True) + DOWNLOADED_LOBBY_SAVE + '.' + aExt;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMFileSender }
|
||||
constructor TKMFileSender.Create(aType: TKMTransferType; const aBundleName: UnicodeString; aMapKind: TKMMapKind;
|
||||
aReceiverIndex: TKMNetHandleIndex);
|
||||
var
|
||||
I, J: Integer;
|
||||
F: TSearchRec;
|
||||
fileName, postfix: UnicodeString;
|
||||
sourceStream: TKMemoryStream;
|
||||
compressionStream: TCompressionStream;
|
||||
scriptPreProcessor: TKMScriptPreProcessor;
|
||||
scriptFiles: TKMScriptFilesCollection;
|
||||
begin
|
||||
inherited Create;
|
||||
fReceiverIndex := aReceiverIndex;
|
||||
fSendStream := TKMemoryStreamBinary.Create;
|
||||
fSendStream.PlaceMarker('TransferCompressed');
|
||||
fSendStream.Write(aType, SizeOf(aType));
|
||||
fSendStream.WriteW(aBundleName);
|
||||
//Fill stream with data to be sent
|
||||
case aType of
|
||||
kttMap: begin
|
||||
// Supports next file names:
|
||||
// MapName.dat
|
||||
// MapName.map
|
||||
// MapName.txt
|
||||
// MapName.pdf
|
||||
// MapName.script + any included script
|
||||
for I := Low(VALID_MAP_EXTENSIONS) to High(VALID_MAP_EXTENSIONS) do
|
||||
begin
|
||||
fileName := GetFullSourceFileName(aType, aBundleName, aMapKind, '', VALID_MAP_EXTENSIONS[I]);
|
||||
if FileExists(fileName) then
|
||||
AddFileToStream(fileName, '', VALID_MAP_EXTENSIONS[I]);
|
||||
//Add all included script files
|
||||
if (VALID_MAP_EXTENSIONS[I] = EXT_FILE_SCRIPT) and FileExists(fileName) then
|
||||
begin
|
||||
scriptPreProcessor := TKMScriptPreProcessor.Create(False);
|
||||
try
|
||||
if not scriptPreProcessor.PreProcessFile(fileName) then
|
||||
//throw an Exception if PreProcessor was not successful to cancel FileSender creation
|
||||
raise Exception.Create('Can''n start send file because of error while script pre-processing');
|
||||
scriptFiles := scriptPreProcessor.ScriptFilesInfo;
|
||||
for J := 0 to scriptFiles.IncludedCount - 1 do
|
||||
begin
|
||||
if FileExists(scriptFiles[J].FullFilePath) then
|
||||
AddFileToStream(scriptFiles[J].FullFilePath, '', EXT_FILE_SCRIPT);
|
||||
end;
|
||||
finally
|
||||
scriptPreProcessor.Free;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
// Supports next file names:
|
||||
// MapName.snd_name.wav / .ogg
|
||||
// MapName.snd_name.eng.wav / .ogg
|
||||
// MapName.eng.libx
|
||||
// MapName.eng.pdf
|
||||
for I := Low(fileName) to High(VALID_MAP_EXTENSIONS_POSTFIX) do
|
||||
begin
|
||||
fileName := GetFullSourceFileName(aType, aBundleName, aMapKind, '.*', VALID_MAP_EXTENSIONS_POSTFIX[I]);
|
||||
try
|
||||
if FindFirst(fileName, faAnyFile, F) = 0 then
|
||||
begin
|
||||
repeat
|
||||
if (F.Attr and faDirectory = 0) then
|
||||
begin
|
||||
// Find everything except map name and extension
|
||||
// Cut ext with dot
|
||||
postfix := ChangeFileExt(F.Name, '');
|
||||
// Find 'MapName.'
|
||||
if Pos(aBundleName + '.', postfix) = 1 then
|
||||
// Copy '.snd_name.eng'
|
||||
postfix := Copy(postfix, Length(aBundleName) + 1, Length(postfix))
|
||||
else
|
||||
// No postfix was found
|
||||
postfix := '';
|
||||
|
||||
AddFileToStream(ExtractFilePath(fileName) + F.Name, postfix, VALID_MAP_EXTENSIONS_POSTFIX[I]);
|
||||
end;
|
||||
until FindNext(F) <> 0;
|
||||
end;
|
||||
finally
|
||||
FindClose(F);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
kttSave: for I := Low(VALID_SAVE_EXTENSIONS) to High(VALID_SAVE_EXTENSIONS) do
|
||||
begin
|
||||
fileName := TKMSavesCollection.FullPath(aBundleName, VALID_SAVE_EXTENSIONS[I], True);
|
||||
if FileExists(fileName) then
|
||||
AddFileToStream(fileName, '', VALID_SAVE_EXTENSIONS[I]);
|
||||
end;
|
||||
end;
|
||||
|
||||
// Compress fSendStream
|
||||
sourceStream := fSendStream;
|
||||
fSendStream := TKMemoryStreamBinary.Create;
|
||||
fSendStream.PlaceMarker('Transfer');
|
||||
compressionStream := TCompressionStream.Create(clDefault, fSendStream);
|
||||
compressionStream.CopyFrom(sourceStream, 0);
|
||||
// fSendStream now contains the compressed data from SourceStream
|
||||
compressionStream.Free;
|
||||
sourceStream.Free;
|
||||
fSendStream.Position := 0;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMFileSender.Destroy;
|
||||
begin
|
||||
fSendStream.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileSender.AckReceived;
|
||||
begin
|
||||
Dec(fChunksInFlight);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileSender.AddFileToStream(const aFileName, aPostFix, aExt: UnicodeString);
|
||||
var
|
||||
fileStream: TKMemoryStream;
|
||||
begin
|
||||
fileStream := TKMemoryStreamBinary.Create;
|
||||
fileStream.LoadFromFile(aFileName);
|
||||
|
||||
fSendStream.PlaceMarker('FileStart');
|
||||
fSendStream.WriteW(TruncateExt(ExtractFileName(aFileName)));
|
||||
fSendStream.WriteW(aPostFix);
|
||||
fSendStream.WriteW(aExt);
|
||||
fSendStream.Write(Cardinal(fileStream.Size));
|
||||
if fileStream.Size > 0 then
|
||||
fSendStream.CopyFrom(fileStream, fileStream.Size);
|
||||
|
||||
fileStream.Free;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileSender.WriteChunk(aStream: TKMemoryStream; aLength: Cardinal);
|
||||
begin
|
||||
if aLength > fSendStream.Size - fSendStream.Position then
|
||||
aLength := fSendStream.Size - fSendStream.Position;
|
||||
|
||||
aStream.PlaceMarker('FileChunk');
|
||||
aStream.Write(aLength);
|
||||
aStream.Write(Cardinal(fSendStream.Size)); //Every chunk includes the total transfer size
|
||||
aStream.CopyFrom(fSendStream, aLength);
|
||||
Inc(fChunksInFlight);
|
||||
end;
|
||||
|
||||
|
||||
function TKMFileSender.StreamEnd: Boolean;
|
||||
begin
|
||||
Result := fSendStream.Position = fSendStream.Size;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMFileReceiver }
|
||||
constructor TKMFileReceiver.Create(aType: TKMTransferType; const aBundleName: UnicodeString; aBundleCRC: Cardinal = 0);
|
||||
begin
|
||||
inherited Create;
|
||||
fReceiveStream := TKMemoryStreamBinary.Create;
|
||||
fType := aType;
|
||||
fBundleName := aBundleName;
|
||||
fBundleCRC := aBundleCRC;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMFileReceiver.Destroy;
|
||||
begin
|
||||
FreeAndNil(fReceiveStream);
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileReceiver.DataReceived(aStream: TKMemoryStream);
|
||||
var
|
||||
chunkSize: Cardinal;
|
||||
begin
|
||||
aStream.CheckMarker('FileChunk');
|
||||
aStream.Read(chunkSize);
|
||||
aStream.Read(fTotalSize); //Every chunk includes the total transfer size
|
||||
Assert(aStream.Size - aStream.Position = chunkSize, 'Chunk corrupted');
|
||||
fReceiveStream.CopyFrom(aStream, chunkSize);
|
||||
fReceivedSize := fReceivedSize + chunkSize;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileReceiver.ClearExistingFiles;
|
||||
var
|
||||
searchRec: TSearchRec;
|
||||
fileName, saveFolder: UnicodeString;
|
||||
begin
|
||||
//Prepare destination
|
||||
case fType of
|
||||
kttMap: begin
|
||||
//Create downloads folder if it's missing
|
||||
fileName := ExeDir + MAPS_DL_FOLDER_NAME;
|
||||
if not DirectoryExists(fileName) then
|
||||
CreateDir(fileName);
|
||||
//Create map folder if it is missing
|
||||
fileName := fileName + PathDelim + fBundleName;
|
||||
if not DirectoryExists(fileName) then
|
||||
CreateDir(fileName)
|
||||
else
|
||||
try
|
||||
//If any files already exist in the folder, delete them
|
||||
if FindFirst(fileName + PathDelim + fBundleName + '*.*', faAnyFile, searchRec) = 0 then
|
||||
begin
|
||||
repeat
|
||||
if (searchRec.Attr and faDirectory = 0) then
|
||||
KMDeleteFile(fileName + PathDelim + searchRec.Name);
|
||||
until FindNext(searchRec) <> 0;
|
||||
end;
|
||||
finally
|
||||
FindClose(searchRec);
|
||||
end;
|
||||
end;
|
||||
kttSave: begin
|
||||
saveFolder := TKMSavesCollection.Path(DOWNLOADED_LOBBY_SAVE, True);
|
||||
KMDeleteFolder(saveFolder); // Delete old folder
|
||||
ForceDirectories(saveFolder); // Create new
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function TKMFileReceiver.ValidExtension(const aExt: UnicodeString): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
|
||||
case fType of
|
||||
kttMap: begin
|
||||
for I := Low(VALID_MAP_EXTENSIONS) to High(VALID_MAP_EXTENSIONS) do
|
||||
if aExt = VALID_MAP_EXTENSIONS[I] then
|
||||
Exit(True);
|
||||
for I := Low(VALID_MAP_EXTENSIONS_POSTFIX) to High(VALID_MAP_EXTENSIONS_POSTFIX) do
|
||||
if aExt = VALID_MAP_EXTENSIONS_POSTFIX[I] then
|
||||
Exit(True);
|
||||
end;
|
||||
kttSave: for I := Low(VALID_SAVE_EXTENSIONS) to High(VALID_SAVE_EXTENSIONS) do
|
||||
if aExt = VALID_SAVE_EXTENSIONS[I] then
|
||||
Exit(True);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function TKMFileReceiver.ProcessTransfer: Boolean;
|
||||
var
|
||||
readType: TKMTransferType;
|
||||
readName, ext, postfix, transferedFileName, fileName: UnicodeString;
|
||||
readSize: Cardinal;
|
||||
fileStream: TKMemoryStream;
|
||||
decompressionStream: TDecompressionStream;
|
||||
readStream: TKMemoryStream;
|
||||
begin
|
||||
Result := False;
|
||||
if fReceiveStream.Size = 0 then Exit; //Transfer was aborted
|
||||
|
||||
//Decompress the stream
|
||||
fReceiveStream.Position := 0;
|
||||
fReceiveStream.CheckMarker('Transfer');
|
||||
decompressionStream := TDecompressionStream.Create(fReceiveStream);
|
||||
//We need custom methods like ReadAssert, ReadW, etc. so we need to read from a TKMemoryStream
|
||||
readStream := TKMemoryStreamBinary.Create;
|
||||
readStream.CopyFromDecompression(decompressionStream);
|
||||
decompressionStream.Free;
|
||||
readStream.Position := 0;
|
||||
|
||||
//Read from the stream
|
||||
readStream.CheckMarker('TransferCompressed');
|
||||
readStream.Read(readType, SizeOf(readType));
|
||||
Assert(readType = fType, 'Unexpected transfer type received');
|
||||
readStream.ReadW(readName);
|
||||
if (readName <> fBundleName) and (readName + '_' + IntToHex(fBundleCRC, 8) <> fBundleName) then
|
||||
raise Exception.Create('Unexpected transfer name received');
|
||||
|
||||
ClearExistingFiles;
|
||||
|
||||
//Load each file
|
||||
while readStream.Position < readStream.Size do
|
||||
begin
|
||||
readStream.CheckMarker('FileStart');
|
||||
readStream.ReadW(transferedFileName);
|
||||
readStream.ReadW(postfix);
|
||||
readStream.ReadW(ext);
|
||||
//Check EXT is valid (so we don't allow EXEs and stuff)
|
||||
Assert(ValidExtension(ext), 'Unexpected file extension received');
|
||||
|
||||
readStream.Read(readSize);
|
||||
fileStream := TKMemoryStreamBinary.Create;
|
||||
// Don't try to CopyFrom, if file is empty!
|
||||
// because, according to http://docwiki.embarcadero.com/Libraries/Sydney/en/System.Classes.TStream.CopyFrom
|
||||
// > If Count is 0, CopyFrom sets Source position to 0 before reading and then copies the entire contents of Source into the stream
|
||||
if readSize > 0 then
|
||||
fileStream.CopyFrom(readStream, readSize);
|
||||
|
||||
// Scripts can have arbitrary names
|
||||
if (ext = EXT_FILE_SCRIPT) and (transferedFileName <> readName) then
|
||||
fileName := GetFullDestFileName(fType, fBundleName, postfix, ext, transferedFileName)
|
||||
else
|
||||
fileName := GetFullDestFileName(fType, fBundleName, postfix, ext);
|
||||
|
||||
// Sometimes the file is not deleted yet. Probably OS glitches
|
||||
if FileExists(fileName) then
|
||||
KMDeleteFile(fileName);
|
||||
|
||||
fileStream.SaveToFile(fileName);
|
||||
fileStream.Free;
|
||||
end;
|
||||
readStream.Free;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMFileSenderManager }
|
||||
procedure TKMFileSenderManager.AbortAllTransfers;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
FreeAndNil(fSenders[I]);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileSenderManager.AckReceived(aReceiverIndex: TKMNetHandleIndex);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
if (fSenders[I] <> nil) and (fSenders[I].ReceiverIndex = aReceiverIndex) then
|
||||
fSenders[I].AckReceived;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileSenderManager.ClientDisconnected(aReceiverIndex: TKMNetHandleIndex);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
if (fSenders[I] <> nil) and (fSenders[I].ReceiverIndex = aReceiverIndex) then
|
||||
FreeAndNil(fSenders[I]);
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMFileSenderManager.Destroy;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
FreeAndNil(fSenders[I]);
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
function TKMFileSenderManager.StartNewSend(aType: TKMTransferType; const aBundleName: String; aMapKind: TKMMapKind;
|
||||
aReceiverIndex: TKMNetHandleIndex): Boolean;
|
||||
var
|
||||
I: Integer;
|
||||
bundleName: String;
|
||||
begin
|
||||
Result := False;
|
||||
bundleName := aBundleName; //To save const String param locally //@Rey: There are no threads here, it is not needed?
|
||||
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
if (fSenders[I] = nil) or (fSenders[I].ReceiverIndex = aReceiverIndex) then
|
||||
begin
|
||||
if fSenders[I] <> nil then
|
||||
//There is an existing transfer to this client, so free it
|
||||
fSenders[I].Free;
|
||||
try
|
||||
fSenders[I] := TKMFileSender.Create(aType, bundleName, aMapKind, aReceiverIndex);
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
gLog.AddTime(E.Message);
|
||||
// Ignore exception, just return False
|
||||
Exit(False);
|
||||
end;
|
||||
end;
|
||||
Exit(True);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function TKMFileSenderManager.ActiveTransferCount: Byte;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
if fSenders[I] <> nil then
|
||||
Inc(Result);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMFileSenderManager.UpdateStateIdle(aSendBufferEmpty: Boolean);
|
||||
var
|
||||
I: Integer;
|
||||
stream: TKMemoryStream;
|
||||
clientIndex: TKMNetHandleIndex;
|
||||
maxChunksInFlightPerSender: Byte;
|
||||
begin
|
||||
//Reserve some bandwidth for each sender
|
||||
maxChunksInFlightPerSender := Max(1, MAX_CHUNKS_BEFORE_ACK div Max(1, ActiveTransferCount));
|
||||
for I := Low(fSenders) to High(fSenders) do
|
||||
while (fSenders[I] <> nil) and (fSenders[I].fChunksInFlight < maxChunksInFlightPerSender) and aSendBufferEmpty do
|
||||
begin
|
||||
stream := TKMemoryStreamBinary.Create;
|
||||
fSenders[I].WriteChunk(stream, FILE_CHUNK_SIZE);
|
||||
fOnTransferPacket(fSenders[I].ReceiverIndex, stream, aSendBufferEmpty); //Updates SendBufferEmpty
|
||||
stream.Free;
|
||||
if fSenders[I].StreamEnd then
|
||||
begin
|
||||
clientIndex := fSenders[I].ReceiverIndex;
|
||||
FreeAndNil(fSenders[I]); //We must free it before calling OnTransferCompleted
|
||||
fOnTransferCompleted(clientIndex);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
unit KM_NetGameInfo;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, StrUtils, KM_CommonTypes, KM_CommonClasses, KM_NetTypes, KM_Defaults, KM_GameOptions;
|
||||
|
||||
type
|
||||
TKMPlayerGameResult = (pgrNone, pgrWin, pgrDefeat);
|
||||
|
||||
//Stores information about a multiplayer game to be sent: Host -> Server -> Queriers
|
||||
TKMNetGameInfo = class
|
||||
public
|
||||
GameState: TMPGameState;
|
||||
PasswordLocked: Boolean;
|
||||
PlayerCount: Byte;
|
||||
GameOptions: TKMGameOptions;
|
||||
Players: array[1..MAX_LOBBY_SLOTS] of record
|
||||
Name: AnsiString;
|
||||
Color: Cardinal;
|
||||
Connected: Boolean;
|
||||
LangCode: AnsiString;
|
||||
Team: Integer;
|
||||
IsSpectator: Boolean;
|
||||
IsHost: Boolean;
|
||||
PlayerType: TKMNetPlayerType;
|
||||
WonOrLost: TWonOrLost;
|
||||
end;
|
||||
Description: UnicodeString;
|
||||
Map: UnicodeString;
|
||||
GameTime: TDateTime;
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
function GetFormattedTime: UnicodeString;
|
||||
procedure LoadFromStream(aStream: TKMemoryStream);
|
||||
procedure SaveToStream(aStream: TKMemoryStream);
|
||||
function HTMLPlayersList: string;
|
||||
function ConnectedPlayerCount: Byte;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
VerySimpleXML, KM_CommonUtils;
|
||||
|
||||
|
||||
{ TKMNetGameInfo }
|
||||
constructor TKMNetGameInfo.Create;
|
||||
begin
|
||||
inherited;
|
||||
GameOptions := TKMGameOptions.Create;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetGameInfo.Destroy;
|
||||
begin
|
||||
FreeAndNil(GameOptions);
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetGameInfo.LoadFromStream(aStream: TKMemoryStream);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
aStream.Read(GameState, SizeOf(GameState));
|
||||
aStream.Read(PasswordLocked);
|
||||
aStream.Read(PlayerCount);
|
||||
if GameOptions = nil then
|
||||
GameOptions := TKMGameOptions.Create;
|
||||
GameOptions.Load(aStream);
|
||||
for I := 1 to PlayerCount do
|
||||
begin
|
||||
aStream.ReadA(Players[I].Name);
|
||||
aStream.Read(Players[I].Color);
|
||||
aStream.Read(Players[I].Connected);
|
||||
aStream.ReadA(Players[I].LangCode);
|
||||
aStream.Read(Players[I].Team);
|
||||
aStream.Read(Players[I].IsSpectator);
|
||||
aStream.Read(Players[I].IsHost);
|
||||
aStream.Read(Players[I].PlayerType, SizeOf(Players[I].PlayerType));
|
||||
aStream.Read(Players[I].WonOrLost, SizeOf(Players[I].WonOrLost));
|
||||
end;
|
||||
aStream.ReadW(Description);
|
||||
aStream.ReadW(Map);
|
||||
aStream.Read(GameTime, SizeOf(GameTime));
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetGameInfo.GetFormattedTime: UnicodeString;
|
||||
begin
|
||||
if GameTime >= 0 then
|
||||
Result := TimeToString(GameTime)
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetGameInfo.SaveToStream(aStream: TKMemoryStream);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
aStream.Write(GameState, SizeOf(GameState));
|
||||
aStream.Write(PasswordLocked);
|
||||
aStream.Write(PlayerCount);
|
||||
GameOptions.Save(aStream);
|
||||
for I := 1 to PlayerCount do
|
||||
begin
|
||||
aStream.WriteA(Players[I].Name);
|
||||
aStream.Write(Players[I].Color);
|
||||
aStream.Write(Players[I].Connected);
|
||||
aStream.WriteA(Players[I].LangCode);
|
||||
aStream.Write(Players[I].Team);
|
||||
aStream.Write(Players[I].IsSpectator);
|
||||
aStream.Write(Players[I].IsHost);
|
||||
aStream.Write(Players[I].PlayerType, SizeOf(Players[I].PlayerType));
|
||||
aStream.Write(Players[I].WonOrLost, SizeOf(Players[I].WonOrLost));
|
||||
end;
|
||||
aStream.WriteW(Description);
|
||||
aStream.WriteW(Map);
|
||||
aStream.Write(GameTime, SizeOf(GameTime));
|
||||
end;
|
||||
|
||||
|
||||
//todo -cPractical: This function should do its own XML escaping
|
||||
function TKMNetGameInfo.HTMLPlayersList: string;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
for I := 1 to PlayerCount do
|
||||
if Players[I].PlayerType = nptHuman then
|
||||
begin
|
||||
if Result <> '' then Result := Result + ', ';
|
||||
if not Players[I].Connected then Result := Result + '<strike>';
|
||||
Result := Result + XMLEscape(UnicodeString(Players[I].Name));
|
||||
if not Players[I].Connected then Result := Result + '</strike>';
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetGameInfo.ConnectedPlayerCount: Byte;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for I := 1 to PlayerCount do
|
||||
if Players[I].Connected and (Players[I].PlayerType = nptHuman) then
|
||||
Inc(Result);
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+1719
File diff suppressed because it is too large.
Load diff
+1286
File diff suppressed because it is too large.
Load diff
+313
@@ -0,0 +1,313 @@
|
||||
unit KM_NetServerLNet;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, lNet;
|
||||
|
||||
|
||||
{ This unit knows nothing about KaM, it's just a puppet in hands of KM_ServerControl,
|
||||
doing all the low level work on TCP. So we can replace this unit with other TCP client
|
||||
without KaM even noticing. }
|
||||
|
||||
const
|
||||
MAX_SEND_BUFFER = 1048576; //1 MB
|
||||
|
||||
|
||||
type
|
||||
TKMClientInfo = class
|
||||
public
|
||||
Tag: Integer;
|
||||
Buffer: array of byte;
|
||||
BufferLen: Cardinal;
|
||||
constructor Create(aTag: Integer);
|
||||
procedure AttemptSend(aSocket: TLSocket);
|
||||
procedure PutInBuffer(aData:pointer; aLength:cardinal);
|
||||
function BufferFull: Boolean;
|
||||
end;
|
||||
|
||||
THandleEvent = procedure (aHandle: SmallInt) of object;
|
||||
TNotifyDataEvent = procedure(aHandle: SmallInt; aData:pointer; aLength:cardinal)of object;
|
||||
|
||||
TKMNetServerLNet = class
|
||||
private
|
||||
fSocketServer: TLTCP;
|
||||
fLastTag:integer;
|
||||
fListening: Boolean;
|
||||
fOnError: TGetStrProc;
|
||||
fOnClientConnect: THandleEvent;
|
||||
fOnClientDisconnect: THandleEvent;
|
||||
fOnDataAvailable: TNotifyDataEvent;
|
||||
procedure ClientConnect(aSocket: TLSocket);
|
||||
procedure ClientDisconnect(aSocket: TLSocket);
|
||||
procedure ReceiveData(aSocket: TLSocket);
|
||||
procedure CanSend(aSocket: TLSocket);
|
||||
procedure HandleError(const aMsg: string; aSocket: TLSocket);
|
||||
function GetMaxHandle: SmallInt;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure StartListening(aPort: Word);
|
||||
procedure StopListening;
|
||||
procedure SendData(aHandle: SmallInt; aData:pointer; aLength:cardinal);
|
||||
procedure Kick(aHandle: SmallInt);
|
||||
procedure UpdateStateIdle;
|
||||
function IsValidHandle(aHandle: Integer): Boolean;
|
||||
function GetIP(aHandle: Integer): string;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnClientConnect: THandleEvent write fOnClientConnect;
|
||||
property OnClientDisconnect: THandleEvent write fOnClientDisconnect;
|
||||
property OnDataAvailable: TNotifyDataEvent write fOnDataAvailable;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
Math;
|
||||
|
||||
|
||||
//Tagging starts with some number away from -2 -1 0 used as sender/recipient constants
|
||||
//and off from usual players indexes 1..8, so we could not confuse them by mistake
|
||||
const
|
||||
FIRST_TAG = 15;
|
||||
|
||||
|
||||
{ TKMClientInfo }
|
||||
constructor TKMClientInfo.Create(aTag: Integer);
|
||||
begin
|
||||
inherited Create;
|
||||
Tag := aTag;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMClientInfo.AttemptSend(aSocket: TLSocket);
|
||||
var
|
||||
lenSent: Integer;
|
||||
begin
|
||||
if BufferLen <= 0 then Exit;
|
||||
|
||||
lenSent := aSocket.Send(Buffer[0], BufferLen);
|
||||
|
||||
if lenSent > 0 then
|
||||
begin
|
||||
BufferLen := BufferLen - lenSent;
|
||||
if BufferLen > 0 then
|
||||
Move(Buffer[lenSent], Buffer[0], BufferLen);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMClientInfo.PutInBuffer(aData:pointer; aLength:cardinal);
|
||||
begin
|
||||
SetLength(Buffer, BufferLen + aLength);
|
||||
Move(aData^, Buffer[BufferLen], aLength);
|
||||
BufferLen := BufferLen + aLength;
|
||||
end;
|
||||
|
||||
|
||||
function TKMClientInfo.BufferFull: Boolean;
|
||||
begin
|
||||
Result := BufferLen >= MAX_SEND_BUFFER;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMNetServerLNet }
|
||||
constructor TKMNetServerLNet.Create;
|
||||
begin
|
||||
Inherited Create;
|
||||
fLastTag := FIRST_TAG-1; //First client will be fLastTag+1
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetServerLNet.Destroy;
|
||||
begin
|
||||
StopListening;
|
||||
if fSocketServer<> nil then fSocketServer.Free;
|
||||
Inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.StartListening(aPort: Word);
|
||||
begin
|
||||
FreeAndNil(fSocketServer);
|
||||
fSocketServer := TLTCP.Create(nil);
|
||||
fSocketServer.OnError := HandleError;
|
||||
fSocketServer.OnAccept := ClientConnect;
|
||||
fSocketServer.OnDisconnect := ClientDisconnect;
|
||||
fSocketServer.OnReceive := ReceiveData;
|
||||
fSocketServer.OnCanSend := CanSend;
|
||||
//This is the time it will wait in CallAction for the OS to respond, it's better than calling Sleep(1)
|
||||
//This value could be higher, it won't cause delays since it's just a timeout on waiting for network events
|
||||
fSocketServer.Timeout := 100;
|
||||
fSocketServer.ReuseAddress := True; //Allows us to overwrite an old connection in the wait state rather than saying the port is still in use
|
||||
if not fSocketServer.Listen(aPort) then
|
||||
raise Exception.Create('Server failed to start');
|
||||
fListening := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.StopListening;
|
||||
begin
|
||||
if fSocketServer <> nil then
|
||||
begin
|
||||
//We have to disconnect all the clients too
|
||||
fSocketServer.IterReset;
|
||||
while fSocketServer.IterNext do
|
||||
begin
|
||||
if fSocketServer.Iterator.UserData <> nil then
|
||||
begin
|
||||
TKMClientInfo(fSocketServer.Iterator.UserData).Free;
|
||||
fSocketServer.Iterator.UserData := nil;
|
||||
end;
|
||||
fSocketServer.Iterator.Disconnect(True);
|
||||
end;
|
||||
fSocketServer.Disconnect(True);
|
||||
end;
|
||||
FreeAndNil(fSocketServer);
|
||||
fLastTag := FIRST_TAG-1;
|
||||
fListening := False;
|
||||
end;
|
||||
|
||||
|
||||
//Someone has connected to us
|
||||
procedure TKMNetServerLNet.ClientConnect(aSocket: TLSocket);
|
||||
begin
|
||||
//Identify index of the Client, so we can address it
|
||||
if fLastTag = GetMaxHandle then fLastTag := FIRST_TAG-1; //I'll be surprised if this is ever necessary
|
||||
inc(fLastTag);
|
||||
aSocket.UserData := TKMClientInfo.Create(fLastTag);
|
||||
fOnClientConnect(fLastTag);
|
||||
aSocket.SetState(ssNoDelay, True); //Send packets ASAP (disables Nagle's algorithm)
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.ClientDisconnect(aSocket: TLSocket);
|
||||
begin
|
||||
if aSocket.UserData = nil then Exit;
|
||||
fOnClientDisconnect(TKMClientInfo(aSocket.UserData).Tag);
|
||||
TKMClientInfo(aSocket.UserData).Free;
|
||||
aSocket.UserData := nil;
|
||||
end;
|
||||
|
||||
|
||||
//We recieved data from someone
|
||||
procedure TKMNetServerLNet.ReceiveData(aSocket: TLSocket);
|
||||
const
|
||||
BUFFER_SIZE = 10240; //10kb
|
||||
var
|
||||
P: Pointer;
|
||||
L: Integer; //L could be -1 when no data is available
|
||||
begin
|
||||
if aSocket.UserData = nil then
|
||||
begin
|
||||
aSocket.Disconnect(True); //Sometimes disconnecting them fails the first time(?) LNet bug?
|
||||
//fOnError('Received data from an unknown client');
|
||||
Exit;
|
||||
end;
|
||||
|
||||
GetMem(P, BUFFER_SIZE+1); //+1 to avoid RangeCheckError when L = BufferSize
|
||||
L := aSocket.Get(P^, BUFFER_SIZE);
|
||||
|
||||
if L > 0 then //if L=0 then exit;
|
||||
fOnDataAvailable(TKMClientInfo(aSocket.UserData).Tag, P, L);
|
||||
|
||||
FreeMem(P);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.HandleError(const aMsg: string; aSocket: TLSocket);
|
||||
begin
|
||||
fOnError('LNet: ' + aMsg);
|
||||
if (aSocket <> nil) and not aSocket.Connected then
|
||||
ClientDisconnect(aSocket); //Sometimes this is the only event we get when a client disconnects (LNet bug?)
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.CanSend(aSocket: TLSocket);
|
||||
begin
|
||||
if aSocket.UserData = nil then Exit;
|
||||
TKMClientInfo(aSocket.UserData).AttemptSend(aSocket);
|
||||
end;
|
||||
|
||||
|
||||
//Make sure we send data to specified client
|
||||
procedure TKMNetServerLNet.SendData(aHandle: SmallInt; aData: Pointer; aLength: Cardinal);
|
||||
begin
|
||||
fSocketServer.IterReset;
|
||||
while fSocketServer.IterNext do
|
||||
if fSocketServer.Iterator.Connected then //Sometimes this occurs just before ClientDisconnect
|
||||
if (fSocketServer.Iterator.UserData <> nil) and (TKMClientInfo(fSocketServer.Iterator.UserData).Tag = aHandle) then
|
||||
begin
|
||||
if TKMClientInfo(fSocketServer.Iterator.UserData).BufferFull then
|
||||
begin
|
||||
fOnError('Send buffer full for client '+IntToStr(TKMClientInfo(fSocketServer.Iterator.UserData).Tag));
|
||||
Exit;
|
||||
end;
|
||||
TKMClientInfo(fSocketServer.Iterator.UserData).PutInBuffer(aData, aLength);
|
||||
TKMClientInfo(fSocketServer.Iterator.UserData).AttemptSend(fSocketServer.Iterator);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.Kick(aHandle: SmallInt);
|
||||
var
|
||||
iter: TLSocket;
|
||||
begin
|
||||
fSocketServer.IterReset;
|
||||
while fSocketServer.IterNext do
|
||||
if (fSocketServer.Iterator.UserData <> nil) and (TKMClientInfo(fSocketServer.Iterator.UserData).Tag = aHandle) then
|
||||
begin
|
||||
//Found the client that must be kicked
|
||||
iter := fSocketServer.Iterator; //Calling ClientDisconnect can reset Iterator due to sending a message, so we must remember it (don't use it after this line)
|
||||
//Seems to be a bug in LNet where ClientDisconnect is not called sometimes? So call it ourself just in case
|
||||
ClientDisconnect(iter);
|
||||
if iter.ConnectionStatus in [scConnected, scConnecting, scDisconnecting] then
|
||||
//We always use forceful disconnects otherwise sockets can be left in FIN_WAIT2 state forever. See:
|
||||
//http://stackoverflow.com/questions/9819745/how-tcp-stack-distinguish-close-and-shutdown
|
||||
iter.Disconnect(True)
|
||||
else
|
||||
fOnError('Warning: Attempted to kick a client that is not connected');
|
||||
Exit; //Only one client should have this handle
|
||||
end;
|
||||
//If we reached here we didn't find a match
|
||||
fOnError('Warning: Attempted to kick a client that has already disconnected');
|
||||
fOnClientDisconnect(aHandle); //Client has already disconnected somehow, so send the event to ensure they are removed
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetServerLNet.GetMaxHandle: SmallInt;
|
||||
begin
|
||||
Result := 32767;
|
||||
end;
|
||||
|
||||
|
||||
// Take in Integer to check it against actual small range
|
||||
function TKMNetServerLNet.IsValidHandle(aHandle: Integer): Boolean;
|
||||
begin
|
||||
// This is rather poor check that can be refactored to check against real fMaxHandle
|
||||
Result := InRange(aHandle, FIRST_TAG, GetMaxHandle);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLNet.UpdateStateIdle;
|
||||
begin
|
||||
if fSocketServer <> nil then
|
||||
fSocketServer.CallAction; //Process network events
|
||||
|
||||
if fListening and not fSocketServer.Connected then
|
||||
raise Exception.Create('Server is no longer listening! Server will restart.');
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetServerLNet.GetIP(aHandle: Integer): string;
|
||||
begin
|
||||
Result := '';
|
||||
fSocketServer.IterReset;
|
||||
while fSocketServer.IterNext do
|
||||
if (fSocketServer.Iterator.UserData <> nil) and (TKMClientInfo(fSocketServer.Iterator.UserData).Tag = aHandle) then
|
||||
Exit(fSocketServer.Iterator.PeerAddress); // Only one client should have this handle
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
unit KM_NetServerLocator;
|
||||
{$I KaM_Remake.inc}
|
||||
{$WARN IMPLICIT_STRING_CAST OFF}
|
||||
interface
|
||||
uses
|
||||
Classes,
|
||||
KM_HTTPClient;
|
||||
|
||||
type
|
||||
// Class responsible for interaction with MasterServer:
|
||||
// - getting Master Server Announcement
|
||||
// - getting list of public servers from Master Server
|
||||
// - notify Master Server about this server
|
||||
// - notify Master Server about current game
|
||||
|
||||
// Basically this a client to Master Server,
|
||||
TKMNetServerLocator = class
|
||||
private
|
||||
fMasterServerAddress: string;
|
||||
fIsDedicated: Boolean;
|
||||
|
||||
fHTTPClient: TKMHTTPClient; //To update server status and fetch server list
|
||||
fHTTPAnnouncementsClient: TKMHTTPClient; //To fetch the annoucenemnts at the same time as the server list
|
||||
fHTTPMapsClient: TKMHTTPClient; //To tell master server about the map we played
|
||||
|
||||
fOnError: TGetStrProc;
|
||||
fOnServerList: TGetStrProc;
|
||||
fOnAnnouncements: TGetStrProc;
|
||||
|
||||
procedure ReceiveServerList(const aText: string);
|
||||
procedure ReceiveAnnouncements(const aText: string);
|
||||
procedure Error(const aText: string);
|
||||
public
|
||||
constructor Create(const aMasterServerAddress: string; aDedicated:Boolean);
|
||||
destructor Destroy; override;
|
||||
|
||||
property MasterServerAddress: string write fMasterServerAddress;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnServerList: TGetStrProc write fOnServerList;
|
||||
property OnAnnouncements: TGetStrProc write fOnAnnouncements;
|
||||
|
||||
procedure AnnounceServer(const aName: string; aPort: Word; aPlayerCount, aTTL: Integer);
|
||||
procedure AnnounceGame(const aMapName: string; aCRC: Cardinal; aPlayerCount: Integer);
|
||||
procedure FetchAnnouncements(const aLang: AnsiString);
|
||||
procedure FetchServerList;
|
||||
|
||||
procedure UpdateStateIdle;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
SysUtils,
|
||||
URLUtils, //This is a common URLUtils file used by both Delphi and Lazarus for two reasons:
|
||||
//1. Library specific stuff should all be done in wrappers (e.g. KM_NetServer_Overbyte) so we can easily switch systems.
|
||||
//2. Lazarus' LNet library has broken/incorrectly implemented URLUtils at the moment, so we can't rely on them.
|
||||
KM_Defaults;
|
||||
|
||||
|
||||
{ TKMNetServerLocator }
|
||||
constructor TKMNetServerLocator.Create(const aMasterServerAddress: string; aDedicated:Boolean);
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fHTTPClient := TKMHTTPClient.Create;
|
||||
fHTTPAnnouncementsClient := TKMHTTPClient.Create;
|
||||
fHTTPMapsClient := TKMHTTPClient.Create;
|
||||
fHTTPClient.OnReceive := nil;
|
||||
fHTTPClient.OnError := Error;
|
||||
fMasterServerAddress := aMasterServerAddress;
|
||||
fIsDedicated := aDedicated;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetServerLocator.Destroy;
|
||||
begin
|
||||
fHTTPClient.Free;
|
||||
fHTTPAnnouncementsClient.Free;
|
||||
fHTTPMapsClient.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.Error(const aText: string);
|
||||
begin
|
||||
if Assigned(fOnError) then fOnError(aText);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.ReceiveServerList(const aText: string);
|
||||
begin
|
||||
if Assigned(fOnServerList) then fOnServerList(aText);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.ReceiveAnnouncements(const aText: string);
|
||||
begin
|
||||
if Assigned(fOnAnnouncements) then fOnAnnouncements(aText);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.AnnounceServer(const aName: string; aPort: Word; aPlayerCount, aTTL: Integer);
|
||||
const
|
||||
{$IFDEF MSWindows} OS = 'Windows'; {$ENDIF}
|
||||
{$IFDEF UNIX} OS = 'Unix'; {$ENDIF}
|
||||
{$IFDEF WDC} COMPILER = 'WDC'; {$ENDIF}
|
||||
{$IFDEF FPC} COMPILER = 'FPC'; {$ENDIF}
|
||||
begin
|
||||
fHTTPClient.OnReceive := nil; //We don't care about the response
|
||||
fHTTPClient.GetURL(
|
||||
fMasterServerAddress + 'serveradd.php'+
|
||||
'?name='+UrlEncode(aName)+
|
||||
'&port='+UrlEncode(IntToStr(aPort))+
|
||||
'&playercount='+UrlEncode(IntToStr(aPlayerCount))+
|
||||
'&ttl='+UrlEncode(IntToStr(aTTL))+
|
||||
'&rev='+UrlEncode(NET_PROTOCOL_REVISON)+
|
||||
'&coderev='+UrlEncode(GAME_REVISION)+
|
||||
'&os='+UrlEncode(OS)+
|
||||
'&compiler='+UrlEncode(COMPILER)+
|
||||
'&dedicated='+UrlEncode(IntToStr(Ord(fIsDedicated))), False); //Result doesn't matter so ANSI is fine
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.FetchServerList;
|
||||
begin
|
||||
fHTTPClient.OnReceive := ReceiveServerList;
|
||||
fHTTPClient.GetURL(
|
||||
fMasterServerAddress+'serverquery.php'+
|
||||
'?rev='+UrlEncode(NET_PROTOCOL_REVISON)+
|
||||
'&coderev='+UrlEncode(GAME_REVISION), False); //For now server list is ANSI only
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.FetchAnnouncements(const aLang: AnsiString);
|
||||
begin
|
||||
fHTTPAnnouncementsClient.OnReceive := ReceiveAnnouncements;
|
||||
fHTTPAnnouncementsClient.GetURL(
|
||||
fMasterServerAddress+'announcements.php'+
|
||||
'?lang='+UrlEncode(UnicodeString(aLang))+
|
||||
'&rev='+UrlEncode(NET_PROTOCOL_REVISON)+
|
||||
'&coderev='+UrlEncode(GAME_REVISION), True); //Announcements are in UTF8
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.AnnounceGame(const aMapName: string; aCRC: Cardinal; aPlayerCount: Integer);
|
||||
begin
|
||||
fHTTPMapsClient.OnReceive := nil; //We don't care about the response
|
||||
fHTTPMapsClient.GetURL(
|
||||
fMasterServerAddress+'maps.php'+
|
||||
'?map='+UrlEncode(aMapName)+
|
||||
'&mapcrc='+IntToHex(aCRC, 8)+
|
||||
'&playercount='+UrlEncode(IntToStr(aPlayerCount))+
|
||||
'&rev='+UrlEncode(NET_PROTOCOL_REVISON)+
|
||||
'&coderev='+UrlEncode(GAME_REVISION), False); //Result doesn't matter so ANSI is fine
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerLocator.UpdateStateIdle;
|
||||
begin
|
||||
fHTTPClient.UpdateStateIdle;
|
||||
fHTTPAnnouncementsClient.UpdateStateIdle;
|
||||
fHTTPMapsClient.UpdateStateIdle;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
unit KM_NetServerOverbyte;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, OverbyteIcsWSocket, OverbyteIcsWSocketS, WinSock;
|
||||
|
||||
|
||||
{ This unit knows nothing about KaM, it's just a puppet in hands of KM_ServerControl,
|
||||
doing all the low level work on TCP. So we can replace this unit with other TCP Client
|
||||
without KaM even noticing. }
|
||||
|
||||
type
|
||||
THandleEvent = procedure (aHandle: SmallInt) of object;
|
||||
TNotifyDataEvent = procedure(aHandle: SmallInt; aData: Pointer; aLength: Cardinal) of object;
|
||||
|
||||
TKMNetServerOverbyte = class
|
||||
private
|
||||
fSocketServer:TWSocketServer;
|
||||
fLastTag: SmallInt;
|
||||
fOnError: TGetStrProc;
|
||||
fOnClientConnect: THandleEvent;
|
||||
fOnClientDisconnect: THandleEvent;
|
||||
fOnDataAvailable: TNotifyDataEvent;
|
||||
procedure ClientConnect(Sender: TObject; aClient: TWSocketClient; aError: Word);
|
||||
procedure ClientDisconnect(Sender: TObject; aClient: TWSocketClient; aError: Word);
|
||||
procedure DataAvailable(Sender: TObject; aError: Word);
|
||||
function GetMaxHandle: SmallInt;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure StartListening(aPort: Word);
|
||||
procedure StopListening;
|
||||
procedure SendData(aHandle: SmallInt; aData: Pointer; aLength: Cardinal);
|
||||
procedure Kick(aHandle: SmallInt);
|
||||
function GetIP(aHandle: SmallInt): string;
|
||||
function IsValidHandle(aHandle: Integer): Boolean;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnClientConnect: THandleEvent write fOnClientConnect;
|
||||
property OnClientDisconnect: THandleEvent write fOnClientDisconnect;
|
||||
property OnDataAvailable: TNotifyDataEvent write fOnDataAvailable;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
Math;
|
||||
|
||||
|
||||
// Tagging starts with some number away from -2 -1 0 used as sender/recipient constants
|
||||
// and off from usual players indexes 1..8, so we could not confuse them by mistake
|
||||
const
|
||||
FIRST_TAG = 15;
|
||||
|
||||
|
||||
constructor TKMNetServerOverbyte.Create;
|
||||
var
|
||||
wsaData: TWSAData;
|
||||
begin
|
||||
inherited;
|
||||
fLastTag := FIRST_TAG-1; //First Client will be fLastTag+1
|
||||
if WSAStartup($101, wsaData) <> 0 then
|
||||
fOnError('Error in Network');
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetServerOverbyte.Destroy;
|
||||
begin
|
||||
fSocketServer.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerOverbyte.StartListening(aPort: Word);
|
||||
begin
|
||||
FreeAndNil(fSocketServer);
|
||||
fSocketServer := TWSocketServer.Create(nil);
|
||||
fSocketServer.ComponentOptions := [wsoTcpNoDelay]; //Send packets ASAP (disables Nagle's algorithm)
|
||||
fSocketServer.Proto := 'tcp';
|
||||
fSocketServer.Addr := '0.0.0.0'; //Listen to whole range
|
||||
fSocketServer.Port := IntToStr(aPort);
|
||||
fSocketServer.Banner := '';
|
||||
fSocketServer.OnClientConnect := ClientConnect;
|
||||
fSocketServer.OnClientDisconnect := ClientDisconnect;
|
||||
fSocketServer.OnDataAvailable := DataAvailable;
|
||||
fSocketServer.Listen;
|
||||
fSocketServer.SetTcpNoDelayOption; //Send packets ASAP (disables Nagle's algorithm)
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerOverbyte.StopListening;
|
||||
begin
|
||||
if fSocketServer <> nil then fSocketServer.Close;
|
||||
FreeAndNil(fSocketServer);
|
||||
fLastTag := FIRST_TAG-1;
|
||||
end;
|
||||
|
||||
|
||||
//Someone has connected to us
|
||||
procedure TKMNetServerOverbyte.ClientConnect(Sender: TObject; aClient: TWSocketClient; aError: Word);
|
||||
begin
|
||||
if aError <> 0 then
|
||||
begin
|
||||
fOnError('ClientConnect. Error: '+WSocketErrorDesc(aError)+' (#' + IntToStr(aError)+')');
|
||||
exit;
|
||||
end;
|
||||
|
||||
//Identify index of the Client, so we can address it
|
||||
if fLastTag = GetMaxHandle then fLastTag := FIRST_TAG-1; //I'll be surprised if this is ever necessary
|
||||
inc(fLastTag);
|
||||
aClient.Tag := fLastTag;
|
||||
|
||||
aClient.OnDataAvailable := DataAvailable;
|
||||
aClient.ComponentOptions := [wsoTcpNoDelay]; //Send packets ASAP (disables Nagle's algorithm)
|
||||
aClient.SetTcpNoDelayOption;
|
||||
fOnClientConnect(aClient.Tag);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerOverbyte.ClientDisconnect(Sender: TObject; aClient: TWSocketClient; aError: Word);
|
||||
begin
|
||||
if aError <> 0 then
|
||||
begin
|
||||
fOnError('ClientDisconnect. Error: '+WSocketErrorDesc(aError)+' (#' + IntToStr(aError)+')');
|
||||
//Do not exit because the Client has still disconnected
|
||||
end;
|
||||
|
||||
fOnClientDisconnect(aClient.Tag);
|
||||
end;
|
||||
|
||||
|
||||
//We recieved data from someone
|
||||
procedure TKMNetServerOverbyte.DataAvailable(Sender: TObject; aError: Word);
|
||||
const
|
||||
BUFFER_SIZE = 10240; //10kb
|
||||
var
|
||||
P: Pointer;
|
||||
L: Integer; //L could be -1 when no data is available
|
||||
begin
|
||||
if aError <> 0 then
|
||||
begin
|
||||
fOnError('DataAvailable. Error: '+WSocketErrorDesc(aError)+' (#' + IntToStr(aError)+')');
|
||||
exit;
|
||||
end;
|
||||
|
||||
GetMem(P, BUFFER_SIZE+1); //+1 to avoid RangeCheckError when L = BufferSize
|
||||
L := TWSocket(Sender).Receive(P, BUFFER_SIZE);
|
||||
|
||||
if L > 0 then //if L=0 then exit;
|
||||
fOnDataAvailable(TWSocket(Sender).Tag, P, L);
|
||||
|
||||
FreeMem(P);
|
||||
end;
|
||||
|
||||
|
||||
//Make sure we send data to specified Client
|
||||
procedure TKMNetServerOverbyte.SendData(aHandle: SmallInt; aData: Pointer; aLength: Cardinal);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to fSocketServer.ClientCount - 1 do
|
||||
if fSocketServer.Client[i].Tag = aHandle then
|
||||
begin
|
||||
if fSocketServer.Client[i].State <> wsClosed then //Sometimes this occurs just before ClientDisconnect
|
||||
if fSocketServer.Client[i].Send(aData, aLength) <> aLength then
|
||||
fOnError('Overbyte Server: Failed to send packet to client '+IntToStr(aHandle));
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetServerOverbyte.GetMaxHandle: SmallInt;
|
||||
begin
|
||||
Result := 32767;
|
||||
end;
|
||||
|
||||
|
||||
// Take in Integer to check it against actual small range
|
||||
function TKMNetServerOverbyte.IsValidHandle(aHandle: Integer): Boolean;
|
||||
begin
|
||||
// This is rather poor check that can be refactored to check against real fMaxHandle
|
||||
Result := InRange(aHandle, FIRST_TAG, GetMaxHandle);
|
||||
end;
|
||||
|
||||
|
||||
// Kick the Client specified by the Handle
|
||||
procedure TKMNetServerOverbyte.Kick(aHandle: SmallInt);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to fSocketServer.ClientCount - 1 do
|
||||
if fSocketServer.Client[I].Tag = aHandle then
|
||||
begin
|
||||
if fSocketServer.Client[I].State <> wsClosed then //Sometimes this occurs just before ClientDisconnect
|
||||
fSocketServer.Client[I].Close;
|
||||
Exit; //Only one Client should have this handle
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetServerOverbyte.GetIP(aHandle: SmallInt): string;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
for I := 0 to fSocketServer.ClientCount - 1 do
|
||||
if fSocketServer.Client[I].Tag = aHandle then
|
||||
begin
|
||||
if fSocketServer.Client[I].State <> wsClosed then //Sometimes this occurs just before ClientDisconnect
|
||||
Result := fSocketServer.Client[I].GetPeerAddr;
|
||||
Exit; //Only one aClient should have this handle
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+652
@@ -0,0 +1,652 @@
|
||||
unit KM_NetServerPoller;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes,
|
||||
KM_CommonClasses, KM_CommonTypes,
|
||||
KM_NetClient, KM_NetServerLocator, KM_NetUDP, KM_NetTypes, KM_NetGameInfo;
|
||||
|
||||
|
||||
type
|
||||
TKMServerInfo = record
|
||||
Name: string;
|
||||
IP: string;
|
||||
Port: Word;
|
||||
ServerType: TKMServerType;
|
||||
Ping: Word;
|
||||
end;
|
||||
|
||||
TKMRoomInfo = record
|
||||
ServerIndex: Integer; //Points to some TKMServerInfo in TKMServerList
|
||||
GameRevision: TKMGameRevision;
|
||||
RoomID: Integer;
|
||||
OnlyRoom: Boolean; //Is this the only room in the server?
|
||||
GameInfo: TKMNetGameInfo;
|
||||
end;
|
||||
|
||||
TKMServerDataEvent = procedure(aServerID: Integer; aStream: TKMemoryStream; aPingStarted: Cardinal) of object;
|
||||
|
||||
TKMServerSortMethod = (
|
||||
ssmByTypeAsc, ssmByTypeDesc, // Client or Dedicated
|
||||
ssmByPasswordAsc, ssmByPasswordDesc, // Passworded or not
|
||||
ssmByNameAsc, ssmByNameDesc, // Server names
|
||||
ssmByPingAsc, ssmByPingDesc, // Server pings
|
||||
ssmByStateAsc, ssmByStateDesc, // Room state
|
||||
ssmByPlayersAsc, ssmByPlayersDesc // Player count in room
|
||||
);
|
||||
|
||||
TKMQuery = class
|
||||
private const
|
||||
QUERY_TIMEOUT = 5000; //The maximum amount of time for an individual query to take (will take at least 2*ping)
|
||||
private
|
||||
fNetClient: TKMNetClient;
|
||||
fQueryActive: Boolean;
|
||||
fQueryIsDone: Boolean;
|
||||
fPingStarted: Cardinal;
|
||||
fQueryStarted: Cardinal;
|
||||
fIndexOnServer: Integer;
|
||||
fServerID: Integer;
|
||||
fOnServerData: TKMServerDataEvent;
|
||||
fOnQueryDone: TNotifyEvent;
|
||||
procedure NetClientReceive(aNetClient: TKMNetClient; aSenderIndex: TKMNetHandleIndex; aData: Pointer; aLength: Cardinal);
|
||||
procedure PacketSend(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind); overload;
|
||||
procedure HandleMessage(aMessageKind: TKMNetMessageKind; aStream: TKMemoryStream);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure PerformQuery(const aAddress: string; aPort: Word; aServerID: Integer);
|
||||
procedure Disconnect;
|
||||
property OnServerData: TKMServerDataEvent read fOnServerData write fOnServerData;
|
||||
property OnQueryDone: TNotifyEvent read fOnQueryDone write fOnQueryDone;
|
||||
procedure UpdateStateIdle;
|
||||
end;
|
||||
|
||||
TKMRoomList = class
|
||||
private
|
||||
fCount: Integer;
|
||||
fRooms: array of TKMRoomInfo;
|
||||
procedure AddRoom(aServerIndex, aRoomID: Integer; aOnlyRoom: Boolean; aGameRev: TKMGameRevision; aGameInfoStream: TKMemoryStream);
|
||||
function GetRoom(aIndex: Integer): TKMRoomInfo;
|
||||
procedure SetRoom(aIndex: Integer; aValue: TKMRoomInfo);
|
||||
procedure Clear;
|
||||
public
|
||||
destructor Destroy; override;
|
||||
property Rooms[aIndex: Integer]: TKMRoomInfo read GetRoom write SetRoom; default;
|
||||
property Count: Integer read fCount;
|
||||
procedure LoadData(aServerID: Integer; aStream: TKMemoryStream);
|
||||
procedure SwapRooms(A, B: Integer);
|
||||
end;
|
||||
|
||||
TKMServerList = class
|
||||
private
|
||||
fCount: Integer;
|
||||
fLastQueried: Integer;
|
||||
fServers: array of TKMServerInfo;
|
||||
procedure AddServer(const aIP, aServerName: String; aPort: Word; aType: TKMServerType; aPing: Word);
|
||||
function GetServer(aIndex: Integer): TKMServerInfo;
|
||||
procedure Clear;
|
||||
procedure AddFromText(const aText: UnicodeString);
|
||||
public
|
||||
property Servers[aIndex: Integer]: TKMServerInfo read GetServer; default;
|
||||
property Count: Integer read fCount;
|
||||
procedure TakeNewQuery(aQuery: TKMQuery);
|
||||
procedure SetPing(aServerID: Integer; aPing: Cardinal);
|
||||
end;
|
||||
|
||||
// Polls the Master Server and Server List
|
||||
TKMNetServerPoller = class
|
||||
private const
|
||||
MAX_QUERIES = 16; //The maximum number of individual server queries that can be happening at once
|
||||
MIN_UDP_SCAN_TIME = 1000; //Assume we won't get any more UDP responses after this time (remove 'Refreshing...' text if no servers found)
|
||||
private
|
||||
fServerLocator: TKMNetServerLocator;
|
||||
fServerList: TKMServerList; //List of servers fetch from master
|
||||
fRoomList: TKMRoomList; //Info about each room populated after query completed
|
||||
fQuery: array[0..MAX_QUERIES-1] of TKMQuery;
|
||||
fSortMethod: TKMServerSortMethod;
|
||||
fRefreshStartedTime: Cardinal;
|
||||
fReceivedMasterServerList: Boolean;
|
||||
|
||||
fUDPScanner: TKMNetUDPScan;
|
||||
|
||||
fOnListUpdated: TNotifyEvent;
|
||||
fOnAnnouncements: TUnicodeStringEvent;
|
||||
|
||||
procedure DetectUDPServer(const aAddress: String; const aPort: Word; const aName: string);
|
||||
procedure ReceiveServerList(const aText: String);
|
||||
procedure ReceiveAnnouncements(const aText: String);
|
||||
|
||||
procedure ServerDataReceive(aServerID: Integer; aStream: TKMemoryStream; aPingStarted: Cardinal);
|
||||
procedure QueryDone(Sender: TObject);
|
||||
|
||||
procedure Sort;
|
||||
procedure SetSortMethod(aMethod: TKMServerSortMethod);
|
||||
|
||||
function ActiveQueryCount: Integer;
|
||||
public
|
||||
constructor Create(const aMasterServerAddress: String; aServerUDPScanPort: Word);
|
||||
destructor Destroy; override;
|
||||
|
||||
property OnListUpdated: TNotifyEvent read fOnListUpdated write fOnListUpdated;
|
||||
property OnAnnouncements: TUnicodeStringEvent read fOnAnnouncements write fOnAnnouncements;
|
||||
property Servers: TKMServerList read fServerList;
|
||||
property Rooms: TKMRoomList read fRoomList;
|
||||
|
||||
property SortMethod: TKMServerSortMethod read fSortMethod write SetSortMethod;
|
||||
|
||||
procedure FetchServerList;
|
||||
procedure FetchAnnouncements(const aLocale: AnsiString);
|
||||
procedure AnnounceGame(const aMapName: UnicodeString; aCRC: Cardinal; aPlayerCount: Integer);
|
||||
procedure UpdateStateIdle;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
SysUtils,
|
||||
KM_Defaults, KM_CommonUtils, KM_NetworkUtils;
|
||||
|
||||
|
||||
{ TKMRoomList }
|
||||
destructor TKMRoomList.Destroy;
|
||||
begin
|
||||
// Free GameInfo
|
||||
Clear;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMRoomList.AddRoom(aServerIndex, aRoomID: Integer; aOnlyRoom: Boolean; aGameRev: TKMGameRevision; aGameInfoStream: TKMemoryStream);
|
||||
begin
|
||||
if Length(fRooms) <= fCount then SetLength(fRooms, fCount+16);
|
||||
fRooms[fCount].ServerIndex := aServerIndex;
|
||||
fRooms[fCount].GameRevision := aGameRev;
|
||||
fRooms[fCount].RoomID := aRoomID;
|
||||
fRooms[fCount].OnlyRoom := aOnlyRoom;
|
||||
fRooms[fCount].GameInfo := TKMNetGameInfo.Create;
|
||||
fRooms[fCount].GameInfo.LoadFromStream(aGameInfoStream);
|
||||
Inc(fCount);
|
||||
end;
|
||||
|
||||
|
||||
function TKMRoomList.GetRoom(aIndex:integer): TKMRoomInfo;
|
||||
begin
|
||||
Result := fRooms[aIndex];
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMRoomList.SetRoom(aIndex: Integer; aValue: TKMRoomInfo);
|
||||
begin
|
||||
fRooms[aIndex] := aValue;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMRoomList.Clear;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
for I := 0 to fCount - 1 do
|
||||
fRooms[I].GameInfo.Free;
|
||||
fCount := 0;
|
||||
SetLength(fRooms, 0);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMRoomList.LoadData(aServerID: Integer; aStream: TKMemoryStream);
|
||||
var
|
||||
I, roomCount, roomID: Integer;
|
||||
gameRev: TKMGameRevision;
|
||||
begin
|
||||
aStream.Read(roomCount);
|
||||
for I := 0 to roomCount - 1 do //We don't actually use i as the server sends us RoomID (rooms might not be in order)
|
||||
begin
|
||||
aStream.Read(roomID);
|
||||
aStream.Read(gameRev);
|
||||
AddRoom(aServerID, roomID, (roomCount = 1), gameRev, aStream);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMRoomList.SwapRooms(A, B: Integer);
|
||||
var
|
||||
temp: TKMRoomInfo;
|
||||
begin
|
||||
temp := fRooms[A];
|
||||
fRooms[A] := fRooms[B];
|
||||
fRooms[B] := temp;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMServerList }
|
||||
procedure TKMServerList.AddServer(const aIP, aServerName: string; aPort: Word; aType: TKMServerType; aPing: Word);
|
||||
begin
|
||||
if Length(fServers) <= fCount then SetLength(fServers, fCount+16);
|
||||
fServers[fCount].Name := TKMNetworkUtils.GetEscapedNewLineServerName(aServerName);
|
||||
fServers[fCount].IP := aIP;
|
||||
fServers[fCount].Port := aPort;
|
||||
fServers[fCount].ServerType := aType;
|
||||
fServers[fCount].Ping := aPing;
|
||||
Inc(fCount);
|
||||
end;
|
||||
|
||||
|
||||
function TKMServerList.GetServer(aIndex: Integer): TKMServerInfo;
|
||||
begin
|
||||
Result := fServers[aIndex];
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMServerList.Clear;
|
||||
begin
|
||||
fCount := 0;
|
||||
SetLength(fServers, 0);
|
||||
fLastQueried := -1; //First is 0
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMServerList.AddFromText(const aText: UnicodeString);
|
||||
|
||||
function GetServerType(const aDedicated, aOS: UnicodeString): TKMServerType;
|
||||
begin
|
||||
if aDedicated = '0' then
|
||||
Result := mstClient
|
||||
else
|
||||
Result := mstDedicated;
|
||||
end;
|
||||
|
||||
var
|
||||
I: Integer;
|
||||
srvList, srvInfo: TStringList;
|
||||
begin
|
||||
srvList := TStringList.Create;
|
||||
srvInfo := TStringList.Create;
|
||||
srvList.Text := aText; //Parse according to EOLs
|
||||
for I := 0 to srvList.Count - 1 do
|
||||
begin
|
||||
ParseDelimited(srvList[I], ',', srvInfo); //Automatically clears srvInfo and loads each value
|
||||
if srvInfo.Count = 5 then //Must have 5 parameters
|
||||
AddServer(srvInfo[1], srvInfo[0], StrToInt(srvInfo[2]), GetServerType(srvInfo[3], srvInfo[4]), 0);
|
||||
end;
|
||||
|
||||
srvInfo.Free;
|
||||
srvList.Free;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMServerList.TakeNewQuery(aQuery: TKMQuery);
|
||||
begin
|
||||
if fLastQueried < fCount - 1 then
|
||||
begin
|
||||
Inc(fLastQueried);
|
||||
aQuery.PerformQuery(Servers[fLastQueried].IP, Servers[fLastQueried].Port, fLastQueried);
|
||||
end
|
||||
else
|
||||
aQuery.Disconnect;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMServerList.SetPing(aServerID:Integer; aPing: Cardinal);
|
||||
begin
|
||||
fServers[aServerID].Ping := aPing;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMQuery }
|
||||
constructor TKMQuery.Create;
|
||||
begin
|
||||
inherited;
|
||||
|
||||
fNetClient := TKMNetClient.Create;
|
||||
fQueryIsDone := False;
|
||||
fQueryActive := False;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMQuery.Destroy;
|
||||
begin
|
||||
fNetClient.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMQuery.PerformQuery(const aAddress: string; aPort: Word; aServerID: Integer);
|
||||
begin
|
||||
fQueryActive := True;
|
||||
fServerID := aServerID;
|
||||
fQueryStarted := TimeGet;
|
||||
fNetClient.Disconnect;
|
||||
fNetClient.OnRecieveData := NetClientReceive;
|
||||
fNetClient.ConnectTo(aAddress, aPort);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMQuery.Disconnect;
|
||||
begin
|
||||
fNetClient.Disconnect;
|
||||
fNetClient.OnRecieveData := nil;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMQuery.UpdateStateIdle;
|
||||
begin
|
||||
if not fQueryActive then Exit;
|
||||
|
||||
fNetClient.UpdateStateIdle;
|
||||
if TimeSince(fQueryStarted) > QUERY_TIMEOUT then
|
||||
fQueryIsDone := True; //Give up
|
||||
if fQueryIsDone then
|
||||
begin
|
||||
fQueryIsDone := False;
|
||||
fQueryActive := False;
|
||||
fOnQueryDone(Self);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMQuery.NetClientReceive(aNetClient: TKMNetClient; aSenderIndex: TKMNetHandleIndex; aData: Pointer; aLength: Cardinal);
|
||||
var
|
||||
dataStream: TKMemoryStream;
|
||||
messageKind: TKMNetMessageKind;
|
||||
begin
|
||||
Assert(aLength >= SizeOf(messageKind), 'Unexpectedly short message');
|
||||
|
||||
dataStream := TKMemoryStreamBinary.Create;
|
||||
try
|
||||
dataStream.WriteBuffer(aData^, aLength);
|
||||
dataStream.Position := 0;
|
||||
dataStream.Read(messageKind, SizeOf(messageKind));
|
||||
|
||||
HandleMessage(messageKind, dataStream);
|
||||
finally
|
||||
dataStream.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMQuery.HandleMessage(aMessageKind: TKMNetMessageKind; aStream: TKMemoryStream);
|
||||
var
|
||||
tmpHandleIndex: TKMNetHandleIndex;
|
||||
tmpString: AnsiString;
|
||||
begin
|
||||
case aMessageKind of
|
||||
mkNetProtocolVersion:
|
||||
begin
|
||||
aStream.ReadA(tmpString);
|
||||
if tmpString <> NET_PROTOCOL_REVISON then
|
||||
fQueryIsDone := True;
|
||||
end;
|
||||
|
||||
mkIndexOnServer:
|
||||
begin
|
||||
aStream.Read(tmpHandleIndex);
|
||||
fIndexOnServer := tmpHandleIndex;
|
||||
fPingStarted := TimeGet;
|
||||
PacketSend(NET_ADDRESS_SERVER, mkGetServerInfo);
|
||||
end;
|
||||
|
||||
mkServerInfo:
|
||||
begin
|
||||
fOnServerData(fServerID, aStream, fPingStarted);
|
||||
fQueryIsDone := True; //We cannot call fOnQueryDone now because that would disconnect the socket halfway through the receive procedure (crashes)
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMQuery.PacketSend(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind);
|
||||
var
|
||||
M: TKMemoryStream;
|
||||
begin
|
||||
Assert(NetPacketType[aKind] = pfNoData);
|
||||
|
||||
M := TKMemoryStreamBinary.Create;
|
||||
M.Write(aKind, SizeOf(aKind));
|
||||
|
||||
fNetClient.SendData(fIndexOnServer, aRecipient, M.Memory, M.Size);
|
||||
M.Free;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMNetServerPoller }
|
||||
constructor TKMNetServerPoller.Create(const aMasterServerAddress: String; aServerUDPScanPort: Word);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fServerLocator := TKMNetServerLocator.Create(aMasterServerAddress, False);
|
||||
fServerLocator.OnServerList := ReceiveServerList;
|
||||
fServerLocator.OnAnnouncements := ReceiveAnnouncements;
|
||||
|
||||
fUDPScanner := TKMNetUDPScan.Create(aServerUDPScanPort);
|
||||
fUDPScanner.OnServerDetected := DetectUDPServer;
|
||||
|
||||
fSortMethod := ssmByStateAsc; //Default sorting is by state
|
||||
|
||||
fServerList := TKMServerList.Create;
|
||||
fRoomList := TKMRoomList.Create;
|
||||
for I := 0 to MAX_QUERIES - 1 do
|
||||
begin
|
||||
fQuery[I] := TKMQuery.Create;
|
||||
fQuery[I].OnServerData := ServerDataReceive;
|
||||
fQuery[I].OnQueryDone := QueryDone;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetServerPoller.Destroy;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
fServerLocator.Free;
|
||||
fUDPScanner.Free;
|
||||
fServerList.Free;
|
||||
fRoomList.Free;
|
||||
for I := 0 to MAX_QUERIES - 1 do
|
||||
fQuery[I].Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.FetchServerList;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
//Clean up first
|
||||
for I := 0 to MAX_QUERIES - 1 do
|
||||
fQuery[I].Disconnect;
|
||||
fRoomList.Clear;
|
||||
fServerList.Clear;
|
||||
fReceivedMasterServerList := False;
|
||||
fRefreshStartedTime := TimeGet;
|
||||
|
||||
fUDPScanner.ScanForServers;
|
||||
if LOCAL_SERVER_LIST then
|
||||
// Pretend we already received it (for debug)
|
||||
ReceiveServerList('Localhost,127.0.0.1,56789,0,Windows')
|
||||
//+#13+'Localhost,127.0.0.1,56788,1,Windows'+#13+'Localhost,127.0.0.1,56787,1,Unix'
|
||||
else
|
||||
fServerLocator.FetchServerList; //Start the query
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.DetectUDPServer(const aAddress: string; const aPort: Word; const aName: string);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
//Make sure this isn't a duplicate (UDP is connectionless so we could get a response from an old query)
|
||||
for I := 0 to fServerList.Count - 1 do
|
||||
if (fServerList[I].IP = aAddress) and (fServerList[I].Port = aPort) then
|
||||
Exit;
|
||||
|
||||
fServerList.AddServer(aAddress, aName, aPort, mstLocal, 0);
|
||||
for I := 0 to MAX_QUERIES - 1 do
|
||||
if not fQuery[I].fQueryActive then
|
||||
fServerList.TakeNewQuery(fQuery[I]);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.ReceiveServerList(const aText: string);
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
fReceivedMasterServerList := True;
|
||||
fServerList.AddFromText(aText);
|
||||
for I := 0 to MAX_QUERIES - 1 do
|
||||
if not fQuery[I].fQueryActive then
|
||||
fServerList.TakeNewQuery(fQuery[I]);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.ReceiveAnnouncements(const aText: string);
|
||||
begin
|
||||
if Assigned(fOnAnnouncements) then
|
||||
fOnAnnouncements(aText);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.ServerDataReceive(aServerID: Integer; aStream: TKMemoryStream; aPingStarted: Cardinal);
|
||||
begin
|
||||
fRoomList.LoadData(aServerID, aStream); //Tell RoomsList to load data about rooms
|
||||
fServerList.SetPing(aServerID, TimeSince(aPingStarted)); //Tell ServersList ping
|
||||
|
||||
Sort; //Apply sorting
|
||||
if Assigned(fOnListUpdated) then
|
||||
fOnListUpdated(Self);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.QueryDone(Sender: TObject);
|
||||
begin
|
||||
fServerList.TakeNewQuery(TKMQuery(Sender));
|
||||
end;
|
||||
|
||||
|
||||
//Get the server announcements in specified language
|
||||
procedure TKMNetServerPoller.FetchAnnouncements(const aLocale: AnsiString);
|
||||
begin
|
||||
fServerLocator.FetchAnnouncements(aLocale);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.AnnounceGame(const aMapName: UnicodeString; aCRC: Cardinal; aPlayerCount: Integer);
|
||||
begin
|
||||
fServerLocator.AnnounceGame(aMapName, aCRC, aPlayerCount);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.UpdateStateIdle;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
fServerLocator.UpdateStateIdle;
|
||||
fUDPScanner.UpdateStateIdle;
|
||||
for I := 0 to MAX_QUERIES - 1 do
|
||||
fQuery[I].UpdateStateIdle;
|
||||
|
||||
//Once we've received the master list, finished all queries and waited long enough for UDP scan
|
||||
//we should update the list (this removes 'Refreshing...' when no servers respond)
|
||||
if fReceivedMasterServerList and (ActiveQueryCount = 0) and (TimeSince(fRefreshStartedTime) > MIN_UDP_SCAN_TIME) then
|
||||
begin
|
||||
fReceivedMasterServerList := False; //Don't do this again until the next refresh
|
||||
fUDPScanner.TerminateScan; //Closes UDP listen
|
||||
if Assigned(fOnListUpdated) then
|
||||
fOnListUpdated(Self);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.Sort;
|
||||
var
|
||||
tempRooms: array of TKMRoomInfo;
|
||||
|
||||
function Compare(const A, B: TKMRoomInfo): Boolean;
|
||||
const
|
||||
STATE_SORT_ORDER: array[TMPGameState] of Byte = (4,1,2,3);
|
||||
var
|
||||
serverA, serverB: TKMServerInfo;
|
||||
begin
|
||||
Result := False; //By default everything remains in place
|
||||
serverA := Servers[A.ServerIndex];
|
||||
serverB := Servers[B.ServerIndex];
|
||||
case fSortMethod of
|
||||
ssmByPasswordAsc: Result := A.GameInfo.PasswordLocked and not B.GameInfo.PasswordLocked;
|
||||
ssmByPasswordDesc:Result := not A.GameInfo.PasswordLocked and B.GameInfo.PasswordLocked;
|
||||
ssmByTypeAsc: Result := serverA.ServerType > serverB.ServerType;
|
||||
ssmByTypeDesc: Result := serverA.ServerType < serverB.ServerType;
|
||||
ssmByNameAsc: Result := CompareText(serverA.Name, serverB.Name) > 0;
|
||||
ssmByNameDesc: Result := CompareText(serverA.Name, serverB.Name) < 0;
|
||||
ssmByPingAsc: Result := serverA.Ping > serverB.Ping;
|
||||
ssmByPingDesc: Result := serverA.Ping < serverB.Ping;
|
||||
ssmByStateAsc: Result := STATE_SORT_ORDER[A.GameInfo.GameState] > STATE_SORT_ORDER[B.GameInfo.GameState];
|
||||
ssmByStateDesc: Result := STATE_SORT_ORDER[A.GameInfo.GameState] < STATE_SORT_ORDER[B.GameInfo.GameState];
|
||||
ssmByPlayersAsc: Result := A.GameInfo.ConnectedPlayerCount > B.GameInfo.ConnectedPlayerCount;
|
||||
ssmByPlayersDesc: Result := A.GameInfo.ConnectedPlayerCount < B.GameInfo.ConnectedPlayerCount;
|
||||
end;
|
||||
|
||||
//Always put local servers at the top
|
||||
if (serverA.ServerType = mstLocal) and (serverB.ServerType <> mstLocal) then
|
||||
Result := False
|
||||
else
|
||||
if (serverA.ServerType <> mstLocal) and (serverB.ServerType = mstLocal) then
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure MergeSort(left, right: integer);
|
||||
var
|
||||
I, J, middle, ind1, ind2: integer;
|
||||
begin
|
||||
if right <= left then
|
||||
Exit;
|
||||
|
||||
middle := (left+right) div 2;
|
||||
MergeSort(left, middle);
|
||||
Inc(middle);
|
||||
MergeSort(middle, right);
|
||||
ind1 := left;
|
||||
ind2 := middle;
|
||||
for I := left to right do
|
||||
begin
|
||||
if (ind1 < middle) and ((ind2 > right) or not Compare(fRoomList[ind1], fRoomList[ind2])) then
|
||||
begin
|
||||
tempRooms[I] := fRoomList[ind1];
|
||||
Inc(ind1);
|
||||
end
|
||||
else
|
||||
begin
|
||||
tempRooms[I] := fRoomList[ind2];
|
||||
Inc(ind2);
|
||||
end;
|
||||
end;
|
||||
for J := left to right do
|
||||
fRoomList[J] := tempRooms[J];
|
||||
end;
|
||||
|
||||
begin
|
||||
SetLength(tempRooms, fRoomList.Count);
|
||||
MergeSort(0, fRoomList.Count - 1);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetServerPoller.SetSortMethod(aMethod: TKMServerSortMethod);
|
||||
begin
|
||||
fSortMethod := aMethod;
|
||||
Sort; //New sorting method has been set, we need to apply it
|
||||
end;
|
||||
|
||||
|
||||
function TKMNetServerPoller.ActiveQueryCount: Integer;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
for I := 0 to MAX_QUERIES-1 do
|
||||
if fQuery[I].fQueryActive then
|
||||
Inc(Result);
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
unit KM_NetTypes;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Math, KM_Points, KM_Lists, KM_CommonClasses;
|
||||
|
||||
|
||||
type
|
||||
TKMNetPlayerKind = (lpkHost, lpkJoiner);
|
||||
|
||||
TKMNetGameState = (
|
||||
lgsNone,
|
||||
lgsConnecting, // Connecting to server
|
||||
lgsQuery,
|
||||
lgsLobby, // Setting up the game in the room
|
||||
lgsLoading, // Loading the game
|
||||
lgsGame, // Playing the game
|
||||
lgsReconnecting // Reconnecting to server
|
||||
);
|
||||
|
||||
TKMNetGameKind = (ngkNone, ngkMap, ngkSave);
|
||||
TKMChatSound = (csNone, csJoin, csLeave, csSystem, csGameStart, csSaveGame, csChat, csChatWhisper, csChatTeam);
|
||||
|
||||
const
|
||||
// Special senders/recipients
|
||||
NET_ADDRESS_EMPTY = 0; // Never used. Uninitialized value
|
||||
NET_ADDRESS_OTHERS = -1; // Everyone in the Room except the sender (Server does the replication upon recieving)
|
||||
NET_ADDRESS_ALL = -2; // Everyone in the Room including sender if player send it AND Everyone on the Server if server sends it
|
||||
//@Rey: NET_ADDRESS_ALL needs to be split into 2 consts for 2 listed usecases. Otherwise we get issues like with mkClientLost
|
||||
NET_ADDRESS_HOST = -3; // Host of the Room
|
||||
NET_ADDRESS_SERVER = -4; // Server
|
||||
|
||||
//Size of chunks that a file is sent in (must be smaller than MAX_PACKET_SIZE)
|
||||
//Making it less than Ethernet MTU (~1500) helps to avoids inefficient IP fragmentation
|
||||
FILE_CHUNK_SIZE = 1024; //max value less then MTU
|
||||
MAX_CUMULATIVE_PACKET_SIZE = 1460; //max value less than MTU
|
||||
MAX_PACKET_SIZE = 20480; //20kb. Maximum length of a KM packet
|
||||
MAX_CHUNKS_BEFORE_ACK = 80; //Number of chunks of a file that can be in flight
|
||||
DEFAULT_PACKET_ACC_DELAY = 20;
|
||||
|
||||
type
|
||||
// Client-Server-Client exchange packets. Each packet is a certain type
|
||||
//
|
||||
// Attention!!!
|
||||
// Add any new message kinds to the end of the next enum !!!
|
||||
//
|
||||
// Real situation example:
|
||||
// ************************************************************************************************************************
|
||||
// That issue was caused by adding new network command type of mkAskToSendCrashreport, which was added in r14423.
|
||||
// Command type is used not only in the game code, but in server code as well.
|
||||
// Most commands are just ignored by the server, but some commands are not,
|
||||
// f.e. server knows about the password so he should be able to receive that command type.
|
||||
// But mkAskToSendCrashreport was not added to the end of the list, but before mkPassword command.
|
||||
// Thus in r13651 mkPassword got an ID of 23 (let's say), but on r14423 its already 24, because of the new mkAskToSendCrashreport.
|
||||
// So when r14432 game client send mkPassword as a host he sends command ID of 24, but old server got 24 and its not mkPassword for him,
|
||||
// but some other command, which he did not except to receive and just ignores it. Thus password is not set on the server side.
|
||||
// When server and game client both have same version - their commands IDs are equal and everything is fine.
|
||||
// ************************************************************************************************************************
|
||||
//
|
||||
// Alternative solution could be to send message kind as a text, to avoid such errors in the future
|
||||
// Or even better solution is to use fixed IDs for enum values
|
||||
//
|
||||
TKMNetMessageKind = (
|
||||
mkAskToJoin, //Client asks Host if he can join
|
||||
mkAllowToJoin, //Host allows Client to join
|
||||
mkRefuseToJoin, //Host can refuse when e.g. Nickname is already taken
|
||||
|
||||
mkAskForAuth, //Joiner sends challenge to host, and askes host to send its challenge
|
||||
mkIndexOnServer, //Server tells Client his index
|
||||
mkClientLost, //Server tells clients that someone has disconnected
|
||||
mkReassignHost, //Server tells clients who is the new host if he disconnects or was willingly transferred with mkGiveHost
|
||||
|
||||
mkNetProtocolVersion, //Server tells a new client which NET_PROTOCOL_REVISON we are using (was GAME_VERSION before, but it is unnecessarily strict)
|
||||
mkWelcomeMessage, //Server sends a welcome message to the client
|
||||
mkServerName, //Server sends the server name to the client
|
||||
mkJoinRoom, //Client requests to be placed in a room
|
||||
mkConnectedToRoom, //Server tells a client they have been successfully added to a room
|
||||
mkSetGameInfo, //Host tells the server the player list, map, etc to be reported to queries
|
||||
mkKickPlayer, //Host askes the server to kick someone
|
||||
mkBanPlayer, //Host askes the server to ban someone from this room
|
||||
mkGiveHost, //Host askes the server to reassign host
|
||||
mkResetBans, //Host askes the server to reset the ban list for this room
|
||||
mkKicked, //Server tells a client they were kicked just before disconnecting then
|
||||
mkLangCode, //Client tells host his language code
|
||||
mkAuthChallenge, //Host sends solution and own challenge back to joiner
|
||||
|
||||
mkGetServerInfo, //Client askes the server for the server info
|
||||
mkServerInfo, //Server sends client the server info on request
|
||||
|
||||
mkDisconnect, //Joiner tells Host that he is leaving the lobby/game deliberately
|
||||
//Host tells Joiners that he is quitting
|
||||
//A. Server runs on the same machine and stops right after
|
||||
//B. Server runs on different machine and assigns Host role to some Client
|
||||
|
||||
mkPing, //Server pings Clients
|
||||
mkPong, //Clients reply to Server with pong
|
||||
mkPingFpsInfo, //Server sends list of ping/fps to Clients
|
||||
mkFPS, //Client tells server his FPS
|
||||
|
||||
mkPlayersList, //Host keeps the players list and sends it to everyone on change
|
||||
mkGameOptions, //Host keeps the game options and sends it to everyone on change
|
||||
|
||||
mkRequestHand, // Joiner tells Host he wants to select some hand
|
||||
mkRequestTeam, // Joiner tells Host he wants to select some team
|
||||
mkRequestFlagColor, // Joiner tells Host he wants to select some color
|
||||
|
||||
mkResetMap, //Reset the map selection to blank
|
||||
mkMapSelect, //Host selects the map to play
|
||||
mkSaveSelect, //Host selects the save to play
|
||||
mkReadyToStart, //Joiner tells he's ready to play the game
|
||||
mkHasMapOrSave, //Joiner tells host he has the map/save file
|
||||
mkStart, //Host says to start the game
|
||||
mkReadyToReturnToLobby, //Joiner/host tells others they are ready to return to lobby
|
||||
|
||||
mkReadyToPlay, //Joiner tells Host he has loaded the map and clock can start
|
||||
mkPlay, //Host tells everyone that the game may begin
|
||||
mkAskToReconnect, //Dropped player askes permission from the host to reconnect
|
||||
mkRefuseReconnect, //Host tells the dropped player he is not allowed to reconnect
|
||||
mkResyncFromTick, //Dropped player requests other players to send missed commands from specified tick
|
||||
mkReconnectionAccepted, //Host tells dropped player they are accepted back into the game
|
||||
mkClientReconnected, //Host tells other players the index of a reconnected client
|
||||
|
||||
mkCommands, //Clients exchange commands for next ticks
|
||||
mkTextTranslated, //Clients exchange translated text (system messages)
|
||||
mkTextChat, //Clients exchange chat messages
|
||||
|
||||
mkReqPassword, //Server requests joiner to send password
|
||||
mkPassword, //Joiner sends password to server
|
||||
mkSetPassword, //Host sets password on server
|
||||
|
||||
mkFileRequest, //Joiner requests host to send file
|
||||
mkFileSendStarted, //Host notify all about he start sending file
|
||||
mkFileChunk, //Host sends chunk of file to joiner
|
||||
mkFileAck, //Joiner tells host he received a chunk
|
||||
mkFileEnd, //Host informs joiner that the whole file has been sent
|
||||
mkFileProgress, //Joiner informs other players about his map/save downloading progress
|
||||
|
||||
mkVote, //Joiner tells host his vote
|
||||
mkAskToSendCrashreport // Ask other player to send crashreport, because we got desync error with him
|
||||
);
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
// ^^^ Add new network command types to the end of the list. Check explanation above ^^^
|
||||
// ************************************************************************************************************************
|
||||
|
||||
TKMPacketFormat = (
|
||||
pfNoData, // Packet contains no data
|
||||
pfBinary, // Packet contains binary data (Stream)
|
||||
pfNumber, // Packet contains an integer
|
||||
pfStringA, // Packet contains ANSI string
|
||||
pfStringW // Packet contains Unicode string
|
||||
);
|
||||
|
||||
const
|
||||
NetPacketType: array [TKMNetMessageKind] of TKMPacketFormat = (
|
||||
pfBinary, //mkAskToJoin
|
||||
pfNoData, //mkAllowToJoin
|
||||
pfNumber, //mkRefuseToJoin
|
||||
pfBinary, //mkAskForAuth
|
||||
pfNumber, //mkIndexOnServer
|
||||
pfNumber, //mkClientLost
|
||||
pfBinary, //mkReassignHost
|
||||
pfStringA, //mkNetProtocolVersion
|
||||
pfStringW, //mkWelcomeMessage
|
||||
pfStringA, //mkServerName
|
||||
pfBinary, //mkJoinRoom
|
||||
pfNumber, //mkConnectedToRoom
|
||||
pfBinary, //mkSetGameInfo
|
||||
pfNumber, //mkKickPlayer
|
||||
pfNumber, //mkBanPlayer
|
||||
pfNumber, //mkGiveHost
|
||||
pfNoData, //mkResetBans
|
||||
pfNumber, //mkKicked
|
||||
pfStringA, //mkLangCode
|
||||
pfBinary, //mkAuthChallenge
|
||||
pfNoData, //mkGetServerInfo
|
||||
pfBinary, //mkServerInfo
|
||||
pfNumber, //mkDisconnect
|
||||
pfNoData, //mkPing
|
||||
pfNoData, //mkPong
|
||||
pfBinary, //mkPingFpsInfo
|
||||
pfNumber, //mkFPS
|
||||
pfBinary, //mkPlayersList
|
||||
pfBinary, //mkGameOptions
|
||||
pfNumber, //mkRequestStartingLoc
|
||||
pfNumber, //mkRequestTeam
|
||||
pfNumber, //mkRequestFlagColor
|
||||
pfNoData, //mkResetMap
|
||||
pfBinary, //mkMapSelect
|
||||
pfBinary, //mkSaveSelect
|
||||
pfNoData, //mkReadyToStart
|
||||
pfNoData, //mkHasMapOrSave
|
||||
pfBinary, //mkStart
|
||||
pfNoData, //mkReadyToReturnToLobby
|
||||
pfNoData, //mkReadyToPlay
|
||||
pfNoData, //mkPlay
|
||||
pfStringA, //mkAskToReconnect
|
||||
pfNumber, //mkRefuseReconnect
|
||||
pfNumber, //mkResyncFromTick
|
||||
pfNoData, //mkReconnectionAccepted
|
||||
pfNumber, //mkClientReconnected
|
||||
pfBinary, //mkCommands
|
||||
pfBinary, //mkTextTranslated
|
||||
pfBinary, //mkTextChat
|
||||
pfNoData, //mkReqPassword
|
||||
pfBinary, //mkPassword
|
||||
pfStringA, //mkSetPassword
|
||||
pfStringW, //mkFileRequest
|
||||
pfNumber, //mkFileSendStarted
|
||||
pfBinary, //mkFileChunk
|
||||
pfNoData, //mkFileAck
|
||||
pfNoData, //mkFileEnd
|
||||
pfBinary, //mkFileProgress
|
||||
pfNoData, //mkVote
|
||||
pfStringW //mkAskToSendCrashreport
|
||||
);
|
||||
|
||||
|
||||
type
|
||||
TKMNetHandleIndex = SmallInt;
|
||||
PKMNetHandleIndex = ^TKMNetHandleIndex;
|
||||
TMPGameState = (mgsNone, mgsLobby, mgsLoading, mgsGame);
|
||||
TKMServerType = (mstClient, mstDedicated, mstLocal);
|
||||
TKMNetPlayerType = (nptHuman, nptClosed, nptComputerClassic, nptComputerAdvanced);
|
||||
TKMNetPlayerTypeSet = set of TKMNetPlayerType;
|
||||
|
||||
|
||||
TKMPGameFilter = class
|
||||
private
|
||||
fDynamicFOW: Boolean;
|
||||
fMapsFilterEnabled: Boolean;
|
||||
fMapsCRCList: TKMMapsCRCList;
|
||||
fPeacetimeRng: TKMRangeInt;
|
||||
fSpeedRng: TKMRangeSingle;
|
||||
fSpeedAfterPTRng: TKMRangeSingle;
|
||||
procedure Reset;
|
||||
public
|
||||
constructor Create; overload;
|
||||
constructor Create(aDynamicFOW, aMapsFilterEnabled: Boolean; const aMapsCRCListStr: UnicodeString; const aPeacetimeRng: TKMRangeInt;
|
||||
const aSpeedRng: TKMRangeSingle; const aSpeedRngAfterPT: TKMRangeSingle); overload;
|
||||
destructor Destroy; override;
|
||||
|
||||
function FilterMap(aCRC: Cardinal): Boolean;
|
||||
|
||||
property DynamicFOW: Boolean read fDynamicFOW;
|
||||
property MapsFilterEnabled: Boolean read fMapsFilterEnabled;
|
||||
property MapsCRCList: TKMMapsCRCList read fMapsCRCList;
|
||||
property PeacetimeRng: TKMRangeInt read fPeacetimeRng;
|
||||
property SpeedRng: TKMRangeSingle read fSpeedRng;
|
||||
property SpeedAfterPTRng: TKMRangeSingle read fSpeedAfterPTRng;
|
||||
|
||||
procedure Save(aStream: TKMemoryStream);
|
||||
procedure Load(aStream: TKMemoryStream);
|
||||
end;
|
||||
|
||||
const
|
||||
AI_PLAYER_TYPE_MIN = nptComputerClassic;
|
||||
AI_PLAYER_TYPE_MAX = nptComputerAdvanced;
|
||||
//Used in the dedicated server display as it does not care about translations (translated ones are in KM_TextLibrary)
|
||||
GameStateText: array [TMPGameState] of UnicodeString = ('None', 'Lobby', 'Loading', 'Game');
|
||||
NetPlayerTypeName: array [TKMNetPlayerType] of UnicodeString = ('Human', 'Closed', 'Classic AI Player', 'Advanced AI Player');
|
||||
ServerTypePic: array [TKMServerType] of Word = (74, 75, 79);
|
||||
|
||||
function GetNetAddressStr(aNetworkAddress: Integer): String;
|
||||
function GetAIPlayerIcon(aNetPlayerType: TKMNetPlayerType): Word;
|
||||
|
||||
implementation
|
||||
uses
|
||||
SysUtils;
|
||||
|
||||
|
||||
{ TKMPGameFilter }
|
||||
constructor TKMPGameFilter.Create;
|
||||
begin
|
||||
inherited;
|
||||
|
||||
fMapsCRCList := TKMMapsCRCList.Create;
|
||||
Reset;
|
||||
end;
|
||||
|
||||
|
||||
constructor TKMPGameFilter.Create(aDynamicFOW, aMapsFilterEnabled: Boolean; const aMapsCRCListStr: UnicodeString; const aPeacetimeRng: TKMRangeInt;
|
||||
const aSpeedRng: TKMRangeSingle; const aSpeedRngAfterPT: TKMRangeSingle);
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fDynamicFOW := aDynamicFOW;
|
||||
fMapsFilterEnabled := aMapsFilterEnabled;
|
||||
|
||||
fMapsCRCList := TKMMapsCRCList.Create;
|
||||
fMapsCRCList.LoadFromString(aMapsCRCListStr);
|
||||
|
||||
fPeacetimeRng := aPeacetimeRng;
|
||||
fSpeedRng := aSpeedRng;
|
||||
fSpeedAfterPTRng := aSpeedRngAfterPT;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMPGameFilter.Destroy;
|
||||
begin
|
||||
FreeAndNil(fMapsCRCList);
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMPGameFilter.Save(aStream: TKMemoryStream);
|
||||
begin
|
||||
aStream.Write(fDynamicFOW);
|
||||
aStream.Write(fMapsFilterEnabled);
|
||||
aStream.WriteA(AnsiString(fMapsCRCList.PackToString));
|
||||
aStream.Write(fPeacetimeRng);
|
||||
aStream.Write(fSpeedRng);
|
||||
aStream.Write(fSpeedAfterPTRng);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMPGameFilter.Load(aStream: TKMemoryStream);
|
||||
var
|
||||
strA: AnsiString;
|
||||
begin
|
||||
aStream.Read(fDynamicFOW);
|
||||
aStream.Read(fMapsFilterEnabled);
|
||||
aStream.ReadA(strA);
|
||||
fMapsCRCList.LoadFromString(UnicodeString(strA));
|
||||
aStream.Read(fPeacetimeRng);
|
||||
aStream.Read(fSpeedRng);
|
||||
aStream.Read(fSpeedAfterPTRng);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMPGameFilter.Reset;
|
||||
begin
|
||||
fMapsCRCList.Clear;
|
||||
fDynamicFOW := False;
|
||||
fMapsFilterEnabled := False;
|
||||
fPeacetimeRng.Min := 0;
|
||||
fPeacetimeRng.Max := MaxInt;
|
||||
fSpeedRng.Min := 0;
|
||||
fSpeedRng.Max := MaxSingle;
|
||||
fSpeedAfterPTRng.Min := 0;
|
||||
fSpeedAfterPTRng.Max := MaxSingle;
|
||||
end;
|
||||
|
||||
|
||||
function TKMPGameFilter.FilterMap(aCRC: Cardinal): Boolean;
|
||||
begin
|
||||
Result := not fMapsFilterEnabled or fMapsCRCList.Contains(aCRC);
|
||||
end;
|
||||
|
||||
|
||||
function GetNetAddressStr(aNetworkAddress: Integer): String;
|
||||
begin
|
||||
case aNetworkAddress of
|
||||
NET_ADDRESS_EMPTY : Result := 'EMPTY';
|
||||
NET_ADDRESS_OTHERS : Result := 'OTHERS';
|
||||
NET_ADDRESS_ALL : Result := 'ALL';
|
||||
NET_ADDRESS_HOST : Result := 'HOST';
|
||||
NET_ADDRESS_SERVER : Result := 'SERVER';
|
||||
else Result := IntToStr(aNetworkAddress);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
function GetAIPlayerIcon(aNetPlayerType: TKMNetPlayerType): Word;
|
||||
begin
|
||||
case aNetPlayerType of
|
||||
nptComputerClassic: Result := 62; //PC Icon
|
||||
nptComputerAdvanced: Result := 74; //Large PC Icon
|
||||
else Result := 0; //None
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
unit KM_NetUDP;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, Math, SysUtils, KM_CommonClasses, KM_Defaults
|
||||
{$IFDEF WDC} ,KM_NetUDPOverbyte {$ENDIF}
|
||||
{$IFDEF FPC} ,KM_NetUDPLNet {$ENDIF}
|
||||
;
|
||||
|
||||
|
||||
type
|
||||
TNotifyServerDetectedEvent = procedure(const aAddress: string; const aPort: Word; const aName: string) of object;
|
||||
|
||||
TKMNetUDP = class
|
||||
private
|
||||
{$IFDEF WDC} fUDP: TKMNetUDPOverbyte; {$ENDIF}
|
||||
{$IFDEF FPC} fUDP: TKMNetUDPLNet; {$ENDIF}
|
||||
|
||||
fOnError: TGetStrProc;
|
||||
procedure HandleReceive(const aAddress: string; aData: Pointer; aLength: Cardinal); virtual; abstract;
|
||||
procedure HandleError(const aMsg: string);
|
||||
protected
|
||||
fScanPort: Word;
|
||||
public
|
||||
constructor Create(aScanPort: Word);
|
||||
destructor Destroy; override;
|
||||
procedure UpdateStateIdle;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
end;
|
||||
|
||||
TKMNetUDPAnnounce = class(TKMNetUDP)
|
||||
private
|
||||
fServerName: AnsiString;
|
||||
fGamePort: Word;
|
||||
procedure HandleReceive(const aAddress: string; aData: Pointer; aLength: Cardinal); override;
|
||||
public
|
||||
procedure StartAnnouncing(const aGamePort: Word; const aName: AnsiString; aAnnounce: Boolean);
|
||||
procedure StopAnnouncing;
|
||||
procedure UpdateSettings(const aName: AnsiString; aScanPort: Word);
|
||||
end;
|
||||
|
||||
TKMNetUDPScan = class(TKMNetUDP)
|
||||
private
|
||||
fOnServerDetected: TNotifyServerDetectedEvent;
|
||||
procedure HandleReceive(const aAddress: String; aData: Pointer; aLength: Cardinal); override;
|
||||
public
|
||||
procedure ScanForServers;
|
||||
procedure TerminateScan;
|
||||
property OnServerDetected: TNotifyServerDetectedEvent write fOnServerDetected;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
{ TKMNetUDP }
|
||||
constructor TKMNetUDP.Create(aScanPort: Word);
|
||||
begin
|
||||
inherited Create;
|
||||
{$IFDEF WDC} fUDP := TKMNetUDPOverbyte.Create; {$ENDIF}
|
||||
{$IFDEF FPC} fUDP := TKMNetUDPLNet.Create; {$ENDIF}
|
||||
fScanPort := aScanPort;
|
||||
fUDP.OnError := HandleError;
|
||||
fUDP.OnRecieveData := HandleReceive;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetUDP.Destroy;
|
||||
begin
|
||||
fUDP.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDP.HandleError(const aMsg: String);
|
||||
begin
|
||||
if Assigned(fOnError) then fOnError(aMsg);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDP.UpdateStateIdle;
|
||||
begin
|
||||
{$IFDEF FPC} fUDP.UpdateStateIdle; {$ENDIF}
|
||||
end;
|
||||
|
||||
|
||||
{ TKMNetUDPAnnounce }
|
||||
procedure TKMNetUDPAnnounce.StartAnnouncing(const aGamePort: Word; const aName: AnsiString; aAnnounce: Boolean);
|
||||
begin
|
||||
fServerName := aName;
|
||||
fGamePort := aGamePort;
|
||||
fUDP.StopListening;
|
||||
if aAnnounce then
|
||||
try
|
||||
fUDP.Listen(SERVER_DEFAULT_UDP_ANNOUNCE_PORT);
|
||||
except
|
||||
//UDP announce is not that important, and will fail whenever you start more than 1 server per machine
|
||||
on E: Exception do
|
||||
if Assigned(fOnError) then fOnError('UDP listen for local server detection failed to start: ' + E.Message);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPAnnounce.StopAnnouncing;
|
||||
begin
|
||||
fUDP.StopListening;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPAnnounce.UpdateSettings(const aName: AnsiString; aScanPort: Word);
|
||||
begin
|
||||
fServerName := aName;
|
||||
fScanPort := aScanPort;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPAnnounce.HandleReceive(const aAddress: string; aData: Pointer; aLength: Cardinal);
|
||||
var
|
||||
M: TKMemoryStream;
|
||||
S: AnsiString;
|
||||
begin
|
||||
M := TKMemoryStreamBinary.Create;
|
||||
try
|
||||
M.WriteBuffer(aData^, aLength);
|
||||
M.Position := 0;
|
||||
|
||||
//Check header
|
||||
M.ReadA(S);
|
||||
if S <> 'KaM Remake' then Exit;
|
||||
M.ReadA(S);
|
||||
if S <> NET_PROTOCOL_REVISON then Exit;
|
||||
|
||||
//Only care about scan packets
|
||||
M.ReadA(S);
|
||||
if S <> 'scan' then Exit;
|
||||
|
||||
//Send a response to this scanner
|
||||
M.Free;
|
||||
M := TKMemoryStreamBinary.Create;
|
||||
M.WriteA('KaM Remake');
|
||||
M.WriteA(NET_PROTOCOL_REVISON);
|
||||
M.WriteA('announce');
|
||||
M.Write(fGamePort);
|
||||
M.WriteA(fServerName);
|
||||
|
||||
fUDP.SendPacket(aAddress, fScanPort, M.Memory, M.Size);
|
||||
finally
|
||||
M.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
{ TKMNetUDPDetect }
|
||||
procedure TKMNetUDPScan.ScanForServers;
|
||||
var
|
||||
M: TKMemoryStream;
|
||||
begin
|
||||
//Prepare to receive responses
|
||||
fUDP.StopListening;
|
||||
try
|
||||
fUDP.Listen(fScanPort);
|
||||
except
|
||||
//UDP scan is not that important, and could fail during debugging if running two KaM Remake instances
|
||||
on E: Exception do
|
||||
begin
|
||||
if Assigned(fOnError) then fOnError('UDP scan failed to listen: ' + E.Message);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
M := TKMemoryStreamBinary.Create;
|
||||
try
|
||||
M.WriteA('KaM Remake');
|
||||
M.WriteA(NET_PROTOCOL_REVISON);
|
||||
M.WriteA('scan');
|
||||
try
|
||||
//Broadcast
|
||||
fUDP.SendPacket('255.255.255.255', 56789, M.Memory, M.Size);
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
if Assigned(fOnError) then fOnError('UDP broadcast failed: ' + E.Message);
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
finally
|
||||
M.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPScan.TerminateScan;
|
||||
begin
|
||||
fUDP.StopListening;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPScan.HandleReceive(const aAddress: String; aData: Pointer; aLength: Cardinal);
|
||||
var
|
||||
M: TKMemoryStream;
|
||||
S, ServerName: AnsiString;
|
||||
serverPort: Word;
|
||||
begin
|
||||
M := TKMemoryStreamBinary.Create;
|
||||
try
|
||||
M.WriteBuffer(aData^, aLength);
|
||||
M.Position := 0;
|
||||
|
||||
M.ReadA(S);
|
||||
if S <> 'KaM Remake' then Exit;
|
||||
M.ReadA(S);
|
||||
if S <> NET_PROTOCOL_REVISON then Exit;
|
||||
//Only care about announce packets
|
||||
M.ReadA(S);
|
||||
if S <> 'announce' then Exit;
|
||||
|
||||
//Read the server's game port and report it
|
||||
M.Read(serverPort);
|
||||
M.ReadA(ServerName);
|
||||
fOnServerDetected(aAddress, serverPort, UnicodeString(ServerName));
|
||||
finally
|
||||
M.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
unit KM_NetUDPLNet;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, Math, SysUtils, LNet;
|
||||
|
||||
|
||||
type
|
||||
TNotifyAddressDataEvent = procedure(const aAddress: string; aData: Pointer; aLength: Cardinal) of object;
|
||||
|
||||
TKMNetUDPLNet = class
|
||||
private
|
||||
fUDP: TLUdp;
|
||||
|
||||
fOnError: TGetStrProc;
|
||||
fOnRecieveData: TNotifyAddressDataEvent;
|
||||
procedure HandleReceive(aSocket: TLSocket);
|
||||
procedure HandleError(const aMsg: string; aSocket: TLSocket);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure SendPacket(const aAddress: string; const aPort: Word; aData: Pointer; aLength: Cardinal);
|
||||
procedure Listen(const aPort: Word);
|
||||
procedure StopListening;
|
||||
procedure UpdateStateIdle;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnRecieveData: TNotifyAddressDataEvent write fOnRecieveData;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
constructor TKMNetUDPLNet.Create;
|
||||
begin
|
||||
Inherited Create;
|
||||
|
||||
fUDP := TLUdp.Create(nil);
|
||||
fUDP.OnError := HandleError;
|
||||
fUDP.Timeout := 1;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetUDPLNet.Destroy;
|
||||
begin
|
||||
if fUDP<>nil then fUDP.Free;
|
||||
|
||||
Inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPLNet.Listen(const aPort:Word);
|
||||
begin
|
||||
fUDP.OnReceive := HandleReceive;
|
||||
fUDP.Listen(aPort);
|
||||
fUDP.CallAction;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPLNet.StopListening;
|
||||
begin
|
||||
fUDP.Disconnect;
|
||||
fUDP.OnReceive := nil;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPLNet.SendPacket(const aAddress: string; const aPort: Word; aData: Pointer; aLength: Cardinal);
|
||||
begin
|
||||
fUDP.Send(aData^, aLength, aAddress + ':' + IntToStr(aPort));
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPLNet.HandleReceive(aSocket: TLSocket);
|
||||
const
|
||||
BUFFER_SIZE = 10240; //10kb
|
||||
var
|
||||
P: Pointer;
|
||||
L: Integer; //L could be -1 when no data is available
|
||||
begin
|
||||
GetMem(P, BUFFER_SIZE+1); //+1 to avoid RangeCheckError when L = BUFFER_SIZE
|
||||
L := fUDP.Get(P^, BUFFER_SIZE, aSocket);
|
||||
|
||||
if L > 0 then
|
||||
fOnRecieveData(aSocket.PeerAddress, P, L);
|
||||
|
||||
FreeMem(P);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPLNet.HandleError(const aMsg: string; aSocket: TLSocket);
|
||||
begin
|
||||
fOnError('LNet UDP Error: ' + aMsg);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPLNet.UpdateStateIdle;
|
||||
begin
|
||||
if fUDP <> nil then fUDP.CallAction; //Process network events
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
unit KM_NetUDPOverbyte;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, Math, SysUtils, OverbyteIcsWSocket, OverbyteIcsWinSock;
|
||||
|
||||
|
||||
type
|
||||
TNotifyAddressDataEvent = procedure(const aAddress: string; aData: Pointer; aLength: Cardinal) of object;
|
||||
|
||||
TKMNetUDPOverbyte = class
|
||||
private
|
||||
fSocketSend: TWSocket;
|
||||
fSocketReceive: TWSocket;
|
||||
|
||||
fOnError: TGetStrProc;
|
||||
fOnRecieveData: TNotifyAddressDataEvent;
|
||||
procedure DataAvailable(Sender: TObject; aError: Word);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
procedure SendPacket(const aAddress:string; const aPort: Word; aData: Pointer; aLength: Cardinal);
|
||||
procedure Listen(const aPort: Word);
|
||||
procedure StopListening;
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnRecieveData: TNotifyAddressDataEvent write fOnRecieveData;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
{ TKMNetUDPOverbyte }
|
||||
constructor TKMNetUDPOverbyte.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fSocketSend := TWSocket.Create(nil);
|
||||
fSocketReceive := TWSocket.Create(nil);
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMNetUDPOverbyte.Destroy;
|
||||
begin
|
||||
fSocketSend.Free;
|
||||
fSocketReceive.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPOverbyte.Listen(const aPort: Word);
|
||||
begin
|
||||
fSocketReceive.OnDataAvailable := DataAvailable;
|
||||
try
|
||||
fSocketReceive.Proto := 'udp';
|
||||
fSocketReceive.Addr := '0.0.0.0';
|
||||
fSocketReceive.Port := IntToStr(aPort);
|
||||
fSocketReceive.Listen;
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
// Trap the exception and tell the user. Note: While debugging, Delphi will still stop execution for the exception, but normally the dialouge won't show.
|
||||
fOnError(E.Message);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPOverbyte.StopListening;
|
||||
begin
|
||||
fSocketReceive.Close;
|
||||
fSocketReceive.OnDataAvailable := nil;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPOverbyte.SendPacket(const aAddress:string; const aPort: Word; aData: Pointer; aLength: Cardinal);
|
||||
begin
|
||||
fSocketSend.Proto := 'udp';
|
||||
fSocketSend.Addr := aAddress;
|
||||
fSocketSend.Port := IntToStr(aPort);
|
||||
fSocketSend.LocalPort := '0';
|
||||
fSocketSend.Connect;
|
||||
fSocketSend.Send(aData, aLength);
|
||||
fSocketSend.Close;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMNetUDPOverbyte.DataAvailable(Sender: TObject; aError: Word);
|
||||
const
|
||||
BUFFER_SIZE = 10240; //10kb
|
||||
var
|
||||
P: Pointer;
|
||||
L: Integer; //L could be -1 when no data is available
|
||||
sockAddr: TSockAddr;
|
||||
srcLen : Integer;
|
||||
begin
|
||||
srcLen := SizeOf(sockAddr);
|
||||
if aError <> 0 then
|
||||
begin
|
||||
fOnError('UDP DataAvailable. Error ' + WSocketErrorDesc(aError) + ' (#' + IntToStr(aError) + ')');
|
||||
exit;
|
||||
end;
|
||||
|
||||
GetMem(P, BUFFER_SIZE+1); //+1 to avoid RangeCheckError when L = BUFFER_SIZE
|
||||
L := TWSocket(Sender).ReceiveFrom(P, BUFFER_SIZE, sockAddr, srcLen);
|
||||
|
||||
if L > 0 then
|
||||
fOnRecieveData(UnicodeString(WSocket_inet_ntoa(sockAddr.sin_addr)), P, L);
|
||||
|
||||
FreeMem(P);
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
unit KM_NetworkUtils;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
|
||||
type
|
||||
TKMNetworkUtils = class
|
||||
|
||||
public
|
||||
class function GetEscapedNewLineServerName(str: String): String; static;
|
||||
class function GetEscapedNewLineServerNameA(str: AnsiString): AnsiString; static;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
uses
|
||||
StrUtils, SysUtils;
|
||||
|
||||
|
||||
class function TKMNetworkUtils.GetEscapedNewLineServerName(str: String): String;
|
||||
begin
|
||||
Result := StringReplace(str, '|', ' ', [rfReplaceAll]);
|
||||
end;
|
||||
|
||||
|
||||
// Add AnsiString version of the same procedure, to avoid type casting compilation hint
|
||||
class function TKMNetworkUtils.GetEscapedNewLineServerNameA(str: AnsiString): AnsiString;
|
||||
begin
|
||||
Result := AnsiString(GetEscapedNewLineServerName(String(str)));
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
+2848
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,78 @@
|
||||
unit KM_HTTPClient;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils
|
||||
{$IFDEF WDC} ,KM_HTTPClientOverbyte {$ENDIF}
|
||||
{$IFDEF FPC} ,KM_HTTPClientLNet {$ENDIF};
|
||||
|
||||
type
|
||||
//General wrapper for Delphi/Lazarus implementations
|
||||
TKMHTTPClient = class
|
||||
private
|
||||
{$IFDEF WDC} fClient: TKMHTTPClientOverbyte; {$ENDIF}
|
||||
{$IFDEF FPC} fClient: TKMHTTPClientLNet; {$ENDIF}
|
||||
fOnError: TGetStrProc;
|
||||
fOnReceive: TGetStrProc;
|
||||
procedure Error(const S: string);
|
||||
procedure GetCompleted(const S: string);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure GetURL(const aURL: string; aIsUTF8: Boolean);
|
||||
procedure UpdateStateIdle;
|
||||
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnReceive: TGetStrProc write fOnReceive;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
constructor TKMHTTPClient.Create;
|
||||
begin
|
||||
inherited;
|
||||
|
||||
{$IFDEF WDC} fClient := TKMHTTPClientOverbyte.Create; {$ENDIF}
|
||||
{$IFDEF FPC} fClient := TKMHTTPClientLNet.Create; {$ENDIF}
|
||||
fClient.OnGetCompleted := GetCompleted;
|
||||
fClient.OnError := Error;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMHTTPClient.Destroy;
|
||||
begin
|
||||
fClient.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClient.GetURL(const aURL: string; aIsUTF8: Boolean);
|
||||
begin
|
||||
fClient.GetURL(aUrl, aIsUTF8);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClient.GetCompleted(const S: string);
|
||||
begin
|
||||
if Assigned(fOnReceive) then fOnReceive(S);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClient.Error(const S: string);
|
||||
begin
|
||||
if Assigned(fOnError) then fOnError(S);
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClient.UpdateStateIdle;
|
||||
begin
|
||||
{$IFDEF FPC} fClient.UpdateStateIdle; {$ENDIF}
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
unit KM_HTTPClientLNet;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, lNet, URLUtils, lHTTP;
|
||||
|
||||
type
|
||||
TKMHTTPClientLNet = class
|
||||
private
|
||||
fHTTPClient: TLHTTPClient;
|
||||
HTTPBuffer: AnsiString;
|
||||
fOnError: TGetStrProc;
|
||||
fOnGetCompleted: TGetStrProc;
|
||||
fIsUTF8: Boolean;
|
||||
procedure HTTPClientDoneInput(ASocket: TLHTTPClientSocket);
|
||||
procedure HTTPClientError(const msg: string; aSocket: TLSocket);
|
||||
function HTTPClientInput(ASocket: TLHTTPClientSocket; ABuffer: PChar; ASize: Integer): Integer;
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure GetURL(const aURL: string; aIsUTF8: Boolean);
|
||||
procedure UpdateStateIdle;
|
||||
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnGetCompleted: TGetStrProc write fOnGetCompleted;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
constructor TKMHTTPClientLNet.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fHTTPClient := TLHTTPClient.Create(nil);
|
||||
fHTTPClient.Timeout := 0;
|
||||
fHTTPClient.OnInput := HTTPClientInput;
|
||||
fHTTPClient.OnError := HTTPClientError;
|
||||
fHTTPClient.OnDoneInput := HTTPClientDoneInput;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMHTTPClientLNet.Destroy;
|
||||
begin
|
||||
fHTTPClient.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClientLNet.GetURL(const aURL: string; aIsUTF8: Boolean);
|
||||
var
|
||||
proto, user, pass, host, port, path: string;
|
||||
begin
|
||||
fIsUTF8 := aIsUTF8;
|
||||
fHTTPClient.Disconnect(True); //If we were doing something, stop it
|
||||
HTTPBuffer := '';
|
||||
ParseURL(aURL, proto, user, pass, host, port, path);
|
||||
fHTTPClient.Host := host;
|
||||
fHTTPClient.URI := path;
|
||||
fHTTPClient.Port := StrToIntDef(port, 80);
|
||||
fHTTPClient.SendRequest;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClientLNet.HTTPClientDoneInput(ASocket: TLHTTPClientSocket);
|
||||
var
|
||||
returnString: UnicodeString;
|
||||
begin
|
||||
aSocket.Disconnect;
|
||||
if fIsUTF8 then
|
||||
returnString := UTF8Decode(HTTPBuffer)
|
||||
else
|
||||
returnString := UnicodeString(HTTPBuffer);
|
||||
if Assigned(fOnGetCompleted) then
|
||||
fOnGetCompleted(returnString);
|
||||
HTTPBuffer := '';
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClientLNet.HTTPClientError(const msg: string; aSocket: TLSocket);
|
||||
begin
|
||||
if Assigned(fOnError) then
|
||||
fOnError(msg);
|
||||
end;
|
||||
|
||||
|
||||
function TKMHTTPClientLNet.HTTPClientInput(ASocket: TLHTTPClientSocket; ABuffer: PChar; ASize: Integer): Integer;
|
||||
var
|
||||
oldLength: dword;
|
||||
begin
|
||||
if ASize > 0 then
|
||||
begin
|
||||
oldLength := Length(HTTPBuffer);
|
||||
setlength(HTTPBuffer, oldLength + ASize);
|
||||
move(ABuffer^, HTTPBuffer[oldLength + 1], ASize);
|
||||
end;
|
||||
Result := aSize; // tell the http buffer we read it all
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClientLNet.UpdateStateIdle;
|
||||
begin
|
||||
fHTTPClient.CallAction; //Process network events
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
unit KM_HTTPClientOverbyte;
|
||||
{$I KaM_Remake.inc}
|
||||
interface
|
||||
uses
|
||||
Classes, SysUtils, OverbyteIcsHttpProt;
|
||||
|
||||
|
||||
type
|
||||
TKMHTTPClientOverbyte = class
|
||||
private
|
||||
fHTTPClient: THTTPCli;
|
||||
fOnError: TGetStrProc;
|
||||
fOnGetCompleted: TGetStrProc;
|
||||
fIsUTF8: Boolean;
|
||||
procedure RequestDone(Sender: TObject; RqType: THttpRequest; ErrCode: Word);
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
|
||||
procedure GetURL(const aURL: string; aIsUTF8: Boolean);
|
||||
|
||||
property OnError: TGetStrProc write fOnError;
|
||||
property OnGetCompleted: TGetStrProc write fOnGetCompleted;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
|
||||
constructor TKMHTTPClientOverbyte.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
|
||||
fHTTPClient := THTTPCli.Create(nil);
|
||||
fHTTPClient.OnRequestDone := RequestDone;
|
||||
fHTTPClient.RcvdStream := TMemoryStream.Create;
|
||||
end;
|
||||
|
||||
|
||||
destructor TKMHTTPClientOverbyte.Destroy;
|
||||
begin
|
||||
fHTTPClient.RcvdStream.Free; //RcvdStream is created and managed by us
|
||||
fHTTPClient.Free;
|
||||
|
||||
inherited;
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClientOverbyte.GetURL(const aURL: string; aIsUTF8: Boolean);
|
||||
begin
|
||||
fIsUTF8 := aIsUTF8;
|
||||
fHTTPClient.Abort; //If we were doing something, stop it
|
||||
TMemoryStream(fHTTPClient.RcvdStream).Clear;
|
||||
fHTTPClient.URL := aURL;
|
||||
fHTTPClient.GetASync; //Non-blocking
|
||||
end;
|
||||
|
||||
|
||||
procedure TKMHTTPClientOverbyte.RequestDone(Sender: TObject; RqType: THttpRequest; ErrCode: Word);
|
||||
var
|
||||
rcvdText: AnsiString;
|
||||
returnText: UnicodeString;
|
||||
begin
|
||||
if RqType <> httpGET then exit;
|
||||
if ErrCode <> 0 then
|
||||
begin
|
||||
if Assigned(fOnError) then
|
||||
fOnError(IntToStr(fHTTPClient.StatusCode) + ' ' + fHTTPClient.ReasonPhrase + ' (#' + IntToStr(ErrCode) + ')');
|
||||
Exit;
|
||||
end;
|
||||
|
||||
rcvdText := '';
|
||||
if fHTTPClient.RcvdStream.Size > 0 then
|
||||
begin
|
||||
SetLength(rcvdText, fHTTPClient.RcvdStream.Size);
|
||||
Move(TMemoryStream(fHTTPClient.RcvdStream).Memory^, rcvdText[1], fHTTPClient.RcvdStream.Size);
|
||||
TMemoryStream(fHTTPClient.RcvdStream).Clear;
|
||||
end;
|
||||
|
||||
if fIsUTF8 then
|
||||
returnText := UTF8ToUnicodeString(rcvdText)
|
||||
else
|
||||
returnText := UnicodeString(rcvdText);
|
||||
|
||||
if Assigned(fOnGetCompleted) then
|
||||
fOnGetCompleted(returnText);
|
||||
end;
|
||||
|
||||
|
||||
end.
|
||||
|
||||
Reference in new issue
Block a user