GameTicker.GameRule.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. using System.Linq;
  2. using Content.Server.Administration;
  3. using Content.Server.GameTicking.Rules.Components;
  4. using Content.Shared.Administration;
  5. using Content.Shared.Database;
  6. using Content.Shared.GameTicking.Components;
  7. using Content.Shared.Prototypes;
  8. using JetBrains.Annotations;
  9. using Robust.Shared.Console;
  10. using Robust.Shared.Map;
  11. using Robust.Shared.Prototypes;
  12. using Robust.Shared.Localization;
  13. namespace Content.Server.GameTicking;
  14. public sealed partial class GameTicker
  15. {
  16. [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new();
  17. /// <summary>
  18. /// A list storing the start times of all game rules that have been started this round.
  19. /// Game rules can be started and stopped at any time, including midround.
  20. /// </summary>
  21. public IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules;
  22. private void InitializeGameRules()
  23. {
  24. // Add game rule command.
  25. _consoleHost.RegisterCommand("addgamerule",
  26. string.Empty,
  27. "addgamerule <rules>",
  28. AddGameRuleCommand,
  29. AddGameRuleCompletions);
  30. // End game rule command.
  31. _consoleHost.RegisterCommand("endgamerule",
  32. string.Empty,
  33. "endgamerule <rules>",
  34. EndGameRuleCommand,
  35. EndGameRuleCompletions);
  36. // Clear game rules command.
  37. _consoleHost.RegisterCommand("cleargamerules",
  38. string.Empty,
  39. "cleargamerules",
  40. ClearGameRulesCommand);
  41. // List game rules command.
  42. var localizedHelp = Loc.GetString("listgamerules-command-help");
  43. _consoleHost.RegisterCommand("listgamerules",
  44. string.Empty,
  45. $"listgamerules - {localizedHelp}",
  46. ListGameRuleCommand);
  47. }
  48. private void ShutdownGameRules()
  49. {
  50. _consoleHost.UnregisterCommand("addgamerule");
  51. _consoleHost.UnregisterCommand("endgamerule");
  52. _consoleHost.UnregisterCommand("cleargamerules");
  53. _consoleHost.UnregisterCommand("listgamerules");
  54. }
  55. /// <summary>
  56. /// Adds a game rule to the list, but does not
  57. /// start it yet, instead waiting until the rule is actually started by other code (usually roundstart)
  58. /// </summary>
  59. /// <returns>The entity for the added gamerule</returns>
  60. public EntityUid AddGameRule(string ruleId)
  61. {
  62. var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace);
  63. _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}");
  64. _adminLogger.Add(LogType.EventStarted, $"Added game rule {ToPrettyString(ruleEntity)}");
  65. var str = Loc.GetString("station-event-system-run-event", ("eventName", ToPrettyString(ruleEntity)));
  66. #if DEBUG
  67. _chatManager.SendAdminAlert(str);
  68. #else
  69. if (RunLevel == GameRunLevel.InRound) // avoids telling admins the round type before it starts so that can be handled elsewhere.
  70. {
  71. _chatManager.SendAdminAlert(str);
  72. }
  73. #endif
  74. Log.Info(str);
  75. var ev = new GameRuleAddedEvent(ruleEntity, ruleId);
  76. RaiseLocalEvent(ruleEntity, ref ev, true);
  77. var currentTime = RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration();
  78. if (!HasComp<RoundstartStationVariationRuleComponent>(ruleEntity) && !HasComp<StationVariationPassRuleComponent>(ruleEntity))
  79. {
  80. _allPreviousGameRules.Add((currentTime, ruleId + " (Pending)"));
  81. }
  82. return ruleEntity;
  83. }
  84. /// <summary>
  85. /// Game rules can be 'started' separately from being added. 'Starting' them usually
  86. /// happens at round start while they can be added and removed before then.
  87. /// </summary>
  88. public bool StartGameRule(string ruleId)
  89. {
  90. return StartGameRule(ruleId, out _);
  91. }
  92. /// <summary>
  93. /// Game rules can be 'started' separately from being added. 'Starting' them usually
  94. /// happens at round start while they can be added and removed before then.
  95. /// </summary>
  96. public bool StartGameRule(string ruleId, out EntityUid ruleEntity)
  97. {
  98. ruleEntity = AddGameRule(ruleId);
  99. return StartGameRule(ruleEntity);
  100. }
  101. /// <summary>
  102. /// Game rules can be 'started' separately from being added. 'Starting' them usually
  103. /// happens at round start while they can be added and removed before then.
  104. /// </summary>
  105. public bool StartGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null)
  106. {
  107. if (!Resolve(ruleEntity, ref ruleData))
  108. ruleData ??= EnsureComp<GameRuleComponent>(ruleEntity);
  109. // can't start an already active rule
  110. if (HasComp<ActiveGameRuleComponent>(ruleEntity) || HasComp<EndedGameRuleComponent>(ruleEntity))
  111. return false;
  112. if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
  113. return false;
  114. // If we already have it, then we just skip the delay as it has already happened.
  115. if (!RemComp<DelayedStartRuleComponent>(ruleEntity) && ruleData.Delay != null)
  116. {
  117. var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom));
  118. if (delayTime > TimeSpan.Zero)
  119. {
  120. _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
  121. _adminLogger.Add(LogType.EventStarted,
  122. $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}");
  123. var delayed = EnsureComp<DelayedStartRuleComponent>(ruleEntity);
  124. delayed.RuleStartTime = _gameTiming.CurTime + (delayTime);
  125. return true;
  126. }
  127. }
  128. var currentTime = RunLevel == GameRunLevel.PreRoundLobby ? TimeSpan.Zero : RoundDuration();
  129. // Remove the first occurrence of the pending entry before adding the started entry
  130. var pendingRuleIndex = _allPreviousGameRules.FindIndex(rule => rule.Item2 == id + " (Pending)");
  131. if (pendingRuleIndex >= 0)
  132. {
  133. _allPreviousGameRules.RemoveAt(pendingRuleIndex);
  134. }
  135. if (!HasComp<RoundstartStationVariationRuleComponent>(ruleEntity) && !HasComp<StationVariationPassRuleComponent>(ruleEntity))
  136. {
  137. _allPreviousGameRules.Add((currentTime, id));
  138. }
  139. _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}");
  140. _adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}");
  141. EnsureComp<ActiveGameRuleComponent>(ruleEntity);
  142. ruleData.ActivatedAt = _gameTiming.CurTime;
  143. var ev = new GameRuleStartedEvent(ruleEntity, id);
  144. RaiseLocalEvent(ruleEntity, ref ev, true);
  145. return true;
  146. }
  147. /// <summary>
  148. /// Ends a game rule.
  149. /// </summary>
  150. [PublicAPI]
  151. public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null)
  152. {
  153. if (!Resolve(ruleEntity, ref ruleData))
  154. return false;
  155. // don't end it multiple times
  156. if (HasComp<EndedGameRuleComponent>(ruleEntity))
  157. return false;
  158. if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
  159. return false;
  160. RemComp<ActiveGameRuleComponent>(ruleEntity);
  161. EnsureComp<EndedGameRuleComponent>(ruleEntity);
  162. _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}");
  163. _adminLogger.Add(LogType.EventStopped, $"Ended game rule {ToPrettyString(ruleEntity)}");
  164. var ev = new GameRuleEndedEvent(ruleEntity, id);
  165. RaiseLocalEvent(ruleEntity, ref ev, true);
  166. return true;
  167. }
  168. /// <summary>
  169. /// Returns true if a game rule with the given component has been added.
  170. /// </summary>
  171. public bool IsGameRuleAdded<T>()
  172. where T : IComponent
  173. {
  174. var query = EntityQueryEnumerator<T, GameRuleComponent>();
  175. while (query.MoveNext(out var uid, out _, out _))
  176. {
  177. if (HasComp<EndedGameRuleComponent>(uid))
  178. continue;
  179. return true;
  180. }
  181. return false;
  182. }
  183. public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null)
  184. {
  185. return Resolve(ruleEntity, ref component) && !HasComp<EndedGameRuleComponent>(ruleEntity);
  186. }
  187. public bool IsGameRuleAdded(string rule)
  188. {
  189. foreach (var ruleEntity in GetAddedGameRules())
  190. {
  191. if (MetaData(ruleEntity).EntityPrototype?.ID == rule)
  192. return true;
  193. }
  194. return false;
  195. }
  196. /// <summary>
  197. /// Returns true if a game rule with the given component is active..
  198. /// </summary>
  199. public bool IsGameRuleActive<T>()
  200. where T : IComponent
  201. {
  202. var query = EntityQueryEnumerator<T, ActiveGameRuleComponent, GameRuleComponent>();
  203. // out, damned underscore!!!
  204. while (query.MoveNext(out _, out _, out _, out _))
  205. {
  206. return true;
  207. }
  208. return false;
  209. }
  210. public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null)
  211. {
  212. return Resolve(ruleEntity, ref component) && HasComp<ActiveGameRuleComponent>(ruleEntity);
  213. }
  214. public bool IsGameRuleActive(string rule)
  215. {
  216. foreach (var ruleEntity in GetActiveGameRules())
  217. {
  218. if (MetaData(ruleEntity).EntityPrototype?.ID == rule)
  219. return true;
  220. }
  221. return false;
  222. }
  223. public void ClearGameRules()
  224. {
  225. foreach (var rule in GetAddedGameRules())
  226. {
  227. EndGameRule(rule);
  228. }
  229. }
  230. /// <summary>
  231. /// Gets all the gamerule entities which are currently active.
  232. /// </summary>
  233. public IEnumerable<EntityUid> GetAddedGameRules()
  234. {
  235. var query = EntityQueryEnumerator<GameRuleComponent>();
  236. while (query.MoveNext(out var uid, out var ruleData))
  237. {
  238. if (IsGameRuleAdded(uid, ruleData))
  239. yield return uid;
  240. }
  241. }
  242. /// <summary>
  243. /// Gets all the gamerule entities which are currently active.
  244. /// </summary>
  245. public IEnumerable<EntityUid> GetActiveGameRules()
  246. {
  247. var query = EntityQueryEnumerator<ActiveGameRuleComponent, GameRuleComponent>();
  248. while (query.MoveNext(out var uid, out _, out _))
  249. {
  250. yield return uid;
  251. }
  252. }
  253. /// <summary>
  254. /// Gets all gamerule prototypes
  255. /// </summary>
  256. public IEnumerable<EntityPrototype> GetAllGameRulePrototypes()
  257. {
  258. foreach (var proto in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
  259. {
  260. if (proto.Abstract)
  261. continue;
  262. if (proto.HasComponent<GameRuleComponent>())
  263. yield return proto;
  264. }
  265. }
  266. private void UpdateGameRules()
  267. {
  268. var query = EntityQueryEnumerator<DelayedStartRuleComponent, GameRuleComponent>();
  269. while (query.MoveNext(out var uid, out var delay, out var rule))
  270. {
  271. if (_gameTiming.CurTime < delay.RuleStartTime)
  272. continue;
  273. StartGameRule(uid, rule);
  274. }
  275. }
  276. #region Command Implementations
  277. [AdminCommand(AdminFlags.Fun)]
  278. private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
  279. {
  280. if (args.Length == 0)
  281. return;
  282. foreach (var rule in args)
  283. {
  284. if (!_prototypeManager.HasIndex(rule))
  285. {
  286. shell.WriteError($"Invalid game rule {rule} was skipped.");
  287. continue;
  288. }
  289. if (shell.Player != null)
  290. {
  291. _adminLogger.Add(LogType.EventStarted, $"{shell.Player} tried to add game rule [{rule}] via command");
  292. _chatManager.SendAdminAnnouncement(Loc.GetString("add-gamerule-admin", ("rule", rule), ("admin", shell.Player)));
  293. }
  294. else
  295. {
  296. _adminLogger.Add(LogType.EventStarted, $"Unknown tried to add game rule [{rule}] via command");
  297. }
  298. var ent = AddGameRule(rule);
  299. // Start rule if we're already in the middle of a round
  300. if (RunLevel == GameRunLevel.InRound)
  301. {
  302. StartGameRule(ent);
  303. }
  304. }
  305. }
  306. private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args)
  307. {
  308. return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), "<rule>");
  309. }
  310. [AdminCommand(AdminFlags.Fun)]
  311. private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
  312. {
  313. if (args.Length == 0)
  314. return;
  315. foreach (var rule in args)
  316. {
  317. if (!NetEntity.TryParse(rule, out var ruleEntNet) || !TryGetEntity(ruleEntNet, out var ruleEnt))
  318. continue;
  319. if (shell.Player != null)
  320. {
  321. _adminLogger.Add(LogType.EventStopped, $"{shell.Player} tried to end game rule [{rule}] via command");
  322. }
  323. else
  324. {
  325. _adminLogger.Add(LogType.EventStopped, $"Unknown tried to end game rule [{rule}] via command");
  326. }
  327. EndGameRule(ruleEnt.Value);
  328. }
  329. }
  330. private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args)
  331. {
  332. var opts = GetAddedGameRules().Select(ent => new CompletionOption(ent.ToString(), ToPrettyString(ent))).ToList();
  333. return CompletionResult.FromHintOptions(opts, "<added rule>");
  334. }
  335. [AdminCommand(AdminFlags.Fun)]
  336. private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args)
  337. {
  338. ClearGameRules();
  339. }
  340. [AdminCommand(AdminFlags.Admin)]
  341. private void ListGameRuleCommand(IConsoleShell shell, string argstr, string[] args)
  342. {
  343. _sawmill.Info($"{shell.Player} tried to get list of game rules via command");
  344. _adminLogger.Add(LogType.Action, $"{shell.Player} tried to get list of game rules via command");
  345. var message = GetGameRulesListMessage(false);
  346. shell.WriteLine(message);
  347. }
  348. private string GetGameRulesListMessage(bool forChatWindow)
  349. {
  350. if (_allPreviousGameRules.Count > 0)
  351. {
  352. var sortedRules = _allPreviousGameRules.OrderBy(rule => rule.Item1).ToList();
  353. var message = "\n";
  354. if (!forChatWindow)
  355. {
  356. var header = Loc.GetString("list-gamerule-admin-header");
  357. message += $"\n{header}\n";
  358. message += "|------------|------------------\n";
  359. }
  360. foreach (var (time, rule) in sortedRules)
  361. {
  362. var formattedTime = time.ToString(@"hh\:mm\:ss");
  363. message += $"| {formattedTime,-10} | {rule,-16} \n";
  364. }
  365. return message;
  366. }
  367. else
  368. {
  369. return Loc.GetString("list-gamerule-admin-no-rules");
  370. }
  371. }
  372. #endregion
  373. }