2021-06-20 10:09:24 +02:00
using System.Globalization ;
2021-11-26 03:02:46 -06:00
using System.Linq ;
2023-07-08 14:08:32 +10:00
using System.Numerics ;
2021-11-15 18:14:34 +00:00
using Content.Server.Ghost ;
2021-06-20 10:09:24 +02:00
using Content.Server.Players ;
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 ;
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 ;
2022-08-07 08:00:42 +02:00
using Job = Content . Server . Roles . Job ;
2021-06-20 10:09:24 +02:00
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-07-28 20:59:45 -04:00
/// <summary>
/// How many players have joined the round through normal methods.
/// Useful for game rules to look at. Doesn't count observers, people in lobby, etc.
/// </summary>
public int PlayersJoinedRoundNormally = 0 ;
2021-06-20 10:09:24 +02:00
// 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 )
{
2023-05-19 15:45:09 -05:00
if ( playerNetIds . Contains ( player ) )
continue ;
2022-05-19 07:48:00 +10:00
toRemove . Add ( player ) ;
}
foreach ( var player in toRemove )
{
profiles . Remove ( player ) ;
}
}
2023-05-19 15:45:09 -05:00
var spawnableStations = EntityQuery < StationJobsComponent , StationSpawningComponent > ( ) . Select ( x = > x . Item1 . Owner ) . ToList ( ) ;
var assignedJobs = _stationJobs . AssignJobs ( profiles , spawnableStations ) ;
2022-02-21 14:11:39 -08:00
2023-05-19 15:45:09 -05:00
_stationJobs . AssignOverflowJobs ( ref assignedJobs , playerNetIds , profiles , spawnableStations ) ;
2022-02-21 14:11:39 -08:00
2022-05-27 06:01:07 +02:00
// Calculate extended access for stations.
2023-05-19 15:45:09 -05:00
var stationJobCounts = spawnableStations . ToDictionary ( e = > e , _ = > 0 ) ;
2022-10-29 23:49:43 -07:00
foreach ( var ( netUser , ( job , station ) ) in assignedJobs )
2022-05-27 06:01:07 +02:00
{
2022-10-29 23:49:43 -07:00
if ( job = = null )
{
var playerSession = _playerManager . GetSessionByUserId ( netUser ) ;
_chatManager . DispatchServerMessage ( playerSession , Loc . GetString ( "job-not-available-wait-in-lobby" ) ) ;
}
else
{
stationJobCounts [ station ] + = 1 ;
}
2022-05-27 06:01:07 +02:00
}
_stationJobs . CalcExtendedAccess ( stationJobCounts ) ;
2022-02-21 14:11:39 -08:00
// Spawn everybody in!
foreach ( var ( player , ( job , station ) ) in assignedJobs )
{
2022-10-29 23:49:43 -07:00
if ( job = = null )
continue ;
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 ) ;
2022-08-07 08:00:42 +02:00
if ( jobBans = = null | | jobId ! = null & & jobBans . Contains ( jobId ) )
return ;
if ( jobId ! = null & & ! _playTimeTrackings . IsAllowed ( player , jobId ) )
2022-02-21 19:45:59 -08:00
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
{
2023-05-19 15:45:09 -05:00
var stations = EntityQuery < StationJobsComponent , StationSpawningComponent > ( ) . Select ( x = > x . Item1 . Owner ) . 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
{
2023-06-21 13:04:07 +12:00
JoinAsObserver ( 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 ;
}
2022-08-07 08:00:42 +02:00
// Figure out job restrictions
var restrictedRoles = new HashSet < string > ( ) ;
var getDisallowed = _playTimeTrackings . GetDisallowedJobs ( player ) ;
restrictedRoles . UnionWith ( getDisallowed ) ;
var jobBans = _roleBanManager . GetJobBans ( player . UserId ) ;
if ( jobBans ! = null ) restrictedRoles . UnionWith ( jobBans ) ;
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 ,
2022-08-07 08:00:42 +02:00
restrictedRoles ) ;
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 )
{
2023-06-21 13:04:07 +12:00
JoinAsObserver ( player ) ;
2022-01-23 09:44:27 -08:00
}
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 ) ;
2023-06-21 13:04:07 +12:00
var newMind = _mind . CreateMind ( data ! . UserId , character . Name ) ;
_mind . SetUserId ( newMind , 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 ) ;
2023-06-21 13:04:07 +12:00
_mind . AddRole ( newMind , job ) ;
2021-06-20 10:09:24 +02:00
2022-08-07 08:00:42 +02:00
_playTimeTrackings . PlayerRolesChanged ( player ) ;
2022-11-08 16:13:20 -05:00
var mobMaybe = _stationSpawning . SpawnPlayerCharacterOnStation ( station , job , character ) ;
DebugTools . AssertNotNull ( mobMaybe ) ;
var mob = mobMaybe ! . Value ;
2023-06-21 13:04:07 +12:00
_mind . TransferTo ( newMind , mob ) ;
2022-11-08 16:13:20 -05:00
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" ,
2022-11-08 19:04:50 -05:00
( "character" , MetaData ( mob ) . EntityName ) ,
2021-06-20 10:09:24 +02:00
( "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
}
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" ) ) ;
}
2022-10-22 14:51:51 -07:00
if ( TryComp ( station , out MetaDataComponent ? metaData ) )
{
_chatManager . DispatchServerMessage ( player ,
Loc . GetString ( "job-greet-station-name" , ( "stationName" , metaData . EntityName ) ) ) ;
}
2023-03-22 20:29:55 +11:00
// Arrivals is unable to do this during spawning as no actor is attached yet.
// We also want this message last.
if ( lateJoin & & _arrivals . Enabled )
{
_chatManager . DispatchServerMessage ( player , Loc . GetString ( "latejoin-arrivals-direction" ) ) ;
}
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.
2022-07-28 20:59:45 -04:00
PlayersJoinedRoundNormally + + ;
var aev = new PlayerSpawnCompleteEvent ( mob , player , jobId , lateJoin , PlayersJoinedRoundNormally , 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 )
{
2023-06-21 13:04:07 +12:00
_mind . WipeMind ( player ) ;
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
{
2022-08-14 12:54:49 -07:00
if ( ! _playerGameStatuses . ContainsKey ( player . UserId ) )
2022-08-07 08:00:42 +02:00
return ;
2021-06-20 10:09:24 +02:00
2022-08-07 08:00:42 +02:00
if ( ! _userDb . IsLoadComplete ( player ) )
2021-06-20 10:09:24 +02:00
return ;
2021-11-26 03:02:46 -06:00
SpawnPlayer ( player , station , jobId ) ;
2021-06-20 10:09:24 +02:00
}
2023-06-21 13:04:07 +12:00
/// <summary>
/// Causes the given player to join the current game as observer ghost. See also <see cref="SpawnObserver"/>
/// </summary>
public void JoinAsObserver ( IPlayerSession player )
2021-06-20 10:09:24 +02:00
{
// Can't spawn players with a dummy ticker!
if ( DummyTicker )
return ;
PlayerJoinGame ( player ) ;
2023-06-21 13:04:07 +12:00
SpawnObserver ( player ) ;
}
2021-06-20 10:09:24 +02:00
2023-06-21 13:04:07 +12:00
/// <summary>
/// Spawns an observer ghost and attaches the given player to it. If the player does not yet have a mind, the
/// player is given a new mind with the observer role. Otherwise, the current mind is transferred to the ghost.
/// </summary>
public void SpawnObserver ( IPlayerSession player )
{
if ( DummyTicker )
return ;
2021-06-20 10:09:24 +02:00
2023-06-21 13:04:07 +12:00
var mind = player . GetMind ( ) ;
if ( mind = = null )
{
mind = _mind . CreateMind ( player . UserId ) ;
_mind . SetUserId ( mind , player . UserId ) ;
_mind . AddRole ( mind , new ObserverRole ( mind ) ) ;
}
2021-06-20 10:09:24 +02:00
2023-06-21 13:04:07 +12:00
var name = GetPlayerProfile ( player ) . Name ;
var ghost = SpawnObserverMob ( ) ;
MetaData ( ghost ) . EntityName = name ;
_ghost . SetCanReturnToBody ( ghost , false ) ;
_mind . TransferTo ( mind , ghost ) ;
2021-06-20 10:09:24 +02:00
}
#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 ( )
{
_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
{
2022-08-29 15:05:53 +10:00
if ( point . SpawnType ! = SpawnPointType . Observer )
continue ;
_possiblePositions . Add ( transform . Coordinates ) ;
}
var metaQuery = GetEntityQuery < MetaDataComponent > ( ) ;
// Fallback to a random grid.
if ( _possiblePositions . Count = = 0 )
{
foreach ( var grid in _mapManager . GetAllGrids ( ) )
{
2022-12-12 14:59:02 +11:00
if ( ! metaQuery . TryGetComponent ( grid . Owner , out var meta ) | |
2022-08-29 15:05:53 +10:00
meta . EntityPaused )
{
continue ;
}
2022-12-12 14:59:02 +11:00
_possiblePositions . Add ( new EntityCoordinates ( grid . Owner , Vector2 . Zero ) ) ;
2022-08-29 15:05:53 +10:00
}
2021-06-20 10:09:24 +02:00
}
if ( _possiblePositions . Count ! = 0 )
2022-08-29 15:05:53 +10:00
{
// TODO: This is just here for the eye lerping.
// Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
// update which means we need to add a hack somewhere around it.
var spawn = _robustRandom . Pick ( _possiblePositions ) ;
var toMap = spawn . ToMap ( EntityManager ) ;
2023-05-28 23:22:44 +10:00
if ( _mapManager . TryFindGridAt ( toMap , out var gridUid , out _ ) )
2022-08-29 15:05:53 +10:00
{
2023-05-28 23:22:44 +10:00
var gridXform = Transform ( gridUid ) ;
2022-11-01 11:27:18 +11:00
2023-05-28 23:22:44 +10:00
return new EntityCoordinates ( gridUid ,
2022-11-01 11:27:18 +11:00
gridXform . InvWorldMatrix . Transform ( toMap . Position ) ) ;
2022-08-29 15:05:53 +10:00
}
return spawn ;
}
if ( _mapManager . MapExists ( DefaultMap ) )
{
return new EntityCoordinates ( _mapManager . GetMapEntityId ( DefaultMap ) , Vector2 . Zero ) ;
}
// Just pick a point at this point I guess.
foreach ( var map in _mapManager . GetAllMapIds ( ) )
{
var mapUid = _mapManager . GetMapEntityId ( map ) ;
if ( ! metaQuery . TryGetComponent ( mapUid , out var meta ) | |
meta . EntityPaused )
{
continue ;
}
return new EntityCoordinates ( mapUid , Vector2 . Zero ) ;
}
2021-06-20 10:09:24 +02:00
2022-08-29 15:05:53 +10:00
// AAAAAAAAAAAAA
2023-01-24 13:33:49 +13:00
// This should be an error, if it didn't cause tests to start erroring when they delete a player.
_sawmill . Warning ( "Found no observer spawn points!" ) ;
2022-08-29 15:05:53 +10:00
return EntityCoordinates . Invalid ;
2021-06-20 10:09:24 +02:00
}
#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-07-28 20:59:45 -04:00
// Ex. If this is the 27th person to join, this will be 27.
public int JoinOrder { get ; }
public PlayerSpawnCompleteEvent ( EntityUid mob , IPlayerSession player , string? jobId , bool lateJoin , int joinOrder , EntityUid station , HumanoidCharacterProfile profile )
2021-12-21 21:23:29 +01:00
{
Mob = mob ;
Player = player ;
JobId = jobId ;
LateJoin = lateJoin ;
Station = station ;
Profile = profile ;
2022-07-28 20:59:45 -04:00
JoinOrder = joinOrder ;
2021-12-21 21:23:29 +01:00
}
}
2021-06-20 10:09:24 +02:00
}