]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Split codewords into its own system (#37928)
authorSimon <63975668+Simyon264@users.noreply.github.com>
Thu, 5 Jun 2025 23:19:41 +0000 (01:19 +0200)
committerGitHub <noreply@github.com>
Thu, 5 Jun 2025 23:19:41 +0000 (19:19 -0400)
* Split codewords into its own system

* Fix admin log

* Nuke unused code

* Fix formatting errors

* Fix tests

* Make the codeword system add itself if called when not active

* Put comment in right place.

* Review: Rename prototypes

* Review: Make codewords serializable

* Fix build

* Reviews: Change the system to not be a gamerule.

* Fix YAML Linter

* Fix test fail

* Remove unused import

12 files changed:
Content.Client/Entry/EntryPoint.cs
Content.Server/Codewords/CodewordComponent.cs [new file with mode: 0644]
Content.Server/Codewords/CodewordFactionPrototype.cs [new file with mode: 0644]
Content.Server/Codewords/CodewordGeneratorPrototype.cs [new file with mode: 0644]
Content.Server/Codewords/CodewordManagerComponent.cs [new file with mode: 0644]
Content.Server/Codewords/CodewordSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
Content.Server/Traitor/Components/TraitorCodePaperComponent.cs
Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs
Resources/Prototypes/Codewords/codeword_factions.yml [new file with mode: 0644]
Resources/Prototypes/Codewords/codeword_generators.yml [new file with mode: 0644]

index ebed5269f89afb6a2e1aef107bb0d51a1ee2bd99..322b6e113caf7a0d9c3f0d4e66869539ee554787 100644 (file)
@@ -123,6 +123,8 @@ namespace Content.Client.Entry
             _prototypeManager.RegisterIgnore("alertLevels");
             _prototypeManager.RegisterIgnore("nukeopsRole");
             _prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
+            _prototypeManager.RegisterIgnore("codewordGenerator");
+            _prototypeManager.RegisterIgnore("codewordFaction");
 
             _componentFactory.GenerateNetIds();
             _adminManager.Initialize();
diff --git a/Content.Server/Codewords/CodewordComponent.cs b/Content.Server/Codewords/CodewordComponent.cs
new file mode 100644 (file)
index 0000000..6ceb3a5
--- /dev/null
@@ -0,0 +1,14 @@
+namespace Content.Server.Codewords;
+
+/// <summary>
+/// Container for generated codewords.
+/// </summary>
+[RegisterComponent, Access(typeof(CodewordSystem))]
+public sealed partial class CodewordComponent : Component
+{
+    /// <summary>
+    /// The codewords that were generated.
+    /// </summary>
+    [DataField]
+    public string[] Codewords = [];
+}
diff --git a/Content.Server/Codewords/CodewordFactionPrototype.cs b/Content.Server/Codewords/CodewordFactionPrototype.cs
new file mode 100644 (file)
index 0000000..72d24b1
--- /dev/null
@@ -0,0 +1,20 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Codewords;
+
+/// <summary>
+/// This is a prototype for easy access to codewords using identifiers instead of magic strings.
+/// </summary>
+[Prototype]
+public sealed partial class CodewordFactionPrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    /// The generator to use for this faction.
+    /// </summary>
+    [DataField(required:true)]
+    public ProtoId<CodewordGeneratorPrototype> Generator { get; } = default!;
+}
diff --git a/Content.Server/Codewords/CodewordGeneratorPrototype.cs b/Content.Server/Codewords/CodewordGeneratorPrototype.cs
new file mode 100644 (file)
index 0000000..15e50eb
--- /dev/null
@@ -0,0 +1,32 @@
+using Content.Shared.Dataset;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Codewords;
+
+/// <summary>
+/// This is a prototype for specifying codeword generation
+/// </summary>
+[Prototype]
+public sealed partial class CodewordGeneratorPrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    /// List of datasets to use for word generation. All values will be concatenated into one list and then randomly chosen from
+    /// </summary>
+    [DataField]
+    public List<ProtoId<LocalizedDatasetPrototype>> Words { get; } =
+    [
+        "Adjectives",
+        "Verbs",
+    ];
+
+
+    /// <summary>
+    /// How many codewords should be generated?
+    /// </summary>
+    [DataField]
+    public int Amount = 3;
+}
diff --git a/Content.Server/Codewords/CodewordManagerComponent.cs b/Content.Server/Codewords/CodewordManagerComponent.cs
new file mode 100644 (file)
index 0000000..46ddc35
--- /dev/null
@@ -0,0 +1,17 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Codewords;
+
+/// <summary>
+/// Component that defines <see cref="CodewordGeneratorPrototype"/> to use and keeps track of generated codewords.
+/// </summary>
+[RegisterComponent, Access(typeof(CodewordSystem))]
+public sealed partial class CodewordManagerComponent : Component
+{
+    /// <summary>
+    /// The generated codewords. The value contains the entity that has the <see cref="CodewordComponent"/>
+    /// </summary>
+    [DataField]
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Dictionary<ProtoId<CodewordFactionPrototype>, EntityUid> Codewords = new();
+}
diff --git a/Content.Server/Codewords/CodewordSystem.cs b/Content.Server/Codewords/CodewordSystem.cs
new file mode 100644 (file)
index 0000000..54f0e93
--- /dev/null
@@ -0,0 +1,90 @@
+using System.Linq;
+using Content.Server.Administration.Logs;
+using Content.Server.GameTicking.Events;
+using Content.Shared.Database;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Codewords;
+
+/// <summary>
+/// Gamerule that provides codewords for other gamerules that rely on them.
+/// </summary>
+public sealed class CodewordSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<RoundStartingEvent>(OnRoundStart);
+    }
+
+    private void OnRoundStart(RoundStartingEvent ev)
+    {
+        var manager = Spawn();
+        AddComp<CodewordManagerComponent>(manager);
+    }
+
+    /// <summary>
+    /// Retrieves codewords for the faction specified.
+    /// </summary>
+    public string[] GetCodewords(ProtoId<CodewordFactionPrototype> faction)
+    {
+        var query = EntityQueryEnumerator<CodewordManagerComponent>();
+        while (query.MoveNext(out  _, out var manager))
+        {
+            if (!manager.Codewords.TryGetValue(faction, out var codewordEntity))
+                return GenerateForFaction(faction, ref manager);
+
+            return Comp<CodewordComponent>(codewordEntity).Codewords;
+        }
+
+        Log.Warning("Codeword system not initialized. Returning empty array.");
+        // While throwing in this situation would be cool, that causes a test fail (in SpawnAndDeleteEntityCountTest)
+        // as the traitor codewords paper gets spawned in and calls this method,
+        // but the "start round" event never gets called in this test case.
+        return [];
+    }
+
+    private string[] GenerateForFaction(ProtoId<CodewordFactionPrototype> faction, ref CodewordManagerComponent manager)
+    {
+        var factionProto = _prototypeManager.Index<CodewordFactionPrototype>(faction.Id);
+
+        var codewords = GenerateCodewords(factionProto.Generator);
+        var codewordsContainer = EntityManager.Spawn(protoName:null, MapCoordinates.Nullspace);
+        EnsureComp<CodewordComponent>(codewordsContainer)
+            .Codewords = codewords;
+        manager.Codewords[faction] = codewordsContainer;
+        _adminLogger.Add(LogType.EventStarted, LogImpact.Low, $"Codewords generated for faction {faction}: {string.Join(", ", codewords)}");
+
+        return codewords;
+    }
+
+    /// <summary>
+    /// Generates codewords as specified by the <see cref="CodewordGeneratorPrototype"/> codeword generator.
+    /// </summary>
+    public string[] GenerateCodewords(ProtoId<CodewordGeneratorPrototype> generatorId)
+    {
+        var generator = _prototypeManager.Index(generatorId);
+
+        var codewordPool = new List<string>();
+        foreach (var dataset in generator.Words
+                     .Select(datasetPrototype => _prototypeManager.Index(datasetPrototype)))
+        {
+            codewordPool.AddRange(dataset.Values);
+        }
+
+        var finalCodewordCount = Math.Min(generator.Amount, codewordPool.Count);
+        var codewords = new string[finalCodewordCount];
+        for (var i = 0; i < finalCodewordCount; i++)
+        {
+            codewords[i] = Loc.GetString(_random.PickAndTake(codewordPool));
+        }
+        return codewords;
+    }
+}
index bfaf87e97c9fbc1cc44f1de94e4d9df2b08ad12d..092f4b71c20e2d32845653e8efb79cc1c929440a 100644 (file)
@@ -1,6 +1,7 @@
+using Content.Server.Codewords;
 using Content.Shared.Dataset;
 using Content.Shared.FixedPoint;
