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.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 ;
2024-08-14 03:57:09 +02:00
using Content.Shared.CCVar ;
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 ;
2024-08-14 03:57:09 +02:00
using Robust.Shared.Configuration ;
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.Collections ;
2024-07-21 03:32:25 -07:00
using Content.Shared.Ghost.Roles.Components ;
2024-09-02 08:32:49 +03:00
using Content.Shared.Roles.Jobs ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
namespace Content.Server.Ghost.Roles ;
[UsedImplicitly]
public sealed class GhostRoleSystem : EntitySystem
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
[Dependency] private readonly IConfigurationManager _cfg = default ! ;
[Dependency] private readonly EuiManager _euiManager = default ! ;
[Dependency] private readonly IPlayerManager _playerManager = default ! ;
[Dependency] private readonly IAdminLogManager _adminLogger = default ! ;
[Dependency] private readonly IRobustRandom _random = default ! ;
[Dependency] private readonly FollowerSystem _followerSystem = default ! ;
[Dependency] private readonly TransformSystem _transform = default ! ;
[Dependency] private readonly SharedMindSystem _mindSystem = default ! ;
[Dependency] private readonly SharedRoleSystem _roleSystem = default ! ;
[Dependency] private readonly IGameTiming _timing = default ! ;
[Dependency] private readonly PopupSystem _popupSystem = default ! ;
[Dependency] private readonly IPrototypeManager _prototype = default ! ;
private uint _nextRoleIdentifier ;
private bool _needsUpdateGhostRoleCount = true ;
private readonly Dictionary < uint , Entity < GhostRoleComponent > > _ghostRoles = new ( ) ;
private readonly Dictionary < uint , Entity < GhostRoleRaffleComponent > > _ghostRoleRaffles = new ( ) ;
private readonly Dictionary < ICommonSession , GhostRolesEui > _openUis = new ( ) ;
private readonly Dictionary < ICommonSession , MakeGhostRoleEui > _openMakeGhostRoleUis = new ( ) ;
[ViewVariables]
public IReadOnlyCollection < Entity < GhostRoleComponent > > GhostRoles = > _ghostRoles . Values ;
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < RoundRestartCleanupEvent > ( Reset ) ;
SubscribeLocalEvent < PlayerAttachedEvent > ( OnPlayerAttached ) ;
2024-10-10 10:48:56 +02:00
2024-08-15 20:26:57 +02:00
SubscribeLocalEvent < GhostTakeoverAvailableComponent , MindAddedMessage > ( OnMindAdded ) ;
SubscribeLocalEvent < GhostTakeoverAvailableComponent , MindRemovedMessage > ( OnMindRemoved ) ;
SubscribeLocalEvent < GhostTakeoverAvailableComponent , MobStateChangedEvent > ( OnMobStateChanged ) ;
2024-10-10 10:48:56 +02:00
SubscribeLocalEvent < GhostTakeoverAvailableComponent , TakeGhostRoleEvent > ( OnTakeoverTakeRole ) ;
2024-08-15 20:26:57 +02:00
SubscribeLocalEvent < GhostRoleComponent , MapInitEvent > ( OnMapInit ) ;
SubscribeLocalEvent < GhostRoleComponent , ComponentStartup > ( OnRoleStartup ) ;
SubscribeLocalEvent < GhostRoleComponent , ComponentShutdown > ( OnRoleShutdown ) ;
SubscribeLocalEvent < GhostRoleComponent , EntityPausedEvent > ( OnPaused ) ;
SubscribeLocalEvent < GhostRoleComponent , EntityUnpausedEvent > ( OnUnpaused ) ;
2024-10-10 10:48:56 +02:00
2024-08-15 20:26:57 +02:00
SubscribeLocalEvent < GhostRoleRaffleComponent , ComponentInit > ( OnRaffleInit ) ;
SubscribeLocalEvent < GhostRoleRaffleComponent , ComponentShutdown > ( OnRaffleShutdown ) ;
2024-10-10 10:48:56 +02:00
2024-08-15 20:26:57 +02:00
SubscribeLocalEvent < GhostRoleMobSpawnerComponent , TakeGhostRoleEvent > ( OnSpawnerTakeRole ) ;
SubscribeLocalEvent < GhostRoleMobSpawnerComponent , GetVerbsEvent < Verb > > ( OnVerb ) ;
SubscribeLocalEvent < GhostRoleMobSpawnerComponent , GhostRoleRadioMessage > ( OnGhostRoleRadioMessage ) ;
_playerManager . PlayerStatusChanged + = PlayerStatusChanged ;
}
2021-11-03 23:48:12 +00:00
2024-08-15 20:26:57 +02:00
private void OnMobStateChanged ( Entity < GhostTakeoverAvailableComponent > component , ref MobStateChangedEvent args )
{
if ( ! TryComp ( component , out GhostRoleComponent ? ghostRole ) )
return ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
switch ( args . NewMobState )
{
case MobState . Alive :
{
if ( ! ghostRole . Taken )
RegisterGhostRole ( ( component , ghostRole ) ) ;
2022-01-01 22:00:21 -08:00
break ;
2024-08-15 20:26:57 +02:00
}
case MobState . Critical :
case MobState . Dead :
UnregisterGhostRole ( ( component , ghostRole ) ) ;
break ;
2022-01-01 22:00:21 -08:00
}
2024-08-15 20:26:57 +02:00
}
2022-01-01 22:00:21 -08:00
2024-08-15 20:26:57 +02:00
public override void Shutdown ( )
{
base . Shutdown ( ) ;
2021-11-03 23:48:12 +00:00
2024-08-15 20:26:57 +02:00
_playerManager . PlayerStatusChanged - = PlayerStatusChanged ;
}
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
private uint GetNextRoleIdentifier ( )
{
return unchecked ( _nextRoleIdentifier + + ) ;
}
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
public void OpenEui ( ICommonSession session )
{
if ( session . AttachedEntity is not { Valid : true } attached | |
! EntityManager . HasComponent < GhostComponent > ( attached ) )
return ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
if ( _openUis . ContainsKey ( session ) )
CloseEui ( session ) ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
var eui = _openUis [ session ] = new GhostRolesEui ( ) ;
_euiManager . OpenEui ( eui , session ) ;
eui . StateDirty ( ) ;
}
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
public void OpenMakeGhostRoleEui ( ICommonSession session , EntityUid uid )
{
if ( session . AttachedEntity = = null )
return ;
2021-02-16 09:51:27 +01:00
2024-08-15 20:26:57 +02:00
if ( _openMakeGhostRoleUis . ContainsKey ( session ) )
CloseEui ( session ) ;
2021-02-16 09:51:27 +01:00
2024-08-15 20:26:57 +02:00
var eui = _openMakeGhostRoleUis [ session ] = new MakeGhostRoleEui ( EntityManager , GetNetEntity ( uid ) ) ;
_euiManager . OpenEui ( eui , session ) ;
eui . StateDirty ( ) ;
}
2021-02-16 09:51:27 +01:00
2024-08-15 20:26:57 +02:00
public void CloseEui ( ICommonSession session )
{
if ( ! _openUis . ContainsKey ( session ) )
return ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
_openUis . Remove ( session , out var eui ) ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
eui ? . Close ( ) ;
}
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
public void CloseMakeGhostRoleEui ( ICommonSession session )
{
if ( _openMakeGhostRoleUis . Remove ( session , out var eui ) )
2021-02-16 09:51:27 +01:00
{
2024-08-15 20:26:57 +02:00
eui . Close ( ) ;
2021-02-16 09:51:27 +01:00
}
2024-08-15 20:26:57 +02:00
}
2021-02-16 09:51:27 +01:00
2024-08-15 20:26:57 +02:00
public void UpdateAllEui ( )
{
foreach ( var eui in _openUis . Values )
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
eui . StateDirty ( ) ;
2021-11-03 23:48:12 +00:00
}
2024-08-15 20:26:57 +02: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 ;
}
2021-11-03 23:48:12 +00:00
2024-08-15 20:26:57 +02:00
public override void Update ( float frameTime )
{
base . Update ( frameTime ) ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
UpdateGhostRoleCount ( ) ;
UpdateRaffles ( frameTime ) ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <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 ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
_needsUpdateGhostRoleCount = false ;
var response = new GhostUpdateGhostRoleCountEvent ( GetGhostRoleCount ( ) ) ;
foreach ( var player in _playerManager . Sessions )
{
RaiseNetworkEvent ( response , player . Channel ) ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <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 ) )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +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-03 23:48:12 +00:00
{
2024-08-15 20:26:57 +02:00
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
continue ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
raffle . Countdown = raffle . Countdown . Subtract ( TimeSpan . FromSeconds ( frameTime ) ) ;
if ( raffle . Countdown . Ticks > 0 )
continue ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
// 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 ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( ghostRole . RaffleConfig is null )
{
Log . Warning ( $"Ghost role raffle finished on {entityUid} but RaffleConfig became null" ) ;
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
continue ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
var foundWinner = false ;
var deciderPrototype = _prototype . Index ( ghostRole . RaffleConfig . Decider ) ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
// use the ghost role's chosen winner picker to find a winner
deciderPrototype . Decider . PickWinner (
raffle . CurrentMembers . AsEnumerable ( ) ,
session = >
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
var success = TryTakeover ( session , raffle . Identifier ) ;
foundWinner | = success ;
return success ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
) ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( ! foundWinner )
{
Log . Warning ( $"Ghost role raffle for {entityUid} ({ghostRole.RoleName}) finished without " +
$"{ghostRole.RaffleConfig?.Decider} finding a winner" ) ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
// raffle over
RemoveRaffleAndUpdateEui ( entityUid , raffle ) ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
}
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 ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( Takeover ( player , identifier ) )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
// takeover successful, we have a winner! remove the winner from other raffles they might be in
LeaveAllRaffles ( player ) ;
return true ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
return false ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
private void RemoveRaffleAndUpdateEui ( EntityUid entityUid , GhostRoleRaffleComponent raffle )
{
_ghostRoleRaffles . Remove ( raffle . Identifier ) ;
RemComp ( entityUid , raffle ) ;
UpdateAllEui ( ) ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
private void PlayerStatusChanged ( object? blah , SessionStatusEventArgs args )
{
if ( args . NewStatus = = SessionStatus . InGame )
{
var response = new GhostUpdateGhostRoleCountEvent ( _ghostRoles . Count ) ;
RaiseNetworkEvent ( response , args . Session . Channel ) ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
else
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
// people who disconnect are removed from ghost role raffles
LeaveAllRaffles ( args . Session ) ;
2021-11-03 23:48:12 +00:00
}
2024-08-15 20:26:57 +02:00
}
2021-11-03 23:48:12 +00:00
2024-08-15 20:26:57 +02:00
public void RegisterGhostRole ( Entity < GhostRoleComponent > role )
{
if ( _ghostRoles . ContainsValue ( role ) )
return ;
_ghostRoles [ role . Comp . Identifier = GetNextRoleIdentifier ( ) ] = role ;
UpdateAllEui ( ) ;
}
public void UnregisterGhostRole ( Entity < GhostRoleComponent > role )
{
var comp = role . Comp ;
if ( ! _ghostRoles . ContainsKey ( comp . Identifier ) | | _ghostRoles [ comp . Identifier ] ! = role )
return ;
_ghostRoles . Remove ( comp . Identifier ) ;
if ( TryComp ( role . Owner , out GhostRoleRaffleComponent ? raffle ) )
2021-11-03 23:48:12 +00:00
{
2024-08-15 20:26:57 +02:00
// if a raffle is still running, get rid of it
RemoveRaffleAndUpdateEui ( role . Owner , raffle ) ;
2021-02-12 04:35:56 +01:00
}
2024-08-15 20:26:57 +02:00
else
2021-02-12 04:35:56 +01:00
{
2023-10-19 12:34:31 -07:00
UpdateAllEui ( ) ;
2021-02-12 04:35:56 +01:00
}
2024-08-15 20:26:57 +02:00
}
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
// 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 ) )
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
// can't have a raffle for a ghost role that doesn't exist
RemComp < GhostRoleRaffleComponent > ( ent ) ;
return ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
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 )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
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 ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
var raffle = ent . Comp ;
raffle . Identifier = ghostRole . Identifier ;
var countdown = _cfg . GetCVar ( CCVars . GhostQuickLottery ) ? 1 : settings . InitialDuration ;
raffle . Countdown = TimeSpan . FromSeconds ( countdown ) ;
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 ) ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <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 ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
// 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 ) ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
_ghostRoleRaffles . TryAdd ( identifier , ( roleEnt . Owner , raffle ) ) ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( ! raffle . CurrentMembers . Add ( player ) )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
Log . Warning ( $"{player.Name} tried to join raffle for ghost role {identifier} but they are already in the raffle" ) ;
return ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
// 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 )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
raffle . Countdown + = raffle . JoinExtendsDurationBy ;
raffle . CumulativeTime + = raffle . JoinExtendsDurationBy ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
UpdateAllEui ( ) ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <summary>
/// Makes the given player leave the raffle corresponding to the given ID.
/// </summary>
public void LeaveRaffle ( ICommonSession player , uint identifier )
{
if ( ! _ghostRoleRaffles . TryGetValue ( identifier , out var raffleEnt ) )
return ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( raffleEnt . Comp . CurrentMembers . Remove ( player ) )
{
2021-02-12 04:35:56 +01:00
UpdateAllEui ( ) ;
}
2024-08-15 20:26:57 +02:00
else
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
Log . Warning ( $"{player.Name} tried to leave raffle for ghost role {identifier} but they are not in the raffle" ) ;
}
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
// (raffle ending because all players left is handled in update())
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <summary>
/// Makes the given player leave all ghost role raffles.
/// </summary>
public void LeaveAllRaffles ( ICommonSession player )
{
var shouldUpdateEui = false ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
foreach ( var raffleEnt in _ghostRoleRaffles . Values )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
shouldUpdateEui | = raffleEnt . Comp . CurrentMembers . Remove ( player ) ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( shouldUpdateEui )
UpdateAllEui ( ) ;
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <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 ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( roleEnt . Comp . RaffleConfig is not null )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
JoinRaffle ( player , identifier ) ;
2024-05-07 03:48:16 +02:00
}
2024-08-15 20:26:57 +02:00
else
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
Takeover ( player , identifier ) ;
}
}
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
/// <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
2024-08-15 20:26:57 +02:00
var ev = new TakeGhostRoleEvent ( player ) ;
RaiseLocalEvent ( role , ref ev ) ;
2022-01-09 17:54:36 -08:00
2024-08-15 20:26:57 +02:00
if ( ! ev . TookRole )
return false ;
2022-01-09 17:54:36 -08:00
2024-08-15 20:26:57 +02:00
if ( player . AttachedEntity ! = null )
_adminLogger . Add ( LogType . GhostRoleTaken , LogImpact . Low , $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity}" ) ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
CloseEui ( player ) ;
return true ;
}
2023-10-19 12:34:31 -07:00
2024-08-15 20:26:57 +02:00
public void Follow ( ICommonSession player , uint identifier )
{
if ( ! _ghostRoles . TryGetValue ( identifier , out var role ) )
return ;
2022-04-03 01:06:29 +03:00
2024-08-15 20:26:57 +02:00
if ( player . AttachedEntity = = null )
return ;
2022-04-03 01:06:29 +03:00
2024-08-15 20:26:57 +02:00
_followerSystem . StartFollowingEntity ( player . AttachedEntity . Value , role ) ;
}
2021-11-15 18:14:34 +00:00
2024-08-15 20:26:57 +02:00
public void GhostRoleInternalCreateMindAndTransfer ( ICommonSession player , EntityUid roleUid , EntityUid mob , GhostRoleComponent ? role = null )
{
if ( ! Resolve ( roleUid , ref role ) )
return ;
2021-11-15 18:14:34 +00:00
2024-08-15 20:26:57 +02:00
DebugTools . AssertNotNull ( player . ContentData ( ) ) ;
2021-11-15 18:14:34 +00:00
2024-08-15 20:26:57 +02:00
var newMind = _mindSystem . CreateMind ( player . UserId ,
EntityManager . GetComponent < MetaDataComponent > ( mob ) . EntityName ) ;
2024-10-10 10:48:56 +02:00
_roleSystem . MindAddRole ( newMind , "MindRoleGhostMarker" ) ;
2024-10-14 16:05:25 +13:00
if ( _roleSystem . MindHasRole < GhostRoleMarkerRoleComponent > ( newMind ! , out var markerRole ) )
markerRole . Value . Comp2 . Name = role . RoleName ;
2021-11-15 18:14:34 +00:00
2024-08-15 20:26:57 +02:00
_mindSystem . SetUserId ( newMind , player . UserId ) ;
_mindSystem . TransferTo ( newMind , mob ) ;
}
/// <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 )
{
var roles = new List < GhostRoleInfo > ( ) ;
var metaQuery = GetEntityQuery < MetaDataComponent > ( ) ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
foreach ( var ( id , ( uid , role ) ) in _ghostRoles )
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
if ( metaQuery . GetComponent ( uid ) . EntityPaused )
continue ;
2021-02-12 04:35:56 +01:00
2023-06-10 22:24:34 +10:00
2024-08-15 20:26:57 +02:00
var kind = GhostRoleKind . FirstComeFirstServe ;
GhostRoleRaffleComponent ? raffle = null ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( role . RaffleConfig is not null )
{
kind = GhostRoleKind . RaffleReady ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( _ghostRoleRaffles . TryGetValue ( id , out var raffleEnt ) )
2024-05-07 03:48:16 +02:00
{
2024-08-15 20:26:57 +02:00
kind = GhostRoleKind . RaffleInProgress ;
raffle = raffleEnt . Comp ;
2024-05-07 03:48:16 +02:00
2024-08-15 20:26:57 +02:00
if ( player is not null & & raffle . CurrentMembers . Contains ( player ) )
kind = GhostRoleKind . RaffleJoined ;
2024-05-07 03:48:16 +02:00
}
2021-02-12 04:35:56 +01:00
}
2024-08-15 20:26:57 +02:00
var rafflePlayerCount = ( uint? ) raffle ? . CurrentMembers . Count ? ? 0 ;
var raffleEndTime = raffle is not null
? _timing . CurTime . Add ( raffle . Countdown )
: TimeSpan . MinValue ;
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
roles . Add ( new GhostRoleInfo
{
Identifier = id ,
Name = role . RoleName ,
Description = role . RoleDescription ,
Rules = role . RoleRules ,
Requirements = role . Requirements ,
Kind = kind ,
RafflePlayerCount = rafflePlayerCount ,
RaffleEndTime = raffleEndTime
} ) ;
}
return roles . ToArray ( ) ;
}
2023-10-19 12:34:31 -07:00
2024-08-15 20:26:57 +02:00
private void OnPlayerAttached ( PlayerAttachedEvent message )
{
// Close the session of any player that has a ghost roles window open and isn't a ghost anymore.
if ( ! _openUis . ContainsKey ( message . Player ) )
return ;
if ( HasComp < GhostComponent > ( message . Entity ) )
return ;
// 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 ) ;
CloseEui ( message . Player ) ;
}
2023-10-19 12:34:31 -07:00
2024-08-15 20:26:57 +02:00
private void OnMindAdded ( EntityUid uid , GhostTakeoverAvailableComponent component , MindAddedMessage args )
{
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) )
return ;
2021-02-12 04:35:56 +01:00
2024-09-02 08:32:49 +03:00
if ( ghostRole . JobProto ! = null )
{
2024-10-10 10:48:56 +02:00
_roleSystem . MindAddJobRole ( args . Mind , args . Mind , silent : false , ghostRole . JobProto ) ;
2024-09-02 08:32:49 +03:00
}
2024-08-15 20:26:57 +02:00
ghostRole . Taken = true ;
UnregisterGhostRole ( ( uid , ghostRole ) ) ;
}
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
private void OnMindRemoved ( EntityUid uid , GhostTakeoverAvailableComponent component , MindRemovedMessage args )
{
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) )
return ;
2022-01-12 18:25:05 +11:00
2024-08-15 20:26:57 +02:00
// Avoid re-registering it for duplicate entries and potential exceptions.
if ( ! ghostRole . ReregisterOnGhost | | component . LifeStage > ComponentLifeStage . Running )
return ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
ghostRole . Taken = false ;
RegisterGhostRole ( ( uid , ghostRole ) ) ;
}
2022-01-11 14:12:19 +11:00
2024-08-15 20:26:57 +02:00
public void Reset ( RoundRestartCleanupEvent ev )
{
foreach ( var session in _openUis . Keys )
{
CloseEui ( session ) ;
2022-01-01 08:48:45 -08:00
}
2024-08-15 20:26:57 +02:00
_openUis . Clear ( ) ;
_ghostRoles . Clear ( ) ;
_ghostRoleRaffles . Clear ( ) ;
_nextRoleIdentifier = 0 ;
}
2021-02-12 04:35:56 +01:00
2024-08-15 20:26:57 +02:00
private void OnPaused ( EntityUid uid , GhostRoleComponent component , ref EntityPausedEvent args )
{
if ( HasComp < ActorComponent > ( uid ) )
return ;
2022-05-12 06:11:50 -05:00
2024-08-15 20:26:57 +02:00
UpdateAllEui ( ) ;
}
2023-06-10 22:24:34 +10:00
2024-08-15 20:26:57 +02:00
private void OnUnpaused ( EntityUid uid , GhostRoleComponent component , ref EntityUnpausedEvent args )
{
if ( HasComp < ActorComponent > ( uid ) )
return ;
2023-06-10 22:24:34 +10:00
2024-08-15 20:26:57 +02:00
UpdateAllEui ( ) ;
}
2023-06-10 22:24:34 +10:00
2024-08-15 20:26:57 +02:00
private void OnMapInit ( Entity < GhostRoleComponent > ent , ref MapInitEvent args )
{
if ( ent . Comp . Probability < 1f & & ! _random . Prob ( ent . Comp . Probability ) )
RemCompDeferred < GhostRoleComponent > ( ent ) ;
}
2023-06-10 22:24:34 +10:00
2024-08-15 20:26:57 +02:00
private void OnRoleStartup ( Entity < GhostRoleComponent > ent , ref ComponentStartup args )
{
RegisterGhostRole ( ent ) ;
}
2022-07-14 22:20:37 -07:00
2024-08-15 20:26:57 +02:00
private void OnRoleShutdown ( Entity < GhostRoleComponent > role , ref ComponentShutdown args )
{
UnregisterGhostRole ( role ) ;
}
2022-05-12 06:11:50 -05:00
2024-08-15 20:26:57 +02:00
private void OnSpawnerTakeRole ( EntityUid uid , GhostRoleMobSpawnerComponent component , ref TakeGhostRoleEvent args )
{
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) | |
! CanTakeGhost ( uid , ghostRole ) )
2022-05-12 06:11:50 -05:00
{
2024-08-15 20:26:57 +02:00
args . TookRole = false ;
return ;
2022-05-12 06:11:50 -05:00
}
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
if ( string . IsNullOrEmpty ( component . Prototype ) )
throw new NullReferenceException ( "Prototype string cannot be null or empty!" ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
var mob = Spawn ( component . Prototype , Transform ( uid ) . Coordinates ) ;
_transform . AttachToGridOrMap ( mob ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
var spawnedEvent = new GhostRoleSpawnerUsedEvent ( uid , mob ) ;
RaiseLocalEvent ( mob , spawnedEvent ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
if ( ghostRole . MakeSentient )
MakeSentientCommand . MakeSentient ( mob , EntityManager , ghostRole . AllowMovement , ghostRole . AllowSpeech ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
EnsureComp < MindContainerComponent > ( mob ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
GhostRoleInternalCreateMindAndTransfer ( args . Player , uid , mob , ghostRole ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
if ( + + component . CurrentTakeovers < component . AvailableTakeovers )
{
args . TookRole = true ;
return ;
}
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
ghostRole . Taken = true ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
if ( component . DeleteOnSpawn )
QueueDel ( uid ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
args . TookRole = true ;
}
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02: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
2024-08-15 20:26:57 +02:00
private void OnTakeoverTakeRole ( EntityUid uid , GhostTakeoverAvailableComponent component , ref TakeGhostRoleEvent args )
{
if ( ! TryComp ( uid , out GhostRoleComponent ? ghostRole ) | |
! CanTakeGhost ( uid , ghostRole ) )
2023-06-10 22:24:34 +10:00
{
2024-08-15 20:26:57 +02:00
args . TookRole = false ;
return ;
2023-06-10 22:24:34 +10:00
}
2024-08-15 20:26:57 +02:00
ghostRole . Taken = true ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
var mind = EnsureComp < MindContainerComponent > ( uid ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
if ( mind . HasMind )
{
args . TookRole = false ;
return ;
}
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
if ( ghostRole . MakeSentient )
MakeSentientCommand . MakeSentient ( uid , EntityManager , ghostRole . AllowMovement , ghostRole . AllowSpeech ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
GhostRoleInternalCreateMindAndTransfer ( args . Player , uid , uid , ghostRole ) ;
UnregisterGhostRole ( ( uid , ghostRole ) ) ;
2023-04-12 06:32:14 -07:00
2024-08-15 20:26:57 +02:00
args . TookRole = true ;
}
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
private void OnVerb ( EntityUid uid , GhostRoleMobSpawnerComponent component , GetVerbsEvent < Verb > args )
{
var prototypes = component . SelectablePrototypes ;
if ( prototypes . Count < 1 )
return ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
if ( ! args . CanAccess | | ! args . CanInteract | | args . Hands = = null )
return ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
var verbs = new ValueList < Verb > ( ) ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
foreach ( var prototypeID in prototypes )
{
if ( _prototype . TryIndex < GhostRolePrototype > ( prototypeID , out var prototype ) )
2024-04-26 09:06:43 -04:00
{
2024-08-15 20:26:57 +02:00
var verb = CreateVerb ( uid , component , args . User , prototype ) ;
verbs . Add ( verb ) ;
2024-04-26 09:06:43 -04:00
}
}
2024-08-15 20:26:57 +02:00
args . Verbs . UnionWith ( verbs ) ;
}
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
private Verb CreateVerb ( EntityUid uid , GhostRoleMobSpawnerComponent component , EntityUid userUid , GhostRolePrototype prototype )
{
var verbText = Loc . GetString ( prototype . Name ) ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
return new Verb ( )
2024-04-26 09:06:43 -04:00
{
2024-08-15 20:26:57 +02:00
Text = verbText ,
Disabled = component . Prototype = = prototype . EntityPrototype ,
Category = VerbCategory . SelectType ,
Act = ( ) = > SetMode ( uid , prototype , verbText , component , userUid )
} ;
}
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
public void SetMode ( EntityUid uid , GhostRolePrototype prototype , string verbText , GhostRoleMobSpawnerComponent ? component , EntityUid ? userUid = null )
{
if ( ! Resolve ( uid , ref component ) )
return ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
var ghostrolecomp = EnsureComp < GhostRoleComponent > ( uid ) ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
component . Prototype = prototype . EntityPrototype ;
ghostrolecomp . RoleName = verbText ;
ghostrolecomp . RoleDescription = prototype . Description ;
ghostrolecomp . RoleRules = prototype . Rules ;
2024-04-26 09:06:43 -04:00
2024-08-15 20:26:57 +02:00
// Dirty(ghostrolecomp);
2024-07-21 03:32:25 -07:00
2024-08-15 20:26:57 +02:00
if ( userUid ! = null )
2024-07-21 03:32:25 -07:00
{
2024-08-15 20:26:57 +02:00
var msg = Loc . GetString ( "ghostrole-spawner-select" , ( "mode" , verbText ) ) ;
_popupSystem . PopupEntity ( msg , uid , userUid . Value ) ;
2024-07-21 03:32:25 -07:00
}
2021-02-12 04:35:56 +01:00
}
2024-08-15 20:26:57 +02:00
public void OnGhostRoleRadioMessage ( Entity < GhostRoleMobSpawnerComponent > entity , ref GhostRoleRadioMessage args )
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
if ( ! _prototype . TryIndex ( args . ProtoId , out var ghostRoleProto ) )
return ;
2024-05-12 17:34:52 -07:00
2024-08-15 20:26:57 +02:00
// if the prototype chosen isn't actually part of the selectable options, ignore it
foreach ( var selectableProto in entity . Comp . SelectablePrototypes )
2021-02-12 04:35:56 +01:00
{
2024-08-15 20:26:57 +02:00
if ( selectableProto = = ghostRoleProto . EntityPrototype . Id )
return ;
2021-02-12 04:35:56 +01:00
}
2024-08-15 20:26:57 +02:00
SetMode ( entity . Owner , ghostRoleProto , ghostRoleProto . Name , entity . Comp ) ;
}
}
[AnyCommand]
public sealed class GhostRoles : IConsoleCommand
{
[Dependency] private readonly IEntityManager _e = default ! ;
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 )
{
if ( shell . Player ! = null )
_e . System < GhostRoleSystem > ( ) . OpenEui ( shell . Player ) ;
else
shell . WriteLine ( "You can only open the ghost roles UI on a client." ) ;
2021-02-12 04:35:56 +01:00
}
}