4116 lines
120 KiB
Ucode
4116 lines
120 KiB
Ucode
//=============================================================================
|
|
// GameInfo.
|
|
//
|
|
// The GameInfo defines the game being played: the game rules, scoring, what actors
|
|
// are allowed to exist in this game type, and who may enter the game. While the
|
|
// GameInfo class is the public interface, much of this functionality is delegated
|
|
// to several classes to allow easy modification of specific game components. These
|
|
// classes include GameInfo, AccessControl, Mutator, and BroadcastHandler.
|
|
// A GameInfo actor is instantiated when the level is initialized for gameplay (in
|
|
// C++ UGameEngine::LoadMap() ). The class of this GameInfo actor is determined by
|
|
// (in order) either the URL ?game=xxx, or the
|
|
// DefaultGame entry in the game's .ini file (in the Engine.Engine section), unless
|
|
// its a network game in which case the DefaultServerGame entry is used.
|
|
// The GameType used can be overridden in the GameInfo script event SetGameType(), called
|
|
// on the game class picked by the above process.
|
|
//
|
|
// Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
|
|
//=============================================================================
|
|
class GameInfo extends Info
|
|
config(Game)
|
|
native
|
|
dependson(OnlineSubsystem);
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Variables.
|
|
|
|
var bool bRestartLevel; // Level should be restarted when player dies
|
|
var bool bPauseable; // Whether the game is pauseable.
|
|
var bool bTeamGame; // This is a team game.
|
|
var bool bGameEnded; // set when game ends
|
|
var bool bOverTime;
|
|
var bool bDelayedStart;
|
|
var bool bWaitingToStartMatch;
|
|
var globalconfig bool bChangeLevels;
|
|
var bool bAlreadyChanged;
|
|
var globalconfig bool bAdminCanPause;
|
|
var bool bGameRestarted;
|
|
var bool bLevelChange; // level transition in progress
|
|
var globalconfig bool bKickLiveIdlers; // if true, even playercontrollers with pawns can be kicked for idling
|
|
|
|
/** Whether this match is going to use arbitration or not */
|
|
var bool bUsingArbitration;
|
|
|
|
/**
|
|
* Whether the arbitrated handshaking has occurred or not.
|
|
* NOTE: The code will reject new connections once handshaking has started
|
|
*/
|
|
var bool bHasArbitratedHandshakeBegun;
|
|
|
|
/** Whether the arbitrated handshaking has occurred or not. */
|
|
var bool bNeedsEndGameHandshake;
|
|
|
|
/** Whether the arbitrated handshaking has completed or not. */
|
|
var bool bIsEndGameHandshakeComplete;
|
|
|
|
/** Used to indicate when an arbitrated match has started its end sequence */
|
|
var bool bHasEndGameHandshakeBegun;
|
|
|
|
/** Whether the game expects a fixed player start for profiling. */
|
|
var bool bFixedPlayerStart;
|
|
|
|
/** The causeevent= string that the game passed in This is separate from automatedPerfTesting which is going to probably spawn bots / effects **/
|
|
var string CauseEventCommand;
|
|
|
|
/** This is the BugIt String Data. Other info should be stored here **/
|
|
/** Currently stores the location string form **/
|
|
var string BugLocString;
|
|
|
|
/** Currently stores the rotation in string form **/
|
|
var string BugRotString;
|
|
|
|
/**
|
|
* List of player controllers we're awaiting handshakes with
|
|
*
|
|
* NOTE: Any PC in this list that does not complete the handshake within
|
|
* ArbitrationHandshakeTimeout will be kicked from the match
|
|
*/
|
|
var array<PlayerController> PendingArbitrationPCs;
|
|
|
|
/**
|
|
* Holds the list of players that passed handshaking and require finalization
|
|
* of arbitration data written to the online subsystem
|
|
*/
|
|
var array<PlayerController> ArbitrationPCs;
|
|
|
|
/** Amount of time a client can take for arbitration handshaking before being kicked */
|
|
var globalconfig float ArbitrationHandshakeTimeout;
|
|
|
|
var globalconfig float GameDifficulty;
|
|
`if(`__TW_)
|
|
var float GameDifficultyModifier;
|
|
`endif
|
|
var globalconfig int GoreLevel; // 0=Normal, increasing values=less gore
|
|
var float GameSpeed; // Scale applied to game rate.
|
|
|
|
var class<Pawn> DefaultPawnClass;
|
|
|
|
// user interface
|
|
var class<HUD> HUDType; // HUD class this game uses.
|
|
var class<HUD> SecondaryHUDType; // Secondary HUD class this game uses.
|
|
|
|
var globalconfig int MaxSpectators; // Maximum number of spectators allowed by this server.
|
|
var int MaxSpectatorsAllowed; // Maximum number of spectators ever allowed (MaxSpectators is clamped to this in initgame()
|
|
var int NumSpectators; // Current number of spectators.
|
|
`if (`__TW_)
|
|
var config int MaxPlayers; // Maximum number of players allowed by this server.
|
|
`else
|
|
var globalconfig int MaxPlayers; // Maximum number of players allowed by this server.
|
|
`endif
|
|
var int MaxPlayersAllowed; // Maximum number of players ever allowed (MaxPlayers is clamped to this in initgame()
|
|
var int NumPlayers; // number of human players
|
|
var int NumBots; // number of non-human players (AI controlled but participating as a player)
|
|
//@SABER_EGS_BEGIN Crossplay support
|
|
var int NumEosPlayers; // number of players, using epic online system
|
|
//@SABER_EGS_END
|
|
/** number of players that are still travelling from a previous map */
|
|
var int NumTravellingPlayers;
|
|
var int CurrentID; // used to assign unique PlayerIDs to each PlayerReplicationInfo
|
|
var localized string DefaultPlayerName;
|
|
var localized string GameName;
|
|
var float FearCostFallOff; // how fast the FearCost in NavigationPoints falls off
|
|
var bool bDoFearCostFallOff; // If true, FearCost will fall off over time in NavigationPoints. Reset to false once all reach FearCosts reach 0.
|
|
|
|
var config int GoalScore; // what score is needed to end the match
|
|
var config int MaxLives; // max number of lives for match, unless overruled by level's GameDetails
|
|
var config int TimeLimit; // time limit in minutes
|
|
|
|
// Message classes.
|
|
var class<LocalMessage> DeathMessageClass;
|
|
var class<GameMessage> GameMessageClass;
|
|
|
|
//-------------------------------------
|
|
// GameInfo components
|
|
var Mutator BaseMutator; // linked list of Mutators (for modifying actors as they enter the game)
|
|
var class<AccessControl> AccessControlClass;
|
|
var AccessControl AccessControl; // AccessControl controls whether players can enter and/or become admins
|
|
var class<BroadcastHandler> BroadcastHandlerClass;
|
|
var BroadcastHandler BroadcastHandler; // handles message (text and localized) broadcasts
|
|
|
|
/** Class of automated test manager used by this game class */
|
|
var class<AutoTestManager> AutoTestManagerClass;
|
|
|
|
/** Instantiated AutoTestManager - only exists if requested by command-line */
|
|
var AutoTestManager MyAutoTestManager;
|
|
|
|
var class<PlayerController> PlayerControllerClass; // type of player controller to spawn for players logging in
|
|
var class<PlayerReplicationInfo> PlayerReplicationInfoClass;
|
|
|
|
// ReplicationInfo
|
|
var() class<GameReplicationInfo> GameReplicationInfoClass;
|
|
var GameReplicationInfo GameReplicationInfo;
|
|
|
|
var CrowdPopulationManagerBase PopulationManager;
|
|
var class<CrowdPopulationManagerBase> PopulationManagerClass;
|
|
|
|
var globalconfig float MaxIdleTime; // maximum time players are allowed to idle before being kicked
|
|
|
|
/** Max interval that client clock is allowed to get ahead of server clock before triggering speed hack detection */
|
|
var globalconfig float MaxTimeMargin;
|
|
|
|
/** How fast we allow client clock to drift from server clock over time without ever triggering speed hack detection */
|
|
var globalconfig float TimeMarginSlack;
|
|
|
|
/** Clamps how far behind server clock we let time margin get. Used to prevent speedhacks where client slows their clock down for a while then speeds it up. */
|
|
var globalconfig float MinTimeMargin;
|
|
|
|
var array<PlayerReplicationInfo> InactivePRIArray; /** PRIs of players who have left game (saved in case they reconnect) */
|
|
|
|
/** The list of delegates to check before unpausing a game */
|
|
var array<delegate<CanUnpause> > Pausers;
|
|
|
|
/** Cached online subsystem variable */
|
|
var OnlineSubsystem OnlineSub;
|
|
|
|
//@HSL_BEGIN - BWJ - 6-16-16 - Playfab support
|
|
var PlayfabInterface PlayfabInter;
|
|
//@HSL_END
|
|
|
|
/** Cached online game interface variable */
|
|
var OnlineGameInterface GameInterface;
|
|
|
|
/** Class sent to clients to use to create and hold their stats */
|
|
var class<OnlineStatsWrite> OnlineStatsWriteClass;
|
|
|
|
/** The leaderboard to write the stats to for skill/scoring */
|
|
var int LeaderboardId;
|
|
|
|
/** The arbitrated leaderboard to write the stats to for skill/scoring */
|
|
var int ArbitratedLeaderboardId;
|
|
|
|
/** perform map travels using SeamlessTravel() which loads in the background and doesn't disconnect clients
|
|
* @see WorldInfo::SeamlessTravel()
|
|
*/
|
|
var bool bUseSeamlessTravel;
|
|
|
|
/** Base copy of cover changes that need to be replicated to clients on join */
|
|
var protected CoverReplicator CoverReplicatorBase;
|
|
|
|
/** Tracks whether the server can travel due to a critical network error or not */
|
|
var bool bHasNetworkError;
|
|
|
|
/** Whether this game type requires voice to be push to talk or not */
|
|
var const bool bRequiresPushToTalk;
|
|
|
|
/** The class to use when registering dedicated servers with the online service */
|
|
var const class<OnlineGameSettings> OnlineGameSettingsClass;
|
|
|
|
/** The options to apply for dedicated server when it starts to register */
|
|
var string ServerOptions;
|
|
|
|
/** Current adjusted net speed - Used for dynamically managing netspeed for listen servers*/
|
|
var int AdjustedNetSpeed;
|
|
|
|
/** Last time netspeed was updated for server (by client entering or leaving) */
|
|
var float LastNetSpeedUpdateTime;
|
|
|
|
/** Total available bandwidth for listen server, split dynamically across net connections */
|
|
var globalconfig int TotalNetBandwidth;
|
|
|
|
/** Minimum bandwidth dynamically set per connection */
|
|
var globalconfig int MinDynamicBandwidth;
|
|
|
|
/** Maximum bandwidth dynamically set per connection */
|
|
var globalconfig int MaxDynamicBandwidth;
|
|
|
|
/** Standby cheat detection vars */
|
|
/** Used to determine if checking for standby cheats should occur */
|
|
var config bool bIsStandbyCheckingEnabled;
|
|
/** Tracks standby checking status */
|
|
var bool bIsStandbyCheckingOn;
|
|
/** Used to determine whether we've already caught a cheat or not */
|
|
var bool bHasStandbyCheatTriggered;
|
|
/** The amount of time without packets before triggering the cheat code */
|
|
var config float StandbyRxCheatTime;
|
|
/** The amount of time without packets before triggering the cheat code */
|
|
var config float StandbyTxCheatTime;
|
|
/** The point we determine the server is either delaying packets or has bad upstream */
|
|
var config int BadPingThreshold;
|
|
/** The percentage of clients missing RX data before triggering the standby code */
|
|
var config float PercentMissingForRxStandby;
|
|
/** The percentage of clients missing TX data before triggering the standby code */
|
|
var config float PercentMissingForTxStandby;
|
|
/** The percentage of clients with bad ping before triggering the standby code */
|
|
var config float PercentForBadPing;
|
|
/** The amount of time to wait before checking a connection for standby issues */
|
|
var config float JoinInProgressStandbyWaitTime;
|
|
|
|
/** Material used for drawing a streaming pause icon. */
|
|
var Material StreamingPauseIcon;
|
|
|
|
/** Describes which standby detection event occured so the game can take appropriate action */
|
|
enum EStandbyType
|
|
{
|
|
STDBY_Rx,
|
|
STDBY_Tx,
|
|
STDBY_BadPing
|
|
};
|
|
/** End standby cheat vars */
|
|
|
|
struct native GameClassShortName
|
|
{
|
|
var string ShortName;
|
|
var string GameClassName;
|
|
};
|
|
`if(`__TW_)
|
|
var() protected const array<GameClassShortName> GameInfoClassAliases;
|
|
`else
|
|
var() protected config const array<GameClassShortName> GameInfoClassAliases;
|
|
`endif
|
|
|
|
/**
|
|
* GameTypePrefix helper structure.
|
|
* Used to find valid gametypes for a map via its prefix.
|
|
*/
|
|
struct native GameTypePrefix
|
|
{
|
|
/** map prefix, e.g. "DM" */
|
|
var string Prefix;
|
|
/** if TRUE, generate a common package for the gametype */
|
|
var bool bUsesCommonPackage;
|
|
/** gametype used if none specified on the URL */
|
|
var string GameType;
|
|
/** additional gametypes supported by this map prefix via the URL (used for cooking) */
|
|
var array<string> AdditionalGameTypes;
|
|
/** forced objects (and classes) that should go into the common package to avoid cooking into every map */
|
|
var array<string> ForcedObjects;
|
|
};
|
|
|
|
/** The default game type to use on a map */
|
|
var config string DefaultGameType;
|
|
/** Used for loading appropriate game type if non-specified in URL */
|
|
var config array<GameTypePrefix> DefaultMapPrefixes;
|
|
/** Used for loading appropriate game type if non-specified in URL */
|
|
var config array<GameTypePrefix> CustomMapPrefixes;
|
|
|
|
|
|
/** Size of the AnimTree pool. System will keep this number of extra AnimTrees around per Template */
|
|
var config int AnimTreePoolSize;
|
|
|
|
/**
|
|
* Retrieve the FGameTypePrefix struct for the given map filename.
|
|
*
|
|
* @param InFilename The map file name
|
|
* @param OutGameType The gametype prefix struct to fill in
|
|
* @param bCheckExt Optional parameter to check the extension of the InFilename to ensure it is a map
|
|
*
|
|
* @return UBOOL TRUE if successful, FALSE if map prefix not found.
|
|
* NOTE: FALSE will fill in with the default gametype.
|
|
*/
|
|
function native bool GetSupportedGameTypes(const out string InFilename, out GameTypePrefix OutGameType, optional bool bCheckExt = false) const;
|
|
|
|
/**
|
|
* Retrieve the name of the common package (if any) for the given map filename.
|
|
*
|
|
* @param InFilename The map file name
|
|
* @param OutCommonPackageName The nane of the common package for the given map
|
|
*
|
|
* @return UBOOL TRUE if successful, FALSE if map prefix not found.
|
|
*/
|
|
function native bool GetMapCommonPackageName(const out string InFilename, out string OutCommonPackageName) const;
|
|
|
|
cpptext
|
|
{
|
|
/** called on the default object of the class specified by DefaultGame in the [Engine.GameInfo] section of Game.ini
|
|
* whenever worlds are saved.
|
|
* Gives the game a chance to add supported gametypes to the WorldInfo's GameTypesSupportedOnThisMap array
|
|
* (used for console cooking)
|
|
* @param Info: the WorldInfo of the world being saved
|
|
*/
|
|
virtual void AddSupportedGameTypes(AWorldInfo* Info, const TCHAR* WorldFilename, TArray<FString>& AdditionalPackagesToCook) const
|
|
{
|
|
}
|
|
|
|
/** Allows for game classname remapping and/or aliasing (e.g. for shorthand names) */
|
|
static FString StaticGetRemappedGameClassName(FString const& GameClassName);
|
|
|
|
#if __TW_SDK_
|
|
virtual void PreBeginPlay();
|
|
virtual UBOOL IsWhitelisted();
|
|
virtual UBOOL IsStandardGame();
|
|
virtual void SetGameUnranked( UBOOL bUnranked ) {}
|
|
virtual UBOOL IsUnrankedGame() { return true; }
|
|
#endif
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Engine notifications.
|
|
|
|
event PreBeginPlay()
|
|
{
|
|
AdjustedNetSpeed = MaxDynamicBandwidth;
|
|
SetGameSpeed(GameSpeed);
|
|
GameReplicationInfo = Spawn(GameReplicationInfoClass);
|
|
WorldInfo.GRI = GameReplicationInfo;
|
|
|
|
InitGameReplicationInfo();
|
|
InitCrowdPopulationManager();
|
|
}
|
|
|
|
function CoverReplicator GetCoverReplicator()
|
|
{
|
|
if (CoverReplicatorBase == None && WorldInfo.NetMode != NM_Standalone)
|
|
{
|
|
CoverReplicatorBase = Spawn(class'CoverReplicator');
|
|
}
|
|
return CoverReplicatorBase;
|
|
}
|
|
|
|
event PostBeginPlay()
|
|
{
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
if ( MaxIdleTime > 0 )
|
|
{
|
|
MaxIdleTime = FMax(MaxIdleTime, 20);
|
|
}
|
|
|
|
//@HSL_BEGIN - BWJ - 6-16-16 - Playfab support
|
|
PlayfabInter = class'GameEngine'.static.GetPlayfabInterface();
|
|
//@SABER_EGS IsEOSDedicatedServer() case added
|
|
if( WorldInfo.IsConsoleDedicatedServer() || WorldInfo.IsEOSDedicatedServer() )
|
|
{
|
|
if( PlayfabInter != none )
|
|
{
|
|
if( PlayfabInter.GetGameSettings() == None )
|
|
{
|
|
PlayfabInter.CreateGameSettings( OnlineGameSettingsClass );
|
|
GameSettings = PlayfabInter.GetGameSettings();
|
|
GameSettings.UpdateFromURL(ServerOptions, self);
|
|
|
|
// If this server wasn't launched by playfab, register the server now
|
|
if( !WasLaunchedByPlayfab() )
|
|
{
|
|
// First we update game settings
|
|
UpdateGameSettings();
|
|
// Now register the game
|
|
PlayfabInter.ServerRegisterGame();
|
|
}
|
|
|
|
// Initialize title data, see also TWRefreshOnlineGameData
|
|
PlayfabInter.AddTitleDataReadCompleteDelegate( OnServerTitleDataRead );
|
|
PlayfabInter.ReadTitleData();
|
|
}
|
|
}
|
|
}
|
|
else // So that it'll get deleted if Hardsuit ever removes this block
|
|
//@HSL_END
|
|
if (WorldInfo.NetMode == NM_DedicatedServer)
|
|
{
|
|
// Update any online advertised settings
|
|
UpdateGameSettings();
|
|
}
|
|
}
|
|
|
|
|
|
//@HSL_BEGIN - BWJ - 1-5-17 - Support for reading title data
|
|
function OnServerTitleDataRead()
|
|
{
|
|
PlayfabInter.ClearTitleDataReadCompleteDelegate(OnServerTitleDataRead);
|
|
}
|
|
//@HSL_END
|
|
|
|
|
|
/**
|
|
* Use 'ShowGameDebug' console command to show this debug info
|
|
* Useful to show general debug info not tied to a particular actor physically in the level.
|
|
*/
|
|
simulated function DisplayDebug(HUD HUD, out float out_YL, out float out_YPos)
|
|
{
|
|
local Canvas Canvas;
|
|
|
|
Canvas = HUD.Canvas;
|
|
Canvas.SetDrawColor(255,255,255);
|
|
|
|
Canvas.DrawText("Game:" $GameName );
|
|
out_YPos += out_YL;
|
|
Canvas.SetPos(4,out_YPos);
|
|
|
|
if ( WorldInfo.PopulationManager != None )
|
|
{
|
|
WorldInfo.PopulationManager.DisplayDebug(HUD, out_YL, out_YPos);
|
|
}
|
|
}
|
|
|
|
|
|
/* Reset() - reset actor to initial state - used when restarting level without reloading.
|
|
@note: GameInfo::Reset() called after all other actors have been reset */
|
|
function Reset()
|
|
{
|
|
super.Reset();
|
|
|
|
bGameEnded = false;
|
|
bOverTime = false;
|
|
InitGameReplicationInfo();
|
|
}
|
|
|
|
/** @return true if ActorToReset should have Reset() called on it while restarting the game, false if the GameInfo will manually reset it
|
|
or if the actor does not need to be reset
|
|
*/
|
|
function bool ShouldReset(Actor ActorToReset)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/** Resets level by calling Reset() on all actors */
|
|
function ResetLevel()
|
|
{
|
|
local Controller C;
|
|
local Actor A;
|
|
local Sequence GameSeq;
|
|
local array<SequenceObject> AllSeqEvents;
|
|
local array<int> ActivateIndices;
|
|
local int i;
|
|
|
|
`Log("Reset" @ self);
|
|
// Reset ALL controllers first
|
|
foreach WorldInfo.AllControllers(class'Controller', C)
|
|
{
|
|
if ( PlayerController(C) != None )
|
|
{
|
|
PlayerController(C).ClientReset();
|
|
}
|
|
C.Reset();
|
|
}
|
|
|
|
// Reset all actors (except controllers, the GameInfo, and any other actors specified by ShouldReset())
|
|
foreach AllActors(class'Actor', A)
|
|
{
|
|
if (A != self && !A.IsA('Controller') && ShouldReset(A))
|
|
{
|
|
A.Reset();
|
|
}
|
|
}
|
|
|
|
// reset the GameInfo
|
|
Reset();
|
|
|
|
// reset Kismet and activate any Level Reset events
|
|
GameSeq = WorldInfo.GetGameSequence();
|
|
if (GameSeq != None)
|
|
{
|
|
// reset the game sequence
|
|
GameSeq.Reset();
|
|
|
|
// find any Level Loaded events that exist
|
|
GameSeq.FindSeqObjectsByClass(class'SeqEvent_LevelLoaded', true, AllSeqEvents);
|
|
|
|
// activate them
|
|
ActivateIndices[0] = 2;
|
|
for (i = 0; i < AllSeqEvents.Length; i++)
|
|
{
|
|
SeqEvent_LevelLoaded(AllSeqEvents[i]).CheckActivate(WorldInfo, None, false, ActivateIndices);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
event Timer()
|
|
{
|
|
BroadcastHandler.UpdateSentText();
|
|
|
|
// Update navigation point fear cost fall off.
|
|
if ( bDoFearCostFallOff )
|
|
{
|
|
DoNavFearCostFallOff();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check to see if we should start in cinematic mode (e.g. matinee movie capture).
|
|
*
|
|
* @param OutHidePlayer Whether to hide the player
|
|
* @param OutHideHud Whether to hide the HUD
|
|
* @param OutDisableMovement Whether to disable movement
|
|
* @param OutDisableTurning Whether to disable turning
|
|
* @param OutDisableInput Whether to disable input
|
|
*
|
|
* @return UBOOL TRUE if we should turn on cinematic mode,
|
|
* FALSE if if we should not.
|
|
*/
|
|
final native function bool ShouldStartInCinematicMode(out int OutHidePlayer, out int OutHideHud, out int OutDisableMovement, out int OutDisableTurning, out int OutDisableInput);
|
|
|
|
/** Update navigation point fear cost fall off. */
|
|
final native function DoNavFearCostFallOff();
|
|
|
|
/** notification when a NavigationPoint becomes blocked or unblocked */
|
|
function NotifyNavigationChanged(NavigationPoint N);
|
|
|
|
// Called when game shutsdown.
|
|
event GameEnding()
|
|
{
|
|
if (AccessControl != none)
|
|
{
|
|
AccessControl.NotifyGameEnding();
|
|
}
|
|
|
|
ClearOnlineDelegates();
|
|
|
|
EndLogging("serverquit");
|
|
}
|
|
|
|
/* KickIdler() called if
|
|
if ( (Pawn != None) || (PlayerReplicationInfo.bOnlySpectator && (ViewTarget != self))
|
|
|| (WorldInfo.Pauser != None) || WorldInfo.Game.bWaitingToStartMatch || WorldInfo.Game.bGameEnded )
|
|
{
|
|
LastActiveTime = WorldInfo.TimeSeconds;
|
|
}
|
|
else if ( (WorldInfo.Game.MaxIdleTime > 0) && (WorldInfo.TimeSeconds - LastActiveTime > WorldInfo.Game.MaxIdleTime) )
|
|
KickIdler(self);
|
|
*/
|
|
event KickIdler(PlayerController PC)
|
|
{
|
|
`log("Kicking idle player "$PC.PlayerReplicationInfo.PlayerName);
|
|
AccessControl.KickPlayer(PC, AccessControl.IdleKickReason);
|
|
}
|
|
|
|
// This will kick any player, even if they are an admin.
|
|
event ForceKickPlayer(PlayerController PC, string KickReason)
|
|
{
|
|
`log("Force kicking player "$PC.PlayerReplicationInfo.PlayerName);
|
|
AccessControl.ForceKickPlayer(PC, KickReason);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Replication
|
|
|
|
function InitGameReplicationInfo()
|
|
{
|
|
GameReplicationInfo.GameClass = Class;
|
|
GameReplicationInfo.ReceivedGameClass();
|
|
}
|
|
|
|
native function string GetNetworkNumber();
|
|
|
|
function int GetNumPlayers()
|
|
{
|
|
return NumPlayers + NumTravellingPlayers;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Misc.
|
|
|
|
/**
|
|
* Default delegate that provides an implementation for those that don't have
|
|
* special needs other than a toggle
|
|
*/
|
|
delegate bool CanUnpause()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Adds the delegate to the list if the player controller has the right to pause
|
|
* the game. The delegate is called to see if it is ok to unpause the game, e.g.
|
|
* the reason the game was paused has been cleared.
|
|
*
|
|
* @param PC the player controller to check for admin privs
|
|
* @param CanUnpauseDelegate the delegate to query when checking for unpause
|
|
*/
|
|
function bool SetPause(PlayerController PC, optional delegate<CanUnpause> CanUnpauseDelegate=CanUnpause)
|
|
{
|
|
local int FoundIndex;
|
|
|
|
if ( AllowPausing(PC) )
|
|
{
|
|
// Don't add the delegate twice (no need)
|
|
FoundIndex = Pausers.Find(CanUnpauseDelegate);
|
|
if (FoundIndex == INDEX_NONE)
|
|
{
|
|
// Not in the list so add it for querying
|
|
FoundIndex = Pausers.Length;
|
|
Pausers.Length = FoundIndex + 1;
|
|
Pausers[FoundIndex] = CanUnpauseDelegate;
|
|
}
|
|
// Let the first one in "own" the pause state
|
|
if (WorldInfo.Pauser == None)
|
|
{
|
|
WorldInfo.Pauser = PC.PlayerReplicationInfo;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks the list of delegates to determine if the pausing can be cleared. If
|
|
* the delegate says it's ok to unpause, that delegate is removed from the list
|
|
* and the rest are checked. The game is considered unpaused when the list is
|
|
* empty.
|
|
*/
|
|
event ClearPause()
|
|
{
|
|
local int Index;
|
|
local delegate<CanUnpause> CanUnpauseCriteriaMet;
|
|
|
|
if ( !AllowPausing() && Pausers.Length > 0 )
|
|
{
|
|
`log("Clearing list of UnPause delegates for" @ Name @ "because game type is not pauseable");
|
|
Pausers.Length = 0;
|
|
}
|
|
|
|
for (Index = 0; Index < Pausers.Length; Index++)
|
|
{
|
|
CanUnpauseCriteriaMet = Pausers[Index];
|
|
if (CanUnpauseCriteriaMet())
|
|
{
|
|
Pausers.Remove(Index--,1);
|
|
}
|
|
}
|
|
|
|
// Clear the pause state if the list is empty
|
|
if ( Pausers.Length == 0 )
|
|
{
|
|
WorldInfo.Pauser = None;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Forcibly removes an object's CanUnpause delegates from the list of pausers. If any of the object's CanUnpause delegate
|
|
* handlers were in the list, triggers a call to ClearPause().
|
|
*
|
|
* Called when the player controller is being destroyed to prevent the game from being stuck in a paused state when a PC that
|
|
* paused the game is destroyed before the game is unpaused.
|
|
*/
|
|
native final function ForceClearUnpauseDelegates( Actor PauseActor );
|
|
|
|
/**
|
|
* Dumps the pause delegate list to track down who has the game paused
|
|
*/
|
|
function DebugPause()
|
|
{
|
|
local int Index;
|
|
local delegate<CanUnpause> CanUnpauseCriteriaMet;
|
|
|
|
for (Index = 0; Index < Pausers.Length; Index++)
|
|
{
|
|
CanUnpauseCriteriaMet = Pausers[Index];
|
|
if (CanUnpauseCriteriaMet())
|
|
{
|
|
`Log("Pauser in index "$Index$" thinks it's ok to unpause:" @ CanUnpauseCriteriaMet);
|
|
}
|
|
else
|
|
{
|
|
`Log("Pauser in index "$Index$" thinks the game should remain paused:" @ CanUnpauseCriteriaMet);
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Game parameters.
|
|
|
|
//
|
|
// Set gameplay speed.
|
|
//
|
|
function SetGameSpeed( Float T )
|
|
{
|
|
GameSpeed = FMax(T, 0.00001);
|
|
WorldInfo.TimeDilation = GameSpeed;
|
|
SetTimer(WorldInfo.TimeDilation, true);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Player start functions
|
|
|
|
//
|
|
// Grab the next option from a string.
|
|
//
|
|
static function bool GrabOption( out string Options, out string Result )
|
|
{
|
|
if( Left(Options,1)=="?" )
|
|
{
|
|
// Get result.
|
|
Result = Mid(Options,1);
|
|
if( InStr(Result,"?")>=0 )
|
|
Result = Left( Result, InStr(Result,"?") );
|
|
|
|
// Update options.
|
|
Options = Mid(Options,1);
|
|
if( InStr(Options,"?")>=0 )
|
|
Options = Mid( Options, InStr(Options,"?") );
|
|
else
|
|
Options = "";
|
|
|
|
return true;
|
|
}
|
|
else return false;
|
|
}
|
|
|
|
//
|
|
// Break up a key=value pair into its key and value.
|
|
//
|
|
static function GetKeyValue( string Pair, out string Key, out string Value )
|
|
{
|
|
if( InStr(Pair,"=")>=0 )
|
|
{
|
|
Key = Left(Pair,InStr(Pair,"="));
|
|
Value = Mid(Pair,InStr(Pair,"=")+1);
|
|
}
|
|
else
|
|
{
|
|
Key = Pair;
|
|
Value = "";
|
|
}
|
|
}
|
|
|
|
/* ParseOption()
|
|
Find an option in the options string and return it.
|
|
*/
|
|
static function string ParseOption( string Options, string InKey )
|
|
{
|
|
local string Pair, Key, Value;
|
|
while( GrabOption( Options, Pair ) )
|
|
{
|
|
GetKeyValue( Pair, Key, Value );
|
|
if( Key ~= InKey )
|
|
return Value;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
//
|
|
// HasOption - return true if the option is specified on the command line.
|
|
//
|
|
static function bool HasOption( string Options, string InKey )
|
|
{
|
|
local string Pair, Key, Value;
|
|
while( GrabOption( Options, Pair ) )
|
|
{
|
|
GetKeyValue( Pair, Key, Value );
|
|
if( Key ~= InKey )
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static function int GetIntOption( string Options, string ParseString, int CurrentValue)
|
|
{
|
|
local string InOpt;
|
|
|
|
InOpt = ParseOption( Options, ParseString );
|
|
if ( InOpt != "" )
|
|
{
|
|
return int(InOpt);
|
|
}
|
|
return CurrentValue;
|
|
}
|
|
|
|
/** @return the full path to the optimal GameInfo class to use for the specified map and options
|
|
* this is used for preloading cooked packages, etc. and therefore doesn't need to include any fallbacks
|
|
* as SetGameType() will be called later to actually find/load the desired class
|
|
*/
|
|
static event string GetDefaultGameClassPath(string MapName, string Options, string Portal)
|
|
{
|
|
return PathName(Default.Class);
|
|
}
|
|
|
|
/** @return the class of GameInfo to spawn for the game on the specified map and the specified options
|
|
* this function should include any fallbacks in case the desired class can't be found
|
|
*/
|
|
static event class<GameInfo> SetGameType(string MapName, string Options, string Portal)
|
|
{
|
|
return Default.Class;
|
|
}
|
|
|
|
/* Initialize the game.
|
|
The GameInfo's InitGame() function is called before any other scripts (including
|
|
PreBeginPlay() ), and is used by the GameInfo to initialize parameters and spawn
|
|
its helper classes.
|
|
Warning: this is called before actors' PreBeginPlay.
|
|
*/
|
|
event InitGame( string Options, out string ErrorMessage )
|
|
{
|
|
local string InOpt, LeftOpt;
|
|
local int pos;
|
|
local class<AccessControl> ACClass;
|
|
local AccessControl CurAC;
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
MaxPlayers = Clamp(GetIntOption( Options, "MaxPlayers", MaxPlayers ),0,MaxPlayersAllowed);
|
|
MaxSpectators = Clamp(GetIntOption( Options, "MaxSpectators", MaxSpectators ),0,MaxSpectatorsAllowed);
|
|
GameDifficulty = FMax(0,GetIntOption(Options, "Difficulty", GameDifficulty));
|
|
|
|
InOpt = ParseOption( Options, "GameSpeed");
|
|
if( InOpt != "" )
|
|
{
|
|
`log("GameSpeed"@InOpt);
|
|
SetGameSpeed(float(InOpt));
|
|
}
|
|
|
|
TimeLimit = Max(0,GetIntOption( Options, "TimeLimit", TimeLimit ));
|
|
|
|
BroadcastHandler = spawn(BroadcastHandlerClass);
|
|
|
|
InOpt = ParseOption( Options, "AccessControl");
|
|
if( InOpt != "" )
|
|
{
|
|
ACClass = class<AccessControl>(DynamicLoadObject(InOpt, class'Class'));
|
|
}
|
|
if ( ACClass == None )
|
|
{
|
|
ACClass = AccessControlClass;
|
|
}
|
|
|
|
LeftOpt = ParseOption( Options, "AdminName" );
|
|
InOpt = ParseOption( Options, "AdminPassword");
|
|
/* @FIXME: this is a compiler error, "can't set defaults of non-config properties". Fix or remove at some point.
|
|
if( LeftOpt!="" && InOpt!="" )
|
|
ACClass.default.bDontAddDefaultAdmin = true;
|
|
*/
|
|
|
|
// Only spawn access control if we are a server
|
|
if (WorldInfo.NetMode == NM_ListenServer || WorldInfo.NetMode == NM_DedicatedServer)
|
|
{
|
|
// If we are coming out of seamless travel, and there is an access control carried over from the previous level,
|
|
// use that instead of creating a new one
|
|
if (WorldInfo.IsInSeamlessTravel())
|
|
{
|
|
foreach DynamicActors(Class'AccessControl', CurAC)
|
|
{
|
|
AccessControl = CurAC;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (AccessControl == none)
|
|
{
|
|
AccessControl = Spawn(ACClass);
|
|
}
|
|
|
|
if (AccessControl != None && InOpt != "")
|
|
{
|
|
AccessControl.SetAdminPassword(InOpt);
|
|
}
|
|
}
|
|
|
|
InOpt = ParseOption( Options, "Mutator");
|
|
if ( InOpt != "" )
|
|
{
|
|
`log("Mutators"@InOpt);
|
|
while ( InOpt != "" )
|
|
{
|
|
pos = InStr(InOpt,",");
|
|
if ( pos > 0 )
|
|
{
|
|
LeftOpt = Left(InOpt, pos);
|
|
InOpt = Right(InOpt, Len(InOpt) - pos - 1);
|
|
}
|
|
else
|
|
{
|
|
LeftOpt = InOpt;
|
|
InOpt = "";
|
|
}
|
|
AddMutator(LeftOpt, true);
|
|
}
|
|
}
|
|
|
|
InOpt = ParseOption( Options, "GamePassword");
|
|
if( InOpt != "" && AccessControl != None)
|
|
{
|
|
AccessControl.SetGamePassWord(InOpt);
|
|
`log( "GamePassword" @ InOpt );
|
|
}
|
|
|
|
bFixedPlayerStart = ( ParseOption( Options, "FixedPlayerStart" ) ~= "1" );
|
|
CauseEventCommand = ( ParseOption( Options, "causeevent" ) );
|
|
|
|
if ( ParseOption( Options, "AutoTests" ) ~= "1" )
|
|
{
|
|
if ( MyAutoTestManager == None )
|
|
{
|
|
MyAutoTestManager = spawn(AutoTestManagerClass);
|
|
}
|
|
MyAutoTestManager.InitializeOptions(Options);
|
|
}
|
|
|
|
BugLocString = ParseOption(Options, "BugLoc");
|
|
BugRotString = ParseOption(Options, "BugRot");
|
|
|
|
if (BaseMutator != none)
|
|
{
|
|
BaseMutator.InitMutator(Options, ErrorMessage);
|
|
}
|
|
|
|
// Cache a pointer to the online subsystem
|
|
OnlineSub = class'GameEngine'.static.GetOnlineSubsystem();
|
|
if (OnlineSub != None)
|
|
{
|
|
// And grab one for the game interface since it will be used often
|
|
GameInterface = OnlineSub.GameInterface;
|
|
if (GameInterface != None)
|
|
{
|
|
// Grab the current game settings object out
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
if (GameSettings != None)
|
|
{
|
|
// Check for an arbitrated match
|
|
bUsingArbitration = GameSettings.bUsesArbitration;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((WorldInfo.IsConsoleBuild(CONSOLE_Any) == false) &&
|
|
(WorldInfo.NetMode != NM_Standalone) &&
|
|
// Don't register the session if the UI already has
|
|
(GameSettings == None))
|
|
{
|
|
// Cache this so it can be used later by async processes
|
|
ServerOptions = Options;
|
|
// If there isn't a login to process, immediately register the server
|
|
// Otherwise the server will be registered when the login completes
|
|
if (!ProcessServerLogin())
|
|
{
|
|
RegisterServer();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Called when a connection closes before getting to PostLogin() */
|
|
event NotifyPendingConnectionLost();
|
|
|
|
function AddMutator(string mutname, optional bool bUserAdded)
|
|
{
|
|
local class<Mutator> mutClass;
|
|
local Mutator mut;
|
|
local int i;
|
|
|
|
if ( !Static.AllowMutator(MutName) )
|
|
return;
|
|
|
|
mutClass = class<Mutator>(DynamicLoadObject(mutname, class'Class'));
|
|
if (mutClass == None)
|
|
return;
|
|
|
|
if (mutClass.Default.GroupNames.length > 0 && BaseMutator != None)
|
|
{
|
|
// make sure no mutators with same groupname
|
|
for (mut = BaseMutator; mut != None; mut = mut.NextMutator)
|
|
{
|
|
for (i = 0; i < mut.GroupNames.length; i++)
|
|
{
|
|
if (mutClass.default.GroupNames.Find(mut.GroupNames[i]) != INDEX_NONE)
|
|
{
|
|
`log("Not adding "$mutClass$" because already have a mutator in the same group - "$mut);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure this mutator is not added already
|
|
for ( mut=BaseMutator; mut!=None; mut=mut.NextMutator )
|
|
if ( mut.Class == mutClass )
|
|
{
|
|
`log("Not adding "$mutClass$" because this mutator is already added - "$mut);
|
|
return;
|
|
}
|
|
|
|
mut = Spawn(mutClass);
|
|
// mc, beware of mut being none
|
|
if (mut == None)
|
|
return;
|
|
|
|
// Meant to verify if this mutator was from Command Line parameters or added from other Actors
|
|
mut.bUserAdded = bUserAdded;
|
|
|
|
if (BaseMutator == None)
|
|
{
|
|
BaseMutator = mut;
|
|
}
|
|
else
|
|
{
|
|
BaseMutator.AddMutator(mut);
|
|
}
|
|
}
|
|
|
|
/* RemoveMutator()
|
|
Remove a mutator from the mutator list
|
|
*/
|
|
function RemoveMutator( Mutator MutatorToRemove )
|
|
{
|
|
local Mutator M;
|
|
|
|
// remove from mutator list
|
|
if ( BaseMutator == MutatorToRemove )
|
|
{
|
|
BaseMutator = MutatorToRemove.NextMutator;
|
|
}
|
|
else if ( BaseMutator != None )
|
|
{
|
|
for ( M=BaseMutator; M!=None; M=M.NextMutator )
|
|
{
|
|
if ( M.NextMutator == MutatorToRemove )
|
|
{
|
|
M.NextMutator = MutatorToRemove.NextMutator;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
`if (`__TW_)
|
|
function string CheckNextMap(string NextMap)
|
|
{
|
|
return NextMap;
|
|
}
|
|
|
|
static function bool GetShouldShowLength()
|
|
{
|
|
return true;
|
|
}
|
|
`endif
|
|
|
|
/* ProcessServerTravel()
|
|
Optional handling of ServerTravel for network games.
|
|
*/
|
|
function ProcessServerTravel(string URL, optional bool bAbsolute)
|
|
{
|
|
local PlayerController LocalPlayer;
|
|
local bool bSeamless;
|
|
//@HSL_BEGIN_XBOX
|
|
local string NextMap, EncodedPlayerName;
|
|
//@HSL_END_XBOX
|
|
local Guid NextMapGuid;
|
|
local int OptionStart;
|
|
|
|
bLevelChange = true;
|
|
EndLogging("mapchange");
|
|
|
|
// force an old style load screen if the server has been up for a long time so that TimeSeconds doesn't overflow and break everything
|
|
bSeamless = (bUseSeamlessTravel && WorldInfo.TimeSeconds < 172800.0f); // 172800 seconds == 48 hours
|
|
|
|
if (InStr(Caps(URL), "?RESTART") != INDEX_NONE)
|
|
{
|
|
NextMap = string(WorldInfo.GetPackageName());
|
|
}
|
|
else
|
|
{
|
|
OptionStart = InStr(URL, "?");
|
|
if (OptionStart == INDEX_NONE)
|
|
{
|
|
NextMap = URL;
|
|
}
|
|
else
|
|
{
|
|
NextMap = Left(URL, OptionStart);
|
|
}
|
|
}
|
|
`if (`__TW_)
|
|
NextMap = CheckNextMap(NextMap);
|
|
if (OptionStart == INDEX_NONE)
|
|
{
|
|
URL = NextMap;
|
|
}
|
|
else
|
|
{
|
|
URL = NextMap $ Right(URL, Len(URL)-OptionStart);
|
|
}
|
|
`endif
|
|
|
|
NextMapGuid = GetPackageGuid(name(NextMap));
|
|
|
|
// Notify clients we're switching level and give them time to receive.
|
|
LocalPlayer = ProcessClientTravel(URL, NextMapGuid, bSeamless, bAbsolute);
|
|
|
|
`log("ProcessServerTravel:"@URL);
|
|
WorldInfo.NextURL = URL;
|
|
if (WorldInfo.NetMode == NM_ListenServer && LocalPlayer != None)
|
|
{
|
|
//@HSL_BEGIN_XBOX
|
|
// is this necessary or can we assume the DefaultURL name is valid?
|
|
EncodedPlayerName = LocalPlayer.GetDefaultURL("Name");
|
|
class'GameEngine'.static.EncodeURLString(EncodedPlayerName);
|
|
|
|
WorldInfo.NextURL $= "?Team="$LocalPlayer.GetDefaultURL("Team")
|
|
$"?Name="$EncodedPlayerName
|
|
$"?Class="$LocalPlayer.GetDefaultURL("Class")
|
|
$"?Character="$LocalPlayer.GetDefaultURL("Character");
|
|
//@HSL_END_XBOX
|
|
}
|
|
|
|
|
|
// Notify access control, to cleanup online subsystem references
|
|
if (AccessControl != none)
|
|
{
|
|
AccessControl.NotifyServerTravel(bSeamless);
|
|
}
|
|
|
|
// Trigger cleanup of online delegates
|
|
ClearOnlineDelegates();
|
|
|
|
if (bSeamless)
|
|
{
|
|
WorldInfo.SeamlessTravel(WorldInfo.NextURL, bAbsolute);
|
|
WorldInfo.NextURL = "";
|
|
}
|
|
// Switch immediately if not networking.
|
|
else if (WorldInfo.NetMode != NM_DedicatedServer && WorldInfo.NetMode != NM_ListenServer)
|
|
{
|
|
WorldInfo.NextSwitchCountdown = 0.0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies all clients to travel to the specified URL.
|
|
*
|
|
* @param URL a string containing the mapname (or IP address) to travel to, along with option key/value pairs
|
|
* @param NextMapGuid the GUID of the server's version of the next map
|
|
* @param bSeamless indicates whether the travel should use seamless travel or not.
|
|
* @param bAbsolute indicates which type of travel the server will perform (i.e. TRAVEL_Relative or TRAVEL_Absolute)
|
|
*/
|
|
function PlayerController ProcessClientTravel( out string URL, Guid NextMapGuid, bool bSeamless, bool bAbsolute )
|
|
{
|
|
local PlayerController P, LP;
|
|
|
|
// We call PreClientTravel directly on any local PlayerPawns (ie listen server)
|
|
foreach WorldInfo.AllControllers(class'PlayerController', P)
|
|
{
|
|
if ( NetConnection(P.Player) != None )
|
|
{
|
|
// remote player
|
|
P.ClientTravel(URL, TRAVEL_Relative, bSeamless, NextMapGuid);
|
|
}
|
|
else
|
|
{
|
|
// local player
|
|
LP = P;
|
|
P.PreClientTravel(URL, bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative, bSeamless);
|
|
}
|
|
}
|
|
|
|
return LP;
|
|
}
|
|
|
|
function byte GetModifiedGameDifficulty()
|
|
{
|
|
return GameDifficulty;
|
|
}
|
|
|
|
function bool RequiresPassword()
|
|
{
|
|
return ( (AccessControl != None) && AccessControl.RequiresPassword() );
|
|
}
|
|
|
|
//
|
|
// Accept or reject a player on the server.
|
|
// Fails login if you set the Error to a non-empty string.
|
|
// NOTE: UniqueId should not be trusted at this stage, it requires authentication
|
|
//
|
|
event PreLogin(string Options, string Address, const UniqueNetId UniqueId, bool bSupportsAuth, out string ErrorMessage)
|
|
{
|
|
local bool bSpectator;
|
|
local bool bPerfTesting;
|
|
|
|
// Check for an arbitrated match in progress and kick if needed
|
|
if (WorldInfo.NetMode != NM_Standalone && bUsingArbitration && bHasArbitratedHandshakeBegun)
|
|
{
|
|
ErrorMessage = PathName(WorldInfo.Game.GameMessageClass) $ ".ArbitrationMessage";
|
|
return;
|
|
}
|
|
|
|
// If this player is banned, reject him
|
|
if (AccessControl != none && AccessControl.IsIDBanned(UniqueId))
|
|
{
|
|
`log(Address@"is banned, rejecting...");
|
|
ErrorMessage = "Engine.AccessControl.SessionBanned";
|
|
return;
|
|
}
|
|
|
|
|
|
bPerfTesting = ( ParseOption( Options, "AutomatedPerfTesting" ) ~= "1" );
|
|
bSpectator = bPerfTesting || ( ParseOption( Options, "SpectatorOnly" ) ~= "1" ) || ( ParseOption( Options, "CauseEvent" ) ~= "FlyThrough" );
|
|
|
|
if (AccessControl != None)
|
|
{
|
|
AccessControl.PreLogin(Options, Address, UniqueId, bSupportsAuth, ErrorMessage, bSpectator);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If called from within PreLogin, pauses the login process for the currently connecting client (usually to delay login for authentication)
|
|
*
|
|
* @return A reference to the Player/NetConnection representing the connecting client; used to resume the login process
|
|
*/
|
|
native static final function Player PauseLogin();
|
|
|
|
/**
|
|
* Resumes the login process for the specified client Player/NetConnection
|
|
*
|
|
* @param InPlayer The Player/NetConnection to resume logging in
|
|
*/
|
|
native static final function ResumeLogin(Player InPlayer);
|
|
|
|
/**
|
|
* Rejects login for the specified client/NetConnection, with the specified error message
|
|
* NOTE: Error is the same error format PreLogin would take in OutError
|
|
* NOTE: Not restricted to clients at login; can be used on any valid NetConnection to disconnect a client
|
|
*
|
|
* @param InPlayer The Player/NetConnection to reject from the server
|
|
* @param Error The error message to give the player
|
|
*/
|
|
native static final function RejectLogin(Player InPlayer, string Error);
|
|
|
|
/*
|
|
* Is the server currently at capacity?
|
|
* @param bSpectator - Whether we should check against player or spectator limits
|
|
* @return TRUE if the server is full, FALSE otherwise
|
|
*/
|
|
`if (`__TW_ONLINESUBSYSTEM_)
|
|
function bool AtCapacity(bool bSpectator, optional const UniqueNetId PlayerID)
|
|
`else
|
|
function bool AtCapacity(bool bSpectator)
|
|
`endif
|
|
{
|
|
if ( WorldInfo.NetMode == NM_Standalone )
|
|
return false;
|
|
|
|
if ( bSpectator )
|
|
return ( (NumSpectators >= MaxSpectators)
|
|
&& ((WorldInfo.NetMode != NM_ListenServer) || (NumPlayers > 0)) );
|
|
else
|
|
return ( (MaxPlayers>0) && (GetNumPlayers() >= MaxPlayers) );
|
|
}
|
|
|
|
native final function int GetNextPlayerID();
|
|
|
|
/** spawns a PlayerController at the specified location; split out from Login()/HandleSeamlessTravelPlayer() for easier overriding */
|
|
function PlayerController SpawnPlayerController(vector SpawnLocation, rotator SpawnRotation)
|
|
{
|
|
return Spawn(PlayerControllerClass,,, SpawnLocation, SpawnRotation);
|
|
}
|
|
|
|
//
|
|
// Log a player in.
|
|
// Fails login if you set the Error string.
|
|
// PreLogin is called before Login, but significant game time may pass before
|
|
// Login is called, especially if content is downloaded.
|
|
// NOTE: If the AccessControl utilizes PauseLogin, UniqueID should be fully authenticated here
|
|
//
|
|
event PlayerController Login(string Portal, string Options, const UniqueNetID UniqueID, out string ErrorMessage)
|
|
{
|
|
local NavigationPoint StartSpot;
|
|
local PlayerController NewPlayer;
|
|
local string InName, InCharacter/*, InAdminName*/, InPassword;
|
|
local byte InTeam;
|
|
local bool bSpectator, bAdmin, bPerfTesting;
|
|
local rotator SpawnRotation;
|
|
local UniqueNetId ZeroId;
|
|
|
|
bAdmin = false;
|
|
|
|
// Kick the player if they joined during the handshake process
|
|
if (bUsingArbitration && bHasArbitratedHandshakeBegun)
|
|
{
|
|
`if(`__TW_)
|
|
ErrorMessage = "<Strings:"$PathName(WorldInfo.Game.GameMessageClass) $ ".ArbitrationMessage>";
|
|
`else
|
|
ErrorMessage = PathName(WorldInfo.Game.GameMessageClass) $ ".ArbitrationMessage";
|
|
`endif
|
|
return None;
|
|
}
|
|
|
|
if ( BaseMutator != None )
|
|
BaseMutator.ModifyLogin(Portal, Options);
|
|
|
|
bPerfTesting = ( ParseOption( Options, "AutomatedPerfTesting" ) ~= "1" );
|
|
bSpectator = bPerfTesting || ( ParseOption( Options, "SpectatorOnly" ) ~= "1" );
|
|
|
|
// Get URL options.
|
|
`if(`__TW_)
|
|
//@HSL_BEGIN_XBOX
|
|
InName = ParseOption ( Options, "Name");
|
|
// Steam names can contain up to 32 characters
|
|
InName = Left(ParseOption ( Options, "Name"), 32);
|
|
class'GameEngine'.static.DecodeURLString(InName);
|
|
//@HSL_END_XBOX
|
|
`else
|
|
InName = Left(ParseOption ( Options, "Name"), 20);
|
|
`endif
|
|
InTeam = GetIntOption( Options, "Team", 255 ); // default to "no team"
|
|
//InAdminName= ParseOption ( Options, "AdminName");
|
|
InPassword = ParseOption ( Options, "Password" );
|
|
//InChecksum = ParseOption ( Options, "Checksum" );
|
|
|
|
if ( AccessControl != None )
|
|
{
|
|
bAdmin = AccessControl.ParseAdminOptions(Options);
|
|
}
|
|
|
|
// Make sure there is capacity except for admins. (This might have changed since the PreLogin call).
|
|
if ( !bAdmin && AtCapacity(bSpectator) )
|
|
{
|
|
`if(`__TW_)
|
|
ErrorMessage = "<Strings:"$PathName(WorldInfo.Game.GameMessageClass) $ ".MaxedOutMessage>";
|
|
`else
|
|
ErrorMessage = PathName(WorldInfo.Game.GameMessageClass) $ ".MaxedOutMessage";
|
|
`endif
|
|
return None;
|
|
}
|
|
|
|
// if this player is banned, kick him
|
|
if( ( WorldInfo.Game.AccessControl != none ) && (WorldInfo.Game.AccessControl.IsIDBanned(UniqueId)) )
|
|
{
|
|
`Log(InName @ "is banned, rejecting...");
|
|
`if(`__TW_)
|
|
ErrorMessage = "<Strings:Engine.AccessControl.SessionBanned>";
|
|
`else
|
|
ErrorMessage = "Engine.AccessControl.SessionBanned";
|
|
`endif
|
|
return None;
|
|
}
|
|
|
|
// If admin, force spectate mode if the server already full of reg. players
|
|
if ( bAdmin && AtCapacity(false) )
|
|
{
|
|
bSpectator = true;
|
|
}
|
|
|
|
// Pick a team (if need teams)
|
|
`if(`__TW_)
|
|
InTeam = PickTeam(InTeam,None,UniqueID);
|
|
`else
|
|
InTeam = PickTeam(InTeam,None);
|
|
`endif
|
|
// Find a start spot.
|
|
StartSpot = FindPlayerStart( None, InTeam, Portal );
|
|
|
|
if( StartSpot == None )
|
|
{
|
|
`if(`__TW_)
|
|
ErrorMessage = "<Strings:"$PathName(WorldInfo.Game.GameMessageClass) $ ".FailedPlaceMessage>";
|
|
`else
|
|
ErrorMessage = PathName(WorldInfo.Game.GameMessageClass) $ ".FailedPlaceMessage";
|
|
`endif
|
|
return None;
|
|
}
|
|
|
|
SpawnRotation.Yaw = StartSpot.Rotation.Yaw;
|
|
NewPlayer = SpawnPlayerController(StartSpot.Location, SpawnRotation);
|
|
|
|
// Handle spawn failure.
|
|
if( NewPlayer == None )
|
|
{
|
|
`log("Couldn't spawn player controller of class "$PlayerControllerClass);
|
|
`if(`__TW_)
|
|
ErrorMessage = "<Strings:"$PathName(WorldInfo.Game.GameMessageClass) $ ".FailedSpawnMessage>";
|
|
`else
|
|
ErrorMessage = PathName(WorldInfo.Game.GameMessageClass) $ ".FailedSpawnMessage";
|
|
`endif
|
|
return None;
|
|
}
|
|
|
|
NewPlayer.StartSpot = StartSpot;
|
|
|
|
// Set the player's ID.
|
|
NewPlayer.PlayerReplicationInfo.PlayerID = GetNextPlayerID();
|
|
|
|
// If the access control is currently authenticating the players UID, don't store the UID until it is authenticated
|
|
if (AccessControl == none || !AccessControl.IsPendingAuth(UniqueId))
|
|
{
|
|
NewPlayer.PlayerReplicationInfo.SetUniqueId(UniqueId);
|
|
}
|
|
|
|
if (OnlineSub != None &&
|
|
OnlineSub.GameInterface != None &&
|
|
UniqueId != ZeroId)
|
|
{
|
|
// Go ahead and register the player as part of the session
|
|
WorldInfo.Game.OnlineSub.GameInterface.RegisterPlayer(PlayerReplicationInfoClass.default.SessionName, UniqueId, HasOption(Options, "bIsFromInvite"));
|
|
}
|
|
// Now that the unique id is replicated, this player can contribute to skill
|
|
RecalculateSkillRating();
|
|
|
|
// Init player's name
|
|
if( InName=="" )
|
|
{
|
|
InName=DefaultPlayerName$NewPlayer.PlayerReplicationInfo.PlayerID;
|
|
}
|
|
|
|
ChangeName( NewPlayer, InName, false );
|
|
|
|
InCharacter = ParseOption(Options, "Character");
|
|
NewPlayer.SetCharacter(InCharacter);
|
|
|
|
if ( bSpectator || NewPlayer.PlayerReplicationInfo.bOnlySpectator || !ChangeTeam(newPlayer, InTeam, false) )
|
|
{
|
|
NewPlayer.GotoState('Spectating');
|
|
NewPlayer.PlayerReplicationInfo.bOnlySpectator = true;
|
|
NewPlayer.PlayerReplicationInfo.bIsSpectator = true;
|
|
NewPlayer.PlayerReplicationInfo.bOutOfLives = true;
|
|
return NewPlayer;
|
|
}
|
|
|
|
// perform auto-login if admin password/name was passed on the url
|
|
if ( AccessControl != None && AccessControl.AdminLogin(NewPlayer, InPassword) )
|
|
{
|
|
AccessControl.AdminEntered(NewPlayer);
|
|
}
|
|
|
|
|
|
// if delayed start, don't give a pawn to the player yet
|
|
// Normal for multiplayer games
|
|
if ( bDelayedStart )
|
|
{
|
|
// @todo ib2merge: Chair had this commented out
|
|
NewPlayer.GotoState('PlayerWaiting');
|
|
return NewPlayer;
|
|
}
|
|
|
|
return newPlayer;
|
|
}
|
|
|
|
`if(`__TW_)
|
|
static function bool AllowAnalyticsLogging();
|
|
|
|
function ScoreDamage( int DamageAmount, int HealthBeforeDamage, Controller InstigatedBy, Pawn DamagedPawn, class<DamageType> DamageType );
|
|
function ScoreHeal( int HealAmount, int HealthBeforeHeal, Controller InstigatedBy, Pawn HealedPawn, class<DamageType> DamageType );
|
|
|
|
// OnlineSubsystem
|
|
event bool SeatPlayer(const UniqueNetId SeatedPlayerID);
|
|
event MakeReservations(const string URLOptions, const UniqueNetId PlayerId, out string OutError);
|
|
event bool ConfirmReservation(const UniqueNetId PlayerID);
|
|
|
|
native function SetNeedsRestart();
|
|
native function SetNeedsReload();
|
|
`endif
|
|
|
|
/* StartMatch()
|
|
Start the game - inform all actors that the match is starting, and spawn player pawns
|
|
*/
|
|
function StartMatch()
|
|
{
|
|
local Actor A;
|
|
|
|
if ( MyAutoTestManager != None )
|
|
{
|
|
MyAutoTestManager.StartMatch();
|
|
}
|
|
|
|
// tell all actors the game is starting
|
|
ForEach AllActors(class'Actor', A)
|
|
{
|
|
A.MatchStarting();
|
|
}
|
|
|
|
// start human players first
|
|
StartHumans();
|
|
|
|
// start AI players
|
|
StartBots();
|
|
|
|
bWaitingToStartMatch = false;
|
|
|
|
StartOnlineGame();
|
|
|
|
// fire off any level startup events
|
|
WorldInfo.NotifyMatchStarted();
|
|
}
|
|
|
|
/**
|
|
* Tells the online system to start the game and waits for the callback. Tells
|
|
* each connected client to mark their session as in progress
|
|
*/
|
|
function StartOnlineGame()
|
|
{
|
|
local PlayerController PC;
|
|
|
|
if (GameInterface != None)
|
|
{
|
|
// Tell clients to mark their game as started
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
// Skip notifying local PCs as they are handled automatically
|
|
if (!PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientStartOnlineGame();
|
|
}
|
|
}
|
|
// Register the start callback so that the stat guid can be read
|
|
GameInterface.AddStartOnlineGameCompleteDelegate(OnStartOnlineGameComplete);
|
|
// Start the game locally and wait for it to complete
|
|
GameInterface.StartOnlineGame(PlayerReplicationInfoClass.default.SessionName);
|
|
}
|
|
else
|
|
{
|
|
// Notify all clients that the match has begun
|
|
GameReplicationInfo.StartMatch();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback when the start completes
|
|
*
|
|
* @param SessionName the name of the session this is for
|
|
* @param bWasSuccessful true if it worked, false otherwise
|
|
*/
|
|
function OnStartOnlineGameComplete(name SessionName,bool bWasSuccessful)
|
|
{
|
|
local PlayerController PC;
|
|
local string StatGuid;
|
|
|
|
GameInterface.ClearStartOnlineGameCompleteDelegate(OnStartOnlineGameComplete);
|
|
if (bWasSuccessful && OnlineSub.StatsInterface != None)
|
|
{
|
|
// Get the stat guid for the server
|
|
StatGuid = OnlineSub.StatsInterface.GetHostStatGuid();
|
|
if (StatGuid != "")
|
|
{
|
|
// Send the stat guid to all clients
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsLocalPlayerController() == false)
|
|
{
|
|
PC.ClientRegisterHostStatGuid(StatGuid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify all clients that the match has begun
|
|
GameReplicationInfo.StartMatch();
|
|
}
|
|
|
|
function StartHumans()
|
|
{
|
|
local PlayerController P;
|
|
|
|
foreach WorldInfo.AllControllers(class'PlayerController', P)
|
|
{
|
|
if (P.Pawn == None)
|
|
{
|
|
if ( bGameEnded )
|
|
{
|
|
return; // telefrag ended the game with ridiculous frag limit
|
|
}
|
|
else if (P.CanRestartPlayer())
|
|
{
|
|
RestartPlayer(P);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function StartBots()
|
|
{
|
|
local Controller P;
|
|
|
|
foreach WorldInfo.AllControllers(class'Controller', P)
|
|
{
|
|
if (P.bIsPlayer && !P.IsA('PlayerController'))
|
|
{
|
|
if (WorldInfo.NetMode == NM_Standalone)
|
|
{
|
|
RestartPlayer(P);
|
|
}
|
|
else
|
|
{
|
|
P.GotoState('Dead','MPStart');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// Restart a player.
|
|
//
|
|
function RestartPlayer(Controller NewPlayer)
|
|
{
|
|
local NavigationPoint startSpot;
|
|
local int TeamNum, Idx;
|
|
local array<SequenceObject> Events;
|
|
local SeqEvent_PlayerSpawned SpawnedEvent;
|
|
local LocalPlayer LP;
|
|
local PlayerController PC;
|
|
|
|
if( bRestartLevel && WorldInfo.NetMode!=NM_DedicatedServer && WorldInfo.NetMode!=NM_ListenServer )
|
|
{
|
|
`warn("bRestartLevel && !server, abort from RestartPlayer"@WorldInfo.NetMode);
|
|
return;
|
|
}
|
|
// figure out the team number and find the start spot
|
|
TeamNum = ((NewPlayer.PlayerReplicationInfo == None) || (NewPlayer.PlayerReplicationInfo.Team == None)) ? 255 : NewPlayer.PlayerReplicationInfo.Team.TeamIndex;
|
|
StartSpot = FindPlayerStart(NewPlayer, TeamNum);
|
|
|
|
// if a start spot wasn't found,
|
|
if (startSpot == None)
|
|
{
|
|
// check for a previously assigned spot
|
|
if (NewPlayer.StartSpot != None)
|
|
{
|
|
StartSpot = NewPlayer.StartSpot;
|
|
`warn("Player start not found, using last start spot");
|
|
}
|
|
else
|
|
{
|
|
// otherwise abort
|
|
`warn("Player start not found, failed to restart player");
|
|
return;
|
|
}
|
|
}
|
|
// try to create a pawn to use of the default class for this player
|
|
if (NewPlayer.Pawn == None)
|
|
{
|
|
NewPlayer.Pawn = SpawnDefaultPawnFor(NewPlayer, StartSpot);
|
|
}
|
|
if (NewPlayer.Pawn == None)
|
|
{
|
|
`log("failed to spawn player at "$StartSpot);
|
|
NewPlayer.GotoState('Dead');
|
|
if ( PlayerController(NewPlayer) != None )
|
|
{
|
|
PlayerController(NewPlayer).ClientGotoState('Dead','Begin');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// initialize and start it up
|
|
NewPlayer.Pawn.SetAnchor(startSpot);
|
|
if ( PlayerController(NewPlayer) != None )
|
|
{
|
|
PlayerController(NewPlayer).TimeMargin = -0.1;
|
|
startSpot.AnchoredPawn = None; // SetAnchor() will set this since IsHumanControlled() won't return true for the Pawn yet
|
|
}
|
|
NewPlayer.Pawn.LastStartSpot = PlayerStart(startSpot);
|
|
NewPlayer.Pawn.LastStartTime = WorldInfo.TimeSeconds;
|
|
NewPlayer.Possess(NewPlayer.Pawn, false);
|
|
NewPlayer.Pawn.PlayTeleportEffect(true, true);
|
|
NewPlayer.ClientSetRotation(NewPlayer.Pawn.Rotation, TRUE);
|
|
|
|
if (!WorldInfo.bNoDefaultInventoryForPlayer)
|
|
{
|
|
AddDefaultInventory(NewPlayer.Pawn);
|
|
}
|
|
SetPlayerDefaults(NewPlayer.Pawn);
|
|
|
|
// activate spawned events
|
|
if (WorldInfo.GetGameSequence() != None)
|
|
{
|
|
WorldInfo.GetGameSequence().FindSeqObjectsByClass(class'SeqEvent_PlayerSpawned',TRUE,Events);
|
|
for (Idx = 0; Idx < Events.Length; Idx++)
|
|
{
|
|
SpawnedEvent = SeqEvent_PlayerSpawned(Events[Idx]);
|
|
if (SpawnedEvent != None &&
|
|
SpawnedEvent.CheckActivate(NewPlayer,NewPlayer))
|
|
{
|
|
SpawnedEvent.SpawnPoint = startSpot;
|
|
SpawnedEvent.PopulateLinkedVariableValues();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// To fix custom post processing chain when not running in editor or PIE.
|
|
PC = PlayerController(NewPlayer);
|
|
if (PC != none)
|
|
{
|
|
LP = LocalPlayer(PC.Player);
|
|
if(LP != None)
|
|
{
|
|
LP.RemoveAllPostProcessingChains();
|
|
LP.InsertPostProcessingChain(LP.Outer.GetWorldPostProcessChain(),INDEX_NONE,true);
|
|
if(PC.myHUD != None)
|
|
{
|
|
PC.myHUD.NotifyBindPostProcessEffects();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a pawn of the default pawn class
|
|
*
|
|
* @param NewPlayer - Controller for whom this pawn is spawned
|
|
* @param StartSpot - PlayerStart at which to spawn pawn
|
|
*
|
|
* @return pawn
|
|
*/
|
|
function Pawn SpawnDefaultPawnFor(Controller NewPlayer, NavigationPoint StartSpot)
|
|
{
|
|
local class<Pawn> DefaultPlayerClass;
|
|
local Rotator StartRotation;
|
|
local Pawn ResultPawn;
|
|
|
|
DefaultPlayerClass = GetDefaultPlayerClass(NewPlayer);
|
|
|
|
// don't allow pawn to be spawned with any pitch or roll
|
|
StartRotation.Yaw = StartSpot.Rotation.Yaw;
|
|
|
|
ResultPawn = Spawn(DefaultPlayerClass,,,StartSpot.Location,StartRotation);
|
|
if ( ResultPawn == None )
|
|
{
|
|
`log("Couldn't spawn player of type "$DefaultPlayerClass$" at "$StartSpot);
|
|
}
|
|
return ResultPawn;
|
|
}
|
|
|
|
/**
|
|
* Returns the default pawn class for the specified controller,
|
|
*
|
|
* @param C - controller to figure out pawn class for
|
|
*
|
|
* @return default pawn class
|
|
*/
|
|
function class<Pawn> GetDefaultPlayerClass(Controller C)
|
|
{
|
|
// default to the game specified pawn class
|
|
return DefaultPawnClass;
|
|
}
|
|
|
|
/** replicates the current level streaming status to the given PlayerController */
|
|
function ReplicateStreamingStatus(PlayerController PC)
|
|
{
|
|
local int LevelIndex;
|
|
local LevelStreaming TheLevel;
|
|
|
|
// don't do this for local players or players after the first on a splitscreen client
|
|
if (LocalPlayer(PC.Player) == None && ChildConnection(PC.Player) == None)
|
|
{
|
|
// if we've loaded levels via CommitMapChange() that aren't normally in the StreamingLevels array, tell the client about that
|
|
if (WorldInfo.CommittedPersistentLevelName != 'None')
|
|
{
|
|
PC.ClientPrepareMapChange(WorldInfo.CommittedPersistentLevelName, true, true);
|
|
// tell the client to commit the level immediately
|
|
PC.ClientCommitMapChange();
|
|
}
|
|
|
|
if (WorldInfo.StreamingLevels.length > 0)
|
|
{
|
|
// Tell the player controller the current streaming level status
|
|
for (LevelIndex = 0; LevelIndex < WorldInfo.StreamingLevels.Length; LevelIndex++)
|
|
{
|
|
// streamingServer
|
|
TheLevel = WorldInfo.StreamingLevels[LevelIndex];
|
|
|
|
if( TheLevel != none )
|
|
{
|
|
`log( "levelStatus: " $ TheLevel.PackageName $ " "
|
|
$ TheLevel.bShouldBeVisible $ " "
|
|
$ TheLevel.bIsVisible $ " "
|
|
$ TheLevel.bShouldBeLoaded $ " "
|
|
$ TheLevel.LoadedLevel $ " "
|
|
$ TheLevel.bHasLoadRequestPending $ " "
|
|
) ;
|
|
|
|
PC.ClientUpdateLevelStreamingStatus(
|
|
TheLevel.PackageName,
|
|
TheLevel.bShouldBeLoaded,
|
|
TheLevel.bShouldBeVisible,
|
|
TheLevel.bShouldBlockOnLoad);
|
|
}
|
|
}
|
|
PC.ClientFlushLevelStreaming();
|
|
}
|
|
|
|
// if we're preparing to load different levels using PrepareMapChange() inform the client about that now
|
|
if (WorldInfo.PreparingLevelNames.length > 0)
|
|
{
|
|
for (LevelIndex = 0; LevelIndex < WorldInfo.PreparingLevelNames.length; LevelIndex++)
|
|
{
|
|
PC.ClientPrepareMapChange(WorldInfo.PreparingLevelNames[LevelIndex], LevelIndex == 0, LevelIndex == WorldInfo.PreparingLevelNames.length - 1);
|
|
}
|
|
// DO NOT commit these changes yet - we'll send that when we're done preparing them
|
|
}
|
|
}
|
|
}
|
|
|
|
/** handles all player initialization that is shared between the travel methods
|
|
* (i.e. called from both PostLogin() and HandleSeamlessTravelPlayer())
|
|
*/
|
|
function GenericPlayerInitialization(Controller C)
|
|
{
|
|
local PlayerController PC;
|
|
|
|
PC = PlayerController(C);
|
|
if (PC != None)
|
|
{
|
|
// Keep track of the best host to migrate to in case of a disconnect
|
|
UpdateBestNextHosts();
|
|
|
|
// Notify the game that we can now be muted and mute others
|
|
UpdateGameplayMuteList(PC);
|
|
|
|
// tell client what hud class to use
|
|
PC.ClientSetHUD(HudType);
|
|
|
|
// tell client what secondary hud class to use
|
|
PC.ClientSetSecondaryHUD(SecondaryHudType);
|
|
|
|
ReplicateStreamingStatus(PC);
|
|
|
|
// see if we need to spawn a CoverReplicator for this player
|
|
if (CoverReplicatorBase != None)
|
|
{
|
|
PC.SpawnCoverReplicator();
|
|
}
|
|
|
|
// Set the rich presence strings on the client (has to be done there)
|
|
PC.ClientSetOnlineStatus();
|
|
}
|
|
|
|
if (BaseMutator != None)
|
|
{
|
|
BaseMutator.NotifyLogin(C);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sort the list of best hosts. Clients with the most peer connections come first.
|
|
* Then sort based on time of join so that newest players are preferred.
|
|
*
|
|
* @param A first item to compare
|
|
* @param B second item to compare
|
|
* @return 0 if A==B, < 0 if A < B, > 0 if A > B
|
|
*/
|
|
function int BestNextHostSort(PlayerController A, PlayerController B)
|
|
{
|
|
local int Result;
|
|
|
|
if (A.ConnectedPeers.Length == B.ConnectedPeers.Length &&
|
|
A.PlayerReplicationInfo != None && B.PlayerReplicationInfo != None)
|
|
{
|
|
// sort by newest start time
|
|
Result = FCeil(B.PlayerReplicationInfo.StartTime) - FCeil(A.PlayerReplicationInfo.StartTime);
|
|
}
|
|
else
|
|
{
|
|
// sort by largest connected peers
|
|
Result = A.ConnectedPeers.Length - B.ConnectedPeers.Length;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
/**
|
|
* Updates the list of best next hosts on the current server and also replicates this list to all clients.
|
|
*/
|
|
function UpdateBestNextHosts()
|
|
{
|
|
local PlayerController PC;
|
|
local array<PlayerController> SortedPCList;
|
|
local UniqueNetId SortedPlayerIdList[10];
|
|
local UniqueNetId ZeroId;
|
|
local int Idx,NumEntries;
|
|
|
|
// copy list of remote PCs
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (!PC.IsLocalPlayerController() &&
|
|
PC.PlayerReplicationInfo != None &&
|
|
PC.PlayerReplicationInfo.UniqueId != ZeroId &&
|
|
PC.IsPrimaryPlayer())
|
|
{
|
|
SortedPCList.AddItem(PC);
|
|
}
|
|
}
|
|
// sort list of PCs from best to worst host
|
|
SortedPCList.Sort(BestNextHostSort);
|
|
|
|
// copy to list of unique net ids
|
|
NumEntries = Min(SortedPCList.Length,10);
|
|
for (Idx=0; Idx < NumEntries; Idx++)
|
|
{
|
|
SortedPlayerIdList[Idx] = SortedPCList[Idx].PlayerReplicationInfo.UniqueId;
|
|
}
|
|
|
|
// send list of best hosts to clients
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (!PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientUpdateBestNextHosts(SortedPlayerIdList,NumEntries);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Called after a successful login. This is the first place
|
|
// it is safe to call replicated functions on the PlayerController.
|
|
//
|
|
event PostLogin( PlayerController NewPlayer )
|
|
{
|
|
local string Address, StatGuid;
|
|
local int pos, i;
|
|
local Sequence GameSeq;
|
|
local array<SequenceObject> AllInterpActions;
|
|
|
|
local int HidePlayer, HideHud, DisableMovement, DisableTurning, DisableInput;
|
|
|
|
// update player count
|
|
if (NewPlayer.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
NumSpectators++;
|
|
}
|
|
else if (WorldInfo.IsInSeamlessTravel() || NewPlayer.HasClientLoadedCurrentWorld())
|
|
{
|
|
NumPlayers++;
|
|
}
|
|
else
|
|
{
|
|
NumTravellingPlayers++;
|
|
}
|
|
//@SABER_EGS_BEGIN Crossplay support
|
|
if (WorldInfo.IsEOSDedicatedServer() && NewPlayer.bIsEosPlayer && !NewPlayer.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
NumEosPlayers++;
|
|
}
|
|
//@SABER_EGS_END
|
|
// Tell the online subsystem the number of players in the game
|
|
UpdateGameSettingsCounts();
|
|
|
|
// save network address for re-associating with reconnecting player, after stripping out port number
|
|
Address = NewPlayer.GetPlayerNetworkAddress();
|
|
pos = InStr(Address,":");
|
|
NewPlayer.PlayerReplicationInfo.SavedNetworkAddress = (pos > 0) ? left(Address,pos) : Address;
|
|
|
|
// check if this player is reconnecting and already has PRI
|
|
FindInactivePRI(NewPlayer);
|
|
|
|
if ( !bDelayedStart )
|
|
{
|
|
// start match, or let player enter, immediately
|
|
bRestartLevel = false; // let player spawn once in levels that must be restarted after every death
|
|
if ( bWaitingToStartMatch )
|
|
StartMatch();
|
|
else
|
|
RestartPlayer(newPlayer);
|
|
bRestartLevel = Default.bRestartLevel;
|
|
}
|
|
|
|
if (NewPlayer.Pawn != None)
|
|
{
|
|
NewPlayer.Pawn.ClientSetRotation(NewPlayer.Pawn.Rotation);
|
|
}
|
|
|
|
NewPlayer.ClientCapBandwidth(NewPlayer.Player.CurrentNetSpeed);
|
|
UpdateNetSpeeds();
|
|
|
|
GenericPlayerInitialization(NewPlayer);
|
|
|
|
// Tell the new player the stat guid
|
|
if (GameReplicationInfo.bMatchHasBegun && OnlineSub != None && OnlineSub.StatsInterface != None)
|
|
{
|
|
// Get the stat guid for the server
|
|
StatGuid = OnlineSub.StatsInterface.GetHostStatGuid();
|
|
if (StatGuid != "")
|
|
{
|
|
NewPlayer.ClientRegisterHostStatGuid(StatGuid);
|
|
}
|
|
}
|
|
|
|
// HSL - BWJ - 3-18-16 - Push to talk for non console build sonly
|
|
// Tell the player to disable voice by default and use the push to talk method
|
|
if (bRequiresPushToTalk && !WorldInfo.IsConsoleDedicatedServer())
|
|
{
|
|
NewPlayer.ClientStopNetworkedVoice();
|
|
}
|
|
else
|
|
{
|
|
NewPlayer.ClientStartNetworkedVoice();
|
|
}
|
|
|
|
if (NewPlayer.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
NewPlayer.ClientGotoState('Spectating');
|
|
}
|
|
|
|
// add the player to any matinees running so that it gets in on any cinematics already running, etc
|
|
GameSeq = WorldInfo.GetGameSequence();
|
|
if (GameSeq != None)
|
|
{
|
|
// find any matinee actions that exist
|
|
GameSeq.FindSeqObjectsByClass(class'SeqAct_Interp', true, AllInterpActions);
|
|
|
|
// tell them all to add this PC to any running Director tracks
|
|
for (i = 0; i < AllInterpActions.Length; i++)
|
|
{
|
|
SeqAct_Interp(AllInterpActions[i]).AddPlayerToDirectorTracks(NewPlayer);
|
|
}
|
|
}
|
|
|
|
//Check to see if we should start in cinematic mode (matinee movie capture)
|
|
if(ShouldStartInCinematicMode(HidePlayer, HideHud, DisableMovement, DisableTurning, DisableInput))
|
|
{
|
|
NewPlayer.SetCinematicMode(true, HidePlayer == 1, HideHud == 1, DisableMovement == 1, DisableTurning == 1, DisableInput == 1);
|
|
}
|
|
|
|
// Pass on to access control
|
|
if (AccessControl != none)
|
|
{
|
|
AccessControl.PostLogin(NewPlayer);
|
|
}
|
|
}
|
|
|
|
function UpdateNetSpeeds()
|
|
{
|
|
local int NewNetSpeed;
|
|
local PlayerController PC;
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
if (GameInterface != None)
|
|
{
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
}
|
|
|
|
if ( (WorldInfo.NetMode == NM_DedicatedServer) || (WorldInfo.NetMode == NM_Standalone) || (GameSettings != None && GameSettings.bIsLanMatch) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( WorldInfo.TimeSeconds - LastNetSpeedUpdateTime < 1.0 )
|
|
{
|
|
SetTimer( 1.0, false, nameof(UpdateNetSpeeds) );
|
|
return;
|
|
}
|
|
|
|
LastNetSpeedUpdateTime = WorldInfo.TimeSeconds;
|
|
|
|
NewNetSpeed = CalculatedNetSpeed();
|
|
`log("New Dynamic NetSpeed "$NewNetSpeed$" vs old "$AdjustedNetSpeed,,'DevNet');
|
|
|
|
if ( AdjustedNetSpeed != NewNetSpeed )
|
|
{
|
|
AdjustedNetSpeed = NewNetSpeed;
|
|
ForEach WorldInfo.AllControllers(class'PlayerController', PC)
|
|
{
|
|
PC.SetNetSpeed(AdjustedNetSpeed);
|
|
}
|
|
}
|
|
}
|
|
|
|
function int CalculatedNetSpeed()
|
|
{
|
|
return Clamp(TotalNetBandwidth/Max(NumPlayers,1), MinDynamicBandwidth, MaxDynamicBandwidth);
|
|
}
|
|
|
|
/**
|
|
* Engine is shutting down.
|
|
*/
|
|
event PreExit()
|
|
{
|
|
if (AccessControl != none)
|
|
{
|
|
AccessControl.NotifyExit();
|
|
}
|
|
|
|
ClearOnlineDelegates();
|
|
}
|
|
|
|
//
|
|
// Player exits.
|
|
//
|
|
function Logout( Controller Exiting )
|
|
{
|
|
local PlayerController PC;
|
|
local int PCIndex;
|
|
|
|
PC = PlayerController(Exiting);
|
|
if ( PC != None )
|
|
{
|
|
if (AccessControl != None &&
|
|
AccessControl.AdminLogout( PlayerController(Exiting) ))
|
|
{
|
|
AccessControl.AdminExited( PlayerController(Exiting) );
|
|
}
|
|
|
|
if ( PC.PlayerReplicationInfo.bOnlySpectator )
|
|
{
|
|
NumSpectators--;
|
|
}
|
|
else
|
|
{
|
|
if (WorldInfo.IsInSeamlessTravel() || PC.HasClientLoadedCurrentWorld())
|
|
{
|
|
NumPlayers--;
|
|
}
|
|
else
|
|
{
|
|
NumTravellingPlayers--;
|
|
}
|
|
//@SABER_EGS_BEGIN Crossplay support
|
|
if (WorldInfo.IsEOSDedicatedServer() && PC.bIsEosPlayer)
|
|
{
|
|
NumEosPlayers--;
|
|
}
|
|
//@SABER_EGS_END
|
|
// Tell the online subsystem the number of players in the game
|
|
UpdateGameSettingsCounts();
|
|
}
|
|
// This person has left during an arbitration period
|
|
if (bUsingArbitration && bHasArbitratedHandshakeBegun && !bHasEndGameHandshakeBegun)
|
|
{
|
|
`Log("Player "$PC.PlayerReplicationInfo.PlayerName$" has dropped");
|
|
}
|
|
// Unregister the player from the online layer
|
|
UnregisterPlayer(PC);
|
|
// Remove from the arbitrated PC list if in an arbitrated match
|
|
if (bUsingArbitration)
|
|
{
|
|
// Find the PC in the list and remove it if found
|
|
PCIndex = ArbitrationPCs.Find(PC);
|
|
if (PCIndex != INDEX_NONE)
|
|
{
|
|
ArbitrationPCs.Remove(PCIndex,1);
|
|
}
|
|
}
|
|
}
|
|
//notify mutators that a player exited
|
|
if (BaseMutator != None)
|
|
{
|
|
BaseMutator.NotifyLogout(Exiting);
|
|
}
|
|
if ( PC != None )
|
|
{
|
|
UpdateNetSpeeds();
|
|
}
|
|
}
|
|
|
|
simulated function ExileServerUsingKickBan()
|
|
{
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
if (GameInterface != None)
|
|
{
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
}
|
|
|
|
if (GameSettings != None)
|
|
{
|
|
GameSettings.bServerExiled = true;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Removes the player from the named session when they leave
|
|
*
|
|
* @param PC the player controller that just left
|
|
*/
|
|
function UnregisterPlayer(PlayerController PC)
|
|
{
|
|
local UniqueNetId ZeroId;
|
|
|
|
// If there is a session that matches the name, unregister this remote player
|
|
if (WorldInfo.NetMode != NM_Standalone &&
|
|
GameInterface != None &&
|
|
PC.PlayerReplicationInfo.UniqueId != ZeroId &&
|
|
GameInterface.GetGameSettings(PC.PlayerReplicationInfo.SessionName) != None)
|
|
{
|
|
// Unregister the player from the session
|
|
GameInterface.UnregisterPlayer(PC.PlayerReplicationInfo.SessionName,PC.PlayerReplicationInfo.UniqueId);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Examine the passed player's inventory, and accept or discard each item.
|
|
// AcceptInventory needs to gracefully handle the case of some inventory
|
|
// being accepted but other inventory not being accepted (such as the default
|
|
// weapon). There are several things that can go wrong: A weapon's
|
|
// AmmoType not being accepted but the weapon being accepted -- the weapon
|
|
// should be killed off. Or the player's selected inventory item, active
|
|
// weapon, etc. not being accepted, leaving the player weaponless or leaving
|
|
// the HUD inventory rendering messed up (AcceptInventory should pick another
|
|
// applicable weapon/item as current).
|
|
//
|
|
event AcceptInventory(pawn PlayerPawn)
|
|
{
|
|
//default accept all inventory except default weapon (spawned explicitly)
|
|
}
|
|
|
|
//
|
|
// Spawn any default inventory for the player.
|
|
//
|
|
event AddDefaultInventory(Pawn P)
|
|
{
|
|
// Allow the pawn itself to modify its inventory
|
|
P.AddDefaultInventory();
|
|
|
|
if ( P.InvManager == None )
|
|
{
|
|
`warn("GameInfo::AddDefaultInventory - P.InvManager == None");
|
|
}
|
|
}
|
|
|
|
/* Mutate()
|
|
Pass an input string to the mutator list. Used by PlayerController.Mutate(), intended to allow
|
|
mutators to have input exec functions (by binding mutate xxx to keys)
|
|
*/
|
|
function Mutate(string MutateString, PlayerController Sender)
|
|
{
|
|
if ( BaseMutator != None )
|
|
BaseMutator.Mutate(MutateString, Sender);
|
|
}
|
|
|
|
/* SetPlayerDefaults()
|
|
first make sure pawn properties are back to default, then give mutators an opportunity
|
|
to modify them
|
|
*/
|
|
function SetPlayerDefaults(Pawn PlayerPawn)
|
|
{
|
|
PlayerPawn.AirControl = PlayerPawn.Default.AirControl;
|
|
PlayerPawn.GroundSpeed = PlayerPawn.Default.GroundSpeed;
|
|
PlayerPawn.WaterSpeed = PlayerPawn.Default.WaterSpeed;
|
|
PlayerPawn.AirSpeed = PlayerPawn.Default.AirSpeed;
|
|
PlayerPawn.Acceleration = PlayerPawn.Default.Acceleration;
|
|
PlayerPawn.AccelRate = PlayerPawn.Default.AccelRate;
|
|
PlayerPawn.JumpZ = PlayerPawn.Default.JumpZ;
|
|
if ( BaseMutator != None )
|
|
BaseMutator.ModifyPlayer(PlayerPawn);
|
|
PlayerPawn.PhysicsVolume.ModifyPlayer(PlayerPawn);
|
|
}
|
|
|
|
function NotifyKilled(Controller Killer, Controller Killed, Pawn KilledPawn, class<DamageType> damageType )
|
|
{
|
|
local Controller C;
|
|
|
|
foreach WorldInfo.AllControllers(class'Controller', C)
|
|
{
|
|
C.NotifyKilled(Killer, Killed, KilledPawn, damageType);
|
|
}
|
|
}
|
|
|
|
function Killed( Controller Killer, Controller KilledPlayer, Pawn KilledPawn, class<DamageType> damageType )
|
|
{
|
|
if( KilledPlayer != None && KilledPlayer.bIsPlayer )
|
|
{
|
|
KilledPlayer.PlayerReplicationInfo.IncrementDeaths();
|
|
KilledPlayer.PlayerReplicationInfo.SetNetUpdateTime(FMin(KilledPlayer.PlayerReplicationInfo.NetUpdateTime, WorldInfo.TimeSeconds + 0.3 * FRand()));
|
|
BroadcastDeathMessage(Killer, KilledPlayer, damageType);
|
|
}
|
|
|
|
if( KilledPlayer != None )
|
|
{
|
|
ScoreKill(Killer, KilledPlayer);
|
|
}
|
|
|
|
DiscardInventory(KilledPawn, Killer);
|
|
NotifyKilled(Killer, KilledPlayer, KilledPawn, damageType);
|
|
}
|
|
|
|
function bool PreventDeath(Pawn KilledPawn, Controller Killer, class<DamageType> DamageType, vector HitLocation)
|
|
{
|
|
if ( BaseMutator == None )
|
|
return false;
|
|
return BaseMutator.PreventDeath(KilledPawn, Killer, DamageType, HitLocation);
|
|
}
|
|
|
|
function BroadcastDeathMessage(Controller Killer, Controller Other, class<DamageType> damageType)
|
|
{
|
|
if ( (Killer == Other) || (Killer == None) )
|
|
{
|
|
BroadcastLocalized(self, DeathMessageClass, 1, None, Other.PlayerReplicationInfo, damageType);
|
|
}
|
|
else
|
|
{
|
|
BroadcastLocalized(self, DeathMessageClass, 0, Killer.PlayerReplicationInfo, Other.PlayerReplicationInfo, damageType);
|
|
}
|
|
}
|
|
|
|
function Kick( string S )
|
|
{
|
|
if (AccessControl != None)
|
|
{
|
|
AccessControl.Kick(S);
|
|
}
|
|
else
|
|
{
|
|
`log("NO ACCESS CONTROL!!!");
|
|
}
|
|
}
|
|
|
|
function KickBan( string S )
|
|
{
|
|
if (AccessControl != None)
|
|
AccessControl.KickBan(S);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Level gameplay modification.
|
|
|
|
//
|
|
// Return whether Viewer is allowed to spectate from the
|
|
// point of view of ViewTarget.
|
|
//
|
|
function bool CanSpectate( PlayerController Viewer, PlayerReplicationInfo ViewTarget )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/* ReduceDamage:
|
|
Use reduce damage for teamplay modifications, etc. */
|
|
`if(`__TW_)
|
|
function ReduceDamage(out int Damage, pawn injured, Controller instigatedBy, vector HitLocation, out vector Momentum, class<DamageType> DamageType, Actor DamageCauser, TraceHitInfo HitInfo)
|
|
`else
|
|
function ReduceDamage(out int Damage, pawn injured, Controller instigatedBy, vector HitLocation, out vector Momentum, class<DamageType> DamageType, Actor DamageCauser)
|
|
`endif
|
|
{
|
|
local int OriginalDamage;
|
|
|
|
OriginalDamage = Damage;
|
|
|
|
if ( injured.PhysicsVolume.bNeutralZone || injured.InGodMode() )
|
|
{
|
|
Damage = 0;
|
|
return;
|
|
}
|
|
|
|
if (BaseMutator != None)
|
|
{
|
|
BaseMutator.NetDamage(OriginalDamage, Damage, Injured, InstigatedBy, HitLocation, Momentum, DamageType, DamageCauser);
|
|
}
|
|
}
|
|
|
|
/* CheckRelevance()
|
|
returns true if actor is relevant to this game and should not be destroyed. Called in Actor.PreBeginPlay(), intended to allow
|
|
mutators to remove or replace actors being spawned
|
|
*/
|
|
function bool CheckRelevance(Actor Other)
|
|
{
|
|
if ( BaseMutator == None )
|
|
return true;
|
|
return BaseMutator.CheckRelevance(Other);
|
|
}
|
|
|
|
/**
|
|
* Return whether an item should respawn. Default implementation allows item respawning in multiplayer games.
|
|
*/
|
|
function bool ShouldRespawn( PickupFactory Other )
|
|
{
|
|
return ( WorldInfo.NetMode != NM_Standalone );
|
|
}
|
|
|
|
/**
|
|
* Called when pawn has a chance to pick Item up (i.e. when
|
|
* the pawn touches a weapon pickup). Should return true if
|
|
* he wants to pick it up, false if he does not want it.
|
|
* @param Other the Pawn that wants the item
|
|
* @param ItemClass the Inventory class the Pawn can pick up
|
|
* @param Pickup the Actor containing that item (this may be a PickupFactory or it may be a DroppedPickup)
|
|
* @return whether or not the Pickup actor should give its item to Other
|
|
*/
|
|
function bool PickupQuery(Pawn Other, class<Inventory> ItemClass, Actor Pickup)
|
|
{
|
|
local byte bAllowPickup;
|
|
|
|
if (BaseMutator != None && BaseMutator.OverridePickupQuery(Other, ItemClass, Pickup, bAllowPickup))
|
|
{
|
|
return bool(bAllowPickup);
|
|
}
|
|
|
|
if ( Other.InvManager == None )
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return Other.InvManager.HandlePickupQuery(ItemClass, Pickup);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Discard a player's inventory after he dies.
|
|
*/
|
|
function DiscardInventory( Pawn Other, optional controller Killer )
|
|
{
|
|
if ( Other.InvManager != None )
|
|
Other.InvManager.DiscardInventory();
|
|
}
|
|
|
|
/* Try to change a player's name.
|
|
*/
|
|
function ChangeName( Controller Other, coerce string S, bool bNameChange )
|
|
{
|
|
if( S == "" )
|
|
{
|
|
return;
|
|
}
|
|
|
|
Other.PlayerReplicationInfo.SetPlayerName(S);
|
|
}
|
|
|
|
/* Return whether a team change is allowed.
|
|
*/
|
|
function bool ChangeTeam(Controller Other, int N, bool bNewTeam)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/* Return a picked team number if none was specified
|
|
*/
|
|
`if(`__TW_)
|
|
function byte PickTeam(byte Current, Controller C, const out UniqueNetId PlayerId)
|
|
`else
|
|
function byte PickTeam(byte Current, Controller C)
|
|
`endif
|
|
{
|
|
return Current;
|
|
}
|
|
|
|
/* Send a player to a URL.
|
|
*/
|
|
function SendPlayer( PlayerController aPlayer, string URL )
|
|
{
|
|
aPlayer.ClientTravel( URL, TRAVEL_Relative );
|
|
}
|
|
|
|
/** @return the map we should travel to for the next game */
|
|
function string GetNextMap();
|
|
|
|
/**
|
|
* Returns true if we want to travel_absolute
|
|
*/
|
|
function bool GetTravelType()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* Restart the game.
|
|
*/
|
|
function RestartGame()
|
|
{
|
|
local string NextMap;
|
|
local string TransitionMapCmdLine;
|
|
local string URLString;
|
|
local int URLMapLen;
|
|
local int MapNameLen;
|
|
|
|
// If we are using arbitration and haven't done the end game handshaking,
|
|
// do that process first and then come back here afterward
|
|
if (bUsingArbitration)
|
|
{
|
|
if (bIsEndGameHandshakeComplete)
|
|
{
|
|
// All arbitrated matches must exit after one match
|
|
NotifyArbitratedMatchEnd();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (BaseMutator != None && BaseMutator.HandleRestartGame())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (bGameRestarted)
|
|
{
|
|
return;
|
|
}
|
|
bGameRestarted = true;
|
|
|
|
// these server travels should all be relative to the current URL
|
|
if ( bChangeLevels && !bAlreadyChanged )
|
|
{
|
|
// get the next map and start the transition
|
|
bAlreadyChanged = true;
|
|
|
|
if ( (MyAutoTestManager != None) && MyAutoTestManager.bUsingAutomatedTestingMapList)
|
|
{
|
|
NextMap = MyAutoTestManager.GetNextAutomatedTestingMap();
|
|
}
|
|
else
|
|
{
|
|
NextMap = GetNextMap();
|
|
}
|
|
|
|
if (NextMap != "")
|
|
{
|
|
if ( (MyAutoTestManager == None) || !MyAutoTestManager.bUsingAutomatedTestingMapList )
|
|
{
|
|
WorldInfo.ServerTravel(NextMap,GetTravelType());
|
|
}
|
|
else
|
|
{
|
|
if ( !MyAutoTestManager.bAutomatedTestingWithOpen )
|
|
{
|
|
URLString = WorldInfo.GetLocalURL();
|
|
URLMapLen = Len(URLString);
|
|
|
|
MapNameLen = InStr(URLString, "?");
|
|
if (MapNameLen != -1)
|
|
{
|
|
URLString = Right(URLString, URLMapLen - MapNameLen);
|
|
}
|
|
|
|
// The ENTIRE url needs to be recreated here...
|
|
TransitionMapCmdLine = NextMap$URLString$"?AutomatedTestingMapIndex="$MyAutoTestManager.AutomatedTestingMapIndex;
|
|
`log(">>> Issuing server travel on " $ TransitionMapCmdLine);
|
|
WorldInfo.ServerTravel(TransitionMapCmdLine,GetTravelType());
|
|
}
|
|
else
|
|
{
|
|
TransitionMapCmdLine = "?AutomatedTestingMapIndex="$MyAutoTestManager.AutomatedTestingMapIndex$"?NumberOfMatchesPlayed="$MyAutoTestManager.NumberOfMatchesPlayed$"?NumMapListCyclesDone="$MyAutoTestManager.NumMapListCyclesDone;
|
|
`log(">>> Issuing open command on " $ NextMap $ TransitionMapCmdLine);
|
|
ConsoleCommand( "open " $ NextMap $ TransitionMapCmdLine);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
`log("GameInfo - RestartGame - Call ServerTravel()");
|
|
WorldInfo.ServerTravel("?Restart",GetTravelType());
|
|
}
|
|
|
|
//==========================================================================
|
|
// Message broadcasting functions (handled by the BroadCastHandler)
|
|
|
|
event Broadcast( Actor Sender, coerce string Msg, optional name Type )
|
|
{
|
|
BroadcastHandler.Broadcast(Sender,Msg,Type);
|
|
}
|
|
|
|
function BroadcastTeam( Controller Sender, coerce string Msg, optional name Type )
|
|
{
|
|
BroadcastHandler.BroadcastTeam(Sender,Msg,Type);
|
|
}
|
|
|
|
/*
|
|
Broadcast a localized message to all players.
|
|
Most message deal with 0 to 2 related PRIs.
|
|
The LocalMessage class defines how the PRI's and optional actor are used.
|
|
*/
|
|
event BroadcastLocalized( actor Sender, class<LocalMessage> Message, optional int Switch, optional PlayerReplicationInfo RelatedPRI_1, optional PlayerReplicationInfo RelatedPRI_2, optional Object OptionalObject )
|
|
{
|
|
BroadcastHandler.AllowBroadcastLocalized(Sender,Message,Switch,RelatedPRI_1,RelatedPRI_2,OptionalObject);
|
|
}
|
|
|
|
/*
|
|
Broadcast a localized message to all players on a team.
|
|
Most message deal with 0 to 2 related PRIs.
|
|
The LocalMessage class defines how the PRI's and optional actor are used.
|
|
*/
|
|
event BroadcastLocalizedTeam( int TeamIndex, actor Sender, class<LocalMessage> Message, optional int Switch, optional PlayerReplicationInfo RelatedPRI_1, optional PlayerReplicationInfo RelatedPRI_2, optional Object OptionalObject )
|
|
{
|
|
BroadcastHandler.AllowBroadcastLocalizedTeam(TeamIndex, Sender,Message,Switch,RelatedPRI_1,RelatedPRI_2,OptionalObject);
|
|
}
|
|
|
|
//==========================================================================
|
|
function bool CheckModifiedEndGame(PlayerReplicationInfo Winner, string Reason)
|
|
{
|
|
return (BaseMutator != None && !BaseMutator.CheckEndGame(Winner, Reason));
|
|
}
|
|
|
|
function bool CheckEndGame(PlayerReplicationInfo Winner, string Reason)
|
|
{
|
|
local Controller P;
|
|
|
|
if ( CheckModifiedEndGame(Winner, Reason) )
|
|
return false;
|
|
|
|
// all player cameras focus on winner or final scene (picked by mutator)
|
|
foreach WorldInfo.AllControllers(class'Controller', P)
|
|
{
|
|
P.GameHasEnded();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Tells all clients to write stats and then handles writing local stats
|
|
*/
|
|
function WriteOnlineStats()
|
|
{
|
|
local PlayerController PC;
|
|
local OnlineGameSettings CurrentSettings;
|
|
|
|
if (GameInterface != None)
|
|
{
|
|
// Make sure that we are recording stats
|
|
CurrentSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
if (CurrentSettings != None && CurrentSettings.bUsesStats)
|
|
{
|
|
// Iterate through the controllers telling them to write stats
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsLocalPlayerController() == false)
|
|
{
|
|
PC.ClientWriteLeaderboardStats(OnlineStatsWriteClass);
|
|
}
|
|
}
|
|
// Iterate through local controllers telling them to write stats
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientWriteLeaderboardStats(OnlineStatsWriteClass);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the match is arbitrated, tells all clients to write out their copies
|
|
* of the player scores. If not arbitrated, it only has the first local player
|
|
* write the scores.
|
|
*/
|
|
function WriteOnlinePlayerScores()
|
|
{
|
|
local PlayerController PC;
|
|
|
|
if (bUsingArbitration)
|
|
{
|
|
// Iterate through the controllers telling them to write stats
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
PC.ClientWriteOnlinePlayerScores(ArbitratedLeaderboardId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Find the first local player and have them write the data
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientWriteOnlinePlayerScores(LeaderboardId);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* End of game.
|
|
*/
|
|
function EndGame( PlayerReplicationInfo Winner, string Reason )
|
|
{
|
|
// don't end game if not really ready
|
|
if ( !CheckEndGame(Winner, Reason) )
|
|
{
|
|
bOverTime = true;
|
|
return;
|
|
}
|
|
|
|
// Allow replication to happen before reporting scores, stats, etc.
|
|
SetTimer( 1.5,false,nameof(PerformEndGameHandling) );
|
|
|
|
bGameEnded = true;
|
|
EndLogging(Reason);
|
|
}
|
|
|
|
/** Does end of game handling for the online layer */
|
|
function PerformEndGameHandling()
|
|
{
|
|
if (GameInterface != None)
|
|
{
|
|
// Write out any online stats
|
|
WriteOnlineStats();
|
|
// Write the player data used in determining skill ratings
|
|
WriteOnlinePlayerScores();
|
|
// Force the stats to flush and change the status of the match
|
|
// to ended making join in progress an option again
|
|
EndOnlineGame();
|
|
// Notify clients that the session has ended for arbitrated
|
|
if (bUsingArbitration)
|
|
{
|
|
PendingArbitrationPCs.Length = 0;
|
|
ArbitrationPCs.Length = 0;
|
|
// Do end of session handling
|
|
NotifyArbitratedMatchEnd();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tells the online system to end the game and tells all clients to do the same
|
|
*/
|
|
function EndOnlineGame()
|
|
{
|
|
local PlayerController PC;
|
|
|
|
GameReplicationInfo.EndGame();
|
|
if (GameInterface != None)
|
|
{
|
|
// Have clients end their games
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
// Skip notifying local PCs as they are handled automatically
|
|
if (!PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientEndOnlineGame();
|
|
}
|
|
}
|
|
// Server is handled here
|
|
GameInterface.EndOnlineGame(PlayerReplicationInfoClass.default.SessionName);
|
|
}
|
|
}
|
|
|
|
/** Entry point of the stats code for custom game logging at a regular interval */
|
|
function GameEventsPoll();
|
|
function EndLogging(string Reason); // Stub function
|
|
|
|
/** returns whether the given Controller StartSpot property should be used as the spawn location for its Pawn */
|
|
function bool ShouldSpawnAtStartSpot(Controller Player)
|
|
{
|
|
return ( WorldInfo.NetMode == NM_Standalone && Player != None && Player.StartSpot != None &&
|
|
(bWaitingToStartMatch || (Player.PlayerReplicationInfo != None && Player.PlayerReplicationInfo.bWaitingPlayer)) );
|
|
}
|
|
|
|
/** FindPlayerStart()
|
|
* Return the 'best' player start for this player to start from. PlayerStarts are rated by RatePlayerStart().
|
|
* @param Player is the controller for whom we are choosing a playerstart
|
|
* @param InTeam specifies the Player's team (if the player hasn't joined a team yet)
|
|
* @param IncomingName specifies the tag of a teleporter to use as the Playerstart
|
|
* @returns NavigationPoint chosen as player start (usually a PlayerStart)
|
|
*/
|
|
function NavigationPoint FindPlayerStart( Controller Player, optional byte InTeam, optional string IncomingName )
|
|
{
|
|
local NavigationPoint N, BestStart;
|
|
local Teleporter Tel;
|
|
|
|
// allow GameRulesModifiers to override playerstart selection
|
|
if (BaseMutator != None)
|
|
{
|
|
N = BaseMutator.FindPlayerStart(Player, InTeam, IncomingName);
|
|
if (N != None)
|
|
{
|
|
return N;
|
|
}
|
|
}
|
|
|
|
// if incoming start is specified, then just use it
|
|
if( incomingName!="" )
|
|
{
|
|
ForEach WorldInfo.AllNavigationPoints( class 'Teleporter', Tel )
|
|
if( string(Tel.Tag)~=incomingName )
|
|
return Tel;
|
|
}
|
|
|
|
// always pick StartSpot at start of match
|
|
`if(`__TW_)
|
|
// Allow ShouldSpawnAtStartSpot() to handle rating - see KFGameInfo
|
|
if ( ShouldSpawnAtStartSpot(Player) )
|
|
`else
|
|
if ( ShouldSpawnAtStartSpot(Player) &&
|
|
(PlayerStart(Player.StartSpot) == None || RatePlayerStart(PlayerStart(Player.StartSpot), InTeam, Player) >= 0.0) )
|
|
`endif
|
|
{
|
|
return Player.StartSpot;
|
|
}
|
|
|
|
BestStart = ChoosePlayerStart(Player, InTeam);
|
|
|
|
if ( (BestStart == None) && (Player == None) )
|
|
{
|
|
// no playerstart found, so pick any NavigationPoint to keep player from failing to enter game
|
|
`log("Warning - PATHS NOT DEFINED or NO PLAYERSTART with positive rating");
|
|
ForEach AllActors( class 'NavigationPoint', N )
|
|
{
|
|
BestStart = N;
|
|
break;
|
|
}
|
|
}
|
|
return BestStart;
|
|
}
|
|
|
|
/** ChoosePlayerStart()
|
|
* Return the 'best' player start for this player to start from. PlayerStarts are rated by RatePlayerStart().
|
|
* @param Player is the controller for whom we are choosing a playerstart
|
|
* @param InTeam specifies the Player's team (if the player hasn't joined a team yet)
|
|
* @returns NavigationPoint chosen as player start (usually a PlayerStart)
|
|
*/
|
|
function PlayerStart ChoosePlayerStart( Controller Player, optional byte InTeam )
|
|
{
|
|
local PlayerStart P, BestStart;
|
|
local float BestRating, NewRating;
|
|
local byte Team;
|
|
|
|
// use InTeam if player doesn't have a team yet
|
|
Team = ( (Player != None) && (Player.PlayerReplicationInfo != None) && (Player.PlayerReplicationInfo.Team != None) )
|
|
? byte(Player.PlayerReplicationInfo.Team.TeamIndex)
|
|
: InTeam;
|
|
|
|
// Find best playerstart
|
|
foreach WorldInfo.AllNavigationPoints(class'PlayerStart', P)
|
|
{
|
|
NewRating = RatePlayerStart(P,Team,Player);
|
|
if ( NewRating > BestRating )
|
|
{
|
|
BestRating = NewRating;
|
|
BestStart = P;
|
|
}
|
|
}
|
|
return BestStart;
|
|
}
|
|
|
|
/** RatePlayerStart()
|
|
* Return a score representing how desireable a playerstart is.
|
|
* @param P is the playerstart being rated
|
|
* @param Team is the team of the player choosing the playerstart
|
|
* @param Player is the controller choosing the playerstart
|
|
* @returns playerstart score
|
|
*/
|
|
function float RatePlayerStart(PlayerStart P, byte Team, Controller Player)
|
|
{
|
|
local float Rating;
|
|
if ( !P.bEnabled )
|
|
{
|
|
return 5.f;
|
|
}
|
|
else
|
|
{
|
|
Rating = 10.f;
|
|
if (P.bPrimaryStart)
|
|
{
|
|
Rating += 10.f;
|
|
}
|
|
if (P.TeamIndex == Team)
|
|
{
|
|
Rating += 15.f;
|
|
}
|
|
return Rating;
|
|
}
|
|
}
|
|
|
|
function AddObjectiveScore(PlayerReplicationInfo Scorer, Int Score)
|
|
{
|
|
if ( Scorer != None )
|
|
{
|
|
Scorer.Score += Score;
|
|
}
|
|
if (BaseMutator != None)
|
|
{
|
|
BaseMutator.ScoreObjective(Scorer, Score);
|
|
}
|
|
}
|
|
|
|
function ScoreObjective(PlayerReplicationInfo Scorer, Int Score)
|
|
{
|
|
AddObjectiveScore(Scorer, Score);
|
|
CheckScore(Scorer);
|
|
}
|
|
|
|
/* CheckScore()
|
|
see if this score means the game ends
|
|
*/
|
|
function bool CheckScore(PlayerReplicationInfo Scorer)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
function ScoreKill(Controller Killer, Controller Other)
|
|
{
|
|
if( (killer == Other) || (killer == None) )
|
|
{
|
|
if ( (Other!=None) && (Other.PlayerReplicationInfo != None) )
|
|
{
|
|
Other.PlayerReplicationInfo.Score -= 1;
|
|
Other.PlayerReplicationInfo.bForceNetUpdate = TRUE;
|
|
}
|
|
}
|
|
else if ( killer.PlayerReplicationInfo != None )
|
|
{
|
|
Killer.PlayerReplicationInfo.Score += 1;
|
|
Killer.PlayerReplicationInfo.bForceNetUpdate = TRUE;
|
|
Killer.PlayerReplicationInfo.Kills++;
|
|
}
|
|
|
|
ModifyScoreKill(Killer, Other);
|
|
|
|
if (Killer != None || MaxLives > 0)
|
|
{
|
|
CheckScore(Killer.PlayerReplicationInfo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For subclasses which don't call GameInfo.ScoreKill()
|
|
*/
|
|
function ModifyScoreKill(Controller Killer, Controller Other)
|
|
{
|
|
if (BaseMutator != None)
|
|
{
|
|
BaseMutator.ScoreKill(Killer, Other);
|
|
}
|
|
}
|
|
|
|
function DriverEnteredVehicle(Vehicle V, Pawn P)
|
|
{
|
|
if ( BaseMutator != None )
|
|
BaseMutator.DriverEnteredVehicle(V, P);
|
|
}
|
|
|
|
function bool CanLeaveVehicle(Vehicle V, Pawn P)
|
|
{
|
|
if ( BaseMutator == None )
|
|
return true;
|
|
return BaseMutator.CanLeaveVehicle(V, P);
|
|
}
|
|
|
|
function DriverLeftVehicle(Vehicle V, Pawn P)
|
|
{
|
|
if ( BaseMutator != None )
|
|
BaseMutator.DriverLeftVehicle(V, P);
|
|
}
|
|
|
|
function bool PlayerCanRestartGame( PlayerController aPlayer )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Player Can be restarted ?
|
|
function bool PlayerCanRestart( PlayerController aPlayer )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Returns whether a mutator should be allowed with this gametype
|
|
static function bool AllowMutator( string MutatorClassName )
|
|
{
|
|
return !class'WorldInfo'.static.IsDemoBuild();
|
|
}
|
|
|
|
|
|
function bool AllowCheats(PlayerController P)
|
|
{
|
|
return ( WorldInfo.NetMode == NM_Standalone );
|
|
}
|
|
|
|
/**
|
|
* @return TRUE if the player is allowed to pause the game.
|
|
*/
|
|
function bool AllowPausing( optional PlayerController PC )
|
|
{
|
|
return bPauseable
|
|
|| WorldInfo.NetMode == NM_Standalone
|
|
|| (bAdminCanPause && AccessControl.IsAdmin(PC));
|
|
}
|
|
|
|
/**
|
|
* Called from C++'s CommitMapChange before unloading previous level
|
|
* @param PreviousMapName Name of the previous persistent level
|
|
* @param NextMapName Name of the persistent level being streamed to
|
|
*/
|
|
event PreCommitMapChange(string PreviousMapName, string NextMapName);
|
|
|
|
/**
|
|
* Called from C++'s CommitMapChange after unloading previous level and loading new level+sublevels
|
|
*/
|
|
event PostCommitMapChange();
|
|
|
|
/** AddInactivePRI()
|
|
* Add PRI to the inactive list, remove from the active list
|
|
*/
|
|
function AddInactivePRI(PlayerReplicationInfo PRI, PlayerController PC)
|
|
{
|
|
local int i;
|
|
local PlayerReplicationInfo NewPRI, CurrentPRI;
|
|
local bool bIsConsole;
|
|
|
|
// don't store if it's an old PRI from the previous level or if it's a spectator
|
|
if (!PRI.bFromPreviousLevel && !PRI.bOnlySpectator)
|
|
{
|
|
NewPRI = PRI.Duplicate();
|
|
WorldInfo.GRI.RemovePRI(NewPRI);
|
|
|
|
// make PRI inactive
|
|
NewPRI.RemoteRole = ROLE_None;
|
|
|
|
// delete after 5 minutes
|
|
NewPRI.LifeSpan = 300;
|
|
|
|
// On console, we have to check the unique net id as network address isn't valid
|
|
bIsConsole = WorldInfo.IsConsoleBuild();
|
|
|
|
// make sure no duplicates
|
|
for (i=0; i<InactivePRIArray.Length; i++)
|
|
{
|
|
CurrentPRI = InactivePRIArray[i];
|
|
if ( (CurrentPRI == None) || CurrentPRI.bDeleteMe ||
|
|
(!bIsConsole && (CurrentPRI.SavedNetworkAddress == NewPRI.SavedNetworkAddress)) ||
|
|
(bIsConsole && CurrentPRI.UniqueId == NewPRI.UniqueId) )
|
|
{
|
|
InactivePRIArray.Remove(i,1);
|
|
i--;
|
|
}
|
|
}
|
|
InactivePRIArray[InactivePRIArray.Length] = NewPRI;
|
|
|
|
// cap at 16 saved PRIs
|
|
if ( InactivePRIArray.Length > 16 )
|
|
{
|
|
InactivePRIArray.Remove(0, InactivePRIArray.Length - 16);
|
|
}
|
|
}
|
|
|
|
PRI.Destroy();
|
|
// Readjust the skill rating now that this player has left
|
|
RecalculateSkillRating();
|
|
}
|
|
|
|
/** FindInactivePRI()
|
|
* returns the PRI associated with this re-entering player
|
|
*/
|
|
function bool FindInactivePRI(PlayerController PC)
|
|
{
|
|
local string NewNetworkAddress, NewName;
|
|
local int i;
|
|
local PlayerReplicationInfo OldPRI, CurrentPRI;
|
|
local bool bIsConsole;
|
|
|
|
// don't bother for spectators
|
|
if (PC.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// On console, we have to check the unique net id as network address isn't valid
|
|
bIsConsole = WorldInfo.IsConsoleBuild();
|
|
|
|
NewNetworkAddress = PC.PlayerReplicationInfo.SavedNetworkAddress;
|
|
NewName = PC.PlayerReplicationInfo.PlayerName;
|
|
for (i=0; i<InactivePRIArray.Length; i++)
|
|
{
|
|
CurrentPRI = InactivePRIArray[i];
|
|
if ( (CurrentPRI == None) || CurrentPRI.bDeleteMe )
|
|
{
|
|
InactivePRIArray.Remove(i,1);
|
|
i--;
|
|
}
|
|
else if ( (bIsConsole && CurrentPRI.UniqueId == PC.PlayerReplicationInfo.UniqueId) ||
|
|
(!bIsConsole && (CurrentPRI.SavedNetworkAddress ~= NewNetworkAddress) && (CurrentPRI.PlayerName ~= NewName)) )
|
|
{
|
|
// found it!
|
|
OldPRI = PC.PlayerReplicationInfo;
|
|
PC.PlayerReplicationInfo = CurrentPRI;
|
|
PC.PlayerReplicationInfo.SetOwner(PC);
|
|
PC.PlayerReplicationInfo.RemoteRole = ROLE_SimulatedProxy;
|
|
PC.PlayerReplicationInfo.Lifespan = 0;
|
|
OverridePRI(PC, OldPRI);
|
|
WorldInfo.GRI.AddPRI(PC.PlayerReplicationInfo);
|
|
InactivePRIArray.Remove(i,1);
|
|
OldPRI.bIsInactive = TRUE;
|
|
OldPRI.Destroy();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** OverridePRI()
|
|
* override as needed properties of NewPRI with properties from OldPRI which were assigned during the login process
|
|
*/
|
|
function OverridePRI(PlayerController PC, PlayerReplicationInfo OldPRI)
|
|
{
|
|
PC.PlayerReplicationInfo.OverrideWith(OldPRI);
|
|
}
|
|
|
|
/** called on server during seamless level transitions to get the list of Actors that should be moved into the new level
|
|
* PlayerControllers, Role < ROLE_Authority Actors, and any non-Actors that are inside an Actor that is in the list
|
|
* (i.e. Object.Outer == Actor in the list)
|
|
* are all autmoatically moved regardless of whether they're included here
|
|
* only dynamic (!bStatic and !bNoDelete) actors in the PersistentLevel may be moved (this includes all actors spawned during gameplay)
|
|
* this is called for both parts of the transition because actors might change while in the middle (e.g. players might join or leave the game)
|
|
* @see also PlayerController::GetSeamlessTravelActorList() (the function that's called on clients)
|
|
* @param bToEntry true if we are going from old level -> entry, false if we are going from entry -> new level
|
|
* @param ActorList (out) list of actors to maintain
|
|
*/
|
|
event GetSeamlessTravelActorList(bool bToEntry, out array<Actor> ActorList)
|
|
{
|
|
local int i;
|
|
|
|
// always keep PlayerReplicationInfos and TeamInfos, so that after we restart we can keep players on the same team, etc
|
|
for (i = 0; i < WorldInfo.GRI.PRIArray.Length; i++)
|
|
{
|
|
WorldInfo.GRI.PRIArray[i].bFromPreviousLevel = true;
|
|
WorldInfo.GRI.PRIArray[i].bForceNetUpdate = true;
|
|
ActorList[ActorList.length] = WorldInfo.GRI.PRIArray[i];
|
|
}
|
|
|
|
if (bToEntry)
|
|
{
|
|
// keep general game state until we transition to the final destination
|
|
ActorList[ActorList.length] = WorldInfo.GRI;
|
|
if (BroadcastHandler != None)
|
|
{
|
|
ActorList[ActorList.length] = BroadcastHandler;
|
|
}
|
|
}
|
|
|
|
if (BaseMutator != None)
|
|
{
|
|
BaseMutator.GetSeamlessTravelActorList(bToEntry, ActorList);
|
|
}
|
|
|
|
if (MyAutoTestManager != None)
|
|
{
|
|
ActorList[ActorList.length] = MyAutoTestManager;
|
|
}
|
|
|
|
// Keep the AccessControl persistent, as it needs to >always< be ready for handling auth callbacks
|
|
if (AccessControl != none)
|
|
{
|
|
ActorList[ActorList.Length] = AccessControl;
|
|
}
|
|
}
|
|
|
|
/** used to swap a viewport/connection's PlayerControllers when seamless travelling and the new gametype's
|
|
* controller class is different than the previous
|
|
* includes network handling
|
|
* @param OldPC - the old PC that should be discarded
|
|
* @param NewPC - the new PC that should be used for the player
|
|
*/
|
|
native final function SwapPlayerControllers(PlayerController OldPC, PlayerController NewPC);
|
|
|
|
/** called after a seamless level transition has been completed on the *new* GameInfo
|
|
* used to reinitialize players already in the game as they won't have *Login() called on them
|
|
*/
|
|
event PostSeamlessTravel()
|
|
{
|
|
local Controller C;
|
|
|
|
// handle players that are already loaded
|
|
foreach WorldInfo.AllControllers(class'Controller', C)
|
|
{
|
|
if (C.bIsPlayer)
|
|
{
|
|
if (PlayerController(C) == None)
|
|
{
|
|
HandleSeamlessTravelPlayer(C);
|
|
}
|
|
else
|
|
{
|
|
if (!C.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
NumTravellingPlayers++;
|
|
}
|
|
if (PlayerController(C).HasClientLoadedCurrentWorld())
|
|
{
|
|
HandleSeamlessTravelPlayer(C);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bWaitingToStartMatch && !bDelayedStart && NumPlayers + NumBots > 0)
|
|
{
|
|
StartMatch();
|
|
}
|
|
if (WorldInfo.NetMode == NM_DedicatedServer)
|
|
{
|
|
// Update any online advertised settings
|
|
UpdateGameSettings();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used to update any changes in game settings that need to be published to
|
|
* players that are searching for games
|
|
*/
|
|
function UpdateGameSettings();
|
|
|
|
/** handles reinitializing players that remained through a seamless level transition
|
|
* called from C++ for players that finished loading after the server
|
|
* @param C the Controller to handle
|
|
*/
|
|
event HandleSeamlessTravelPlayer(out Controller C)
|
|
{
|
|
local rotator StartRotation;
|
|
local NavigationPoint StartSpot;
|
|
local PlayerController PC, NewPC;
|
|
local PlayerReplicationInfo OldPRI;
|
|
|
|
`log(">> GameInfo::HandleSeamlessTravelPlayer:" @ C,,'SeamlessTravel');
|
|
|
|
PC = PlayerController(C);
|
|
if (PC != None && PC.Class != PlayerControllerClass)
|
|
{
|
|
if (PC.Player != None)
|
|
{
|
|
// we need to spawn a new PlayerController to replace the old one
|
|
NewPC = SpawnPlayerController(PC.Location, PC.Rotation);
|
|
if (NewPC == None)
|
|
{
|
|
`Warn("Failed to spawn new PlayerController for" @ PC.GetHumanReadableName() @ "(old class" @ PC.Class $ ")");
|
|
PC.Destroy();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
PC.CleanUpAudioComponents();
|
|
PC.SeamlessTravelTo(NewPC);
|
|
NewPC.SeamlessTravelFrom(PC);
|
|
SwapPlayerControllers(PC, NewPC);
|
|
PC = NewPC;
|
|
C = NewPC;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PC.Destroy();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// clear out data that was only for the previous game
|
|
C.PlayerReplicationInfo.Reset();
|
|
// create a new PRI and copy over info; this is necessary because the old gametype may have used a different PRI class
|
|
OldPRI = C.PlayerReplicationInfo;
|
|
C.InitPlayerReplicationInfo();
|
|
OldPRI.SeamlessTravelTo(C.PlayerReplicationInfo);
|
|
// we don't need the old PRI anymore
|
|
//@fixme: need a way to replace PRIs that doesn't cause incorrect "player left the game"/"player entered the game" messages
|
|
OldPRI.Destroy();
|
|
}
|
|
|
|
// get rid of team if this is not a team game
|
|
if (!bTeamGame && C.PlayerReplicationInfo.Team != None)
|
|
{
|
|
C.PlayerReplicationInfo.Team.Destroy();
|
|
C.PlayerReplicationInfo.Team = None;
|
|
}
|
|
|
|
// Find a start spot.
|
|
StartSpot = FindPlayerStart(C, C.GetTeamNum());
|
|
|
|
if (StartSpot == None)
|
|
{
|
|
`warn(GameMessageClass.Default.FailedPlaceMessage);
|
|
}
|
|
else
|
|
{
|
|
StartRotation.Yaw = StartSpot.Rotation.Yaw;
|
|
C.SetLocation(StartSpot.Location);
|
|
C.SetRotation(StartRotation);
|
|
}
|
|
|
|
C.StartSpot = StartSpot;
|
|
|
|
if (PC != None)
|
|
{
|
|
PC.CleanUpAudioComponents();
|
|
|
|
// tell the player controller to register its data stores again
|
|
PC.ClientInitializeDataStores();
|
|
|
|
SetSeamlessTravelViewTarget(PC);
|
|
if (PC.PlayerReplicationInfo.bOnlySpectator)
|
|
{
|
|
PC.GotoState('Spectating');
|
|
PC.PlayerReplicationInfo.bIsSpectator = true;
|
|
PC.PlayerReplicationInfo.bOutOfLives = true;
|
|
NumSpectators++;
|
|
}
|
|
else
|
|
{
|
|
NumPlayers++;
|
|
NumTravellingPlayers--;
|
|
PC.GotoState('PlayerWaiting');
|
|
}
|
|
|
|
|
|
}
|
|
else
|
|
{
|
|
NumBots++;
|
|
C.GotoState('RoundEnded');
|
|
}
|
|
|
|
GenericPlayerInitialization(C);
|
|
|
|
`log("<< GameInfo::HandleSeamlessTravelPlayer:" @ C,,'SeamlessTravel');
|
|
}
|
|
|
|
function SetSeamlessTravelViewTarget(PlayerController PC)
|
|
{
|
|
PC.SetViewTarget(PC);
|
|
}
|
|
|
|
/**
|
|
* Updates the online subsystem's information for player counts so that
|
|
* LAN matches can show the correct player counts
|
|
*/
|
|
function UpdateGameSettingsCounts()
|
|
{
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
if (GameInterface != None)
|
|
{
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
if (GameSettings != None && GameSettings.bIsLanMatch)
|
|
{
|
|
// Update the number of open slots available
|
|
GameSettings.NumOpenPublicConnections = GameSettings.NumPublicConnections - GetNumPlayers();
|
|
if (GameSettings.NumOpenPublicConnections < 0)
|
|
{
|
|
GameSettings.NumOpenPublicConnections = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a base (empty) implementation of the completion notification
|
|
*
|
|
* @param PC the player controller to mark as done
|
|
* @param bWasSuccessful whether the PC was able to register for arbitration or not
|
|
*/
|
|
function ProcessClientRegistrationCompletion(PlayerController PC,bool bWasSuccessful);
|
|
|
|
/**
|
|
* Empty implementation of the code that kicks off async registration
|
|
*/
|
|
function StartArbitrationRegistration();
|
|
|
|
/**
|
|
* Empty implementation of the code that starts an arbitrated match
|
|
*/
|
|
function StartArbitratedMatch();
|
|
|
|
/**
|
|
* Empty implementation of the code that registers the server for arbitration
|
|
*/
|
|
function RegisterServerForArbitration();
|
|
|
|
/**
|
|
* Empty implementation of the code that handles the callback for completion
|
|
*
|
|
* @param SessionName the name of the session this is for
|
|
* @param bWasSuccessful whether the call worked or not
|
|
*/
|
|
function ArbitrationRegistrationComplete(name SessionName,bool bWasSuccessful);
|
|
|
|
function bool MatchIsInProgress()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This state is used to change the flow of start/end match to handle arbitration
|
|
*
|
|
* Basic flow of events:
|
|
* Server prepares to start the match and tells all clients to register arbitration
|
|
* Clients register with arbitration and tell the server when they are done
|
|
* Server checks for all clients to be registered and kicks any clients if
|
|
* they don't register in time.
|
|
* Server registers with arbitration and the match begins
|
|
*
|
|
* Match ends and the server tells connected clients to write arbitrated stats
|
|
* Clients write stats and notifies server of completion
|
|
* Server writes stats and ends the match
|
|
*/
|
|
auto State PendingMatch
|
|
{
|
|
function bool MatchIsInProgress()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Tells all of the currently connected clients to register with arbitration.
|
|
* The clients will call back to the server once they have done so, which
|
|
* will tell this state to see if it is time for the server to register with
|
|
* arbitration.
|
|
*/
|
|
function StartMatch()
|
|
{
|
|
if (bUsingArbitration)
|
|
{
|
|
StartArbitrationRegistration();
|
|
}
|
|
else
|
|
{
|
|
Global.StartMatch();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kicks off the async tasks of having the clients register with
|
|
* arbitration before the server does. Sets a timeout for when
|
|
* all slow to respond clients get kicked
|
|
*/
|
|
function StartArbitrationRegistration()
|
|
{
|
|
local PlayerController PC;
|
|
local UniqueNetId HostId;
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
if (!bHasArbitratedHandshakeBegun)
|
|
{
|
|
// Tell PreLogin() to reject new connections
|
|
bHasArbitratedHandshakeBegun = true;
|
|
|
|
// Get the host id from the game settings in case splitscreen works with arbitration
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
HostId = GameSettings.OwningPlayerId;
|
|
|
|
PendingArbitrationPCs.Length = 0;
|
|
// Iterate the controller list and tell them to register with arbitration
|
|
foreach WorldInfo.AllControllers(class'PlayerController', PC)
|
|
{
|
|
// Skip notifying local PCs as they are handled automatically
|
|
if (!PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientSetHostUniqueId(HostId);
|
|
PC.ClientRegisterForArbitration();
|
|
// Add to the pending list
|
|
PendingArbitrationPCs[PendingArbitrationPCs.Length] = PC;
|
|
}
|
|
else
|
|
{
|
|
// Add them as having completed arbitration
|
|
ArbitrationPCs[ArbitrationPCs.Length] = PC;
|
|
}
|
|
}
|
|
// Start the kick timer
|
|
SetTimer( ArbitrationHandshakeTimeout,false,nameof(ArbitrationTimeout) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Does the registration for the server. This must be done last as it
|
|
* includes all the players info from their registration
|
|
*/
|
|
function RegisterServerForArbitration()
|
|
{
|
|
if (GameInterface != None)
|
|
{
|
|
GameInterface.AddArbitrationRegistrationCompleteDelegate(ArbitrationRegistrationComplete);
|
|
GameInterface.RegisterForArbitration(PlayerReplicationInfoClass.default.SessionName);
|
|
}
|
|
else
|
|
{
|
|
// Fake as working without subsystem
|
|
ArbitrationRegistrationComplete(PlayerReplicationInfoClass.default.SessionName,true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback from the server that starts the match if the registration was
|
|
* successful. If not, it goes back to the menu
|
|
*
|
|
* @param SessionName the name of the session this is for
|
|
* @param bWasSuccessful whether the registration worked or not
|
|
*/
|
|
function ArbitrationRegistrationComplete(name SessionName,bool bWasSuccessful)
|
|
{
|
|
// Clear the delegate so we don't leak with GC
|
|
GameInterface.ClearArbitrationRegistrationCompleteDelegate(ArbitrationRegistrationComplete);
|
|
if (bWasSuccessful)
|
|
{
|
|
// Start the match
|
|
StartArbitratedMatch();
|
|
}
|
|
else
|
|
{
|
|
ConsoleCommand("Disconnect");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles kicking any clients that haven't completed handshaking
|
|
*/
|
|
function ArbitrationTimeout()
|
|
{
|
|
local int Index;
|
|
|
|
// Kick any pending players
|
|
for (Index = 0; Index < PendingArbitrationPCs.Length; Index++)
|
|
{
|
|
`if(`__TW_)
|
|
AccessControl.KickPlayer(PendingArbitrationPCs[Index], "<Strings:"$PathName(WorldInfo.Game.GameMessageClass) $ ".MaxedOutMessage>");
|
|
`else
|
|
AccessControl.KickPlayer(PendingArbitrationPCs[Index],GameMessageClass.Default.MaxedOutMessage);
|
|
`endif
|
|
}
|
|
PendingArbitrationPCs.Length = 0;
|
|
// Do the server registration now that any remaining clients are kicked
|
|
RegisterServerForArbitration();
|
|
}
|
|
|
|
/**
|
|
* Called once arbitration has completed and kicks off the real start of the match
|
|
*/
|
|
function StartArbitratedMatch()
|
|
{
|
|
bNeedsEndGameHandshake = true;
|
|
// Start the match
|
|
Global.StartMatch();
|
|
}
|
|
|
|
/**
|
|
* Removes the player controller from the pending list. Kicks that PC if it
|
|
* failed to register for arbitration. Starts the match if all clients have
|
|
* completed their registration
|
|
*
|
|
* @param PC the player controller to mark as done
|
|
* @param bWasSuccessful whether the PC was able to register for arbitration or not
|
|
*/
|
|
function ProcessClientRegistrationCompletion(PlayerController PC,bool bWasSuccessful)
|
|
{
|
|
local int FoundIndex;
|
|
|
|
// Search for the specified PC and remove if found
|
|
FoundIndex = PendingArbitrationPCs.Find(PC);
|
|
if (FoundIndex != INDEX_NONE)
|
|
{
|
|
PendingArbitrationPCs.Remove(FoundIndex,1);
|
|
if (bWasSuccessful)
|
|
{
|
|
// Add to the completed list
|
|
ArbitrationPCs[ArbitrationPCs.Length] = PC;
|
|
}
|
|
else
|
|
{
|
|
`if(`__TW_)
|
|
AccessControl.KickPlayer(PC, "<Strings:"$PathName(WorldInfo.Game.GameMessageClass) $ ".MaxedOutMessage>");
|
|
`else
|
|
AccessControl.KickPlayer(PC,GameMessageClass.Default.MaxedOutMessage);
|
|
`endif
|
|
}
|
|
}
|
|
// Start the match if all clients have responded
|
|
if (PendingArbitrationPCs.Length == 0)
|
|
{
|
|
// Clear the kick timer
|
|
SetTimer( 0,false,nameof(ArbitrationTimeout) );
|
|
RegisterServerForArbitration();
|
|
}
|
|
}
|
|
|
|
event EndState(name NextStateName)
|
|
{
|
|
// Clear the kick timer
|
|
SetTimer( 0,false,nameof(ArbitrationTimeout) );
|
|
|
|
if( GameInterface != None )
|
|
{
|
|
GameInterface.ClearArbitrationRegistrationCompleteDelegate(ArbitrationRegistrationComplete);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tells all clients to disconnect and then goes to the menu
|
|
*/
|
|
function NotifyArbitratedMatchEnd()
|
|
{
|
|
local PlayerController PC;
|
|
|
|
// Iterate through the controllers telling them to disconnect
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsLocalPlayerController() == false)
|
|
{
|
|
PC.ClientArbitratedMatchEnded();
|
|
}
|
|
}
|
|
// Iterate through local controllers telling them to disconnect
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsLocalPlayerController())
|
|
{
|
|
PC.ClientArbitratedMatchEnded();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used to notify the game type that it is ok to update a player's gameplay
|
|
* specific muting information now. The playercontroller needs to notify
|
|
* the server when it is possible to do so or the unique net id will be
|
|
* incorrect and the muting not work.
|
|
*
|
|
* @param PC the playercontroller that is ready for updates
|
|
*/
|
|
function UpdateGameplayMuteList(PlayerController PC)
|
|
{
|
|
// Let the server start sending voice packets
|
|
PC.bHasVoiceHandshakeCompleted = true;
|
|
// And tell the client it can start sending voice packets
|
|
PC.ClientVoiceHandshakeComplete();
|
|
}
|
|
|
|
/**
|
|
* Used by the game type to update the advertised skill for this game
|
|
*/
|
|
function RecalculateSkillRating()
|
|
{
|
|
local int Index;
|
|
local array<UniqueNetId> Players;
|
|
local UniqueNetId ZeroId;
|
|
|
|
if (WorldInfo.NetMode != NM_Standalone &&
|
|
OnlineSub != None &&
|
|
OnlineSub.GameInterface != None)
|
|
{
|
|
// Iterate through the players adding their unique id for skill calculation
|
|
for (Index = 0; Index < GameReplicationInfo.PRIArray.Length; Index++)
|
|
{
|
|
if (ZeroId != GameReplicationInfo.PRIArray[Index].UniqueId)
|
|
{
|
|
Players[Players.Length] = GameReplicationInfo.PRIArray[Index].UniqueId;
|
|
}
|
|
}
|
|
if (Players.Length > 0)
|
|
{
|
|
// Update the skill rating with the list of players
|
|
OnlineSub.GameInterface.RecalculateSkillRating(PlayerReplicationInfoClass.default.SessionName,Players);
|
|
}
|
|
};
|
|
}
|
|
|
|
/** Called when this PC is in cinematic mode, and its matinee is cancelled by the user. */
|
|
event MatineeCancelled();
|
|
|
|
|
|
/**
|
|
* Checks for the login parameters being passed on the command line. If
|
|
* present, it does an async login before starting the dedicated server
|
|
* registration process
|
|
*
|
|
* @return true if the login is in progress, false otherwise
|
|
*/
|
|
function bool ProcessServerLogin()
|
|
{
|
|
if (OnlineSub != None)
|
|
{
|
|
if (OnlineSub.PlayerInterface != None)
|
|
{
|
|
OnlineSub.PlayerInterface.AddLoginChangeDelegate(OnLoginChange);
|
|
OnlineSub.PlayerInterface.AddLoginFailedDelegate(0,OnLoginFailed);
|
|
// Check the command line for login information and login async
|
|
if (OnlineSub.PlayerInterface.AutoLogin() == false)
|
|
{
|
|
ClearAutoLoginDelegates();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Clears the login delegates once the login process has passed or failed
|
|
*/
|
|
function ClearAutoLoginDelegates()
|
|
{
|
|
if (OnlineSub.PlayerInterface != None)
|
|
{
|
|
OnlineSub.PlayerInterface.ClearLoginChangeDelegate(OnLoginChange);
|
|
OnlineSub.PlayerInterface.ClearLoginFailedDelegate(0,OnLoginFailed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called if the autologin fails
|
|
*
|
|
* @param LocalUserNum the controller number of the associated user
|
|
* @param ErrorCode the async error code that occurred
|
|
*/
|
|
function OnLoginFailed(byte LocalUserNum,EOnlineServerConnectionStatus ErrorCode)
|
|
{
|
|
ClearAutoLoginDelegates();
|
|
}
|
|
|
|
/**
|
|
* Used to tell the game when the autologin has completed
|
|
*
|
|
* @param LocalUserNum ignored
|
|
*/
|
|
function OnLoginChange(byte LocalUserNum)
|
|
{
|
|
ClearAutoLoginDelegates();
|
|
// The login has completed so start the dedicated server
|
|
RegisterServer();
|
|
}
|
|
|
|
/**
|
|
* Registers the dedicated server with the online service
|
|
*/
|
|
function RegisterServer()
|
|
{
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
if (OnlineGameSettingsClass != None && OnlineSub != None && OnlineSub.GameInterface != None)
|
|
{
|
|
// Create the default settings to get the standard settings to advertise
|
|
GameSettings = new OnlineGameSettingsClass;
|
|
// Serialize any custom settings from the URL
|
|
GameSettings.UpdateFromURL(ServerOptions, self);
|
|
|
|
// If 'bIsLanMatch' is set, disable all authentication
|
|
if (AccessControl != None &&
|
|
(WorldInfo.NetMode == NM_DedicatedServer || WorldInfo.NetMode == NM_ListenServer) && GameSettings.bIsLanMatch)
|
|
{
|
|
`log("Disabling all authentication, due to bIsLanMatch being set to true");
|
|
AccessControl.ClearAuthDelegates(false);
|
|
}
|
|
|
|
// Register the delegate so we can see when it's done
|
|
OnlineSub.GameInterface.AddCreateOnlineGameCompleteDelegate(OnServerCreateComplete);
|
|
// Now kick off the async publish
|
|
if ( !OnlineSub.GameInterface.CreateOnlineGame(0,PlayerReplicationInfoClass.default.SessionName,GameSettings) )
|
|
{
|
|
OnlineSub.GameInterface.ClearCreateOnlineGameCompleteDelegate(OnServerCreateComplete);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
`Warn("No game settings to register with the online service. Game won't be advertised");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies us of the game being registered successfully or not
|
|
*
|
|
* @param SessionName the name of the session that was created
|
|
* @param bWasSuccessful flag telling us whether it worked or not
|
|
*/
|
|
function OnServerCreateComplete(name SessionName,bool bWasSuccessful)
|
|
{
|
|
local OnlineGameSettings GameSettings;
|
|
|
|
GameInterface.ClearCreateOnlineGameCompleteDelegate(OnServerCreateComplete);
|
|
if (bWasSuccessful == false)
|
|
{
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
if (GameSettings != None && GameSettings.bIsLanMatch == false)
|
|
{
|
|
`Warn("Failed to register game with online service. Registering as a LAN match");
|
|
// Force to be a LAN match
|
|
GameSettings.bIsLanMatch = true;
|
|
// Register the delegate so we can see when it's done
|
|
GameInterface.AddCreateOnlineGameCompleteDelegate(OnServerCreateComplete);
|
|
// Now kick off the async publish
|
|
if (!GameInterface.CreateOnlineGame(0,SessionName,GameSettings))
|
|
{
|
|
GameInterface.ClearCreateOnlineGameCompleteDelegate(OnServerCreateComplete);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
`Warn("Failed to register game with online service. Game won't be advertised");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If a game server is started without Steam running, auth code needs late initialization
|
|
// (as the auth interface is only setup within CreateOnlineGame when Steam was not started)
|
|
if (OnlineSub.Class.Name == 'OnlineSubsystemSteamworks' && AccessControl != none && AccessControl.CachedAuthInt == none)
|
|
{
|
|
AccessControl.InitAuthHooks();
|
|
|
|
// This delegate was not set until after the auth interface became ready, so kick it off here
|
|
AccessControl.OnAuthReady();
|
|
}
|
|
|
|
UpdateGameSettings();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterates the player controllers and tells them to return to their party
|
|
*/
|
|
function TellClientsToReturnToPartyHost()
|
|
{
|
|
local PlayerController PC;
|
|
local OnlineGameSettings GameSettings;
|
|
local UniqueNetId RequestingPlayerId;
|
|
|
|
OnlineSub = class'GameEngine'.static.GetOnlineSubsystem();
|
|
if (OnlineSub != None)
|
|
{
|
|
// And grab one for the game interface since it will be used often
|
|
GameInterface = OnlineSub.GameInterface;
|
|
if (GameInterface != None)
|
|
{
|
|
// Use the game session owner as the host requesting travel
|
|
GameSettings = GameInterface.GetGameSettings(PlayerReplicationInfoClass.default.SessionName);
|
|
if (GameSettings != None)
|
|
{
|
|
RequestingPlayerId = GameSettings.OwningPlayerId;
|
|
}
|
|
else
|
|
{
|
|
// If no valid game session then use local player's net id as the host requesting the travel
|
|
foreach LocalPlayerControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsPrimaryPlayer() &&
|
|
PC.PlayerReplicationInfo != None)
|
|
{
|
|
RequestingPlayerId = PC.PlayerReplicationInfo.UniqueId;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Tell all clients to return using the net id of the host
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if (!PC.IsLocalPlayerController() &&
|
|
PC.IsPrimaryPlayer())
|
|
{
|
|
PC.ClientReturnToParty(RequestingPlayerId);
|
|
}
|
|
}
|
|
// Host travels last as this can trigger a disconnect on clients
|
|
foreach LocalPlayerControllers(class'PlayerController',PC)
|
|
{
|
|
if (PC.IsPrimaryPlayer())
|
|
{
|
|
PC.ClientReturnToParty(RequestingPlayerId);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send notification to clients that a party host is about to leave the match
|
|
*
|
|
* @param PartyHostPlayerId net id of the party host that is leaving
|
|
*/
|
|
function TellClientsPartyHostIsLeaving(UniqueNetId PartyHostPlayerId)
|
|
{
|
|
local PlayerController PC;
|
|
|
|
// Tell all clients to return using the net id of the host
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if ( PC.IsPrimaryPlayer() )
|
|
{
|
|
PC.ClientNotifyPartyHostLeaving(PartyHostPlayerId);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Iterates the player controllers and tells remote players to travel to the specified session
|
|
*
|
|
* @param SessionName the name of the session to register
|
|
* @param SearchClass the search that should be populated with the session
|
|
* @param PlatformSpecificInfo the binary data to place in the platform specific areas
|
|
*/
|
|
function TellClientsToTravelToSession(name SessionName,class<OnlineGameSearch> SearchClass,byte PlatformSpecificInfo[80])
|
|
{
|
|
local PlayerController PC;
|
|
|
|
foreach WorldInfo.AllControllers(class'PlayerController',PC)
|
|
{
|
|
if ( !PC.IsLocalPlayerController() && PC.IsPrimaryPlayer() )
|
|
{
|
|
PC.ClientTravelToSession(SessionName,SearchClass,PlatformSpecificInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
//=================================================================
|
|
/**
|
|
* AutoTestManager INTERFACE
|
|
*/
|
|
|
|
/** function to start the world traveling **/
|
|
exec function DoTravelTheWorld()
|
|
{
|
|
if ( MyAutoTestManager != None )
|
|
{
|
|
GotoState('TravelTheWorld');
|
|
MyAutoTestManager.DoTravelTheWorld();
|
|
}
|
|
}
|
|
|
|
/** Alters the synthetic bandwidth limit for a running game **/
|
|
exec native function SetBandwidthLimit( float AsyncIOBandwidthLimit );
|
|
|
|
/** This our state which allows us to have delayed actions while traveling the world (e.g. waiting for levels to stream in) **/
|
|
state TravelTheWorld
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @returns true if Automated Performance testing is enabled
|
|
*/
|
|
function bool IsAutomatedPerfTesting()
|
|
{
|
|
return (MyAutoTestManager != None) && MyAutoTestManager.bAutomatedPerfTesting;
|
|
}
|
|
|
|
/**
|
|
* @returns true if checking for fragmentation is enabled
|
|
*/
|
|
function bool IsCheckingForFragmentation()
|
|
{
|
|
return (MyAutoTestManager != None) && MyAutoTestManager.bCheckingForFragmentation;
|
|
}
|
|
|
|
/**
|
|
* @returns true if checking for memory leaks is enabled
|
|
*/
|
|
function bool IsCheckingForMemLeaks()
|
|
{
|
|
return (MyAutoTestManager != None) && MyAutoTestManager.bCheckingForMemLeaks;
|
|
}
|
|
|
|
/**
|
|
* @returns true if doing a sentinel run
|
|
*/
|
|
function bool IsDoingASentinelRun()
|
|
{
|
|
return (MyAutoTestManager != None) && MyAutoTestManager.bDoingASentinelRun;
|
|
}
|
|
|
|
/**
|
|
* @returns true if should auto-continue to next round
|
|
*/
|
|
function bool ShouldAutoContinueToNextRound()
|
|
{
|
|
return (MyAutoTestManager != None) && MyAutoTestManager.bAutoContinueToNextRound;
|
|
}
|
|
|
|
/**
|
|
* Asks AutoTestManager to start a sentinel run if needed
|
|
* Must be called by gameinfo subclass - not called in base implementation of GameInfo.StartMatch()
|
|
* @returns true if should skip normal startmatch process
|
|
*/
|
|
function bool CheckForSentinelRun()
|
|
{
|
|
return (MyAutoTestManager != None) && MyAutoTestManager.CheckForSentinelRun();
|
|
}
|
|
|
|
/** This is for the QA team who don't use UFE nor commandline :-( **/
|
|
exec simulated function BeginBVT( optional coerce string TagDesc )
|
|
{
|
|
if ( MyAutoTestManager == None )
|
|
{
|
|
MyAutoTestManager = spawn(AutoTestManagerClass);
|
|
}
|
|
|
|
MyAutoTestManager.BeginSentinelRun( "BVT", "", TagDesc );
|
|
MyAutoTestManager.SetTimer( 3.0f, TRUE, nameof(MyAutoTestManager.DoTimeBasedSentinelStatGathering) );
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Turns standby detection on/off
|
|
*
|
|
* @param bIsEnabled true to turn it on, false to disable it
|
|
*/
|
|
native function EnableStandbyCheatDetection(bool bIsEnabled);
|
|
|
|
/**
|
|
* Notifies the game code that a standby cheat was detected
|
|
*
|
|
* @param StandbyType the type of cheat detected
|
|
*/
|
|
event StandbyCheatDetected(EStandbyType StandbyType);
|
|
|
|
|
|
/**
|
|
* Used to create a new online session after the previous one has been destroyed after travelling
|
|
*
|
|
* @param SessionName The name of the game session (not used with Steamworks)
|
|
* @param bWasSuccesful Whether or not the session was succesfully destroyed
|
|
*/
|
|
function OnDestroyOnlineGameComplete(name SessionName, bool bWasSuccessful)
|
|
{
|
|
if (!ProcessServerLogin())
|
|
RegisterServer();
|
|
|
|
GameInterface.ClearDestroyOnlineGameCompleteDelegate(OnDestroyOnlineGameComplete);
|
|
}
|
|
|
|
/**
|
|
* Notifies the game code that the engine has finished loading. This
|
|
* function will only be called one time only.
|
|
*/
|
|
event OnEngineHasLoaded();
|
|
|
|
function InitCrowdPopulationManager()
|
|
{
|
|
if( PopulationManagerClass != None )
|
|
{
|
|
PopulationManager = Spawn(PopulationManagerClass);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up any online subsystem delegates that are set
|
|
*/
|
|
function ClearOnlineDelegates();
|
|
|
|
|
|
//@HSL_BEGIN - BWJ - 6-8-16 - Playfab hooks for server. Not using delegates for these because they are one-off and don't have to worry about cleanup
|
|
event OnRetreivedPFInternalUserData( const string ForPlayerId, array<string> Keys, array<string> Values );
|
|
|
|
/** returns TRUE if this server was launched by playfab */
|
|
native function bool WasLaunchedByPlayfab();
|
|
|
|
event string GetFriendlyNameForCurrentGameMode();
|
|
//@HSL_END
|
|
|
|
|
|
//@HSL_BEGIN - BWJ - 2-16-17 - Hooks for controller change notifications
|
|
function NotifyControllerDisconnected();
|
|
function NotifyControllerReconnected();
|
|
//@HSL_END
|
|
|
|
`if (`__TW_)
|
|
event bool GetRequiresPassword()
|
|
{
|
|
return RequiresPassword();
|
|
}
|
|
`endif
|
|
|
|
defaultproperties
|
|
{
|
|
// The game spawns bots/players which can't be done during physics ticking
|
|
TickGroup=TG_PreAsyncWork
|
|
|
|
GameSpeed=1.0
|
|
bDelayedStart=true
|
|
HUDType=class'Engine.HUD'
|
|
bWaitingToStartMatch=false
|
|
bRestartLevel=True
|
|
bPauseable=True
|
|
AccessControlClass=class'Engine.AccessControl'
|
|
BroadcastHandlerClass=class'Engine.BroadcastHandler'
|
|
DeathMessageClass=class'LocalMessage'
|
|
PlayerControllerClass=class'Engine.PlayerController'
|
|
GameMessageClass=class'GameMessage'
|
|
GameReplicationInfoClass=class'GameReplicationInfo'
|
|
AutoTestManagerClass=class'Engine.AutoTestManager'
|
|
FearCostFalloff=+0.95
|
|
CurrentID=1
|
|
PlayerReplicationInfoClass=Class'Engine.PlayerReplicationInfo'
|
|
MaxSpectatorsAllowed=32
|
|
MaxPlayersAllowed=32
|
|
|
|
Components.Remove(Sprite)
|
|
|
|
// Defaults for if your game has only one skill leaderboard
|
|
LeaderboardId=0xFFFE0000
|
|
ArbitratedLeaderboardId=0xFFFF0000
|
|
|
|
// PopulationManagerClass=class'CrowdPopulationManagerBase'
|
|
|
|
StreamingPauseIcon=Material'EngineResources.M_StreamingPause'
|
|
|
|
`if(`__TW_)
|
|
// @note: this could be be done in the config (GameInfoClassAliases is normally config),
|
|
// but then we have to mess with ini patching, etc... so just do it here.
|
|
GameInfoClassAliases.Add((ShortName="BenchMark", GameClassName="KFGameContent.KFGameInfo_BenchMark"))
|
|
GameInfoClassAliases.Add((ShortName="Survival", GameClassName="KFGameContent.KFGameInfo_Survival"))
|
|
GameInfoClassAliases.Add((ShortName="Versus", GameClassName="KFGameContent.KFGameInfo_VersusSurvival"))
|
|
GameInfoClassAliases.Add((ShortName="Weekly", GameClassName="KFGameContent.KFGameInfo_WeeklySurvival"))
|
|
GameInfoClassAliases.Add((ShortName="Tutorial", GameClassName="KFGameContent.KFGameInfo_Tutorial"))
|
|
GameInfoClassAliases.Add((ShortName="Endless", GameClassName="KFGameContent.KFGameInfo_Endless"))
|
|
GameInfoClassAliases.Add((ShortName="Objective", GameClassName="KFGameContent.KFGameInfo_Objective"))
|
|
`endif
|
|
//@SABER_EGS_BEGIN Crossplay support
|
|
// number of players, using epic online system
|
|
NumEosPlayers=0
|
|
//@SABER_EGS_END
|
|
}
|