-using Content.Shared.NPC.Prototypes;
+using Content.Shared.NPC.Prototypes;
 using Content.Shared.Random;
 using Content.Shared.Roles;
 using Robust.Shared.Audio;
@@ -18,16 +19,13 @@ public sealed partial class TraitorRuleComponent : Component
     public ProtoId<AntagPrototype> TraitorPrototypeId = "Traitor";
 
     [DataField]
-    public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
+    public ProtoId<CodewordFactionPrototype> CodewordFactionPrototypeId = "Traitor";
 
     [DataField]
-    public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
-
-    [DataField]
-    public ProtoId<LocalizedDatasetPrototype> CodewordAdjectives = "Adjectives";
+    public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
 
     [DataField]
-    public ProtoId<LocalizedDatasetPrototype> CodewordVerbs = "Verbs";
+    public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
 
     [DataField]
     public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
@@ -51,7 +49,6 @@ public sealed partial class TraitorRuleComponent : Component
     public bool GiveBriefing = true;
 
     public int TotalTraitors => TraitorMinds.Count;
-    public string[] Codewords = new string[3];
 
     public enum SelectionState
     {
@@ -77,12 +74,6 @@ public sealed partial class TraitorRuleComponent : Component
     [DataField]
     public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
 
-    /// <summary>
-    /// The amount of codewords that are selected.
-    /// </summary>
-    [DataField]
-    public int CodewordCount = 4;
-
     /// <summary>
     /// The amount of TC traitors start with.
     /// </summary>
index 790b14579eb461ea89a207fde1137de71bad00e7..7940eebef14058b4c80ad421b0f0b44b426ce859 100644 (file)
@@ -20,6 +20,7 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using System.Linq;
 using System.Text;
+using Content.Server.Codewords;
 
 namespace Content.Server.GameTicking.Rules;
 
@@ -37,6 +38,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
     [Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
     [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
     [Dependency] private readonly UplinkSystem _uplink = default!;
+    [Dependency] private readonly CodewordSystem _codewordSystem = default!;
 
     public override void Initialize()
     {
@@ -48,41 +50,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
     }
 
-    protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
-    {
-        base.Added(uid, component, gameRule, args);
-        SetCodewords(component, args.RuleEntity);
-    }
-
     private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
     {
         Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}");
         MakeTraitor(args.EntityUid, ent);
     }
 
-    private void SetCodewords(TraitorRuleComponent component, EntityUid ruleEntity)
-    {
-        component.Codewords = GenerateTraitorCodewords(component);
-        _adminLogger.Add(LogType.EventStarted, LogImpact.Low, $"Codewords generated for game rule {ToPrettyString(ruleEntity)}: {string.Join(", ", component.Codewords)}");
-    }
-
-    public string[] GenerateTraitorCodewords(TraitorRuleComponent component)
-    {
-        var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values;
-        var verbs = _prototypeManager.Index(component.CodewordVerbs).Values;
-        var codewordPool = adjectives.Concat(verbs).ToList();
-        var finalCodewordCount = Math.Min(component.CodewordCount, codewordPool.Count);
-        string[] codewords = new string[finalCodewordCount];
-        for (var i = 0; i < finalCodewordCount; i++)
-        {
-            codewords[i] = Loc.GetString(_random.PickAndTake(codewordPool));
-        }
-        return codewords;
-    }
-
     public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
     {
         Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - start");
+        var factionCodewords = _codewordSystem.GetCodewords(component.CodewordFactionPrototypeId);
 
         //Grab the mind if it wasn't provided
         if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
@@ -96,7 +73,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         if (component.GiveCodewords)
         {
             Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - added codewords flufftext to briefing");
-            briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
+            briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", factionCodewords)));
         }
 
         var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers));
@@ -129,7 +106,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         if (component.GiveCodewords)
         {
             Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - set codewords from component");
-            codewords = component.Codewords;
+            codewords = factionCodewords;
         }
 
         if (component.GiveBriefing)
@@ -161,7 +138,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
         var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found
 
         RoleCodewordComponent codewordComp = EnsureComp<RoleCodewordComponent>(mindId);
-        _roleCodewordSystem.SetRoleCodewords(codewordComp, "traitor", component.Codewords.ToList(), color);
+        _roleCodewordSystem.SetRoleCodewords(codewordComp, "traitor", factionCodewords.ToList(), color);
 
         // Change the faction
         Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Change faction");
@@ -211,7 +188,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
     private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
     {
         if(comp.GiveCodewords)
-            args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
+            args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", _codewordSystem.GetCodewords(comp.CodewordFactionPrototypeId))));
     }
 
     // TODO: figure out how to handle this? add priority to briefing event?
index 7887248f21db6b9116a32a8524be911d94081e89..d4ce40c066bdfcaa96039a055cedc5775d8ce724 100644 (file)
@@ -1,3 +1,6 @@
+using Content.Server.Codewords;
+using Robust.Shared.Prototypes;
+
 namespace Content.Server.Traitor.Components;
 
 /// <summary>
@@ -6,6 +9,18 @@ namespace Content.Server.Traitor.Components;
 [RegisterComponent]
 public sealed partial class TraitorCodePaperComponent : Component
 {
+    /// <summary>
+    /// The faction to get codewords for.
+    /// </summary>
+    [DataField]
+    public ProtoId<CodewordFactionPrototype> CodewordFaction = "Traitor";
+
+    /// <summary>
+    /// The generator to use for the fake words.
+    /// </summary>
+    [DataField]
+    public ProtoId<CodewordGeneratorPrototype> CodewordGenerator = "TraitorCodewordGenerator";
+
     /// <summary>
     /// The number of codewords that should be generated on this paper.
     /// Will not extend past the max number of available codewords.
index bccbd80bf5a45347206a0532110cbb15a79b797d..f1a0f97f540bb878e7a041128bafb0e7e61cf446 100644 (file)
@@ -7,6 +7,7 @@ using Content.Server.Traitor.Components;
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 using System.Linq;
+using Content.Server.Codewords;
 using Content.Shared.Paper;
 
 namespace Content.Server.Traitor.Systems;
@@ -17,6 +18,7 @@ public sealed class TraitorCodePaperSystem : EntitySystem
     [Dependency] private readonly TraitorRuleSystem _traitorRuleSystem = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly PaperSystem _paper = default!;
+    [Dependency] private readonly CodewordSystem _codewordSystem = default!;
 
     public override void Initialize()
     {
@@ -48,23 +50,12 @@ public sealed class TraitorCodePaperSystem : EntitySystem
         traitorCode = null;
 
         var codesMessage = new FormattedMessage();
-        List<string> codeList = new();
-        // Find the first nuke that matches the passed location.
-        if (_gameTicker.IsGameRuleAdded<TraitorRuleComponent>())
-        {
-            var ruleEnts = _gameTicker.GetAddedGameRules();
-            foreach (var ruleEnt in ruleEnts)
-            {
-                if (TryComp(ruleEnt, out TraitorRuleComponent? traitorComp))
-                {
-                    codeList.AddRange(traitorComp.Codewords.ToList());
-                }
-            }
-        }
+        var codeList = _codewordSystem.GetCodewords(component.CodewordFaction).ToList();
+
         if (codeList.Count == 0)
         {
             if (component.FakeCodewords)
-                codeList = _traitorRuleSystem.GenerateTraitorCodewords(new TraitorRuleComponent()).ToList();
+                codeList = _codewordSystem.GenerateCodewords(component.CodewordGenerator).ToList();
             else
                 codeList = [Loc.GetString("traitor-codes-none")];
         }
diff --git a/Resources/Prototypes/Codewords/codeword_factions.yml b/Resources/Prototypes/Codewords/codeword_factions.yml
new file mode 100644 (file)
index 0000000..9dc7e31
--- /dev/null
@@ -0,0 +1,3 @@
+- type: codewordFaction
+  id: Traitor
+  generator: TraitorCodewordGenerator
diff --git a/Resources/Prototypes/Codewords/codeword_generators.yml b/Resources/Prototypes/Codewords/codeword_generators.yml
new file mode 100644 (file)
index 0000000..c66fd37
--- /dev/null
@@ -0,0 +1,6 @@
+- type: codewordGenerator
+  id: TraitorCodewordGenerator
+  words:
+  - Adjectives
+  - Verbs
+  amount: 4