From 81809dfba9a8329f07d68d1fd66133161234effa Mon Sep 17 00:00:00 2001 From: BobTheBob <32057864+BobTheBob9@users.noreply.github.com> Date: Thu, 3 Mar 2022 20:54:59 +0000 Subject: spectator refactor and private match spectator support --- .../mod/scripts/vscripts/mp/_spectator.gnut | 223 +++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 Northstar.CustomServers/mod/scripts/vscripts/mp/_spectator.gnut (limited to 'Northstar.CustomServers/mod/scripts/vscripts/mp/_spectator.gnut') diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_spectator.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_spectator.gnut new file mode 100644 index 000000000..aa2fc1089 --- /dev/null +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_spectator.gnut @@ -0,0 +1,223 @@ +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 ) + } + 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 + player.SetSpecReplayDelay( 0.0 ) + player.StartObserverMode( OBS_MODE_CHASE ) + } + + return true +} \ No newline at end of file -- cgit v1.2.3