From e158a247522230d97c5b16ed62af1969c626fd73 Mon Sep 17 00:00:00 2001 From: BobTheBob9 Date: Fri, 15 Jul 2022 21:19:14 +0100 Subject: fix race condition crash when boarding dropship as it explodes --- .../mod/scripts/vscripts/evac/_evac.gnut | 858 +++++++++++---------- 1 file changed, 430 insertions(+), 428 deletions(-) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut index bce8b4c7..da623dbf 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut @@ -1,428 +1,430 @@ -untyped -global function Evac_Init -global function AddEvacNode -global function SetEvacSpaceNode -global function IsEvacDropship - -global function EvacEpilogueSetup -global function Evac - -const float EVAC_INITIAL_WAIT = 5.0 -const float EVAC_ARRIVAL_TIME = 40.0 -const float EVAC_WAIT_TIME = 18.0 - -const int EVAC_SHIP_HEALTH = 25000 -const int EVAC_SHIP_SHIELDS = 2000 - -// we don't use these because they're busted, just keeping them -const array EVAC_EMBARK_ANIMS_1P = [ - "ptpov_e3_rescue_side_embark_A", - "ptpov_e3_rescue_side_embark_B", - "ptpov_e3_rescue_side_embark_C", - "ptpov_e3_rescue_side_embark_D", - "ptpov_e3_rescue_side_embark_E", - "ptpov_e3_rescue_side_embark_F", - "ptpov_e3_rescue_side_embark_G", - "ptpov_e3_rescue_side_embark_H" -] - -const array EVAC_EMBARK_ANIMS_3P = [ - "pt_e3_rescue_side_embark_A", - "pt_e3_rescue_side_embark_B", - "pt_e3_rescue_side_embark_C", - "pt_e3_rescue_side_embark_D", - "pt_e3_rescue_side_embark_E", - "pt_e3_rescue_side_embark_F", - "pt_e3_rescue_side_embark_G", - "pt_e3_rescue_side_embark_H" -] - -const array EVAC_IDLE_ANIMS_1P = [ - "ptpov_e3_rescue_side_embark_A_idle", - "ptpov_e3_rescue_side_embark_B_idle", - "ptpov_e3_rescue_side_embark_C_idle", - "ptpov_e3_rescue_side_embark_D_idle", - "ptpov_e3_rescue_side_embark_E_idle", - "ptpov_e3_rescue_side_embark_F_idle", - "ptpov_e3_rescue_side_embark_G_idle", - "ptpov_e3_rescue_side_embark_H_idle" -] - -const array EVAC_IDLE_ANIMS_3P = [ - "pt_e3_rescue_side_idle_A", - "pt_e3_rescue_side_idle_B", - "pt_e3_rescue_side_idle_C", - "pt_e3_rescue_side_idle_D", - "pt_e3_rescue_side_idle_E", - "pt_e3_rescue_side_idle_F", - "pt_e3_rescue_side_idle_G", - "pt_e3_rescue_side_idle_H" -] - -struct { - array evacNodes - entity spaceNode - - // this only supports 1 evac at once atm, is this ideal? - entity currentEvacNode - entity evacDropship - entity evacIcon -} file - -void function Evac_Init() -{ - EvacShared_Init() - RegisterSignal( "EvacShipLeaves" ) - RegisterSignal( "EvacOver" ) -} - -void function AddEvacNode( entity evacNode ) -{ - file.evacNodes.append( evacNode ) -} - -void function SetEvacSpaceNode( entity spaceNode ) -{ - file.spaceNode = spaceNode -} - -bool function IsEvacDropship( entity ent ) -{ - return file.evacDropship == ent && IsValid( file.evacDropship ) -} - -// evac epilogue -void function EvacEpilogueSetup() -{ - // don't do much here because other scripts should be able to call evac stuff themselves - AddCallback_GameStateEnter( eGameState.Epilogue, EvacEpilogue ) -} - -void function EvacEpilogue() -{ - int winner = GetWinningTeam() - - // make sure we don't run evac if it won't be supported for current map/gamestate - bool canRunEvac = GetCurrentPlaylistVarInt( "max_teams", 2 ) == 2 && - ( winner == TEAM_MILITIA || winner == TEAM_IMC ) && - ( file.evacNodes.len() > 0 || IsValid( GetEnt( "escape_node1" ) ) ) // code assumes escape_node1 should be first node if automatically registering - - if ( canRunEvac ) - { - thread SetRespawnAndWait( false ) - thread Evac( GetOtherTeam( winner ), EVAC_INITIAL_WAIT, EVAC_ARRIVAL_TIME, EVAC_WAIT_TIME, EvacEpiloguePlayerCanBoard, EvacEpilogueShouldLeaveEarly, EvacEpilogueCompleted ) - } - else - { - thread SetRespawnAndWait( false ) //prevent respawns during the fade to black, should only be an issue if the match is a draw - thread EvacEpilogueCompleted( null ) //this is hacky but like, this also shouldn't really be hit in normal gameplay - } -} - -void function EvacSpectatorFunc( entity player ) -{ - svGlobal.levelEnt.EndSignal( "GameStateChanged" ) - file.evacDropship.EndSignal( "OnDestroy" ) - - entity cam = GetEnt( expect string( file.currentEvacNode.kv.target ) ) - if ( !IsValid( cam ) ) - return - - player.SetObserverModeStaticPosition( cam.GetOrigin() ) - player.SetObserverModeStaticAngles( cam.GetAngles() ) - player.StartObserverMode( OBS_MODE_STATIC ) - - file.evacDropship.WaitSignal( "EvacOver" ) -} - -void function SetRespawnAndWait( bool mode ) -{ - wait GAME_EPILOGUE_PLAYER_RESPAWN_LEEWAY - SetRespawnsEnabled( mode ) -} - -bool function EvacEpiloguePlayerCanBoard( entity dropship, entity player ) -{ - // can't board a dropship on a different team - if ( dropship.GetTeam() != player.GetTeam() ) - return false - - // check if there are any free slots on the dropship, if there are then they can board - foreach ( entity player in dropship.s.evacSlots ) - if ( !IsValid( player ) ) - return true - - // no empty slots - return false -} - -bool function EvacEpilogueShouldLeaveEarly( entity dropship ) -{ - int numEvacing - foreach ( entity player in dropship.s.evacSlots ) - if ( IsValid( player ) ) - numEvacing++ - - return GetPlayerArrayOfTeam_Alive( dropship.GetTeam() ).len() == numEvacing || numEvacing == dropship.s.evacSlots.len() -} - -void function EvacEpilogueCompleted( entity dropship ) -{ - wait 5.0 - - foreach ( entity player in GetPlayerArray() ) - ScreenFadeToBlackForever( player, 2.0 ) - - wait 2.0 - SetGameState( eGameState.Postmatch ) -} - -// global evac func, anything can call this, it's not necessarily an epilogue thing -void function Evac( int evacTeam, float initialWait, float arrivalTime, float waitTime, bool functionref( entity, entity ) canBoardCallback, bool functionref( entity ) shouldLeaveEarlyCallback, void functionref( entity ) completionCallback, entity customEvacNode = null ) -{ - wait initialWait - - // setup evac nodes if not manually registered - if ( file.evacNodes.len() == 0 && !IsValid( customEvacNode ) ) - { - for ( int i = 1; ; i++ ) - { - entity newNode = GetEnt( "escape_node" + i ) - if ( !IsValid( newNode ) ) - break - - file.evacNodes.append( newNode ) - } - } - - // setup space node if not manually registered - if ( !IsValid( file.spaceNode ) ) - file.spaceNode = GetEnt( "spaceNode" ) - - entity evacNode = customEvacNode - if ( !IsValid( customEvacNode ) ) - evacNode = file.evacNodes.getrandom() - - file.currentEvacNode = evacNode - - // setup client evac position - file.evacIcon = CreateEntity( "info_target" ) - file.evacIcon.SetOrigin( evacNode.GetOrigin() ) - file.evacIcon.kv.spawnFlags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT - DispatchSpawn( file.evacIcon ) - file.evacIcon.DisableHibernation() - - wait 0.5 // need to wait here, or the target won't appear on clients for some reason - // eta until arrive - SetTeamActiveObjective( evacTeam, "EG_DropshipExtract", Time() + arrivalTime, file.evacIcon ) - SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract", Time() + arrivalTime, file.evacIcon ) - - // would've liked to use cd_dropship_rescue_side_start length here, but can't since this is done before dropship spawn, can't - wait arrivalTime - 4.33333 - - entity dropship = CreateDropship( evacTeam, evacNode.GetOrigin(), evacNode.GetAngles() ) - dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) - dropship.SetValueForModelKey( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) - dropship.SetMaxHealth( EVAC_SHIP_HEALTH ) - dropship.SetHealth( EVAC_SHIP_HEALTH ) - dropship.SetShieldHealth( EVAC_SHIP_SHIELDS ) - SetTargetName( dropship, "#NPC_EVAC_DROPSHIP" ) - DispatchSpawn( dropship ) - dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) - AddEntityCallback_OnKilled( dropship, EvacDropshipKilled ) - - dropship.s.evacSlots <- [ null, null, null, null, null, null, null, null ] - file.evacDropship = dropship - - dropship.EndSignal( "OnDestroy" ) - OnThreadEnd( function() : ( evacTeam, completionCallback, dropship ) - { - if ( "evacTrigger" in dropship.s ) - dropship.s.evacTrigger.Destroy() - - // this should be for both teams - SetTeamActiveObjective( evacTeam, "EG_DropshipExtractDropshipDestroyed" ) - SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_DropshipExtractDropshipDestroyed" ) - - foreach ( entity player in dropship.s.evacSlots ) - { - if ( !IsValid( player ) ) - continue - - player.ClearInvulnerable() - } - - // this is called whether dropship is destroyed or evac finishes, callback can handle this itself - thread completionCallback( dropship ) - }) - - // flyin - Spectator_SetCustomSpectatorFunc( EvacSpectatorFunc ) - thread PlayAnim( dropship, "cd_dropship_rescue_side_start", evacNode ) - - // calculate time until idle start - float sequenceDuration = dropship.GetSequenceDuration( "cd_dropship_rescue_side_start" ) - float cycleFrac = dropship.GetScriptedAnimEventCycleFrac( "cd_dropship_rescue_side_start", "ReadyToLoad" ) - wait sequenceDuration * cycleFrac - - thread PlayAnim( dropship, "cd_dropship_rescue_side_idle", evacNode ) - - // eta until leave - SetTeamActiveObjective( evacTeam, "EG_DropshipExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon ) - SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon ) - - // setup evac trigger - entity trigger = CreateEntity( "trigger_cylinder" ) - trigger.SetRadius( 150 ) - trigger.SetAboveHeight( 100 ) - trigger.SetBelowHeight( 100 ) - trigger.SetOrigin( dropship.GetOrigin() ) - trigger.SetParent( dropship, "ORIGIN" ) - DispatchSpawn( trigger ) - // have to do this inline since we capture the completionCallback - trigger.SetEnterCallback( void function( entity trigger, entity player ) : ( canBoardCallback, dropship ) - { - if ( !player.IsPlayer() || !IsAlive( player ) || player.IsTitan() || player.ContextAction_IsBusy() || !canBoardCallback( dropship, player ) || PlayerInDropship( player, dropship ) ) - return - - thread AddPlayerToEvacDropship( dropship, player ) - }) - - dropship.s.evacTrigger <- trigger - - float waitStartTime = Time() - while ( Time() - waitStartTime < waitTime ) - { - if ( shouldLeaveEarlyCallback( dropship ) ) - break - - WaitFrame() - } - - // holster all weapons - foreach ( entity player in dropship.s.evacSlots ) - if ( IsValid( player ) ) - player.HolsterWeapon() - - // fly away - dropship.Signal( "EvacShipLeaves" ) - thread PlayAnim( dropship, "cd_dropship_rescue_side_end", evacNode ) - - SetTeamActiveObjective( evacTeam, "EG_DropshipExtractDropshipFlyingAway" ) - SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtractDropshipFlyingAway" ) - - // todo: play the warpout effect somewhere here - - wait dropship.GetSequenceDuration( "cd_dropship_rescue_side_end" ) - WARPINFXTIME - - foreach ( entity player in dropship.s.evacSlots ) - if ( IsValid( player ) ) - Remote_CallFunction_NonReplay( player, "ServerCallback_PlayScreenFXWarpJump" ) - - wait WARPINFXTIME - - // go to space - - // hardcoded angles here are a hack, spacenode position doesn't face the planet in the skybox, for some reason - // nvm removing for now - //file.spaceNode.SetAngles( < 30, -75, 20 > ) - - dropship.SetOrigin( file.spaceNode.GetOrigin() ) - dropship.SetAngles( file.spaceNode.GetAngles() ) - dropship.SetInvulnerable() - dropship.Signal( "EvacOver" ) - thread PlayAnim( dropship, "ds_space_flyby_dropshipA", file.spaceNode ) - - foreach( entity player in GetPlayerArray() ) - { - // evac-ed players only beyond this point - if ( !PlayerInDropship( player, dropship ) ) - { - if ( player.GetTeam() == dropship.GetTeam() ) - SetPlayerActiveObjective( player, "EG_DropshipExtractFailedEscape" ) - - continue - } - - SetPlayerActiveObjective( player, "EG_DropshipExtractSuccessfulEscape" ) - - // skybox - player.SetSkyCamera( GetEnt( SKYBOXSPACE ) ) - Remote_CallFunction_NonReplay( player, "ServerCallback_DisableHudForEvac" ) - Remote_CallFunction_NonReplay( player, "ServerCallback_SetClassicSkyScale", dropship.GetEncodedEHandle(), 0.7 ) - Remote_CallFunction_NonReplay( player, "ServerCallback_SetMapSettings", 4.0, false, 0.4, 0.125 ) - - // display player [Evacuated] in killfeed - foreach ( entity otherPlayer in GetPlayerArray() ) - Remote_CallFunction_NonReplay( otherPlayer, "ServerCallback_EvacObit", player.GetEncodedEHandle() ) - } - -} - -void function AddPlayerToEvacDropship( entity dropship, entity player ) -{ - int slot = RandomInt( dropship.s.evacSlots.len() ) - for ( int i = 0; i < dropship.s.evacSlots.len(); i++ ) - { - if ( !IsValid( dropship.s.evacSlots[ slot ] ) ) - { - dropship.s.evacSlots[ slot ] = player - break - } - - slot = ( slot + 1 ) % expect int( dropship.s.evacSlots.len() ) - } - - // no slots available - if ( !PlayerInDropship( player, dropship ) ) - return - - player.SetInvulnerable() - player.UnforceCrouch() - player.ForceStand() - - FirstPersonSequenceStruct fp - //fp.firstPersonAnim = EVAC_EMBARK_ANIMS_1P[ slot ] - fp.thirdPersonAnim = EVAC_EMBARK_ANIMS_3P[ slot ] - fp.attachment = "RESCUE" - fp.teleport = true - fp.thirdPersonCameraAttachments = [ "VDU" ] // this seems wrong, firstperson anim has better angles, but no head - - EmitSoundOnEntityOnlyToPlayer( player, player, SHIFTER_START_SOUND_3P ) - // should play SHIFTER_START_SOUND_1P when they actually arrive in the ship i think, unsure how this is supposed to be done - PlayPhaseShiftDisappearFX( player ) - waitthread FirstPersonSequence( fp, player, dropship ) - - FirstPersonSequenceStruct idleFp - idleFp.firstPersonAnimIdle = EVAC_IDLE_ANIMS_1P[ slot ] - idleFp.thirdPersonAnimIdle = EVAC_IDLE_ANIMS_3P[ slot ] - idleFp.attachment = "RESCUE" - idleFp.teleport = true - idleFp.hideProxy = true - idleFp.viewConeFunction = ViewConeWide - - thread FirstPersonSequence( idleFp, player, dropship ) - ViewConeWide( player ) // gotta do this after for some reason, adding it to idleFp does not work for some reason -} - -bool function PlayerInDropship( entity player, entity dropship ) -{ - // couldn't get "player in dropship.s.evacSlots" to work for some reason, likely due to static typing? - foreach ( entity dropshipPlayer in dropship.s.evacSlots ) - if ( dropshipPlayer == player ) - return true - - return false -} - -void function EvacDropshipKilled( entity dropship, var damageInfo ) -{ - foreach ( entity player in dropship.s.evacSlots ) - { - if ( IsValid( player ) ) - { - player.ClearParent() - player.Die( DamageInfo_GetAttacker( damageInfo ), DamageInfo_GetWeapon( damageInfo ), { damageSourceId = eDamageSourceId.evac_dropship_explosion, scriptType = DF_GIB } ) - } - } -} +untyped +global function Evac_Init +global function AddEvacNode +global function SetEvacSpaceNode +global function IsEvacDropship + +global function EvacEpilogueSetup +global function Evac + +const float EVAC_INITIAL_WAIT = 5.0 +const float EVAC_ARRIVAL_TIME = 40.0 +const float EVAC_WAIT_TIME = 18.0 + +const int EVAC_SHIP_HEALTH = 25000 +const int EVAC_SHIP_SHIELDS = 2000 + +// we don't use these because they're busted, just keeping them +const array EVAC_EMBARK_ANIMS_1P = [ + "ptpov_e3_rescue_side_embark_A", + "ptpov_e3_rescue_side_embark_B", + "ptpov_e3_rescue_side_embark_C", + "ptpov_e3_rescue_side_embark_D", + "ptpov_e3_rescue_side_embark_E", + "ptpov_e3_rescue_side_embark_F", + "ptpov_e3_rescue_side_embark_G", + "ptpov_e3_rescue_side_embark_H" +] + +const array EVAC_EMBARK_ANIMS_3P = [ + "pt_e3_rescue_side_embark_A", + "pt_e3_rescue_side_embark_B", + "pt_e3_rescue_side_embark_C", + "pt_e3_rescue_side_embark_D", + "pt_e3_rescue_side_embark_E", + "pt_e3_rescue_side_embark_F", + "pt_e3_rescue_side_embark_G", + "pt_e3_rescue_side_embark_H" +] + +const array EVAC_IDLE_ANIMS_1P = [ + "ptpov_e3_rescue_side_embark_A_idle", + "ptpov_e3_rescue_side_embark_B_idle", + "ptpov_e3_rescue_side_embark_C_idle", + "ptpov_e3_rescue_side_embark_D_idle", + "ptpov_e3_rescue_side_embark_E_idle", + "ptpov_e3_rescue_side_embark_F_idle", + "ptpov_e3_rescue_side_embark_G_idle", + "ptpov_e3_rescue_side_embark_H_idle" +] + +const array EVAC_IDLE_ANIMS_3P = [ + "pt_e3_rescue_side_idle_A", + "pt_e3_rescue_side_idle_B", + "pt_e3_rescue_side_idle_C", + "pt_e3_rescue_side_idle_D", + "pt_e3_rescue_side_idle_E", + "pt_e3_rescue_side_idle_F", + "pt_e3_rescue_side_idle_G", + "pt_e3_rescue_side_idle_H" +] + +struct { + array evacNodes + entity spaceNode + + // this only supports 1 evac at once atm, is this ideal? + entity currentEvacNode + entity evacDropship + entity evacIcon +} file + +void function Evac_Init() +{ + EvacShared_Init() + RegisterSignal( "EvacShipLeaves" ) + RegisterSignal( "EvacOver" ) +} + +void function AddEvacNode( entity evacNode ) +{ + file.evacNodes.append( evacNode ) +} + +void function SetEvacSpaceNode( entity spaceNode ) +{ + file.spaceNode = spaceNode +} + +bool function IsEvacDropship( entity ent ) +{ + return file.evacDropship == ent && IsValid( file.evacDropship ) +} + +// evac epilogue +void function EvacEpilogueSetup() +{ + // don't do much here because other scripts should be able to call evac stuff themselves + AddCallback_GameStateEnter( eGameState.Epilogue, EvacEpilogue ) +} + +void function EvacEpilogue() +{ + int winner = GetWinningTeam() + + // make sure we don't run evac if it won't be supported for current map/gamestate + bool canRunEvac = GetCurrentPlaylistVarInt( "max_teams", 2 ) == 2 && + ( winner == TEAM_MILITIA || winner == TEAM_IMC ) && + ( file.evacNodes.len() > 0 || IsValid( GetEnt( "escape_node1" ) ) ) // code assumes escape_node1 should be first node if automatically registering + + if ( canRunEvac ) + { + thread SetRespawnAndWait( false ) + thread Evac( GetOtherTeam( winner ), EVAC_INITIAL_WAIT, EVAC_ARRIVAL_TIME, EVAC_WAIT_TIME, EvacEpiloguePlayerCanBoard, EvacEpilogueShouldLeaveEarly, EvacEpilogueCompleted ) + } + else + { + thread SetRespawnAndWait( false ) //prevent respawns during the fade to black, should only be an issue if the match is a draw + thread EvacEpilogueCompleted( null ) //this is hacky but like, this also shouldn't really be hit in normal gameplay + } +} + +void function EvacSpectatorFunc( entity player ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + file.evacDropship.EndSignal( "OnDestroy" ) + + entity cam = GetEnt( expect string( file.currentEvacNode.kv.target ) ) + if ( !IsValid( cam ) ) + return + + player.SetObserverModeStaticPosition( cam.GetOrigin() ) + player.SetObserverModeStaticAngles( cam.GetAngles() ) + player.StartObserverMode( OBS_MODE_STATIC ) + + file.evacDropship.WaitSignal( "EvacOver" ) +} + +void function SetRespawnAndWait( bool mode ) +{ + wait GAME_EPILOGUE_PLAYER_RESPAWN_LEEWAY + SetRespawnsEnabled( mode ) +} + +bool function EvacEpiloguePlayerCanBoard( entity dropship, entity player ) +{ + // can't board a dropship on a different team + if ( dropship.GetTeam() != player.GetTeam() ) + return false + + // check if there are any free slots on the dropship, if there are then they can board + foreach ( entity player in dropship.s.evacSlots ) + if ( !IsValid( player ) ) + return true + + // no empty slots + return false +} + +bool function EvacEpilogueShouldLeaveEarly( entity dropship ) +{ + int numEvacing + foreach ( entity player in dropship.s.evacSlots ) + if ( IsValid( player ) ) + numEvacing++ + + return GetPlayerArrayOfTeam_Alive( dropship.GetTeam() ).len() == numEvacing || numEvacing == dropship.s.evacSlots.len() +} + +void function EvacEpilogueCompleted( entity dropship ) +{ + wait 5.0 + + foreach ( entity player in GetPlayerArray() ) + ScreenFadeToBlackForever( player, 2.0 ) + + wait 2.0 + SetGameState( eGameState.Postmatch ) +} + +// global evac func, anything can call this, it's not necessarily an epilogue thing +void function Evac( int evacTeam, float initialWait, float arrivalTime, float waitTime, bool functionref( entity, entity ) canBoardCallback, bool functionref( entity ) shouldLeaveEarlyCallback, void functionref( entity ) completionCallback, entity customEvacNode = null ) +{ + wait initialWait + + // setup evac nodes if not manually registered + if ( file.evacNodes.len() == 0 && !IsValid( customEvacNode ) ) + { + for ( int i = 1; ; i++ ) + { + entity newNode = GetEnt( "escape_node" + i ) + if ( !IsValid( newNode ) ) + break + + file.evacNodes.append( newNode ) + } + } + + // setup space node if not manually registered + if ( !IsValid( file.spaceNode ) ) + file.spaceNode = GetEnt( "spaceNode" ) + + entity evacNode = customEvacNode + if ( !IsValid( customEvacNode ) ) + evacNode = file.evacNodes.getrandom() + + file.currentEvacNode = evacNode + + // setup client evac position + file.evacIcon = CreateEntity( "info_target" ) + file.evacIcon.SetOrigin( evacNode.GetOrigin() ) + file.evacIcon.kv.spawnFlags = SF_INFOTARGET_ALWAYS_TRANSMIT_TO_CLIENT + DispatchSpawn( file.evacIcon ) + file.evacIcon.DisableHibernation() + + wait 0.5 // need to wait here, or the target won't appear on clients for some reason + // eta until arrive + SetTeamActiveObjective( evacTeam, "EG_DropshipExtract", Time() + arrivalTime, file.evacIcon ) + SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract", Time() + arrivalTime, file.evacIcon ) + + // would've liked to use cd_dropship_rescue_side_start length here, but can't since this is done before dropship spawn, can't + wait arrivalTime - 4.33333 + + entity dropship = CreateDropship( evacTeam, evacNode.GetOrigin(), evacNode.GetAngles() ) + dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) + dropship.SetValueForModelKey( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) + dropship.SetMaxHealth( EVAC_SHIP_HEALTH ) + dropship.SetHealth( EVAC_SHIP_HEALTH ) + dropship.SetShieldHealth( EVAC_SHIP_SHIELDS ) + SetTargetName( dropship, "#NPC_EVAC_DROPSHIP" ) + DispatchSpawn( dropship ) + dropship.SetModel( $"models/vehicle/crow_dropship/crow_dropship_hero.mdl" ) + AddEntityCallback_OnKilled( dropship, EvacDropshipKilled ) + + dropship.s.evacSlots <- [ null, null, null, null, null, null, null, null ] + file.evacDropship = dropship + + dropship.EndSignal( "OnDestroy" ) + OnThreadEnd( function() : ( evacTeam, completionCallback, dropship ) + { + if ( "evacTrigger" in dropship.s ) + dropship.s.evacTrigger.Destroy() + + // this should be for both teams + SetTeamActiveObjective( evacTeam, "EG_DropshipExtractDropshipDestroyed" ) + SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_DropshipExtractDropshipDestroyed" ) + + foreach ( entity player in dropship.s.evacSlots ) + { + if ( !IsValid( player ) ) + continue + + player.ClearInvulnerable() + } + + // this is called whether dropship is destroyed or evac finishes, callback can handle this itself + thread completionCallback( dropship ) + }) + + // flyin + Spectator_SetCustomSpectatorFunc( EvacSpectatorFunc ) + thread PlayAnim( dropship, "cd_dropship_rescue_side_start", evacNode ) + + // calculate time until idle start + float sequenceDuration = dropship.GetSequenceDuration( "cd_dropship_rescue_side_start" ) + float cycleFrac = dropship.GetScriptedAnimEventCycleFrac( "cd_dropship_rescue_side_start", "ReadyToLoad" ) + wait sequenceDuration * cycleFrac + + thread PlayAnim( dropship, "cd_dropship_rescue_side_idle", evacNode ) + + // eta until leave + SetTeamActiveObjective( evacTeam, "EG_DropshipExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon ) + SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon ) + + // setup evac trigger + entity trigger = CreateEntity( "trigger_cylinder" ) + trigger.SetRadius( 150 ) + trigger.SetAboveHeight( 100 ) + trigger.SetBelowHeight( 100 ) + trigger.SetOrigin( dropship.GetOrigin() ) + trigger.SetParent( dropship, "ORIGIN" ) + DispatchSpawn( trigger ) + // have to do this inline since we capture the completionCallback + trigger.SetEnterCallback( void function( entity trigger, entity player ) : ( canBoardCallback, dropship ) + { + if ( !player.IsPlayer() || !IsAlive( player ) || player.IsTitan() || player.ContextAction_IsBusy() || !canBoardCallback( dropship, player ) || PlayerInDropship( player, dropship ) ) + return + + thread AddPlayerToEvacDropship( dropship, player ) + }) + + dropship.s.evacTrigger <- trigger + + float waitStartTime = Time() + while ( Time() - waitStartTime < waitTime ) + { + if ( shouldLeaveEarlyCallback( dropship ) ) + break + + WaitFrame() + } + + // holster all weapons + foreach ( entity player in dropship.s.evacSlots ) + if ( IsValid( player ) ) + player.HolsterWeapon() + + // fly away + dropship.Signal( "EvacShipLeaves" ) + thread PlayAnim( dropship, "cd_dropship_rescue_side_end", evacNode ) + + SetTeamActiveObjective( evacTeam, "EG_DropshipExtractDropshipFlyingAway" ) + SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtractDropshipFlyingAway" ) + + // todo: play the warpout effect somewhere here + + wait dropship.GetSequenceDuration( "cd_dropship_rescue_side_end" ) - WARPINFXTIME + + foreach ( entity player in dropship.s.evacSlots ) + if ( IsValid( player ) ) + Remote_CallFunction_NonReplay( player, "ServerCallback_PlayScreenFXWarpJump" ) + + wait WARPINFXTIME + + // go to space + + // hardcoded angles here are a hack, spacenode position doesn't face the planet in the skybox, for some reason + // nvm removing for now + //file.spaceNode.SetAngles( < 30, -75, 20 > ) + + dropship.SetOrigin( file.spaceNode.GetOrigin() ) + dropship.SetAngles( file.spaceNode.GetAngles() ) + dropship.SetInvulnerable() + dropship.Signal( "EvacOver" ) + thread PlayAnim( dropship, "ds_space_flyby_dropshipA", file.spaceNode ) + + foreach( entity player in GetPlayerArray() ) + { + // evac-ed players only beyond this point + if ( !PlayerInDropship( player, dropship ) ) + { + if ( player.GetTeam() == dropship.GetTeam() ) + SetPlayerActiveObjective( player, "EG_DropshipExtractFailedEscape" ) + + continue + } + + SetPlayerActiveObjective( player, "EG_DropshipExtractSuccessfulEscape" ) + + // skybox + player.SetSkyCamera( GetEnt( SKYBOXSPACE ) ) + Remote_CallFunction_NonReplay( player, "ServerCallback_DisableHudForEvac" ) + Remote_CallFunction_NonReplay( player, "ServerCallback_SetClassicSkyScale", dropship.GetEncodedEHandle(), 0.7 ) + Remote_CallFunction_NonReplay( player, "ServerCallback_SetMapSettings", 4.0, false, 0.4, 0.125 ) + + // display player [Evacuated] in killfeed + foreach ( entity otherPlayer in GetPlayerArray() ) + Remote_CallFunction_NonReplay( otherPlayer, "ServerCallback_EvacObit", player.GetEncodedEHandle() ) + } +} + +void function AddPlayerToEvacDropship( entity dropship, entity player ) +{ + int slot = RandomInt( dropship.s.evacSlots.len() ) + for ( int i = 0; i < dropship.s.evacSlots.len(); i++ ) + { + if ( !IsValid( dropship.s.evacSlots[ slot ] ) ) + { + dropship.s.evacSlots[ slot ] = player + break + } + + slot = ( slot + 1 ) % expect int( dropship.s.evacSlots.len() ) + } + + // no slots available + if ( !PlayerInDropship( player, dropship ) ) + return + + // need to cancel if the dropship dies + dropship.EndSignal( "OnDeath", "OnDestroy" ) + + player.SetInvulnerable() + player.UnforceCrouch() + player.ForceStand() + + FirstPersonSequenceStruct fp + //fp.firstPersonAnim = EVAC_EMBARK_ANIMS_1P[ slot ] + fp.thirdPersonAnim = EVAC_EMBARK_ANIMS_3P[ slot ] + fp.attachment = "RESCUE" + fp.teleport = true + fp.thirdPersonCameraAttachments = [ "VDU" ] // this seems wrong, firstperson anim has better angles, but no head + + EmitSoundOnEntityOnlyToPlayer( player, player, SHIFTER_START_SOUND_3P ) + // should play SHIFTER_START_SOUND_1P when they actually arrive in the ship i think, unsure how this is supposed to be done + PlayPhaseShiftDisappearFX( player ) + FirstPersonSequence( fp, player, dropship ) + + FirstPersonSequenceStruct idleFp + idleFp.firstPersonAnimIdle = EVAC_IDLE_ANIMS_1P[ slot ] + idleFp.thirdPersonAnimIdle = EVAC_IDLE_ANIMS_3P[ slot ] + idleFp.attachment = "RESCUE" + idleFp.teleport = true + idleFp.hideProxy = true + idleFp.viewConeFunction = ViewConeWide + + thread FirstPersonSequence( idleFp, player, dropship ) + ViewConeWide( player ) // gotta do this after for some reason, adding it to idleFp does not work for some reason +} + +bool function PlayerInDropship( entity player, entity dropship ) +{ + // couldn't get "player in dropship.s.evacSlots" to work for some reason, likely due to static typing? + foreach ( entity dropshipPlayer in dropship.s.evacSlots ) + if ( dropshipPlayer == player ) + return true + + return false +} + +void function EvacDropshipKilled( entity dropship, var damageInfo ) +{ + foreach ( entity player in dropship.s.evacSlots ) + { + if ( IsValid( player ) ) + { + player.ClearParent() + player.Die( DamageInfo_GetAttacker( damageInfo ), DamageInfo_GetWeapon( damageInfo ), { damageSourceId = eDamageSourceId.evac_dropship_explosion, scriptType = DF_GIB } ) + } + } +} -- cgit v1.2.3