From 3030323eaee6ef7348e2795aa537d0bdde384448 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 29 Jun 2024 15:39:57 +1000 Subject: [PATCH] Ensure trait groups get validated (#28730) * Ensure trait groups get validated The only validation being done was on the UI. I also made the "Default" group match the PascalCase naming schema so might be a slight breaking change but the original PR only got merged a few days ago. * overwatch --- .../Lobby/UI/HumanoidProfileEditor.xaml.cs | 35 +++-- .../Preferences/HumanoidCharacterProfile.cs | 127 ++++++++++++------ .../Traits/TraitCategoryPrototype.cs | 2 + 3 files changed, 112 insertions(+), 52 deletions(-) diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index be6e510571..304b69353e 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -478,10 +478,10 @@ namespace Content.Client.Lobby.UI return; } - //Setup model - Dictionary> model = new(); + // Setup model + Dictionary> traitGroups = new(); List defaultTraits = new(); - model.Add("default", defaultTraits); + traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits); foreach (var trait in traits) { @@ -491,18 +491,19 @@ namespace Content.Client.Lobby.UI continue; } - if (!model.ContainsKey(trait.Category)) - { - model.Add(trait.Category, new()); - } - model[trait.Category].Add(trait.ID); + if (!_prototypeManager.HasIndex(trait.Category)) + continue; + + var group = traitGroups.GetOrNew(trait.Category); + group.Add(trait.ID); } - //Create UI view from model - foreach (var (categoryId, traitId) in model) + // Create UI view from model + foreach (var (categoryId, categoryTraits) in traitGroups) { TraitCategoryPrototype? category = null; - if (categoryId != "default") + + if (categoryId != TraitCategoryPrototype.Default) { category = _prototypeManager.Index(categoryId); // Label @@ -517,7 +518,7 @@ namespace Content.Client.Lobby.UI List selectors = new(); var selectionCount = 0; - foreach (var traitProto in traitId) + foreach (var traitProto in categoryTraits) { var trait = _prototypeManager.Index(traitProto); var selector = new TraitPreferenceSelector(trait); @@ -528,7 +529,15 @@ namespace Content.Client.Lobby.UI selector.PreferenceChanged += preference => { - Profile = Profile?.WithTraitPreference(trait.ID, categoryId, preference); + if (preference) + { + Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager); + } + else + { + Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager); + } + SetDirty(); RefreshTraits(); // If too many traits are selected, they will be reset to the real value. }; diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index e4a9f9a490..4a93eadbe4 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -396,48 +396,58 @@ namespace Content.Shared.Preferences }; } - public HumanoidCharacterProfile WithTraitPreference(ProtoId traitId, string? categoryId, bool pref) + public HumanoidCharacterProfile WithTraitPreference(ProtoId traitId, IPrototypeManager protoManager) { - var prototypeManager = IoCManager.Resolve(); - var traitProto = prototypeManager.Index(traitId); + // null category is assumed to be default. + if (!protoManager.TryIndex(traitId, out var traitProto)) + return new(this); - TraitCategoryPrototype? categoryProto = null; - if (categoryId != null && categoryId != "default") - categoryProto = prototypeManager.Index(categoryId); + var category = traitProto.Category; + // Category not found so dump it. + TraitCategoryPrototype? traitCategory = null; + + if (category != null && !protoManager.TryIndex(category, out traitCategory)) + return new(this); + + var list = new HashSet>(_traitPreferences) { traitId }; + + if (traitCategory == null || traitCategory.MaxTraitPoints < 0) + { + return new(this) + { + _traitPreferences = list, + }; + } + + var count = 0; + foreach (var trait in list) + { + // If trait not found or another category don't count its points. + if (!protoManager.TryIndex(trait, out var otherProto) || + otherProto.Category != traitCategory) + { + continue; + } + + count += otherProto.Cost; + } + + if (count > traitCategory.MaxTraitPoints && traitProto.Cost != 0) + { + return new(this); + } + + return new(this) + { + _traitPreferences = list, + }; + } + + public HumanoidCharacterProfile WithoutTraitPreference(ProtoId traitId, IPrototypeManager protoManager) + { var list = new HashSet>(_traitPreferences); - - if (pref) - { - list.Add(traitId); - - if (categoryProto == null || categoryProto.MaxTraitPoints < 0) - { - return new(this) - { - _traitPreferences = list, - }; - } - - var count = 0; - foreach (var trait in list) - { - var traitProtoTemp = prototypeManager.Index(trait); - count += traitProtoTemp.Cost; - } - - if (count > categoryProto.MaxTraitPoints && traitProto.Cost != 0) - { - return new(this) - { - _traitPreferences = _traitPreferences, - }; - } - } - else - { - list.Remove(traitId); - } + list.Remove(traitId); return new(this) { @@ -614,7 +624,7 @@ namespace Content.Shared.Preferences _antagPreferences.UnionWith(antags); _traitPreferences.Clear(); - _traitPreferences.UnionWith(traits); + _traitPreferences.UnionWith(GetValidTraits(traits, prototypeManager)); // Checks prototypes exist for all loadouts and dump / set to default if not. var toRemove = new ValueList(); @@ -636,6 +646,45 @@ namespace Content.Shared.Preferences } } + /// + /// Takes in an IEnumerable of traits and returns a List of the valid traits. + /// + public List> GetValidTraits(IEnumerable> traits, IPrototypeManager protoManager) + { + // Track points count for each group. + var groups = new Dictionary(); + var result = new List>(); + + foreach (var trait in traits) + { + if (!protoManager.TryIndex(trait, out var traitProto)) + continue; + + // Always valid. + if (traitProto.Category == null) + { + result.Add(trait); + continue; + } + + // No category so dump it. + if (!protoManager.TryIndex(traitProto.Category, out var category)) + continue; + + var existing = groups.GetOrNew(category.ID); + existing += traitProto.Cost; + + // Too expensive. + if (existing > category.MaxTraitPoints) + continue; + + groups[category.ID] = existing; + result.Add(trait); + } + + return result; + } + public ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection) { var profile = new HumanoidCharacterProfile(this); diff --git a/Content.Shared/Traits/TraitCategoryPrototype.cs b/Content.Shared/Traits/TraitCategoryPrototype.cs index 1da624173a..e5bb919bf0 100644 --- a/Content.Shared/Traits/TraitCategoryPrototype.cs +++ b/Content.Shared/Traits/TraitCategoryPrototype.cs @@ -8,6 +8,8 @@ namespace Content.Shared.Traits; [Prototype] public sealed partial class TraitCategoryPrototype : IPrototype { + public const string Default = "Default"; + [ViewVariables] [IdDataField] public string ID { get; private set; } = default!;