diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 82e2872914..c04b8d6711 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -94,7 +94,7 @@ public sealed partial class GameTicker ruleData ??= EnsureComp(ruleEntity); // can't start an already active rule - if (ruleData.Active || ruleData.Ended) + if (HasComp(ruleEntity) || HasComp(ruleEntity)) return false; if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up @@ -103,8 +103,9 @@ public sealed partial class GameTicker _allPreviousGameRules.Add((RoundDuration(), id)); _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}"); - ruleData.Active = true; + EnsureComp(ruleEntity); ruleData.ActivatedAt = _gameTiming.CurTime; + var ev = new GameRuleStartedEvent(ruleEntity, id); RaiseLocalEvent(ruleEntity, ref ev, true); return true; @@ -120,14 +121,15 @@ public sealed partial class GameTicker return false; // don't end it multiple times - if (ruleData.Ended) + if (HasComp(ruleEntity)) return false; if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up return false; - ruleData.Active = false; - ruleData.Ended = true; + RemComp(ruleEntity); + EnsureComp(ruleEntity); + _sawmill.Info($"Ended game rule {ToPrettyString(ruleEntity)}"); var ev = new GameRuleEndedEvent(ruleEntity, id); @@ -137,7 +139,7 @@ public sealed partial class GameTicker public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null) { - return Resolve(ruleEntity, ref component) && !component.Ended; + return Resolve(ruleEntity, ref component) && !HasComp(ruleEntity); } public bool IsGameRuleAdded(string rule) @@ -153,7 +155,7 @@ public sealed partial class GameTicker public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null) { - return Resolve(ruleEntity, ref component) && component.Active; + return Resolve(ruleEntity, ref component) && HasComp(ruleEntity); } public bool IsGameRuleActive(string rule) @@ -193,11 +195,10 @@ public sealed partial class GameTicker /// public IEnumerable GetActiveGameRules() { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var ruleData)) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out _)) { - if (ruleData.Active) - yield return uid; + yield return uid; } } diff --git a/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs new file mode 100644 index 0000000000..956768bdd9 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Added to game rules before and removed before . +/// Mutually exclusive with . +/// +[RegisterComponent] +public sealed partial class ActiveGameRuleComponent : Component +{ +} diff --git a/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs new file mode 100644 index 0000000000..4484abd4d0 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Added to game rules before . +/// Mutually exclusive with . +/// +[RegisterComponent] +public sealed partial class EndedGameRuleComponent : Component +{ +} diff --git a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs index cc384b47d3..80a5ab340a 100644 --- a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs @@ -9,25 +9,17 @@ namespace Content.Server.GameTicking.Rules.Components; [RegisterComponent] public sealed partial class GameRuleComponent : Component { - /// - /// Whether or not the rule is active. - /// Is enabled after and disabled after - /// - [DataField("active")] - public bool Active; - /// /// Game time when game rule was activated /// - [DataField("activatedAt", customTypeSerializer:typeof(TimeOffsetSerializer))] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan ActivatedAt; /// - /// Whether or not the gamerule finished. - /// Used for tracking whether a non-active gamerule has been started before. + /// The minimum amount of players needed for this game rule. /// - [DataField("ended")] - public bool Ended; + [DataField] + public int MinPlayers; } /// diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index 8ddfd9c14b..55c14f88d4 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -4,7 +4,6 @@ using Content.Server.StationEvents.Events; using Content.Shared.Dataset; using Content.Shared.Roles; using Robust.Shared.Map; -using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; @@ -16,79 +15,80 @@ namespace Content.Server.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))] public sealed partial class NukeopsRuleComponent : Component { + // TODO Replace with GameRuleComponent.minPlayers /// /// The minimum needed amount of players /// - [DataField("minPlayers")] + [DataField] public int MinPlayers = 20; /// /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative /// - [DataField("playersPerOperative")] + [DataField] public int PlayersPerOperative = 10; - [DataField("maxOps")] - public int MaxOperatives = 5; + [DataField] + public int MaxOps = 5; /// /// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event. /// - [DataField("roundEndBehavior")] + [DataField] public RoundEndBehavior RoundEndBehavior = RoundEndBehavior.ShuttleCall; /// /// Text for shuttle call if RoundEndBehavior is ShuttleCall. /// - [DataField("roundEndTextSender")] + [DataField] public string RoundEndTextSender = "comms-console-announcement-title-centcom"; /// /// Text for shuttle call if RoundEndBehavior is ShuttleCall. /// - [DataField("roundEndTextShuttleCall")] + [DataField] public string RoundEndTextShuttleCall = "nuke-ops-no-more-threat-announcement-shuttle-call"; /// /// Text for announcement if RoundEndBehavior is ShuttleCall. Used if shuttle is already called /// - [DataField("roundEndTextAnnouncement")] + [DataField] public string RoundEndTextAnnouncement = "nuke-ops-no-more-threat-announcement"; /// /// Time to emergency shuttle to arrive if RoundEndBehavior is ShuttleCall. /// - [DataField("evacShuttleTime")] + [DataField] public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(10); /// /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event. /// - [DataField("spawnOutpost")] + [DataField] public bool SpawnOutpost = true; /// /// Whether or not nukie left their outpost /// - [DataField("leftOutpost")] - public bool LeftOutpost = false; + [DataField] + public bool LeftOutpost; /// /// Enables opportunity to get extra TC for war declaration /// - [DataField("canEnableWarOps")] + [DataField] public bool CanEnableWarOps = true; /// /// Indicates time when war has been declared, null if not declared /// - [DataField("warDeclaredTime", customTypeSerializer: typeof(TimeOffsetSerializer))] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan? WarDeclaredTime; /// /// This amount of TC will be given to each nukie /// - [DataField("warTCAmountPerNukie")] + [DataField] public int WarTCAmountPerNukie = 40; /// @@ -100,55 +100,55 @@ public sealed partial class NukeopsRuleComponent : Component /// /// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare /// - [DataField("warNukieArriveDelay")] + [DataField] public TimeSpan? WarNukieArriveDelay = TimeSpan.FromMinutes(15); /// /// Minimal operatives count for war declaration /// - [DataField("warDeclarationMinOps")] + [DataField] public int WarDeclarationMinOps = 4; - [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string SpawnPointPrototype = "SpawnPointNukies"; + [DataField] + public EntProtoId SpawnPointProto = "SpawnPointNukies"; - [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; + [DataField] + public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; - [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string CommanderRolePrototype = "NukeopsCommander"; + [DataField] + public ProtoId CommanderRoleProto = "NukeopsCommander"; - [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string OperativeRoleProto = "Nukeops"; + [DataField] + public ProtoId OperativeRoleProto = "Nukeops"; - [DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string MedicRoleProto = "NukeopsMedic"; + [DataField] + public ProtoId MedicRoleProto = "NukeopsMedic"; - [DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string CommanderStartGearPrototype = "SyndicateCommanderGearFull"; + [DataField] + public ProtoId CommanderStartGearProto = "SyndicateCommanderGearFull"; - [DataField("medicStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string MedicStartGearPrototype = "SyndicateOperativeMedicFull"; + [DataField] + public ProtoId MedicStartGearProto = "SyndicateOperativeMedicFull"; - [DataField("operativeStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string OperativeStartGearPrototype = "SyndicateOperativeGearFull"; + [DataField] + public ProtoId OperativeStartGearProto = "SyndicateOperativeGearFull"; - [DataField("eliteNames", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string EliteNames = "SyndicateNamesElite"; - [DataField("normalNames", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string NormalNames = "SyndicateNamesNormal"; - [DataField("outpostMap", customTypeSerializer: typeof(ResPathSerializer))] - public ResPath NukieOutpostMap = new("/Maps/nukieplanet.yml"); + [DataField(customTypeSerializer: typeof(ResPathSerializer))] + public ResPath OutpostMap = new("/Maps/nukieplanet.yml"); - [DataField("shuttleMap", customTypeSerializer: typeof(ResPathSerializer))] - public ResPath NukieShuttleMap = new("/Maps/infiltrator.yml"); + [DataField(customTypeSerializer: typeof(ResPathSerializer))] + public ResPath ShuttleMap = new("/Maps/infiltrator.yml"); - [DataField("winType")] + [DataField] public WinType WinType = WinType.Neutral; - [DataField("winConditions")] + [DataField] public List WinConditions = new (); public MapId? NukiePlanet; @@ -162,30 +162,30 @@ public sealed partial class NukeopsRuleComponent : Component /// /// Cached starting gear prototypes. /// - [DataField("startingGearPrototypes")] + [DataField] public Dictionary StartingGearPrototypes = new (); /// /// Cached operator name prototypes. /// - [DataField("operativeNames")] + [DataField] public Dictionary> OperativeNames = new(); /// /// Data to be used in for an operative once the Mind has been added. /// - [DataField("operativeMindPendingData")] + [DataField] public Dictionary OperativeMindPendingData = new(); /// /// Players who played as an operative at some point in the round. /// Stores the mind as well as the entity name /// - [DataField("operativePlayers")] + [DataField] public Dictionary OperativePlayers = new(); - [DataField("faction", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)] - public string Faction = default!; + [DataField(required: true)] + public ProtoId Faction = default!; } public enum WinType : byte diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs index e87660c2cc..b13f00f363 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -1,9 +1,11 @@ +using Content.Server.Chat.Managers; using Content.Server.GameTicking.Rules.Components; namespace Content.Server.GameTicking.Rules; public abstract partial class GameRuleSystem : EntitySystem where T : Component { + [Dependency] protected readonly IChatManager ChatManager = default!; [Dependency] protected readonly GameTicker GameTicker = default!; public override void Initialize() @@ -36,6 +38,7 @@ public abstract partial class GameRuleSystem : EntitySystem where T : Compone Ended(uid, component, ruleData, args); } + /// /// Called when the gamerule is added /// @@ -68,6 +71,36 @@ public abstract partial class GameRuleSystem : EntitySystem where T : Compone } + protected EntityQueryEnumerator QueryActiveRules() + { + return EntityQueryEnumerator(); + } + + protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out _, out _, out var gameRule)) + { + var minPlayers = gameRule.MinPlayers; + if (!ev.Forced && ev.Players.Length < minPlayers) + { + ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", + ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers), + ("presetName", localizedPresetName))); + ev.Cancel(); + continue; + } + + if (ev.Players.Length == 0) + { + ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready")); + ev.Cancel(); + } + } + + return !ev.Cancelled; + } + public override void Update(float frameTime) { base.Update(frameTime); diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index df9fbcc130..3bd3d13d27 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -595,7 +595,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // Basically copied verbatim from traitor code var playersPerOperative = nukeops.PlayersPerOperative; - var maxOperatives = nukeops.MaxOperatives; + var maxOperatives = nukeops.MaxOps; // Dear lord what is happening HERE. var everyone = new List(ev.PlayerPool); @@ -614,15 +614,15 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } var profile = ev.Profiles[player.UserId]; - if (profile.AntagPreferences.Contains(nukeops.OperativeRoleProto)) + if (profile.AntagPreferences.Contains(nukeops.OperativeRoleProto.Id)) { prefList.Add(player); } - if (profile.AntagPreferences.Contains(nukeops.MedicRoleProto)) + if (profile.AntagPreferences.Contains(nukeops.MedicRoleProto.Id)) { medPrefList.Add(player); } - if (profile.AntagPreferences.Contains(nukeops.CommanderRolePrototype)) + if (profile.AntagPreferences.Contains(nukeops.CommanderRoleProto.Id)) { cmdrPrefList.Add(player); } @@ -808,8 +808,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!component.SpawnOutpost) return true; - var path = component.NukieOutpostMap; - var shuttlePath = component.NukieShuttleMap; + var path = component.OutpostMap; + var shuttlePath = component.ShuttleMap; var mapId = _mapManager.CreateMap(); var options = new MapLoadOptions @@ -866,18 +866,18 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { case 0: name = Loc.GetString("nukeops-role-commander") + " " + _random.PickAndTake(component.OperativeNames[component.EliteNames]); - role = component.CommanderRolePrototype; - gear = component.CommanderStartGearPrototype; + role = component.CommanderRoleProto; + gear = component.CommanderStartGearProto; break; case 1: name = Loc.GetString("nukeops-role-agent") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]); role = component.MedicRoleProto; - gear = component.MedicStartGearPrototype; + gear = component.MedicStartGearProto; break; default: name = Loc.GetString("nukeops-role-operator") + " " + _random.PickAndTake(component.OperativeNames[component.NormalNames]); role = component.OperativeRoleProto; - gear = component.OperativeStartGearPrototype; + gear = component.OperativeStartGearProto; break; } @@ -915,7 +915,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // Forgive me for hardcoding prototypes foreach (var (_, meta, xform) in EntityQuery(true)) { - if (meta.EntityPrototype?.ID != component.SpawnPointPrototype) + if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id) continue; if (xform.ParentUid != component.NukieOutpost) @@ -981,7 +981,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } // Basically copied verbatim from traitor code var playersPerOperative = component.PlayersPerOperative; - var maxOperatives = component.MaxOperatives; + var maxOperatives = component.MaxOps; var playerPool = _playerManager.ServerSessions.ToList(); var numNukies = MathHelper.Clamp(playerPool.Count / playersPerOperative, 1, maxOperatives); @@ -1115,9 +1115,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // TODO: Loot table or something foreach (var proto in new[] { - component.CommanderStartGearPrototype, - component.MedicStartGearPrototype, - component.OperativeStartGearPrototype + component.CommanderStartGearProto, + component.MedicStartGearProto, + component.OperativeStartGearProto }) { component.StartingGearPrototypes.Add(proto, _prototypeManager.Index(proto));