Files
wwdpublic/Content.Server/Ghost/Roles/GhostRoleSystem.cs
RedFoxIV daf4f66414 "Proper" "Softcrit" "Support" (#1545)
# Description

Implements the softcrit functionality.
Similiar to critical state but spessmen will be able to communicate and
crawl around, but not pick up items.
Also supports configuring what is and isn't allowed in different
MobStates (per mob prototype): you can enable picking up items while in
softcrit so people can pick up their lasgun and continue shooting after
taking a 40x46mm to their ass cheeks from the guest nukies while being
dragged to safety.

![escape-from-tarkov-raid](https://github.com/user-attachments/assets/7f31702d-5677-4daf-a13d-8a9525fd3f9f)

<details> <summary><h1>Technical details</h1></summary>
New prototype type: "mobStateParams" (`MobStateParametersPrototype`)
Used to specify what can and can't be done when in a certain mobstate.
Of note that they are not actually bound to any `MobState` by
themselves. To assign a params prototype to a mobstate, use
`InitMobStateParams` in `MobStateComponent`.
It has to be a prototype because if I just did something akin to
`Dictionary<MobState, Dictionary<string, bool>>`, you'd have to check
the parent and copy every flag besides the one you wish to modify. That
is, if I understand how the prototype system works correctly, which I
frankly doubt. <!-- Working on softcrit made me hate prototypes. -->

MobStateComponent now has:
- `Dictionary<string, string> InitMobStateParams`, for storing "mobstate
- parameter prototype" pairs. `<string, string>` because it has to be
editable via mob prototypes. Named "mobStateParams" for mob prototypes.
- `public Dictionary<MobState, MobStateParametersPrototype>
MobStateParams` for actually storing the params for each state
- `public Dictionary<MobState, MobStateParametersOverride>
MobStateParamsOverrides` for storing overrides.
`MobStateParametersOverride` is a struct which mirrors all
`MobStateParametersPrototype`'s fields, except they're all nullable.
This is meant for code which wants to temporarily override some setting,
like a spell which allows dead people to talk. This is not the best
solution, but it should do at first. A better option would be tracking
each change separately, instead of hoping different systems overriding
the same flag will play nicely with eachother.
- a shitton of getter methods

TraitModifyMobState now has:
- `public Dictionary<string, string> Params` to specify a new prototype
to use.
- Important note: All values of `MobStateParametersPrototype` are
nullable, which is a hack to support `TraitModifyMobState`. This trait
takes one `MobStateParametersPrototype` per mobstate and applies all of
its non-null values. This way, a params prototype can be created which
will only have `pointing: true` and the trait can apply it (e.g. to
critstate, so we can spam pointing while dying like it's a game of turbo
dota)
- The above is why that wall of getters exists: They check the relevant
override struct, then the relevant prototype. If both are null, they
default to false (0f for floats.) The only exception is
OxyDamageOverlay, because it's used both for oxy damage overlay (if
null) and as a vision-limiting black void in crit..

MobStateSystem now has:
- a bunch of new "IsSomething"/"CanDoSomething" methods to check the
various flags, alongside rewritten old ones.
-
![image](https://github.com/user-attachments/assets/33a6b296-c12c-4311-9abe-90ca4288e871)
lookin ahh predicate factory

</details>
---

# TODO

done:
- [x] Make proper use of `MobStateSystem.IsIncapacitated()`.
done: some checks were changed, some left as they did what was (more or
less) intended.
<details>Previous `IsIncapacitated()` implementation simply checked if
person was in crit or dead. Now there is a `IsIncapacitated` flag in the
parameters, but it's heavily underutilized. I may need some help on this
one, since I don't know where would be a good place to check for it and
I absolutely will not just scour the entire build in search for them.
</details>

- [x] Separate force-dropping items from being downed
done: dropItemsOnEntering bool field. If true, will drop items upon
entering linked mobstate.
- [x] Don't drop items if `ForceDown` is true but `PickingUp` is also
true.
done: dropItemsOnEntering bool field. If true, will drop items upon
entering linked mobstate.
- [x] Actually check what are "conscious attempts" are used for
done: whether or not mob is conscious. Renamed the bool field
accordingly.
- [x] Look into adding a way to make people choke "slowly" in softcrit
as opposed to choking at "regular speed" in crit. Make that into a param
option? Make that into a float so the speed can be finetuned?
done: `BreathingMultiplier` float field added.
<details>
1f is regular breathing, 0.25 is "quarter-breathing". Air taken is
multiplied by `BreathingMultiplier` and suffocation damage taken (that
is dealt by RespiratorSystem, not all oxy damage) is multiplied by
`1-BreathingMultiplier`.
</details>

- [x] make sure the serializer actually does its job
done: it doesn't. Removed.
- [x] Make an option to prohibit using radio headsets while in softcrit
done: Requires Incapacitated parameter to be false to be able to use
headset radio.
- [x] Make sure it at least compiles

not done:
- [ ] probably move some other stuff to Params if it makes sense. Same
thing as with `IsIncapacitated` though: I kinda don't want to, at least
for now.

---

<details><summary><h1>No media</h1></summary>
<p>

:p

</p>
</details>

---

# Changelog

🆑
- add: Soft critical state. Crawl to safety, or to your doom - whatever
is closer.

---------

Signed-off-by: RedFoxIV <38788538+RedFoxIV@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>

(cherry picked from commit 9a357c1774f1a783844a07b5414f504ca574d84c)
2025-02-15 00:12:50 +03:00

808 lines
30 KiB
C#

using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.EUI;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Ghost.Roles.Raffles;
using Content.Server.Ghost.Roles.UI;
using Content.Server.Mind.Commands;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Follower;
using Content.Shared.GameTicking;
using Content.Shared.Ghost;
using Content.Shared.Ghost.Roles;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Players;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Server.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Collections;
namespace Content.Server.Ghost.Roles
{
[UsedImplicitly]
public sealed partial class GhostRoleSystem : EntitySystem // Converted to partial to allow for DeltaV character ghost roles
{
[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);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
SubscribeLocalEvent<GhostRoleCharacterSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeCharacter);
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
}
private void OnMobStateChanged(Entity<GhostTakeoverAvailableComponent> component, ref MobStateChangedEvent args)
{
if (!TryComp(component, out GhostRoleComponent? ghostRole))
return;
switch (args.NewMobState)
{
case MobState.Alive:
{
if (!ghostRole.Taken)
RegisterGhostRole((component, ghostRole));
break;
}
//case MobState.SoftCritical: // allow ghosts to enter a softcritted vessel and beg for mercy
case MobState.Critical:
case MobState.Dead:
UnregisterGhostRole((component, ghostRole));
break;
}
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= PlayerStatusChanged;
}
private uint GetNextRoleIdentifier()
{
return unchecked(_nextRoleIdentifier++);
}
public void OpenEui(ICommonSession session)
{
if (session.AttachedEntity is not { Valid: true } attached ||
!EntityManager.HasComponent<GhostComponent>(attached))
return;
if (_openUis.ContainsKey(session))
CloseEui(session);
var eui = _openUis[session] = new GhostRolesEui();
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
public void OpenMakeGhostRoleEui(ICommonSession session, EntityUid uid)
{
if (session.AttachedEntity == null)
return;
if (_openMakeGhostRoleUis.ContainsKey(session))
CloseEui(session);
var eui = _openMakeGhostRoleUis[session] = new MakeGhostRoleEui(EntityManager, GetNetEntity(uid));
_euiManager.OpenEui(eui, session);
eui.StateDirty();
}
public void CloseEui(ICommonSession session)
{
if (!_openUis.ContainsKey(session))
return;
_openUis.Remove(session, out var eui);
eui?.Close();
}
public void CloseMakeGhostRoleEui(ICommonSession session)
{
if (_openMakeGhostRoleUis.Remove(session, out var eui))
{
eui.Close();
}
}
public void UpdateAllEui()
{
foreach (var eui in _openUis.Values)
{
eui.StateDirty();
}
// 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);
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))
{
if (meta.EntityPaused)
continue;
// if all participants leave/were removed from the raffle, the raffle is canceled.
if (raffle.CurrentMembers.Count == 0)
{
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;
}
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;
}
return false;
}
private void RemoveRaffleAndUpdateEui(EntityUid entityUid, GhostRoleRaffleComponent raffle)
{
_ghostRoleRaffles.Remove(raffle.Identifier);
RemComp(entityUid, raffle);
UpdateAllEui();
}
private void PlayerStatusChanged(object? blah, SessionStatusEventArgs args)
{
if (args.NewStatus == SessionStatus.InGame)
{
var response = new GhostUpdateGhostRoleCountEvent(_ghostRoles.Count);
RaiseNetworkEvent(response, args.Session.Channel);
}
else
{
// people who disconnect are removed from ghost role raffles
LeaveAllRaffles(args.Session);
}
}
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))
{
// 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;
}
UpdateAllEui();
}
/// <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;
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;
var ev = new TakeGhostRoleEvent(player);
RaiseLocalEvent(role, ref ev);
if (!ev.TookRole)
return false;
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}");
CloseEui(player);
return true;
}
public void Follow(ICommonSession player, uint identifier)
{
if (!_ghostRoles.TryGetValue(identifier, out var role))
return;
if (player.AttachedEntity == null)
return;
_followerSystem.StartFollowingEntity(player.AttachedEntity.Value, role);
}
public void GhostRoleInternalCreateMindAndTransfer(ICommonSession player, EntityUid roleUid, EntityUid mob, GhostRoleComponent? role = null)
{
if (!Resolve(roleUid, ref role))
return;
DebugTools.AssertNotNull(player.ContentData());
var newMind = _mindSystem.CreateMind(player.UserId,
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
_roleSystem.MindAddRole(newMind, new GhostRoleMarkerRoleComponent { Name = role.RoleName });
_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>();
foreach (var (id, (uid, role)) in _ghostRoles)
{
if (metaQuery.GetComponent(uid).EntityPaused)
continue;
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
});
}
return roles.ToArray();
}
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);
}
private void OnMindAdded(EntityUid uid, GhostTakeoverAvailableComponent component, MindAddedMessage args)
{
if (!TryComp(uid, out GhostRoleComponent? ghostRole))
return;
ghostRole.Taken = true;
UnregisterGhostRole((uid, ghostRole));
}
private void OnMindRemoved(EntityUid uid, GhostTakeoverAvailableComponent component, MindRemovedMessage args)
{
if (!TryComp(uid, out GhostRoleComponent? ghostRole))
return;
// Avoid re-registering it for duplicate entries and potential exceptions.
if (!ghostRole.ReregisterOnGhost || component.LifeStage > ComponentLifeStage.Running)
return;
ghostRole.Taken = false;
RegisterGhostRole((uid, ghostRole));
}
public void Reset(RoundRestartCleanupEvent ev)
{
foreach (var session in _openUis.Keys)
{
CloseEui(session);
}
_openUis.Clear();
_ghostRoles.Clear();
_ghostRoleRaffles.Clear();
_nextRoleIdentifier = 0;
}
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();
}
private void OnMapInit(Entity<GhostRoleComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.Probability < 1f && !_random.Prob(ent.Comp.Probability))
RemCompDeferred<GhostRoleComponent>(ent);
}
private void OnRoleStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
{
RegisterGhostRole(ent);
}
private void OnRoleShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
{
UnregisterGhostRole(role);
}
private void OnSpawnerTakeRole(EntityUid uid, GhostRoleMobSpawnerComponent component, ref TakeGhostRoleEvent args)
{
if (!TryComp(uid, out GhostRoleComponent? ghostRole) ||
!CanTakeGhost(uid, ghostRole))
{
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);
EnsureComp<MindContainerComponent>(mob);
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;
}
private bool CanTakeGhost(EntityUid uid, GhostRoleComponent? component = null)
{
return Resolve(uid, ref component, false) &&
!component.Taken &&
!MetaData(uid).EntityPaused;
}
private void OnTakeoverTakeRole(EntityUid uid, GhostTakeoverAvailableComponent component, ref TakeGhostRoleEvent args)
{
if (!TryComp(uid, out GhostRoleComponent? ghostRole) ||
!CanTakeGhost(uid, ghostRole))
{
args.TookRole = false;
return;
}
ghostRole.Taken = true;
var mind = EnsureComp<MindContainerComponent>(uid);
if (mind.HasMind)
{
args.TookRole = false;
return;
}
if (ghostRole.MakeSentient)
MakeSentientCommand.MakeSentient(uid, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole);
UnregisterGhostRole((uid, ghostRole));
args.TookRole = true;
}
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);
}
}
}
[AnyCommand]
public sealed class GhostRoles : IConsoleCommand
{
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)
EntitySystem.Get<GhostRoleSystem>().OpenEui(shell.Player);
else
shell.WriteLine("You can only open the ghost roles UI on a client.");
}
}
}