]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add active and ended game rule components, generic TryRoundStartAttempt and minPlayer...
authorDrSmugleaf <DrSmugleaf@users.noreply.github.com>
Thu, 28 Sep 2023 14:48:59 +0000 (07:48 -0700)
committerGitHub <noreply@github.com>
Thu, 28 Sep 2023 14:48:59 +0000 (10:48 -0400)
* Improve active game rule querying, add generic try round start attempt method, move minPlayers to GameRuleComponent

* Nukeops todo and cleanup

* Remove Active field

* Add EndedGameRuleComponent

Content.Server/GameTicking/GameTicker.GameRule.cs
Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs
Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs
Content.Server/GameTicking/Rules/GameRuleSystem.cs
Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs

index 82e2872914e3b415cf5b83c1cb3e745ae886354e..c04b8d671161c940231866e9b581bed3b3b8dbbd 100644 (file)
@@ -94,7 +94,7 @@ public sealed partial class GameTicker
             ruleData ??= EnsureComp<GameRuleComponent>(ruleEntity);
 
         // can't start an already active rule
-        if (ruleData.Active || ruleData.Ended)
+        if (HasComp<ActiveGameRuleComponent>(ruleEntity) || HasComp<EndedGameRuleComponent>(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<ActiveGameRuleComponent>(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<EndedGameRuleComponent>(ruleEntity))
             return false;
 
         if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
             return false;
 
-        ruleData.Active = false;
-        ruleData.Ended = true;
+        RemComp<ActiveGameRuleComponent>(ruleEntity);
+        EnsureComp<EndedGameRuleComponent>(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<EndedGameRuleComponent>(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<ActiveGameRuleComponent>(ruleEntity);
     }
 
     public bool IsGameRuleActive(string rule)
@@ -193,11 +195,10 @@ public sealed partial class GameTicker
     /// </summary>
     public IEnumerable<EntityUid> GetActiveGameRules()
     {
-        var query = EntityQueryEnumerator<GameRuleComponent>();
-        while (query.MoveNext(out var uid, out var ruleData))
+        var query = EntityQueryEnumerator<ActiveGameRuleComponent, GameRuleComponent>();
+        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 (file)
index 0000000..956768b
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+///     Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
+///     Mutually exclusive with <seealso cref="EndedGameRuleComponent"/>.
+/// </summary>
+[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 (file)
index 0000000..4484abd
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Server.GameTicking.Rules.Components;
+
+/// <summary>
+///     Added to game rules before <see cref="GameRuleEndedEvent"/>.
+///     Mutually exclusive with <seealso cref="ActiveGameRuleComponent"/>.
+/// </summary>
+[RegisterComponent]
+public sealed partial class EndedGameRuleComponent : Component
+{
+}
index cc384b47d3c771063b7293e1c977b1cdfa68836d..80a5ab340aa1159a4a46ba685a4b11bf131c83d7 100644 (file)
@@ -9,25 +9,17 @@ namespace Content.Server.GameTicking.Rules.Components;
 [RegisterComponent]
 public sealed partial class GameRuleComponent : Component
 {
-    /// <summary>
-    /// Whether or not the rule is active.
-    /// Is enabled after <see cref="GameRuleStartedEvent"/> and disabled after <see cref="GameRuleEndedEvent"/>
-    /// </summary>
-    [DataField("active")]
-    public bool Active;
-
     /// <summary>
     /// Game time when game rule was activated
     /// </summary>
-    [DataField("activatedAt", customTypeSerializer:typeof(TimeOffsetSerializer))]
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
     public TimeSpan ActivatedAt;
 
     /// <summary>
-    /// 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.
     /// </summary>
-    [DataField("ended")]
-    public bool Ended;
+    [DataField]
+    public int MinPlayers;
 }
 
 /// <summary>
index 8ddfd9c14bb771b46039d62f454287c4e21a3322..55c14f88d4e6d92677c3135fa699689e4c8de99c 100644 (file)
@@ -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
     /// <summary>
     /// The minimum needed amount of players
     /// </summary>
-    [DataField("minPlayers")]
+    [DataField]
     public int MinPlayers = 20;
 
     /// <summary>
     ///     This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
     /// </summary>
-    [DataField("playersPerOperative")]
+    [DataField]
     public int PlayersPerOperative = 10;
 
-    [DataField("maxOps")]
-    public int MaxOperatives = 5;
+    [DataField]
+    public int MaxOps = 5;
 
     /// <summary>
     /// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
     /// </summary>
-    [DataField("roundEndBehavior")]
+    [DataField]
     public RoundEndBehavior RoundEndBehavior = RoundEndBehavior.ShuttleCall;
 
     /// <summary>
     /// Text for shuttle call if RoundEndBehavior is ShuttleCall.
     /// </summary>
-    [DataField("roundEndTextSender")]
+    [DataField]
     public string RoundEndTextSender = "comms-console-announcement-title-centcom";
 
     /// <summary>
     /// Text for shuttle call if RoundEndBehavior is ShuttleCall.
     /// </summary>
-    [DataField("roundEndTextShuttleCall")]
+    [DataField]
     public string RoundEndTextShuttleCall = "nuke-ops-no-more-threat-announcement-shuttle-call";
 
     /// <summary>
     /// Text for announcement if RoundEndBehavior is ShuttleCall. Used if shuttle is already called
     /// </summary>
-    [DataField("roundEndTextAnnouncement")]
+    [DataField]
     public string RoundEndTextAnnouncement = "nuke-ops-no-more-threat-announcement";
 
     /// <summary>
     /// Time to emergency shuttle to arrive if RoundEndBehavior is ShuttleCall.
     /// </summary>
-    [DataField("evacShuttleTime")]
+    [DataField]
     public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(10);
 
     /// <summary>
     /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
     /// </summary>
-    [DataField("spawnOutpost")]
+    [DataField]
     public bool SpawnOutpost = true;
 
     /// <summary>
     /// Whether or not nukie left their outpost
     /// </summary>
-    [DataField("leftOutpost")]
-    public bool LeftOutpost = false;
+    [DataField]
+    public bool LeftOutpost;
 
     /// <summary>
     ///     Enables opportunity to get extra TC for war declaration
     /// </summary>
-    [DataField("canEnableWarOps")]
+    [DataField]
     public bool CanEnableWarOps = true;
 
     /// <summary>
     ///     Indicates time when war has been declared, null if not declared
     /// </summary>
-    [DataField("warDeclaredTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
     public TimeSpan? WarDeclaredTime;
 
     /// <summary>
     ///     This amount of TC will be given to each nukie
     /// </summary>
-    [DataField("warTCAmountPerNukie")]
+    [DataField]
     public int WarTCAmountPerNukie = 40;
 
     /// <summary>
@@ -100,55 +100,55 @@ public sealed partial class NukeopsRuleComponent : Component
     /// <summary>
     ///     Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare
     /// </summary>
-    [DataField("warNukieArriveDelay")]
+    [DataField]
     public TimeSpan? WarNukieArriveDelay = TimeSpan.FromMinutes(15);
 
     /// <summary>
     ///     Minimal operatives count for war declaration
     /// </summary>
-    [DataField("warDeclarationMinOps")]
+    [DataField]
     public int WarDeclarationMinOps = 4;
 
-    [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string SpawnPointPrototype = "SpawnPointNukies";
+    [DataField]
+    public EntProtoId SpawnPointProto = "SpawnPointNukies";
 
-    [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
+    [DataField]
+    public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
 
-    [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
-    public string CommanderRolePrototype = "NukeopsCommander";
+    [DataField]
+    public ProtoId<AntagPrototype> CommanderRoleProto = "NukeopsCommander";
 
-    [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
-    public string OperativeRoleProto = "Nukeops";
+    [DataField]
+    public ProtoId<AntagPrototype> OperativeRoleProto = "Nukeops";
 
-    [DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
-    public string MedicRoleProto = "NukeopsMedic";
+    [DataField]
+    public ProtoId<AntagPrototype> MedicRoleProto = "NukeopsMedic";
 
-    [DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
-    public string CommanderStartGearPrototype = "SyndicateCommanderGearFull";
+    [DataField]
+    public ProtoId<StartingGearPrototype> CommanderStartGearProto = "SyndicateCommanderGearFull";
 
-    [DataField("medicStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
-    public string MedicStartGearPrototype = "SyndicateOperativeMedicFull";
+    [DataField]
+    public ProtoId<StartingGearPrototype> MedicStartGearProto = "SyndicateOperativeMedicFull";
 
-    [DataField("operativeStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
-    public string OperativeStartGearPrototype = "SyndicateOperativeGearFull";
+    [DataField]
+    public ProtoId<StartingGearPrototype> OperativeStartGearProto = "SyndicateOperativeGearFull";
 
-    [DataField("eliteNames", customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
+    [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
     public string EliteNames = "SyndicateNamesElite";
 
-    [DataField("normalNames", customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
+    [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
     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<WinCondition> WinConditions = new ();
 
     public MapId? NukiePlanet;
@@ -162,30 +162,30 @@ public sealed partial class NukeopsRuleComponent : Component
     /// <summary>
     ///     Cached starting gear prototypes.
     /// </summary>
-    [DataField("startingGearPrototypes")]
+    [DataField]
     public Dictionary<string, StartingGearPrototype> StartingGearPrototypes = new ();
 
     /// <summary>
     ///     Cached operator name prototypes.
     /// </summary>
-    [DataField("operativeNames")]
+    [DataField]
     public Dictionary<string, List<string>> OperativeNames = new();
 
     /// <summary>
     ///     Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
     /// </summary>
-    [DataField("operativeMindPendingData")]
+    [DataField]
     public Dictionary<EntityUid, string> OperativeMindPendingData = new();
 
     /// <summary>
     ///     Players who played as an operative at some point in the round.
     ///     Stores the mind as well as the entity name
     /// </summary>
-    [DataField("operativePlayers")]
+    [DataField]
     public Dictionary<string, EntityUid> OperativePlayers = new();
 
-    [DataField("faction", customTypeSerializer: typeof(PrototypeIdSerializer<NpcFactionPrototype>), required: true)]
-    public string Faction = default!;
+    [DataField(required: true)]
+    public ProtoId<NpcFactionPrototype> Faction = default!;
 }
 
 public enum WinType : byte
index e87660c2cc4a65f1d700b4d89e9ee6dc9bbec508..b13f00f36383a3345c35ed322c8e2e744fb82253 100644 (file)
@@ -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<T> : 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<T> : EntitySystem where T : Compone
         Ended(uid, component, ruleData, args);
     }
 
+
     /// <summary>
     /// Called when the gamerule is added
     /// </summary>
@@ -68,6 +71,36 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : Compone
 
     }
 
+    protected EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent> QueryActiveRules()
+    {
+        return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
+    }
+
+    protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
+    {
+        var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
+        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);
index df9fbcc13017ea919fad11b0a24c3a943febf5ba..3bd3d13d2778926ea4310f58ba0b0ddb9a2fd6ff 100644 (file)
@@ -595,7 +595,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
 
             // 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<IPlayerSession>(ev.PlayerPool);
@@ -614,15 +614,15 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
                 }
 
                 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<NukeopsRuleComponent>
         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<NukeopsRuleComponent>
         {
             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<NukeopsRuleComponent>
         // Forgive me for hardcoding prototypes
         foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(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<NukeopsRuleComponent>
         }
         // 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<NukeopsRuleComponent>
         // 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<StartingGearPrototype>(proto));