2021-06-20 10:09:24 +02:00
using System.Globalization ;
2021-11-26 03:02:46 -06:00
using System.Linq ;
2021-10-22 05:31:07 +03:00
using Content.Server.Access.Systems ;
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.Hands.Components ;
using Content.Server.Players ;
using Content.Server.Roles ;
using Content.Server.Spawners.Components ;
using Content.Server.Speech.Components ;
2021-11-26 03:02:46 -06:00
using Content.Server.Station ;
2021-12-26 17:07:28 +13:00
using Content.Shared.Access.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 ;
2022-03-17 20:13:31 +13:00
using Content.Shared.Hands.EntitySystems ;
2021-06-20 10:09:24 +02:00
using Content.Shared.Inventory ;
2021-12-16 23:42:02 +13:00
using Content.Shared.PDA ;
2021-06-20 10:09:24 +02:00
using Content.Shared.Preferences ;
using Content.Shared.Roles ;
2022-01-08 19:53:14 -06:00
using Content.Shared.Species ;
2021-11-26 03:02:46 -06:00
using Content.Shared.Station ;
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" ;
2021-10-22 05:31:07 +03:00
[Dependency] private readonly IdCardSystem _cardSystem = default ! ;
2021-12-30 22:56:10 +01:00
[Dependency] private readonly InventorySystem _inventorySystem = default ! ;
2022-03-17 20:13:31 +13:00
[Dependency] private readonly SharedHandsSystem _handsSystem = default ! ;
2021-10-22 05:31:07 +03:00
2021-11-26 03:02:46 -06:00
/// <summary>
/// Can't yet be removed because every test ever seems to depend on it. I'll make removing this a different PR.
/// </summary>
2021-06-20 10:09:24 +02:00
[ViewVariables(VVAccess.ReadWrite)]
private EntityCoordinates _spawnPoint ;
// Mainly to avoid allocations.
private readonly List < EntityCoordinates > _possiblePositions = new ( ) ;
2022-02-21 14:11:39 -08:00
private void SpawnPlayers ( List < IPlayerSession > readyPlayers , IPlayerSession [ ] origReadyPlayers ,
Dictionary < NetUserId , HumanoidCharacterProfile > profiles , bool force )
{
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
RaiseLocalEvent ( new RulePlayerSpawningEvent ( readyPlayers , profiles , force ) ) ;
var assignedJobs = AssignJobs ( readyPlayers , profiles ) ;
AssignOverflowJobs ( assignedJobs , origReadyPlayers , profiles ) ;
// Spawn everybody in!
foreach ( var ( player , ( job , station ) ) in assignedJobs )
{
SpawnPlayer ( player , profiles [ player . UserId ] , station , job , false ) ;
}
RefreshLateJoinAllowed ( ) ;
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
RaiseLocalEvent ( new RulePlayerJobsAssignedEvent ( assignedJobs . Keys . ToArray ( ) , profiles , force ) ) ;
}
private void AssignOverflowJobs ( IDictionary < IPlayerSession , ( string , StationId ) > assignedJobs ,
IPlayerSession [ ] origReadyPlayers , IReadOnlyDictionary < NetUserId , HumanoidCharacterProfile > profiles )
{
// For players without jobs, give them the overflow job if they have that set...
foreach ( var player in origReadyPlayers )
{
if ( assignedJobs . ContainsKey ( player ) )
{
continue ;
}
var profile = profiles [ player . UserId ] ;
if ( profile . PreferenceUnavailable ! = PreferenceUnavailableMode . SpawnAsOverflow )
continue ;
// Pick a random station
var stations = _stationSystem . StationInfo . Keys . ToList ( ) ;
if ( stations . Count = = 0 )
{
assignedJobs . Add ( player , ( FallbackOverflowJob , StationId . Invalid ) ) ;
continue ;
}
_robustRandom . Shuffle ( stations ) ;
foreach ( var station in stations )
{
// Pick a random overflow job from that station
var overflows = _stationSystem . StationInfo [ station ] . MapPrototype . OverflowJobs . Clone ( ) ;
_robustRandom . Shuffle ( overflows ) ;
// Stations with no overflow slots should simply get skipped over.
if ( overflows . Count = = 0 )
continue ;
// If the overflow exists, put them in as it.
assignedJobs . Add ( player , ( overflows [ 0 ] , stations [ 0 ] ) ) ;
break ;
}
}
}
2021-11-26 03:02:46 -06:00
private void SpawnPlayer ( IPlayerSession player , StationId 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
UpdateJobsAvailable ( ) ;
}
2021-11-26 03:02:46 -06:00
private void SpawnPlayer ( IPlayerSession player , HumanoidCharacterProfile character , StationId 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 ;
2021-11-26 03:02:46 -06:00
if ( station = = StationId . Invalid )
{
var stations = _stationSystem . StationInfo . Keys . ToList ( ) ;
_robustRandom . Shuffle ( stations ) ;
if ( stations . Count = = 0 )
station = StationId . Invalid ;
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-02-21 14:11:39 -08:00
jobId ? ? = PickBestAvailableJob ( player , character , station ) ;
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 )
{
_chatManager . DispatchStationAnnouncement ( Loc . GetString (
"latejoin-arrival-announcement" ,
( "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
}
2021-11-26 03:02:46 -06:00
var mob = SpawnPlayerMob ( job , character , station , lateJoin ) ;
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
}
AddManifestEntry ( character . Name , jobId ) ;
AddSpawnedPosition ( jobId ) ;
EquipIdCard ( mob , character . Name , jobPrototype ) ;
2021-09-16 15:17:19 +02:00
foreach ( var jobSpecial in jobPrototype . Special )
{
jobSpecial . AfterEquip ( mob ) ;
}
2021-06-20 10:09:24 +02:00
2021-11-27 00:43:43 -06:00
_stationSystem . TryAssignJobToStation ( station , jobPrototype ) ;
2021-11-26 03:02:46 -06:00
if ( lateJoin )
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . LateJoin , LogImpact . Medium , $"Player {player.Name} late joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}." ) ;
2021-11-26 03:02:46 -06:00
else
2021-12-14 00:22:58 +13:00
_adminLogSystem . Add ( LogType . RoundStartJoin , LogImpact . Medium , $"Player {player.Name} joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}." ) ;
2021-11-26 03:02:46 -06:00
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 ) ;
RaiseLocalEvent ( mob , aev ) ;
2021-06-20 10:09:24 +02:00
}
public void Respawn ( IPlayerSession player )
{
player . ContentData ( ) ? . WipeMind ( ) ;
2021-11-26 03:02:46 -06:00
_adminLogSystem . Add ( LogType . Respawn , LogImpact . Medium , $"Player {player} was respawned." ) ;
2021-06-20 10:09:24 +02:00
if ( LobbyEnabled )
PlayerJoinLobby ( player ) ;
else
2021-11-26 03:02:46 -06:00
SpawnPlayer ( player , StationId . Invalid ) ;
2021-06-20 10:09:24 +02:00
}
2021-11-26 03:02:46 -06:00
public void MakeJoinGame ( IPlayerSession player , StationId 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 SpawnPlayerMob ( Job job , HumanoidCharacterProfile ? profile , StationId station , bool lateJoin = true )
2021-06-20 10:09:24 +02:00
{
2021-11-26 03:02:46 -06:00
var coordinates = lateJoin ? GetLateJoinSpawnPoint ( station ) : GetJobSpawnPoint ( job . Prototype . ID , station ) ;
2022-01-08 19:53:14 -06:00
var entity = EntityManager . SpawnEntity (
_prototypeManager . Index < SpeciesPrototype > ( profile ? . Species ? ? SpeciesManager . DefaultSpecies ) . Prototype ,
coordinates ) ;
2021-06-20 10:09:24 +02:00
if ( job . StartingGear ! = null )
{
var startingGear = _prototypeManager . Index < StartingGearPrototype > ( job . StartingGear ) ;
EquipStartingGear ( entity , startingGear , profile ) ;
}
if ( profile ! = null )
{
2021-12-04 12:35:33 +01:00
_humanoidAppearanceSystem . UpdateFromProfile ( entity , profile ) ;
EntityManager . GetComponent < MetaDataComponent > ( entity ) . EntityName = profile . Name ;
2021-06-20 10:09:24 +02:00
}
return entity ;
}
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 Equip Helpers
2021-12-04 12:35:33 +01:00
public void EquipStartingGear ( EntityUid entity , StartingGearPrototype startingGear , HumanoidCharacterProfile ? profile )
2021-06-20 10:09:24 +02:00
{
2021-12-30 22:56:10 +01:00
if ( _inventorySystem . TryGetSlots ( entity , out var slotDefinitions ) )
2021-06-20 10:09:24 +02:00
{
2021-12-30 22:56:10 +01:00
foreach ( var slot in slotDefinitions )
2021-06-20 10:09:24 +02:00
{
2021-12-30 22:56:10 +01:00
var equipmentStr = startingGear . GetGear ( slot . Name , profile ) ;
2021-09-30 13:35:35 +02:00
if ( ! string . IsNullOrEmpty ( equipmentStr ) )
2021-06-20 10:09:24 +02:00
{
2021-12-07 14:19:41 -08:00
var equipmentEntity = EntityManager . SpawnEntity ( equipmentStr , EntityManager . GetComponent < TransformComponent > ( entity ) . Coordinates ) ;
2021-12-30 22:56:10 +01:00
_inventorySystem . TryEquip ( entity , equipmentEntity , slot . Name , true ) ;
2021-06-20 10:09:24 +02:00
}
}
}
2022-03-17 20:13:31 +13:00
if ( ! TryComp ( entity , out HandsComponent ? handsComponent ) )
return ;
var inhand = startingGear . Inhand ;
var coords = EntityManager . GetComponent < TransformComponent > ( entity ) . Coordinates ;
foreach ( var ( hand , prototype ) in inhand )
2021-06-20 10:09:24 +02:00
{
2022-03-17 20:13:31 +13:00
var inhandEntity = EntityManager . SpawnEntity ( prototype , coords ) ;
_handsSystem . TryPickup ( entity , inhandEntity , hand , checkActionBlocker : false , handsComp : handsComponent ) ;
2021-06-20 10:09:24 +02:00
}
}
2021-12-04 12:35:33 +01:00
public void EquipIdCard ( EntityUid entity , string characterName , JobPrototype jobPrototype )
2021-06-20 10:09:24 +02:00
{
2021-12-30 22:56:10 +01:00
if ( ! _inventorySystem . TryGetSlotEntity ( entity , "id" , out var idUid ) )
2021-06-20 10:09:24 +02:00
return ;
2021-12-30 22:56:10 +01:00
if ( ! EntityManager . TryGetComponent ( idUid , out PDAComponent ? pdaComponent ) | | pdaComponent . ContainedID = = null )
2021-06-20 10:09:24 +02:00
return ;
var card = pdaComponent . ContainedID ;
2021-12-03 15:53:09 +01:00
_cardSystem . TryChangeFullName ( card . Owner , characterName , card ) ;
_cardSystem . TryChangeJobTitle ( card . Owner , jobPrototype . Name , card ) ;
2021-06-20 10:09:24 +02:00
2021-12-07 14:19:41 -08:00
var access = EntityManager . GetComponent < AccessComponent > ( card . Owner ) ;
2021-06-20 10:09:24 +02:00
var accessTags = access . Tags ;
accessTags . UnionWith ( jobPrototype . Access ) ;
2021-12-04 12:35:33 +01:00
_pdaSystem . SetOwner ( pdaComponent , characterName ) ;
2021-06-20 10:09:24 +02:00
}
#endregion
private void AddManifestEntry ( string characterName , string jobId )
{
_manifest . Add ( new ManifestEntry ( characterName , jobId ) ) ;
}
#region Spawn Points
2021-11-26 03:02:46 -06:00
public EntityCoordinates GetJobSpawnPoint ( string jobId , StationId station )
2021-06-20 10:09:24 +02:00
{
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
{
2021-11-26 03:02:46 -06:00
var matchingStation =
EntityManager . TryGetComponent < StationComponent > ( transform . ParentUid , out var stationComponent ) & &
stationComponent . Station = = station ;
DebugTools . Assert ( EntityManager . TryGetComponent < IMapGridComponent > ( transform . ParentUid , out _ ) ) ;
if ( point . SpawnType = = SpawnPointType . Job & & point . Job ? . ID = = jobId & & matchingStation )
2021-06-20 10:09:24 +02:00
_possiblePositions . Add ( transform . Coordinates ) ;
}
if ( _possiblePositions . Count ! = 0 )
location = _robustRandom . Pick ( _possiblePositions ) ;
2021-11-26 03:02:46 -06:00
else
location = GetLateJoinSpawnPoint ( station ) ; // We need a sane fallback here, so latejoin it is.
2021-06-20 10:09:24 +02:00
return location ;
}
2021-11-26 03:02:46 -06:00
public EntityCoordinates GetLateJoinSpawnPoint ( StationId station )
2021-06-20 10:09:24 +02:00
{
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
{
2021-11-26 03:02:46 -06:00
var matchingStation =
EntityManager . TryGetComponent < StationComponent > ( transform . ParentUid , out var stationComponent ) & &
stationComponent . Station = = station ;
DebugTools . Assert ( EntityManager . TryGetComponent < IMapGridComponent > ( transform . ParentUid , out _ ) ) ;
if ( point . SpawnType = = SpawnPointType . LateJoin & & matchingStation )
_possiblePositions . Add ( transform . Coordinates ) ;
2021-06-20 10:09:24 +02:00
}
if ( _possiblePositions . Count ! = 0 )
location = _robustRandom . Pick ( _possiblePositions ) ;
return location ;
}
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-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 ; }
public StationId Station { get ; }
public PlayerBeforeSpawnEvent ( IPlayerSession player , HumanoidCharacterProfile profile , string? jobId , bool lateJoin , StationId station )
{
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-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 ; }
public StationId Station { get ; }
public HumanoidCharacterProfile Profile { get ; }
public PlayerSpawnCompleteEvent ( EntityUid mob , IPlayerSession player , string? jobId , bool lateJoin , StationId station , HumanoidCharacterProfile profile )
{
Mob = mob ;
Player = player ;
JobId = jobId ;
LateJoin = lateJoin ;
Station = station ;
Profile = profile ;
}
}
2021-06-20 10:09:24 +02:00
}