diff options
Diffstat (limited to 'Northstar.Coop/scripts/vscripts/sp/_savegame.gnut')
-rw-r--r-- | Northstar.Coop/scripts/vscripts/sp/_savegame.gnut | 860 |
1 files changed, 860 insertions, 0 deletions
diff --git a/Northstar.Coop/scripts/vscripts/sp/_savegame.gnut b/Northstar.Coop/scripts/vscripts/sp/_savegame.gnut new file mode 100644 index 00000000..f4f29003 --- /dev/null +++ b/Northstar.Coop/scripts/vscripts/sp/_savegame.gnut @@ -0,0 +1,860 @@ +global function AddCallback_OnLoadSaveGame +global function CheckPoint +global function CheckPoint_Forced +global function CheckPoint_ForcedSilent +global function CheckPoint_Silent +global function RestartFromLevelTransition +global function CodeCallback_IsSaveGameSafeToCommit +global function CodeCallback_OnLoadSaveGame +global function CodeCallback_OnSavedSaveGame +global function InitSaveGame +global function LoadSaveTimeDamageMultiplier +global function ReloadForMissionFailure +global function GetLastCheckPointLoadTime +global function SafeForCheckPoint +global function SafeToSpawnAtOrigin +global function JustLoadedFromCheckpoint +global function GetFirstPlayer + +const float LAST_SAVE_DEBOUNCE = 4.0 +const float TITAN_SAFE_DIST = 1200 +const SAVE_DEBUG = true +global const SAVE_NEW = false +global const MAX_SAFELOCATION_DIST = 5000 +global const float SAVE_SPAWN_VOFFSET = 2.0 + +global struct CheckPointData +{ + float searchTime = 5.0 + bool onGroundRequired = true + array<entity> safeLocations + bool forced + bool skipDelayedIsAliveCheck + bool skipSaveToActualPlayerLocation + bool functionref( entity ) ornull additionalCheck = null +} + +struct +{ + bool nextSaveSilent + table signalDummy + float loadSaveTime + + float lastSaveTime + + array<StoredWeapon> saveStoredWeapons + string storedTitanWeapon + + vector saveOrigin + vector saveAngles + bool saveRestoreResetsPlayer + +} file + +void function InitSaveGame() +{ + Assert( MAX_SAFELOCATION_DIST < TIME_ZOFFSET * 0.8 ) + + RegisterSignal( "StopSearchingForSafeSave" ) + RegisterSignal( "RespawnNow" ) + + FlagInit( "OnLoadSaveGame_PlayerRecoveryEnabled", true ) + FlagInit( "SaveRequires_PlayerIsTitan" ) + AddClientCommandCallback( "RestartFromLevelTransition", RestartFromLevelTransition ) + AddClientCommandCallback( "RespawnNowSP", RespawnNowSP ) +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// GLOBAL COMMANDS +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +void function CheckPoint_Silent( CheckPointData ornull checkPointData = null ) +{ + CheckPointData data = GetCheckPointDataOrDefaults( checkPointData ) + CheckPoint_Silent_FromData( data ) +} + +void function CheckPoint_Silent_FromData( CheckPointData checkPointData ) +{ + file.nextSaveSilent = true + CheckPoint( checkPointData ) +} + +void function CheckPoint_ForcedSilent() +{ + file.nextSaveSilent = true + CheckPoint_Forced() +} + +void function CheckPoint_Forced() +{ + CheckPointData checkPointData + checkPointData.forced = true + CheckPoint_FromData( checkPointData ) +} + +void function CheckPoint( CheckPointData ornull checkPointData = null ) +{ + CheckPointData data = GetCheckPointDataOrDefaults( checkPointData ) + thread CheckPoint_FromData( data ) +} + +void function CheckPoint_FromData( CheckPointData checkPointData ) +{ + printt( "SAVEGAME: New save attempt (8/3/2016)" ) + + float searchTime = checkPointData.searchTime + bool onGroundRequired = checkPointData.onGroundRequired + bool skipDelayedIsAliveCheck = checkPointData.skipDelayedIsAliveCheck + bool skipSaveToActualPlayerLocation = checkPointData.skipSaveToActualPlayerLocation + bool forced = checkPointData.forced + array<entity> safeLocations = checkPointData.safeLocations + bool functionref( entity ) ornull additionalCheck = checkPointData.additionalCheck + + if ( !Flag( "SaveGame_Enabled" ) ) + { + printt( "SAVEGAME: failed: Saves are disabled" ) + return + } + + entity player = GetFirstPlayer() + entity safeLocation + + if ( forced ) + { + printt( "SAVEGAME: creating: Forced" ) + } + else + { + if ( Flag( "SaveRequires_PlayerIsTitan" ) && !player.IsTitan() ) + { + printt( "SAVEGAME: failed: Flag SaveRequires_PlayerIsTitan and player is not a Titan" ) + return + } + + printt( "SAVEGAME: creating: Safe for check point?" ) + EndSignal( file.signalDummy, "StopSearchingForSafeSave" ) + + bool regularSafetyCheck = GetBugReproNum() != 184641 // respawned with no UI + bool functionref( entity ) safeCheck + if ( onGroundRequired ) + safeCheck = SafeForCheckPoint_OnGround + else + safeCheck = SafeForCheckPoint + + + if ( skipSaveToActualPlayerLocation ) + { + printt( "SAVEGAME: skipSaveToActualPlayerLocation." ) + } + else + { + float rate = 0.333 + int reps = int( searchTime / rate ) + + if ( regularSafetyCheck ) + { + // wait a few seconds until it is safe to save + for ( int i = 0; i < reps; i++ ) + { + if ( safeCheck( player ) ) + break + + wait rate + } + } + else + { + printt( "SAVEGAME: regularSafetyCheck disabled for bug repro" ) + } + } + + if ( !regularSafetyCheck || skipSaveToActualPlayerLocation || !safeCheck( player ) ) + { + printt( "SAVEGAME: Not safe to save actual player position." ) + if ( !safeLocations.len() ) + { + printt( "SAVEGAME: failed: no alternate safe locations." ) + return + } + + // titan cores do funky stuff with weapons and make it hard to resume to a new position safely + if ( !IsTitanCoreFiring( player ) ) + safeLocation = GetBestSaveLocationEnt( safeLocations ) + + if ( safeLocation == null ) + { + printt( "SAVEGAME: failed: couldn't find a safe location from those provided." ) + return + } + + printt( "SAVEGAME: Found safe location " + safeLocation.GetOrigin() ) + } + else + { + printt( "SAVEGAME: Safe to save at actual player location: " + player.GetOrigin() ) + } + + if ( Time() - file.lastSaveTime < 3 ) + { + printt( "SAVEGAME: failed: last save was too recent." ) + return + } + + if ( additionalCheck != null ) + { + expect bool functionref(entity)( additionalCheck ) + if ( !additionalCheck( player ) ) + { + printt( "SAVEGAME: failed: custom check failed." ) + return + } + } + } + + if ( IsValid( safeLocation ) ) + { + printt( "SAVEGAME: saveRestoreResetsPlayer = true" ) + WriteRestoreLocationFromEntity( safeLocation ) + file.saveRestoreResetsPlayer = true + } + else + { + printt( "SAVEGAME: saveRestoreResetsPlayer = false" ) + file.saveRestoreResetsPlayer = false + } + + int startPointIndex = GetCurrentStartPointIndex() + + if ( !IsAlive( player ) ) + { + printt( "SAVEGAME: failed: Tried to save while player was dead" ) + return + } + + if ( skipDelayedIsAliveCheck || forced ) + { + printt( "SAVEGAME: SaveGame_Create" ) + file.lastSaveTime = Time() + SaveGame_Create( GetSaveName(), SAVEGAME_VERSION, startPointIndex ) + } + else + { + printt( "SAVEGAME: SaveGame_CreateWithCommitDelay" ) + file.lastSaveTime = Time() + SaveGame_CreateWithCommitDelay( GetSaveName(), SAVEGAME_VERSION, 3.5, 1, startPointIndex ) + } + + // kill any ongoing attempts to save. Note this also may end this thread, so it is called last. + Signal( file.signalDummy, "StopSearchingForSafeSave" ) +} + +CheckPointData function GetCheckPointDataOrDefaults( CheckPointData ornull checkPointData = null ) +{ + if ( checkPointData != null ) + return expect CheckPointData( checkPointData ) + + CheckPointData data + return data +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// CODE CALLBACKS +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool function CodeCallback_IsSaveGameSafeToCommit() +{ + if ( !Flag( "SaveGame_Enabled" ) ) + return false + + foreach ( player in GetPlayerArray() ) + { + if ( !IsAlive( player ) ) + return false + } + + return true +} + + +void function ClearPlayerVelocityOnContext( entity player ) +{ + if ( player.IsTitan() ) + return + + switch ( GetCurrentStartPoint() ) + { + case "Rising World Jump": + return + } + + player.SetVelocity( <0,0,0> ) +} + + +void function CodeCallback_OnLoadSaveGame() +{ + printt( "SaveGame: OnLoadSaveGame" ) + file.loadSaveTime = Time() + + UpdateCollectiblesAfterLoadSaveGame() + array<entity> players = GetPlayerArray() + + Assert( players.len() == 1 ) + entity player = players[0] + + // restore health on load from save + if ( !IsAlive( player ) ) + return + + ClearPlayerVelocityOnContext( player ) + + thread UpdateUI( player.IsTitan(), player ) + + // run on load save game callbacks + foreach ( callbackFunc in svGlobalSP.onLoadSaveGameCallbacks ) + { + callbackFunc( player ) + } + + thread DelayedOnLoadSetup( player ) +} + +void function DelayedOnLoadSetup( entity player ) +{ + player.EndSignal( "OnDeath" ) + + WaitFrame() // fixes crash bug, also can't issue a remote call on this codecallback until waiting a frame. + + Remote_CallFunction_UI( player, "ServerCallback_GetObjectiveReminderOnLoad" ) // show objective reminder + + bool forceStanding = file.saveRestoreResetsPlayer + if ( file.saveRestoreResetsPlayer ) + { + file.saveRestoreResetsPlayer = false + + printt( "SAVEGAME: Restoring player to safe location " + file.saveOrigin ) + player.SetOrigin( file.saveOrigin ) + player.SetAngles( file.saveAngles ) + player.SetVelocity( <0,0,0> ) + + TakeAllWeapons( player ) + + player.ForceStand() + + if ( IsPilot( player ) ) + { + GiveWeaponsFromStoredArray( player, file.saveStoredWeapons ) + } + else + { + player.GiveWeapon( file.storedTitanWeapon ) + } + } + + if ( Flag( "OnLoadSaveGame_PlayerRecoveryEnabled" ) ) + { + if ( player.IsTitan() ) + { + RestoreTitan( player ) + + int index = GetConVarInt( "sp_titanLoadoutCurrent" ) + if ( index > -1 && IsBTLoadoutUnlocked( index ) ) + { + string weapon = GetTitanWeaponFromIndex( index ) + TakeAllWeapons( player ) + player.GiveWeapon( weapon ) + } + } + else + { + player.SetHealth( player.GetMaxHealth() ) + entity offhand = player.GetOffhandWeapon( OFFHAND_SPECIAL ) + if ( IsValid( offhand ) ) + { + // restore offhand, so for example, cloak will be ready to use + int max = offhand.GetWeaponPrimaryClipCountMax() + offhand.SetWeaponPrimaryClipCount(max) + } + + entity petTitan = player.GetPetTitan() + if ( IsAlive( petTitan ) ) + { + RestoreTitan( petTitan ) + } + } + } + + WaitFrame() + + if ( forceStanding ) + player.UnforceStand() + +} + +void function CodeCallback_OnSavedSaveGame( bool saved ) +{ + printt( "SAVEGAME: Success (OnSavedSaveGame)" ) + + // failed to save + if ( !saved ) + return + + // tell clients about the save + foreach ( player in GetPlayerArray() ) + { + Remote_CallFunction_UI( player, "ServerCallback_ClearObjectiveReminderOnLoad" ) // dont need objective reminder again on load + } + + BroadcastCheckpointMessage() +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// SAVE UTILITIES +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + + + +entity function GetFirstPlayer() +{ + foreach ( player in GetPlayerArray() ) + { + return player + } + + unreachable +} + +bool function SafeToSpawnAtOrigin( entity player, vector origin ) +{ + if ( player.IsTitan() ) + { + array<entity> titans = GetNPCArrayEx( "npc_titan", TEAM_ANY, TEAM_MILITIA, origin, TITAN_SAFE_DIST ) + if ( titans.len() > 0 ) + return false + + array<entity> superSpectres = GetNPCArrayEx( "npc_super_spectre", TEAM_ANY, TEAM_MILITIA, origin, TITAN_SAFE_DIST ) + if ( superSpectres.len() > 0 ) + return false + } + else + { + array<entity> enemies = GetNPCArrayEx( "any", TEAM_ANY, TEAM_MILITIA, origin, 300 ) + if ( enemies.len() > 0 ) + return false + } + + return IsPlayerSafeFromProjectiles( player, origin ) +} + +entity function GetBestSaveLocationEnt( array<entity> safeLocations ) +{ + entity player = GetFirstPlayer() + vector mins = player.GetBoundingMins() + vector maxs = player.GetBoundingMaxs() + + vector playerOrg = player.GetOrigin() + array<entity> orderedLocations = ArrayClosest( safeLocations, player.GetOrigin() ) + foreach ( ent in orderedLocations ) + { + vector org = ent.GetOrigin() + <0,0,SAVE_SPAWN_VOFFSET> + + // too far to spawn + if ( Distance( playerOrg, org ) > MAX_SAFELOCATION_DIST ) + break + + if ( !SafeToSpawnAtOrigin( player, ent.GetOrigin() ) ) + continue + + // blocked at that origin? + TraceResults result = TraceHull( org, org + Vector( 0, 0, 1), mins, maxs, [ player ], TRACE_MASK_PLAYERSOLID, TRACE_COLLISION_GROUP_PLAYER ) + if ( result.startSolid ) + continue + if ( result.fraction != 1.0 ) + continue + + return ent + } + + return null +} + +void function WriteRestoreLocationFromEntity( entity safeLocation ) +{ + Assert( safeLocation != null ) + entity player = GetFirstPlayer() + Assert( IsAlive( player ), "Tried to save while player was dead" ) + + if ( IsPilot( player ) ) + { + file.saveStoredWeapons = StoreWeapons( player ) + } + else + { + array<entity> weapons = player.GetMainWeapons() + Assert( weapons.len() == 1, "Player had multiple titan weapons" ) + file.storedTitanWeapon = weapons[0].GetWeaponClassName() + } + + file.saveOrigin = safeLocation.GetOrigin() + <0,0,SAVE_SPAWN_VOFFSET> + file.saveAngles = safeLocation.GetAngles() +} + +bool function RestartFromLevelTransition( entity player, array<string> args ) +{ + thread ReloadFromLevelStart() + + return true +} + +void function ReloadForMissionFailure( float extraDelay = 0 ) +{ + // prevent automatic restarts + //thread ReloadForMissionFailure_Thread( extraDelay ) +} + +void function ReloadForMissionFailure_Thread( float extraDelay ) +{ + FlagClear( "SaveGame_Enabled" ) // no more saving, you have lost + FlagSet( "MissionFailed" ) + + Signal( file.signalDummy, "StopSearchingForSafeSave" ) + + entity player = GetFirstPlayer() + if ( IsValid( player ) ) + EmitSoundOnEntityOnlyToPlayer( player, player, "4_second_fadeout" ) + + wait 1 + + waitthread WaitForRespawnNowOrTimePass( player, 2.8 + extraDelay ) + + ReloadFromSave_RestartFallback() +} + +void function WaitForRespawnNowOrTimePass( entity player, float delay ) +{ + player.EndSignal( "RespawnNow" ) + wait delay +} + +void function SkipReloadDelay_Thread( entity player ) +{ + EmitSoundOnEntityOnlyToPlayer( player, player, "1_second_fadeout" ) + ScreenFadeToBlackForever( player, 0.5 ) + wait 1.0 + player.Signal( "RespawnNow" ) +} + +bool function RespawnNowSP( entity player, array<string> args ) +{ + // this clientcommand is always bound now so we need to make sure it isn't called while player is alive + if ( IsAlive( player )) + return true + + // wait a few seconds before we allow respawns + if ( player.nv.nextRespawnTime > Time() ) + return true + + //thread SkipReloadDelay_Thread( player ) + + // do our respawning code + entity ornull respawningOn = GetPlayerToSpawnOn() + + if ( respawningOn == null ) + { + // todo: logic for restarting level without dropping clients + print( "everyone is dead! restarting from last checkpoint..." ) + RestartWithoutDroppingPlayers() + } + else + { + expect entity( respawningOn ) + // HACK: compiler has a fit if we directly call player.RespawnPlayer in this file + // so we need to make a functionref in another file that does it + + hackRespawnPlayerFunc( player, respawningOn ) + // honestly unsure if the respawningOn arg works with players so manually set pos too + thread TeleportToEntitySafe( player, respawningOn ) + } + + + return true +} + +void function ReloadFromSave_RestartFallback() +{ + if ( LoadedFromSave() ) + return + + ReloadFromLevelStart() +} + +void function ReloadFromLevelStart() +{ + array<entity> players = GetPlayerArray() + Assert( players.len() == 1 ) + entity player = players[0] + ServerRestartMission( player ) +} + +bool function SafeForCheckPoint_OnGround( entity player ) +{ + Assert( player.IsPlayer() ) + + if ( !player.IsOnGround() ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: !player.IsOnGround()" ) + #endif + return false + } + + if ( player.IsWallRunning() ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: player.IsWallRunning()" ) + #endif + return false + } + + return SafeForCheckPoint( player ) +} + +bool function SafeForCheckPoint( entity player ) +{ + Assert( player.IsPlayer() ) + + if ( Flag( "CheckPointDisabled" ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: Flag( \"CheckPointDisabled\" )" ) + #endif + return false + } + + if ( !IsAlive( player ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: !IsAlive( player )" ) + #endif + return false + } + + if ( IsPlayerEmbarking( player ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: IsPlayerEmbarking( player )" ) + #endif + return false + } + + if ( IsPlayerDisembarking( player ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: IsPlayerDisembarking( player )" ) + #endif + return false + } + + if ( player.p.doingQuickDeath ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: player.p.doingQuickDeath" ) + #endif + return false + } + + if ( EntityIsOutOfBounds( player ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: EntityIsOutOfBounds( player )" ) + #endif + return false + } + + + float range = 300 + if ( player.IsTitan() ) + { + range = 1000 + + // awkward to resume into a core-in-progress + if ( IsTitanCoreFiring( player ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: IsTitanCoreFiring( player )" ) + #endif + return false + } + } + else + { + entity weapon = player.GetActiveWeapon() + if ( IsValid( weapon ) ) + { + // cooking grenade? + if ( player.GetOffhandWeapon( OFFHAND_ORDNANCE ) == weapon ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: Cooking grenade" ) + #endif + return false + } + } + + if ( player.IsPhaseShifted() ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: player.IsPhaseShifted()" ) + #endif + return false + } + } + + array<entity> enemies = GetNPCArrayEx( "any", TEAM_ANY, TEAM_MILITIA, player.GetOrigin(), range ) + if ( enemies.len() > 0 ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: Enemy within " + range + " units" ) + #endif + return false + } + + if ( player.IsTitan() ) + { + + array<entity> titans = GetNPCArrayEx( "npc_titan", TEAM_ANY, TEAM_MILITIA, player.GetOrigin(), 3000 ) + foreach ( titan in titans ) + { + if ( titan.GetEnemy() == player ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: Enemy Titan within 3000 units" ) + #endif + return false + } + } + } + + if ( !IsPlayerSafeFromNPCs( player ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: !IsPlayerSafeFromNPCs( player )" ) + #endif + return false + } + + if ( !IsPlayerSafeFromProjectiles( player, player.GetOrigin() ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: !IsPlayerSafeFromProjectiles( player )" ) + #endif + return false + } + + if ( WasRecentlyHitByDamageSourceId( player, eDamageSourceId.toxic_sludge, 3.0 ) ) + { + #if SAVE_DEBUG + printt( "SaveGame: Failed: WasRecentlyHitByDamageSourceId( player, eDamageSourceId.toxic_sludge, 3.0 )" ) + #endif + return false + } + + return true +} + +void function BroadcastCheckpointMessage() +{ + if ( file.nextSaveSilent ) + { + file.nextSaveSilent = false + return + } + + // tell clients about the save + foreach ( player in GetPlayerArray() ) + { + if ( !IsAlive( player ) ) + continue + + Remote_CallFunction_NonReplay( player, "SCB_CheckPoint" ) + } +} + + +void function AddCallback_OnLoadSaveGame( void functionref( entity ) callbackFunc ) +{ + Assert( !svGlobalSP.onLoadSaveGameCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " with AddCallback_OnLoadSaveGame" ) + svGlobalSP.onLoadSaveGameCallbacks.append( callbackFunc ) +} + +// Why does this need to be threaded?? I don't know! But it doesn't work if I don't wait a bit. +void function UpdateUI( bool isTitan, entity player ) +{ + if ( !IsValid( player ) ) + return + + player.EndSignal( "OnDeath" ) + + wait 0.1 + + UpdatePauseMenuMissionLog( player ) + + if ( isTitan ) + { + UI_NotifySPTitanLoadoutChange( player ) + NotifyUI_ShowTitanLoadout( player, null ) + } + else + { + NotifyUI_HideTitanLoadout( player, null ) + } +} + +float function LoadSaveTimeDamageMultiplier() +{ + return GraphCapped( Time() - file.loadSaveTime, 2.5, 4.0, 0.0, 1.0 ) +} + +float function GetLastCheckPointLoadTime() +{ + return file.loadSaveTime +} + +bool function JustLoadedFromCheckpoint() +{ + return Time() - file.loadSaveTime < 1.0 +} + +bool function LoadedFromSave() +{ + printt( "SAVEGAME: Trying to load saveName" ) + if ( !HasValidSaveGame() ) + { + printt( "SAVEGAME: !HasValidSaveGame" ) + return false + } + + string saveName = GetSaveName() + printt( "SAVEGAME: Did load " + saveName ) + + // set the correct loadscreen + string mapName = SaveGame_GetMapName( saveName ) + int startPointIndex = SaveGame_GetStartPoint( saveName ) + + array<string> clientCommands = GetLoadingClientCommands( mapName, startPointIndex, DETENT_FORCE_DISABLE, false ) + ExecuteClientCommands( clientCommands ) + + if ( GetBugReproNum() != 196356 ) + { + printt( "LOADPROGRESS" ) + ClientCommand( GetFirstPlayer(), "show_loading_progress" ) + WaitFrame() + } + + SaveGame_LoadWithStartPointFallback() + return true +} |