//=============================================================================
// KFPlayerReplicationInfo
//=============================================================================
// A PlayerReplicationInfo is created for every player on a server (or in a standalone game).
// Players are PlayerControllers, or other Controllers with bIsPlayer=true
// PlayerReplicationInfos are replicated to all clients, and contain network game relevant information about the player,
// such as playername, score, etc.
//=============================================================================
// Killing Floor 2
// Copyright (C) 2015 Tripwire Interactive LLC
// - Andrew "Strago" Ladenberger
//=============================================================================

class KFPlayerReplicationInfo extends PlayerReplicationInfo
	config(Game)
	native(ReplicationInfo)
	nativereplication
	dependson(KFVoteCollector, KFLocalMessage_VoiceComms);

`include(KFGame\KFGameAnalytics.uci);
`include(KFGame\KFMatchStats.uci);
`include(KFProfileSettings.uci);

/** The time at which this PRI left the game */
var float LastQuitTime;
/** The number of times this PRI has reconnected to this game */
var byte NumTimesReconnected;

/** TRUE if this player has spawned in for the active round. This is used for realtime multipay on PS4 */
var bool bHasSpawnedIn;

/** UTC timestamp representing the last time a crate was gifted to this player. Tracked by server only */
var string LastCrateGiftTimestamp;
/** Seconds of gameplay for this player for crate gifting. Tracked by server only */
var int SecondsOfGameplay;

/** Whether or not dosh can be earned */
var bool bAllowDoshEarning;

/************************************
 *  Character class related variables
 ************************************/

/** This variable exists to tell native our max number of attachments */
const NUM_COSMETIC_ATTACHMENTS = `MAX_COSMETIC_ATTACHMENTS;

/** List of all playable characters */
var const array<KFCharacterInfo_Human> CharacterArchetypes;

/** Replication info for player character customization */
struct native CustomizationInfo
{
	/** Index of the current char archetype among the AvailableCharArchetypes array */
	var const int CharacterIndex;
	var const int HeadMeshIndex;
	var const int HeadSkinIndex;
	var const int BodyMeshIndex;
	var const int BodySkinIndex;
	var const int AttachmentMeshIndices[`MAX_COSMETIC_ATTACHMENTS];
	var const int AttachmentSkinIndices[`MAX_COSMETIC_ATTACHMENTS];

	structcpptext
	{
		FCustomizationInfo& operator=(FCustomizationInfo& rhs)
		{
			CharacterIndex = rhs.CharacterIndex;
			HeadMeshIndex = rhs.HeadMeshIndex;
			HeadSkinIndex = rhs.HeadSkinIndex;
			BodyMeshIndex = rhs.BodyMeshIndex;
			BodySkinIndex = rhs.BodySkinIndex;
			for(INT i = 0; i < UCONST_NUM_COSMETIC_ATTACHMENTS /*MAX_COSMETIC_ATTACHMENTS*/; ++i)
			{
				AttachmentMeshIndices[i] = rhs.AttachmentMeshIndices[i];
				AttachmentSkinIndices[i] = rhs.AttachmentSkinIndices[i];
			}

			return *this;
		}
	}

