SharedMindSystem.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Shared.Administration.Logs;
  4. using Content.Shared.Database;
  5. using Content.Shared.Examine;
  6. using Content.Shared.GameTicking;
  7. using Content.Shared.Humanoid;
  8. using Content.Shared.Interaction.Events;
  9. using Content.Shared.Mind.Components;
  10. using Content.Shared.Mobs.Components;
  11. using Content.Shared.Mobs.Systems;
  12. using Content.Shared.Objectives.Systems;
  13. using Content.Shared.Players;
  14. using Content.Shared.Whitelist;
  15. using Robust.Shared.Map;
  16. using Robust.Shared.Network;
  17. using Robust.Shared.Player;
  18. using Robust.Shared.Utility;
  19. namespace Content.Shared.Mind;
  20. public abstract class SharedMindSystem : EntitySystem
  21. {
  22. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  23. [Dependency] private readonly INetManager _net = default!;
  24. [Dependency] private readonly MobStateSystem _mobState = default!;
  25. [Dependency] private readonly SharedObjectivesSystem _objectives = default!;
  26. [Dependency] private readonly SharedPlayerSystem _player = default!;
  27. [Dependency] private readonly MetaDataSystem _metadata = default!;
  28. [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
  29. [ViewVariables]
  30. protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
  31. public override void Initialize()
  32. {
  33. base.Initialize();
  34. SubscribeLocalEvent<MindContainerComponent, ExaminedEvent>(OnExamined);
  35. SubscribeLocalEvent<MindContainerComponent, SuicideEvent>(OnSuicide);
  36. SubscribeLocalEvent<VisitingMindComponent, EntityTerminatingEvent>(OnVisitingTerminating);
  37. SubscribeLocalEvent<RoundRestartCleanupEvent>(OnReset);
  38. SubscribeLocalEvent<MindComponent, ComponentStartup>(OnMindStartup);
  39. SubscribeLocalEvent<MindComponent, EntityRenamedEvent>(OnRenamed);
  40. }
  41. public override void Shutdown()
  42. {
  43. base.Shutdown();
  44. WipeAllMinds();
  45. }
  46. private void OnMindStartup(EntityUid uid, MindComponent component, ComponentStartup args)
  47. {
  48. if (component.UserId == null)
  49. return;
  50. if (UserMinds.TryAdd(component.UserId.Value, uid))
  51. return;
  52. var existing = UserMinds[component.UserId.Value];
  53. if (existing == uid)
  54. return;
  55. if (!Exists(existing))
  56. {
  57. Log.Error($"Found deleted entity in mind dictionary while initializing mind {ToPrettyString(uid)}");
  58. UserMinds[component.UserId.Value] = uid;
  59. return;
  60. }
  61. Log.Error($"Encountered a user {component.UserId} that is already assigned to a mind while initializing mind {ToPrettyString(uid)}. Ignoring user field.");
  62. component.UserId = null;
  63. }
  64. private void OnReset(RoundRestartCleanupEvent ev)
  65. {
  66. WipeAllMinds();
  67. }
  68. public virtual void WipeAllMinds()
  69. {
  70. Log.Info($"Wiping all minds");
  71. foreach (var mind in UserMinds.Values.ToArray())
  72. {
  73. WipeMind(mind);
  74. }
  75. if (UserMinds.Count == 0)
  76. return;
  77. foreach (var mind in UserMinds.Values)
  78. {
  79. if (Exists(mind))
  80. Log.Error($"Failed to wipe mind: {ToPrettyString(mind)}");
  81. }
  82. UserMinds.Clear();
  83. }
  84. public EntityUid? GetMind(NetUserId user)
  85. {
  86. TryGetMind(user, out var mind, out _);
  87. return mind;
  88. }
  89. public virtual bool TryGetMind(NetUserId user, [NotNullWhen(true)] out EntityUid? mindId, [NotNullWhen(true)] out MindComponent? mind)
  90. {
  91. if (UserMinds.TryGetValue(user, out var mindIdValue) &&
  92. TryComp(mindIdValue, out mind))
  93. {
  94. DebugTools.Assert(mind.UserId == user);
  95. mindId = mindIdValue;
  96. return true;
  97. }
  98. mindId = null;
  99. mind = null;
  100. return false;
  101. }
  102. public bool TryGetMind(NetUserId user, [NotNullWhen(true)] out Entity<MindComponent>? mind)
  103. {
  104. if (!TryGetMind(user, out var mindId, out var mindComp))
  105. {
  106. mind = null;
  107. return false;
  108. }
  109. mind = (mindId.Value, mindComp);
  110. return true;
  111. }
  112. public Entity<MindComponent> GetOrCreateMind(NetUserId user)
  113. {
  114. if (!TryGetMind(user, out var mind))
  115. mind = CreateMind(user);
  116. return mind.Value;
  117. }
  118. private void OnVisitingTerminating(EntityUid uid, VisitingMindComponent component, ref EntityTerminatingEvent args)
  119. {
  120. if (component.MindId != null)
  121. UnVisit(component.MindId.Value);
  122. }
  123. private void OnExamined(EntityUid uid, MindContainerComponent mindContainer, ExaminedEvent args)
  124. {
  125. if (!mindContainer.ShowExamineInfo || !args.IsInDetailsRange)
  126. return;
  127. // TODO predict we can't right now because session stuff isnt networked
  128. if (_net.IsClient)
  129. return;
  130. var dead = _mobState.IsDead(uid);
  131. var hasUserId = CompOrNull<MindComponent>(mindContainer.Mind)?.UserId;
  132. var hasSession = CompOrNull<MindComponent>(mindContainer.Mind)?.Session;
  133. if (dead && hasUserId == null)
  134. args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-dead-and-irrecoverable", ("ent", uid))}[/color]");
  135. else if (dead && hasSession == null)
  136. args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-dead-and-ssd", ("ent", uid))}[/color]");
  137. else if (dead)
  138. args.PushMarkup($"[color=red]{Loc.GetString("comp-mind-examined-dead", ("ent", uid))}[/color]");
  139. else if (hasUserId == null)
  140. args.PushMarkup($"[color=mediumpurple]{Loc.GetString("comp-mind-examined-catatonic", ("ent", uid))}[/color]");
  141. else if (hasSession == null)
  142. args.PushMarkup($"[color=yellow]{Loc.GetString("comp-mind-examined-ssd", ("ent", uid))}[/color]");
  143. }
  144. /// <summary>
  145. /// Checks to see if the user's mind prevents them from suicide
  146. /// Handles the suicide event without killing the user if true
  147. /// </summary>
  148. private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideEvent args)
  149. {
  150. if (args.Handled)
  151. return;
  152. if (TryComp(component.Mind, out MindComponent? mind) && mind.PreventSuicide)
  153. args.Handled = true;
  154. }
  155. private void OnRenamed(Entity<MindComponent> ent, ref EntityRenamedEvent args)
  156. {
  157. ent.Comp.CharacterName = args.NewName;
  158. Dirty(ent);
  159. }
  160. public EntityUid? GetMind(EntityUid uid, MindContainerComponent? mind = null)
  161. {
  162. if (!Resolve(uid, ref mind))
  163. return null;
  164. if (mind.HasMind)
  165. return mind.Mind;
  166. return null;
  167. }
  168. public Entity<MindComponent> CreateMind(NetUserId? userId, string? name = null)
  169. {
  170. var mindId = Spawn(null, MapCoordinates.Nullspace);
  171. _metadata.SetEntityName(mindId, name == null ? "mind" : $"mind ({name})");
  172. var mind = EnsureComp<MindComponent>(mindId);
  173. mind.CharacterName = name;
  174. SetUserId(mindId, userId, mind);
  175. return (mindId, mind);
  176. }
  177. /// <summary>
  178. /// True if the OwnedEntity of this mind is physically dead.
  179. /// This specific definition, as opposed to CharacterDeadIC, is used to determine if ghosting should allow return.
  180. /// </summary>
  181. public bool IsCharacterDeadPhysically(MindComponent mind)
  182. {
  183. // This is written explicitly so that the logic can be understood.
  184. // But it's also weird and potentially situational.
  185. // Specific considerations when updating this:
  186. // + Does being turned into a borg (if/when implemented) count as dead?
  187. // *If not, add specific conditions to users of this property where applicable.*
  188. // + Is being transformed into a donut 'dead'?
  189. // TODO: Consider changing the way ghost roles work.
  190. // Mind is an *IC* mind, therefore ghost takeover is IC revival right now.
  191. // + Is it necessary to have a reference to a specific 'mind iteration' to cycle when certain events happen?
  192. // (If being a borg or AI counts as dead, then this is highly likely, as it's still the same Mind for practical purposes.)
  193. if (mind.OwnedEntity == null)
  194. return true;
  195. // This can be null if they're deleted (spike / brain nom)
  196. var targetMobState = EntityManager.GetComponentOrNull<MobStateComponent>(mind.OwnedEntity);
  197. // This can be null if it's a brain (this happens very often)
  198. // Brains are the result of gibbing so should definitely count as dead
  199. if (targetMobState == null)
  200. return true;
  201. // They might actually be alive.
  202. return _mobState.IsDead(mind.OwnedEntity.Value, targetMobState);
  203. }
  204. public virtual void Visit(EntityUid mindId, EntityUid entity, MindComponent? mind = null)
  205. {
  206. }
  207. /// <summary>
  208. /// Returns the mind to its original entity.
  209. /// </summary>
  210. public virtual void UnVisit(EntityUid mindId, MindComponent? mind = null)
  211. {
  212. }
  213. /// <summary>
  214. /// Returns the mind to its original entity.
  215. /// </summary>
  216. public void UnVisit(ICommonSession? player)
  217. {
  218. if (player == null || !TryGetMind(player, out var mindId, out var mind))
  219. return;
  220. UnVisit(mindId, mind);
  221. }
  222. /// <summary>
  223. /// Cleans up the VisitingEntity.
  224. /// </summary>
  225. /// <param name="mind"></param>
  226. protected void RemoveVisitingEntity(EntityUid mindId, MindComponent mind)
  227. {
  228. if (mind.VisitingEntity == null)
  229. return;
  230. var oldVisitingEnt = mind.VisitingEntity.Value;
  231. // Null this before removing the component to avoid any infinite loops.
  232. mind.VisitingEntity = null;
  233. if (TryComp(oldVisitingEnt, out VisitingMindComponent? visitComp))
  234. {
  235. visitComp.MindId = null;
  236. RemCompDeferred(oldVisitingEnt, visitComp);
  237. }
  238. Dirty(mindId, mind);
  239. RaiseLocalEvent(oldVisitingEnt, new MindUnvisitedMessage(), true);
  240. }
  241. public void WipeMind(ICommonSession player)
  242. {
  243. var mind = _player.ContentData(player)?.Mind;
  244. DebugTools.Assert(GetMind(player.UserId) == mind);
  245. WipeMind(mind);
  246. }
  247. /// <summary>
  248. /// Detaches a mind from all entities and clears the user ID.
  249. /// </summary>
  250. public void WipeMind(EntityUid? mindId, MindComponent? mind = null)
  251. {
  252. if (mindId == null || !Resolve(mindId.Value, ref mind, false))
  253. return;
  254. TransferTo(mindId.Value, null, createGhost:false, mind: mind);
  255. SetUserId(mindId.Value, null, mind: mind);
  256. }
  257. /// <summary>
  258. /// Transfer this mind's control over to a new entity.
  259. /// </summary>
  260. /// <param name="mindId">The mind to transfer</param>
  261. /// <param name="entity">
  262. /// The entity to control.
  263. /// Can be null, in which case it will simply detach the mind from any entity.
  264. /// </param>
  265. /// <param name="ghostCheckOverride">
  266. /// If true, skips ghost check for Visiting Entity
  267. /// </param>
  268. /// <exception cref="ArgumentException">
  269. /// Thrown if <paramref name="entity"/> is already controlled by another player.
  270. /// </exception>
  271. public virtual void TransferTo(EntityUid mindId, EntityUid? entity, bool ghostCheckOverride = false, bool createGhost = true, MindComponent? mind = null)
  272. {
  273. }
  274. public virtual void ControlMob(EntityUid user, EntityUid target) {}
  275. public virtual void ControlMob(NetUserId user, EntityUid target) {}
  276. /// <summary>
  277. /// Tries to create and add an objective from its prototype id.
  278. /// </summary>
  279. /// <returns>Returns true if adding the objective succeeded.</returns>
  280. public bool TryAddObjective(EntityUid mindId, MindComponent mind, string proto)
  281. {
  282. var objective = _objectives.TryCreateObjective(mindId, mind, proto);
  283. if (objective == null)
  284. return false;
  285. AddObjective(mindId, mind, objective.Value);
  286. return true;
  287. }
  288. /// <summary>
  289. /// Adds an objective that already exists, and is assumed to have had its requirements checked.
  290. /// </summary>
  291. public void AddObjective(EntityUid mindId, MindComponent mind, EntityUid objective)
  292. {
  293. var title = Name(objective);
  294. _adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) added to mind of {MindOwnerLoggingString(mind)}");
  295. mind.Objectives.Add(objective);
  296. }
  297. /// <summary>
  298. /// Removes an objective from this mind.
  299. /// </summary>
  300. /// <returns>Returns true if the removal succeeded.</returns>
  301. public bool TryRemoveObjective(EntityUid mindId, MindComponent mind, int index)
  302. {
  303. if (index < 0 || index >= mind.Objectives.Count)
  304. return false;
  305. var objective = mind.Objectives[index];
  306. var title = Name(objective);
  307. _adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
  308. mind.Objectives.Remove(objective);
  309. // garbage collection - only delete the objective entity if no mind uses it anymore
  310. // This comes up for stuff like paradox clones where the objectives share the same entity
  311. var mindQuery = new AllEntityQueryEnumerator<MindComponent>();
  312. while (mindQuery.MoveNext(out _, out var queryComp))
  313. {
  314. if (queryComp.Objectives.Contains(objective))
  315. return true;
  316. }
  317. Del(objective);
  318. return true;
  319. }
  320. public bool TryGetObjectiveComp<T>(EntityUid uid, [NotNullWhen(true)] out T? objective) where T : IComponent
  321. {
  322. if (TryGetMind(uid, out var mindId, out var mind) && TryGetObjectiveComp(mindId, out objective, mind))
  323. {
  324. return true;
  325. }
  326. objective = default;
  327. return false;
  328. }
  329. public bool TryGetObjectiveComp<T>(EntityUid mindId, [NotNullWhen(true)] out T? objective, MindComponent? mind = null) where T : IComponent
  330. {
  331. if (Resolve(mindId, ref mind))
  332. {
  333. var query = GetEntityQuery<T>();
  334. foreach (var uid in mind.Objectives)
  335. {
  336. if (query.TryGetComponent(uid, out objective))
  337. {
  338. return true;
  339. }
  340. }
  341. }
  342. objective = default;
  343. return false;
  344. }
  345. /// <summary>
  346. /// Copies objectives from one mind to another, so that they are shared between two players.
  347. /// </summary>
  348. /// <remarks>
  349. /// Only copies the reference to the objective entity, not the entity itself.
  350. /// This relies on the fact that objectives are never changed after spawning them.
  351. /// If someone ever changes that, they will have to address this.
  352. /// </remarks>
  353. /// <param name="source"> mind entity of the player to copy from </param>
  354. /// <param name="target"> mind entity of the player to copy to </param>
  355. /// <param name="except"> whitelist for objectives that should be copied </param>
  356. /// <param name="except"> blacklist for objectives that should not be copied </param>
  357. public void CopyObjectives(Entity<MindComponent?> source, Entity<MindComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
  358. {
  359. if (!Resolve(source, ref source.Comp) || !Resolve(target, ref target.Comp))
  360. return;
  361. foreach (var objective in source.Comp.Objectives)
  362. {
  363. if (target.Comp.Objectives.Contains(objective))
  364. continue; // target already has this objective
  365. if (_whitelist.CheckBoth(objective, blacklist, whitelist))
  366. AddObjective(target, target.Comp, objective);
  367. }
  368. }
  369. /// <summary>
  370. /// Tries to find an objective that has the same prototype as the argument.
  371. /// </summary>
  372. /// <remarks>
  373. /// Will not work for objectives that have no prototype, or duplicate objectives with the same prototype.
  374. /// <//remarks>
  375. public bool TryFindObjective(Entity<MindComponent?> mind, string prototype, [NotNullWhen(true)] out EntityUid? objective)
  376. {
  377. objective = null;
  378. if (!Resolve(mind, ref mind.Comp))
  379. return false;
  380. foreach (var uid in mind.Comp.Objectives)
  381. {
  382. if (MetaData(uid).EntityPrototype?.ID == prototype)
  383. {
  384. objective = uid;
  385. return true;
  386. }
  387. }
  388. return false;
  389. }
  390. public bool TryGetSession(EntityUid? mindId, [NotNullWhen(true)] out ICommonSession? session)
  391. {
  392. session = null;
  393. return TryComp(mindId, out MindComponent? mind) && (session = mind.Session) != null;
  394. }
  395. /// <summary>
  396. /// Gets a mind from uid and/or MindContainerComponent. Used for null checks.
  397. /// </summary>
  398. /// <param name="uid">Entity UID that owns the mind.</param>
  399. /// <param name="mindId">The mind id.</param>
  400. /// <param name="mind">The returned mind.</param>
  401. /// <param name="container">Mind component on <paramref name="uid"/> to get the mind from.</param>
  402. /// <returns>True if mind found. False if not.</returns>
  403. public bool TryGetMind(
  404. EntityUid uid,
  405. out EntityUid mindId,
  406. [NotNullWhen(true)] out MindComponent? mind,
  407. MindContainerComponent? container = null,
  408. VisitingMindComponent? visitingmind = null)
  409. {
  410. mindId = default;
  411. mind = null;
  412. if (!Resolve(uid, ref container, false))
  413. return false;
  414. if (!container.HasMind)
  415. {
  416. // The container has no mind. Check for a visiting mind...
  417. if (!Resolve(uid, ref visitingmind, false))
  418. return false;
  419. mindId = visitingmind.MindId ?? default;
  420. return TryComp(mindId, out mind);
  421. }
  422. mindId = container.Mind ?? default;
  423. return TryComp(mindId, out mind);
  424. }
  425. // TODO MIND make this return a nullable EntityUid or Entity<MindComponent>
  426. public bool TryGetMind(
  427. ICommonSession? player,
  428. out EntityUid mindId,
  429. [NotNullWhen(true)] out MindComponent? mind)
  430. {
  431. if (player == null)
  432. {
  433. mindId = default;
  434. mind = null;
  435. return false;
  436. }
  437. if (TryGetMind(player.UserId, out var mindUid, out mind))
  438. {
  439. mindId = mindUid.Value;
  440. return true;
  441. }
  442. mindId = default;
  443. return false;
  444. }
  445. /// <summary>
  446. /// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the
  447. /// entity that any mind is connected to, except as a side effect of the fact that it may change a player's
  448. /// attached entity. E.g., ghosts get deleted.
  449. /// </summary>
  450. public virtual void SetUserId(EntityUid mindId, NetUserId? userId, MindComponent? mind = null)
  451. {
  452. }
  453. /// <summary>
  454. /// True if this Mind is 'sufficiently dead' IC (Objectives, EndText).
  455. /// Note that this is *IC logic*, it's not necessarily tied to any specific truth.
  456. /// "If administrators decide that zombies are dead, this returns true for zombies."
  457. /// (Maybe you were looking for the action blocker system?)
  458. /// </summary>
  459. public bool IsCharacterDeadIc(MindComponent mind)
  460. {
  461. if (mind.OwnedEntity is { } owned)
  462. {
  463. var ev = new GetCharactedDeadIcEvent(null);
  464. RaiseLocalEvent(owned, ref ev);
  465. if (ev.Dead != null)
  466. return ev.Dead.Value;
  467. }
  468. return IsCharacterDeadPhysically(mind);
  469. }
  470. /// <summary>
  471. /// A string to represent the mind for logging
  472. /// </summary>
  473. public string MindOwnerLoggingString(MindComponent mind)
  474. {
  475. if (mind.OwnedEntity != null)
  476. return ToPrettyString(mind.OwnedEntity.Value);
  477. if (mind.UserId != null)
  478. return mind.UserId.Value.ToString();
  479. return "(originally " + mind.OriginalOwnerUserId + ")";
  480. }
  481. public string? GetCharacterName(NetUserId userId)
  482. {
  483. return TryGetMind(userId, out _, out var mind) ? mind.CharacterName : null;
  484. }
  485. /// <summary>
  486. /// Returns a list of every living humanoid player's minds, except for a single one which is exluded.
  487. /// </summary>
  488. public HashSet<Entity<MindComponent>> GetAliveHumans(EntityUid? exclude = null)
  489. {
  490. var allHumans = new HashSet<Entity<MindComponent>>();
  491. // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen
  492. var query = EntityQueryEnumerator<MobStateComponent, HumanoidAppearanceComponent>();
  493. while (query.MoveNext(out var uid, out var mobState, out _))
  494. {
  495. // the player needs to have a mind and not be the excluded one +
  496. // the player has to be alive
  497. if (!TryGetMind(uid, out var mind, out var mindComp) || mind == exclude || !_mobState.IsAlive(uid, mobState))
  498. continue;
  499. allHumans.Add(new Entity<MindComponent>(mind, mindComp));
  500. }
  501. return allHumans;
  502. }
  503. }
  504. /// <summary>
  505. /// Raised on an entity to determine whether or not they are "dead" in IC-logic.
  506. /// If not handled, then it will simply check if they are dead physically.
  507. /// </summary>
  508. /// <param name="Dead"></param>
  509. [ByRefEvent]
  510. public record struct GetCharactedDeadIcEvent(bool? Dead);