2018-11-22 10:37:58 +01:00
using System ;
2018-11-25 19:04:49 +01:00
using System.Collections.Generic ;
2018-11-22 10:37:58 +01:00
using System.Linq ;
using Content.Server.GameObjects ;
2019-03-17 15:52:27 +01:00
using Content.Server.GameObjects.Components.Markers ;
2018-11-22 10:37:58 +01:00
using Content.Server.GameTicking.GamePresets ;
2019-04-13 09:45:09 +02:00
using Content.Server.Interfaces.Chat ;
2018-11-22 10:37:58 +01:00
using Content.Server.Interfaces.GameTicking ;
2018-11-25 19:04:49 +01:00
using Content.Server.Mobs ;
using Content.Server.Players ;
using Content.Shared ;
2018-11-22 10:37:58 +01:00
using Content.Shared.GameObjects.Components.Inventory ;
2019-04-15 21:11:38 -06:00
using Robust.Server.Interfaces.Console ;
using Robust.Server.Interfaces.Maps ;
using Robust.Server.Interfaces.Player ;
using Robust.Server.Player ;
using Robust.Shared.Configuration ;
using Robust.Shared.Enums ;
using Robust.Shared.GameObjects ;
using Robust.Shared.Interfaces.Configuration ;
using Robust.Shared.Interfaces.GameObjects ;
using Robust.Shared.Interfaces.Map ;
using Robust.Shared.Interfaces.Network ;
using Robust.Shared.Interfaces.Timing ;
using Robust.Shared.IoC ;
using Robust.Shared.Log ;
using Robust.Shared.Map ;
using Robust.Shared.Maths ;
using Robust.Shared.Network ;
using Robust.Shared.Timers ;
using Robust.Shared.Timing ;
using Robust.Shared.Utility ;
using Robust.Shared.ViewVariables ;
2018-11-22 10:37:58 +01:00
namespace Content.Server.GameTicking
{
2018-11-25 19:04:49 +01:00
public class GameTicker : SharedGameTicker , IGameTicker
2018-11-22 10:37:58 +01:00
{
[ViewVariables]
public GameRunLevel RunLevel
{
get = > _runLevel ;
private set
{
if ( _runLevel = = value )
{
return ;
}
var old = _runLevel ;
_runLevel = value ;
OnRunLevelChanged ? . Invoke ( new GameRunLevelChangedEventArgs ( old , value ) ) ;
}
}
2018-11-26 10:02:47 +01:00
public event Action < GameRunLevelChangedEventArgs > OnRunLevelChanged ;
2018-11-25 19:04:49 +01:00
2018-11-22 10:37:58 +01:00
private const string PlayerPrototypeName = "HumanMob_Content" ;
2018-11-25 19:04:49 +01:00
private const string ObserverPrototypeName = "MobObserver" ;
2018-11-22 10:37:58 +01:00
private const string MapFile = "Maps/stationstation.yml" ;
2018-11-25 19:04:49 +01:00
// Seconds.
private const float LobbyDuration = 20 ;
[ViewVariables] private bool _initialized ;
2019-01-18 11:40:30 +01:00
[ViewVariables(VVAccess.ReadWrite)] private GridCoordinates _spawnPoint ;
2018-11-25 19:04:49 +01:00
[ViewVariables] private GameRunLevel _runLevel ;
[ViewVariables] private bool LobbyEnabled = > _configurationManager . GetCVar < bool > ( "game.lobbyenabled" ) ;
// Value is whether they're ready.
[ViewVariables]
private readonly Dictionary < IPlayerSession , bool > _playersInLobby = new Dictionary < IPlayerSession , bool > ( ) ;
[ViewVariables] private bool _roundStartCountdownHasNotStartedYetDueToNoPlayers ;
private DateTime _roundStartTimeUtc ;
2018-11-22 10:37:58 +01:00
2019-03-17 15:52:27 +01:00
private readonly Random _spawnRandom = new Random ( ) ;
2019-05-08 09:55:36 +02:00
[ViewVariables] private readonly List < GameRule > _gameRules = new List < GameRule > ( ) ;
2019-05-15 15:49:02 +02:00
[ViewVariables] private Type _presetType ;
2018-11-22 10:37:58 +01:00
#pragma warning disable 649
[Dependency] private IEntityManager _entityManager ;
[Dependency] private IMapManager _mapManager ;
[Dependency] private IMapLoader _mapLoader ;
[Dependency] private IGameTiming _gameTiming ;
[Dependency] private IConfigurationManager _configurationManager ;
2018-11-25 19:04:49 +01:00
[Dependency] private IPlayerManager _playerManager ;
[Dependency] private IChatManager _chatManager ;
[Dependency] private IServerNetManager _netManager ;
2019-05-08 09:55:36 +02:00
[Dependency] private IDynamicTypeFactory _dynamicTypeFactory ;
2018-11-22 10:37:58 +01:00
#pragma warning restore 649
public void Initialize ( )
{
DebugTools . Assert ( ! _initialized ) ;
_configurationManager . RegisterCVar ( "game.lobbyenabled" , false , CVar . ARCHIVE ) ;
2018-11-25 19:04:49 +01:00
_playerManager . PlayerStatusChanged + = _handlePlayerStatusChanged ;
_netManager . RegisterNetMessage < MsgTickerJoinLobby > ( nameof ( MsgTickerJoinLobby ) ) ;
_netManager . RegisterNetMessage < MsgTickerJoinGame > ( nameof ( MsgTickerJoinGame ) ) ;
_netManager . RegisterNetMessage < MsgTickerLobbyStatus > ( nameof ( MsgTickerLobbyStatus ) ) ;
2018-11-22 10:37:58 +01:00
RestartRound ( ) ;
_initialized = true ;
}
public void Update ( FrameEventArgs frameEventArgs )
{
2018-11-25 19:04:49 +01:00
if ( RunLevel ! = GameRunLevel . PreRoundLobby | | _roundStartTimeUtc > DateTime . UtcNow | |
_roundStartCountdownHasNotStartedYetDueToNoPlayers )
{
return ;
}
StartRound ( ) ;
2018-11-22 10:37:58 +01:00
}
public void RestartRound ( )
{
Logger . InfoS ( "ticker" , "Restarting round!" ) ;
RunLevel = GameRunLevel . PreRoundLobby ;
_resettingCleanup ( ) ;
_preRoundSetup ( ) ;
2018-11-25 19:04:49 +01:00
if ( ! LobbyEnabled )
2018-11-22 10:37:58 +01:00
{
2018-11-25 19:04:49 +01:00
StartRound ( ) ;
}
else
{
if ( _playerManager . PlayerCount = = 0 )
{
_roundStartCountdownHasNotStartedYetDueToNoPlayers = true ;
}
else
{
_roundStartTimeUtc = DateTime . UtcNow + TimeSpan . FromSeconds ( LobbyDuration ) ;
}
2019-05-08 16:42:36 +02:00
_sendStatusToAll ( ) ;
2018-11-22 10:37:58 +01:00
}
}
public void StartRound ( )
{
DebugTools . Assert ( RunLevel = = GameRunLevel . PreRoundLobby ) ;
Logger . InfoS ( "ticker" , "Starting round!" ) ;
RunLevel = GameRunLevel . InRound ;
// TODO: Allow other presets to be selected.
2019-05-16 14:09:11 +02:00
var preset = ( GamePreset ) _dynamicTypeFactory . CreateInstance ( _presetType ? ? typeof ( PresetSandbox ) ) ;
2018-11-22 10:37:58 +01:00
preset . Start ( ) ;
2018-11-25 19:04:49 +01:00
foreach ( var ( playerSession , ready ) in _playersInLobby . ToList ( ) )
{
2019-05-08 16:42:36 +02:00
if ( LobbyEnabled & & ! ready )
2018-11-25 19:04:49 +01:00
{
continue ;
}
_spawnPlayer ( playerSession ) ;
}
_sendStatusToAll ( ) ;
2018-11-22 10:37:58 +01:00
}
public void EndRound ( )
{
DebugTools . Assert ( RunLevel = = GameRunLevel . InRound ) ;
Logger . InfoS ( "ticker" , "Ending round!" ) ;
RunLevel = GameRunLevel . PostRound ;
}
2018-11-25 19:04:49 +01:00
public void Respawn ( IPlayerSession targetPlayer )
{
targetPlayer . ContentData ( ) . WipeMind ( ) ;
if ( LobbyEnabled )
{
_playerJoinLobby ( targetPlayer ) ;
}
else
{
_spawnPlayer ( targetPlayer ) ;
}
}
public void MakeObserve ( IPlayerSession player )
{
if ( ! _playersInLobby . ContainsKey ( player ) )
{
return ;
}
_spawnObserver ( player ) ;
}
public void MakeJoinGame ( IPlayerSession player )
{
if ( ! _playersInLobby . ContainsKey ( player ) )
{
return ;
}
_spawnPlayer ( player ) ;
}
public void ToggleReady ( IPlayerSession player , bool ready )
{
if ( ! _playersInLobby . ContainsKey ( player ) )
{
return ;
}
_playersInLobby [ player ] = ready ;
_netManager . ServerSendMessage ( _getStatusMsg ( player ) , player . ConnectedClient ) ;
}
2019-05-08 09:55:36 +02:00
public T AddGameRule < T > ( ) where T : GameRule , new ( )
{
var instance = _dynamicTypeFactory . CreateInstance < T > ( ) ;
_gameRules . Add ( instance ) ;
instance . Added ( ) ;
return instance ;
}
public void RemoveGameRule ( GameRule rule )
{
if ( _gameRules . Contains ( rule ) )
{
return ;
}
rule . Removed ( ) ;
_gameRules . Remove ( rule ) ;
}
public IEnumerable < GameRule > ActiveGameRules = > _gameRules ;
2019-05-15 15:49:02 +02:00
public void SetStartPreset ( Type type )
{
if ( ! typeof ( GamePreset ) . IsAssignableFrom ( type ) )
{
throw new ArgumentException ( "type must inherit GamePreset" ) ;
}
_presetType = type ;
}
2018-11-25 19:04:49 +01:00
private IEntity _spawnPlayerMob ( )
2018-11-22 10:37:58 +01:00
{
2019-07-07 22:24:44 +02:00
var entity = _entityManager . SpawnEntityAt ( PlayerPrototypeName , _getLateJoinSpawnPoint ( ) ) ;
2018-11-22 10:37:58 +01:00
var shoes = _entityManager . SpawnEntity ( "ShoesItem" ) ;
var uniform = _entityManager . SpawnEntity ( "UniformAssistant" ) ;
if ( entity . TryGetComponent ( out InventoryComponent inventory ) )
{
inventory . Equip ( EquipmentSlotDefines . Slots . INNERCLOTHING , uniform . GetComponent < ClothingComponent > ( ) ) ;
inventory . Equip ( EquipmentSlotDefines . Slots . SHOES , shoes . GetComponent < ClothingComponent > ( ) ) ;
}
return entity ;
}
2018-11-25 19:04:49 +01:00
private IEntity _spawnObserverMob ( )
{
2019-07-07 22:24:44 +02:00
return _entityManager . SpawnEntityAt ( ObserverPrototypeName , _getLateJoinSpawnPoint ( ) ) ;
2019-03-17 15:52:27 +01:00
}
private GridCoordinates _getLateJoinSpawnPoint ( )
{
var location = _spawnPoint ;
var possiblePoints = new List < GridCoordinates > ( ) ;
foreach ( var entity in _entityManager . GetEntities ( new TypeEntityQuery ( typeof ( SpawnPointComponent ) ) ) )
{
var point = entity . GetComponent < SpawnPointComponent > ( ) ;
if ( point . SpawnType = = SpawnPointType . LateJoin )
{
possiblePoints . Add ( entity . Transform . GridPosition ) ;
}
}
if ( possiblePoints . Count ! = 0 )
{
location = _spawnRandom . Pick ( possiblePoints ) ;
}
return location ;
2018-11-25 19:04:49 +01:00
}
2018-11-22 10:37:58 +01:00
/// <summary>
/// Cleanup that has to run to clear up anything from the previous round.
/// Stuff like wiping the previous map clean.
/// </summary>
private void _resettingCleanup ( )
{
// Delete all entities.
foreach ( var entity in _entityManager . GetEntities ( ) . ToList ( ) )
{
// TODO: Maybe something less naive here?
// FIXME: Actually, definitely.
entity . Delete ( ) ;
}
// Delete all maps outside of nullspace.
foreach ( var map in _mapManager . GetAllMaps ( ) . ToList ( ) )
{
// TODO: Maybe something less naive here?
if ( map . Index ! = MapId . Nullspace )
{
_mapManager . DeleteMap ( map . Index ) ;
}
}
2018-11-25 19:04:49 +01:00
// Delete the minds of everybody.
// TODO: Maybe move this into a separate manager?
foreach ( var unCastData in _playerManager . GetAllPlayerData ( ) )
{
unCastData . ContentData ( ) . WipeMind ( ) ;
}
2019-05-08 09:55:36 +02:00
// Clear up any game rules.
foreach ( var rule in _gameRules )
{
rule . Removed ( ) ;
}
_gameRules . Clear ( ) ;
2019-05-08 16:42:36 +02:00
// Move everybody currently in the server to lobby.
foreach ( var player in _playerManager . GetAllPlayers ( ) )
{
if ( _playersInLobby . ContainsKey ( player ) )
{
continue ;
}
_playerJoinLobby ( player ) ;
}
2018-11-22 10:37:58 +01:00
}
private void _preRoundSetup ( )
{
var newMap = _mapManager . CreateMap ( ) ;
var startTime = _gameTiming . RealTime ;
var grid = _mapLoader . LoadBlueprint ( newMap , MapFile ) ;
2019-01-18 11:40:30 +01:00
_spawnPoint = new GridCoordinates ( Vector2 . Zero , grid ) ;
2018-11-22 10:37:58 +01:00
var timeSpan = _gameTiming . RealTime - startTime ;
Logger . InfoS ( "ticker" , $"Loaded map in {timeSpan.TotalMilliseconds:N2}ms." ) ;
}
2018-11-25 19:04:49 +01:00
private void _handlePlayerStatusChanged ( object sender , SessionStatusEventArgs args )
{
var session = args . Session ;
switch ( args . NewStatus )
{
case SessionStatus . Connected :
{
// Always make sure the client has player data. Mind gets assigned on spawn.
if ( session . Data . ContentDataUncast = = null )
{
session . Data . ContentDataUncast = new PlayerData ( session . SessionId ) ;
}
// timer time must be > tick length
Timer . Spawn ( 0 , args . Session . JoinGame ) ;
2019-04-13 09:45:09 +02:00
_chatManager . DispatchServerAnnouncement ( $"Player {args.Session.SessionId} joined server!" ) ;
2018-11-25 19:04:49 +01:00
if ( LobbyEnabled & & _roundStartCountdownHasNotStartedYetDueToNoPlayers )
{
_roundStartCountdownHasNotStartedYetDueToNoPlayers = false ;
_roundStartTimeUtc = DateTime . UtcNow + TimeSpan . FromSeconds ( LobbyDuration ) ;
}
break ;
}
case SessionStatus . InGame :
{
var data = session . ContentData ( ) ;
if ( data . Mind = = null )
{
if ( LobbyEnabled )
{
_playerJoinLobby ( session ) ;
return ;
}
_spawnPlayer ( session ) ;
}
else
{
if ( data . Mind . CurrentEntity = = null )
{
_spawnPlayer ( session ) ;
}
else
{
session . AttachToEntity ( data . Mind . CurrentEntity ) ;
_playerJoinGame ( session ) ;
}
}
break ;
}
case SessionStatus . Disconnected :
{
if ( _playersInLobby . ContainsKey ( session ) )
{
_playersInLobby . Remove ( session ) ;
}
2019-04-13 09:45:09 +02:00
_chatManager . DispatchServerAnnouncement ( $"Player {args.Session.SessionId} left server!" ) ;
2018-11-25 19:04:49 +01:00
break ;
}
}
}
private void _spawnPlayer ( IPlayerSession session )
{
_playerJoinGame ( session ) ;
var data = session . ContentData ( ) ;
data . WipeMind ( ) ;
data . Mind = new Mind ( session . SessionId ) ;
var mob = _spawnPlayerMob ( ) ;
data . Mind . TransferTo ( mob ) ;
}
private void _spawnObserver ( IPlayerSession session )
{
_playerJoinGame ( session ) ;
var data = session . ContentData ( ) ;
data . WipeMind ( ) ;
data . Mind = new Mind ( session . SessionId ) ;
var mob = _spawnObserverMob ( ) ;
data . Mind . TransferTo ( mob ) ;
}
private void _playerJoinLobby ( IPlayerSession session )
{
_playersInLobby . Add ( session , false ) ;
_netManager . ServerSendMessage ( _netManager . CreateNetMessage < MsgTickerJoinLobby > ( ) , session . ConnectedClient ) ;
_netManager . ServerSendMessage ( _getStatusMsg ( session ) , session . ConnectedClient ) ;
}
private void _playerJoinGame ( IPlayerSession session )
{
2019-07-19 01:21:09 +02:00
_chatManager . DispatchServerMessage ( session , $"Welcome to Space Station 14! If this is your first time checking out the game, be sure to check out the tutorial in the top left!" ) ;
2018-11-25 19:04:49 +01:00
if ( _playersInLobby . ContainsKey ( session ) )
{
_playersInLobby . Remove ( session ) ;
}
_netManager . ServerSendMessage ( _netManager . CreateNetMessage < MsgTickerJoinGame > ( ) , session . ConnectedClient ) ;
}
private MsgTickerLobbyStatus _getStatusMsg ( IPlayerSession session )
{
_playersInLobby . TryGetValue ( session , out var ready ) ;
var msg = _netManager . CreateNetMessage < MsgTickerLobbyStatus > ( ) ;
msg . IsRoundStarted = RunLevel ! = GameRunLevel . PreRoundLobby ;
msg . StartTime = _roundStartTimeUtc ;
msg . YouAreReady = ready ;
return msg ;
}
private void _sendStatusToAll ( )
{
foreach ( var player in _playersInLobby . Keys )
{
_netManager . ServerSendMessage ( _getStatusMsg ( player ) , player . ConnectedClient ) ;
}
}
2018-11-22 10:37:58 +01:00
}
public enum GameRunLevel
{
PreRoundLobby = 0 ,
2018-11-26 10:02:47 +01:00
InRound = 1 ,
PostRound = 2
2018-11-22 10:37:58 +01:00
}
public class GameRunLevelChangedEventArgs : EventArgs
{
public GameRunLevel OldRunLevel { get ; }
public GameRunLevel NewRunLevel { get ; }
public GameRunLevelChangedEventArgs ( GameRunLevel oldRunLevel , GameRunLevel newRunLevel )
{
OldRunLevel = oldRunLevel ;
NewRunLevel = newRunLevel ;
}
}
class StartRoundCommand : IClientCommand
{
public string Command = > "startround" ;
public string Description = > "Ends PreRoundLobby state and starts the round." ;
public string Help = > String . Empty ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
if ( ticker . RunLevel ! = GameRunLevel . PreRoundLobby )
{
shell . SendText ( player , "This can only be executed while the game is in the pre-round lobby." ) ;
return ;
}
ticker . StartRound ( ) ;
}
}
class EndRoundCommand : IClientCommand
{
public string Command = > "endround" ;
public string Description = > "Ends the round and moves the server to PostRound." ;
public string Help = > String . Empty ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
if ( ticker . RunLevel ! = GameRunLevel . InRound )
{
shell . SendText ( player , "This can only be executed while the game is in a round." ) ;
return ;
}
ticker . EndRound ( ) ;
}
}
class NewRoundCommand : IClientCommand
{
public string Command = > "restartround" ;
public string Description = > "Moves the server from PostRound to a new PreRoundLobby." ;
public string Help = > String . Empty ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
ticker . RestartRound ( ) ;
}
}
2018-11-25 19:04:49 +01:00
class RespawnCommand : IClientCommand
{
public string Command = > "respawn" ;
public string Description = > "Respawns a player, kicking them back to the lobby." ;
2019-07-19 12:40:03 +02:00
public string Help = > "respawn [player]" ;
2018-11-25 19:04:49 +01:00
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
2019-07-19 12:40:03 +02:00
if ( args . Length > 1 )
2018-11-25 19:04:49 +01:00
{
2019-07-19 12:40:03 +02:00
shell . SendText ( player , "Must provide <= 1 argument." ) ;
2018-11-25 19:04:49 +01:00
return ;
}
var playerMgr = IoCManager . Resolve < IPlayerManager > ( ) ;
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
2019-07-19 12:40:03 +02:00
NetSessionId sessionId ;
if ( args . Length = = 0 )
2018-11-25 19:04:49 +01:00
{
2019-07-19 12:40:03 +02:00
if ( player = = null )
{
shell . SendText ( ( IPlayerSession ) null , "If not a player, an argument must be given." ) ;
return ;
}
sessionId = player . SessionId ;
}
else
{
sessionId = new NetSessionId ( args [ 0 ] ) ;
}
if ( ! playerMgr . TryGetSessionById ( sessionId , out var targetPlayer ) )
{
if ( ! playerMgr . TryGetPlayerData ( sessionId , out var data ) )
2018-11-25 19:04:49 +01:00
{
shell . SendText ( player , "Unknown player" ) ;
return ;
}
data . ContentData ( ) . WipeMind ( ) ;
shell . SendText ( player ,
"Player is not currently online, but they will respawn if they come back online" ) ;
return ;
}
ticker . Respawn ( targetPlayer ) ;
}
}
class ObserveCommand : IClientCommand
{
public string Command = > "observe" ;
public string Description = > "" ;
public string Help = > "" ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
if ( player = = null )
{
return ;
}
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
ticker . MakeObserve ( player ) ;
}
}
class JoinGameCommand : IClientCommand
{
public string Command = > "joingame" ;
public string Description = > "" ;
public string Help = > "" ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
if ( player = = null )
{
return ;
}
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
ticker . MakeJoinGame ( player ) ;
}
}
class ToggleReadyCommand : IClientCommand
{
public string Command = > "toggleready" ;
public string Description = > "" ;
public string Help = > "" ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
if ( player = = null )
{
return ;
}
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
ticker . ToggleReady ( player , bool . Parse ( args [ 0 ] ) ) ;
}
}
2019-05-15 15:49:02 +02:00
class SetGamePresetCommand : IClientCommand
{
public string Command = > "setgamepreset" ;
public string Description = > "" ;
public string Help = > "" ;
public void Execute ( IConsoleShell shell , IPlayerSession player , string [ ] args )
{
if ( args . Length ! = 1 )
{
shell . SendText ( player , "Need exactly one argument." ) ;
return ;
}
var ticker = IoCManager . Resolve < IGameTicker > ( ) ;
Type presetType ;
switch ( args [ 0 ] )
{
case "DeathMatch" :
presetType = typeof ( PresetDeathMatch ) ;
break ;
case "Sandbox" :
presetType = typeof ( PresetSandbox ) ;
break ;
default :
shell . SendText ( player , "That is not a valid game preset!" ) ;
return ;
}
ticker . SetStartPreset ( presetType ) ;
}
}
2018-11-22 10:37:58 +01:00
}