	structdefaultproperties
	{
		// Index of `CLEARED_ATTACHMENT_INDEX implies don't use any attachment
		// Note: Array size should match MAX_COSMETIC_ATTACHMENTS
		AttachmentMeshIndices[0]=`CLEARED_ATTACHMENT_INDEX
		AttachmentMeshIndices[1]=`CLEARED_ATTACHMENT_INDEX
		AttachmentMeshIndices[2]=`CLEARED_ATTACHMENT_INDEX
	}
};

/** Current customization settings */
var const repnotify CustomizationInfo RepCustomizationInfo;

/** Texture of render of custom character head. */
var	texture		CharPortrait;

/** 0 is Not Talking, 1 is Public, 2 is Team, 3 is Squad, 4 is Vehicle, 5 is Spectator, 6 is Sequestered Spectator */
var	repnotify byte	VOIPStatus;

/** TRUE if player has registered as a local talker with the OSS */
var repnotify bool bVOIPRegisteredWithOSS;

/** The cumulative amount of damage this player has dealt, resets on team change */
var int DamageDealtOnTeam;

/************************************
 *  Replicated Perk Data
 ************************************/
/** Selected perk info replicated to all */
var  	byte			NetPerkIndex; // @todo: replace with class?
var  			Class<KFPerk>	CurrentPerkClass;
var private 	byte	ActivePerkLevel;
var private 	byte	ActivePerkPrestigeLevel;
/** Kill assists. Need an integer here because it is very easy to exceed 255 assists. */
var 			int 			Assists;
var 	byte			PlayerHealth; //represented as a percentage
var 	byte			PlayerHealthPercent; //represented as a percentage
/** The firebug range skill increases the range of fire weapons we need to tell other clients if it is on */
var 			bool 			bExtraFireRange;
/** The firebug splash damage skill changes the explosion template */
var 			bool 			bSplashActive;
/** The demo Nuke skill changes the explosion template */
var 			bool 			bNukeActive;
/** The demo Concussive skill changes the explosion template */
var 			bool 			bConcussiveActive;
/** Certain perks can supply ammo etc. We need to replicate that for the HUD */
var 			byte 			PerkSupplyLevel;

/************************************
 *  Not replicated Perk Data,
 *  local client only
 ************************************/
var 			bool 			bPerkPrimarySupplyUsed;
var 			bool 			bPerkSecondarySupplyUsed;

var bool bVotedToSkipTraderTime;
var	bool bAlreadyStartedASkipTraderVote;

/************************************
 *  Not replicated Voice Comms Request
 *  local client only -ZG
 ************************************/

var 			EVoiceCommsType CurrentVoiceCommsRequest;
var				float 			VoiceCommsStatusDisplayInterval;
var				int 			VoiceCommsStatusDisplayIntervalCount;
var				int 			VoiceCommsStatusDisplayIntervalMax;

/************************************
 *  Replicated Unlocks
 ************************************/
var private int		SharedUnlocks;

var repnotify private int CurrentHeadShotEffectID;
/************************************
 *  Objective
 ************************************/
 var		bool		bObjectivePlayer;

/************************************
 * Replicated, compressed locations of human players
 ************************************/
var private	Vector 		PawnLocationCompressed;
var private	Vector 		LastReplicatedSmoothedLocation;
var 		bool 		bShowNonRelevantPlayers;

/** Cached (non-replicated) player owner, used by server */
var KFPlayerController KFPlayerOwner;

/** Whether we're waiting for our online subsystem inventory to load (used to determine selectable characters) */
var transient bool bWaitingForInventory;
/** What character should be checked for selection once the online subsystem inventory loads */
var transient int WaitingForInventoryCharacterIndex;

/** Whether the character is currently holding a transport objective */
var bool bCarryingCollectible;

/************************************
*  native
************************************/
cpptext
{
	INT* GetOptimizedRepList( BYTE* InDefault, FPropertyRetirement* Retire, INT* Ptr, UPackageMap* Map, UActorChannel* Channel );
	UBOOL ShowNonRelevantPlayerInfo();

	/** Inventory */
	UBOOL DelayCharacterOwnership();

	void LoadCosmeticContent(UKFCharacterInfo_Human* CharArch, INT CosmeticType, INT CosmeticIdx);
	void CacheCosmeticContent(UKFCharacterInfo_Human* CharArch, INT CosmeticType, INT CosmeticIdx);

	void CacheHeadshotFxContent();

	virtual void AddReferencedObjects(TArray<UObject*>& ObjectArray);
}

native function bool StartLoadCosmeticContent(KFCharacterInfo_Human CharArch, INT CosmeticType, INT CosmeticIdx);
native function StartLoadHeadshotFxContent();

replication
{
	if ( bNetDirty )
		RepCustomizationInfo, NetPerkIndex, ActivePerkLevel, ActivePerkPrestigeLevel, bHasSpawnedIn,
		CurrentPerkClass, bObjectivePlayer, Assists, PlayerHealth, PlayerHealthPercent,
		bExtraFireRange, bSplashActive, bNukeActive, bConcussiveActive, PerkSupplyLevel,
		CharPortrait, DamageDealtOnTeam, bVOIPRegisteredWithOSS, CurrentVoiceCommsRequest,CurrentHeadShotEffectID, bCarryingCollectible;

  	// sent to non owning clients
 	if ( bNetDirty && (!bNetOwner || bDemoRecording) )
 		VOIPStatus, SharedUnlocks;

 	if( !bNetOwner && bNetDirty )
		PawnLocationCompressed;
}

simulated event ReplicatedEvent(name VarName)
{
	local KFPlayerController LocalPC;
	local PlayerNameIdPair newPlayer;

	if ( VarName == 'RepCustomizationInfo' )
	{
		CharacterCustomizationChanged();
	}
	else if ( VarName == nameof(VOIPStatus) )
	{
		VOIPStatusChanged(self, VOIPStatus > 0);
	}
	else if ( VarName == nameof(Score) )
	{
		UpdateTraderDosh();
	}
	else if ( VarName == 'PlayerName' )
	{
		LocalPC = KFPlayerController(GetALocalPlayerController());
		if( LocalPC != none )
		{
			newPlayer.PlayerName = PlayerName;
			LocalPC.RecentlyMetPlayers.AddItem(newPlayer);

			// Refresh the party widget when the name changes
			if( LocalPC.MyGFxManager != none )
			{
				LocalPC.MyGFxManager.ForceUpdateNextFrame();
			}
		}
	}
	else if (VarName == 'bVOIPRegisteredWithOSS')
	{
		OnTalkerRegistered();
	}
	else if (VarName == nameof(CurrentHeadShotEffectID))
	{
		CurrentHeadShotEffectIdChanged();
	}


	if ( VarName == 'Team' )
	{
		ClientRecieveNewTeam();
	}

	Super.ReplicatedEvent(VarName);
}

simulated event PostBeginPlay()
{
	super.PostBeginPlay();

	if( Role == ROLE_Authority )
	{
		KFPlayerOwner = KFPlayerController( Owner );
		ResetSkipTrader();
	}
}

/*********************************************************************************************
`*Team management
*********************************************************************************************/
reliable server function ServerSwitchTeam();
reliable client function ClientRecieveNewTeam();

/*********************************************************************************************
`* Current Perk Level
********************************************************************************************* */

simulated function byte GetActivePerkLevel()
{
	return ActivePerkLevel;
}

simulated function byte GetActivePerkPrestigeLevel()
{
	return ActivePerkPrestigeLevel;
}

/*********************************************************************************************
`* VOIP
********************************************************************************************* */


reliable server function ServerNotifyStartVoip()
{
	local int i;
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	//ClearTimer('ClearVOIP');

	if ( !KFGameInfo(WorldInfo.Game).bDisableVOIP && !KFGameInfo(WorldInfo.Game).bDisablePublicVOIPChannel )
	{
		if ( !bOnlySpectator )
		{
			bNetDirty = true;
			bForceNetUpdate = true;

			if(!KFPC.IsLocalController())
			{
				VOIPStatusChanged(self, true);
			}
            VOIPStatus = 1;

			KFPC.VoiceReceivers.Remove(0, KFPC.VoiceReceivers.Length);

			if ( KFPC.Pawn != none && KFPC.Pawn.Health > 0 )
			{
				for ( i = 0; i < WorldInfo.GRI.PRIArray.Length; i++ )
				{
					if ( !WorldInfo.GRI.PRIArray[i].bBot )
					{
						KFPC.VoiceReceivers.AddItem(WorldInfo.GRI.PRIArray[i].UniqueId);

						if ( PlayerController(WorldInfo.GRI.PRIArray[i].Owner) != none )
						{
							PlayerController(WorldInfo.GRI.PRIArray[i].Owner).VoiceSenders.AddItem(UniqueId);
						}
					}
				}
			}
			else if ( KFGameInfo(WorldInfo.Game).bEnableDeadToVOIP )
			{
				for ( i = 0; i < WorldInfo.GRI.PRIArray.Length; i++ )
				{
					if ( !WorldInfo.GRI.PRIArray[i].bBot )
					{
						KFPC.VoiceReceivers.AddItem(WorldInfo.GRI.PRIArray[i].UniqueId);

						if ( PlayerController(WorldInfo.GRI.PRIArray[i].Owner) != none )
						{
							PlayerController(WorldInfo.GRI.PRIArray[i].Owner).VoiceSenders.AddItem(UniqueId);
						}
					}
				}
			}

			if ( KFPC.VoiceReceivers.Length <= 0 )
			{
				KFPC.VoiceReceivers.AddItem(UniqueId);
			}
		}
		else
		{
			ServerStartSpectatorVoiceChat();
		}
	}
	else
	{
		KFPC.VoiceReceivers.Remove(0, KFPC.VoiceReceivers.Length);
		KFPC.VoiceReceivers.AddItem(UniqueId);
	}
}

reliable server function ServerNotifyStartTeamVoip()
{
	local byte TeamIndex;
	local int i;
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	//ClearTimer('ClearVOIP');

	if ( !KFGameInfo(WorldInfo.Game).bDisableVoip )
	{
		TeamIndex = GetTeamNum();

		if ( !bOnlySpectator )
		{
			bNetDirty = true;
			bForceNetUpdate = true;

			if(!KFPC.IsLocalController())
			{
				VOIPStatusChanged(self, true);
			}

            VOIPStatus = 2;

			KFPC.VoiceReceivers.Remove(0, KFPC.VoiceReceivers.Length);

			if ( KFPC.Pawn != none && KFPC.Pawn.Health > 0 )
			{
				for ( i = 0; i < WorldInfo.GRI.PRIArray.Length; i++ )
				{
					if ( !WorldInfo.GRI.PRIArray[i].bBot && (WorldInfo.GRI.PRIArray[i].GetTeamNum() == TeamIndex || !WorldInfo.GRI.bMatchHasBegun || WorldInfo.GRI.bMatchIsOver) )
					{
						KFPC.VoiceReceivers.AddItem(WorldInfo.GRI.PRIArray[i].UniqueId);

						if ( PlayerController(WorldInfo.GRI.PRIArray[i].Owner) != none )
						{
							PlayerController(WorldInfo.GRI.PRIArray[i].Owner).VoiceSenders.AddItem(UniqueId);
						}
					}
				}
			}
			else if ( KFGameInfo(WorldInfo.Game).bEnableDeadToVOIP )
			{
				for ( i = 0; i < WorldInfo.GRI.PRIArray.Length; i++ )
				{
					if ( !WorldInfo.GRI.PRIArray[i].bBot &&
						 (WorldInfo.GRI.PRIArray[i].GetTeamNum() == TeamIndex || !WorldInfo.GRI.bMatchHasBegun || WorldInfo.GRI.bMatchIsOver) )
					{
						KFPC.VoiceReceivers.AddItem(WorldInfo.GRI.PRIArray[i].UniqueId);

						if ( PlayerController(WorldInfo.GRI.PRIArray[i].Owner) != none )
						{
							PlayerController(WorldInfo.GRI.PRIArray[i].Owner).VoiceSenders.AddItem(UniqueId);
						}
					}
				}
			}

			if ( KFPC.VoiceReceivers.Length <= 0 )
			{
				KFPC.VoiceReceivers.AddItem(UniqueId);
			}
		}
		else
		{
			ServerStartSpectatorVoiceChat();
		}
	}
	else
	{
		KFPC.VoiceReceivers.Remove(0, KFPC.VoiceReceivers.Length);
		KFPC.VoiceReceivers.AddItem(UniqueId);
	}
}


function ServerStartSpectatorVoiceChat()
{
	local int i;

	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	ClearTimer('ClearVOIP');

	KFPC.VoiceReceivers.Remove(0, KFPC.VoiceReceivers.Length);

	VOIPStatus =KFGameInfo(WorldInfo.Game).bPartitionSpectators ?  6 : 5;

	for ( i = 0; i < WorldInfo.GRI.PRIArray.Length; ++i )
	{
		if ( !WorldInfo.GRI.PRIArray[i].bBot &&
			(!KFGameInfo(WorldInfo.Game).bPartitionSpectators || WorldInfo.GRI.PRIArray[i].bOnlySpectator) )
		{
			KFPC.VoiceReceivers.AddItem(WorldInfo.GRI.PRIArray[i].UniqueId);

			if ( PlayerController(WorldInfo.GRI.PRIArray[i].Owner) != none )
			{
				PlayerController(WorldInfo.GRI.PRIArray[i].Owner).VoiceSenders.AddItem(UniqueId);
			}
		}
	}

	if ( KFPC.VoiceReceivers.Length <= 0 )
	{
		KFPC.VoiceReceivers.AddItem(UniqueId);
	}
}

reliable server function ServerNotifyStopVOIP()
{
	local KFPlayerController KFPC;

	VOIPStatus = 0;
	VOIPStatusChanged(self, false);

	KFPC = KFPlayerController(Owner);

	if( KFPC != none )
	{
	KFPC.VoiceReceivers.Remove(0, KFPC.VoiceReceivers.Length);
	}

	foreach WorldInfo.AllControllers(class'KFPlayerController', KFPC)
	{
		while ( KFPC.VoiceSenders.Find('Uid', UniqueId.Uid) != INDEX_NONE )
		{
			KFPC.VoiceSenders.RemoveItem(UniqueId);
		}
	}
}


simulated function VOIPStatusChanged( PlayerReplicationInfo Talker, bool bIsTalking )
{
	local KFPlayerController KFPC;
	local KFPlayerReplicationInfo TalkerKFPRI;
	local KFGFxHudWrapper MyGFxHUD;
	local OnlineSubsystem OSS;

	OSS = class'GameEngine'.static.GetOnlineSubsystem();

    foreach WorldInfo.LocalPlayerControllers(class'KFPlayerController', KFPC)
	{
		// BWJ - 10-4-16 - Exit out immediately if local player has a chat restriction
		if( OSS != None && OSS.HasChatRestriction( LocalPlayer(KFPC.Player).ControllerId ) )
		{
			return;
		}

		MyGFxHUD = KFGFxHudWrapper(KFPC.myHUD);

		TalkerKFPRI = KFPlayerReplicationInfo(Talker);
		if( TalkerKFPRI != none )
		{
			if( TalkerKFPRI.VOIPStatus == 6 && !KFPC.PlayerReplicationInfo.bOnlySpectator )
			{
				return;
			}
		}

		if(MyGFxHUD != none && MyGFxHUD.HudMovie != none)
		{
			MyGFxHUD.HudMovie.VOIPWidget.VOIPEventTriggered(Talker, bIsTalking);
		}

		if(KFPC.MyGFxManager != none)
		{
			KFPC.MyGFxManager.UpdateVOIP(Talker, bIsTalking);
		}
	}
}


simulated function OnTalkerRegistered()
{
	local PlayerController LocalPC;
	local int i;
	local KFPlayerReplicationInfo PRI;
	local OnlineSubsystem OnlineSub;

	OnlineSub = class'GameEngine'.static.GetOnlineSubsystem();

	LocalPC = GetALocalPlayerController();

	// If there is no PRI for the local player or GRI, we wait (really shouldn't be more than a frame) and try again
	if( LocalPC.PlayerReplicationInfo == None || WorldInfo.GRI == None )
	{
		SetTimer( 0.1, false, nameof(OnTalkerRegistered) );
	}

	if (LocalPC != none && LocalPC.PlayerReplicationInfo != none && WorldInfo.GRI != none)
	{
		// Local player has regigistered
		if (LocalPC.PlayerReplicationInfo == self)
		{
			// Register all other players that have been registered
			for (i = 0; i < WorldInfo.GRI.PRIArray.Length; i++)
			{
				PRI = KFPlayerReplicationInfo(WorldInfo.GRI.PRIArray[i]);
				if (PRI != self && PRI.bVOIPRegisteredWithOSS)
				{
					OnlineSub.VoiceInterface.RegisterRemoteTalker(PRI.UniqueId);
				}
			}
		}
		// Registering someone else other than the local player
		else if (KFPlayerReplicationInfo(LocalPC.PlayerReplicationInfo).bVOIPRegisteredWithOSS)
		{
			OnlineSub.VoiceInterface.RegisterRemoteTalker(UniqueId);
		}
	}
}


// JRO - Make sure the talking icon doesn't continue to show up after leaving
simulated function UnregisterPlayerFromSession()
{
	VOIPStatusChanged(self, false);
	super.UnregisterPlayerFromSession();
}

/*********************************************************************************************
`* Kick Voting
********************************************************************************************* */

reliable client function ShowKickVote(PlayerReplicationInfo PRI, byte VoteDuration, bool bShowChoices)
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxHUD != none)
	{
		KFPC.MyGFxHUD.ShowKickVote(PRI, VoteDuration, bShowChoices);
	}

