GhostRoleSystem.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. using System.Linq;
  2. using Content.Server.Administration.Logs;
  3. using Content.Server.EUI;
  4. using Content.Server.Ghost.Roles.Components;
  5. using Content.Server.Ghost.Roles.Events;
  6. using Content.Shared.Ghost.Roles.Raffles;
  7. using Content.Server.Ghost.Roles.UI;
  8. using Content.Server.Mind.Commands;
  9. using Content.Shared.Administration;
  10. using Content.Shared.CCVar;
  11. using Content.Shared.Database;
  12. using Content.Shared.Follower;
  13. using Content.Shared.GameTicking;
  14. using Content.Shared.Ghost;
  15. using Content.Shared.Ghost.Roles;
  16. using Content.Shared.Mind;
  17. using Content.Shared.Mind.Components;
  18. using Content.Shared.Mobs;
  19. using Content.Shared.Players;
  20. using Content.Shared.Roles;
  21. using JetBrains.Annotations;
  22. using Robust.Server.GameObjects;
  23. using Robust.Server.Player;
  24. using Robust.Shared.Configuration;
  25. using Robust.Shared.Console;
  26. using Robust.Shared.Enums;
  27. using Robust.Shared.Player;
  28. using Robust.Shared.Prototypes;
  29. using Robust.Shared.Random;
  30. using Robust.Shared.Timing;
  31. using Robust.Shared.Utility;
  32. using Content.Server.Popups;
  33. using Content.Shared.Verbs;
  34. using Robust.Shared.Collections;
  35. using Content.Shared.Ghost.Roles.Components;
  36. namespace Content.Server.Ghost.Roles;
  37. [UsedImplicitly]
  38. public sealed class GhostRoleSystem : EntitySystem
  39. {
  40. [Dependency] private readonly IConfigurationManager _cfg = default!;
  41. [Dependency] private readonly EuiManager _euiManager = default!;
  42. [Dependency] private readonly IPlayerManager _playerManager = default!;
  43. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  44. [Dependency] private readonly IRobustRandom _random = default!;
  45. [Dependency] private readonly FollowerSystem _followerSystem = default!;
  46. [Dependency] private readonly TransformSystem _transform = default!;
  47. [Dependency] private readonly SharedMindSystem _mindSystem = default!;
  48. [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
  49. [Dependency] private readonly IGameTiming _timing = default!;
  50. [Dependency] private readonly PopupSystem _popupSystem = default!;
  51. [Dependency] private readonly IPrototypeManager _prototype = default!;
  52. private uint _nextRoleIdentifier;
  53. private bool _needsUpdateGhostRoleCount = true;
  54. private readonly Dictionary<uint, Entity<GhostRoleComponent>> _ghostRoles = new();
  55. private readonly Dictionary<uint, Entity<GhostRoleRaffleComponent>> _ghostRoleRaffles = new();
  56. private readonly Dictionary<ICommonSession, GhostRolesEui> _openUis = new();
  57. private readonly Dictionary<ICommonSession, MakeGhostRoleEui> _openMakeGhostRoleUis = new();
  58. [ViewVariables]
  59. public IReadOnlyCollection<Entity<GhostRoleComponent>> GhostRoles => _ghostRoles.Values;
  60. public override void Initialize()
  61. {
  62. base.Initialize();
  63. SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
  64. SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
  65. SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindAddedMessage>(OnMindAdded);
  66. SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
  67. SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
  68. SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
  69. SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
  70. SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
  71. SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
  72. SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
  73. SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
  74. SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
  75. SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
  76. SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
  77. SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
  78. SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GhostRoleRadioMessage>(OnGhostRoleRadioMessage);
  79. _playerManager.PlayerStatusChanged += PlayerStatusChanged;
  80. }
  81. private void OnMobStateChanged(Entity<GhostTakeoverAvailableComponent> component, ref MobStateChangedEvent args)
  82. {
  83. if (!TryComp(component, out GhostRoleComponent? ghostRole))
  84. return;
  85. switch (args.NewMobState)
  86. {
  87. case MobState.Alive:
  88. {
  89. if (!ghostRole.Taken)
  90. RegisterGhostRole((component, ghostRole));
  91. break;
  92. }
  93. case MobState.Critical:
  94. case MobState.Dead:
  95. UnregisterGhostRole((component, ghostRole));
  96. break;
  97. }
  98. }
  99. public override void Shutdown()
  100. {
  101. base.Shutdown();
  102. _playerManager.PlayerStatusChanged -= PlayerStatusChanged;
  103. }
  104. private uint GetNextRoleIdentifier()
  105. {
  106. return unchecked(_nextRoleIdentifier++);
  107. }
  108. public void OpenEui(ICommonSession session)
  109. {
  110. if (session.AttachedEntity is not { Valid: true } attached ||
  111. !EntityManager.HasComponent<GhostComponent>(attached))
  112. return;
  113. if (_openUis.ContainsKey(session))
  114. CloseEui(session);
  115. var eui = _openUis[session] = new GhostRolesEui();
  116. _euiManager.OpenEui(eui, session);
  117. eui.StateDirty();
  118. }
  119. public void OpenMakeGhostRoleEui(ICommonSession session, EntityUid uid)
  120. {
  121. if (session.AttachedEntity == null)
  122. return;
  123. if (_openMakeGhostRoleUis.ContainsKey(session))
  124. CloseEui(session);
  125. var eui = _openMakeGhostRoleUis[session] = new MakeGhostRoleEui(EntityManager, GetNetEntity(uid));
  126. _euiManager.OpenEui(eui, session);
  127. eui.StateDirty();
  128. }
  129. public void CloseEui(ICommonSession session)
  130. {
  131. if (!_openUis.ContainsKey(session))
  132. return;
  133. _openUis.Remove(session, out var eui);
  134. eui?.Close();
  135. }
  136. public void CloseMakeGhostRoleEui(ICommonSession session)
  137. {
  138. if (_openMakeGhostRoleUis.Remove(session, out var eui))
  139. {
  140. eui.Close();
  141. }
  142. }
  143. public void UpdateAllEui()
  144. {
  145. foreach (var eui in _openUis.Values)
  146. {
  147. eui.StateDirty();
  148. }
  149. // Note that this, like the EUIs, is deferred.
  150. // This is for roughly the same reasons, too:
  151. // Someone might spawn a ton of ghost roles at once.
  152. _needsUpdateGhostRoleCount = true;
  153. }
  154. public override void Update(float frameTime)
  155. {
  156. base.Update(frameTime);
  157. UpdateGhostRoleCount();
  158. UpdateRaffles(frameTime);
  159. }
  160. /// <summary>
  161. /// Handles sending count update for the ghost role button in ghost UI, if ghost role count changed.
  162. /// </summary>
  163. private void UpdateGhostRoleCount()
  164. {
  165. if (!_needsUpdateGhostRoleCount)
  166. return;
  167. _needsUpdateGhostRoleCount = false;
  168. var response = new GhostUpdateGhostRoleCountEvent(GetGhostRoleCount());
  169. foreach (var player in _playerManager.Sessions)
  170. {
  171. RaiseNetworkEvent(response, player.Channel);
  172. }
  173. }
  174. /// <summary>
  175. /// Handles ghost role raffle logic.
  176. /// </summary>
  177. private void UpdateRaffles(float frameTime)
  178. {
  179. var query = EntityQueryEnumerator<GhostRoleRaffleComponent, MetaDataComponent>();
  180. while (query.MoveNext(out var entityUid, out var raffle, out var meta))
  181. {
  182. if (meta.EntityPaused)
  183. continue;
  184. // if all participants leave/were removed from the raffle, the raffle is canceled.
  185. if (raffle.CurrentMembers.Count == 0)
  186. {
  187. RemoveRaffleAndUpdateEui(entityUid, raffle);
  188. continue;
  189. }
  190. raffle.Countdown = raffle.Countdown.Subtract(TimeSpan.FromSeconds(frameTime));
  191. if (raffle.Countdown.Ticks > 0)
  192. continue;
  193. // the raffle is over! find someone to take over the ghost role
  194. if (!TryComp(entityUid, out GhostRoleComponent? ghostRole))
  195. {
  196. Log.Warning($"Ghost role raffle finished on {entityUid} but {nameof(GhostRoleComponent)} is missing");
  197. RemoveRaffleAndUpdateEui(entityUid, raffle);
  198. continue;
  199. }
  200. if (ghostRole.RaffleConfig is null)
  201. {
  202. Log.Warning($"Ghost role raffle finished on {entityUid} but RaffleConfig became null");
  203. RemoveRaffleAndUpdateEui(entityUid, raffle);
  204. continue;
  205. }
  206. var foundWinner = false;
  207. var deciderPrototype = _prototype.Index(ghostRole.RaffleConfig.Decider);
  208. // use the ghost role's chosen winner picker to find a winner
  209. deciderPrototype.Decider.PickWinner(
  210. raffle.CurrentMembers.AsEnumerable(),
  211. session =>
  212. {
  213. var success = TryTakeover(session, raffle.Identifier);
  214. foundWinner |= success;
  215. return success;
  216. }
  217. );
  218. if (!foundWinner)
  219. {
  220. Log.Warning($"Ghost role raffle for {entityUid} ({ghostRole.RoleName}) finished without " +
  221. $"{ghostRole.RaffleConfig?.Decider} finding a winner");
  222. }
  223. // raffle over
  224. RemoveRaffleAndUpdateEui(entityUid, raffle);
  225. }
  226. }
  227. private bool TryTakeover(ICommonSession player, uint identifier)
  228. {
  229. // TODO: the following two checks are kind of redundant since they should already be removed
  230. // from the raffle
  231. // can't win if you are disconnected (although you shouldn't be a candidate anyway)
  232. if (player.Status != SessionStatus.InGame)
  233. return false;
  234. // can't win if you are no longer a ghost (e.g. if you returned to your body)
  235. if (player.AttachedEntity == null || !HasComp<GhostComponent>(player.AttachedEntity))
  236. return false;
  237. if (Takeover(player, identifier))
  238. {
  239. // takeover successful, we have a winner! remove the winner from other raffles they might be in
  240. LeaveAllRaffles(player);
  241. return true;
  242. }
  243. return false;
  244. }
  245. private void RemoveRaffleAndUpdateEui(EntityUid entityUid, GhostRoleRaffleComponent raffle)
  246. {
  247. _ghostRoleRaffles.Remove(raffle.Identifier);
  248. RemComp(entityUid, raffle);
  249. UpdateAllEui();
  250. }
  251. private void PlayerStatusChanged(object? blah, SessionStatusEventArgs args)
  252. {
  253. if (args.NewStatus == SessionStatus.InGame)
  254. {
  255. var response = new GhostUpdateGhostRoleCountEvent(_ghostRoles.Count);
  256. RaiseNetworkEvent(response, args.Session.Channel);
  257. }
  258. else
  259. {
  260. // people who disconnect are removed from ghost role raffles
  261. LeaveAllRaffles(args.Session);
  262. }
  263. }
  264. public void RegisterGhostRole(Entity<GhostRoleComponent> role)
  265. {
  266. if (_ghostRoles.ContainsValue(role))
  267. return;
  268. _ghostRoles[role.Comp.Identifier = GetNextRoleIdentifier()] = role;
  269. UpdateAllEui();
  270. }
  271. public void UnregisterGhostRole(Entity<GhostRoleComponent> role)
  272. {
  273. var comp = role.Comp;
  274. if (!_ghostRoles.ContainsKey(comp.Identifier) || _ghostRoles[comp.Identifier] != role)
  275. return;
  276. _ghostRoles.Remove(comp.Identifier);
  277. if (TryComp(role.Owner, out GhostRoleRaffleComponent? raffle))
  278. {
  279. // if a raffle is still running, get rid of it
  280. RemoveRaffleAndUpdateEui(role.Owner, raffle);
  281. }
  282. else
  283. {
  284. UpdateAllEui();
  285. }
  286. }
  287. // probably fine to be init because it's never added during entity initialization, but much later
  288. private void OnRaffleInit(Entity<GhostRoleRaffleComponent> ent, ref ComponentInit args)
  289. {
  290. if (!TryComp(ent, out GhostRoleComponent? ghostRole))
  291. {
  292. // can't have a raffle for a ghost role that doesn't exist
  293. RemComp<GhostRoleRaffleComponent>(ent);
  294. return;
  295. }
  296. var config = ghostRole.RaffleConfig;
  297. if (config is null)
  298. return; // should, realistically, never be reached but you never know
  299. var settings = config.SettingsOverride
  300. ?? _prototype.Index<GhostRoleRaffleSettingsPrototype>(config.Settings).Settings;
  301. if (settings.MaxDuration < settings.InitialDuration)
  302. {
  303. Log.Error($"Ghost role on {ent} has invalid raffle settings (max duration shorter than initial)");
  304. ghostRole.RaffleConfig = null; // make it a non-raffle role so stuff isn't entirely broken
  305. RemComp<GhostRoleRaffleComponent>(ent);
  306. return;
  307. }
  308. var raffle = ent.Comp;
  309. raffle.Identifier = ghostRole.Identifier;
  310. var countdown = _cfg.GetCVar(CCVars.GhostQuickLottery)? 1 : settings.InitialDuration;
  311. raffle.Countdown = TimeSpan.FromSeconds(countdown);
  312. raffle.CumulativeTime = TimeSpan.FromSeconds(settings.InitialDuration);
  313. // we copy these settings into the component because they would be cumbersome to access otherwise
  314. raffle.JoinExtendsDurationBy = TimeSpan.FromSeconds(settings.JoinExtendsDurationBy);
  315. raffle.MaxDuration = TimeSpan.FromSeconds(settings.MaxDuration);
  316. }
  317. private void OnRaffleShutdown(Entity<GhostRoleRaffleComponent> ent, ref ComponentShutdown args)
  318. {
  319. _ghostRoleRaffles.Remove(ent.Comp.Identifier);
  320. }
  321. /// <summary>
  322. /// Joins the given player onto a ghost role raffle, or creates it if it doesn't exist.
  323. /// </summary>
  324. /// <param name="player">The player.</param>
  325. /// <param name="identifier">The ID that represents the ghost role or ghost role raffle.
  326. /// (A raffle will have the same ID as the ghost role it's for.)</param>
  327. private void JoinRaffle(ICommonSession player, uint identifier)
  328. {
  329. if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
  330. return;
  331. // get raffle or create a new one if it doesn't exist
  332. var raffle = _ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt)
  333. ? raffleEnt.Comp
  334. : EnsureComp<GhostRoleRaffleComponent>(roleEnt.Owner);
  335. _ghostRoleRaffles.TryAdd(identifier, (roleEnt.Owner, raffle));
  336. if (!raffle.CurrentMembers.Add(player))
  337. {
  338. Log.Warning($"{player.Name} tried to join raffle for ghost role {identifier} but they are already in the raffle");
  339. return;
  340. }
  341. // if this is the first time the player joins this raffle, and the player wasn't the starter of the raffle:
  342. // extend the countdown, but only if doing so will not make the raffle take longer than the maximum
  343. // duration
  344. if (raffle.AllMembers.Add(player) && raffle.AllMembers.Count > 1
  345. && raffle.CumulativeTime.Add(raffle.JoinExtendsDurationBy) <= raffle.MaxDuration)
  346. {
  347. raffle.Countdown += raffle.JoinExtendsDurationBy;
  348. raffle.CumulativeTime += raffle.JoinExtendsDurationBy;
  349. }
  350. UpdateAllEui();
  351. }
  352. /// <summary>
  353. /// Makes the given player leave the raffle corresponding to the given ID.
  354. /// </summary>
  355. public void LeaveRaffle(ICommonSession player, uint identifier)
  356. {
  357. if (!_ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt))
  358. return;
  359. if (raffleEnt.Comp.CurrentMembers.Remove(player))
  360. {
  361. UpdateAllEui();
  362. }
  363. else
  364. {
  365. Log.Warning($"{player.Name} tried to leave raffle for ghost role {identifier} but they are not in the raffle");
  366. }
  367. // (raffle ending because all players left is handled in update())
  368. }
  369. /// <summary>
  370. /// Makes the given player leave all ghost role raffles.
  371. /// </summary>
  372. public void LeaveAllRaffles(ICommonSession player)
  373. {
  374. var shouldUpdateEui = false;
  375. foreach (var raffleEnt in _ghostRoleRaffles.Values)
  376. {
  377. shouldUpdateEui |= raffleEnt.Comp.CurrentMembers.Remove(player);
  378. }
  379. if (shouldUpdateEui)
  380. UpdateAllEui();
  381. }
  382. /// <summary>
  383. /// Request a ghost role. If it's a raffled role starts or joins a raffle, otherwise the player immediately
  384. /// takes over the ghost role if possible.
  385. /// </summary>
  386. /// <param name="player">The player.</param>
  387. /// <param name="identifier">ID of the ghost role.</param>
  388. public void Request(ICommonSession player, uint identifier)
  389. {
  390. if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
  391. return;
  392. if (roleEnt.Comp.RaffleConfig is not null)
  393. {
  394. JoinRaffle(player, identifier);
  395. }
  396. else
  397. {
  398. Takeover(player, identifier);
  399. }
  400. }
  401. /// <summary>
  402. /// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
  403. /// </summary>
  404. /// <returns>True if takeover was successful, otherwise false.</returns>
  405. public bool Takeover(ICommonSession player, uint identifier)
  406. {
  407. if (!_ghostRoles.TryGetValue(identifier, out var role))
  408. return false;
  409. var ev = new TakeGhostRoleEvent(player);
  410. RaiseLocalEvent(role, ref ev);
  411. if (!ev.TookRole)
  412. return false;
  413. if (player.AttachedEntity != null)
  414. _adminLogger.Add(LogType.GhostRoleTaken, LogImpact.Low, $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity}");
  415. CloseEui(player);
  416. return true;
  417. }
  418. public void Follow(ICommonSession player, uint identifier)
  419. {
  420. if (!_ghostRoles.TryGetValue(identifier, out var role))
  421. return;
  422. if (player.AttachedEntity == null)
  423. return;
  424. _followerSystem.StartFollowingEntity(player.AttachedEntity.Value, role);
  425. }
  426. public void GhostRoleInternalCreateMindAndTransfer(ICommonSession player, EntityUid roleUid, EntityUid mob, GhostRoleComponent? role = null)
  427. {
  428. if (!Resolve(roleUid, ref role))
  429. return;
  430. DebugTools.AssertNotNull(player.ContentData());
  431. var newMind = _mindSystem.CreateMind(player.UserId,
  432. EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
  433. _mindSystem.SetUserId(newMind, player.UserId);
  434. _mindSystem.TransferTo(newMind, mob);
  435. _roleSystem.MindAddRoles(newMind.Owner, role.MindRoles, newMind.Comp);
  436. if (_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
  437. markerRole.Value.Comp2.Name = role.RoleName;
  438. }
  439. /// <summary>
  440. /// Returns the number of available ghost roles.
  441. /// </summary>
  442. public int GetGhostRoleCount()
  443. {
  444. var metaQuery = GetEntityQuery<MetaDataComponent>();
  445. return _ghostRoles.Count(pair => metaQuery.GetComponent(pair.Value.Owner).EntityPaused == false);
  446. }
  447. /// <summary>
  448. /// Returns information about all available ghost roles.
  449. /// </summary>
  450. /// <param name="player">
  451. /// If not null, the <see cref="GhostRoleInfo"/>s will show if the given player is in a raffle.
  452. /// </param>
  453. public GhostRoleInfo[] GetGhostRolesInfo(ICommonSession? player)
  454. {
  455. var roles = new List<GhostRoleInfo>();
  456. var metaQuery = GetEntityQuery<MetaDataComponent>();
  457. foreach (var (id, (uid, role)) in _ghostRoles)
  458. {
  459. if (metaQuery.GetComponent(uid).EntityPaused)
  460. continue;
  461. var kind = GhostRoleKind.FirstComeFirstServe;
  462. GhostRoleRaffleComponent? raffle = null;
  463. if (role.RaffleConfig is not null)
  464. {
  465. kind = GhostRoleKind.RaffleReady;
  466. if (_ghostRoleRaffles.TryGetValue(id, out var raffleEnt))
  467. {
  468. kind = GhostRoleKind.RaffleInProgress;
  469. raffle = raffleEnt.Comp;
  470. if (player is not null && raffle.CurrentMembers.Contains(player))
  471. kind = GhostRoleKind.RaffleJoined;
  472. }
  473. }
  474. var rafflePlayerCount = (uint?) raffle?.CurrentMembers.Count ?? 0;
  475. var raffleEndTime = raffle is not null
  476. ? _timing.CurTime.Add(raffle.Countdown)
  477. : TimeSpan.MinValue;
  478. roles.Add(new GhostRoleInfo
  479. {
  480. Identifier = id,
  481. Name = role.RoleName,
  482. Description = role.RoleDescription,
  483. Rules = role.RoleRules,
  484. Requirements = role.Requirements,
  485. Kind = kind,
  486. RafflePlayerCount = rafflePlayerCount,
  487. RaffleEndTime = raffleEndTime
  488. });
  489. }
  490. return roles.ToArray();
  491. }
  492. private void OnPlayerAttached(PlayerAttachedEvent message)
  493. {
  494. // Close the session of any player that has a ghost roles window open and isn't a ghost anymore.
  495. if (!_openUis.ContainsKey(message.Player))
  496. return;
  497. if (HasComp<GhostComponent>(message.Entity))
  498. return;
  499. // The player is not a ghost (anymore), so they should not be in any raffles. Remove them.
  500. // This ensures player doesn't win a raffle after returning to their (revived) body and ends up being
  501. // forced into a ghost role.
  502. LeaveAllRaffles(message.Player);
  503. CloseEui(message.Player);
  504. }
  505. private void OnMindAdded(EntityUid uid, GhostTakeoverAvailableComponent component, MindAddedMessage args)
  506. {
  507. if (!TryComp(uid, out GhostRoleComponent? ghostRole))
  508. return;
  509. if (ghostRole.JobProto != null)
  510. {
  511. _roleSystem.MindAddJobRole(args.Mind, args.Mind, silent:false,ghostRole.JobProto);
  512. }
  513. ghostRole.Taken = true;
  514. UnregisterGhostRole((uid, ghostRole));
  515. }
  516. private void OnMindRemoved(EntityUid uid, GhostTakeoverAvailableComponent component, MindRemovedMessage args)
  517. {
  518. if (!TryComp(uid, out GhostRoleComponent? ghostRole))
  519. return;
  520. // Avoid re-registering it for duplicate entries and potential exceptions.
  521. if (!ghostRole.ReregisterOnGhost || component.LifeStage > ComponentLifeStage.Running)
  522. return;
  523. ghostRole.Taken = false;
  524. RegisterGhostRole((uid, ghostRole));
  525. }
  526. public void Reset(RoundRestartCleanupEvent ev)
  527. {
  528. foreach (var session in _openUis.Keys)
  529. {
  530. CloseEui(session);
  531. }
  532. _openUis.Clear();
  533. _ghostRoles.Clear();
  534. _ghostRoleRaffles.Clear();
  535. _nextRoleIdentifier = 0;
  536. }
  537. private void OnPaused(EntityUid uid, GhostRoleComponent component, ref EntityPausedEvent args)
  538. {
  539. if (HasComp<ActorComponent>(uid))
  540. return;
  541. UpdateAllEui();
  542. }
  543. private void OnUnpaused(EntityUid uid, GhostRoleComponent component, ref EntityUnpausedEvent args)
  544. {
  545. if (HasComp<ActorComponent>(uid))
  546. return;
  547. UpdateAllEui();
  548. }
  549. private void OnMapInit(Entity<GhostRoleComponent> ent, ref MapInitEvent args)
  550. {
  551. if (ent.Comp.Probability < 1f && !_random.Prob(ent.Comp.Probability))
  552. RemCompDeferred<GhostRoleComponent>(ent);
  553. }
  554. private void OnRoleStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
  555. {
  556. RegisterGhostRole(ent);
  557. }
  558. private void OnRoleShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
  559. {
  560. UnregisterGhostRole(role);
  561. }
  562. private void OnSpawnerTakeRole(EntityUid uid, GhostRoleMobSpawnerComponent component, ref TakeGhostRoleEvent args)
  563. {
  564. if (!TryComp(uid, out GhostRoleComponent? ghostRole) ||
  565. !CanTakeGhost(uid, ghostRole))
  566. {
  567. args.TookRole = false;
  568. return;
  569. }
  570. if (string.IsNullOrEmpty(component.Prototype))
  571. throw new NullReferenceException("Prototype string cannot be null or empty!");
  572. var mob = Spawn(component.Prototype, Transform(uid).Coordinates);
  573. _transform.AttachToGridOrMap(mob);
  574. var spawnedEvent = new GhostRoleSpawnerUsedEvent(uid, mob);
  575. RaiseLocalEvent(mob, spawnedEvent);
  576. if (ghostRole.MakeSentient)
  577. MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
  578. EnsureComp<MindContainerComponent>(mob);
  579. GhostRoleInternalCreateMindAndTransfer(args.Player, uid, mob, ghostRole);
  580. if (++component.CurrentTakeovers < component.AvailableTakeovers)
  581. {
  582. args.TookRole = true;
  583. return;
  584. }
  585. ghostRole.Taken = true;
  586. if (component.DeleteOnSpawn)
  587. QueueDel(uid);
  588. args.TookRole = true;
  589. }
  590. private bool CanTakeGhost(EntityUid uid, GhostRoleComponent? component = null)
  591. {
  592. return Resolve(uid, ref component, false) &&
  593. !component.Taken &&
  594. !MetaData(uid).EntityPaused;
  595. }
  596. private void OnTakeoverTakeRole(EntityUid uid, GhostTakeoverAvailableComponent component, ref TakeGhostRoleEvent args)
  597. {
  598. if (!TryComp(uid, out GhostRoleComponent? ghostRole) ||
  599. !CanTakeGhost(uid, ghostRole))
  600. {
  601. args.TookRole = false;
  602. return;
  603. }
  604. ghostRole.Taken = true;
  605. var mind = EnsureComp<MindContainerComponent>(uid);
  606. if (mind.HasMind)
  607. {
  608. args.TookRole = false;
  609. return;
  610. }
  611. if (ghostRole.MakeSentient)
  612. MakeSentientCommand.MakeSentient(uid, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
  613. GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole);
  614. UnregisterGhostRole((uid, ghostRole));
  615. args.TookRole = true;
  616. }
  617. private void OnVerb(EntityUid uid, GhostRoleMobSpawnerComponent component, GetVerbsEvent<Verb> args)
  618. {
  619. var prototypes = component.SelectablePrototypes;
  620. if (prototypes.Count < 1)
  621. return;
  622. if (!args.CanAccess || !args.CanInteract || args.Hands == null)
  623. return;
  624. var verbs = new ValueList<Verb>();
  625. foreach (var prototypeID in prototypes)
  626. {
  627. if (_prototype.TryIndex<GhostRolePrototype>(prototypeID, out var prototype))
  628. {
  629. var verb = CreateVerb(uid, component, args.User, prototype);
  630. verbs.Add(verb);
  631. }
  632. }
  633. args.Verbs.UnionWith(verbs);
  634. }
  635. private Verb CreateVerb(EntityUid uid, GhostRoleMobSpawnerComponent component, EntityUid userUid, GhostRolePrototype prototype)
  636. {
  637. var verbText = Loc.GetString(prototype.Name);
  638. return new Verb()
  639. {
  640. Text = verbText,
  641. Disabled = component.Prototype == prototype.EntityPrototype,
  642. Category = VerbCategory.SelectType,
  643. Act = () => SetMode(uid, prototype, verbText, component, userUid)
  644. };
  645. }
  646. public void SetMode(EntityUid uid, GhostRolePrototype prototype, string verbText, GhostRoleMobSpawnerComponent? component, EntityUid? userUid = null)
  647. {
  648. if (!Resolve(uid, ref component))
  649. return;
  650. var ghostrolecomp = EnsureComp<GhostRoleComponent>(uid);
  651. component.Prototype = prototype.EntityPrototype;
  652. ghostrolecomp.RoleName = verbText;
  653. ghostrolecomp.RoleDescription = prototype.Description;
  654. ghostrolecomp.RoleRules = prototype.Rules;
  655. // Dirty(ghostrolecomp);
  656. if (userUid != null)
  657. {
  658. var msg = Loc.GetString("ghostrole-spawner-select", ("mode", verbText));
  659. _popupSystem.PopupEntity(msg, uid, userUid.Value);
  660. }
  661. }
  662. public void OnGhostRoleRadioMessage(Entity<GhostRoleMobSpawnerComponent> entity, ref GhostRoleRadioMessage args)
  663. {
  664. if (!_prototype.TryIndex(args.ProtoId, out var ghostRoleProto))
  665. return;
  666. // if the prototype chosen isn't actually part of the selectable options, ignore it
  667. foreach (var selectableProto in entity.Comp.SelectablePrototypes)
  668. {
  669. if (selectableProto == ghostRoleProto.EntityPrototype.Id)
  670. return;
  671. }
  672. SetMode(entity.Owner, ghostRoleProto, ghostRoleProto.Name, entity.Comp);
  673. }
  674. }
  675. [AnyCommand]
  676. public sealed class GhostRoles : IConsoleCommand
  677. {
  678. [Dependency] private readonly IEntityManager _e = default!;
  679. public string Command => "ghostroles";
  680. public string Description => "Opens the ghost role request window.";
  681. public string Help => $"{Command}";
  682. public void Execute(IConsoleShell shell, string argStr, string[] args)
  683. {
  684. if (shell.Player != null)
  685. _e.System<GhostRoleSystem>().OpenEui(shell.Player);
  686. else
  687. shell.WriteLine("You can only open the ghost roles UI on a client.");
  688. }
  689. }