aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Northstar.CustomServers/mod.json12
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut31
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut7
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut3
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut5
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut142
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut7
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut14
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut4
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/_spectator.gnut223
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut7
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/sh_mp_utility.gnut146
12 files changed, 458 insertions, 143 deletions
diff --git a/Northstar.CustomServers/mod.json b/Northstar.CustomServers/mod.json
index 09d8fc2d..00dd06bc 100644
--- a/Northstar.CustomServers/mod.json
+++ b/Northstar.CustomServers/mod.json
@@ -18,6 +18,11 @@
"DefaultValue": "1"
},
{
+ "Name": "ns_allow_spectators",
+ "DefaultValue": "0",
+ "Flags": 8192
+ },
+ {
"Name": "ns_private_match_last_mode",
"DefaultValue": "tdm"
},
@@ -102,6 +107,13 @@
"RunOn": "SERVER && MP"
},
{
+ "Path": "mp/_spectator.gnut",
+ "RunOn": "SERVER && MP",
+ "ServerCallback": {
+ "After": "Spectator_Init"
+ }
+ },
+ {
"Path": "_loadouts_mp.gnut",
"RunOn": "SERVER && MP",
"ServerCallback": {
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut
index 9fe95445..bce8b4c7 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/evac/_evac.gnut
@@ -63,6 +63,8 @@ struct {
array<entity> evacNodes
entity spaceNode
+ // this only supports 1 evac at once atm, is this ideal?
+ entity currentEvacNode
entity evacDropship
entity evacIcon
} file
@@ -71,6 +73,7 @@ void function Evac_Init()
{
EvacShared_Init()
RegisterSignal( "EvacShipLeaves" )
+ RegisterSignal( "EvacOver" )
}
void function AddEvacNode( entity evacNode )
@@ -116,7 +119,23 @@ void function EvacEpilogue()
}
}
-void function SetRespawnAndWait(bool mode)
+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 )
@@ -180,9 +199,11 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa
if ( !IsValid( file.spaceNode ) )
file.spaceNode = GetEnt( "spaceNode" )
- entity evacNode
+ entity evacNode = customEvacNode
if ( !IsValid( customEvacNode ) )
evacNode = file.evacNodes.getrandom()
+
+ file.currentEvacNode = evacNode
// setup client evac position
file.evacIcon = CreateEntity( "info_target" )
@@ -236,6 +257,7 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa
})
// flyin
+ Spectator_SetCustomSpectatorFunc( EvacSpectatorFunc )
thread PlayAnim( dropship, "cd_dropship_rescue_side_start", evacNode )
// calculate time until idle start
@@ -247,7 +269,7 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa
// eta until leave
SetTeamActiveObjective( evacTeam, "EG_DropshipExtract2", Time() + EVAC_WAIT_TIME, file.evacIcon )
- SetTeamActiveObjective( GetOtherTeam( evacTeam ), "EG_StopExtract2", 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" )
@@ -308,6 +330,7 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa
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() )
@@ -324,7 +347,7 @@ void function Evac( int evacTeam, float initialWait, float arrivalTime, float wa
SetPlayerActiveObjective( player, "EG_DropshipExtractSuccessfulEscape" )
// skybox
- player.SetSkyCamera( GetEnt( "skybox_cam_intro" ) )
+ 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 )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
index 3102326c..6b30a399 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
@@ -36,7 +36,12 @@ void function TTDMIntroStartThreaded()
ClassicMP_OnIntroStarted()
foreach ( entity player in GetPlayerArray() )
- TTDMIntroShowIntermissionCam( player )
+ {
+ if ( !IsPrivateMatchSpectator( player ) )
+ TTDMIntroShowIntermissionCam( player )
+ else
+ RespawnPrivateMatchSpectator( player )
+ }
wait TTDMIntroLength
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut
index 0f37251d..33c0b8e9 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_lobby.gnut
@@ -19,7 +19,8 @@ void function Lobby_Init()
void function Lobby_OnClientConnectionStarted( entity player )
{
-
+ if ( !( IsPrivateMatch() || GetCurrentPlaylistName() == "private_match" ) || !GetConVarBool( "ns_allow_spectators" ) )
+ player.SetPersistentVar( "privateMatchState", 0 ) // disable spectator
}
void function Lobby_OnClientConnectionCompleted( entity player )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut
index c56e537a..c410869e 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/lobby/_private_lobby.gnut
@@ -123,7 +123,10 @@ bool function ClientCommandCallback_PrivateMatchSwitchTeams( entity player, arra
bool function ClientCommandCallback_PrivateMatchToggleSpectate( entity player, array<string> args )
{
- // not currently working, gotta figure it out at some point
+ if ( file.startState == ePrivateMatchStartState.STARTING || !GetConVarBool( "ns_allow_spectators" ) )
+ return true
+
+ player.SetPersistentVar( "privateMatchState", player.GetPersistentVarAsInt( "privateMatchState" ) == 0 ? 1 : 0 )
return true
}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut
index 453e35ec..d22f2627 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_base_gametype_mp.gnut
@@ -31,12 +31,6 @@ struct {
void function BaseGametype_Init_MPSP()
{
AddSpawnCallback( "info_intermission", SetIntermissionCamera )
- AddCallback_EntitiesDidLoad( SetSpecCams )
-
- RegisterSignal( "ObserverTargetChanged" )
- AddClientCommandCallback( "spec_next", ClientCommandCallback_spec_next )
- AddClientCommandCallback( "spec_prev", ClientCommandCallback_spec_prev )
- AddClientCommandCallback( "spec_mode", ClientCommandCallback_spec_mode )
AddPostDamageCallback( "player", AddToTitanDamageStat )
AddPostDamageCallback( "npc_titan", AddToTitanDamageStat )
@@ -50,19 +44,6 @@ void function SetIntermissionCamera( entity camera )
file.intermissionCamera = camera
}
-void function SetSpecCams()
-{
- // 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 ( lastCam != null )
- file.specCams.append( lastCam )
- } while ( lastCam != null )
-}
-
void function CodeCallback_OnClientConnectionStarted( entity player )
{
// not a real player?
@@ -173,6 +154,17 @@ void function CodeCallback_OnClientConnectionCompleted( entity player )
svGlobal.levelEnt.Signal( "PlayerDidSpawn", { player = player } )
+ if ( GetConVarBool( "ns_allow_spectators" ) )
+ {
+ if ( IsPrivateMatchSpectator( player ) )
+ {
+ InitialisePrivateMatchSpectatorPlayer( player )
+ return
+ }
+ }
+ else
+ player.SetPersistentVar( "privateMatchState", 0 )
+
// handle spawning late joiners
if ( GetGameState() == eGameState.Playing )
{
@@ -492,118 +484,6 @@ void function RespawnAsTitan( entity player, bool manualPosition = false )
}
-// spectator stuff
-
-void function PlayerBecomesSpectator( entity player )
-{
- player.StartObserverMode( OBS_MODE_CHASE )
- player.StopPhysics()
-
- player.EndSignal( "OnRespawned" )
- player.EndSignal( "OnDestroy" )
- player.EndSignal( "PlayerRespawnStarted" )
-
- int targetIndex = 0
-
- OnThreadEnd( function() : ( player )
- {
- if ( IsValid( player ) )
- player.StopObserverMode()
- })
-
- while ( true )
- {
- table result = player.WaitSignal( "ObserverTargetChanged" )
-
- array<entity> targets
-
- targets.append( file.intermissionCamera )
- foreach( entity cam in file.specCams )
- targets.append( cam )
-
- array<entity> targetPlayers
- if ( IsFFAGame() )
- targetPlayers = GetPlayerArray_Alive()
- else
- targetPlayers = GetPlayerArrayOfTeam_Alive( player.GetTeam() )
-
- foreach( entity player in targetPlayers )
- targets.append( player )
-
- 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()
- }
-}
-
-bool function ClientCommandCallback_spec_next( entity player, array<string> 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<string> 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<string> 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
-}
-
void function TryGameModeAnnouncement( entity player ) // only putting this here because it's here in gametype_sp lol
{
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
index 1e19abd3..80fae331 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_dropship_intro.gnut
@@ -112,7 +112,12 @@ void function OnPrematchStart()
}
foreach ( entity player in GetPlayerArray() )
- thread SpawnPlayerIntoDropship( player )
+ {
+ if ( !IsPrivateMatchSpectator( player ) )
+ thread SpawnPlayerIntoDropship( player )
+ else
+ RespawnPrivateMatchSpectator( player )
+ }
thread EndIntroWhenFinished()
}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut
index 106f867b..7901e3a2 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_classic_mp_no_intro.gnut
@@ -38,8 +38,12 @@ void function ClassicMP_DefaultNoIntro_Start()
foreach ( entity player in GetPlayerArray() )
{
- player.UnfreezeControlsOnServer()
- RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ if ( !IsPrivateMatchSpectator( player ) )
+ {
+ player.UnfreezeControlsOnServer()
+ RemoveCinematicFlag( player, CE_FLAG_CLASSIC_MP_SPAWNING )
+ }
+
TryGameModeAnnouncement( player )
}
}
@@ -52,6 +56,12 @@ void function ClassicMP_DefaultNoIntro_SpawnPlayer( entity player )
if ( GetGameState() != eGameState.Prematch )
return
+ if ( IsPrivateMatchSpectator( player ) ) // private match spectators use custom spawn logic
+ {
+ RespawnPrivateMatchSpectator( player )
+ return
+ }
+
if ( IsAlive( player ) )
player.Die()
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
index aa14477a..7e9943c3 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_gamestate_mp.nut
@@ -198,7 +198,9 @@ void function StartGameWithoutClassicMP()
foreach ( entity player in GetPlayerArray() )
{
- RespawnAsPilot( player )
+ if ( !IsPrivateMatchSpectator( player ) )
+ RespawnAsPilot( player )
+
ScreenFadeFromBlack( player, 0 )
}
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 00000000..aa2fc108
--- /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<entity> 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<entity> 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<string> 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<string> 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<string> 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
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut
index bd0f2d62..5af01346 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/levels/mp_wargames.nut
@@ -182,7 +182,12 @@ void function OnPrematchStart()
// launch players into intro
foreach ( entity player in GetPlayerArray() )
- thread PlayerWatchesWargamesIntro( player )
+ {
+ if ( !IsPrivateMatchSpectator( player ) )
+ thread PlayerWatchesWargamesIntro( player )
+ else
+ RespawnPrivateMatchSpectator( player )
+ }
// 7 seconds of nothing until we start the pod sequence
wait 7.0
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_mp_utility.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sh_mp_utility.gnut
new file mode 100644
index 00000000..2e6e04f4
--- /dev/null
+++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_mp_utility.gnut
@@ -0,0 +1,146 @@
+untyped
+
+globalize_all_functions
+
+struct
+{
+ table<string,table<string,int> > mapModeScoreLimits
+} file
+
+int function GetRoundScoreLimit_FromPlaylist()
+{
+ if ( !GameMode_IsDefined( GAMETYPE ) )
+ return GetCurrentPlaylistVarInt( "roundscorelimit", 10 )
+
+ return GameMode_GetRoundScoreLimit( GAMETYPE )
+}
+
+int function GetScoreLimit_FromPlaylist()
+{
+ if ( GameMode_HasMapSpecificScoreLimits( GAMETYPE ) )
+ return GameMode_GetMapSpecificScoreLimit( GAMETYPE )
+
+ if ( !GameMode_IsDefined( GAMETYPE ) )
+ return GetCurrentPlaylistVarInt( "scorelimit", 10 )
+
+ return GameMode_GetScoreLimit( GAMETYPE )
+}
+
+bool function GameMode_HasMapSpecificScoreLimits( string gameType )
+{
+ if ( gameType in file.mapModeScoreLimits )
+ {
+ if ( GetMapName() in file.mapModeScoreLimits[gameType] )
+ return true
+ }
+ return false
+}
+
+int function GameMode_GetMapSpecificScoreLimit( string gameType )
+{
+ return file.mapModeScoreLimits[gameType][GetMapName()]
+}
+
+void function GameMode_SetMapSpecificScoreLimit( table<string,int> mapModeScoreTable, string gameType )
+{
+ Assert( !( gameType in file.mapModeScoreLimits ), "GAMETYPE has already been added to mapModeScoreLimits" )
+ file.mapModeScoreLimits[gameType] <- mapModeScoreTable
+}
+
+bool function IsSuddenDeathGameMode()
+{
+ return GameMode_GetSuddenDeathEnabled( GameRules_GetGameMode() )
+}
+
+bool function IsCaptureMode()
+{
+ return GameRules_GetGameMode() == CAPTURE_POINT
+}
+
+bool function GameModeWantsToSkipBoostsAndTitanEarning()
+{
+ if ( Riff_TitanAvailability() == eTitanAvailability.Never )
+ return true
+ if ( Riff_BoostAvailability() == eBoostAvailability.Disabled )
+ return true
+
+ return false
+}
+
+IntFromEntityCompare function GetScoreboardCompareFunc()
+{
+ return ScoreboardCompareFuncForGamemode( GameRules_GetGameMode() )
+}
+
+IntFromEntityCompare function ScoreboardCompareFuncForGamemode( string gamemode )
+{
+ IntFromEntityCompare func = GameMode_GetScoreCompareFunc( gamemode )
+ if ( func != null )
+ return func
+
+ return CompareScore
+}
+
+
+bool function IsRoundWinningKillReplayEnabled()
+{
+ return expect bool ( level.nv.roundWinningKillReplayEnabled )
+}
+
+bool function IsRoundWinningKillReplayPlaying()
+{
+ return expect bool ( level.nv.roundWinningKillReplayPlaying )
+}
+
+bool function HasRoundScoreLimitBeenReached() //Different from RoundScoreLimit_Complete in that it only checks to see if the score required has been reached. Allows us to use it on the client to cover 90% of the cases we want
+{
+ if ( !IsRoundBased() )
+ return false
+
+ int roundLimit = GetRoundScoreLimit_FromPlaylist()
+
+ if ( !roundLimit )
+ return false
+
+ int militiaScore = GameRules_GetTeamScore2( TEAM_MILITIA )
+ int imcScore = GameRules_GetTeamScore2( TEAM_IMC )
+
+ if ( ( militiaScore >= roundLimit ) || ( imcScore >= roundLimit ) )
+ return true
+
+ return false
+}
+
+
+bool function IsTitanAvailable( entity player )
+{
+ var shiftIndex = player.GetEntIndex() - 1
+ var elimMask = (1 << shiftIndex)
+
+ return (level.nv.titanAvailableBits & elimMask) != 0
+}
+
+
+
+bool function IsRespawnAvailable( entity player )
+{
+ var shiftIndex = player.GetEntIndex() - 1
+ var elimMask = (1 << shiftIndex)
+
+ return (level.nv.respawnAvailableBits & elimMask) != 0
+}
+
+bool function IsPrivateMatchSpectator( entity player )
+{
+ // JFS
+ #if SERVER
+ if ( !player.p.clientScriptInitialized )
+ return false
+ #endif
+
+ // NS: allow spectators on non-private_match playlists
+ if ( ( IsPrivateMatch() || GetConVarBool( "ns_allow_spectators" ) ) && player.GetPersistentVarAsInt( "privateMatchState" ) == 1 )
+ return true
+
+ return false
+} \ No newline at end of file