	if(KFPC != none && KFPC.MyGFxManager != none && bShowChoices)
	{
		KFPC.MyGFxManager.ShowKickVote(PRI);
	}
}

reliable client function HideKickVote()
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxHUD != none)
	{
		KFPC.MyGFxHUD.HideKickVote();
	}

	if(KFPC != none && KFPC.MyGFxManager != none)
	{
		KFPC.MyGFxManager.HideKickVote();
	}
}

reliable server function ServerStartKickVote(PlayerReplicationInfo Kickee, PlayerReplicationInfo Kicker)
{
	local KFGameReplicationInfo KFGRI;

	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if(KFGRI != none)
	{
		KFGRI.ServerStartVoteKick(Kickee, Kicker);
	}
}

simulated function CastKickVote(PlayerReplicationInfo PRI, bool bKick)
{
	local KFPlayerController KFPC;

	ServerCastKickVote(self, bKick);

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxManager != none)
	{
		KFPC.MyGFxManager.HideKickVote();
	}
}

reliable server function ServerCastKickVote(PlayerReplicationInfo PRI, bool bKick)
{
	local KFGameReplicationInfo KFGRI;

	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if(KFGRI != none)
	{
		KFGRI.RecieveVoteKick(PRI, bKick);
	}
}

/*********************************************************************************************
`* Skip Trader
********************************************************************************************* */

