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