2024-05-07 03:48:16 +02:00
using System.Linq ;
2022-01-09 17:54:36 -08:00
using Content.Server.Administration.Logs ;
2021-06-09 22:19:39 +02:00
using Content.Server.EUI ;
using Content.Server.Ghost.Roles.Components ;
2023-04-12 06:32:14 -07:00
using Content.Server.Ghost.Roles.Events ;
2024-05-07 03:48:16 +02:00
using Content.Server.Ghost.Roles.Raffles ;
using Content.Shared.Ghost.Roles.Raffles ;
2021-06-09 22:19:39 +02:00
using Content.Server.Ghost.Roles.UI ;
2023-08-28 16:53:24 -07:00
using Content.Server.Mind.Commands ;
2021-12-13 16:45:49 +11:00
using Content.Shared.Administration ;
2022-01-09 17:54:36 -08:00
using Content.Shared.Database ;
2022-04-03 01:06:29 +03:00
using Content.Shared.Follower ;
2021-02-12 04:35:56 +01:00
using Content.Shared.GameTicking ;
2021-11-03 23:48:12 +00:00
using Content.Shared.Ghost ;
2021-12-05 21:02:04 +01:00
using Content.Shared.Ghost.Roles ;
2023-08-30 21:46:11 -07:00
using Content.Shared.Mind ;
using Content.Shared.Mind.Components ;
2023-01-13 16:57:10 -08:00
using Content.Shared.Mobs ;
2023-10-28 09:59:53 +11:00
using Content.Shared.Players ;
2023-08-30 21:46:11 -07:00
using Content.Shared.Roles ;
2021-02-12 04:35:56 +01:00
using JetBrains.Annotations ;
using Robust.Server.GameObjects ;
2021-02-12 10:45:22 +01:00
using Robust.Server.Player ;
2021-02-12 04:35:56 +01:00
using Robust.Shared.Console ;
2021-12-05 21:02:04 +01:00
using Robust.Shared.Enums ;
2023-10-28 09:59:53 +11:00
using Robust.Shared.Player ;
2024-05-07 03:48:16 +02:00
using Robust.Shared.Prototypes ;
2022-07-14 22:20:37 -07:00
using Robust.Shared.Random ;
2024-05-07 03:48:16 +02:00
using Robust.Shared.Timing ;
2021-11-15 18:14:34 +00:00
using Robust.Shared.Utility ;
2024-04-26 09:06:43 -04:00
using Content.Server.Popups ;
using Content.Shared.Verbs ;
using Robust.Shared.Prototypes ;
using Robust.Shared.Collections ;
2021-02-12 04:35:56 +01:00
2021-06-09 22:19:39 +02:00
namespace Content.Server.Ghost.Roles
2021-02-12 04:35:56 +01:00
{
[UsedImplicitly]
2022-02-16 00:23:23 -07:00
public sealed class GhostRoleSystem : EntitySystem
2021-02-12 04:35:56 +01:00
{
[Dependency] private readonly EuiManager _euiManager = default ! ;
2021-11-22 23:11:48 -08:00
[Dependency] private readonly IPlayerManager _playerManager = default ! ;
2022-05-28 23:41:17 -07:00
[Dependency] private readonly IAdminLogManager _adminLogger = default ! ;
2022-07-14 22:20:37 -07:00
[Dependency] private readonly IRobustRandom _random = default ! ;
2022-04-03 01:06:29 +03:00
[Dependency] private readonly FollowerSystem _followerSystem = default ! ;
2023-04-12 06:32:14 -07:00
[Dependency] private readonly TransformSystem _transform = default ! ;
2023-08-30 21:46:11 -07:00
[Dependency] private readonly SharedMindSystem _mindSystem = default ! ;
[Dependency] private readonly SharedRoleSystem _roleSystem = default ! ;
2024-05-07 03:48:16 +02:00
[Dependency] private readonly IGameTiming _timing = default ! ;
2024-04-26 09:06:43 -04:00
[Dependency] private readonly PopupSystem _popupSystem = default ! ;
[Dependency] private readonly IPrototypeManager _prototype = default ! ;
2021-02-12 04:35:56 +01:00
2021-11-22 23:11:48 -08:00
private uint _nextRoleIdentifier ;
2021-11-03 23:48:12 +00:00
private bool _needsUpdateGhostRoleCount = true ;
2024-05-07 03:48:16 +02:00
2023-10-19 12:34:31 -07:00
private readonly Dictionary < uint , Entity < GhostRoleComponent > > _ghostRoles = new ( ) ;
2024-05-07 03:48:16 +02:00
private readonly Dictionary < uint , Entity < GhostRoleRaffleComponent > > _ghostRoleRaffles = new ( ) ;
2023-08-30 21:46:11 -07:00
private readonly Dictionary < ICommonSession , GhostRolesEui > _openUis = new ( ) ;
private readonly Dictionary < ICommonSession , MakeGhostRoleEui > _openMakeGhostRoleUis = new ( ) ;
2021-02-12 04:35:56 +01:00
[ViewVariables]
2023-10-19 12:34:31 -07:00
public IReadOnlyCollection < Entity < GhostRoleComponent > > GhostRoles = > _ghostRoles . Values ;
2021-02-12 04:35:56 +01:00
public override void Initialize ( )
{
2021-04-09 16:08:12 +02:00
base . Initialize ( ) ;
2021-06-29 15:56:07 +02:00
SubscribeLocalEvent < RoundRestartCleanupEvent > ( Reset ) ;
2021-05-30 11:39:42 +02:00
SubscribeLocalEvent < PlayerAttachedEvent > ( OnPlayerAttached ) ;
2022-01-12 18:25:05 +11:00
SubscribeLocalEvent < GhostTakeoverAvailableComponent , MindAddedMessage > ( OnMindAdded ) ;
2022-01-01 22:00:21 -08:00
SubscribeLocalEvent < GhostTakeoverAvailableComponent , MindRemovedMessage > ( OnMindRemoved ) ;
SubscribeLocalEvent < GhostTakeoverAvailableComponent , MobStateChangedEvent > ( OnMobStateChanged ) ;
2024-03-27 17:31:26 +13:00
SubscribeLocalEvent < GhostRoleComponent , MapInitEvent > ( OnMapInit ) ;
2024-05-07 03:48:16 +02:00
SubscribeLocalEvent < GhostRoleComponent , ComponentStartup > ( OnRoleStartup ) ;
SubscribeLocalEvent < GhostRoleComponent , ComponentShutdown > ( OnRoleShutdown ) ;
2023-06-10 22:24:34 +10:00
SubscribeLocalEvent < GhostRoleComponent , EntityPausedEvent > ( OnPaused ) ;
SubscribeLocalEvent < GhostRoleComponent , EntityUnpausedEvent > ( OnUnpaused ) ;
2024-05-07 03:48:16 +02:00
SubscribeLocalEvent < GhostRoleRaffleComponent , ComponentInit > ( OnRaffleInit ) ;
SubscribeLocalEvent < GhostRoleRaffleComponent , ComponentShutdown > ( OnRaffleShutdown ) ;
2023-04-12 06:32:14 -07:00
SubscribeLocalEvent < GhostRoleMobSpawnerComponent , TakeGhostRoleEvent > ( OnSpawnerTakeRole ) ;
SubscribeLocalEvent < GhostTakeoverAvailableComponent , TakeGhostRoleEvent > ( OnTakeoverTakeRole ) ;
2024-04-26 09:06:43 -04:00
SubscribeLocalEvent < GhostRoleMobSpawnerComponent , GetVerbsEvent < Verb > > ( OnVerb ) ;
2021-11-03 23:48:12 +00:00
_playerManager . PlayerStatusChanged + = PlayerStatusChanged ;
}
2023-10-19 12:34:31 -07:00
private void OnMobStateChanged ( Entity < GhostTakeoverAvailableComponent > component , ref MobStateChangedEvent args )
2022-01-01 22:00:21 -08:00
{
2023-10-19 12:34:31 -07:00
if ( ! TryComp ( component , out GhostRoleComponent ? ghostRole ) )
2023-04-12 06:32:14 -07:00
return ;
2023-01-13 16:57:10 -08:00
switch ( args . NewMobState )
2022-01-01 22:00:21 -08:00
{
2023-01-13 16:57:10 -08:00
case MobState . Alive :
2024-04-26 09:06:43 -04:00
{
if ( ! ghostRole . Taken )
RegisterGhostRole ( ( component , ghostRole ) ) ;
break ;
}
2023-01-13 16:57:10 -08:00
case MobState . Critical :
case MobState . Dead :
2023-10-19 12:34:31 -07:00
UnregisterGhostRole ( ( component , ghostRole ) ) ;
2022-01-01 22:00:21 -08:00
break ;
}
}
2021-11-03 23:48:12 +00:00
public override void Shutdown ( )
{
base . Shutdown ( ) ;
_playerManager . PlayerStatusChanged - = PlayerStatusChanged ;
2021-02-12 04:35:56 +01:00
}
private uint GetNextRoleIdentifier ( )
{
return unchecked ( _nextRoleIdentifier + + ) ;
}
2023-10-28 09:59:53 +11:00
public void OpenEui ( ICommonSession session )
2021-02-12 04:35:56 +01:00
{
2024-04-26 09:06:43 -04:00
if ( session . AttachedEntity is not { Valid : true } attached | |
2021-12-05 21:02:04 +01:00
! EntityManager . HasComponent < GhostComponent > ( attached ) )
2021-02-12 04:35:56 +01:00
return ;
2024-04-26 09:06:43 -04:00
if ( _openUis . ContainsKey ( session ) )
2021-02-12 04:35:56 +01:00
CloseEui ( session ) ;
var eui = _openUis [ session ] = new GhostRolesEui ( ) ;
_euiManager . OpenEui ( eui , session ) ;
eui . StateDirty ( ) ;
}
2023-10-28 09:59:53 +11:00
public void OpenMakeGhostRoleEui ( ICommonSession session , EntityUid uid )
2021-02-16 09:51:27 +01:00
{
if ( session . AttachedEntity = = null )
return ;
if ( _openMakeGhostRoleUis . ContainsKey ( session ) )
CloseEui ( session ) ;
2023-09-11 14:31:45 +10:00
var eui = _openMakeGhostRoleUis [ session ] = new MakeGhostRoleEui ( EntityManager , GetNetEntity ( uid ) ) ;
2021-02-16 09:51:27 +01:00
_euiManager . OpenEui ( eui , session ) ;
eui . StateDirty ( ) ;
}
2023-08-30 21:46:11 -07:00
public void CloseEui ( ICommonSession session )
2021-02-12 04:35:56 +01:00
{
2023-10-19 12:34:31 -07:00
if ( ! _openUis . ContainsKey ( session ) )
return ;
2021-02-12 04:35:56 +01:00
_openUis . Remove ( session , out var eui ) ;
eui ? . Close ( ) ;
}
2023-08-30 21:46:11 -07:00
public void CloseMakeGhostRoleEui ( ICommonSession session )
2021-02-16 09:51:27 +01:00
{
if ( _openMakeGhostRoleUis . Remove ( session , out var eui ) )
{
2021-11-22 23:11:48 -08:00
eui . Close ( ) ;
2021-02-16 09:51:27 +01:00
}
}
2021-02-12 04:35:56 +01:00
public void UpdateAllEui ( )
{
foreach ( var eui in _openUis . Values )
{
eui . StateDirty ( ) ;
}
2021-11-03 23:48:12 +00:00
// Note that this, like the EUIs, is deferred.
// This is for roughly the same reasons, too:
// Someone might spawn a ton of ghost roles at once.
_needsUpdateGhostRoleCount = true ;
}
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
2024-05-07 03:48:16 +02:00
UpdateGhostRoleCount ( ) ;
UpdateRaffles ( frameTime ) ;
}
/// <summary>
/// Handles sending count update for the ghost role button in ghost UI, if ghost role count changed.
/// </summary>
private void UpdateGhostRoleCount ( )
{
if ( ! _needsUpdateGhostRoleCount )
return ;
_needsUpdateGhostRoleCount = false ;
var response = new GhostUpdateGhostRoleCountEvent ( GetGhostRoleCount ( ) ) ;
foreach ( var player in _playerManager . Sessions )
{
RaiseNetworkEvent ( response , player . Channel ) ;
}
}
/// <summary>
/// Handles ghost role raffle logic.
/// </summary>
private void UpdateRaffles ( float frameTime )
{
var query = EntityQueryEnumerator < GhostRoleRaffleComponent , MetaDataComponent > ( ) ;
while ( query . MoveNext ( out var entityUid , out var raffle , out var meta ) )
2021-11-03 23:48:12 +00:00
{
2024-05-07 03:48:16 +02:00
if ( meta . EntityPaused )
continue ;
// if all participants leave/were removed from the raffle, the raffle is canceled.
if ( raffle . CurrentMembers . Count = = 0 )
2021-11-22 23:11:48 -08:00
{
2024-05-07 03:48:16 +02:00
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
continue ;
}
raffle . Countdown = raffle . Countdown . Subtract ( TimeSpan . FromSeconds ( frameTime ) ) ;
if ( raffle . Countdown . Ticks > 0 )
continue ;
// the raffle is over! find someone to take over the ghost role
if ( ! TryComp ( entityUid , out GhostRoleComponent ? ghostRole ) )
{
Log . Warning ( $"Ghost role raffle finished on {entityUid} but {nameof(GhostRoleComponent)} is missing" ) ;
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
continue ;
}
if ( ghostRole . RaffleConfig is null )
{
Log . Warning ( $"Ghost role raffle finished on {entityUid} but RaffleConfig became null" ) ;
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
continue ;
2021-11-22 23:11:48 -08:00
}
2024-05-07 03:48:16 +02:00
var foundWinner = false ;
var deciderPrototype = _prototype . Index ( ghostRole . RaffleConfig . Decider ) ;
// use the ghost role's chosen winner picker to find a winner
deciderPrototype . Decider . PickWinner (
raffle . CurrentMembers . AsEnumerable ( ) ,
session = >
{
var success = TryTakeover ( session , raffle . Identifier ) ;
foundWinner | = success ;
return success ;
}
) ;
if ( ! foundWinner )
{
Log . Warning ( $"Ghost role raffle for {entityUid} ({ghostRole.RoleName}) finished without " +
$"{ghostRole.RaffleConfig?.Decider} finding a winner" ) ;
}
// raffle over
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
}
}
private bool TryTakeover ( ICommonSession player , uint identifier )
{
// TODO: the following two checks are kind of redundant since they should already be removed
// from the raffle
// can't win if you are disconnected (although you shouldn't be a candidate anyway)
if ( player . Status ! = SessionStatus . InGame )
return false ;
// can't win if you are no longer a ghost (e.g. if you returned to your body)
if ( player . AttachedEntity = = null | | ! HasComp < GhostComponent > ( player . AttachedEntity ) )
return false ;
if ( Takeover ( player , identifier ) )
{
// takeover successful, we have a winner! remove the winner from other raffles they might be in
LeaveAllRaffles ( player ) ;
return true ;
2021-11-03 23:48:12 +00:00
}
2024-05-07 03:48:16 +02:00
return false ;
}
private void RemoveRaffleAndUpdateEui ( EntityUid entityUid , GhostRoleRaffleComponent raffle )
{
_ghostRoleRaffles . Remove ( raffle . Identifier ) ;
RemComp ( entityUid , raffle ) ;
UpdateAllEui ( ) ;
2021-11-03 23:48:12 +00:00
}
private void PlayerStatusChanged ( object? blah , SessionStatusEventArgs args )
{
if ( args . NewStatus = = SessionStatus . InGame )
{
var response = new GhostUpdateGhostRoleCountEvent ( _ghostRoles . Count ) ;
2024-01-22 23:14:13 +01:00
RaiseNetworkEvent ( response , args . Session . Channel ) ;
2021-11-03 23:48:12 +00:00
}
2024-05-07 03:48:16 +02:00
else
{
// people who disconnect are removed from ghost role raffles
LeaveAllRaffles ( args . Session ) ;
}
2021-02-12 04:35:56 +01:00
}
2023-10-19 12:34:31 -07:00
public void RegisterGhostRole ( Entity < GhostRoleComponent > role )
2021-02-12 04:35:56 +01:00
{
2023-10-19 12:34:31 -07:00
if ( _ghostRoles . ContainsValue ( role ) )
return ;
2021-02-12 04:35:56 +01:00
2023-10-19 12:34:31 -07:00
_ghostRoles [ role . Comp . Identifier = GetNextRoleIdentifier ( ) ] = role ;
UpdateAllEui ( ) ;
2021-02-12 04:35:56 +01:00
}
2023-10-19 12:34:31 -07:00
public void UnregisterGhostRole ( Entity < GhostRoleComponent > role )
2021-02-12 04:35:56 +01:00
{
2023-10-19 12:34:31 -07:00
var comp = role . Comp ;
if ( ! _ghostRoles . ContainsKey ( comp . Identifier ) | | _ghostRoles [ comp . Identifier ] ! = role )
return ;
_ghostRoles . Remove ( comp . Identifier ) ;
2024-05-07 03:48:16 +02:00
if ( TryComp ( role . Owner , out GhostRoleRaffleComponent ? raffle ) )
{
// if a raffle is still running, get rid of it
RemoveRaffleAndUpdateEui ( role . Owner , raffle ) ;
}
else
{
UpdateAllEui ( ) ;
}
}
// probably fine to be init because it's never added during entity initialization, but much later
private void OnRaffleInit ( Entity < GhostRoleRaffleComponent > ent , ref ComponentInit args )
{
if ( ! TryComp ( ent , out GhostRoleComponent ? ghostRole ) )
{
// can't have a raffle for a ghost role that doesn't exist
RemComp < GhostRoleRaffleComponent > ( ent ) ;
return ;
}
var config = ghostRole . RaffleConfig ;
if ( config is null )
return ; // should, realistically, never be reached but you never know
var settings = config . SettingsOverride
? ? _prototype . Index < GhostRoleRaffleSettingsPrototype > ( config . Settings ) . Settings ;
if ( settings . MaxDuration < settings . InitialDuration )
{
Log . Error ( $"Ghost role on {ent} has invalid raffle settings (max duration shorter than initial)" ) ;
ghostRole . RaffleConfig = null ; // make it a non-raffle role so stuff isn't entirely broken
RemComp < GhostRoleRaffleComponent > ( ent ) ;
return ;
}
var raffle = ent . Comp ;
raffle . Identifier = ghostRole . Identifier ;
raffle . Countdown = TimeSpan . FromSeconds ( settings . InitialDuration ) ;
raffle . CumulativeTime = TimeSpan . FromSeconds ( settings . InitialDuration ) ;
// we copy these settings into the component because they would be cumbersome to access otherwise
raffle . JoinExtendsDurationBy = TimeSpan . FromSeconds ( settings . JoinExtendsDurationBy ) ;
raffle . MaxDuration = TimeSpan . FromSeconds ( settings . MaxDuration ) ;
}
private void OnRaffleShutdown ( Entity < GhostRoleRaffleComponent > ent , ref ComponentShutdown args )
{
_ghostRoleRaffles . Remove ( ent . Comp . Identifier ) ;
}
/// <summary>
/// Joins the given player onto a ghost role raffle, or creates it if it doesn't exist.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="identifier">The ID that represents the ghost role or ghost role raffle.
/// (A raffle will have the same ID as the ghost role it's for.)</param>
private void JoinRaffle ( ICommonSession player , uint identifier )
{
if ( ! _ghostRoles . TryGetValue ( identifier , out var roleEnt ) )
return ;
// get raffle or create a new one if it doesn't exist
var raffle = _ghostRoleRaffles . TryGetValue ( identifier , out var raffleEnt )
? raffleEnt . Comp
: EnsureComp < GhostRoleRaffleComponent > ( roleEnt . Owner ) ;
_ghostRoleRaffles . TryAdd ( identifier , ( roleEnt . Owner , raffle ) ) ;
if ( ! raffle . CurrentMembers . Add ( player ) )
{
Log . Warning ( $"{player.Name} tried to join raffle for ghost role {identifier} but they are already in the raffle" ) ;
return ;
}
// if this is the first time the player joins this raffle, and the player wasn't the starter of the raffle:
// extend the countdown, but only if doing so will not make the raffle take longer than the maximum
// duration
if ( raffle . AllMembers . Add ( player ) & & raffle . AllMembers . Count > 1
& & raffle . CumulativeTime . Add ( raffle . JoinExtendsDurationBy ) < = raffle . MaxDuration )
{
raffle . Countdown + = raffle . JoinExtendsDurationBy ;
raffle . CumulativeTime + = raffle . JoinExtendsDurationBy ;
}
2021-02-12 04:35:56 +01:00
UpdateAllEui ( ) ;
}
2024-05-07 03:48:16 +02:00
/// <summary>
/// Makes the given player leave the raffle corresponding to the given ID.
/// </summary>
public void LeaveRaffle ( ICommonSession player , uint identifier )
2021-02-12 04:35:56 +01:00
{
2024-05-07 03:48:16 +02:00
if ( ! _ghostRoleRaffles . TryGetValue ( identifier , out var raffleEnt ) )
2023-10-19 12:34:31 -07:00
return ;
2023-04-12 06:32:14 -07:00
2024-05-07 03:48:16 +02:00
if ( raffleEnt . Comp . CurrentMembers . Remove ( player ) )
{
UpdateAllEui ( ) ;
}
else
{
Log . Warning ( $"{player.Name} tried to leave raffle for ghost role {identifier} but they are not in the raffle" ) ;
}
// (raffle ending because all players left is handled in update())
}
/// <summary>
/// Makes the given player leave all ghost role raffles.
/// </summary>
public void LeaveAllRaffles ( ICommonSession player )
{
var shouldUpdateEui = false ;
foreach ( var raffleEnt in _ghostRoleRaffles . Values )
{
shouldUpdateEui | = raffleEnt . Comp . CurrentMembers . Remove ( player ) ;
}
if ( shouldUpdateEui )
UpdateAllEui ( ) ;
}
/// <summary>
/// Request a ghost role. If it's a raffled role starts or joins a raffle, otherwise the player immediately
/// takes over the ghost role if possible.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="identifier">ID of the ghost role.</param>
public void Request ( ICommonSession player , uint identifier )
{
if ( ! _ghostRoles . TryGetValue ( identifier , out var roleEnt ) )
return ;
if ( roleEnt . Comp . RaffleConfig is not null )
{
JoinRaffle ( player , identifier ) ;
}
else
{
Takeover ( player , identifier ) ;
}
}
/// <summary>
/// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
/// </summary>
/// <returns>True if takeover was successful, otherwise false.</returns>
public bool Takeover ( ICommonSession player , uint identifier )
{
if ( ! _ghostRoles . TryGetValue ( identifier , out var role ) )
return false ;
2023-04-12 06:32:14 -07:00
var ev = new TakeGhostRoleEvent ( player ) ;
2023-10-19 12:34:31 -07:00
RaiseLocalEvent ( role , ref ev ) ;
2023-04-12 06:32:14 -07:00
2023-10-19 12:34:31 -07:00
if ( ! ev . TookRole )
2024-05-07 03:48:16 +02:00
return false ;
2022-01-09 17:54:36 -08:00
if ( player . AttachedEntity ! = null )
2023-10-19 12:34:31 -07:00
_adminLogger . Add ( LogType . GhostRoleTaken , LogImpact . Low , $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity}" ) ;
2022-01-09 17:54:36 -08:00
2021-02-12 04:35:56 +01:00
CloseEui ( player ) ;
2024-05-07 03:48:16 +02:00
return true ;
2021-02-12 04:35:56 +01:00
}
2023-08-30 21:46:11 -07:00
public void Follow ( ICommonSession player , uint identifier )
2022-04-03 01:06:29 +03:00
{
2023-10-19 12:34:31 -07:00
if ( ! _ghostRoles . TryGetValue ( identifier , out var role ) )
return ;
if ( player . AttachedEntity = = null )
return ;
2022-04-03 01:06:29 +03:00
2023-10-19 12:34:31 -07:00
_followerSystem . StartFollowingEntity ( player . AttachedEntity . Value , role ) ;
2022-04-03 01:06:29 +03:00
}
2023-08-30 21:46:11 -07:00
public void GhostRoleInternalCreateMindAndTransfer ( ICommonSession player , EntityUid roleUid , EntityUid mob , GhostRoleComponent ? role = null )
2021-11-15 18:14:34 +00:00
{
2023-10-19 12:34:31 -07:00
if ( ! Resolve ( roleUid , ref role ) )
return ;
2021-11-15 18:14:34 +00:00
2023-06-18 11:33:19 -07:00
DebugTools . AssertNotNull ( player . ContentData ( ) ) ;
2021-11-15 18:14:34 +00:00
2023-06-18 11:33:19 -07:00
var newMind = _mindSystem . CreateMind ( player . UserId ,
EntityManager . GetComponent < MetaDataComponent > ( mob ) . EntityName ) ;
2023-08-28 16:53:24 -07:00
_roleSystem . MindAddRole ( newMind , new GhostRoleMarkerRoleComponent { Name = role . RoleName } ) ;
2021-11-15 18:14:34 +00:00
2023-06-20 16:29:26 +12:00
_mindSystem . SetUserId ( newMind , player . UserId ) ;
2023-06-18 11:33:19 -07:00
_mindSystem . TransferTo ( newMind , mob ) ;
2021-11-15 18:14:34 +00:00
}
2024-05-07 03:48:16 +02:00
/// <summary>
/// Returns the number of available ghost roles.
/// </summary>
public int GetGhostRoleCount ( )
{
var metaQuery = GetEntityQuery < MetaDataComponent > ( ) ;
return _ghostRoles . Count ( pair = > metaQuery . GetComponent ( pair . Value . Owner ) . EntityPaused = = false ) ;
}
/// <summary>
/// Returns information about all available ghost roles.
/// </summary>
/// <param name="player">
/// If not null, the <see cref="GhostRoleInfo"/>s will show if the given player is in a raffle.
/// </param>
public GhostRoleInfo [ ] GetGhostRolesInfo ( ICommonSession ? player )
2021-02-12 04:35:56 +01:00
{
2023-06-10 22:24:34 +10:00
var roles = new List < GhostRoleInfo > ( ) ;
var metaQuery = GetEntityQuery < MetaDataComponent > ( ) ;
2021-02-12 04:35:56 +01:00
2023-10-19 12:34:31 -07:00
foreach ( var ( id , ( uid , role ) ) in _ghostRoles )
2021-02-12 04:35:56 +01:00
{
2023-06-10 22:24:34 +10:00
if ( metaQuery . GetComponent ( uid ) . EntityPaused )
continue ;
2024-05-07 03:48:16 +02:00
var kind = GhostRoleKind . FirstComeFirstServe ;
GhostRoleRaffleComponent ? raffle = null ;
if ( role . RaffleConfig is not null )
{
kind = GhostRoleKind . RaffleReady ;
if ( _ghostRoleRaffles . TryGetValue ( id , out var raffleEnt ) )
{
kind = GhostRoleKind . RaffleInProgress ;
raffle = raffleEnt . Comp ;
if ( player is not null & & raffle . CurrentMembers . Contains ( player ) )
kind = GhostRoleKind . RaffleJoined ;
}
}
var rafflePlayerCount = ( uint? ) raffle ? . CurrentMembers . Count ? ? 0 ;
var raffleEndTime = raffle is not null
? _timing . CurTime . Add ( raffle . Countdown )
: TimeSpan . MinValue ;
roles . Add ( new GhostRoleInfo
{
Identifier = id ,
Name = role . RoleName ,
Description = role . RoleDescription ,
Rules = role . RoleRules ,
Requirements = role . Requirements ,
Kind = kind ,
RafflePlayerCount = rafflePlayerCount ,
RaffleEndTime = raffleEndTime
} ) ;
2021-02-12 04:35:56 +01:00
}
2023-06-10 22:24:34 +10:00
return roles . ToArray ( ) ;
2021-02-12 04:35:56 +01:00
}
2021-05-30 11:39:42 +02:00
private void OnPlayerAttached ( PlayerAttachedEvent message )
2021-02-12 04:35:56 +01:00
{
// Close the session of any player that has a ghost roles window open and isn't a ghost anymore.
2023-10-19 12:34:31 -07:00
if ( ! _openUis . ContainsKey ( message . Player ) )
return ;
if ( HasComp < GhostComponent > ( message . Entity ) )
return ;
2024-05-07 03:48:16 +02:00
// The player is not a ghost (anymore), so they should not be in any raffles. Remove them.
// This ensures player doesn't win a raffle after returning to their (revived) body and ends up being
// forced into a ghost role.
LeaveAllRaffles ( message . Player ) ;
2021-05-30 11:39:42 +02:00
CloseEui ( message . Player ) ;
2021-02-12 04:35:56 +01:00
}
2022-01-12 18:25:05 +11:00
private void OnMindAdded ( EntityUid uid , GhostTakeoverAvailableComponent component , MindAddedMessage args )
{
2023-04-12 06:32:14 -07:00
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) )
return ;
ghostRole . Taken = true ;
2023-10-19 12:34:31 -07:00
UnregisterGhostRole ( ( uid , ghostRole ) ) ;
2022-01-12 18:25:05 +11:00
}
2023-04-12 06:32:14 -07:00
private void OnMindRemoved ( EntityUid uid , GhostTakeoverAvailableComponent component , MindRemovedMessage args )
2022-01-01 08:48:45 -08:00
{
2023-04-12 06:32:14 -07:00
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) )
return ;
2022-01-11 14:12:19 +11:00
// Avoid re-registering it for duplicate entries and potential exceptions.
2023-04-12 06:32:14 -07:00
if ( ! ghostRole . ReregisterOnGhost | | component . LifeStage > ComponentLifeStage . Running )
2022-01-01 08:48:45 -08:00
return ;
2022-01-11 14:12:19 +11:00
2023-04-12 06:32:14 -07:00
ghostRole . Taken = false ;
2023-10-19 12:34:31 -07:00
RegisterGhostRole ( ( uid , ghostRole ) ) ;
2022-01-01 08:48:45 -08:00
}
2021-06-29 15:56:07 +02:00
public void Reset ( RoundRestartCleanupEvent ev )
2021-02-12 04:35:56 +01:00
{
foreach ( var session in _openUis . Keys )
{
CloseEui ( session ) ;
}
_openUis . Clear ( ) ;
_ghostRoles . Clear ( ) ;
2024-05-07 03:48:16 +02:00
_ghostRoleRaffles . Clear ( ) ;
2021-02-12 04:35:56 +01:00
_nextRoleIdentifier = 0 ;
}
2022-05-12 06:11:50 -05:00
2023-06-10 22:24:34 +10:00
private void OnPaused ( EntityUid uid , GhostRoleComponent component , ref EntityPausedEvent args )
{
if ( HasComp < ActorComponent > ( uid ) )
return ;
UpdateAllEui ( ) ;
}
private void OnUnpaused ( EntityUid uid , GhostRoleComponent component , ref EntityUnpausedEvent args )
{
if ( HasComp < ActorComponent > ( uid ) )
return ;
UpdateAllEui ( ) ;
}
2024-03-27 17:31:26 +13:00
private void OnMapInit ( Entity < GhostRoleComponent > ent , ref MapInitEvent args )
2022-05-12 06:11:50 -05:00
{
2024-03-27 17:31:26 +13:00
if ( ent . Comp . Probability < 1f & & ! _random . Prob ( ent . Comp . Probability ) )
RemCompDeferred < GhostRoleComponent > ( ent ) ;
}
2022-07-14 22:20:37 -07:00
2024-05-07 03:48:16 +02:00
private void OnRoleStartup ( Entity < GhostRoleComponent > ent , ref ComponentStartup args )
2024-03-27 17:31:26 +13:00
{
2023-10-19 12:34:31 -07:00
RegisterGhostRole ( ent ) ;
2022-05-12 06:11:50 -05:00
}
2024-05-07 03:48:16 +02:00
private void OnRoleShutdown ( Entity < GhostRoleComponent > role , ref ComponentShutdown args )
2022-05-12 06:11:50 -05:00
{
UnregisterGhostRole ( role ) ;
}
2023-04-12 06:32:14 -07:00
private void OnSpawnerTakeRole ( EntityUid uid , GhostRoleMobSpawnerComponent component , ref TakeGhostRoleEvent args )
{
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) | |
2023-06-10 22:24:34 +10:00
! CanTakeGhost ( uid , ghostRole ) )
2023-04-12 06:32:14 -07:00
{
args . TookRole = false ;
return ;
}
if ( string . IsNullOrEmpty ( component . Prototype ) )
throw new NullReferenceException ( "Prototype string cannot be null or empty!" ) ;
var mob = Spawn ( component . Prototype , Transform ( uid ) . Coordinates ) ;
_transform . AttachToGridOrMap ( mob ) ;
var spawnedEvent = new GhostRoleSpawnerUsedEvent ( uid , mob ) ;
RaiseLocalEvent ( mob , spawnedEvent ) ;
if ( ghostRole . MakeSentient )
MakeSentientCommand . MakeSentient ( mob , EntityManager , ghostRole . AllowMovement , ghostRole . AllowSpeech ) ;
2023-10-11 02:19:46 -07:00
EnsureComp < MindContainerComponent > ( mob ) ;
2023-04-12 06:32:14 -07:00
GhostRoleInternalCreateMindAndTransfer ( args . Player , uid , mob , ghostRole ) ;
if ( + + component . CurrentTakeovers < component . AvailableTakeovers )
{
args . TookRole = true ;
return ;
}
ghostRole . Taken = true ;
if ( component . DeleteOnSpawn )
QueueDel ( uid ) ;
args . TookRole = true ;
}
2023-06-10 22:24:34 +10:00
private bool CanTakeGhost ( EntityUid uid , GhostRoleComponent ? component = null )
{
return Resolve ( uid , ref component , false ) & &
! component . Taken & &
! MetaData ( uid ) . EntityPaused ;
}
2023-04-12 06:32:14 -07:00
private void OnTakeoverTakeRole ( EntityUid uid , GhostTakeoverAvailableComponent component , ref TakeGhostRoleEvent args )
{
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) | |
2023-06-10 22:24:34 +10:00
! CanTakeGhost ( uid , ghostRole ) )
2023-04-12 06:32:14 -07:00
{
args . TookRole = false ;
return ;
}
ghostRole . Taken = true ;
2023-06-18 11:33:19 -07:00
var mind = EnsureComp < MindContainerComponent > ( uid ) ;
2023-04-12 06:32:14 -07:00
if ( mind . HasMind )
{
args . TookRole = false ;
return ;
}
if ( ghostRole . MakeSentient )
MakeSentientCommand . MakeSentient ( uid , EntityManager , ghostRole . AllowMovement , ghostRole . AllowSpeech ) ;
GhostRoleInternalCreateMindAndTransfer ( args . Player , uid , uid , ghostRole ) ;
2023-10-19 12:34:31 -07:00
UnregisterGhostRole ( ( uid , ghostRole ) ) ;
2023-04-12 06:32:14 -07:00
args . TookRole = true ;
}
2024-04-26 09:06:43 -04:00
private void OnVerb ( EntityUid uid , GhostRoleMobSpawnerComponent component , GetVerbsEvent < Verb > args )
{
var prototypes = component . SelectablePrototypes ;
if ( prototypes . Count < 1 )
return ;
if ( ! args . CanAccess | | ! args . CanInteract | | args . Hands = = null )
return ;
var verbs = new ValueList < Verb > ( ) ;
foreach ( var prototypeID in prototypes )
{
if ( _prototype . TryIndex < GhostRolePrototype > ( prototypeID , out var prototype ) )
{
var verb = CreateVerb ( uid , component , args . User , prototype ) ;
verbs . Add ( verb ) ;
}
}
args . Verbs . UnionWith ( verbs ) ;
}
private Verb CreateVerb ( EntityUid uid , GhostRoleMobSpawnerComponent component , EntityUid userUid , GhostRolePrototype prototype )
{
var verbText = Loc . GetString ( prototype . Name ) ;
return new Verb ( )
{
Text = verbText ,
Disabled = component . Prototype = = prototype . EntityPrototype ,
Category = VerbCategory . SelectType ,
Act = ( ) = > SetMode ( uid , prototype , verbText , component , userUid )
} ;
}
public void SetMode ( EntityUid uid , GhostRolePrototype prototype , string verbText , GhostRoleMobSpawnerComponent ? component , EntityUid ? userUid = null )
{
if ( ! Resolve ( uid , ref component ) )
return ;
var ghostrolecomp = EnsureComp < GhostRoleComponent > ( uid ) ;
component . Prototype = prototype . EntityPrototype ;
ghostrolecomp . RoleName = verbText ;
ghostrolecomp . RoleDescription = prototype . Description ;
ghostrolecomp . RoleRules = prototype . Rules ;
// Dirty(ghostrolecomp);
if ( userUid ! = null )
{
var msg = Loc . GetString ( "ghostrole-spawner-select" , ( "mode" , verbText ) ) ;
_popupSystem . PopupEntity ( msg , uid , userUid . Value ) ;
}
}
2021-02-12 04:35:56 +01:00
}
[AnyCommand]
2022-02-16 00:23:23 -07:00
public sealed class GhostRoles : IConsoleCommand
2021-02-12 04:35:56 +01:00
{
public string Command = > "ghostroles" ;
public string Description = > "Opens the ghost role request window." ;
public string Help = > $"{Command}" ;
public void Execute ( IConsoleShell shell , string argStr , string [ ] args )
{
2024-04-26 09:06:43 -04:00
if ( shell . Player ! = null )
2023-10-28 09:59:53 +11:00
EntitySystem . Get < GhostRoleSystem > ( ) . OpenEui ( shell . Player ) ;
2021-02-12 04:35:56 +01:00
else
shell . WriteLine ( "You can only open the ghost roles UI on a client." ) ;
}
}
}