reliable client function ShowSkipTraderVote(PlayerReplicationInfo PRI, byte VoteDuration, bool bShowChoices)
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxHUD != none)
	{
		KFPC.MyGFxHUD.ShowSkipTraderVote(PRI, VoteDuration, bShowChoices);
	}

	if(KFPC != none && KFPC.MyGFxManager != none && bShowChoices)
	{
		KFPC.MyGFxManager.ShowSkipTraderVote(PRI);
	}
}

reliable client function UpdateSkipTraderTime(byte CurrentVoteTime)
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxHUD != none)
	{
		KFPC.MyGFxHUD.UpdateSkipTraderTime(CurrentVoteTime);
	}
}

reliable client function HideSkipTraderVote()
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxHUD != none)
	{
		KFPC.MyGFxHUD.HideSkipTraderVote();
	}

	if(KFPC != none && KFPC.MyGFxManager != none)
	{
		KFPC.MyGFxManager.HideSkipTraderVote();
	}
}

simulated function RequestSkiptTrader(PlayerReplicationInfo PRI)
{
	bVotedToSkipTraderTime = true;
	ServerRequestSkipTraderVote(self);
}

reliable server function ServerRequestSkipTraderVote(PlayerReplicationInfo PRI)
{
	local KFGameReplicationInfo KFGRI;

	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if (KFGRI != none)
	{
		KFGRI.ServerStartVoteSkipTrader(PRI);
	}
}

