Files
kam-mobile/vendor/kam_remake/src/net/KM_Networking.pas
T
admin_d 7063d1b1c9
Sync changed files to Google Drive / sync (push) Successful in 1h45m13s
sync: restore source from Google Drive
2026-09-12 16:50:30 +05:00

2849 lines
102 KiB
ObjectPascal

unit KM_Networking;
{$I KaM_Remake.inc}
interface
uses
{$IFDEF Unix} LCLIntf, {$ENDIF}
Classes, SysUtils, TypInfo, Generics.Collections,
KromUtils,
KM_Console,
KM_CommonClasses, KM_CommonTypes, KM_NetGameInfo, KM_NetTypes, KM_Defaults, KM_Points,
KM_Saves, KM_GameOptions, KM_ResLocales, KM_NetFileTransfer, KM_Maps, KM_MapTypes, KM_NetRoom,
KM_NetDedicatedServer, KM_NetClient, KM_NetServerPoller,
{$IFDEF DBG_SKIP_SECURE_AUTH}
KM_NetAuthUnsecure
{$ELSE}
// If you don't have this file - enable DBG_SKIP_SECURE_AUTH in KaM_Remake.inc
KM_NetAuthSecure
{$ENDIF}
;
type
TKMMapStartEvent = procedure (const aData: UnicodeString; aMapKind: TKMMapKind; aCRC: Cardinal; Spectating: Boolean;
aMissionDifficulty: TKMMissionDifficulty) of object;
// Should handle message exchange and routing, interacting with UI
TKMNetworking = class
private
fPacketsReceived: array [TKMNetMessageKind] of Cardinal;
fPacketsSent: array [TKMNetMessageKind] of Cardinal;
fPacketsStatsStartTime: Cardinal;
fNetServer: TKMNetDedicatedServer;
fNetClient: TKMNetClient;
fNetServerPoller: TKMNetServerPoller;
fNetPlayerKind: TKMNetPlayerKind; // Our role (Host or Joiner)
fNetGameState: TKMNetGameState;
fServerAddress: string; // Used for reconnecting
fServerPort: Word; // Used for reconnecting
fRoomToJoin: Integer; // The room we should join once we hear from the server
fLastProcessedTick: Cardinal;
fReconnectRequested: Cardinal; // TickCount at which a reconnection was requested
fMyNickname: AnsiString;
fWelcomeMessage: UnicodeString;
fServerName: AnsiString; // Name of the server we are currently in (shown in the lobby)
fPassword: AnsiString;
fDescription: UnicodeString;
fEnteringPassword: Boolean;
fMyIndexOnServer: TKMNetHandleIndex;
fMySlotIndex: Integer; // In the room
fHostSlotIndex: Integer; //In the room
fIgnorePings: Integer; // During loading ping measurements will be high, so discard them. (when networking is threaded this might be unnecessary)
fJoinTimeout, fLastVoteTime: Cardinal;
fReturnedToLobby: Boolean; //Did we get to the lobby by return to lobby feature?
fNetRoom: TKMNetRoom;
fMutedPlayersList: TList<TKMNetHandleIndex>; // List of ServerIndexes of muted players.
fMapInfo: TKMMapInfo; // Everything related to selected map
fSaveInfo: TKMSaveInfo;
fSelectGameKind: TKMNetGameKind;
fNetGameOptions: TKMGameOptions;
fNetGameFilter: TKMPGameFilter;
fFileReceiver: TKMFileReceiver;
fFileSenderManager: TKMFileSenderManager;
fMissingBundleType: TKMNetGameKind;
fMissingBundleName: UnicodeString;
fMissingBundleCRC: Cardinal;
fVoteReturnToLobbySucceeded: Boolean;
procedure HandleMessagePingFpsInfo(aStream: TKMemoryStream);
procedure ForcedDisconnect(Sender: TObject);
procedure StartGame;
procedure TryPlayGame;
procedure PlayGame;
procedure SetGameState(aState: TKMNetGameState; aOnMPInfoChanged: Boolean = True);
procedure SendMapOrSave(aRecipient: TKMNetHandleIndex = NET_ADDRESS_OTHERS);
procedure DoReconnection;
procedure HandleMessageAskToJoin(aServerIndex: Integer; aStream: TKMemoryStream);
function IsPlayerHandStillInGame(aSlotIndex: Integer): Boolean;
procedure HandleMessageReassignHost(aSenderIndex: TKMNetHandleIndex; aStream: TKMemoryStream);
procedure HandleMessagePlayerDisconnected(aSenderIndex: TKMNetHandleIndex; aLastSentCommandsTick: Integer);
procedure HandleMessagePlayersList(aStream: TKMemoryStream);
procedure ReturnToLobbyVoteSucceeded;
procedure ResetReturnToLobbyVote;
procedure TransferOnCompleted(aClientIndex: TKMNetHandleIndex);
procedure TransferOnPacket(aClientIndex: TKMNetHandleIndex; aStream: TKMemoryStream; out aSendBufferEmpty: Boolean);
function GetMyRoomSlot: TKMNetRoomSlot;
procedure SetDownloadlInProgress(aSenderIndex: TKMNetHandleIndex; aValue: Boolean);
procedure HandleMessageFileRequest(aSenderIndex: TKMNetHandleIndex; aStream: TKMemoryStream);
procedure HandleMessage(aMessageKind: TKMNetMessageKind; aStream: TKMemoryStream; aSenderIndex: TKMNetHandleIndex);
procedure ConnectSucceed(Sender:TObject);
procedure ConnectFailed(const aText: string);
function GetNetAddressPrintDescr(aNetworkAddress: Integer): String;
procedure LogPacket(aIsSending: Boolean; aKind: TKMNetMessageKind; aNetworkAddress: TKMNetHandleIndex);
procedure PostLogMessageToChat(const aLogMessage: UnicodeString);
procedure PacketRecieve(aNetClient: TKMNetClient; aSenderIndex: TKMNetHandleIndex; aData: Pointer; aLength: Cardinal); //Process all commands
procedure PacketSend(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind); overload;
procedure PacketSendS(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aStream: TKMemoryStream);
procedure PacketSendI(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aParam: Integer);
procedure PacketSendC(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aParam: Cardinal);
// procedure PacketSend(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; const aParams: array of Integer);
procedure PacketSendInd(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aIndexOnServer: TKMNetHandleIndex);
procedure PacketSendA(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; const aText: AnsiString);
procedure PacketSendW(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; const aText: UnicodeString);
procedure SetDescription(const Value: UnicodeString);
function GetPacketsReceived(aKind: TKMNetMessageKind): Cardinal;
function GetPacketsSent(aKind: TKMNetMessageKind): Cardinal;
function GetMapInfo: TKMMapInfo;
public
OnJoinSucc: TKMEvent; // We were allowed to join
OnJoinFail: TUnicodeStringEvent; // We were refused to join
OnJoinPassword: TKMEvent; // Lobby requires password
OnHostFail: TUnicodeStringEvent; // Server failed to start (already running a server?)
OnJoinAssignedHost: TKMEvent; // We were assigned hosting rights upon connection
OnReassignedHost: TKMEvent; // We were reassigned hosting rights when the host quit
OnReassignedJoiner: TKMEvent; // We were reassigned to a joiner from host
OnFileTransferProgress: TKMTransferProgressEvent; // File transfer progress to this player
OnPlayerFileTransferProgress: TKMTransferProgressPlayerEvent; // File transfer progress to other player
OnPlayersSetup: TKMEvent; // Player list updated
OnUpdateMinimap: TKMEvent; // Update minimap
OnGameOptions: TKMEvent; // Game options updated
OnMapName: TUnicodeStringEvent; // Map name updated
OnMapMissing: TUnicodeStringBoolEvent;// Map missing
OnStartMap: TKMMapStartEvent; // Start the game
OnStartSave: TGameStartEvent; // Load the game
OnDoReturnToLobby: TKMEvent;
OnAnnounceReturnToLobby: TKMEvent; // Sends GIC command to create synchronised save file
OnPlay: TKMEvent; // Start the gameplay
OnReadyToPlay: TKMEvent; // Update the list of players ready to play
OnPingInfo: TKMEvent; // Ping info updated
OnMPGameInfoChanged: TKMEvent;
OnSetPassword: TAnsiStringEvent;
OnAbortAllTransfers: TKMEvent;
OnDisconnect: TUnicodeStringEvent; // Lost connection or was kicked
OnJoinerDropped: TIntegerEvent; // Other player disconnected
OnCommands: TStreamIntEvent; // Recieved GIP commands
OnResyncFromTick: TResyncEvent;
OnTextMessage: TUnicodeStringEvent; // Text message recieved
constructor Create(const aMasterServerAddress: string; aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort: Word;
const aPacketsAccDelay: Integer;
aDynamicFOW, aMapsFilterEnabled: Boolean; const aMapsCRCListStr: UnicodeString; const aPeacetimeRng: TKMRangeInt;
const aSpeedRng: TKMRangeSingle; const aSpeedRngAfterPT: TKMRangeSingle);
destructor Destroy; override;
property MySlotIndex: Integer read fMySlotIndex;
property MyIndexOnServer: TKMNetHandleIndex read fMyIndexOnServer;
property HostSlotIndex: Integer read fHostSlotIndex;
property NetGameState: TKMNetGameState read fNetGameState;
property NetGameFilter: TKMPGameFilter read fNetGameFilter;
function GetFullServerName: string;
function IsHost: Boolean;
function IsReconnecting: Boolean;
function CalculateGameCRC: Cardinal;
function IsMuted(aSlotIndex: Integer): Boolean;
procedure ToggleMuted(aSlotIndex: Integer);
function IsSave: Boolean;
function IsMap: Boolean;
//Lobby
property ServerPoller: TKMNetServerPoller read fNetServerPoller;
procedure Host(const aServerName: AnsiString; aPort: Word; const aNickname: AnsiString; aAnnounceServer, aAnnounceUDP: Boolean);
procedure Join(const aServerAddress: string; aPort: Word; const aNickname: AnsiString; aRoom: Integer; aIsReconnection: Boolean = False);
procedure AnnounceDisconnect(aLastSentCmdsTick: Cardinal = LAST_SENT_COMMANDS_TICK_NONE);
procedure Disconnect;
procedure DropPlayers(aPlayers: TKMByteArray);
function Connected: Boolean;
procedure MatchPlayersToSave(aPlayerID: Integer = -1);
procedure AbortAllTransfers;
procedure SelectNoMap(const aErrorMessage: UnicodeString);
procedure SelectMap(const aName: UnicodeString; aMapKind: TKMMapKind; aSendPlayerSetup: Boolean = False);
procedure SelectSave(const aName: UnicodeString);
procedure SelectHand(aSlotIndex: Integer; aHandIndex: Integer);
procedure SelectTeam(aSlotIndex: Integer; aTeam: Integer);
procedure SelectColor(aSlotIndex: Integer; aColor: Cardinal);
procedure KickPlayer(aSlotIndex: Integer);
procedure BanPlayer(aSlotIndex: Integer);
procedure SetToHost(aSlotIndex: Integer);
procedure ResetBans;
procedure SendPassword(const aPassword: AnsiString);
procedure SetPassword(const aPassword: AnsiString);
property Password: AnsiString read fPassword;
property Description: UnicodeString read fDescription write SetDescription;
function ReadyToStart: Boolean;
function CanStart: TKMGameStartMode;
function CanTakeLocation(aSlotIndex, aLoc: Integer; AllowSwapping: Boolean): Boolean;
procedure StartClick; //All required arguments are in our class
procedure SendPlayerListAndRefreshPlayersSetup(aPlayerIndex: TKMNetHandleIndex = NET_ADDRESS_OTHERS);
procedure UpdateGameOptions(aPeacetime: Word; aSpeedPT, aSpeedAfterPT: Single; aDifficulty: TKMMissionDifficulty);
procedure SendGameOptions;
procedure RequestFileTransfer;
procedure VoteReturnToLobby;
procedure AnnounceReadyToReturnToLobby;
procedure WakeUpNotReady;
procedure AskToSendCrashreport(aSlotIndex: Integer; aErrorStr: UnicodeString);
//Common
procedure PostMessage(aTextID: Integer; aSound: TKMChatSound; const aArgument1: UnicodeString = ''; const aArgument2: UnicodeString = '';
aRecipient: TKMNetHandleIndex = NET_ADDRESS_ALL; aArgumentInt: Integer = -1);
procedure PostChat(const aText: UnicodeString; aMode: TKMChatMode; aRecipientServerIndex: TKMNetHandleIndex = NET_ADDRESS_OTHERS); overload;
procedure PostLocalMessage(const aText: UnicodeString; aSound: TKMChatSound = csNone);
procedure AnnounceGameInfo(aGameTime: TDateTime; const aMap: UnicodeString);
//Gameplay
property MapInfo: TKMMapInfo read GetMapInfo;
property SaveInfo: TKMSaveInfo read fSaveInfo;
property NetGameOptions: TKMGameOptions read fNetGameOptions;
property SelectGameKind: TKMNetGameKind read fSelectGameKind;
property Room: TKMNetRoom read fNetRoom;
property MyRoomSlot: TKMNetRoomSlot read GetMyRoomSlot;
property LastProcessedTick: Cardinal write fLastProcessedTick;
property MissingBundleType: TKMNetGameKind read fMissingBundleType;
property MissingBundleName: UnicodeString read fMissingBundleName;
procedure GameCreated;
procedure SendCommands(aStream: TKMemoryStream; aSlotIndex: ShortInt = -1);
procedure AttemptReconnection;
procedure ReturnToLobby;
property PacketsReceived[aKind: TKMNetMessageKind]: Cardinal read GetPacketsReceived;
property PacketsSent[aKind: TKMNetMessageKind]: Cardinal read GetPacketsSent;
property PacketsStatsStartTime: Cardinal read fPacketsStatsStartTime;
procedure ResetPacketsStats;
procedure UpdateState(aGlobalTickCount: cardinal);
procedure UpdateStateIdle;
procedure SendFPSMeasurement(aFPS: Integer);
end;
var
gNetworking: TKMNetworking;
implementation
uses
Math, StrUtils,
KM_NetConsts, KM_Sound, KM_Log, KM_CommonUtils, KM_HandsCollection, KM_Hand,
KM_System, KM_GameApp, KM_GameSettings,
KM_Resource, KM_ResSound, KM_ResTexts, KM_NetworkUtils;
{ TKMNetworking }
constructor TKMNetworking.Create(const aMasterServerAddress: string; aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort: Word;
const aPacketsAccDelay: Integer;
aDynamicFOW, aMapsFilterEnabled: Boolean; const aMapsCRCListStr: UnicodeString; const aPeacetimeRng: TKMRangeInt;
const aSpeedRng: TKMRangeSingle; const aSpeedRngAfterPT: TKMRangeSingle);
var
gameFilter: TKMPGameFilter;
begin
inherited Create;
SetGameState(lgsNone);
fNetServer := TKMNetDedicatedServer.Create(1, aKickTimeout, aPingInterval, aAnnounceInterval, aServerUDPScanPort,
aMasterServerAddress, '', '', aPacketsAccDelay, False);
gameFilter := TKMPGameFilter.Create(aDynamicFOW, aMapsFilterEnabled, aMapsCRCListStr, aPeacetimeRng, aSpeedRng, aSpeedRngAfterPT);
fNetServer.Server.GameFilter := gameFilter;
fNetGameFilter := TKMPGameFilter.Create;
fNetClient := TKMNetClient.Create;
// Handle all 'background (unhandled)' exceptions, so we will be able to intercept them with madExcept
fNetClient.SetHandleBackgrounException;
fNetRoom := TKMNetRoom.Create;
fNetServerPoller := TKMNetServerPoller.Create(aMasterServerAddress, aServerUDPScanPort);
fNetGameOptions := TKMGameOptions.Create;
fFileSenderManager := TKMFileSenderManager.Create;
fMutedPlayersList := TList<TKMNetHandleIndex>.Create;
fFileSenderManager.OnTransferCompleted := TransferOnCompleted;
fFileSenderManager.OnTransferPacket := TransferOnPacket;
gLog.AddOnLogEventSub(PostLogMessageToChat);
fVoteReturnToLobbySucceeded := False;
gNetworking := Self;
end;
destructor TKMNetworking.Destroy;
begin
gLog.RemoveOnLogEventSub(PostLogMessageToChat);
fNetRoom.Free;
fNetServer.Free;
fNetClient.Free;
fNetServerPoller.Free;
fFileSenderManager.Free;
fMutedPlayersList.Free;
FreeAndNil(fMapInfo);
FreeAndNil(fSaveInfo);
FreeAndNil(fNetGameOptions);
FreeAndNil(fNetGameFilter);
gNetworking := nil;
inherited;
end;
function TKMNetworking.IsHost: Boolean;
begin
if Self = nil then Exit(False);
Result := (fNetPlayerKind = lpkHost);
end;
function TKMNetworking.IsReconnecting: Boolean;
begin
Result := (fNetGameState = lgsReconnecting) or (fReconnectRequested <> 0);
end;
//Startup a local server and connect to it as ordinary client
procedure TKMNetworking.Host(const aServerName: AnsiString; aPort: Word; const aNickname: AnsiString;
aAnnounceServer, aAnnounceUDP: Boolean);
begin
fWelcomeMessage := '';
fPassword := '';
fDescription := '';
fIgnorePings := 0; //Accept pings
fNetServer.Stop;
fNetServer.OnMessage := gLog.AddTime; //Log server messages in case there is a problem, but hide from user
try
fNetServer.Start(aServerName, aPort, aAnnounceServer, aAnnounceUDP);
except
on E: Exception do
begin
// Server failed to start
OnHostFail(E.Message);
Exit;
end;
end;
Join('127.0.0.1', aPort, aNickname, 0); //Server will assign hosting rights to us as we are the first joiner
end;
procedure TKMNetworking.Join(const aServerAddress: string; aPort: Word; const aNickname: AnsiString; aRoom: Integer; aIsReconnection: Boolean = False);
begin
Assert(not fNetClient.Connected, 'Cannot connect: We are already connected');
fWelcomeMessage := '';
fPassword := '';
fDescription := '';
fIgnorePings := 0; //Accept pings
fJoinTimeout := TimeGet;
fMySlotIndex := -1; //Host will send us PlayerList and we will get our index from there
fHostSlotIndex := -1;
fMyIndexOnServer := -1; //Assigned by Server
fRoomToJoin := aRoom;
if aIsReconnection then
SetGameState(lgsReconnecting) //Special state so we know we are reconnecting
else
SetGameState(lgsConnecting); //We are still connecting to the server
fServerAddress := aServerAddress;
fServerPort := aPort;
fMyNickname := aNickname;
fNetPlayerKind := lpkJoiner;
fServerName := ''; //Server will tell us once we are joined
fNetClient.OnRecieveData := PacketRecieve;
fNetClient.OnConnectSucceed := ConnectSucceed;
fNetClient.OnConnectFailed := ConnectFailed;
fNetClient.OnForcedDisconnect := ForcedDisconnect;
//fNetClient.OnStatusMessage := OnTextMessage; //For debugging only
fNetClient.ConnectTo(fServerAddress, fServerPort);
end;
//Connection was successful, but we still need mkIndexOnServer to be able to do anything
procedure TKMNetworking.ConnectSucceed(Sender:TObject);
begin
//This is currently unused, the player does not need to see this message
//PostLocalMessage('Connection successful');
end;
procedure TKMNetworking.ConnectFailed(const aText: string);
begin
fNetClient.Disconnect;
OnJoinFail(aText);
end;
//Send message that we have deliberately disconnected
procedure TKMNetworking.AnnounceDisconnect(aLastSentCmdsTick: Cardinal = LAST_SENT_COMMANDS_TICK_NONE);
begin
// gLog.AddTime('AnnounceDisconnect. LastSentCmdsTick = %d', [aLastSentCmdsTick]);
// Tell everyone when we quit
//@Rey: It should not make a big difference, but in here we send Cardinal, yet when we handle it we treat it as Integer
PacketSendC(NET_ADDRESS_OTHERS, mkDisconnect, aLastSentCmdsTick); //Send our last sent commands tick
end;
procedure TKMNetworking.Disconnect;
begin
fIgnorePings := 0;
fReconnectRequested := 0; //Cancel any reconnection that was requested
fEnteringPassword := False;
fReturnedToLobby := False;
SetGameState(lgsNone);
OnJoinSucc := nil;
OnJoinFail := nil;
OnJoinAssignedHost := nil;
OnHostFail := nil;
OnTextMessage := nil;
OnPlayersSetup := nil;
OnUpdateMinimap := nil;
OnMapName := nil;
OnMapMissing := nil;
OnCommands := nil;
OnResyncFromTick := nil;
OnDisconnect := nil;
OnPingInfo := nil;
OnReassignedHost := nil;
OnReassignedJoiner := nil;
fWelcomeMessage := '';
fNetRoom.Clear;
fMutedPlayersList.Clear;
fNetGameOptions.Reset;
fNetClient.Disconnect;
fNetServer.Stop;
FreeAndNil(fMapInfo);
FreeAndNil(fSaveInfo);
FreeAndNil(fFileReceiver);
AbortAllTransfers;
fSelectGameKind := ngkNone;
end;
procedure TKMNetworking.DropPlayers(aPlayers: TKMByteArray);
var
I: Integer;
serverIndex: TKMNetHandleIndex;
begin
Assert(IsHost, 'Only the host is allowed to drop players');
for I := Low(aPlayers) to High(aPlayers) do
begin
if fNetRoom[aPlayers[I]].Dropped then
begin
//Reset players LastSentCommandsTick, so we are not going to wait its last commands anymore... Let's hope for the best...
fNetRoom[aPlayers[I]].LastSentCommandsTick := LAST_SENT_COMMANDS_TICK_NONE;
end
else
begin
serverIndex := fNetRoom[aPlayers[I]].IndexOnServer;
//Make sure this player is properly disconnected from the server
PacketSendInd(NET_ADDRESS_SERVER, mkKickPlayer, serverIndex);
fNetRoom.DropPlayer(serverIndex);
end;
PostMessage(TX_NET_DROPPED, csLeave, fNetRoom[aPlayers[I]].NicknameColoredU);
end;
SendPlayerListAndRefreshPlayersSetup;
//Player being dropped may cause vote to end
if (fNetGameState in [lgsLoading, lgsGame]) and (fNetRoom.FurtherVotesNeededForMajority <= 0) then
ReturnToLobbyVoteSucceeded;
end;
procedure TKMNetworking.ForcedDisconnect(Sender: TObject);
begin
OnDisconnect(gResTexts[TX_NET_SERVER_STOPPED]);
end;
function TKMNetworking.Connected: Boolean;
begin
Result := fNetClient.Connected;
end;
procedure TKMNetworking.HandleMessagePingFpsInfo(aStream: TKMemoryStream);
var
I: Integer;
pingCount: Integer;
playerHandle: TKMNetHandleIndex;
pingValue, fpsValue: Word;
slotIndex: Integer;
begin
if fIgnorePings > 0 then
begin
Dec(fIgnorePings);
Exit;
end;
if fIgnorePings <> 0 then Exit; //-1 means ignore all pings
aStream.Read(pingCount);
for I := 1 to pingCount do
begin
// Read first
aStream.Read(playerHandle);
aStream.Read(pingValue);
aStream.Read(fpsValue);
// Process second
// This player might not be in the lobby yet, could still be asking to join. If so we do not care about their ping
slotIndex := fNetRoom.ServerToLocal(playerHandle);
if slotIndex <> -1 then
begin
fNetRoom[slotIndex].PerformanceAddPing(pingValue);
if slotIndex <> fMySlotIndex then // our own FPS was set immediately after measurement, without delay.
fNetRoom[slotIndex].PerformanceAddFps(fpsValue);
end;
end;
if Assigned(OnPingInfo) then
OnPingInfo;
end;
procedure TKMNetworking.SendMapOrSave(aRecipient: TKMNetHandleIndex = NET_ADDRESS_OTHERS);
var
M: TKMemoryStream;
begin
M := TKMemoryStreamBinary.Create;
case fSelectGameKind of
ngkSave: begin
M.WriteW(fSaveInfo.FileName);
M.Write(fSaveInfo.CRC);
PacketSendS(aRecipient, mkSaveSelect, M);
end;
ngkMap: begin
M.WriteW(fMapInfo.FileNameWithoutHash);
M.Write(fMapInfo.CRC);
PacketSendS(aRecipient, mkMapSelect, M);
end;
else PacketSend(aRecipient, mkResetMap);
end;
M.Free;
end;
procedure TKMNetworking.MatchPlayersToSave(aPlayerID: Integer = -1);
var
I, K: Integer;
begin
Assert(IsHost, 'Only host can match players');
Assert(fSelectGameKind = ngkSave, 'Not a save');
if aPlayerID = -1 then
begin
//If we are matching all then reset them all first so we don't get clashes
for I := 1 to fNetRoom.Count do
if not fNetRoom[I].IsSpectator then
fNetRoom[I].StartLocation := LOC_RANDOM;
for I := 1 to MAX_LOBBY_PLAYERS - fSaveInfo.GameInfo.HumanCount - fNetRoom.GetClosedCount
- Max(fNetRoom.GetSpectatorCount - MAX_LOBBY_SPECTATORS, 0) do
//First 2 spectators don't count towards MAX_LOBBY_PLAYERS (separate section), but additional ones do
if fNetRoom.Count - Min(fNetRoom.GetSpectatorCount, MAX_LOBBY_SPECTATORS) < MAX_LOBBY_PLAYERS then
fNetRoom.AddClosedPlayer; //Close unused slots
end;
//Match players based on their nicknames
for I := 1 to fNetRoom.Count do
for K := 1 to fSaveInfo.GameInfo.PlayerCount do
if fSaveInfo.GameInfo.Enabled[K-1]
and ((I = aPlayerID) or (aPlayerID = -1)) //-1 means update all players
and fNetRoom.LocAvailable(K)
and fNetRoom[I].IsHuman
and not fNetRoom[I].IsSpectator
and (fNetRoom[I].Nickname = fSaveInfo.GameInfo.OwnerNickname[K-1]) then
begin
fNetRoom[I].StartLocation := K;
Break;
end;
end;
procedure TKMNetworking.AbortAllTransfers;
begin
fFileSenderManager.AbortAllTransfers; //Any ongoing transfer is cancelled
if Assigned(OnAbortAllTransfers) then
OnAbortAllTransfers;
end;
//Clear selection from any map/save
procedure TKMNetworking.SelectNoMap(const aErrorMessage: UnicodeString);
begin
Assert(IsHost, 'Only host can reset map');
fSelectGameKind := ngkNone;
FreeAndNil(fMapInfo);
FreeAndNil(fSaveInfo);
AbortAllTransfers;
PacketSend(NET_ADDRESS_OTHERS, mkResetMap);
fNetRoom.ResetLocAndReady; //Reset start locations
MyRoomSlot.ReadyToStart := True;
MyRoomSlot.HasMapOrSave := True;
if Assigned(OnMapName) then
OnMapName(aErrorMessage);
SendPlayerListAndRefreshPlayersSetup;
end;
//Tell other players which map we will be using
//Players will reset their starting locations and "Ready" status on their own
procedure TKMNetworking.SelectMap(const aName: UnicodeString; aMapKind: TKMMapKind; aSendPlayerSetup: Boolean = False);
begin
Assert(IsHost, 'Only host can select maps');
FreeAndNil(fMapInfo);
FreeAndNil(fSaveInfo);
//Strict scanning to force CRC recalculation
fMapInfo := TKMMapInfo.Create(aName, True, aMapKind);
if not fMapInfo.IsValid then
begin
SelectNoMap(gResTexts[TX_NET_ERR_MAP_INVALID]);
PostLocalMessage(gResTexts[TX_NET_ERR_MAP_INVALID_MSG], csSystem);
Exit;
end;
if (aMapKind = mkDL) and not fMapInfo.IsFilenameEndMatchHash then
begin
SelectNoMap(gResTexts[TX_NET_ERR_DL_MAP_FILE_CHANGED]);
PostLocalMessage(gResTexts[TX_NET_ERR_DL_MAP_FILE_CHANGED_MSG], csSystem);
Exit;
end;
fMapInfo.LoadExtra; //Lobby requires extra map info such as CanBeHuman
fNetRoom.ResetLocAndReady; //Reset start locations
fSelectGameKind := ngkMap;
MyRoomSlot.ReadyToStart := True;
MyRoomSlot.HasMapOrSave := True;
AbortAllTransfers; //Any ongoing transfer is cancelled
SendMapOrSave;
if Assigned(OnMapName) then
OnMapName(fMapInfo.Name);
//Don't send player setup by default. Cause it will be sent from lobby just afterwards
if aSendPlayerSetup then
SendPlayerListAndRefreshPlayersSetup;
end;
//Tell other players which save we will be using
//Players will reset their starting locations and "Ready" status on their own
procedure TKMNetworking.SelectSave(const aName: UnicodeString);
var
error: UnicodeString;
begin
Assert(IsHost, 'Only host can select saves');
FreeAndNil(fMapInfo);
FreeAndNil(fSaveInfo);
fSaveInfo := TKMSaveInfo.Create(aName, True);
if not fSaveInfo.IsValid then
begin
error := WrapColor(fSaveInfo.SaveError.ErrorString, clSaveLoadError); //Make a copy since fSaveInfo is freed in SelectNoMap
SelectNoMap(error); //State the error, e.g. wrong version
Exit;
end;
fNetRoom.ResetLocAndReady; //Reset start locations
NetGameOptions.Peacetime := fSaveInfo.GameOptions.Peacetime;
NetGameOptions.SpeedPT := fSaveInfo.GameOptions.SpeedPT;
NetGameOptions.SpeedAfterPT := fSaveInfo.GameOptions.SpeedAfterPT;
SendGameOptions;
if Assigned(OnGameOptions) then OnGameOptions;
fSelectGameKind := ngkSave;
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname); // host's index can change when players are removed
fHostSlotIndex := fMySlotIndex; //@Rey not sure about intention of this assignment here
// Set ReadyToStart and HasMapOrSave with updated fMySlotIndex
MyRoomSlot.ReadyToStart := True;
MyRoomSlot.HasMapOrSave := True;
//Randomise locations within team is disabled for saves
fNetRoom.RandomizeTeamLocations := False;
AbortAllTransfers; //Any ongoing transfer is cancelled
SendMapOrSave;
MatchPlayersToSave; //Don't match players if it's not a valid save
if Assigned(OnMapName) then
OnMapName(fSaveInfo.FileName);
SendPlayerListAndRefreshPlayersSetup;
end;
// Tell other players which Hand (start position) we would like to use
// Each players choice should be unique
procedure TKMNetworking.SelectHand(aSlotIndex: Integer; aHandIndex: Integer);
var
slotIndex: Integer;
begin
//Check if position can be taken before doing anything
if not CanTakeLocation(aSlotIndex, aHandIndex, IsHost and fNetRoom.HostDoesSetup) then
begin
// Early fail, without disturbing the Host
if Assigned(OnPlayersSetup) then
OnPlayersSetup;
Exit;
end;
// If someone else has this HandIndex, switch them (only when HostDoesSetup)
if IsHost and fNetRoom.HostDoesSetup and (aHandIndex <> LOC_RANDOM) and (aHandIndex <> LOC_SPECTATE) then
begin
slotIndex := fNetRoom.StartingLocToLocal(aHandIndex);
if slotIndex <> -1 then
begin
fNetRoom[slotIndex].StartLocation := fNetRoom[aSlotIndex].StartLocation;
//Spectators can't have team
if fNetRoom[slotIndex].StartLocation = LOC_SPECTATE then
fNetRoom[slotIndex].Team := 0;
//If host pushes player to a different loc, the player should be set to not ready (they must agree to change)
if (slotIndex <> fMySlotIndex) and not fNetRoom[slotIndex].IsComputer then
fNetRoom[slotIndex].ReadyToStart := False;
end;
end;
case fNetPlayerKind of
lpkHost: begin
//Host makes rules, Joiner will get confirmation from Host
fNetRoom[aSlotIndex].StartLocation := aHandIndex; //Use aSlotIndex not fMySlotIndex because it could be an AI
//If host pushes player to a different loc, the player should be set to not ready (they must agree to change)
if (aSlotIndex <> fMySlotIndex) and not fNetRoom[aSlotIndex].IsComputer then
fNetRoom[aSlotIndex].ReadyToStart := False;
if aHandIndex = LOC_SPECTATE then
fNetRoom[aSlotIndex].Team := 0; // Spectators can't have team
// Update minimap
if (aSlotIndex = fMySlotIndex) and Assigned(OnUpdateMinimap) then
OnUpdateMinimap;
SendPlayerListAndRefreshPlayersSetup;
end;
lpkJoiner: PacketSendI(NET_ADDRESS_HOST, mkRequestHand, aHandIndex);
end;
end;
// Tell other players which team we want to be on
// Use aSlotIndex not fMySlotIndex because it could be an AI
procedure TKMNetworking.SelectTeam(aSlotIndex: Integer; aTeam: Integer);
begin
fNetRoom[aSlotIndex].Team := aTeam;
case fNetPlayerKind of
lpkHost: begin
// If Host pushes player to a different team, the player should be set to not ready (they must agree to change)
if (aSlotIndex <> fMySlotIndex) and not fNetRoom[aSlotIndex].IsComputer then
fNetRoom[aSlotIndex].ReadyToStart := False;
SendPlayerListAndRefreshPlayersSetup;
end;
lpkJoiner: PacketSendI(NET_ADDRESS_HOST, mkRequestTeam, aTeam);
end;
end;
//Tell other players which color we will be using
procedure TKMNetworking.SelectColor(aSlotIndex: Integer; aColor: Cardinal);
begin
if not fNetRoom.ColorAvailable(aColor) then Exit;
if (fSelectGameKind = ngkSave)
and SaveInfo.IsValid
and SaveInfo.GameInfo.ColorUsed(aColor) then Exit;
//Host makes rules, Joiner will get confirmation from Host
fNetRoom[aSlotIndex].FlagColor := aColor; //Use aSlotIndex not fMySlotIndex because it could be an AI
case fNetPlayerKind of
lpkHost: SendPlayerListAndRefreshPlayersSetup;
lpkJoiner: begin
PacketSendC(NET_ADDRESS_HOST, mkRequestFlagColor, aColor);
if Assigned(OnPlayersSetup) then OnPlayersSetup;
end;
end;
end;
procedure TKMNetworking.KickPlayer(aSlotIndex: Integer);
begin
Assert(IsHost, 'Only host is allowed to kick players out');
//No need to play a sound, server will do that when it announces that player disconnected
PostMessage(TX_NET_KICKED, csNone, fNetRoom[aSlotIndex].NicknameColoredU);
PacketSendInd(NET_ADDRESS_SERVER, mkKickPlayer, fNetRoom[aSlotIndex].IndexOnServer);
end;
procedure TKMNetworking.BanPlayer(aSlotIndex: Integer);
begin
Assert(IsHost, 'Only host is allowed to ban players');
//No need to play a sound, server will do that when it announces that player disconnected
PostMessage(TX_NET_BANNED, csNone, fNetRoom[aSlotIndex].NicknameColoredU);
PacketSendInd(NET_ADDRESS_SERVER, mkBanPlayer, fNetRoom[aSlotIndex].IndexOnServer);
end;
procedure TKMNetworking.SetToHost(aSlotIndex: Integer);
begin
Assert(IsHost, 'Only host is allowed to promote players');
//Don't allow host reassigning if the server is running within this client (if host quits server stops)
if fNetServer.IsListening then
PostLocalMessage(gResTexts[TX_NET_PROMOTE_LOCAL_SERVER], csSystem)
else
PacketSendInd(NET_ADDRESS_SERVER, mkGiveHost, fNetRoom[aSlotIndex].IndexOnServer);
end;
procedure TKMNetworking.ResetBans;
begin
PacketSend(NET_ADDRESS_SERVER, mkResetBans);
PostMessage(TX_NET_BANS_RESET, csSystem);
end;
procedure TKMNetworking.SendPassword(const aPassword: AnsiString);
var
M: TKMemoryStream;
begin
M := TKMemoryStreamBinary.Create;
M.Write(fRoomToJoin);
M.Write(TKMGameRevision(GAME_REVISION_NUM));
M.WriteA(aPassword);
PacketSendS(NET_ADDRESS_SERVER, mkPassword, M);
M.Free;
fEnteringPassword := False;
fJoinTimeout := TimeGet; //Wait another X seconds for host to reply before timing out
end;
procedure TKMNetworking.SetPassword(const aPassword: AnsiString);
begin
Assert(IsHost, 'Only host can set password');
fPassword := aPassword;
OnMPGameInfoChanged; //Send the password state to the server so it is shown in server list
PacketSendA(NET_ADDRESS_SERVER, mkSetPassword, fPassword); //Send to server
PacketSendA(NET_ADDRESS_OTHERS, mkSetPassword, fPassword); //Send to other players as well, just to let them know we have password here
if Assigned(OnSetPassword) then
OnSetPassword(fPassword);
end;
// Joiner indicates that he is ready to start
function TKMNetworking.ReadyToStart: Boolean;
begin
if (fSelectGameKind = ngkSave) and (MyRoomSlot.StartLocation = LOC_RANDOM) then
begin
PostLocalMessage(gResTexts[TX_LOBBY_ERROR_SELECT_PLAYER], csSystem);
Exit(False);
end;
if ((fSelectGameKind = ngkMap) and fMapInfo.IsValid) or
((fSelectGameKind = ngkSave) and fSaveInfo.IsValid) or
MyRoomSlot.IsSpectator then //Spectators can be ready without map selected
begin
//Toggle it
PacketSend(NET_ADDRESS_HOST, mkReadyToStart);
Result := not MyRoomSlot.ReadyToStart;
end
else
begin
PostLocalMessage(gResTexts[TX_LOBBY_ERROR_NO_MAP], csSystem);
Result := False;
end;
end;
function TKMNetworking.CanStart: TKMGameStartMode;
function BoolToGameStartMode(aAllowed: Boolean): TKMGameStartMode;
begin
if not aAllowed then
begin
//Check if we can't start unsupported save
if (fSelectGameKind = ngkSave)
and fSaveInfo.IsValid
and not fSaveInfo.IsValidStrictly then
Result := gsmNoStartWithWarn
else
Result := gsmNoStart;
end
else
begin
if (fSelectGameKind = ngkSave)
and not fSaveInfo.IsValidStrictly then //Save detected as non-strict valid already at that point
Result := gsmStartWithWarn
else
Result := gsmStart;
end;
end;
var
I: Integer;
startAllowed: Boolean;
begin
case fSelectGameKind of
ngkMap: startAllowed := fNetRoom.AllReady and fMapInfo.IsValid;
ngkSave: begin
startAllowed := fNetRoom.AllReady and fSaveInfo.IsValid;
for I := 1 to fNetRoom.Count do //In saves everyone must chose a location
startAllowed := startAllowed and ((fNetRoom[i].StartLocation <> LOC_RANDOM) or fNetRoom[i].IsClosed);
end;
else startAllowed := False;
end;
//At least one player must NOT be a spectator or closed
for I := 1 to fNetRoom.Count do
if not fNetRoom[i].IsSpectator and not fNetRoom[i].IsClosed then
Exit(BoolToGameStartMode(startAllowed)); //Exit with result from above
//If we reached here then all players are spectators so only saves can be started,
//unless this map has AI-only locations (spectators can watch the AIs)
if (fSelectGameKind = ngkMap) and (fMapInfo.AIOnlyLocCount = 0) then
startAllowed := False;
Result := BoolToGameStartMode(startAllowed);
end;
//Tell other players we want to start
procedure TKMNetworking.StartClick;
var
humanUsableLocs, aiUsableLocs, advancedAIUsableLocs: TKMHandIDArray;
errorMessage: UnicodeString;
M: TKMemoryStream;
checkMapInfo: TKMMapInfo;
fixedLocsColors: TKMCardinalArray;
begin
Assert(IsHost, 'Only host can start the game');
Assert(IsGameStartAllowed(CanStart), 'Can''t start the game now');
Assert(fNetGameState = lgsLobby, 'Can only start from lobby');
SetLength(fixedLocsColors, 0);
//Define random parameters (start locations and flag colors)
//This will also remove odd players from the List, they will lose Host in few seconds
case fSelectGameKind of
ngkMap: begin
humanUsableLocs := fMapInfo.HumanUsableLocs;
aiUsableLocs := fMapInfo.AIUsableLocs;
advancedAIUsableLocs := fMapInfo.AdvancedAIUsableLocs;
fixedLocsColors := fMapInfo.FixedLocsColors;
//Check that map's hash hasn't changed
checkMapInfo := TKMMapInfo.Create(fMapInfo.Name, True, fMapInfo.Kind);
try
if checkMapInfo.CRC <> fMapInfo.CRC then
begin
PostLocalMessage(Format(gResTexts[TX_LOBBY_CANNOT_START], [gResTexts[TX_NET_ERR_MAP_FILE_CHANGED]]), csSystem);
Exit;
end;
finally
checkMapInfo.Free;
end;
end;
ngkSave: begin
humanUsableLocs := fSaveInfo.GameInfo.HumanUsableLocs;
//AIs may replace humans
aiUsableLocs := fSaveInfo.GameInfo.HumanUsableLocs;
advancedAIUsableLocs := fSaveInfo.GameInfo.HumanUsableLocs;
fixedLocsColors := fSaveInfo.GameInfo.FixedLocsColors;
end;
else begin
SetLength(humanUsableLocs, 0);
SetLength(aiUsableLocs, 0);
end;
end;
if not fNetRoom.ValidateSetup(humanUsableLocs, aiUsableLocs, advancedAIUsableLocs, fixedLocsColors, errorMessage) then
begin
PostLocalMessage(Format(gResTexts[TX_LOBBY_CANNOT_START], [errorMessage]), csSystem);
Exit;
end;
fNetRoom.ResetReadyToPlay; //Nobody is ready to play
//ValidateSetup removes closed players if successful, so our index changes
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname);
fHostSlotIndex := fMySlotIndex;
//Init random seed for all the players
fNetGameOptions.RandomSeed := RandomRange(1, 2147483646);
//Let everyone start with final version of fNetRoom and fNetGameOptions
SendGameOptions;
M := TKMemoryStreamBinary.Create;
M.Write(fHostSlotIndex);
fNetRoom.SaveToStream(M);
PacketSendS(NET_ADDRESS_OTHERS, mkStart, M);
M.Free;
StartGame;
end;
procedure TKMNetworking.SendPlayerListAndRefreshPlayersSetup(aPlayerIndex: TKMNetHandleIndex = NET_ADDRESS_OTHERS);
var
I: Integer;
M: TKMemoryStream;
aiOnlyColors: TKMCardinalArray;
begin
Assert(IsHost, 'Only host can send player list');
//In saves we should load team and color from the SaveInfo
if (fNetGameState = lgsLobby) and (fSelectGameKind = ngkSave) then
for I := 1 to fNetRoom.Count do
if (fNetRoom[I].StartLocation <> LOC_RANDOM) and (fNetRoom[I].StartLocation <> LOC_SPECTATE) then
begin
fNetRoom[I].FlagColor := fSaveInfo.GameInfo.Color[fNetRoom[I].HandIndex];
fNetRoom[I].Team := fSaveInfo.GameInfo.Team[fNetRoom[I].HandIndex];
end
else
begin
fNetRoom[I].Team := 0;
//Spectators may still change their color, but may not use one from the save
if fNetRoom[I].IsColorSet
and SaveInfo.GameInfo.ColorUsed(fNetRoom[I].FlagColor) then
fNetRoom[I].FlagColor := 0;
end;
// Map change could casue, that some colors are not valid anymore
// Reset those colors then
if (fNetGameState = lgsLobby) and (fSelectGameKind = ngkMap) then
begin
aiOnlyColors := MapInfo.AIOnlyLocsColors; // save it locally to avoid multiple calculations
for I := 1 to fNetRoom.Count do
begin
if fNetRoom[I].IsColorSet
and IsColorCloseToColors(fNetRoom[I].FlagColor, aiOnlyColors, MIN_PLAYER_COLOR_DIST) then
fNetRoom[I].ResetColor;
end;
end;
// The host's index can change when players are removed
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname);
fHostSlotIndex := fMySlotIndex;
OnMPGameInfoChanged; //Tell the server about the changes
M := TKMemoryStreamBinary.Create;
M.Write(fHostSlotIndex);
fNetRoom.SaveToStream(M);
PacketSendS(aPlayerIndex, mkPlayersList, M);
M.Free;
if Assigned(OnPlayersSetup) then OnPlayersSetup;
end;
procedure TKMNetworking.UpdateGameOptions(aPeacetime: Word; aSpeedPT, aSpeedAfterPT: Single; aDifficulty: TKMMissionDifficulty);
begin
fNetGameOptions.Peacetime := aPeacetime;
fNetGameOptions.SpeedPT := aSpeedPT;
fNetGameOptions.SpeedAfterPT := aSpeedAfterPT;
fNetGameOptions.MissionDifficulty := aDifficulty;
fNetRoom.ResetReady;
MyRoomSlot.ReadyToStart := True;
SendGameOptions;
SendPlayerListAndRefreshPlayersSetup;
end;
procedure TKMNetworking.SendGameOptions;
var
M: TKMemoryStream;
begin
Assert(IsHost, 'Only host can send game options');
M := TKMemoryStreamBinary.Create;
fNetGameOptions.Save(M);
PacketSendS(NET_ADDRESS_OTHERS, mkGameOptions, M);
M.Free;
end;
procedure TKMNetworking.RequestFileTransfer;
begin
if fFileReceiver = nil then
case fMissingBundleType of
ngkMap: begin
fFileReceiver := TKMFileReceiver.Create(kttMap, fMissingBundleName, fMissingBundleCRC);
PacketSendW(NET_ADDRESS_HOST, mkFileRequest, fMissingBundleName);
end;
ngkSave: begin
fFileReceiver := TKMFileReceiver.Create(kttSave, fMissingBundleName);
PacketSendW(NET_ADDRESS_HOST, mkFileRequest, fMissingBundleName);
end;
end;
end;
procedure TKMNetworking.VoteReturnToLobby;
begin
//Even if we are the host we still send our vote through the network, that's simpler
PacketSend(NET_ADDRESS_HOST, mkVote);
end;
//We route the message through Server to ensure everyone sees messages in the same order
//with exact same timestamps (possibly added by the Server?)
procedure TKMNetworking.PostChat(const aText: UnicodeString; aMode: TKMChatMode; aRecipientServerIndex: TKMNetHandleIndex = NET_ADDRESS_OTHERS);
var
I: Integer;
M: TKMemoryStream;
begin
//Sending chat during reconnections at best causes messages to be lost and at worst causes crashes due to intermediate connecting states
if IsReconnecting then
Exit; //Fallback in case UI check fails
M := TKMemoryStreamBinary.Create;
M.Write(aMode, SizeOf(aMode));
M.Write(aRecipientServerIndex);
M.WriteW(aText);
case aMode of
cmTeam:
if MyRoomSlot.Team = 0 then
PacketSendS(fMyIndexOnServer, mkTextChat, M) //Send to self only if we have no team
else
for I := 1 to fNetRoom.Count do
if (fNetRoom[I].Team = MyRoomSlot.Team) and fNetRoom[I].IsHuman and (fNetRoom[I].IndexOnServer <> -1) then
PacketSendS(fNetRoom[I].IndexOnServer, mkTextChat, M); //Send to each player on team (includes self)
cmSpectators:
for I := 1 to fNetRoom.Count do
if fNetRoom[I].IsSpectator and fNetRoom[I].IsHuman and (fNetRoom[I].IndexOnServer <> -1) then
PacketSendS(fNetRoom[I].IndexOnServer, mkTextChat, M); //Send to each spectator (includes self)
cmWhisper:
begin
PacketSendS(aRecipientServerIndex, mkTextChat, M); //Send to specific player
PacketSendS(fMyIndexOnServer, mkTextChat, M); //Send to self as well so the player sees it
end;
cmAll:
PacketSendS(NET_ADDRESS_ALL, mkTextChat, M); //Send to all;
end;
M.Free;
end;
procedure TKMNetworking.PostMessage(aTextID: Integer; aSound: TKMChatSound; const aArgument1: UnicodeString = '';
const aArgument2: UnicodeString = ''; aRecipient: TKMNetHandleIndex = NET_ADDRESS_ALL; aArgumentInt: Integer = -1);
var
M: TKMemoryStream;
begin
M := TKMemoryStreamBinary.Create;
M.Write(aTextID);
M.Write(aSound, SizeOf(aSound));
M.WriteW(aArgument1);
M.WriteW(aArgument2);
M.Write(aArgumentInt);
PacketSendS(aRecipient, mkTextTranslated, M);
M.Free;
end;
procedure TKMNetworking.PostLocalMessage(const aText: UnicodeString; aSound: TKMChatSound = csNone);
const
CHAT_SOUND: array [TKMChatSound] of TSoundFXNew = (
sfxnMPChatSystem, // csNone
sfxnMPChatSystem, // csJoin
sfxnMPChatSystem, // csLeave
sfxnMPChatSystem, // csSystem
sfxnMPChatSystem, // csGameStart
sfxnMPChatSystem, // csSaveGame
sfxnMPChatMessage,// csChat
sfxnMPChatTeam, // csChatTeam
sfxnMPChatTeam // csChatWhisper
);
begin
if Assigned(OnTextMessage) then
begin
if (gSystem <> nil) and (gGameApp <> nil) and (gGameSettings <> nil) then
if gGameSettings.FlashOnMessage then
gSystem.FlashingStart;
OnTextMessage(aText);
if aSound <> csNone then
gSoundPlayer.Play(CHAT_SOUND[aSound]);
end;
end;
//Send our commands to either to all players, or to specified one
procedure TKMNetworking.SendCommands(aStream: TKMemoryStream; aSlotIndex: ShortInt = -1);
begin
if aSlotIndex = -1 then
PacketSendS(NET_ADDRESS_OTHERS, mkCommands, aStream)
else
PacketSendS(fNetRoom[aSlotIndex].IndexOnServer, mkCommands, aStream);
end;
procedure TKMNetworking.AttemptReconnection;
begin
if fReconnectRequested = 0 then
fReconnectRequested := TimeGet; //Do it soon
end;
procedure TKMNetworking.DoReconnection;
var
tempMyIndex: Integer;
begin
gLog.LogNetConnection(Format('DoReconnection: %s',[fMyNickname]));
fReconnectRequested := 0;
PostLocalMessage(gResTexts[TX_NET_RECONNECTING], csSystem);
//Stop the previous connection without calling Self.Disconnect as that frees everything
fNetClient.Disconnect;
tempMyIndex := fMySlotIndex;
Join(fServerAddress,fServerPort,fMyNickname,fRoomToJoin, True); //Join the same server/room as before in reconnecting mode
fMySlotIndex := tempMyIndex; //Join overwrites it, but we must remember it
end;
procedure TKMNetworking.HandleMessageAskToJoin(aServerIndex: Integer; aStream: TKMemoryStream);
var
intErrorText: Integer;
strPlayerName: AnsiString;
begin
if IsHost then
begin
if not TKMNetSecurity.ValidateSolution(aStream, aServerIndex) then
intErrorText := TX_NET_YOUR_DATA_FILES
else
begin
aStream.ReadA(strPlayerName);
intErrorText := fNetRoom.CheckCanJoin(strPlayerName, aServerIndex);
if (intErrorText = -1) and (fNetGameState <> lgsLobby) then
intErrorText := TX_NET_GAME_IN_PROGRESS;
end;
if intErrorText = -1 then
begin
// Password was checked by server already
fNetRoom.AddPlayer(strPlayerName, aServerIndex, '');
PacketSend(aServerIndex, mkAllowToJoin);
PacketSendA(aServerIndex, mkSetPassword, fPassword); //Send joiner password, just to tell him
SendMapOrSave(aServerIndex); //Send the map first so it doesn't override starting locs
if fSelectGameKind = ngkSave then
MatchPlayersToSave(fNetRoom.ServerToLocal(aServerIndex)); //Match only this player
SendPlayerListAndRefreshPlayersSetup;
SendGameOptions;
PostMessage(TX_NET_HAS_JOINED, csJoin, UnicodeString(strPlayerName));
end
else
begin
PacketSendI(aServerIndex, mkRefuseToJoin, intErrorText);
// Force them to reconnect and ask for a new challenge
PacketSendInd(NET_ADDRESS_SERVER, mkKickPlayer, aServerIndex);
end;
end;
end;
// Check if player (not spectator) is not defeated and not win
function TKMNetworking.IsPlayerHandStillInGame(aSlotIndex: Integer): Boolean;
begin
Result := (fNetGameState = lgsGame) and (fNetRoom[aSlotIndex].HandIndex <> -1)
and (gHands[fNetRoom[aSlotIndex].HandIndex].AI.IsNotWinnerNotLoser) // This means player is not defeated and not win
and not fNetRoom[aSlotIndex].IsSpectator
end;
// Handle mkReassignHost message
procedure TKMNetworking.HandleMessageReassignHost(aSenderIndex: TKMNetHandleIndex; aStream: TKMemoryStream);
var
newHostIndex, oldHostIndex: TKMNetHandleIndex;
passwordA: AnsiString;
descriptionW: UnicodeString;
begin
aStream.Read(newHostIndex);
if fFileReceiver <> nil then
begin
FreeAndNil(fFileReceiver); //Transfer is aborted if host disconnects/changes
//Reset, otherwise it will freeze in "downloading" state
if Assigned(OnMapMissing) then OnMapMissing('', False); //Set empty str as error msg for now
end;
if IsHost then
begin
//We are no longer the host
AbortAllTransfers;
fNetPlayerKind := lpkJoiner;
if Assigned(OnReassignedJoiner) then OnReassignedJoiner; //Lobby/game might need to know
if Assigned(OnPlayersSetup) then OnPlayersSetup;
end;
if newHostIndex = fMyIndexOnServer then
begin
// We are now the Host!
fNetPlayerKind := lpkHost;
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname);
oldHostIndex := fHostSlotIndex;
// Lobby/game might need to know that we are now hosting
if Assigned(OnReassignedHost) then
OnReassignedHost;
case fNetGameState of
lgsLobby: begin
if InRange(fHostSlotIndex, 1, fNetRoom.Count) then
fNetRoom[fHostSlotIndex].ReadyToStart := False; //Old host is not ready anymore
MyRoomSlot.ReadyToStart := True; //The host is always ready
fNetRoom.SetAIReady; //Set all AI players to ready
SendGameOptions; //Only needs to be sent when in the lobby. Our version becomes standard.
end;
lgsLoading: begin
if Assigned(OnReadyToPlay) then OnReadyToPlay;
TryPlayGame;
end;
end;
fHostSlotIndex := MySlotIndex; //Set it down here as it is used above
//Server tells us the password and description in this packet,
//so they aren't reset when the host is changed
aStream.ReadA(passwordA);
aStream.ReadW(descriptionW);
fPassword := passwordA;
fDescription := descriptionW;
OnMPGameInfoChanged;
if (fSelectGameKind = ngkNone)
or ((fSelectGameKind = ngkMap) and not MapInfo.IsValid)
or ((fSelectGameKind = ngkSave) and not SaveInfo.IsValid) then
SelectNoMap(''); //In case the previous host had the map and we don't
SendPlayerListAndRefreshPlayersSetup;
//If host was dropped already, that mean we have to defeat him, because he intentionally quits the game
//(dropped was set on his mkDisconnect message)
if fNetRoom[oldHostIndex].Dropped
and IsPlayerHandStillInGame(oldHostIndex)
and (fNetRoom[oldHostIndex].HandIndex <> -1)
and Assigned(OnJoinerDropped) then
OnJoinerDropped(fNetRoom[oldHostIndex].HandIndex);
PostMessage(TX_NET_HOSTING_RIGHTS, csSystem, fNetRoom[fMySlotIndex].NicknameColoredU);
gLog.LogNetConnection('Hosting rights reassigned to us ('+UnicodeString(fMyNickname)+')');
end;
end;
// Handle mkPLayerList message
procedure TKMNetworking.HandleMessagePlayersList(aStream: TKMemoryStream);
var
oldLoc: Integer;
isPlayerInitBefore: Boolean;
begin
if fNetPlayerKind = lpkJoiner then
begin
oldLoc := -1234; // some randor value, make compiler happy
isPlayerInitBefore := MySlotIndex > 0;
if isPlayerInitBefore then
oldLoc := MyRoomSlot.StartLocation;
aStream.Read(fHostSlotIndex);
fNetRoom.LoadFromStream(aStream);
// Our index could have changed on players add/removal
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname);
if Assigned(OnPlayersSetup) then OnPlayersSetup;
if Assigned(OnUpdateMinimap)
and ((isPlayerInitBefore
and (oldLoc <> MyRoomSlot.StartLocation))
or not isPlayerInitBefore) then
OnUpdateMinimap;
end;
end;
// Handle mkDisconnect message
procedure TKMNetworking.HandleMessagePlayerDisconnected(aSenderIndex: TKMNetHandleIndex; aLastSentCommandsTick: Integer);
//Post local message about player disconnection
procedure PostPlayerDisconnectedMsg(aSlotIndex: Integer);
var
quitMsgId: Integer;
begin
if IsPlayerHandStillInGame(aSlotIndex) then
quitMsgId := IfThen(fHostSlotIndex = aSlotIndex, TX_MULTIPLAYER_HOST_DISCONNECTED_DEFEATED, TX_NET_HAS_QUIT_AND_DEFEATED)
else
quitMsgId := IfThen(fHostSlotIndex = aSlotIndex, TX_MULTIPLAYER_HOST_DISCONNECTED, TX_NET_HAS_QUIT);
PostLocalMessage(Format(gResTexts[quitMsgId], [fNetRoom[aSlotIndex].NicknameColoredU]), csLeave);
end;
var
slotIndex: Integer;
begin
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
gLog.AddTime('PlayerDisconnected [netPlayer %d [%s] HandID %d]. LastSentCommandsTick = %d',
[slotIndex, fNetRoom[slotIndex].Nickname, fNetRoom[slotIndex].HandIndex, aLastSentCommandsTick]);
case fNetPlayerKind of
lpkHost: begin
fFileSenderManager.ClientDisconnected(aSenderIndex);
if slotIndex = -1 then Exit; //Has already disconnected
PostPlayerDisconnectedMsg(slotIndex);
if fNetGameState in [lgsGame] then
begin
if IsPlayerHandStillInGame(slotIndex) and Assigned(OnJoinerDropped) then
OnJoinerDropped(fNetRoom[slotIndex].HandIndex);
end;
if fNetGameState in [lgsLoading, lgsGame] then
fNetRoom.DropPlayer(aSenderIndex, aLastSentCommandsTick)
else
fNetRoom.RemServerPlayer(aSenderIndex);
SendPlayerListAndRefreshPlayersSetup;
//Player leaving may cause vote to end
if (fNetGameState in [lgsLoading, lgsGame])
and (fNetRoom.FurtherVotesNeededForMajority <= 0) then
ReturnToLobbyVoteSucceeded;
end;
lpkJoiner: begin
if slotIndex = -1 then exit; //Has already disconnected
PostPlayerDisconnectedMsg(slotIndex);
if fHostSlotIndex = slotIndex then
begin
//Host has quit so drop them from the game
if fNetGameState in [lgsLoading, lgsGame] then
fNetRoom.DropPlayer(aSenderIndex, aLastSentCommandsTick)
else
fNetRoom.RemServerPlayer(aSenderIndex);
end;
end;
end;
end;
function TKMNetworking.CalculateGameCRC: Cardinal;
begin
//CRC checks are done on the data we already loaded, not the files on HDD which can change.
Result := gRes.GetDATCRC;
//For debugging/testing it's useful to skip EXE check sometimes (but data files should always be checked)
if not SKIP_EXE_CRC then
Result := Result xor Adler32CRC(ParamStr(0));
end;
function TKMNetworking.CanTakeLocation(aSlotIndex, aLoc: Integer; AllowSwapping: Boolean): Boolean;
begin
Result := True;
if (aLoc <> LOC_SPECTATE) and (aLoc <> LOC_RANDOM) then
case fSelectGameKind of
ngkMap: Result := (fMapInfo <> nil) and fMapInfo.IsValid and (aLoc <= fMapInfo.LocCount);
ngkSave: Result := (fSaveInfo <> nil) and fSaveInfo.IsValid and (aLoc <= fSaveInfo.GameInfo.PlayerCount);
ngkNone: Result := False;
end;
//If we are currently a spectator wanting to be a non-spectator, make sure there is a slot for us
if fNetRoom[aSlotIndex].IsSpectator and (aLoc <> LOC_SPECTATE) then
Result := Result and (fNetRoom.Count-fNetRoom.GetSpectatorCount < MAX_LOBBY_PLAYERS);
//Can't be a spectator if they are disabled
if (aLoc = LOC_SPECTATE) and not fNetRoom.SpectatorsAllowed then
Result := False;
//If we are trying to be a spectator and aren't one already, make sure there is an open spectator slot
if (aLoc = LOC_SPECTATE) and not fNetRoom[aSlotIndex].IsSpectator then
Result := Result and ((fNetRoom.SpectatorSlotsOpen = MAX_LOBBY_SPECTATORS) //Means infinite spectators allowed
or (fNetRoom.SpectatorSlotsOpen-fNetRoom.GetSpectatorCount > 0));
//Check with Room that the location isn't taken already, unless it's our current location
//Host may be allowed to swap when HostDoesSetup is set, meaning it doesn't matter if loc is taken
if (aLoc <> fNetRoom[aSlotIndex].StartLocation) and not AllowSwapping then
Result := Result and fNetRoom.LocAvailable(aLoc);
end;
procedure TKMNetworking.GameCreated;
begin
case fNetPlayerKind of
lpkHost: begin
MyRoomSlot.ReadyToPlay := True;
PacketSend(NET_ADDRESS_OTHERS, mkReadyToPlay);
SendPlayerListAndRefreshPlayersSetup; //Initialise the in-game player setup
//Check this here because it is possible to start a multiplayer game without other humans, just AI (at least for debugging)
TryPlayGame;
end;
lpkJoiner: begin
MyRoomSlot.ReadyToPlay := True;
PacketSend(NET_ADDRESS_OTHERS, mkReadyToPlay);
end;
end;
end;
//Get printable name of network address
function TKMNetworking.GetNetAddressPrintDescr(aNetworkAddress: Integer): String;
function GetNetPlayerDescr: String;
var
slotIndex: Integer;
begin
slotIndex := fNetRoom.ServerToLocal(aNetworkAddress);
if slotIndex = -1 then
Result := 'unknown'
else
Result := Format('%d | %s', [slotIndex, fNetRoom[slotIndex].Nickname]);
end;
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 := Format('Client %d [NetPlayer %s]', [aNetworkAddress, GetNetPlayerDescr]);
end;
end;
procedure TKMNetworking.LogPacket(aIsSending: Boolean; aKind: TKMNetMessageKind; aNetworkAddress: TKMNetHandleIndex);
const
LOGGED_PACKET_KINDS: set of TKMNetMessageKind = [mkReadyToPlay,
mkPlay,
mkReconnectionAccepted,
mkResyncFromTick,
mkClientReconnected,
mkRefuseReconnect,
mkAskToReconnect,
mkAskToSendCrashreport,
mkStart,
mkGiveHost,
mkKicked,
mkDisconnect,
mkVote,
mkReadyToReturnToLobby];
var
logMessage: String;
begin
if aIsSending then
Inc(fPacketsSent[aKind])
else
Inc(fPacketsReceived[aKind]);
if aIsSending then
logMessage := 'Packet send: %-23s to %s' // 23 is the length of mk_ command with the longest name
else
logMessage := 'Packet received: %-23s from %s';
logMessage := Format(logMessage, [GetEnumName(TypeInfo(TKMNetMessageKind), Integer(aKind)),
GetNetAddressPrintDescr(aNetworkAddress)]);
if aKind in LOGGED_PACKET_KINDS then
gLog.AddTime(logMessage)
else
case aKind of
mkPing, mkPong,
mkPingFpsInfo, mkFPS: gLog.LogNetPacketPingFps(logMessage);
mkCommands: gLog.LogNetPacketCommand(logMessage);
else
gLog.LogNetPacketOther(logMessage);
end;
end;
procedure TKMNetworking.PostLogMessageToChat(const aLogMessage: UnicodeString);
begin
if Self = nil then Exit;
if SHOW_LOG_IN_CHAT then
PostLocalMessage(DeleteDoubleSpaces(aLogMessage), csNone);
end;
procedure TKMNetworking.PacketRecieve(aNetClient: TKMNetClient; aSenderIndex: TKMNetHandleIndex; aData: Pointer; aLength: Cardinal);
var
dataStream: TKMemoryStream;
messageKind: TKMNetMessageKind;
err: UnicodeString;
begin
Assert(aLength >= SizeOf(messageKind), 'Unexpectedly short message');
if not Connected then Exit;
dataStream := TKMemoryStreamBinary.Create;
try
dataStream.WriteBuffer(aData^, aLength);
dataStream.Position := 0;
dataStream.Read(messageKind, SizeOf(messageKind));
// Make sure we are allowed to receive this packet at this point
if not (messageKind in NET_ALLOWED_PACKETS_SET[fNetGameState]) then
begin
//When querying or reconnecting to a host we may receive data such as commands, player setup, etc. These should be ignored.
if not (fNetGameState in [lgsQuery, lgsReconnecting]) then
begin
err := 'Received a packet not intended for this state (' +
GetEnumName(TypeInfo(TKMNetGameState), Integer(fNetGameState)) + '): ' +
GetEnumName(TypeInfo(TKMNetMessageKind), Integer(messageKind));
//These warnings sometimes happen when returning to lobby, log them but don't show user
gLog.AddTime(err);
//PostLocalMessage('Error: ' + err, csSystem);
end;
Exit;
end;
LogPacket(False, messageKind, aSenderIndex);
HandleMessage(messageKind, dataStream, aSenderIndex);
finally
dataStream.Free;
end;
end;
procedure TKMNetworking.HandleMessage(aMessageKind: TKMNetMessageKind; aStream: TKMemoryStream; aSenderIndex: TKMNetHandleIndex);
var
I, locID, teamID, slotIndex: Integer;
M2: TKMemoryStream;
tmpInteger, tmpInteger2: Integer;
tmpHandleIndex: TKMNetHandleIndex;
tmpCardinal, tmpCardinal2: Cardinal;
tmpStringA: AnsiString;
tmpStringW, replyStringW: UnicodeString;
tmpChatMode: TKMChatMode;
chatSound: TKMChatSound;
begin
case aMessageKind of
mkNetProtocolVersion:
begin
aStream.ReadA(tmpStringA);
if tmpStringA <> NET_PROTOCOL_REVISON then
begin
Assert(not IsHost);
OnJoinFail(Format(gResTexts[TX_MP_MENU_WRONG_VERSION], [NET_PROTOCOL_REVISON, tmpStringA]));
fNetClient.Disconnect;
end;
end;
mkWelcomeMessage:
begin
aStream.ReadW(tmpStringW);
fWelcomeMessage := tmpStringW;
end;
mkServerName:
begin
aStream.ReadA(tmpStringA);
fServerName := TKMNetworkUtils.GetEscapedNewLineServerNameA(tmpStringA);
end;
mkIndexOnServer:
begin
aStream.Read(tmpHandleIndex);
fMyIndexOnServer := tmpHandleIndex;
//PostLocalMessage('Index on Server - ' + inttostr(fMyIndexOnServer));
// Now we can try to join the room we planned to
M2 := TKMemoryStreamBinary.Create;
M2.Write(fRoomToJoin);
M2.Write(TKMGameRevision(GAME_REVISION_NUM));
PacketSendS(NET_ADDRESS_SERVER, mkJoinRoom, M2);
M2.Free;
end;
mkConnectedToRoom:
begin
aStream.Read(tmpHandleIndex); //Host's index
fNetGameFilter.Load(aStream);
//See if the server assigned hosting rights to us
if tmpHandleIndex = fMyIndexOnServer then
begin
fNetPlayerKind := lpkHost;
// Enter the lobby if we had hosting rights assigned to us
if Assigned(OnJoinAssignedHost) then
OnJoinAssignedHost;
PostLocalMessage(gResTexts[TX_LOBBY_HOST_RIGHTS], csNone);
end;
//We are now clear to proceed with our business
if fNetGameState = lgsReconnecting then
begin
if IsHost then
begin
gLog.LogNetConnection('Hosting reconnection');
//The other players must have been disconnected too, so we will be the host now
SetGameState(lgsGame); //We are now in control of the game, so we are no longer reconnecting
//At this point we now know that every other client was dropped, but we probably missed the disconnect messages
fNetRoom.DisconnectAllClients(fMyNickname); //Mark all human players as disconnected, except for self
//Set our new index on server
fNetRoom[fNetRoom.NicknameToLocal(fMyNickname)].SetIndexOnServer := fMyIndexOnServer;
end
else
begin
PacketSendA(NET_ADDRESS_HOST, mkAskToReconnect, fMyNickname);
fJoinTimeout := TimeGet; //Wait another X seconds for host to reply before timing out
gLog.LogNetConnection('Asking to reconnect');
end;
end
else
case fNetPlayerKind of
lpkHost:
begin
fNetRoom.AddPlayer(fMyNickname, fMyIndexOnServer, gResLocales.UserLocale, LOBBY_HOST_AS_SPECTATOR);
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname);
fHostSlotIndex := fMySlotIndex;
MyRoomSlot.ReadyToStart := True;
MyRoomSlot.HasMapOrSave := True;
if Assigned(OnPlayersSetup) then OnPlayersSetup;
SetGameState(lgsLobby);
gSoundPlayer.Play(sfxnMPChatSystem); //Sound for joining the lobby
if fWelcomeMessage <> '' then PostLocalMessage(fWelcomeMessage, csNone);
end;
lpkJoiner:
begin
SetGameState(lgsQuery);
fJoinTimeout := TimeGet; //Wait another X seconds for host to reply before timing out
M2 := TKMemoryStreamBinary.Create;
TKMNetSecurity.GenerateChallenge(M2, tmpHandleIndex);
PacketSendS(NET_ADDRESS_HOST, mkAskForAuth, M2);
M2.Free;
end;
end;
end;
mkAskToReconnect:
begin
aStream.ReadA(tmpStringA);
slotIndex := fNetRoom.NicknameToLocal(tmpStringA);
tmpInteger := fNetRoom.CheckCanReconnect(slotIndex);
if tmpInteger = -1 {Success} then
begin
gLog.LogNetConnection(UnicodeString(tmpStringA) + ' successfully reconnected');
fNetRoom[slotIndex].SetIndexOnServer := aSenderIndex; //They will have a new index
fNetRoom[slotIndex].Connected := True; //This player is now back online
SendPlayerListAndRefreshPlayersSetup;
PacketSend(aSenderIndex, mkReconnectionAccepted); //Tell this client they are back in the game
PacketSendInd(NET_ADDRESS_OTHERS, mkClientReconnected, aSenderIndex); //Tell everyone to ask him to resync
PacketSendI(aSenderIndex, mkResyncFromTick, Integer(fLastProcessedTick)); //Ask him to resync us
PostMessage(TX_NET_HAS_RECONNECTED, csJoin, fNetRoom[slotIndex].NicknameColoredU);
end
else
begin
gLog.LogNetConnection(UnicodeString(tmpStringA) + ' asked to reconnect: ' + IntToStr(tmpInteger));
PacketSendI(aSenderIndex, mkRefuseReconnect, tmpInteger);
end;
end;
mkRefuseReconnect:
begin
aStream.Read(tmpInteger);
//If the result is < 1 is means silently ignore and keep retrying
if tmpInteger > 0 then
PostLocalMessage(Format(gResTexts[TX_NET_RECONNECTION_FAILED], [gResTexts[tmpInteger]]), csSystem);
if Assigned(OnJoinFail) then
OnJoinFail('');
end;
mkAskToJoin:
HandleMessageAskToJoin(aSenderIndex, aStream);
mkFileRequest:
HandleMessageFileRequest(aSenderIndex, aStream);
mkFileChunk:
if not IsHost and (fFileReceiver <> nil) then
begin
if SLOW_MAP_SAVE_LOAD then
Sleep(50);
fFileReceiver.DataReceived(aStream);
PacketSend(aSenderIndex, mkFileAck);
M2 := TKMemoryStreamBinary.Create;
M2.Write(fFileReceiver.TotalSize);
M2.Write(fFileReceiver.ReceivedSize);
PacketSendS(NET_ADDRESS_OTHERS, mkFileProgress, M2);
M2.Free;
if Assigned(OnFileTransferProgress) then
OnFileTransferProgress(fFileReceiver.TotalSize, fFileReceiver.ReceivedSize);
end;
mkFileAck:
if IsHost then
fFileSenderManager.AckReceived(aSenderIndex);
mkFileEnd:
if not IsHost and (fFileReceiver <> nil) then
begin
fFileReceiver.ProcessTransfer;
FreeAndNil(fFileReceiver);
SetDownloadlInProgress(aSenderIndex, False);
end;
mkFileProgress:
if Assigned(OnPlayerFileTransferProgress) then
begin
aStream.Read(tmpCardinal);
aStream.Read(tmpCardinal2);
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if (slotIndex <> -1) and fNetRoom[slotIndex].DownloadInProgress then
OnPlayerFileTransferProgress(slotIndex, tmpCardinal, tmpCardinal2);
end;
mkFileSendStarted:
if not IsHost then //Host will mark Room before file send
begin
//@Rey: Types in here were quite mismatched
aStream.Read(tmpInteger);
SetDownloadlInProgress(tmpInteger, True);
end;
mkLangCode:
begin
aStream.ReadA(tmpStringA);
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if slotIndex <> -1 then
fNetRoom[slotIndex].LangCode := tmpStringA;
SendPlayerListAndRefreshPlayersSetup;
end;
mkAllowToJoin:
if fNetPlayerKind = lpkJoiner then
begin
OnJoinSucc; // Enter lobby
SetGameState(lgsLobby);
//No need to play a sound here, host will send "<player> has joined" message
if fWelcomeMessage <> '' then PostLocalMessage(fWelcomeMessage, csNone);
PacketSendA(NET_ADDRESS_HOST, mkLangCode, gResLocales.UserLocale);
end;
mkRefuseToJoin:
if fNetPlayerKind = lpkJoiner then
begin
aStream.Read(tmpInteger);
fNetClient.Disconnect;
OnJoinFail(gResTexts[tmpInteger]);
end;
mkReqPassword:
begin
fEnteringPassword := True; //Disables timing out
OnJoinPassword;
end;
mkAskForAuth:
if IsHost then
begin
//We should refuse the joiner immediately if we are not in the lobby
if fNetGameState <> lgsLobby then
PacketSendI(aSenderIndex, mkRefuseToJoin, TX_NET_GAME_IN_PROGRESS)
else
begin
//Solve joiner's challenge
M2 := TKMemoryStreamBinary(TKMNetSecurity.SolveChallenge(aStream, aSenderIndex));
//Send our own challenge
TKMNetSecurity.GenerateChallenge(M2, aSenderIndex);
PacketSendS(aSenderIndex, mkAuthChallenge, M2);
M2.Free;
end;
end;
mkAuthChallenge:
begin
//Validate solution the host sent back to us
if TKMNetSecurity.ValidateSolution(aStream, aSenderIndex) then
begin
//Solve host's challenge and ask to join
M2 := TKMemoryStreamBinary(TKMNetSecurity.SolveChallenge(aStream, aSenderIndex));
M2.WriteA(fMyNickname);
PacketSendS(NET_ADDRESS_HOST, mkAskToJoin, M2);
M2.Free;
end
else
OnJoinFail(gResTexts[TX_NET_YOUR_DATA_FILES]);
end;
mkKicked:
begin
aStream.Read(tmpInteger);
OnDisconnect(gResTexts[tmpInteger]);
end;
mkClientLost:
begin
aStream.Read(tmpHandleIndex);
if IsHost then
begin
fFileSenderManager.ClientDisconnected(tmpHandleIndex);
slotIndex := fNetRoom.ServerToLocal(tmpHandleIndex);
if slotIndex = -1 then exit; //Has already disconnected or not from our room
if not fNetRoom[slotIndex].Dropped then
begin
PostMessage(TX_NET_LOST_CONNECTION, csLeave, fNetRoom[slotIndex].NicknameColoredU);
gLog.LogNetConnection(fNetRoom[slotIndex].NicknameU + ' lost connection');
end;
if fNetGameState = lgsGame then
fNetRoom.DisconnectPlayer(tmpHandleIndex)
else
if fNetGameState = lgsLoading then
begin
fNetRoom.DropPlayer(tmpHandleIndex);
TryPlayGame;
end
else
fNetRoom.RemServerPlayer(tmpHandleIndex);
SendPlayerListAndRefreshPlayersSetup;
end
else
if fNetRoom.ServerToLocal(tmpHandleIndex) <> -1 then
begin
if fNetGameState = lgsGame then
fNetRoom.DisconnectPlayer(tmpHandleIndex)
else
if fNetGameState = lgsLoading then
fNetRoom.DropPlayer(tmpHandleIndex)
else
fNetRoom.RemServerPlayer(tmpHandleIndex); //Remove the player anyway as it might be the host that was lost
end;
end;
mkDisconnect:
begin
aStream.Read(tmpInteger);
HandleMessagePlayerDisconnected(aSenderIndex, tmpInteger);
end;
mkReassignHost:
HandleMessageReassignHost(aSenderIndex, aStream);
mkPing: PacketSend(aSenderIndex, mkPong);
mkPingFpsInfo:
HandleMessagePingFpsInfo(aStream);
// mkFPS: Moved to server in 2017
mkPlayersList:
HandleMessagePlayersList(aStream);
mkGameOptions:
if fNetPlayerKind = lpkJoiner then
begin
fNetGameOptions.Load(aStream);
if Assigned(OnGameOptions) then OnGameOptions;
end;
mkResetMap:
begin
FreeAndNil(fFileReceiver); //Any ongoing transfer is cancelled
fSelectGameKind := ngkNone;
FreeAndNil(fMapInfo);
FreeAndNil(fSaveInfo);
if Assigned(OnMapName) then OnMapName('');
end;
mkMapSelect:
if fNetPlayerKind = lpkJoiner then
begin
FreeAndNil(fFileReceiver); //Any ongoing transfer is cancelled
aStream.ReadW(tmpStringW); //Map name
aStream.Read(tmpCardinal); //CRC
//Try to load map from MP or DL folder
FreeAndNil(fMapInfo);
fMapInfo := TKMMapInfo.Create(tmpStringW, True, mkMP);
if not fMapInfo.IsValid or (fMapInfo.CRC <> tmpCardinal) then
begin
//Append CRC to map name
tmpStringW := tmpStringW + '_' + IntToHex(Integer(tmpCardinal), 8);
fMapInfo := TKMMapInfo.Create(tmpStringW, True, mkDL);
if not fMapInfo.IsValid or (fMapInfo.CRC <> tmpCardinal) then
FreeAndNil(fMapInfo);
end;
if fMapInfo <> nil then
begin
fSelectGameKind := ngkMap;
fMapInfo.LoadExtra; //Lobby requires extra map info such as CanBeHuman
if Assigned(OnMapName) then OnMapName(fMapInfo.Name);
PacketSend(NET_ADDRESS_HOST, mkHasMapOrSave);
end
else
begin
fMissingBundleType := ngkMap;
fMissingBundleName := tmpStringW;
fMissingBundleCRC := tmpCardinal;
fSelectGameKind := ngkNone;
if Assigned(OnMapName) then OnMapName(tmpStringW);
if Assigned(OnMapMissing) then OnMapMissing(tmpStringW, False);
end;
if Assigned(OnPlayersSetup) then OnPlayersSetup;
end;
mkSaveSelect:
if fNetPlayerKind = lpkJoiner then
begin
FreeAndNil(fFileReceiver); //Any ongoing transfer is cancelled
aStream.ReadW(tmpStringW); //Save name
aStream.Read(tmpCardinal); //CRC
//See if we already have the save file the host selected
FreeAndNil(fSaveInfo);
fSaveInfo := TKMSaveInfo.Create(tmpStringW, True);
gLog.AddTime('mk_SaveSelect: fSaveInfo.CRC = %d, tmpCRC = %d', [fSaveInfo.CRC, tmpCardinal]);
if not fSaveInfo.IsValid or (fSaveInfo.CRC <> tmpCardinal) then
begin
if fReturnedToLobby and (tmpStringW = RETURN_TO_LOBBY_SAVE) then
begin
//Host paused file doesn't match ours, host may be cheating!
PostLocalMessage(gResTexts[TX_PAUSED_FILE_MISMATCH], csSystem);
gLog.AddTime('Save error: %s. Check params: fSaveInfo.IsValid = %s; (fSaveInfo.CRC <> tmpCRC) = ;' +
' Save FileExists %s: %s; fSaveError = %s; fInfo.IsValid(True) = %s',
[gResTexts[TX_PAUSED_FILE_MISMATCH], BoolToStr(fSaveInfo.IsValid),
BoolToStr(fSaveInfo.CRC <> tmpCardinal), fSaveInfo.Path + fSaveInfo.FileName + EXT_SAVE_MAIN_DOT,
BoolToStr(FileExists(fSaveInfo.Path + fSaveInfo.FileName + EXT_SAVE_MAIN_DOT)),
fSaveInfo.SaveError.ErrorString, fSaveInfo.GameInfo.IsValid(True)]);
fSelectGameKind := ngkNone;
FreeAndNil(fSaveInfo);
if Assigned(OnMapName) then OnMapName('');
Exit;
end;
//See if the host selected the same save we already downloaded
FreeAndNil(fSaveInfo);
fSaveInfo := TKMSaveInfo.Create(DOWNLOADED_LOBBY_SAVE, True);
end;
if fSaveInfo.IsValid and (fSaveInfo.CRC = tmpCardinal) then
begin
fSelectGameKind := ngkSave;
if Assigned(OnMapName) then OnMapName(tmpStringW);
if Assigned(OnPlayersSetup) then OnPlayersSetup;
PacketSend(NET_ADDRESS_HOST, mkHasMapOrSave);
end
else
begin
FreeAndNil(fSaveInfo);
fSelectGameKind := ngkNone;
//Save file does not exist, so downloaded it
fMissingBundleType := ngkSave;
fMissingBundleName := tmpStringW;
if Assigned(OnMapMissing) then OnMapMissing(tmpStringW, False);
end;
end;
mkRequestHand:
if IsHost and not fNetRoom.HostDoesSetup then
begin
aStream.Read(tmpInteger);
locID := tmpInteger;
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if CanTakeLocation(slotIndex, locID, False) then
begin //Update Players setup
fNetRoom[slotIndex].StartLocation := locID;
//Spectators can't have team
if locID = LOC_SPECTATE then
fNetRoom[slotIndex].Team := 0;
SendPlayerListAndRefreshPlayersSetup;
end
else
// Ignore the request and send out actual setup
SendPlayerListAndRefreshPlayersSetup(aSenderIndex);
end;
mkRequestTeam:
if IsHost and not fNetRoom.HostDoesSetup then
begin
aStream.Read(tmpInteger);
teamID := tmpInteger;
fNetRoom[fNetRoom.ServerToLocal(aSenderIndex)].Team := teamID;
SendPlayerListAndRefreshPlayersSetup;
end;
mkRequestFlagColor:
if IsHost then
begin
aStream.Read(tmpCardinal);
// Availability could have changed since the joiner sent this request (over slow connection)
if fNetRoom.ColorAvailable(tmpCardinal)
and ((fSelectGameKind <> ngkSave) or not SaveInfo.IsValid or not SaveInfo.GameInfo.ColorUsed(tmpCardinal)) then
begin
fNetRoom[fNetRoom.ServerToLocal(aSenderIndex)].FlagColor := tmpCardinal;
SendPlayerListAndRefreshPlayersSetup;
end
else
// Ignore the request and send out actual setup
SendPlayerListAndRefreshPlayersSetup(aSenderIndex);
end;
mkReadyToStart:
if IsHost then
begin
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
fNetRoom[slotIndex].ReadyToStart := not fNetRoom[slotIndex].ReadyToStart;
SendPlayerListAndRefreshPlayersSetup;
end;
mkHasMapOrSave:
if IsHost then
begin
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
fNetRoom[slotIndex].HasMapOrSave := True;
SendPlayerListAndRefreshPlayersSetup;
end;
mkStart:
if fNetPlayerKind = lpkJoiner then
begin
aStream.Read(fHostSlotIndex);
fNetRoom.LoadFromStream(aStream);
fMySlotIndex := fNetRoom.NicknameToLocal(fMyNickname);
StartGame;
end;
mkSetPassword:
if not IsHost then //Save password for joiner's only, as it's used to show Lock image
begin
aStream.ReadA(tmpStringA); //Password
fPassword := tmpStringA;
if Assigned(OnSetPassword) then
OnSetPassword(fPassword);
end;
mkReadyToReturnToLobby:
begin
fNetRoom[fNetRoom.ServerToLocal(aSenderIndex)].ReadyToReturnToLobby := True;
if fNetRoom.AllReadyToReturnToLobby then
begin
ResetReturnToLobbyVote; //So it's reset for next time
OnDoReturnToLobby;
end;
end;
mkReadyToPlay:
begin
fNetRoom[fNetRoom.ServerToLocal(aSenderIndex)].ReadyToPlay := True;
if Assigned(OnReadyToPlay) then OnReadyToPlay;
if IsHost then TryPlayGame;
end;
mkPlay:
if fNetPlayerKind = lpkJoiner then
PlayGame;
mkCommands:
begin
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if (slotIndex<>-1) and not fNetRoom[slotIndex].Dropped then
if Assigned(OnCommands) then OnCommands(aStream, slotIndex);
end;
mkAskToSendCrashreport:
begin
// We were asked to send crashreport. Raise new Exception then
aStream.ReadW(tmpStringW);
gLog.AddTime('Received mkAskToSendCrashreport with player error msg: ' + tmpStringW);
raise Exception.Create(Format(gResTexts[TX_ERROR_ASK_TO_SEND_RNGCHECK_REPORT], [#13#10#13#10, tmpStringW]));
end;
mkResyncFromTick:
begin
aStream.Read(tmpInteger);
gLog.LogNetConnection('Asked to resync from tick ' + IntToStr(tmpInteger));
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if Assigned(OnResyncFromTick) and (slotIndex<>-1) then
begin
gLog.LogNetConnection('Resyncing player ' + fNetRoom[slotIndex].NicknameU);
OnResyncFromTick(slotIndex, Cardinal(tmpInteger));
end;
end;
mkReconnectionAccepted:
begin
//The host has accepted us back into the game!
gLog.LogNetConnection('Reconnection Accepted');
SetGameState(lgsGame); //Game is now running once again
fReconnectRequested := 0; //Cancel any retry in progress
//Request all other clients to resync us
PacketSendI(NET_ADDRESS_OTHERS, mkResyncFromTick, Integer(fLastProcessedTick));
end;
mkClientReconnected:
begin
aStream.Read(tmpHandleIndex);
//The host has accepted a disconnected client back into the game. Request this client to resync us
if tmpHandleIndex = fMyIndexOnServer then exit;
gLog.LogNetConnection('Requesting resync for reconnected client');
PacketSendI(tmpHandleIndex, mkResyncFromTick, Integer(fLastProcessedTick));
end;
mkVote:
begin
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if not fVoteReturnToLobbySucceeded // Do not allow late mkVote after we received enough votes (if it comes while still in game and receiveing mk_readyToReturnToLobby)
and not fNetRoom[slotIndex].VotedYes //No need to vote more than once
and (fNetRoom.HasOnlySpectators or not fNetRoom[slotIndex].IsSpectator) //spectators don't get to vote unless there's only spectators left
then
begin
fLastVoteTime := TimeGet;
fNetRoom[slotIndex].VotedYes := True;
fNetRoom.VoteActive := True;
if fNetRoom.FurtherVotesNeededForMajority <= 0 then
begin
PostMessage(TX_NET_VOTE_PASSED, csSystem, fNetRoom[slotIndex].NicknameColoredU);
ReturnToLobbyVoteSucceeded;
end
else
begin
PostMessage(TX_NET_VOTED, csSystem, fNetRoom[slotIndex].NicknameColoredU, IntToStr(fNetRoom.FurtherVotesNeededForMajority));
SendPlayerListAndRefreshPlayersSetup;
end;
end;
end;
mkTextTranslated:
begin
aStream.Read(tmpInteger);
aStream.Read(chatSound, SizeOf(chatSound));
aStream.ReadW(tmpStringW);
aStream.ReadW(replyStringW);
aStream.Read(tmpInteger2);
if tmpInteger2 = -1 then
PostLocalMessage(Format(gResTexts[tmpInteger], [tmpStringW, replyStringW]), chatSound)
else
PostLocalMessage(Format(gResTexts[tmpInteger], [gResTexts[tmpInteger2]]), chatSound)
end;
mkTextChat:
begin
aStream.Read(tmpChatMode, SizeOf(tmpChatMode));
aStream.Read(tmpHandleIndex);
aStream.ReadW(tmpStringW);
case tmpChatMode of
cmTeam:
begin
tmpStringW := ' [$66FF66]('+gResTexts[TX_CHAT_TEAM]+')[]: ' + tmpStringW;
chatSound := csChatTeam;
end;
cmSpectators:
begin
tmpStringW := ' [$66FF66]('+gResTexts[TX_CHAT_SPECTATORS]+')[]: ' + tmpStringW;
chatSound := csChatTeam;
end;
cmWhisper:
begin
chatSound := csChatWhisper;
I := fNetRoom.ServerToLocal(tmpHandleIndex);
if I <> -1 then
//we want to show colored nickname, so prepare nickname string
tmpStringA := '[]' + fNetRoom[I].NicknameColored + '[$00B9FF]'
else
tmpStringA := '';
tmpStringW := ' [$00B9FF](' + Format(gResTexts[TX_CHAT_WHISPER_TO], [UnicodeString(tmpStringA)]) + ')[]: ' + tmpStringW;
end;
cmAll:
begin
tmpStringW := ' ('+gResTexts[TX_CHAT_ALL]+'): ' + tmpStringW;
chatSound := csChat;
end;
end;
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if (slotIndex <> -1) then
begin
if not IsMuted(slotIndex) then
begin
if fNetRoom[slotIndex].IsColorSet then
tmpStringW := WrapColor(fNetRoom[slotIndex].NicknameU, FlagColorToTextColor(fNetRoom[slotIndex].FlagColor)) + tmpStringW
else
tmpStringW := fNetRoom[slotIndex].NicknameU + tmpStringW;
PostLocalMessage(tmpStringW, chatSound);
end
else
if tmpChatMode = cmWhisper then
// Notify sender, when he is muted
PostMessage(TX_NET_MUTED, csSystem, MyRoomSlot.NicknameColoredU, '', aSenderIndex);
end;
end;
end;
end;
//MessageKind.Data(depends on Kind)
procedure TKMNetworking.PacketSend(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfNoData);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
procedure TKMNetworking.PacketSendS(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aStream: TKMemoryStream);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfBinary);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
aStream.Position := 0;
M.CopyFrom(aStream, aStream.Size);
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
procedure TKMNetworking.PacketSendC(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aParam: Cardinal);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfNumber);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
M.Write(aParam);
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
procedure TKMNetworking.PacketSendI(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aParam: Integer);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfNumber);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
M.Write(aParam);
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
//procedure TKMNetworking.PacketSend(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; const aParams: array of Integer);
//var
// I: Integer;
// M: TKMemoryStream;
//begin
// Assert(NetPacketType[aKind] = pfBinary); //Several numbers are considered as binary
//
// LogPacket(True, aKind, aRecipient);
//
// M := TKMemoryStreamBinary.Create;
// M.Write(aKind, SizeOf(aKind));
//
// for I := 0 to Length(aParams) - 1 do
// M.Write(aParams[I]);
//
// fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
// M.Free;
//end;
procedure TKMNetworking.PacketSendInd(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; aIndexOnServer: TKMNetHandleIndex);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfNumber);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
M.Write(aIndexOnServer);
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
procedure TKMNetworking.PacketSendA(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; const aText: AnsiString);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfStringA);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
M.WriteA(aText);
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
procedure TKMNetworking.PacketSendW(aRecipient: TKMNetHandleIndex; aKind: TKMNetMessageKind; const aText: UnicodeString);
var
M: TKMemoryStream;
begin
Assert(NetPacketType[aKind] = pfStringW);
LogPacket(True, aKind, aRecipient);
M := TKMemoryStreamBinary.Create;
M.Write(aKind, SizeOf(aKind));
M.WriteW(aText);
fNetClient.SendData(fMyIndexOnServer, aRecipient, M.Memory, M.Size);
M.Free;
end;
procedure TKMNetworking.StartGame;
begin
PostLocalMessage(gResTexts[TX_LOBBY_GAME_STARTED], csGameStart);
SetGameState(lgsLoading); //Loading has begun (no further players allowed to join)
fIgnorePings := -1; //Ignore all pings until we have finished loading
case fSelectGameKind of
ngkMap: OnStartMap(fMapInfo.FileNameWithoutHash, fMapInfo.Kind, fMapInfo.CRC, MyRoomSlot.IsSpectator,
fNetGameOptions.MissionDifficulty);
ngkSave: OnStartSave(fSaveInfo.FileName, MyRoomSlot.IsSpectator);
else
raise Exception.Create('Unexpected fSelectGameKind');
end;
end;
procedure TKMNetworking.TryPlayGame;
begin
if fNetRoom.AllReadyToPlay then
begin
PacketSend(NET_ADDRESS_OTHERS, mkPlay);
PlayGame;
end;
end;
procedure TKMNetworking.PlayGame;
begin
fIgnorePings := 5; //Ignore the next few pings as they will have been measured during loading
SetGameState(lgsGame); //The game has begun (no further players allowed to join)
if Assigned(OnPlay) then OnPlay;
end;
procedure TKMNetworking.SetDescription(const Value: UnicodeString);
begin
Assert(IsHost, 'Only host can set description');
fDescription := Value;
OnMPGameInfoChanged; //Send the description to the server so it is shown in room info
end;
// Return if specified NetPlayer is muted locally
function TKMNetworking.IsMuted(aSlotIndex: Integer): Boolean;
begin
Result := (aSlotIndex <> -1) and (fMutedPlayersList.IndexOf(fNetRoom[aSlotIndex].IndexOnServer) <> -1);
end;
function TKMNetworking.IsSave: Boolean;
begin
Result := SelectGameKind = ngkSave;
end;
function TKMNetworking.IsMap: Boolean;
begin
Result := SelectGameKind = ngkMap;
end;
// Toggle mute status of specified NetPlayer
procedure TKMNetworking.ToggleMuted(aSlotIndex: Integer);
var
listIndex: Integer;
begin
if gLog.IsDegubLogEnabled then
gLog.LogDebug(Format('TKMNetworking.ToggleMuted: IndexOnServer for NetPlayer %d [%s] = %d',
[aSlotIndex, fNetRoom[aSlotIndex].Nickname, fNetRoom[aSlotIndex].IndexOnServer]));
listIndex := fMutedPlayersList.IndexOf(fNetRoom[aSlotIndex].IndexOnServer);
if listIndex <> -1 then
fMutedPlayersList.Delete(listIndex)
else
fMutedPlayersList.Add(fNetRoom[aSlotIndex].IndexOnServer);
end;
procedure TKMNetworking.SetGameState(aState: TKMNetGameState; aOnMPInfoChanged: Boolean = True);
begin
gLog.AddTime('SetGameState from %s to %s', [GetEnumName(TypeInfo(TKMNetGameState), Integer(fNetGameState)),
GetEnumName(TypeInfo(TKMNetGameState), Integer(aState))]);
fNetGameState := aState;
if aOnMPInfoChanged and (fNetGameState in [lgsLobby,lgsLoading,lgsGame]) and IsHost and (fMyIndexOnServer <> -1) then
OnMPGameInfoChanged;
end;
//Tell the server what we know about the game
procedure TKMNetworking.AnnounceGameInfo(aGameTime: TDateTime; const aMap: UnicodeString);
var
I: Integer;
M: TKMemoryStream;
netGameInfo: TKMNetGameInfo;
reportGameTime: TDateTime;
reportMap: UnicodeString;
begin
//Only one player per game should send the info - Host
if not IsHost then Exit;
netGameInfo := TKMNetGameInfo.Create;
try
reportGameTime := aGameTime;
reportMap := aMap;
if (fNetGameState in [lgsLobby, lgsLoading]) then
begin
case fSelectGameKind of
ngkSave: reportMap := fSaveInfo.GameInfo.Title;
ngkMap: reportMap := fMapInfo.Name;
else
reportMap := '';
end;
reportGameTime := -1;
end;
netGameInfo.Description := fDescription;
netGameInfo.Map := reportMap;
netGameInfo.GameTime := reportGameTime;
netGameInfo.GameState := NET_MP_GAME_STATE[fNetGameState];
netGameInfo.PasswordLocked := (fPassword <> '');
netGameInfo.PlayerCount := fNetRoom.Count;
netGameInfo.GameOptions.Peacetime := fNetGameOptions.Peacetime;
netGameInfo.GameOptions.SpeedPT := fNetGameOptions.SpeedPT;
netGameInfo.GameOptions.SpeedAfterPT := fNetGameOptions.SpeedAfterPT;
netGameInfo.GameOptions.RandomSeed := fNetGameOptions.RandomSeed; //not needed, but we send it anyway
netGameInfo.GameOptions.MissionDifficulty := fNetGameOptions.MissionDifficulty;
for I := 1 to fNetRoom.Count do
begin
netGameInfo.Players[I].Name := fNetRoom[I].Nickname;
netGameInfo.Players[I].Color := fNetRoom[I].FlagColorDef;
netGameInfo.Players[I].Connected := fNetRoom[I].Connected;
netGameInfo.Players[I].LangCode := fNetRoom[I].LangCode;
netGameInfo.Players[I].Team := fNetRoom[I].Team;
netGameInfo.Players[I].IsSpectator := fNetRoom[I].IsSpectator;
netGameInfo.Players[I].IsHost := fHostSlotIndex = I;
netGameInfo.Players[I].PlayerType := fNetRoom[I].PlayerNetType;
if (gHands = nil) //Game is not loaded yet...
or netGameInfo.Players[I].IsSpectator
or (fNetRoom[I].HandIndex = -1) then
netGameInfo.Players[I].WonOrLost := wolNone
else
netGameInfo.Players[I].WonOrLost := gHands[fNetRoom[I].HandIndex].AI.WonOrLost;
end;
M := TKMemoryStreamBinary.Create;
netGameInfo.SaveToStream(M);
PacketSendS(NET_ADDRESS_SERVER, mkSetGameInfo, M);
M.Free;
finally
netGameInfo.Free;
end;
end;
procedure TKMNetworking.TransferOnCompleted(aClientIndex: TKMNetHandleIndex);
begin
PacketSend(aClientIndex, mkFileEnd);
SendMapOrSave(aClientIndex);
SendPlayerListAndRefreshPlayersSetup(aClientIndex);
end;
procedure TKMNetworking.TransferOnPacket(aClientIndex: TKMNetHandleIndex; aStream: TKMemoryStream; out aSendBufferEmpty: Boolean);
begin
PacketSendS(aClientIndex, mkFileChunk, aStream);
aSendBufferEmpty := fNetClient.SendBufferEmpty;
end;
procedure TKMNetworking.UpdateState(aGlobalTickCount: cardinal);
begin
// Reconnection delay
if (fReconnectRequested <> 0) and (TimeSince(fReconnectRequested) > RECONNECT_PAUSE) then DoReconnection;
// Joining timeout
if fNetGameState in [lgsConnecting, lgsReconnecting, lgsQuery] then
if (TimeSince(fJoinTimeout) > JOIN_TIMEOUT) and not fEnteringPassword
and (fReconnectRequested = 0) then
if Assigned(OnJoinFail) then
OnJoinFail(gResTexts[TX_NET_QUERY_TIMED_OUT]);
// Vote expiring
if (fNetGameState in [lgsLoading, lgsGame]) and IsHost
and fNetRoom.VoteActive and (TimeSince(fLastVoteTime) > VOTE_TIMEOUT) then
begin
PostMessage(TX_NET_VOTE_FAILED, csSystem);
fNetRoom.ResetVote;
SendPlayerListAndRefreshPlayersSetup;
end;
end;
procedure TKMNetworking.UpdateStateIdle;
begin
fNetServer.UpdateState; //Server measures pings etc.
//LNet requires network update calls unless it is being used as visual components
fNetClient.UpdateStateIdle;
fNetServerPoller.UpdateStateIdle;
fFileSenderManager.UpdateStateIdle(fNetClient.SendBufferEmpty);
end;
procedure TKMNetworking.SendFPSMeasurement(aFPS: Integer);
begin
if Self = nil then Exit;
if fNetGameState = lgsGame then
begin
PacketSendI(NET_ADDRESS_SERVER, mkFPS, aFPS);
// We can set own FPS right away
MyRoomSlot.PerformanceAddFps(aFPS);
end;
end;
procedure TKMNetworking.AnnounceReadyToReturnToLobby;
begin
//Send it to ourselves too, that's simplest
PacketSend(NET_ADDRESS_ALL, mkReadyToReturnToLobby);
end;
procedure TKMNetworking.WakeUpNotReady;
var
I, K: Integer;
begin
K := 0;
for I := 1 to fNetRoom.Count do
begin
if fNetRoom[I].Connected and not fNetRoom[I].ReadyToStart then
begin
PostMessage(TX_LOBBY_ALERT_NOT_READY, csSystem, '', '', fNetRoom[I].IndexOnServer, TX_LOBBY_READY);
Inc(K);
end;
end;
PostLocalMessage(Format(gResTexts[TX_LOBBY_ALERT_GET_READY_SENT], [IntToStr(K)]), csSystem);
end;
// Ask other player to send report. We need reports from both players, who got desync error, to compare the saves and fix the desync
procedure TKMNetworking.AskToSendCrashreport(aSlotIndex: Integer; aErrorStr: UnicodeString);
begin
PacketSendW(fNetRoom[aSlotIndex].IndexOnServer, mkAskToSendCrashreport, aErrorStr);
end;
function TKMNetworking.GetFullServerName: string;
begin
Result := Format('%s #%d %s : %d', [fServerName, fRoomToJoin + 1, fServerAddress, fServerPort]);
end;
function TKMNetworking.GetMapInfo: TKMMapInfo;
begin
if Self = nil then Exit(nil);
Result := fMapInfo;
end;
function TKMNetworking.GetMyRoomSlot: TKMNetRoomSlot;
begin
Result := fNetRoom[fMySlotIndex];
end;
procedure TKMNetworking.SetDownloadlInProgress(aSenderIndex: TKMNetHandleIndex; aValue: Boolean);
var
slotIndex: Integer;
begin
slotIndex := fNetRoom.ServerToLocal(aSenderIndex);
if slotIndex <> -1 then
fNetRoom[slotIndex].DownloadInProgress := aValue;
end;
procedure TKMNetworking.HandleMessageFileRequest(aSenderIndex: TKMNetHandleIndex; aStream: TKMemoryStream);
procedure AbortSend;
begin
PacketSend(aSenderIndex, mkFileEnd); //Abort
SetDownloadlInProgress(aSenderIndex, False);
end;
var
tmpStringW: UnicodeString;
begin
if IsHost then
begin
//Validate request and set up file sender
aStream.ReadW(tmpStringW);
case fSelectGameKind of
ngkMap: if ((tmpStringW = MapInfo.Name) or (tmpStringW = MapInfo.Name + '_' + IntToHex(MapInfo.CRC, 8))) then
begin
PacketSendInd(NET_ADDRESS_OTHERS, mkFileSendStarted, aSenderIndex);
SetDownloadlInProgress(aSenderIndex, True);
if not fFileSenderManager.StartNewSend(kttMap, MapInfo.Name, MapInfo.Kind, aSenderIndex) then
AbortSend;
end
else
AbortSend;
ngkSave:if tmpStringW = SaveInfo.FileName then
begin
PacketSendInd(NET_ADDRESS_OTHERS, mkFileSendStarted, aSenderIndex);
SetDownloadlInProgress(aSenderIndex, True);
if not fFileSenderManager.StartNewSend(kttSave, SaveInfo.FileName, mkDL, aSenderIndex) then
AbortSend;
end
else
AbortSend;
end;
end;
end;
procedure TKMNetworking.ReturnToLobbyVoteSucceeded;
begin
// Don't run NetPlayers.ResetVote here, wait until we actually return to the lobby so the vote can't start again
fNetRoom.ResetReadyToReturnToLobby;
fVoteReturnToLobbySucceeded := True;
SendPlayerListAndRefreshPlayersSetup;
OnAnnounceReturnToLobby; // Sends GIC command to create synchronised save file
end;
procedure TKMNetworking.ResetReturnToLobbyVote;
begin
fVoteReturnToLobbySucceeded := False;
fNetRoom.ResetReadyToReturnToLobby;
end;
procedure TKMNetworking.ReturnToLobby;
begin
//Clear events that were used by Game
OnCommands := nil;
OnResyncFromTick := nil;
OnPlay := nil;
OnReadyToPlay := nil;
OnPlayersSetup := nil;
SetGameState(lgsLobby, False);
fReturnedToLobby := True; //Expect pause.sav to match host
if IsHost then
begin
fNetRoom.RemAllAIs; //AIs are included automatically when you start the save
fNetRoom.RemDisconnectedPlayers; //Disconnected players must not be shown in lobby
fNetRoom.ResetVote; //Only reset the vote now that the game has exited
//Don't refresh player setup here since events aren't attached to lobby yet
end;
end;
function TKMNetworking.GetPacketsReceived(aKind: TKMNetMessageKind): Cardinal;
begin
Result := fPacketsReceived[aKind];
end;
function TKMNetworking.GetPacketsSent(aKind: TKMNetMessageKind): Cardinal;
begin
Result := fPacketsSent[aKind];
end;
procedure TKMNetworking.ResetPacketsStats;
var
mKind: TKMNetMessageKind;
begin
for mKind := Low(TKMNetMessageKind) to High(TKMNetMessageKind) do
begin
fPacketsReceived[mKind] := 0;
fPacketsSent[mKind] := 0;
end;
fPacketsStatsStartTime := TimeGet;
end;
end.