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; // 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.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 " 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.