simulated function CastSkipTraderVote(PlayerReplicationInfo PRI, bool bSkipTrader)
{
	local KFPlayerController KFPC;

	ServerCastSkipTraderVote(self, bSkipTrader);

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxManager != none)
	{
		KFPC.MyGFxManager.HideSkipTraderVote();
	}
}

reliable server function ServerCastSkipTraderVote(PlayerReplicationInfo PRI, bool bSkipTrader)
{
	local KFGameReplicationInfo KFGRI;

	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if(KFGRI != none)
	{
		KFGRI.RecieveVoteSkipTrader(PRI, bSkipTrader);
	}
}

reliable server function ResetSkipTrader()
{
	local KFGameReplicationInfo KFGRI;

	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if(KFGRI != none && KFGRI.VoteCollector != none)
	{
		KFGRI.VoteCollector.ConcludeVoteSkipTrader();
	}
}

/*********************************************************************************************
`* Map Vote
********************************************************************************************* */

simulated function CastMapVote(int MapIndex, bool bDoubleClick)
{
	local KFGameInfo KFGI;

	ServerCastMapVote(self, KFGameReplicationInfo(WorldInfo.GRI).VoteCollector.MapList[MapIndex]);

	if(WorldInfo.NetMode == NM_StandAlone)
	{
		KFGI = KFGameInfo(WorldInfo.Game);
		if(KFGI != none)
		{
			//do insta map travel because we are impatient
			KFGI.UpdateCurrentMapVoteTime( bDoubleClick ? 0 : 5 );//insta ish
		}
	}
}

reliable server function ServerCastMapVote(PlayerReplicationInfo PRI, string MapName)
{
	local KFGameReplicationInfo kfGRI;
	local KFGameInfo KFGI;

	KFGI = KFGameInfo(WorldInfo.Game);
	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if(KFGI != none && KFGI.bDisableMapVote)
	{
		return;
	}

	if(KFGRI != none && !bOnlySpectator)
	{
		if(WorldInfo.NetMode == NM_StandAlone)
		{
			KFGRI.ReceiveVoteMap(PRI, KFGameReplicationInfo(WorldInfo.GRI).VoteCollector.MapList.Find(MapName));
		}
		else
		{
			KFGRI.ReceiveVoteMap(PRI, KFGI.GameMapCycles[KFGI.ActiveMapCycle].Maps.Find(MapName));
		}
	}
}

reliable client function RecieveAARMapOption(string MapOption)
{
	local KFGameReplicationInfo kfGRI;
	KFGRI = KFGameReplicationInfo(WorldInfo.GRI);

	if(KFGRI != none && KFGRI.VoteCollector != none)
	{
		KFGRI.VoteCollector.AddMapOption(MapOption);
	}
}

reliable client function RecieveTopMaps(TopVotes VoteObject)
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

	if(KFPC != none && KFPC.MyGFxManager != none && KFPC.MyGFxManager.PostGameMenu != none)
	{
		KFPC.MyGFxManager.PostGameMenu.RecieveTopMaps( VoteObject );
	}
}

/*********************************************************************************************
`* Character Customization
********************************************************************************************* */

native reliable server private event ServerSetSharedUnlocks(int NewUnlocks);

native reliable server private event ServerSetCharacterCustomization(CustomizationInfo NewMeshInfo);

native reliable server private event ServerSetCurrentHeadShotEffect(int ItemID);

simulated final function int GetHeadShotEffectID()
{
	return CurrentHeadShotEffectID;
}

native private function bool SaveCharacterConfig();
native private function bool LoadCharacterConfig(out int CharacterIndex);
native private function RetryCharacterOwnership();
native function ClearCharacterAttachment(int AttachmentIndex);

simulated function ClientInitialize(Controller C)
{
	local OnlineContentInterface OnlineContentInt;

	// workaround for repeated repnotify!?!?
	if ( Role < ROLE_Authority && C == Owner )
	{
		return;
	}

	// Ignore the super since NotifyLocalPlayerTeamReceived is never used
	SetOwner( C );

	if ( C.IsLocalController() )
	{
		KFPlayerController(C).InitializeStats();
		SelectCharacter();

		// checked for owned titles that might unlock shared content (e.g. Road Redemption)
		if (class'WorldInfo'.static.IsConsoleBuild())
		{
			OnlineContentInt = class'GameEngine'.static.GetOnlineSubsystem().ContentInterface;
			OnlineContentInt.ClearCrossTitleContentList(0, OCT_Game);
			OnlineContentInt.AddReadCrossTitleContentCompleteDelegate(
				0, OCT_Game, SharedContentInitChain_CrossTitleContentRead);
			if (!OnlineContentInt.ReadCrossTitleContentList(0, OCT_Game))
			{
				SharedContentInitChain_CrossTitleContentRead(true);
			}
		}
		else
		{
			SharedContentInitChain_CrossTitleContentRead(true);
		}
	}
}

