using System.Linq; using Content.Server.Administration; using Content.Server.GameTicking.Components; using Content.Shared.Administration; using Content.Shared.Database; using Content.Shared.Prototypes; using JetBrains.Annotations; using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Prototypes; namespace Content.Server.GameTicking; public sealed partial class GameTicker { [ViewVariables] private readonly List<(TimeSpan, string)> _allPreviousGameRules = new(); /// /// A list storing the start times of all game rules that have been started this round. /// Game rules can be started and stopped at any time, including midround. /// public IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules; private void InitializeGameRules() { // Add game rule command. _consoleHost.RegisterCommand("addgamerule", string.Empty, "addgamerule ", AddGameRuleCommand, AddGameRuleCompletions); // End game rule command. _consoleHost.RegisterCommand("endgamerule", string.Empty, "endgamerule ", EndGameRuleCommand, EndGameRuleCompletions); // Clear game rules command. _consoleHost.RegisterCommand("cleargamerules", string.Empty, "cleargamerules", ClearGameRulesCommand); } private void ShutdownGameRules() { _consoleHost.UnregisterCommand("addgamerule"); _consoleHost.UnregisterCommand("endgamerule"); _consoleHost.UnregisterCommand("cleargamerules"); } /// /// Adds a game rule to the list, but does not /// start it yet, instead waiting until the rule is actually started by other code (usually roundstart) /// /// The entity for the added gamerule public EntityUid AddGameRule(string ruleId) { var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace); _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}"); _adminLogger.Add(LogType.EventStarted, $"Added game rule {ToPrettyString(ruleEntity)}"); var ev = new GameRuleAddedEvent(ruleEntity, ruleId); RaiseLocalEvent(ruleEntity, ref ev, true); return ruleEntity; } /// /// Game rules can be 'started' separately from being added. 'Starting' them usually /// happens at round start while they can be added and removed before then. /// public bool StartGameRule(string ruleId) { return StartGameRule(ruleId, out _); } /// /// Game rules can be 'started' separately from being added. 'Starting' them usually /// happens at round start while they can be added and removed before then. /// public bool StartGameRule(string ruleId, out EntityUid ruleEntity) { ruleEntity = AddGameRule(ruleId); return StartGameRule(ruleEntity); } /// /// Game rules can be 'started' separately from being added. 'Starting' them usually /// happens at round start while they can be added and removed before then. /// public bool StartGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) { if (!Resolve(ruleEntity, ref ruleData)) ruleData ??= EnsureComp(ruleEntity); // can't start an already active rule if (HasComp(ruleEntity) || HasComp(ruleEntity)) return false; if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up return false; // If we already have it, then we just skip the delay as it has already happened. if (!RemComp(ruleEntity) && ruleData.Delay != null) { var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom)); if (delayTime > TimeSpan.Zero) { _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); _adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); var delayed = EnsureComp(ruleEntity); delayed.RuleStartTime = _gameTiming.CurTime + (delayTime); return true; } } _allPreviousGameRules.Add((RoundDuration(), id)); _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}"); _adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}"); EnsureComp(ruleEntity); ruleData.ActivatedAt = _gameTiming.CurTime; var ev = new GameRuleStartedEvent(ruleEntity, id); RaiseLocalEvent(ruleEntity, ref ev, true); return true; } /// /// Ends a game rule. /// [PublicAPI] public bool EndGameRule(EntityUid ruleEntity, GameRuleComponent? ruleData = null) { if (!Resolve(ruleEntity, ref ruleData)) return false; // don't end it multiple times if (HasComp(ruleEntity)) return false; if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up return false; RemComp(ruleEntity); EnsureComp(ruleEntity); _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}"); _adminLogger.Add(LogType.EventStopped, $"Ended game rule {ToPrettyString(ruleEntity)}"); var ev = new GameRuleEndedEvent(ruleEntity, id); RaiseLocalEvent(ruleEntity, ref ev, true); return true; } /// /// Returns true if a game rule with the given component has been added. /// public bool IsGameRuleAdded() where T : IComponent { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _, out _)) { if (HasComp(uid)) continue; return true; } return false; } public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null) { return Resolve(ruleEntity, ref component) && !HasComp(ruleEntity); } public bool IsGameRuleAdded(string rule) { foreach (var ruleEntity in GetAddedGameRules()) { if (MetaData(ruleEntity).EntityPrototype?.ID == rule) return true; } return false; } /// /// Returns true if a game rule with the given component is active.. /// public bool IsGameRuleActive() where T : IComponent { var query = EntityQueryEnumerator(); // out, damned underscore!!! while (query.MoveNext(out _, out _, out _, out _)) { return true; } return false; } public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null) { return Resolve(ruleEntity, ref component) && HasComp(ruleEntity); } public bool IsGameRuleActive(string rule) { foreach (var ruleEntity in GetActiveGameRules()) { if (MetaData(ruleEntity).EntityPrototype?.ID == rule) return true; } return false; } public void ClearGameRules() { foreach (var rule in GetAddedGameRules()) { EndGameRule(rule); } } /// /// Gets all the gamerule entities which are currently active. /// public IEnumerable GetAddedGameRules() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var ruleData)) { if (IsGameRuleAdded(uid, ruleData)) yield return uid; } } /// /// Gets all the gamerule entities which are currently active. /// public IEnumerable GetActiveGameRules() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out _, out _)) { yield return uid; } } /// /// Gets all gamerule prototypes /// public IEnumerable GetAllGameRulePrototypes() { foreach (var proto in _prototypeManager.EnumeratePrototypes()) { if (proto.Abstract) continue; if (proto.HasComponent()) yield return proto; } } private void UpdateGameRules() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var delay, out var rule)) { if (_gameTiming.CurTime < delay.RuleStartTime) continue; StartGameRule(uid, rule); } } #region Command Implementations [AdminCommand(AdminFlags.Fun)] private void AddGameRuleCommand(IConsoleShell shell, string argstr, string[] args) { if (args.Length == 0) return; foreach (var rule in args) { if (shell.Player != null) { _adminLogger.Add(LogType.EventStarted, $"{shell.Player} tried to add game rule [{rule}] via command"); } else { _adminLogger.Add(LogType.EventStarted, $"Unknown tried to add game rule [{rule}] via command"); } var ent = AddGameRule(rule); // Start rule if we're already in the middle of a round if(RunLevel == GameRunLevel.InRound) StartGameRule(ent); } } private CompletionResult AddGameRuleCompletions(IConsoleShell shell, string[] args) { return CompletionResult.FromHintOptions(GetAllGameRulePrototypes().Select(p => p.ID), ""); } [AdminCommand(AdminFlags.Fun)] private void EndGameRuleCommand(IConsoleShell shell, string argstr, string[] args) { if (args.Length == 0) return; foreach (var rule in args) { if (!NetEntity.TryParse(rule, out var ruleEntNet) || !TryGetEntity(ruleEntNet, out var ruleEnt)) continue; if (shell.Player != null) { _adminLogger.Add(LogType.EventStopped, $"{shell.Player} tried to end game rule [{rule}] via command"); } else { _adminLogger.Add(LogType.EventStopped, $"Unknown tried to end game rule [{rule}] via command"); } EndGameRule(ruleEnt.Value); } } private CompletionResult EndGameRuleCompletions(IConsoleShell shell, string[] args) { var opts = GetAddedGameRules().Select(ent => new CompletionOption(ent.ToString(), ToPrettyString(ent))).ToList(); return CompletionResult.FromHintOptions(opts, ""); } [AdminCommand(AdminFlags.Fun)] private void ClearGameRulesCommand(IConsoleShell shell, string argstr, string[] args) { ClearGameRules(); } #endregion }