sync: restore source from Google Drive
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s

This commit is contained in:
admin_d committed 2026-09-12 16:50:30 +05:00
1 parent e2d8247e91
commit 7063d1b1c9
993 files changed
+949976

No files matched your search

+41
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+313
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
File diff suppressed because it is too large. Load diff
+78
View File
@@ -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.
+110
View File
@@ -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.