simulated function SharedContentInitChain_CrossTitleContentRead(bool bWasSuccessful)
{
	if (class'WorldInfo'.static.IsConsoleBuild())
	{
		class'GameEngine'.static.GetOnlineSubsystem().ContentInterface.
			ClearReadCrossTitleContentCompleteDelegate(0, OCT_Game, SharedContentInitChain_CrossTitleContentRead);

		if (class'GameEngine'.static.GetOnlineSubsystem().CurrentInventory.Length == 0)
		{
			class'GameEngine'.static.GetPlayfabInterface().
				AddInventoryReadCompleteDelegate(SharedContentInitChain_InventoryRead_Playfab);
		}
		else
		{
			SharedContentInitChain_InventoryRead_Playfab(true);
		}
	}
	else
	{
		if (class'GameEngine'.static.GetOnlineSubsystem().bInventoryReady == false)
		{
			class'GameEngine'.static.GetOnlineSubsystem().
				AddOnInventoryReadCompleteDelegate(SharedContentInitChain_InventoryRead_Steamworks);
		}
		else
		{
			SharedContentInitChain_InventoryRead_Steamworks();
		}
	}
}

simulated function SharedContentInitChain_InventoryRead_Steamworks()
{
	class'GameEngine'.static.GetOnlineSubsystem().
		ClearOnInventoryReadCompleteDelegate(SharedContentInitChain_InventoryRead_Steamworks);

	// At this point, inventory should both be available
	class'KFUnlockManager'.static.InitSharedUnlocksFor(self);
}

simulated function SharedContentInitChain_InventoryRead_Playfab(bool bWasSuccessful)
{
	local array<OnlineCrossTitleContent> CrossTitleContent;

	class'GameEngine'.static.GetPlayfabInterface().
		ClearInventoryReadCompleteDelegate(SharedContentInitChain_InventoryRead_Playfab);

	// At this point, cross title content and inventory should both be available
	class'GameEngine'.static.GetOnlineSubsystem().
		ContentInterface.GetCrossTitleContentList(0, OCT_Game, CrossTitleContent);
	class'KFUnlockManager'.static.InitSharedUnlocksFor(self, CrossTitleContent);
}

function OnInventoryReadComplete_Steamworks()
{
	class'GameEngine'.static.GetOnlineSubsystem().ClearOnInventoryReadCompleteDelegate(OnInventoryReadComplete_Steamworks);
	bWaitingForInventory = false;
	SelectCharacter(WaitingForInventoryCharacterIndex);
}

function OnInventoryReadComplete_Playfab(bool bWasSuccessful)
{
	if(bWasSuccessful)
	{
		// select character requires the inventory be set up, so keep this callback around until the inventory read is successful
		class'GameEngine'.static.GetPlayfabInterface().ClearInventoryReadCompleteDelegate(OnInventoryReadComplete_Playfab);
		bWaitingForInventory = false;
		SelectCharacter(WaitingForInventoryCharacterIndex);
	}
}

/**
 * Network: Local Player
 * INDEX_NONE will load last character from config
 */
