2021-06-20 10:09:24 +02:00
using System.Globalization ;
2021-11-26 03:02:46 -06:00
using System.Linq ;
2021-11-15 18:14:34 +00:00
using Content.Server.Ghost ;
2021-06-20 10:09:24 +02:00
using Content.Server.Ghost.Components ;
using Content.Server.Players ;
using Content.Server.Roles ;
using Content.Server.Spawners.Components ;
using Content.Server.Speech.Components ;
2022-05-27 06:01:07 +02:00
using Content.Server.Station.Components ;
2021-11-28 14:56:53 +01:00
using Content.Shared.Database ;
2021-06-20 10:09:24 +02:00
using Content.Shared.GameTicking ;
2021-08-06 00:02:36 -07:00
using Content.Shared.Ghost ;
2021-06-20 10:09:24 +02:00
using Content.Shared.Preferences ;
using Content.Shared.Roles ;
2022-05-10 13:43:30 -05:00
using JetBrains.Annotations ;
2021-06-20 10:09:24 +02:00
using Robust.Server.Player ;
using Robust.Shared.Map ;
2022-02-21 14:11:39 -08:00
using Robust.Shared.Network ;
2021-06-20 10:09:24 +02:00
using Robust.Shared.Random ;
using Robust.Shared.Utility ;
namespace Content.Server.GameTicking
{
2022-02-16 00:23:23 -07:00
public sealed partial class GameTicker
2021-06-20 10:09:24 +02:00
{
private const string ObserverPrototypeName = "MobObserver" ;
2022-05-10 13:43:30 -05:00
[ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")]
2021-06-20 10:09:24 +02:00
private EntityCoordinates _spawnPoint ;
// Mainly to avoid allocations.
private readonly List < EntityCoordinates > _possiblePositions = new ( ) ;
2022-05-19 07:48:00 +10:00
private void SpawnPlayers ( List < IPlayerSession > readyPlayers , Dictionary < NetUserId , HumanoidCharacterProfile > profiles , bool force )
2022-02-21 14:11:39 -08:00
{
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
RaiseLocalEvent ( new RulePlayerSpawningEvent ( readyPlayers , profiles , force ) ) ;
2022-05-19 07:48:00 +10:00
var playerNetIds = readyPlayers . Select ( o = > o . UserId ) . ToHashSet ( ) ;
// RulePlayerSpawning feeds a readonlydictionary of profiles.
// We need to take these players out of the pool of players available as they've been used.
if ( readyPlayers . Count ! = profiles . Count )
{
var toRemove = new RemQueue < NetUserId > ( ) ;
foreach ( var ( player , _ ) in profiles )
{
if ( playerNetIds . Contains ( player ) ) continue ;
toRemove . Add ( player ) ;
}
foreach ( var player in toRemove )
{
profiles . Remove ( player ) ;
}
}
2022-05-10 13:43:30 -05:00
var assignedJobs = _stationJobs . AssignJobs ( profiles , _stationSystem . Stations . ToList ( ) ) ;
2022-02-21 14:11:39 -08:00
2022-05-19 07:48:00 +10:00
_stationJobs . AssignOverflowJobs ( ref assignedJobs , playerNetIds , profiles , _stationSystem . Stations . ToList ( ) ) ;
2022-02-21 14:11:39 -08:00
2022-05-27 06:01:07 +02:00
// Calculate extended access for stations.
var stationJobCounts = _stationSystem . Stations . ToDictionary ( e = > e , _ = > 0 ) ;
foreach ( var ( _ , ( _ , station ) ) in assignedJobs )
{
stationJobCounts [ station ] + = 1 ;
}
_stationJobs . CalcExtendedAccess ( stationJobCounts ) ;
2022-02-21 14:11:39 -08:00
// Spawn everybody in!
foreach ( var ( player , ( job , station ) ) in assignedJobs )
{
2022-05-10 13:43:30 -05:00
SpawnPlayer ( _playerManager . GetSessionByUserId ( player ) , profiles [ player ] , station , job , false ) ;
2022-02-21 14:11:39 -08:00
}
RefreshLateJoinAllowed ( ) ;
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
2022-05-10 13:43:30 -05:00
RaiseLocalEvent ( new RulePlayerJobsAssignedEvent ( assignedJobs . Keys . Select ( x = > _playerManager . GetSessionByUserId ( x ) ) . ToArray ( ) , profiles , force ) ) ;
2022-02-21 14:11:39 -08:00
}
2022-05-10 13:43:30 -05:00
private void SpawnPlayer ( IPlayerSession player , EntityUid station , string? jobId = null , bool lateJoin = true )
2021-06-20 10:09:24 +02:00
{
var character = GetPlayerProfile ( player ) ;
2022-02-21 19:45:59 -08:00
var jobBans = _roleBanManager . GetJobBans ( player . UserId ) ;
if ( jobBans = = null | | ( jobId ! = null & & jobBans . Contains ( jobId ) ) )
return ;
2021-11-26 03:02:46 -06:00
SpawnPlayer ( player , character , station , jobId , lateJoin ) ;
2021-06-20 10:09:24 +02:00
}
2022-05-10 13:43:30 -05:00
private void SpawnPlayer ( IPlayerSession player , HumanoidCharacterProfile character , EntityUid station , string? jobId = null , bool lateJoin = true )
2021-06-20 10:09:24 +02:00
{
2021-12-21 21:23:29 +01:00
// Can't spawn players with a dummy ticker!
if ( DummyTicker )
return ;
2022-05-10 13:43:30 -05:00
if ( station = = EntityUid . Invalid )
2021-11-26 03:02:46 -06:00
{
2022-05-10 13:43:30 -05:00
var stations = _stationSystem . Stations . ToList ( ) ;
2021-11-26 03:02:46 -06:00
_robustRandom . Shuffle ( stations ) ;
if ( stations . Count = = 0 )
2022-05-10 13:43:30 -05:00
station = EntityUid . Invalid ;
2021-11-26 03:02:46 -06:00
else
station = stations [ 0 ] ;
}
2021-12-21 19:25:52 +01:00
if ( lateJoin & & DisallowLateJoin )
2021-12-21 18:56:47 +01:00
{
2021-12-21 19:25:52 +01:00
MakeObserve ( player ) ;
2021-12-21 18:56:47 +01:00
return ;
}
2021-12-21 21:23:29 +01:00
// We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
var bev = new PlayerBeforeSpawnEvent ( player , character , jobId , lateJoin , station ) ;
RaiseLocalEvent ( bev ) ;
// Do nothing, something else has handled spawning this player for us!
if ( bev . Handled )
{
PlayerJoinGame ( player ) ;
return ;
}
2021-11-26 16:51:00 -06:00
// Pick best job best on prefs.
2022-05-10 13:43:30 -05:00
jobId ? ? = _stationJobs . PickBestAvailableJobWithPriority ( station , character . JobPriorities , true ,
_roleBanManager . GetJobBans ( player . UserId ) ) ;
2022-01-23 09:44:27 -08:00
// If no job available, stay in lobby, or if no lobby spawn as observer
2021-11-26 16:51:00 -06:00
if ( jobId is null )
{
2022-01-23 09:44:27 -08:00
if ( ! LobbyEnabled )
{
MakeObserve ( player ) ;
}
2021-11-26 16:51:00 -06:00
_chatManager . DispatchServerMessage ( player , Loc . GetString ( "game-ticker-player-no-jobs-available-when-joining" ) ) ;
return ;
}
2021-06-20 10:09:24 +02:00
PlayerJoinGame ( player ) ;
var data = player . ContentData ( ) ;
DebugTools . AssertNotNull ( data ) ;
data ! . WipeMind ( ) ;
2021-11-15 18:14:34 +00:00
var newMind = new Mind . Mind ( data . UserId )
2021-06-20 10:09:24 +02:00
{
CharacterName = character . Name
} ;
2021-11-15 18:14:34 +00:00
newMind . ChangeOwningPlayer ( data . UserId ) ;
2021-06-20 10:09:24 +02:00
var jobPrototype = _prototypeManager . Index < JobPrototype > ( jobId ) ;
2021-11-15 18:14:34 +00:00
var job = new Job ( newMind , jobPrototype ) ;
newMind . AddRole ( job ) ;
2021-06-20 10:09:24 +02:00
if ( lateJoin )
{
2022-06-03 21:37:35 +10:00
_chatSystem . DispatchStationAnnouncement ( station ,
Loc . GetString (
"latejoin-arrival-announcement" ,
2021-06-20 10:09:24 +02:00
( "character" , character . Name ) ,
( "job" , CultureInfo . CurrentCulture . TextInfo . ToTitleCase ( job . Name ) )
2021-11-22 22:34:48 +00:00
) , Loc . GetString ( "latejoin-arrival-sender" ) ,
playDefaultSound : false ) ;
2021-06-20 10:09:24 +02:00
}
2022-05-10 13:43:30 -05:00
var mobMaybe = _stationSpawning . SpawnPlayerCharacterOnStation ( station , job , character ) ;
DebugTools . AssertNotNull ( mobMaybe ) ;
var mob = mobMaybe ! . Value ;
2021-12-03 15:53:09 +01:00
newMind . TransferTo ( mob ) ;
2021-06-20 10:09:24 +02:00
if ( player . UserId = = new Guid ( "{e887eb93-f503-4b65-95b6-2f282c014192}" ) )
{
2021-12-07 14:19:41 -08:00
EntityManager . AddComponent < OwOAccentComponent > ( mob ) ;
2021-06-20 10:09:24 +02:00
}
2022-05-10 13:43:30 -05:00
_stationJobs . TryAssignJob ( station , jobPrototype ) ;
2021-11-26 03:02:46 -06:00
if ( lateJoin )
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . LateJoin , LogImpact . Medium , $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}." ) ;
2021-11-26 03:02:46 -06:00
else
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . RoundStartJoin , LogImpact . Medium , $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}." ) ;
2021-11-26 03:02:46 -06:00
2022-05-27 06:01:07 +02:00
// Make sure they're aware of extended access.
if ( Comp < StationJobsComponent > ( station ) . ExtendedAccess
& & ( jobPrototype . ExtendedAccess . Count > 0
| | jobPrototype . ExtendedAccessGroups . Count > 0 ) )
{
_chatManager . DispatchServerMessage ( player , Loc . GetString ( "job-greet-crew-shortages" ) ) ;
}
2021-12-21 21:23:29 +01:00
// We raise this event directed to the mob, but also broadcast it so game rules can do something now.
var aev = new PlayerSpawnCompleteEvent ( mob , player , jobId , lateJoin , station , character ) ;
2022-06-22 09:53:41 +10:00
RaiseLocalEvent ( mob , aev , true ) ;
2021-06-20 10:09:24 +02:00
}
public void Respawn ( IPlayerSession player )
{
player . ContentData ( ) ? . WipeMind ( ) ;
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Respawn , LogImpact . Medium , $"Player {player} was respawned." ) ;
2021-06-20 10:09:24 +02:00
if ( LobbyEnabled )
PlayerJoinLobby ( player ) ;
else
2022-05-10 13:43:30 -05:00
SpawnPlayer ( player , EntityUid . Invalid ) ;
2021-06-20 10:09:24 +02:00
}
2022-05-10 13:43:30 -05:00
public void MakeJoinGame ( IPlayerSession player , EntityUid station , string? jobId = null )
2021-06-20 10:09:24 +02:00
{
if ( ! _playersInLobby . ContainsKey ( player ) ) return ;
if ( ! _prefsManager . HavePreferencesLoaded ( player ) )
{
return ;
}
2021-11-26 03:02:46 -06:00
SpawnPlayer ( player , station , jobId ) ;
2021-06-20 10:09:24 +02:00
}
public void MakeObserve ( IPlayerSession player )
{
// Can't spawn players with a dummy ticker!
if ( DummyTicker )
return ;
PlayerJoinGame ( player ) ;
var name = GetPlayerProfile ( player ) . Name ;
var data = player . ContentData ( ) ;
DebugTools . AssertNotNull ( data ) ;
data ! . WipeMind ( ) ;
2021-11-15 18:14:34 +00:00
var newMind = new Mind . Mind ( data . UserId ) ;
newMind . ChangeOwningPlayer ( data . UserId ) ;
newMind . AddRole ( new ObserverRole ( newMind ) ) ;
2021-06-20 10:09:24 +02:00
var mob = SpawnObserverMob ( ) ;
2021-12-07 14:19:41 -08:00
EntityManager . GetComponent < MetaDataComponent > ( mob ) . EntityName = name ;
var ghost = EntityManager . GetComponent < GhostComponent > ( mob ) ;
2021-08-06 00:02:36 -07:00
EntitySystem . Get < SharedGhostSystem > ( ) . SetCanReturnToBody ( ghost , false ) ;
2021-12-03 15:53:09 +01:00
newMind . TransferTo ( mob ) ;
2021-06-20 10:09:24 +02:00
_playersInLobby [ player ] = LobbyPlayerStatus . Observer ;
RaiseNetworkEvent ( GetStatusSingle ( player , LobbyPlayerStatus . Observer ) ) ;
}
#region Mob Spawning Helpers
2021-12-04 12:35:33 +01:00
private EntityUid SpawnObserverMob ( )
2021-06-20 10:09:24 +02:00
{
var coordinates = GetObserverSpawnPoint ( ) ;
2021-08-04 09:25:30 +02:00
return EntityManager . SpawnEntity ( ObserverPrototypeName , coordinates ) ;
2021-06-20 10:09:24 +02:00
}
#endregion
#region Spawn Points
public EntityCoordinates GetObserverSpawnPoint ( )
{
var location = _spawnPoint ;
_possiblePositions . Clear ( ) ;
2021-12-21 21:23:29 +01:00
foreach ( var ( point , transform ) in EntityManager . EntityQuery < SpawnPointComponent , TransformComponent > ( true ) )
2021-06-20 10:09:24 +02:00
{
if ( point . SpawnType = = SpawnPointType . Observer )
_possiblePositions . Add ( transform . Coordinates ) ;
}
if ( _possiblePositions . Count ! = 0 )
location = _robustRandom . Pick ( _possiblePositions ) ;
return location ;
}
#endregion
}
2021-12-21 21:23:29 +01:00
/// <summary>
/// Event raised broadcast before a player is spawned by the GameTicker.
/// You can use this event to spawn a player off-station on late-join but also at round start.
/// When this event is handled, the GameTicker will not perform its own player-spawning logic.
/// </summary>
2022-05-10 13:43:30 -05:00
[PublicAPI]
2022-02-16 00:23:23 -07:00
public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
2021-12-21 21:23:29 +01:00
{
public IPlayerSession Player { get ; }
public HumanoidCharacterProfile Profile { get ; }
public string? JobId { get ; }
public bool LateJoin { get ; }
2022-05-10 13:43:30 -05:00
public EntityUid Station { get ; }
2021-12-21 21:23:29 +01:00
2022-05-10 13:43:30 -05:00
public PlayerBeforeSpawnEvent ( IPlayerSession player , HumanoidCharacterProfile profile , string? jobId , bool lateJoin , EntityUid station )
2021-12-21 21:23:29 +01:00
{
Player = player ;
Profile = profile ;
JobId = jobId ;
LateJoin = lateJoin ;
Station = station ;
}
}
/// <summary>
/// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
/// You can use this to handle people late-joining, or to handle people being spawned at round start.
/// Can be used to give random players a role, modify their equipment, etc.
/// </summary>
2022-05-10 13:43:30 -05:00
[PublicAPI]
2022-02-16 00:23:23 -07:00
public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
2021-12-21 21:23:29 +01:00
{
public EntityUid Mob { get ; }
public IPlayerSession Player { get ; }
public string? JobId { get ; }
public bool LateJoin { get ; }
2022-05-10 13:43:30 -05:00
public EntityUid Station { get ; }
2021-12-21 21:23:29 +01:00
public HumanoidCharacterProfile Profile { get ; }
2022-05-10 13:43:30 -05:00
public PlayerSpawnCompleteEvent ( EntityUid mob , IPlayerSession player , string? jobId , bool lateJoin , EntityUid station , HumanoidCharacterProfile profile )
2021-12-21 21:23:29 +01:00
{
Mob = mob ;
Player = player ;
JobId = jobId ;
LateJoin = lateJoin ;
Station = station ;
Profile = profile ;
}
}
2021-06-20 10:09:24 +02:00
}