Split codewords into its own system (#37928)

* 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
This commit is contained in:
Simon
2025-06-06 01:19:41 +02:00
committed by GitHub
parent db38a9c945
commit d7d7a6c80e
12 changed files with 216 additions and 58 deletions

View File

@@ -123,6 +123,8 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("alertLevels"); _prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole"); _prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider"); _prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
_prototypeManager.RegisterIgnore("codewordGenerator");
_prototypeManager.RegisterIgnore("codewordFaction");
_componentFactory.GenerateNetIds(); _componentFactory.GenerateNetIds();
_adminManager.Initialize(); _adminManager.Initialize();

View File

@@ -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 = [];
}

View File

@@ -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!;
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Codewords;
using Content.Shared.Dataset; using Content.Shared.Dataset;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Prototypes;
using Content.Shared.Random; using Content.Shared.Random;
using Content.Shared.Roles; using Content.Shared.Roles;
using Robust.Shared.Audio; using Robust.Shared.Audio;
@@ -17,18 +18,15 @@ public sealed partial class TraitorRuleComponent : Component
[DataField] [DataField]
public ProtoId<AntagPrototype> TraitorPrototypeId = "Traitor"; public ProtoId<AntagPrototype> TraitorPrototypeId = "Traitor";
[DataField]
public ProtoId<CodewordFactionPrototype> CodewordFactionPrototypeId = "Traitor";
[DataField] [DataField]
public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen"; public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
[DataField] [DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate"; public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<LocalizedDatasetPrototype> CodewordAdjectives = "Adjectives";
[DataField]
public ProtoId<LocalizedDatasetPrototype> CodewordVerbs = "Verbs";
[DataField] [DataField]
public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations"; public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
@@ -51,7 +49,6 @@ public sealed partial class TraitorRuleComponent : Component
public bool GiveBriefing = true; public bool GiveBriefing = true;
public int TotalTraitors => TraitorMinds.Count; public int TotalTraitors => TraitorMinds.Count;
public string[] Codewords = new string[3];
public enum SelectionState public enum SelectionState
{ {
@@ -77,12 +74,6 @@ public sealed partial class TraitorRuleComponent : Component
[DataField] [DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg"); 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> /// <summary>
/// The amount of TC traitors start with. /// The amount of TC traitors start with.
/// </summary> /// </summary>

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Content.Server.Codewords;
namespace Content.Server.GameTicking.Rules; namespace Content.Server.GameTicking.Rules;
@@ -37,6 +38,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!; [Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!; [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly UplinkSystem _uplink = default!; [Dependency] private readonly UplinkSystem _uplink = default!;
[Dependency] private readonly CodewordSystem _codewordSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -48,41 +50,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend); 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) private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{ {
Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}"); Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}");
MakeTraitor(args.EntityUid, 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) public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
{ {
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - start"); Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - start");
var factionCodewords = _codewordSystem.GetCodewords(component.CodewordFactionPrototypeId);
//Grab the mind if it wasn't provided //Grab the mind if it wasn't provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
@@ -96,7 +73,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (component.GiveCodewords) if (component.GiveCodewords)
{ {
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - added codewords flufftext to briefing"); 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)); var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers));
@@ -129,7 +106,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (component.GiveCodewords) if (component.GiveCodewords)
{ {
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - set codewords from component"); Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - set codewords from component");
codewords = component.Codewords; codewords = factionCodewords;
} }
if (component.GiveBriefing) 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 var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found
RoleCodewordComponent codewordComp = EnsureComp<RoleCodewordComponent>(mindId); RoleCodewordComponent codewordComp = EnsureComp<RoleCodewordComponent>(mindId);
_roleCodewordSystem.SetRoleCodewords(codewordComp, "traitor", component.Codewords.ToList(), color); _roleCodewordSystem.SetRoleCodewords(codewordComp, "traitor", factionCodewords.ToList(), color);
// Change the faction // Change the faction
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Change 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) private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{ {
if(comp.GiveCodewords) 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? // TODO: figure out how to handle this? add priority to briefing event?

View File

@@ -1,3 +1,6 @@
using Content.Server.Codewords;
using Robust.Shared.Prototypes;
namespace Content.Server.Traitor.Components; namespace Content.Server.Traitor.Components;
/// <summary> /// <summary>
@@ -6,6 +9,18 @@ namespace Content.Server.Traitor.Components;
[RegisterComponent] [RegisterComponent]
public sealed partial class TraitorCodePaperComponent : Component 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> /// <summary>
/// The number of codewords that should be generated on this paper. /// The number of codewords that should be generated on this paper.
/// Will not extend past the max number of available codewords. /// Will not extend past the max number of available codewords.

View File

@@ -7,6 +7,7 @@ using Content.Server.Traitor.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Linq; using System.Linq;
using Content.Server.Codewords;
using Content.Shared.Paper; using Content.Shared.Paper;
namespace Content.Server.Traitor.Systems; namespace Content.Server.Traitor.Systems;
@@ -17,6 +18,7 @@ public sealed class TraitorCodePaperSystem : EntitySystem
[Dependency] private readonly TraitorRuleSystem _traitorRuleSystem = default!; [Dependency] private readonly TraitorRuleSystem _traitorRuleSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PaperSystem _paper = default!; [Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly CodewordSystem _codewordSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -48,23 +50,12 @@ public sealed class TraitorCodePaperSystem : EntitySystem
traitorCode = null; traitorCode = null;
var codesMessage = new FormattedMessage(); var codesMessage = new FormattedMessage();
List<string> codeList = new(); var codeList = _codewordSystem.GetCodewords(component.CodewordFaction).ToList();
// 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());
}
}
}
if (codeList.Count == 0) if (codeList.Count == 0)
{ {
if (component.FakeCodewords) if (component.FakeCodewords)
codeList = _traitorRuleSystem.GenerateTraitorCodewords(new TraitorRuleComponent()).ToList(); codeList = _codewordSystem.GenerateCodewords(component.CodewordGenerator).ToList();
else else
codeList = [Loc.GetString("traitor-codes-none")]; codeList = [Loc.GetString("traitor-codes-none")];
} }

View File

@@ -0,0 +1,3 @@
- type: codewordFaction
id: Traitor
generator: TraitorCodewordGenerator

View File

@@ -0,0 +1,6 @@
- type: codewordGenerator
id: TraitorCodewordGenerator
words:
- Adjectives
- Verbs
amount: 4