simulated event SelectCharacter( optional int CharIndex=INDEX_None, optional bool bWaitForInventory=FALSE )
{
	local OnlineProfileSettings Settings;

	// If settings are not loaded yet try again later via OnReadProfileSettingsComplete()
	Settings = class'GameEngine'.static.GetOnlineSubsystem().PlayerInterface.GetProfileSettings( LocalPlayer(GetALocalPlayerController().Player).ControllerId );
	if( Settings == none )
	{
		`log("Not selecting character just yet since there's no profile settings");
		return;
	}

	// We need to wait for the online subsystem inventory to be loaded in order to determine whether
	// the requested character is available to select. If the inventory isn't ready, wait for it and
	// handle selection after the callback.
	// @TODO JDR: should this actually ignore additional calls to SelectCharacter while waiting for inventory?
	if (bWaitForInventory)
	{
		if (bWaitingForInventory)
		{
			return;
		}
		else
		{
			//@SABER_EGS IsEosBuild() case added
			if (class'WorldInfo'.static.IsConsoleBuild() || class'WorldInfo'.static.IsEosBuild())
			{
				if (class'GameEngine'.static.GetOnlineSubsystem().CurrentInventory.Length == 0)
				{
					class'GameEngine'.static.GetPlayfabInterface().AddInventoryReadCompleteDelegate(OnInventoryReadComplete_Playfab);
					WaitingForInventoryCharacterIndex = CharIndex;
					bWaitingForInventory = true;
					return;
				}
			}
			else if (!class'GameEngine'.static.GetOnlineSubsystem().bInventoryReady)
			{
				class'GameEngine'.static.GetOnlineSubsystem().AddOnInventoryReadCompleteDelegate(OnInventoryReadComplete_Steamworks);
				WaitingForInventoryCharacterIndex = CharIndex;
				bWaitingForInventory = true;
				return;
			}
		}
	}

	LoadCharacterConfig(CharIndex);

	// Make sure we get a valid character
	if(!class'KFUnlockManager'.static.GetAvailable(CharacterArchetypes[CharIndex]))
	{
		CharIndex = GetAnyAvailableCharacter(CharIndex);
		LoadCharacterConfig(CharIndex);
	}

	// Resave, in case of invalid selection, and then replicate
	Settings.SetProfileSettingValueInt(KFID_StoredCharIndex, CharIndex);
	if ( Role < Role_Authority )
    {
		ServerSetCharacterCustomization( RepCustomizationInfo );
	}
	else
	{
		CharacterCustomizationChanged();
	}
}

/** Returns the first available unlocked character */
private simulated function int GetAnyAvailableCharacter(byte CharIndex)
{
	local byte i;

	for (i = 0; i < CharacterArchetypes.Length; i++)
	{
		if(class'KFUnlockManager'.static.GetAvailable(CharacterArchetypes[CharIndex]))
		{
			return i;
		}
	}

	return 0;
}

/** Config section name for a given character archetype index */
private simulated event string GetCharacterConfigSection(int Idx)
{
	return PathName(CharacterArchetypes[Idx]);
}

/** Called when RepCustomizationInfo is modified. Network: All*/
simulated event CharacterCustomizationChanged()
{
	local KFPawn_Human KFP;
	local KFCharacterInfoBase NewCharArch;

	`AnalyticsLog(("character_change", self, CharacterArchetypes[RepCustomizationInfo.CharacterIndex].Name));

	foreach WorldInfo.AllPawns(class'KFPawn_Human', KFP)
	{
		if (KFP.PlayerReplicationInfo == self ||
			(KFP.DrivenVehicle != None && KFP.DrivenVehicle.PlayerReplicationInfo == self))
		{
			NewCharArch = CharacterArchetypes[RepCustomizationInfo.CharacterIndex];

			if( NewCharArch != KFP.CharacterArch )
			{
				// selected a new character
				KFP.SetCharacterArch( NewCharArch );
			}
			else if( WorldInfo.NetMode != NM_DedicatedServer )
			{
				// refresh cosmetics only
   				KFP.CharacterArch.SetCharacterMeshFromArch( KFP, self );
			}
		}
	}
}

reliable server private event ServerAnnounceNewSharedContent()
{
	if( WorldInfo.GRI != None && WorldInfo.GRI.bMatchHasBegun )
	{
		BroadcastLocalizedMessage( class'KFLocalMessage_Game', GMT_UserSharingContent, self);
	}
}

simulated event CurrentHeadShotEffectIdChanged()
{
	StartLoadHeadshotFxContent();
}

/*********************************************************************************************
* General
********************************************************************************************* */

function PlayerReplicationInfo Duplicate()
{
	local KFPlayerReplicationInfo NewKFPRI;

	NewKFPRI = KFPlayerReplicationInfo(super.Duplicate());
	NewKFPRI.LastQuitTime = LastQuitTime;
	NewKFPRI.NumTimesReconnected = NumTimesReconnected;
	return NewKFPRI;
}

function OverrideWith(PlayerReplicationInfo PRI)
{
	super.OverrideWith(PRI);

	// while super sets Team, SetPlayerTeam sets other important stuff, too
	SetPlayerTeam(Team);
}

function SetPlayerTeam( TeamInfo NewTeam )
{
	if( NewTeam != Team )
	{
		DamageDealtOnTeam = 0;
	}

	super.SetPlayerTeam( NewTeam );

	KFPlayerOwner = KFPlayerController( Owner );

	SetTimer( 1.f, true, nameOf(UpdateReplicatedVariables) );
}

function UpdateReplicatedVariables()
{
	if( !bIsSpectator &&
		KFPlayerOwner != none &&
		KFPlayerOwner.GetTeamNum() == 0 &&
		KFPlayerOwner.Pawn != none &&
		KFPlayerOwner.Pawn.IsAliveAndWell() )
	{
		UpdatePawnLocation();
	}
	else if( !IsZero( PawnLocationCompressed ) )
	{
		PawnLocationCompressed = vect(0,0,0);
	}

	UpdateReplicatedPlayerHealth();
}

/** Called once per second while on the human team to refresh replicated position */
function UpdatePawnLocation()
{
	PawnLocationCompressed = KFPlayerOwner.Pawn.Location;

	// Compress
	PawnLocationCompressed *= 0.01f;
}

function UpdateReplicatedPlayerHealth()
{
	local Pawn OwnerPawn;

	if( KFPlayerOwner != none )
	{
		OwnerPawn = KFPlayerOwner.Pawn;
		if( OwnerPawn != none && OwnerPawn.Health != PlayerHealth )
		{
			PlayerHealth = OwnerPawn.Health;
			PlayerHealthPercent = FloatToByte( float(OwnerPawn.Health) / float(OwnerPawn.HealthMax) );
		}
	}
}

simulated function SetSmoothedPawnIconLocation( vector NewLocation )
{
	LastReplicatedSmoothedLocation = NewLocation;
}

/** Return location used for overhead icon */
simulated function vector GetSmoothedPawnIconLocation(float BlendSpeed)
{
	local vector UncompressedLocation;

	UncompressedLocation = PawnLocationCompressed * 100.f;

	// if new location is nearby add some quick and dirty blending
	// @note: We're faking timestep and making a few assumptions about the HUD
	if ( BlendSpeed > 0 && !IsZero(UncompressedLocation) && VSizeSq(UncompressedLocation - LastReplicatedSmoothedLocation) < Square(768) )
	{
		LastReplicatedSmoothedLocation = VInterpTo( LastReplicatedSmoothedLocation,
                                UncompressedLocation, WorldInfo.DeltaSeconds,
                                VSize(UncompressedLocation - LastReplicatedSmoothedLocation) * BlendSpeed );
	}
	else
	{
		LastReplicatedSmoothedLocation = UncompressedLocation;
	}

	return LastReplicatedSmoothedLocation;
}

simulated function SetPlayerReady( bool bReady )
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);

   	bReadyToPlay = bReady;
	ServerSetPlayerReady( bReady );
	if(KFPC != none && KFPC.LEDEffectsManager != none)
	{
		KFPC.LEDEffectsManager.PlayEffectSetReady(bReadyToPlay);
	}
}

reliable server private function ServerSetPlayerReady( bool bReady )
{
	bReadyToPlay = bReady;
}

