untyped global function PIN_GameStart global function SetGameState global function GameState_EntitiesDidLoad global function WaittillGameStateOrHigher global function AddCallback_OnRoundEndCleanup global function SetShouldUsePickLoadoutScreen global function SetSwitchSidesBased global function SetSuddenDeathBased global function SetTimerBased global function SetShouldUseRoundWinningKillReplay global function SetRoundWinningKillReplayKillClasses global function SetRoundWinningKillReplayAttacker global function SetCallback_TryUseProjectileReplay global function ShouldTryUseProjectileReplay global function SetWinner global function SetTimeoutWinnerDecisionFunc global function AddTeamScore global function GetWinningTeamWithFFASupport global function GameState_GetTimeLimitOverride global function IsRoundBasedGameOver global function ShouldRunEvac global function GiveTitanToPlayer global function GetTimeLimit_ForGameMode struct { // used for togglable parts of gamestate bool usePickLoadoutScreen bool switchSidesBased bool suddenDeathBased bool timerBased = true int functionref() timeoutWinnerDecisionFunc // for waitingforplayers int numPlayersFullyConnected bool hasSwitchedSides int announceRoundWinnerWinningSubstr int announceRoundWinnerLosingSubstr bool roundWinningKillReplayTrackPilotKills = true bool roundWinningKillReplayTrackTitanKills = false bool gameWonThisFrame bool hasKillForGameWonThisFrame float roundWinningKillReplayTime entity roundWinningKillReplayVictim entity roundWinningKillReplayAttacker int roundWinningKillReplayInflictorEHandle // this is either the inflictor or the attacker int roundWinningKillReplayMethodOfDeath float roundWinningKillReplayTimeOfDeath float roundWinningKillReplayHealthFrac array roundEndCleanupCallbacks bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) shouldTryUseProjectileReplayCallback } file /* ██████ █████ ███ ███ ███████ ███████ ████████ █████ ████████ ███████ ███████ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███████ ██ ████ ██ █████ ███████ ██ ███████ ██ █████ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ███████ ███████ ██ ██ ██ ██ ███████ ███████ */ void function PIN_GameStart() { // todo: using the pin telemetry function here, weird and was done veeery early on before i knew how this all worked, should use a different one // called from InitGameState //FlagInit( "ReadyToStartMatch" ) SetServerVar( "switchedSides", 0 ) SetServerVar( "winningTeam", -1 ) AddCallback_GameStateEnter( eGameState.WaitingForCustomStart, GameStateEnter_WaitingForCustomStart ) AddCallback_GameStateEnter( eGameState.WaitingForPlayers, GameStateEnter_WaitingForPlayers ) AddCallback_OnClientConnected( WaitingForPlayers_ClientConnected ) AddCallback_GameStateEnter( eGameState.PickLoadout, GameStateEnter_PickLoadout ) AddCallback_GameStateEnter( eGameState.Prematch, GameStateEnter_Prematch ) AddCallback_GameStateEnter( eGameState.Playing, GameStateEnter_Playing ) AddCallback_GameStateEnter( eGameState.WinnerDetermined, GameStateEnter_WinnerDetermined ) AddCallback_GameStateEnter( eGameState.SwitchingSides, GameStateEnter_SwitchingSides ) AddCallback_GameStateEnter( eGameState.SuddenDeath, GameStateEnter_SuddenDeath ) AddCallback_GameStateEnter( eGameState.Postmatch, GameStateEnter_Postmatch ) AddCallback_OnPlayerKilled( OnPlayerKilled ) AddDeathCallback( "npc_titan", OnTitanKilled ) AddCallback_EntityChangedTeam( "player", OnPlayerChangedTeam ) RegisterSignal( "CleanUpEntitiesForRoundEnd" ) } void function GameState_EntitiesDidLoad() { if ( GetClassicMPMode() || ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) ClassicMP_SetupIntro() } void function WaittillGameStateOrHigher( int gameState ) { while ( GetGameState() < gameState ) svGlobal.levelEnt.WaitSignal( "GameStateChanged" ) } bool function ShouldTryUseProjectileReplay( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) { if ( file.shouldTryUseProjectileReplayCallback != null ) return file.shouldTryUseProjectileReplayCallback( victim, attacker, damageInfo, isRoundEnd ) // default to true (vanilla behaviour) return true } /// This is to move all NPCs that a player owns from one team to the other during a match /// Auto-Titans, Turrets, Ticks and Hacked Spectres will all move along together with the player to the new Team /// Also possibly prevents mods that spawns other types of NPCs that players can own from breaking when switching (i.e Drones, Hacked Reapers) void function OnPlayerChangedTeam( entity player ) { if ( !player.hasConnected ) // Prevents players who just joined to trigger below code, as server always pre setups their teams return if( IsIMCOrMilitiaTeam( player.GetTeam() ) ) NotifyClientsOfTeamChange( player, GetOtherTeam( player.GetTeam() ), player.GetTeam() ) foreach( npc in GetNPCArray() ) { entity bossPlayer = npc.GetBossPlayer() if ( IsValidPlayer( bossPlayer ) && bossPlayer == player && IsAlive( npc ) ) SetTeam( npc, player.GetTeam() ) } } /* ██████ █████ ███ ███ ███████ ███████ ███████ ████████ ██ ██ ██████ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███████ ██ ████ ██ █████ ███████ █████ ██ ██ ██ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ███████ ███████ ███████ ██ ██████ ██ */ void function SetGameState( int newState ) { if ( newState == GetGameState() ) return SetServerVar( "gameStateChangeTime", Time() ) SetServerVar( "gameState", newState ) svGlobal.levelEnt.Signal( "GameStateChanged" ) // added in AddCallback_GameStateEnter foreach ( callbackFunc in svGlobal.gameStateEnterCallbacks[ newState ] ) callbackFunc() } void function AddTeamScore( int team, int amount ) { GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + amount ) GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + amount ) int scoreLimit if ( IsRoundBased() ) scoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE ) else scoreLimit = GameMode_GetScoreLimit( GAMETYPE ) int score = GameRules_GetTeamScore( team ) if ( score >= scoreLimit || GetGameState() == eGameState.SuddenDeath ) SetWinner( team ) else if ( ( file.switchSidesBased && !file.hasSwitchedSides ) && score >= ( scoreLimit.tofloat() / 2.0 ) ) SetGameState( eGameState.SwitchingSides ) } void function SetWinner( int team, string winningReason = "", string losingReason = "" ) { SetServerVar( "winningTeam", team ) file.gameWonThisFrame = true thread UpdateGameWonThisFrameNextFrame() if ( winningReason.len() == 0 ) file.announceRoundWinnerWinningSubstr = 0 else file.announceRoundWinnerWinningSubstr = GetStringID( winningReason ) if ( losingReason.len() == 0 ) file.announceRoundWinnerLosingSubstr = 0 else file.announceRoundWinnerLosingSubstr = GetStringID( losingReason ) if ( GamePlayingOrSuddenDeath() ) { if ( IsRoundBased() ) { if ( team != TEAM_UNASSIGNED ) { GameRules_SetTeamScore( team, GameRules_GetTeamScore( team ) + 1 ) GameRules_SetTeamScore2( team, GameRules_GetTeamScore2( team ) + 1 ) } SetGameState( eGameState.WinnerDetermined ) ScoreEvent_RoundComplete( team ) } else { SetGameState( eGameState.WinnerDetermined ) ScoreEvent_MatchComplete( team ) RegisterMatchStats_OnMatchComplete() } } } void function SetTimeoutWinnerDecisionFunc( int functionref() callback ) { file.timeoutWinnerDecisionFunc = callback } void function SetCallback_TryUseProjectileReplay( bool functionref( entity victim, entity attacker, var damageInfo, bool isRoundEnd ) callback ) { file.shouldTryUseProjectileReplayCallback = callback } void function AddCallback_OnRoundEndCleanup( void functionref() callback ) { file.roundEndCleanupCallbacks.append( callback ) } void function SetShouldUsePickLoadoutScreen( bool shouldUse ) { file.usePickLoadoutScreen = shouldUse } void function SetSwitchSidesBased( bool switchSides ) { file.switchSidesBased = switchSides } void function SetSuddenDeathBased( bool suddenDeathBased ) { file.suddenDeathBased = suddenDeathBased } void function SetTimerBased( bool timerBased ) { file.timerBased = timerBased } void function SetShouldUseRoundWinningKillReplay( bool shouldUse ) { SetServerVar( "roundWinningKillReplayEnabled", shouldUse ) } void function SetRoundWinningKillReplayKillClasses( bool pilot, bool titan ) { file.roundWinningKillReplayTrackPilotKills = pilot file.roundWinningKillReplayTrackTitanKills = titan // player kills in titans should get tracked anyway, might be worth renaming this } void function SetRoundWinningKillReplayAttacker( entity attacker, int inflictorEHandle = -1 ) { file.roundWinningKillReplayTime = Time() file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) file.roundWinningKillReplayAttacker = attacker file.roundWinningKillReplayInflictorEHandle = inflictorEHandle == -1 ? attacker.GetEncodedEHandle() : inflictorEHandle file.roundWinningKillReplayTimeOfDeath = Time() } /* ██████ ██ ██ ███████ ████████ ██████ ███ ███ ███████ ████████ █████ ██████ ████████ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ████ ██ ███████ ██ ███████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██████ ███████ ██ ██████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ */ void function GameStateEnter_WaitingForCustomStart() { // unused in release, comments indicate this was supposed to be used for an e3 demo // perhaps games in this demo were manually started by an employee? no clue really } /* ██ ██ █████ ██ ████████ ██ ███ ██ ██████ ███████ ██████ ██████ ██████ ██ █████ ██ ██ ███████ ██████ ███████ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ███ █████ ██ ██ ██████ ██████ ██ ███████ ████ █████ ██████ ███████ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███ ██ ██ ██ ██ ██ ██ ████ ██████ ██ ██████ ██ ██ ██ ███████ ██ ██ ██ ███████ ██ ██ ███████ */ void function GameStateEnter_WaitingForPlayers() { foreach ( entity player in GetPlayerArray() ) WaitingForPlayers_ClientConnected( player ) thread WaitForPlayers() // like 90% sure there should be a way to get number of loading clients on server but idk it } void function WaitForPlayers( ) { // note: atm if someone disconnects as this happens the game will just wait forever float endTime = Time() + 30.0 while ( ( GetPendingClientsCount() != 0 && endTime > Time() ) || GetPlayerArray().len() == 0 ) WaitFrame() print( "done waiting!" ) wait 1.0 // bit nicer if ( file.usePickLoadoutScreen ) SetGameState( eGameState.PickLoadout ) else SetGameState( eGameState.Prematch ) } void function WaitingForPlayers_ClientConnected( entity player ) { if ( GetGameState() == eGameState.WaitingForPlayers ) ScreenFadeToBlackForever( player, 0.0 ) } /* ██████ ██ ██████ ██ ██ ██ ██████ █████ ██████ ██████ ██ ██ ████████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██ █████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ███████ ██████ ██ ██ ██████ ██████ ██████ ██ */ void function GameStateEnter_PickLoadout() { thread GameStateEnter_PickLoadout_Threaded() } void function GameStateEnter_PickLoadout_Threaded() { float pickloadoutLength = 20.0 // may need tweaking SetServerVar( "minPickLoadOutTime", Time() + pickloadoutLength ) // titan selection menu can change minPickLoadOutTime so we need to wait manually until we hit the time while ( Time() < GetServerVar( "minPickLoadOutTime" ) ) WaitFrame() SetGameState( eGameState.Prematch ) } /* ██████ ██████ ███████ ███ ███ █████ ████████ ██████ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██████ ██████ █████ ██ ████ ██ ███████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██████ ██ ██ */ void function GameStateEnter_Prematch() { int timeLimit = GameMode_GetTimeLimit( GAMETYPE ) * 60 if ( file.switchSidesBased ) timeLimit /= 2 // endtime is half of total per side SetServerVar( "gameEndTime", Time() + timeLimit + ClassicMP_GetIntroLength() ) SetServerVar( "roundEndTime", Time() + ClassicMP_GetIntroLength() + GameMode_GetRoundTimeLimit( GAMETYPE ) * 60 ) if ( !GetClassicMPMode() && !ClassicMP_ShouldTryIntroAndEpilogueWithoutClassicMP() ) thread StartGameWithoutClassicMP() // Initialise any spectators. Hopefully they are all initialised already in CodeCallback_OnClientConnectionCompleted // (_base_gametype_mp.gnut) but for modes like LTS this doesn't seem to happen late enough to work properly. foreach ( player in GetPlayerArray() ) { if ( IsPrivateMatchSpectator( player ) ) InitialisePrivateMatchSpectatorPlayer( player ) } } void function StartGameWithoutClassicMP() { foreach ( entity player in GetPlayerArray() ) if ( IsAlive( player ) ) player.Die() WaitFrame() // wait for callbacks to finish // need these otherwise game will complain SetServerVar( "gameStartTime", Time() ) SetServerVar( "roundStartTime", Time() ) foreach ( entity player in GetPlayerArray() ) { if ( !IsPrivateMatchSpectator( player ) ) RespawnAsPilot( player ) ScreenFadeFromBlack( player, 0 ) } SetGameState( eGameState.Playing ) } /* ██████ ██ █████ ██ ██ ██ ███ ██ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██████ ██ ███████ ████ ██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ████ ██████ */ void function GameStateEnter_Playing() { thread GameStateEnter_Playing_Threaded() } void function GameStateEnter_Playing_Threaded() { WaitFrame() // ensure timelimits are all properly set thread DialoguePlayNormal() // runs dialogue play function while ( GetGameState() == eGameState.Playing ) { // could cache these, but what if we update it midgame? float endTime if ( IsRoundBased() ) endTime = expect float( GetServerVar( "roundEndTime" ) ) else endTime = expect float( GetServerVar( "gameEndTime" ) ) // time's up! if ( Time() >= endTime && file.timerBased ) { int winningTeam if ( file.timeoutWinnerDecisionFunc != null ) winningTeam = file.timeoutWinnerDecisionFunc() else winningTeam = GetWinningTeamWithFFASupport() if ( file.switchSidesBased && !file.hasSwitchedSides && !IsRoundBased() ) // in roundbased modes, we handle this in setwinner SetGameState( eGameState.SwitchingSides ) else if ( file.suddenDeathBased && winningTeam == TEAM_UNASSIGNED ) // suddendeath if we draw and suddendeath is enabled and haven't switched sides SetGameState( eGameState.SuddenDeath ) else SetWinner( winningTeam ) } WaitFrame() } } /* ██ ██ ██ ███ ██ ███ ██ ███████ ██████ ██████ ███████ ████████ ███████ ██████ ███ ███ ██ ███ ██ ███████ ██████ ██ ██ ██ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ██ ██ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██████ ██ ██ █████ ██ █████ ██████ ██ ████ ██ ██ ██ ██ ██ █████ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███ ██ ██ ████ ██ ████ ███████ ██ ██ ██████ ███████ ██ ███████ ██ ██ ██ ██ ██ ██ ████ ███████ ██████ */ // these are likely innacurate const float ROUND_END_FADE_KILLREPLAY = 1.0 const float ROUND_END_DELAY_KILLREPLAY = 3.0 const float ROUND_END_FADE_NOKILLREPLAY = 8.0 const float ROUND_END_DELAY_NOKILLREPLAY = 10.0 void function GameStateEnter_WinnerDetermined() { thread GameStateEnter_WinnerDetermined_Threaded() } void function GameStateEnter_WinnerDetermined_Threaded() { // do win announcement int winningTeam = GetWinningTeamWithFFASupport() DialoguePlayWinnerDetermined() // play a faction dialogue when winner is determined foreach ( entity player in GetPlayerArray() ) { int announcementSubstr if ( winningTeam != TEAM_UNASSIGNED ) announcementSubstr = player.GetTeam() == winningTeam ? file.announceRoundWinnerWinningSubstr : file.announceRoundWinnerLosingSubstr if ( IsRoundBased() ) Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceRoundWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME, GameRules_GetTeamScore2( TEAM_MILITIA ), GameRules_GetTeamScore2( TEAM_IMC ) ) else Remote_CallFunction_NonReplay( player, "ServerCallback_AnnounceWinner", winningTeam, announcementSubstr, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME ) if ( player.GetTeam() == winningTeam ) UnlockAchievement( player, achievements.MP_WIN ) } WaitFrame() // wait a frame so other scripts can setup killreplay stuff // set gameEndTime to current time, so hud doesn't display time left in the match SetServerVar( "gameEndTime", Time() ) SetServerVar( "roundEndTime", Time() ) entity replayAttacker = file.roundWinningKillReplayAttacker bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !ClassicMP_ShouldRunEpilogue() && Time() - file.roundWinningKillReplayTime <= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY && winningTeam != TEAM_UNASSIGNED float replayLength = 2.0 // extra delay if no replay if ( doReplay ) { bool killcamsWereEnabled = KillcamsEnabled() if ( killcamsWereEnabled ) // dont want killcams to interrupt stuff SetKillcamsEnabled( false ) replayLength = ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY if ( "respawnTime" in replayAttacker.s && Time() - replayAttacker.s.respawnTime < replayLength ) replayLength += Time() - expect float ( replayAttacker.s.respawnTime ) SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac ) foreach ( entity player in GetPlayerArray() ) thread PlayerWatchesRoundWinningKillReplay( player, replayLength ) wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see wait replayLength WaitFrame() // prevent a race condition with PlayerWatchesRoundWinningKillReplay file.roundWinningKillReplayAttacker = null // clear this file.roundWinningKillReplayInflictorEHandle = -1 if ( killcamsWereEnabled ) SetKillcamsEnabled( true ) } else if ( IsRoundBased() || !ClassicMP_ShouldRunEpilogue() ) { // these numbers are temp and should really be based on consts of some kind foreach( entity player in GetPlayerArray() ) { player.FreezeControlsOnServer() ScreenFadeToBlackForever( player, 4.0 ) } wait ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see foreach( entity player in GetPlayerArray() ) player.UnfreezeControlsOnServer() } if ( IsRoundBased() ) { svGlobal.levelEnt.Signal( "RoundEnd" ) int roundsPlayed = expect int ( GetServerVar( "roundsPlayed" ) ) SetServerVar( "roundsPlayed", roundsPlayed + 1 ) int winningTeam = GetWinningTeamWithFFASupport() int highestScore = GameRules_GetTeamScore( winningTeam ) int roundScoreLimit = GameMode_GetRoundScoreLimit( GAMETYPE ) if ( highestScore >= roundScoreLimit ) { if ( ClassicMP_ShouldRunEpilogue() ) { ClassicMP_SetupEpilogue() SetGameState( eGameState.Epilogue ) } else SetGameState( eGameState.Postmatch ) } else if ( file.switchSidesBased && !file.hasSwitchedSides && highestScore >= ( roundScoreLimit.tofloat() / 2.0 ) ) // round up SetGameState( eGameState.SwitchingSides ) // note: switchingsides will handle setting to pickloadout and prematch by itself else if ( file.usePickLoadoutScreen ) SetGameState( eGameState.PickLoadout ) else SetGameState ( eGameState.Prematch ) } else { RegisterChallenges_OnMatchEnd() if ( ClassicMP_ShouldRunEpilogue() ) { ClassicMP_SetupEpilogue() SetGameState( eGameState.Epilogue ) } else SetGameState( eGameState.Postmatch ) } } void function PlayerWatchesRoundWinningKillReplay( entity player, float replayLength ) { // end if player dcs player.EndSignal( "OnDestroy" ) player.FreezeControlsOnServer() ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME ) wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME player.SetPredictionEnabled( false ) // prediction fucks with replays entity attacker = file.roundWinningKillReplayAttacker if ( IsValid( attacker ) ) { player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS ) player.SetKillReplayInflictorEHandle( file.roundWinningKillReplayInflictorEHandle ) player.SetKillReplayVictim( file.roundWinningKillReplayVictim ) player.SetViewIndex( attacker.GetIndexForEntity() ) player.SetIsReplayRoundWinning( true ) } if ( replayLength >= ROUND_WINNING_KILL_REPLAY_LENGTH_OF_REPLAY - 0.5 ) // only do fade if close to full length replay { // this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this wait replayLength - 2.0 ScreenFadeToBlackForever( player, 2.0 ) wait 2.0 } else wait replayLength //player.SetPredictionEnabled( true ) doesn't seem needed, as native code seems to set this on respawn player.ClearReplayDelay() player.ClearViewEntity() player.UnfreezeControlsOnServer() } /* ███████ ██ ██ ██ ████████ ██████ ██ ██ ██ ███ ██ ██████ ███████ ██ ██████ ███████ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ █ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ███ ███████ ██ ██ ██ █████ ███████ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ███ ███ ██ ██ ██████ ██ ██ ██ ██ ████ ██████ ███████ ██ ██████ ███████ ███████ */ void function GameStateEnter_SwitchingSides() { thread GameStateEnter_SwitchingSides_Threaded() } void function GameStateEnter_SwitchingSides_Threaded() { bool killcamsWereEnabled = KillcamsEnabled() if ( killcamsWereEnabled ) // dont want killcams to interrupt stuff SetKillcamsEnabled( false ) WaitFrame() // wait a frame so callbacks can set killreplay info entity replayAttacker = file.roundWinningKillReplayAttacker bool doReplay = Replay_IsEnabled() && IsRoundWinningKillReplayEnabled() && IsValid( replayAttacker ) && !IsRoundBased() // for roundbased modes, we've already done the replay && Time() - file.roundWinningKillReplayTime <= SWITCHING_SIDES_DELAY float replayLength = SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay if ( doReplay ) { replayLength = SWITCHING_SIDES_DELAY if ( "respawnTime" in replayAttacker.s && Time() - replayAttacker.s.respawnTime < replayLength ) replayLength += Time() - expect float ( replayAttacker.s.respawnTime ) SetServerVar( "roundWinningKillReplayEntHealthFrac", file.roundWinningKillReplayHealthFrac ) } foreach ( entity player in GetPlayerArray() ) thread PlayerWatchesSwitchingSidesKillReplay( player, doReplay, replayLength ) wait SWITCHING_SIDES_DELAY_REPLAY CleanUpEntitiesForRoundEnd() // fade should be done by this point, so cleanup stuff now when people won't see wait replayLength if ( killcamsWereEnabled ) SetKillcamsEnabled( true ) file.hasSwitchedSides = true svGlobal.levelEnt.Signal( "RoundEnd" ) // might be good to get a new signal for this? not 100% necessary tho i think SetServerVar( "switchedSides", 1 ) file.roundWinningKillReplayAttacker = null // reset this after replay file.roundWinningKillReplayInflictorEHandle = -1 if ( file.usePickLoadoutScreen ) SetGameState( eGameState.PickLoadout ) else SetGameState ( eGameState.Prematch ) } void function PlayerWatchesSwitchingSidesKillReplay( entity player, bool doReplay, float replayLength ) { player.EndSignal( "OnDestroy" ) player.FreezeControlsOnServer() ScreenFadeToBlackForever( player, SWITCHING_SIDES_DELAY_REPLAY ) // automatically cleared wait SWITCHING_SIDES_DELAY_REPLAY if ( doReplay ) { player.SetPredictionEnabled( false ) // prediction fucks with replays // delay seems weird for switchingsides? ends literally the frame the flag is collected entity attacker = file.roundWinningKillReplayAttacker player.SetKillReplayDelay( Time() - replayLength, THIRD_PERSON_KILL_REPLAY_ALWAYS ) player.SetKillReplayInflictorEHandle( file.roundWinningKillReplayInflictorEHandle ) player.SetKillReplayVictim( file.roundWinningKillReplayVictim ) player.SetViewIndex( attacker.GetIndexForEntity() ) player.SetIsReplayRoundWinning( true ) if ( replayLength >= SWITCHING_SIDES_DELAY - 0.5 ) // only do fade if close to full length replay { // this doesn't work because fades don't work on players that are in a replay, unsure how official servers do this wait replayLength - ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME ScreenFadeToBlackForever( player, ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME ) wait ROUND_WINNING_KILL_REPLAY_SCREEN_FADE_TIME } else wait replayLength } else wait SWITCHING_SIDES_DELAY_REPLAY // extra delay if no replay //player.SetPredictionEnabled( true ) doesn't seem needed, as native code seems to set this on respawn player.ClearReplayDelay() player.ClearViewEntity() } /* ███████ ██ ██ ██████ ██████ ███████ ███ ██ ██████ ███████ █████ ████████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ █████ ███████ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██████ ██████ ██████ ███████ ██ ████ ██████ ███████ ██ ██ ██ ██ ██ */ void function GameStateEnter_SuddenDeath() { // disable respawns, suddendeath calling is done on a kill callback SetRespawnsEnabled( false ) // defensive fixes, so game won't stuck in SuddenDeath forever bool mltElimited = false bool imcElimited = false if( GetPlayerArrayOfTeam_Alive( TEAM_MILITIA ).len() < 1 ) mltElimited = true if( GetPlayerArrayOfTeam_Alive( TEAM_IMC ).len() < 1 ) imcElimited = true if( mltElimited && imcElimited ) SetWinner( TEAM_UNASSIGNED ) else if( mltElimited ) SetWinner( TEAM_IMC ) else if( imcElimited ) SetWinner( TEAM_MILITIA ) } /* ██████ ██████ ███████ ████████ ███ ███ █████ ████████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ████ ██ ███████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ███████ ██ ██ ██ ██ ██ ██ ██████ ██ ██ */ void function GameStateEnter_Postmatch() { foreach ( entity player in GetPlayerArray() ) { player.FreezeControlsOnServer() thread ForceFadeToBlack( player ) } thread GameStateEnter_Postmatch_Threaded() } void function GameStateEnter_Postmatch_Threaded() { wait GAME_POSTMATCH_LENGTH GameRules_EndMatch() } void function ForceFadeToBlack( entity player ) { // todo: check if this is still necessary player.EndSignal( "OnDestroy" ) // hack until i figure out what deathcam stuff is causing fadetoblacks to be cleared while ( true ) { WaitFrame() ScreenFadeToBlackForever( player, 0.0 ) } } /* ██ ██ ██ ██ ██ ██████ █████ ██ ██ ██████ █████ ██████ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ███████ ██ ██ ██████ ███████ ██ █████ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ███████ ██████ ██ ██ ███████ ███████ ██████ ██ ██ ██████ ██ ██ ███████ */ void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) { if ( !GamePlayingOrSuddenDeath() ) { if ( file.gameWonThisFrame ) { if ( file.hasKillForGameWonThisFrame ) return } else return } entity inflictor = DamageInfo_GetInflictor( damageInfo ) bool shouldUseInflictor = IsValid( inflictor ) && ShouldTryUseProjectileReplay( victim, attacker, damageInfo, true ) // set round winning killreplay info here if we're tracking pilot kills // todo: make this not count environmental deaths like falls, unsure how to prevent this if ( file.roundWinningKillReplayTrackPilotKills && victim != attacker && attacker != svGlobal.worldspawn && IsValid( attacker ) ) { if ( file.gameWonThisFrame ) file.hasKillForGameWonThisFrame = true file.roundWinningKillReplayTime = Time() file.roundWinningKillReplayVictim = victim file.roundWinningKillReplayAttacker = attacker file.roundWinningKillReplayInflictorEHandle = ( shouldUseInflictor ? inflictor : attacker ).GetEncodedEHandle() file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo ) file.roundWinningKillReplayTimeOfDeath = Time() file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) } if ( ( Riff_EliminationMode() == eEliminationMode.Titans || Riff_EliminationMode() == eEliminationMode.PilotsTitans ) && victim.IsTitan() ) // need an extra check for this OnTitanKilled( victim, damageInfo ) if ( !GamePlayingOrSuddenDeath() ) return // note: pilotstitans is just win if enemy team runs out of either pilots or titans if ( IsPilotEliminationBased() || GetGameState() == eGameState.SuddenDeath ) { if ( GetPlayerArrayOfTeam_Alive( victim.GetTeam() ).len() == 0 ) { // for ffa we need to manually get the last team alive if ( IsFFAGame() ) { array teamsWithLivingPlayers foreach ( entity player in GetPlayerArray_Alive() ) { if ( !teamsWithLivingPlayers.contains( player.GetTeam() ) ) teamsWithLivingPlayers.append( player.GetTeam() ) } if ( teamsWithLivingPlayers.len() == 1 ) SetWinner( teamsWithLivingPlayers[ 0 ], "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" ) else if ( teamsWithLivingPlayers.len() == 0 ) // failsafe: only team was the dead one SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" ) // this is fine in ffa } else SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_PILOTS_ELIMINATED", "#GAMEMODE_FRIENDLY_PILOTS_ELIMINATED" ) } } } void function OnTitanKilled( entity victim, var damageInfo ) { if ( !GamePlayingOrSuddenDeath() ) { if ( file.gameWonThisFrame ) { if ( file.hasKillForGameWonThisFrame ) return } else return } entity inflictor = DamageInfo_GetInflictor( damageInfo ) bool shouldUseInflictor = IsValid( inflictor ) && ShouldTryUseProjectileReplay( victim, DamageInfo_GetAttacker( damageInfo ), damageInfo, true ) // set round winning killreplay info here if we're tracking titan kills // todo: make this not count environmental deaths like falls, unsure how to prevent this entity attacker = DamageInfo_GetAttacker( damageInfo ) if ( file.roundWinningKillReplayTrackTitanKills && victim != attacker && attacker != svGlobal.worldspawn && IsValid( attacker ) ) { if ( file.gameWonThisFrame ) file.hasKillForGameWonThisFrame = true file.roundWinningKillReplayTime = Time() file.roundWinningKillReplayVictim = victim file.roundWinningKillReplayAttacker = attacker file.roundWinningKillReplayInflictorEHandle = ( shouldUseInflictor ? inflictor : attacker ).GetEncodedEHandle() file.roundWinningKillReplayMethodOfDeath = DamageInfo_GetDamageSourceIdentifier( damageInfo ) file.roundWinningKillReplayTimeOfDeath = Time() file.roundWinningKillReplayHealthFrac = GetHealthFrac( attacker ) } if ( !GamePlayingOrSuddenDeath() ) return // note: pilotstitans is just win if enemy team runs out of either pilots or titans if ( IsTitanEliminationBased() ) { int livingTitans foreach ( entity titan in GetTitanArrayOfTeam( victim.GetTeam() ) ) livingTitans++ if ( livingTitans == 0 ) { // for ffa we need to manually get the last team alive if ( IsFFAGame() ) { array teamsWithLivingTitans foreach ( entity titan in GetTitanArray() ) { if ( !teamsWithLivingTitans.contains( titan.GetTeam() ) ) teamsWithLivingTitans.append( titan.GetTeam() ) } if ( teamsWithLivingTitans.len() == 1 ) SetWinner( teamsWithLivingTitans[ 0 ], "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" ) else if ( teamsWithLivingTitans.len() == 0 ) // failsafe: only team was the dead one SetWinner( TEAM_UNASSIGNED, "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" ) // this is fine in ffa } else SetWinner( GetOtherTeam( victim.GetTeam() ), "#GAMEMODE_ENEMY_TITANS_DESTROYED", "#GAMEMODE_FRIENDLY_TITANS_DESTROYED" ) } } } /* ████████ ██████ ██████ ██ ███████ ██ ██ ███ ██ ██████ ████████ ██ ██████ ███ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████ ██████ ███████ ██ ██████ ██ ████ ██████ ██ ██ ██████ ██ ████ ███████ */ void function CleanUpEntitiesForRoundEnd() { // this function should clean up any and all entities that need to be removed between rounds, ideally at a point where it isn't noticable to players SetPlayerDeathsHidden( true ) // hide death sounds and such so people won't notice they're dying foreach ( entity player in GetPlayerArray() ) { ClearTitanAvailable( player ) PROTO_CleanupTrackedProjectiles( player ) player.SetPlayerNetInt( "batteryCount", 0 ) if ( IsAlive( player ) ) player.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.round_end } ) } foreach ( entity npc in GetNPCArray() ) { if ( !IsValid( npc ) || !IsAlive( npc ) ) continue // kill rather than destroy, as destroying will cause issues with children which is an issue especially for dropships and titans npc.Die( svGlobal.worldspawn, svGlobal.worldspawn, { damageSourceId = eDamageSourceId.round_end } ) } // destroy weapons ClearDroppedWeapons() foreach ( entity battery in GetEntArrayByClass_Expensive( "item_titan_battery" ) ) battery.Destroy() // allow other scripts to clean stuff up too svGlobal.levelEnt.Signal( "CleanUpEntitiesForRoundEnd" ) foreach ( void functionref() callback in file.roundEndCleanupCallbacks ) callback() SetPlayerDeathsHidden( false ) } void function UpdateGameWonThisFrameNextFrame() { WaitFrame() file.gameWonThisFrame = false file.hasKillForGameWonThisFrame = false } int function GetWinningTeamWithFFASupport() { if ( !IsFFAGame() ) return GameScore_GetWinningTeam() else { // custom logic for calculating ffa winner as GameScore_GetWinningTeam doesn't handle this int winningTeam = TEAM_UNASSIGNED int winningScore = 0 foreach ( entity player in GetPlayerArray() ) { int currentScore = GameRules_GetTeamScore( player.GetTeam() ) if ( currentScore == winningScore ) winningTeam = TEAM_UNASSIGNED // if 2 teams are equal, return TEAM_UNASSIGNED else if ( currentScore > winningScore ) { winningTeam = player.GetTeam() winningScore = currentScore } } return winningTeam } unreachable } float function GameState_GetTimeLimitOverride() { return 100 } bool function IsRoundBasedGameOver() { return false } bool function ShouldRunEvac() { return true } void function GiveTitanToPlayer( entity player ) { } float function GetTimeLimit_ForGameMode() { string mode = GameRules_GetGameMode() string playlistString = "timelimit" // default to 10 mins, because that seems reasonable return GetCurrentPlaylistVarFloat( playlistString, 10 ) } void function DialoguePlayNormal() { int totalScore = GameMode_GetScoreLimit( GameRules_GetGameMode() ) int winningTeam int losingTeam float diagIntervel = 71 // play a faction dailogue every 70 + 1s to prevent play together with winner dialogue while( GetGameState() == eGameState.Playing ) { wait diagIntervel if( GameRules_GetTeamScore( TEAM_MILITIA ) < GameRules_GetTeamScore( TEAM_IMC ) ) { winningTeam = TEAM_IMC losingTeam = TEAM_MILITIA } if( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) ) { winningTeam = TEAM_MILITIA losingTeam = TEAM_IMC } if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) >= totalScore * 0.4 ) { PlayFactionDialogueToTeam( "scoring_winningLarge", winningTeam ) PlayFactionDialogueToTeam( "scoring_losingLarge", losingTeam ) } else if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) <= totalScore * 0.2 ) { PlayFactionDialogueToTeam( "scoring_winningClose", winningTeam ) PlayFactionDialogueToTeam( "scoring_losingClose", losingTeam ) } else if( GameRules_GetTeamScore( winningTeam ) == GameRules_GetTeamScore( losingTeam ) ) { continue } else { PlayFactionDialogueToTeam( "scoring_winning", winningTeam ) PlayFactionDialogueToTeam( "scoring_losing", losingTeam ) } } } void function DialoguePlayWinnerDetermined() { int totalScore = GameMode_GetScoreLimit( GameRules_GetGameMode() ) int winningTeam int losingTeam if( GameRules_GetTeamScore( TEAM_MILITIA ) < GameRules_GetTeamScore( TEAM_IMC ) ) { winningTeam = TEAM_IMC losingTeam = TEAM_MILITIA } if( GameRules_GetTeamScore( TEAM_MILITIA ) > GameRules_GetTeamScore( TEAM_IMC ) ) { winningTeam = TEAM_MILITIA losingTeam = TEAM_IMC } if( IsRoundBased() ) // check for round based modes { if( GameRules_GetTeamScore( winningTeam ) != GameMode_GetRoundScoreLimit( GAMETYPE ) ) // no winner dialogue till game really ends return } if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) >= totalScore * 0.4 ) { PlayFactionDialogueToTeam( "scoring_wonMercy", winningTeam ) PlayFactionDialogueToTeam( "scoring_lostMercy", losingTeam ) } else if( GameRules_GetTeamScore( winningTeam ) - GameRules_GetTeamScore( losingTeam ) <= totalScore * 0.2 ) { PlayFactionDialogueToTeam( "scoring_wonClose", winningTeam ) PlayFactionDialogueToTeam( "scoring_lostClose", losingTeam ) } else if( GameRules_GetTeamScore( winningTeam ) == GameRules_GetTeamScore( losingTeam ) ) { PlayFactionDialogueToTeam( "scoring_tied", winningTeam ) PlayFactionDialogueToTeam( "scoring_tied", losingTeam ) } else { PlayFactionDialogueToTeam( "scoring_won", winningTeam ) PlayFactionDialogueToTeam( "scoring_lost", losingTeam ) } }