1
0

ObjectivesSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. using Content.Server.GameTicking;
  2. using Content.Server.Shuttles.Systems;
  3. using Content.Shared.Cuffs.Components;
  4. using Content.Shared.GameTicking.Components;
  5. using Content.Shared.Mind;
  6. using Content.Shared.Objectives.Components;
  7. using Content.Shared.Objectives.Systems;
  8. using Content.Shared.Random;
  9. using Content.Shared.Random.Helpers;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Random;
  12. using System.Linq;
  13. using System.Text;
  14. using Content.Server.Objectives.Commands;
  15. using Content.Shared.CCVar;
  16. using Content.Shared.Prototypes;
  17. using Content.Shared.Roles.Jobs;
  18. using Robust.Server.Player;
  19. using Robust.Shared.Configuration;
  20. using Robust.Shared.Utility;
  21. namespace Content.Server.Objectives;
  22. public sealed class ObjectivesSystem : SharedObjectivesSystem
  23. {
  24. [Dependency] private readonly GameTicker _gameTicker = default!;
  25. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  26. [Dependency] private readonly IPlayerManager _player = default!;
  27. [Dependency] private readonly IRobustRandom _random = default!;
  28. [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
  29. [Dependency] private readonly SharedJobSystem _job = default!;
  30. [Dependency] private readonly IConfigurationManager _cfg = default!;
  31. private IEnumerable<string>? _objectives;
  32. private bool _showGreentext;
  33. public override void Initialize()
  34. {
  35. base.Initialize();
  36. SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
  37. Subs.CVar(_cfg, CCVars.GameShowGreentext, value => _showGreentext = value, true);
  38. _prototypeManager.PrototypesReloaded += CreateCompletions;
  39. }
  40. public override void Shutdown()
  41. {
  42. base.Shutdown();
  43. _prototypeManager.PrototypesReloaded -= CreateCompletions;
  44. }
  45. /// <summary>
  46. /// Adds objective text for each game rule's players on round end.
  47. /// </summary>
  48. private void OnRoundEndText(RoundEndTextAppendEvent ev)
  49. {
  50. // go through each gamerule getting data for the roundend summary.
  51. var summaries = new Dictionary<string, Dictionary<string, List<(EntityUid, string)>>>();
  52. var query = EntityQueryEnumerator<GameRuleComponent>();
  53. while (query.MoveNext(out var uid, out var gameRule))
  54. {
  55. if (!_gameTicker.IsGameRuleAdded(uid, gameRule))
  56. continue;
  57. var info = new ObjectivesTextGetInfoEvent(new List<(EntityUid, string)>(), string.Empty);
  58. RaiseLocalEvent(uid, ref info);
  59. if (info.Minds.Count == 0)
  60. continue;
  61. // first group the gamerules by their agents, for example 2 different dragons
  62. var agent = info.AgentName;
  63. if (!summaries.ContainsKey(agent))
  64. summaries[agent] = new Dictionary<string, List<(EntityUid, string)>>();
  65. var prepend = new ObjectivesTextPrependEvent("");
  66. RaiseLocalEvent(uid, ref prepend);
  67. // next group them by their prepended texts
  68. // for example with traitor rule, group them by the codewords they share
  69. var summary = summaries[agent];
  70. if (summary.ContainsKey(prepend.Text))
  71. {
  72. // same prepended text (usually empty) so combine them
  73. summary[prepend.Text].AddRange(info.Minds);
  74. }
  75. else
  76. {
  77. summary[prepend.Text] = info.Minds;
  78. }
  79. }
  80. // convert the data into summary text
  81. foreach (var (agent, summary) in summaries)
  82. {
  83. // first get the total number of players that were in these game rules combined
  84. var total = 0;
  85. var totalInCustody = 0;
  86. foreach (var (_, minds) in summary)
  87. {
  88. total += minds.Count;
  89. totalInCustody += minds.Where(pair => IsInCustody(pair.Item1)).Count();
  90. }
  91. var result = new StringBuilder();
  92. result.AppendLine(Loc.GetString("objectives-round-end-result", ("count", total), ("agent", agent)));
  93. if (agent == Loc.GetString("traitor-round-end-agent-name"))
  94. {
  95. result.AppendLine(Loc.GetString("objectives-round-end-result-in-custody", ("count", total), ("custody", totalInCustody), ("agent", agent)));
  96. }
  97. // next add all the players with its own prepended text
  98. foreach (var (prepend, minds) in summary)
  99. {
  100. if (prepend != string.Empty)
  101. result.Append(prepend);
  102. // add space between the start text and player list
  103. result.AppendLine();
  104. AddSummary(result, agent, minds);
  105. }
  106. ev.AddLine(result.AppendLine().ToString());
  107. }
  108. }
  109. private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds)
  110. {
  111. var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>();
  112. foreach (var (mindId, name) in minds)
  113. {
  114. if (!TryComp<MindComponent>(mindId, out var mind))
  115. continue;
  116. var title = GetTitle((mindId, mind), name);
  117. var custody = IsInCustody(mindId, mind) ? Loc.GetString("objectives-in-custody") : string.Empty;
  118. var objectives = mind.Objectives;
  119. if (objectives.Count == 0)
  120. {
  121. agentSummaries.Add((Loc.GetString("objectives-no-objectives", ("custody", custody), ("title", title), ("agent", agent)), 0f, 0));
  122. continue;
  123. }
  124. var completedObjectives = 0;
  125. var totalObjectives = 0;
  126. var agentSummary = new StringBuilder();
  127. agentSummary.AppendLine(Loc.GetString("objectives-with-objectives", ("custody", custody), ("title", title), ("agent", agent)));
  128. foreach (var objectiveGroup in objectives.GroupBy(o => Comp<ObjectiveComponent>(o).LocIssuer))
  129. {
  130. //TO DO:
  131. //check for the right group here. Getting the target issuer is easy: objectiveGroup.Key
  132. //It should be compared to the type of the group's issuer.
  133. agentSummary.AppendLine(objectiveGroup.Key);
  134. foreach (var objective in objectiveGroup)
  135. {
  136. var info = GetInfo(objective, mindId, mind);
  137. if (info == null)
  138. continue;
  139. var objectiveTitle = info.Value.Title;
  140. var progress = info.Value.Progress;
  141. totalObjectives++;
  142. agentSummary.Append("- ");
  143. if (!_showGreentext)
  144. {
  145. agentSummary.AppendLine(objectiveTitle);
  146. }
  147. else if (progress > 0.99f)
  148. {
  149. agentSummary.AppendLine(Loc.GetString(
  150. "objectives-objective-success",
  151. ("objective", objectiveTitle),
  152. ("markupColor", "green")
  153. ));
  154. completedObjectives++;
  155. }
  156. else
  157. {
  158. agentSummary.AppendLine(Loc.GetString(
  159. "objectives-objective-fail",
  160. ("objective", objectiveTitle),
  161. ("progress", (int) (progress * 100)),
  162. ("markupColor", "red")
  163. ));
  164. }
  165. }
  166. }
  167. var successRate = totalObjectives > 0 ? (float) completedObjectives / totalObjectives : 0f;
  168. agentSummaries.Add((agentSummary.ToString(), successRate, completedObjectives));
  169. }
  170. var sortedAgents = agentSummaries.OrderByDescending(x => x.successRate)
  171. .ThenByDescending(x => x.completedObjectives);
  172. foreach (var (summary, _, _) in sortedAgents)
  173. {
  174. result.AppendLine(summary);
  175. }
  176. }
  177. public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, ProtoId<WeightedRandomPrototype> objectiveGroupProto, float maxDifficulty)
  178. {
  179. if (!_prototypeManager.TryIndex(objectiveGroupProto, out var groupsProto))
  180. {
  181. Log.Error($"Tried to get a random objective, but can't index WeightedRandomPrototype {objectiveGroupProto}");
  182. return null;
  183. }
  184. // Make a copy of the weights so we don't trash the prototype by removing entries
  185. var groups = groupsProto.Weights.ShallowClone();
  186. while (_random.TryPickAndTake(groups, out var groupName))
  187. {
  188. if (!_prototypeManager.TryIndex<WeightedRandomPrototype>(groupName, out var group))
  189. {
  190. Log.Error($"Couldn't index objective group prototype {groupName}");
  191. return null;
  192. }
  193. var objectives = group.Weights.ShallowClone();
  194. while (_random.TryPickAndTake(objectives, out var objectiveProto))
  195. {
  196. if (TryCreateObjective((mindId, mind), objectiveProto, out var objective)
  197. && Comp<ObjectiveComponent>(objective.Value).Difficulty <= maxDifficulty)
  198. return objective;
  199. }
  200. }
  201. return null;
  202. }
  203. /// <summary>
  204. /// Returns whether a target is considered 'in custody' (cuffed on the shuttle).
  205. /// </summary>
  206. private bool IsInCustody(EntityUid mindId, MindComponent? mind = null)
  207. {
  208. if (!Resolve(mindId, ref mind))
  209. return false;
  210. // Ghosting will not save you
  211. bool originalEntityInCustody = false;
  212. EntityUid? originalEntity = GetEntity(mind.OriginalOwnedEntity);
  213. if (originalEntity.HasValue && originalEntity != mind.OwnedEntity)
  214. {
  215. originalEntityInCustody = TryComp<CuffableComponent>(originalEntity, out var origCuffed) && origCuffed.CuffedHandCount > 0
  216. && _emergencyShuttle.IsTargetEscaping(originalEntity.Value);
  217. }
  218. return originalEntityInCustody || (TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0
  219. && _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value));
  220. }
  221. /// <summary>
  222. /// Get the title for a player's mind used in round end.
  223. /// Pass in the original entity name which is shown alongside username.
  224. /// </summary>
  225. public string GetTitle(Entity<MindComponent?> mind, string name)
  226. {
  227. if (Resolve(mind, ref mind.Comp) &&
  228. mind.Comp.OriginalOwnerUserId != null &&
  229. _player.TryGetPlayerData(mind.Comp.OriginalOwnerUserId.Value, out var sessionData))
  230. {
  231. var username = sessionData.UserName;
  232. var nameWithJobMaybe = name;
  233. if (_job.MindTryGetJobName(mind, out var jobName))
  234. nameWithJobMaybe += ", " + jobName;
  235. return Loc.GetString("objectives-player-user-named", ("user", username), ("name", nameWithJobMaybe));
  236. }
  237. return Loc.GetString("objectives-player-named", ("name", name));
  238. }
  239. private void CreateCompletions(PrototypesReloadedEventArgs unused)
  240. {
  241. CreateCompletions();
  242. }
  243. /// <summary>
  244. /// Get all objective prototypes by their IDs.
  245. /// This is used for completions in <see cref="AddObjectiveCommand"/>
  246. /// </summary>
  247. public IEnumerable<string> Objectives()
  248. {
  249. if (_objectives == null)
  250. CreateCompletions();
  251. return _objectives!;
  252. }
  253. private void CreateCompletions()
  254. {
  255. _objectives = _prototypeManager.EnumeratePrototypes<EntityPrototype>()
  256. .Where(p => p.HasComponent<ObjectiveComponent>())
  257. .Select(p => p.ID)
  258. .Order();
  259. }
  260. }
  261. /// <summary>
  262. /// Raised on the game rule to get info for any objectives.
  263. /// If its minds list is set then the players will have their objectives shown in the round end text.
  264. /// AgentName is the generic name for a player in the list.
  265. /// </summary>
  266. /// <remarks>
  267. /// The objectives system already checks if the game rule is added so you don't need to check that in this event's handler.
  268. /// </remarks>
  269. [ByRefEvent]
  270. public record struct ObjectivesTextGetInfoEvent(List<(EntityUid, string)> Minds, string AgentName);
  271. /// <summary>
  272. /// Raised on the game rule before text for each agent's objectives is added, letting you prepend something.
  273. /// </summary>
  274. [ByRefEvent]
  275. public record struct ObjectivesTextPrependEvent(string Text);