/** Called on server to +/- dosh.  Do not modify score directly */
function AddDosh( int DoshAmount, optional bool bEarned )
{
    //If the game has turned off dosh earning for this PRI, early out.
    if (!bAllowDoshEarning && bEarned)
    {
        return;
    }

	// Dosh is stored in PRI->Score
	Score = Max(0, Score + DoshAmount);

    // If the trader menu is open, update our menus dosh
	if ( WorldInfo.NetMode == NM_ListenServer )
	{
		UpdateTraderDosh();
	}

	// If player worked for their dosh track it with the AAR
	if ( bEarned && DoshAmount > 0 )
	{
		`RecordAARIntStat(KFPlayerController(Owner), DOSH_EARNED, DoshAmount);
	}
}

/** Update the dosh in our trader menu if it's open */
simulated function UpdateTraderDosh()
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(Owner);
	if( KFPC != none && KFPC.bClientTraderMenuOpen )
	{
		KFPC.NotifyTraderDoshChanged();
	}
}

simulated event Destroyed()
{
	NotifyHUDofPRIDestroyed();
	super.Destroyed();
}

simulated function NotifyHUDofPRIDestroyed()
{
	local KFPlayerController KFPC;

	KFPC = KFPlayerController(GetALocalPlayerController());
	ServerNotifyStopVOIP();
	if( KFPC != none )
	{
		// notify HUD of player disconnect
		if( KFPC.MyGFxHUD != none )
		{
			KFPC.MyGFxHUD.NotifyHUDofPRIDestroyed(self);
	}

		// notify movie manager of remote player disconnect
		if( KFPC != Owner && KFPC.MyGFxManager != None )
	{
			KFPC.MyGFxManager.RemotePlayerDisconnected(UniqueId);
		}
	}
}

/**
 * @brief Increments the death count for the player
 * @details Chris: We have to overwrite it here because people might reconnect
 * 			in the lobby. Disconnecting means death and death means no start dosh.
 * @param Amt num deaths (How often can you die simultaniusly?)
 */
function IncrementDeaths( optional int Amt = 1 )
{
	if( WorldInfo.GRI != None && WorldInfo.GRI.bMatchHasBegun )
	{
		super.IncrementDeaths( Amt );
	}

	PawnLocationCompressed = vect(0,0,0);
}

reliable client function MarkSupplierOwnerUsed( KFPlayerReplicationInfo SupplierPRI, optional bool bReceivedPrimary=true, optional bool bReceivedSecondary=true )
{
	if( SupplierPRI != none )
	{
		SupplierPRI.MarkSupplierUsed( bReceivedPrimary, bReceivedSecondary );
	}
}

simulated function MarkSupplierUsed( bool bReceivedPrimary, bool bReceivedSecondary )
{
	bPerkPrimarySupplyUsed = bPerkPrimarySupplyUsed || bReceivedPrimary;
	bPerkSecondarySupplyUsed = bPerkSecondarySupplyUsed || bReceivedSecondary;
}

simulated function ResetSupplierUsed()
{
	local array<KFPlayerReplicationInfo> KFPRIArray;
	local int i;

	KFGameReplicationInfo(WorldInfo.GRI).GetKFPRIArray( KFPRIArray );

	for( i = 0; i < KFPRIArray.Length; ++i )
	{
		KFPRIArray[i].bPerkPrimarySupplyUsed = false;
		KFPRIArray[i].bPerkSecondarySupplyUsed = false;
	}
}

/**
 * Called when a wave has ended.
 * Network: ALL
 */
simulated function NotifyWaveEnded()
{
	bVotedToSkipTraderTime = false;
	
	/*local KFGameReplicationInfo KFGRI;

	if( Role == ROLE_Authority )
	{
		KFGRI = KFGameReplicationInfo(WorldInfo.GRI);
		if (KFGRI != none)
		{
			KFGRI.VoteCollector.ResetSkipTraderBeforeWaveStarts();
		}
	}*/
}

//reset the icons here
/**
* Called when a wave has Started.
* Network: ALL
*/
simulated function NotifyWaveStart()
{
	bPerkPrimarySupplyUsed = false;
	bPerkSecondarySupplyUsed = false;
}


// BWJ - 10-5-16 - Check to see if player has had initial spawn. used for PS4 realtime multiplay
native simulated function bool HasHadInitialSpawn();


simulated function SetCurrentVoiceCommsRequest(int NewValue)
{
	//cast it
	CurrentVoiceCommsRequest = EVoiceCommsType(NewValue);
	bNetDirty = true;
	//clear timers
	ClearVoiceCommsRequest();
	//set timers
	SetCurrentIconToVoiceCommsIcon();
}

//this and SetCurrentIconToVoiceCommsIcon used for flashing last voice comms request icon
simulated function SetCurrentIconToPerkIcon()
{
	CurrentVoiceCommsRequest = VCT_NONE;
	bNetDirty = true;
	ClearVoiceCommsRequest();
}

simulated function SetCurrentIconToVoiceCommsIcon()
{
	SetTimer( VoiceCommsStatusDisplayInterval, false, nameof(SetCurrentIconToPerkIcon) );
}

simulated function ClearVoiceCommsRequest()
{
	ClearTimer('SetCurrentIconToPerkIcon');
}

simulated function Texture2D GetCurrentIconToDisplay()
{
	if(CurrentVoiceCommsRequest == VCT_NONE && CurrentPerkClass != none)
	{
		return CurrentPerkClass.default.PerkIcon;
	}

	return class'KFLocalMessage_VoiceComms'.default.VoiceCommsIcons[CurrentVoiceCommsRequest];
}

defaultproperties
{
	// Playable characters from archetypes

	// Mr. Foster is first because he is the only playable character during console installation
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_MrFoster_archetype')

	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Alberts_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Knight_Archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.chr_briar_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Mark_archetype')
 	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Jagerhorn_Archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Ana_Archetype')
    CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Masterson_Archetype')
 	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Alan_Archetype')
 	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Coleman_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.chr_DJSkully_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Strasser_Archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_Tanaka_Archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.chr_rockabilly_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_DAR_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_MrsFoster_archetype')
	CharacterArchetypes.Add(KFCharacterInfo_Human'CHR_Playable_ARCH.CHR_BadSanta_Archetype')

	bShowNonRelevantPlayers=true

	SecondsOfGameplay=-1

    bAllowDoshEarning=true
    VoiceCommsStatusDisplayInterval=5.0f
    VoiceCommsStatusDisplayIntervalMax=1;
    VoiceCommsStatusDisplayIntervalCount=0;
	CurrentVoiceCommsRequest = VCT_NONE
	CurrentHeadShotEffectID=-1;

	bCarryingCollectible=false;
}
