aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers
diff options
context:
space:
mode:
authorBobTheBob9 <for.oliver.kirkham@gmail.com>2022-07-15 21:19:14 +0100
committerBobTheBob9 <for.oliver.kirkham@gmail.com>2022-07-15 21:19:14 +0100
commite158a247522230d97c5b16ed62af1969c626fd73 (patch)
tree111fcf6d7e8077d6c9fc9a8eb501c1d07cda1bac /Northstar.CustomServers
parent85eb27362bd295a9e1560982a3369a688e13ded0 (diff)
downloadNorthstarMods-e158a247522230d97c5b16ed62af1969c626fd73.tar.gz
NorthstarMods-e158a247522230d97c5b16ed62af1969c626fd73.zip
fix race condition crash when boarding dropship as it explodesevac-crash-fix-pr
Diffstat (limited to 'Northstar.CustomServers')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut858
1 files 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<string> 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<string> 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<string> 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<string> 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<entity> 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<string> 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<string> 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<string> 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<string> 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<entity> 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 } )
+ }
+ }
+}