diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index ebed5269f8..322b6e113c 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -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
index 0000000000..6ceb3a513a
--- /dev/null
+++ b/Content.Server/Codewords/CodewordComponent.cs
@@ -0,0 +1,14 @@
+namespace Content.Server.Codewords;
+
+///
+/// Container for generated codewords.
+///
+[RegisterComponent, Access(typeof(CodewordSystem))]
+public sealed partial class CodewordComponent : Component
+{
+ ///
+ /// The codewords that were generated.
+ ///
+ [DataField]
+ public string[] Codewords = [];
+}
diff --git a/Content.Server/Codewords/CodewordFactionPrototype.cs b/Content.Server/Codewords/CodewordFactionPrototype.cs
new file mode 100644
index 0000000000..72d24b1dcd
--- /dev/null
+++ b/Content.Server/Codewords/CodewordFactionPrototype.cs
@@ -0,0 +1,20 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Codewords;
+
+///
+/// This is a prototype for easy access to codewords using identifiers instead of magic strings.
+///
+[Prototype]
+public sealed partial class CodewordFactionPrototype : IPrototype
+{
+ ///
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// The generator to use for this faction.
+ ///
+ [DataField(required:true)]
+ public ProtoId Generator { get; } = default!;
+}
diff --git a/Content.Server/Codewords/CodewordGeneratorPrototype.cs b/Content.Server/Codewords/CodewordGeneratorPrototype.cs
new file mode 100644
index 0000000000..15e50ebf73
--- /dev/null
+++ b/Content.Server/Codewords/CodewordGeneratorPrototype.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Dataset;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Codewords;
+
+///
+/// This is a prototype for specifying codeword generation
+///
+[Prototype]
+public sealed partial class CodewordGeneratorPrototype : IPrototype
+{
+ ///
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// List of datasets to use for word generation. All values will be concatenated into one list and then randomly chosen from
+ ///
+ [DataField]
+ public List> Words { get; } =
+ [
+ "Adjectives",
+ "Verbs",
+ ];
+
+
+ ///
+ /// How many codewords should be generated?
+ ///
+ [DataField]
+ public int Amount = 3;
+}
diff --git a/Content.Server/Codewords/CodewordManagerComponent.cs b/Content.Server/Codewords/CodewordManagerComponent.cs
new file mode 100644
index 0000000000..46ddc357a1
--- /dev/null
+++ b/Content.Server/Codewords/CodewordManagerComponent.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Codewords;
+
+///
+/// Component that defines to use and keeps track of generated codewords.
+///
+[RegisterComponent, Access(typeof(CodewordSystem))]
+public sealed partial class CodewordManagerComponent : Component
+{
+ ///
+ /// The generated codewords. The value contains the entity that has the
+ ///
+ [DataField]
+ [ViewVariables(VVAccess.ReadOnly)]
+ public Dictionary, EntityUid> Codewords = new();
+}
diff --git a/Content.Server/Codewords/CodewordSystem.cs b/Content.Server/Codewords/CodewordSystem.cs
new file mode 100644
index 0000000000..54f0e936b4
--- /dev/null
+++ b/Content.Server/Codewords/CodewordSystem.cs
@@ -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;
+
+///
+/// Gamerule that provides codewords for other gamerules that rely on them.
+///
+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(OnRoundStart);
+ }
+
+ private void OnRoundStart(RoundStartingEvent ev)
+ {
+ var manager = Spawn();
+ AddComp(manager);
+ }
+
+ ///
+ /// Retrieves codewords for the faction specified.
+ ///
+ public string[] GetCodewords(ProtoId faction)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out var manager))
+ {
+ if (!manager.Codewords.TryGetValue(faction, out var codewordEntity))
+ return GenerateForFaction(faction, ref manager);
+
+ return Comp(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 faction, ref CodewordManagerComponent manager)
+ {
+ var factionProto = _prototypeManager.Index(faction.Id);
+
+ var codewords = GenerateCodewords(factionProto.Generator);
+ var codewordsContainer = EntityManager.Spawn(protoName:null, MapCoordinates.Nullspace);
+ EnsureComp(codewordsContainer)
+ .Codewords = codewords;
+ manager.Codewords[faction] = codewordsContainer;
+ _adminLogger.Add(LogType.EventStarted, LogImpact.Low, $"Codewords generated for faction {faction}: {string.Join(", ", codewords)}");
+
+ return codewords;
+ }
+
+ ///
+ /// Generates codewords as specified by the codeword generator.
+ ///
+ public string[] GenerateCodewords(ProtoId generatorId)
+ {
+ var generator = _prototypeManager.Index(generatorId);
+
+ var codewordPool = new List();
+ 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;
+ }
+}
diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
index bfaf87e97c..092f4b71c2 100644
--- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
+++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs
@@ -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;
@@ -17,18 +18,15 @@ public sealed partial class TraitorRuleComponent : Component
[DataField]
public ProtoId TraitorPrototypeId = "Traitor";
+ [DataField]
+ public ProtoId CodewordFactionPrototypeId = "Traitor";
+
[DataField]
public ProtoId NanoTrasenFaction = "NanoTrasen";
[DataField]
public ProtoId SyndicateFaction = "Syndicate";
- [DataField]
- public ProtoId CodewordAdjectives = "Adjectives";
-
- [DataField]
- public ProtoId CodewordVerbs = "Verbs";
-
[DataField]
public ProtoId 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");
- ///
- /// The amount of codewords that are selected.
- ///
- [DataField]
- public int CodewordCount = 4;
-
///
/// The amount of TC traitors start with.
///
diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
index 790b14579e..7940eebef1 100644
--- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
@@ -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
[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
SubscribeLocalEvent(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 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
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
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
var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found
RoleCodewordComponent codewordComp = EnsureComp(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
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?
diff --git a/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs b/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs
index 7887248f21..d4ce40c066 100644
--- a/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs
+++ b/Content.Server/Traitor/Components/TraitorCodePaperComponent.cs
@@ -1,3 +1,6 @@
+using Content.Server.Codewords;
+using Robust.Shared.Prototypes;
+
namespace Content.Server.Traitor.Components;
///
@@ -6,6 +9,18 @@ namespace Content.Server.Traitor.Components;
[RegisterComponent]
public sealed partial class TraitorCodePaperComponent : Component
{
+ ///
+ /// The faction to get codewords for.
+ ///
+ [DataField]
+ public ProtoId CodewordFaction = "Traitor";
+
+ ///
+ /// The generator to use for the fake words.
+ ///
+ [DataField]
+ public ProtoId CodewordGenerator = "TraitorCodewordGenerator";
+
///
/// The number of codewords that should be generated on this paper.
/// Will not extend past the max number of available codewords.
diff --git a/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs b/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs
index bccbd80bf5..f1a0f97f54 100644
--- a/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs
+++ b/Content.Server/Traitor/Systems/TraitorCodePaperSystem.cs
@@ -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 codeList = new();
- // Find the first nuke that matches the passed location.
- if (_gameTicker.IsGameRuleAdded())
- {
- 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
index 0000000000..9dc7e31eac
--- /dev/null
+++ b/Resources/Prototypes/Codewords/codeword_factions.yml
@@ -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
index 0000000000..c66fd37083
--- /dev/null
+++ b/Resources/Prototypes/Codewords/codeword_generators.yml
@@ -0,0 +1,6 @@
+- type: codewordGenerator
+ id: TraitorCodewordGenerator
+ words:
+ - Adjectives
+ - Verbs
+ amount: 4