global function Spectator_Init // stuff called by _base_gametype_mp and such global function InitialisePrivateMatchSpectatorPlayer global function PlayerBecomesSpectator global function RespawnPrivateMatchSpectator // custom spectator state functions // yes, GM_SetSpectatorFunc does exist in vanilla and serves roughly the same purpose, but using custom funcs here seemed better global function Spectator_SetDefaultSpectatorFunc global function Spectator_SetCustomSpectatorFunc global function Spectator_ClearCustomSpectatorFunc // helper funcs global function HACKCleanupStaticObserverStuff global typedef SpectatorFunc void functionref( entity player ) struct { array staticSpecCams SpectatorFunc defaultSpectatorFunc SpectatorFunc nextSpectatorFunc = null int newestFuncIndex = 0 // used to track which players have finished the most recent spectator func } file void function Spectator_Init() { Spectator_SetDefaultSpectatorFunc( SpectatorFunc_Default ) AddCallback_EntitiesDidLoad( SetStaticSpecCams ) RegisterSignal( "ObserverTargetChanged" ) RegisterSignal( "SpectatorFuncChanged" ) AddClientCommandCallback( "spec_next", ClientCommandCallback_spec_next ) AddClientCommandCallback( "spec_prev", ClientCommandCallback_spec_prev ) AddClientCommandCallback( "spec_mode", ClientCommandCallback_spec_mode ) } void function SetStaticSpecCams() { // spec cams are called spec_cam1,2,3 etc by default, so this is the easiest way to get them imo int camNum = 1 entity lastCam = null do { lastCam = GetEnt( "spec_cam" + camNum++ ) if ( IsValid( lastCam ) ) file.staticSpecCams.append( lastCam ) } while ( IsValid( lastCam ) ) } void function Spectator_SetDefaultSpectatorFunc( SpectatorFunc func ) { file.defaultSpectatorFunc = func } // sets the current spectator func, stopping any currently running spectator funcs to start this one void function Spectator_SetCustomSpectatorFunc( SpectatorFunc func ) { file.nextSpectatorFunc = func svGlobal.levelEnt.Signal( "SpectatorFuncChanged" ) // spectator funcs need to listen to this manually file.newestFuncIndex++ } void function Spectator_ClearCustomSpectatorFunc() { Spectator_SetCustomSpectatorFunc( null ) } void function HACKCleanupStaticObserverStuff( entity player ) { // this may look like horrible awful pointless code at first glance, and while it is horrible and awful, it's not pointless // 3.402823466E38 is 0xFFFF7F7F in memory, which is the value the game uses to determine whether the current static observer pos/angles are valid ( i.e. 0xFFFF7F7F = invalid/not initialised ) // in my experience, not cleaning this up after setting static angles will break OBS_MODE_CHASE-ing non-player entities which is bad for custom spectator funcs // this is 100% way lower level than what script stuff should usually be doing, but it's needed here // i sure do hope this won't break in normal use :clueless: player.SetObserverModeStaticPosition( < 3.402823466e38, 3.402823466e38, 3.402823466e38 > ) player.SetObserverModeStaticAngles( < 3.402823466e38, 3.402823466e38, 3.402823466e38 > ) } void function InitialisePrivateMatchSpectatorPlayer( entity player ) { thread PlayerBecomesSpectator( player ) } // this should be called when intros respawn players normally to handle fades and stuff void function RespawnPrivateMatchSpectator( entity player ) { ScreenFadeFromBlack( player, 0.5, 0.5 ) } void function PlayerBecomesSpectator( entity player ) { player.StopPhysics() player.EndSignal( "OnRespawned" ) player.EndSignal( "OnDestroy" ) player.EndSignal( "PlayerRespawnStarted" ) OnThreadEnd( function() : ( player ) { if ( IsValid( player ) ) player.StopObserverMode() }) // keeps track of the most recent func this player has completed // this is to ensure that custom spectator funcs are only run once per player even before being cleared int funcIndex = 0 while ( true ) { SpectatorFunc nextSpectatorFunc = file.defaultSpectatorFunc if ( file.nextSpectatorFunc != null && funcIndex != file.newestFuncIndex ) nextSpectatorFunc = file.nextSpectatorFunc waitthread nextSpectatorFunc( player ) funcIndex = file.newestFuncIndex // assuming this will be set before file.newestFuncIndex increments when the spectator func is ended by SpectatorFuncChanged // surely this will not end up being false in practice :clueless: // cleanup player.StopObserverMode() HACKCleanupStaticObserverStuff( player ) // un-initialise static observer positions/angles WaitFrame() // always wait at least a frame in case an observer func exits immediately to prevent stuff locking up } } void function SpectatorFunc_Default( entity player ) { svGlobal.levelEnt.EndSignal( "SpectatorFuncChanged" ) int targetIndex table result = { next = false } while ( true ) { array targets targets.extend( file.staticSpecCams ) if ( IsFFAGame() ) targets.extend( GetPlayerArray_Alive() ) else targets.extend( GetPlayerArrayOfTeam_Alive( player.GetTeam() ) ) if ( targets.len() > 0 ) { if ( result.next ) targetIndex = ( targetIndex + 1 ) % targets.len() else { if ( targetIndex == 0 ) targetIndex = ( targets.len() - 1 ) else targetIndex-- } if ( targetIndex >= targets.len() ) targetIndex = 0 entity target = targets[ targetIndex ] player.StopObserverMode() if ( player.IsWatchingSpecReplay() ) player.SetSpecReplayDelay( 0.0 ) // clear spectator replay if ( target.IsPlayer() ) { try { player.SetObserverTarget( target ) player.StartObserverMode( OBS_MODE_CHASE ) // the delay of 0.1 seems to fix the spec_mode command not working // when using the keybind player.SetSpecReplayDelay( 0.1 ) } catch ( ex ) { } } else { player.SetObserverModeStaticPosition( target.GetOrigin() ) player.SetObserverModeStaticAngles( target.GetAngles() ) player.StartObserverMode( OBS_MODE_STATIC ) } } player.StopPhysics() result = player.WaitSignal( "ObserverTargetChanged" ) } } bool function ClientCommandCallback_spec_next( entity player, array args ) { if ( player.GetObserverMode() == OBS_MODE_CHASE || player.GetObserverMode() == OBS_MODE_STATIC || player.GetObserverMode() == OBS_MODE_IN_EYE ) player.Signal( "ObserverTargetChanged", { next = true } ) return true } bool function ClientCommandCallback_spec_prev( entity player, array args ) { if ( player.GetObserverMode() == OBS_MODE_CHASE || player.GetObserverMode() == OBS_MODE_STATIC || player.GetObserverMode() == OBS_MODE_IN_EYE ) player.Signal( "ObserverTargetChanged", { next = false } ) return true } bool function ClientCommandCallback_spec_mode( entity player, array args ) { // currently unsure how this actually gets called on client, works through console and has references in client.dll tho if ( player.GetObserverMode() == OBS_MODE_CHASE ) { // set to first person spectate player.SetSpecReplayDelay( FIRST_PERSON_SPECTATOR_DELAY ) player.SetViewEntity( player.GetObserverTarget(), true ) player.StartObserverMode( OBS_MODE_IN_EYE ) } else if ( player.GetObserverMode() == OBS_MODE_IN_EYE ) { // set to third person spectate // the delay of 0.1 seems to fix the spec_mode command not working // when using the keybind player.SetSpecReplayDelay( 0.1 ) player.StartObserverMode( OBS_MODE_CHASE ) } return true }