using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Humanoid.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Content.Shared.Humanoid.Markings; // the better version of MarkingsSet // This one should ensure that a set is valid. Dependency retrieval is // probably not a good idea, and any dependency references should last // only for the length of a call, and not the lifetime of the set itself. // // Compared to MarkingsSet, this should allow for server-side authority. // Instead of sending the set over, we can instead just send the dictionary // and build the set from there. We can also just send a list and rebuild // the set without validating points (we're assuming that the server /// /// Marking set. For humanoid markings. /// /// /// This is serializable for the admin panel that sets markings on demand for a player. /// Most APIs that accept a set of markings usually use a List of type Marking instead. /// [DataDefinition] [Serializable, NetSerializable] public sealed partial class MarkingSet { /// /// Every single marking in this set. /// /// /// The original version of MarkingSet preserved ordering across all /// markings - this one should instead preserve ordering across all /// categories, but not marking categories themselves. This is because /// the layers that markings appear in are guaranteed to be in the correct /// order. This is here to make lookups slightly faster, even if the n of /// a marking set is relatively small, and to encapsulate another important /// feature of markings, which is the limit of markings you can put on a /// humanoid. /// [DataField("markings")] public Dictionary> Markings = new(); /// /// Marking points for each category. /// [DataField("points")] public Dictionary Points = new(); public MarkingSet() {} /// /// Construct a MarkingSet using a list of markings, and a points /// dictionary. This will set up the points dictionary, and /// process the list, truncating if necessary. Markings that /// do not exist as a prototype will be removed. /// /// The lists of markings to use. /// The ID of the points dictionary prototype. public MarkingSet(List markings, string pointsPrototype, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null) { IoCManager.Resolve(ref markingManager, ref prototypeManager); if (!prototypeManager.TryIndex(pointsPrototype, out MarkingPointsPrototype? points)) { return; } Points = MarkingPoints.CloneMarkingPointDictionary(points.Points); foreach (var marking in markings) { if (!markingManager.TryGetMarking(marking, out var prototype)) { continue; } AddBack(prototype.MarkingCategory, marking); } } /// /// Construct a MarkingSet using a dictionary of markings, /// without point validation. This will still validate every /// marking, to ensure that it can be placed into the set. /// /// The list of markings to use. public MarkingSet(List markings, MarkingManager? markingManager = null) { IoCManager.Resolve(ref markingManager); foreach (var marking in markings) { if (!markingManager.TryGetMarking(marking, out var prototype)) { continue; } AddBack(prototype.MarkingCategory, marking); } } /// /// Construct a MarkingSet only with a points dictionary. /// /// The ID of the points dictionary prototype. public MarkingSet(string pointsPrototype, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null) { IoCManager.Resolve(ref markingManager, ref prototypeManager); if (!prototypeManager.TryIndex(pointsPrototype, out MarkingPointsPrototype? points)) { return; } Points = MarkingPoints.CloneMarkingPointDictionary(points.Points); } /// /// Construct a MarkingSet by deep cloning another set. /// /// The other marking set. public MarkingSet(MarkingSet other) { foreach (var (key, list) in other.Markings) { foreach (var marking in list) { AddBack(key, new(marking)); } } Points = MarkingPoints.CloneMarkingPointDictionary(other.Points); } /// /// Filters and colors markings based on species and it's restrictions in the marking's prototype from this marking set. /// /// The species to filter. /// The skin color for recoloring (i.e. slimes). Use null if you want only filter markings /// Marking manager. /// Prototype manager. public void EnsureSpecies(string species, Color? skinColor, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null) { IoCManager.Resolve(ref markingManager); IoCManager.Resolve(ref prototypeManager); var toRemove = new List<(MarkingCategories category, string id)>(); var speciesProto = prototypeManager.Index(species); var onlyWhitelisted = prototypeManager.Index(speciesProto.MarkingPoints).OnlyWhitelisted; foreach (var (category, list) in Markings) { foreach (var marking in list) { if (!markingManager.TryGetMarking(marking, out var prototype)) { toRemove.Add((category, marking.MarkingId)); continue; } if (onlyWhitelisted && prototype.SpeciesRestrictions == null) { toRemove.Add((category, marking.MarkingId)); } if (prototype.SpeciesRestrictions != null && !prototype.SpeciesRestrictions.Contains(species)) { toRemove.Add((category, marking.MarkingId)); } } } foreach (var remove in toRemove) { Remove(remove.category, remove.id); } // Re-color left markings them into skin color if needed (i.e. for slimes) if (skinColor != null) { foreach (var (category, list) in Markings) { foreach (var marking in list) { if (markingManager.TryGetMarking(marking, out var prototype) && markingManager.MustMatchSkin(species, prototype.BodyPart, out var alpha, prototypeManager)) { marking.SetColor(skinColor.Value.WithAlpha(alpha)); } } } } } /// /// Filters markings based on sex and it's restrictions in the marking's prototype from this marking set. /// /// The species to filter. /// Marking manager. public void EnsureSexes(Sex sex, MarkingManager? markingManager = null) { IoCManager.Resolve(ref markingManager); var toRemove = new List<(MarkingCategories category, string id)>(); foreach (var (category, list) in Markings) { foreach (var marking in list) { if (!markingManager.TryGetMarking(marking, out var prototype)) { toRemove.Add((category, marking.MarkingId)); continue; } if (prototype.SexRestriction != null && prototype.SexRestriction != sex) { toRemove.Add((category, marking.MarkingId)); } } } foreach (var remove in toRemove) { Remove(remove.category, remove.id); } } /// /// Ensures that all markings in this set are valid. /// /// Marking manager. public void EnsureValid(MarkingManager? markingManager = null) { IoCManager.Resolve(ref markingManager); var toRemove = new List(); foreach (var (category, list) in Markings) { for (var i = 0; i < list.Count; i++) { if (!markingManager.TryGetMarking(list[i], out var marking)) { toRemove.Add(i); continue; } if (marking.Sprites.Count != list[i].MarkingColors.Count) { list[i] = new Marking(marking.ID, marking.Sprites.Count); } } foreach (var i in toRemove) { Remove(category, i); } } } /// /// Ensures that the default markings as defined by the marking point set in this marking set are applied. /// /// Skin color for marking coloring. /// Eye color for marking coloring. /// Hair color for marking coloring. /// Marking manager. public void EnsureDefault(Color? skinColor = null, Color? eyeColor = null, MarkingManager? markingManager = null) { IoCManager.Resolve(ref markingManager); foreach (var (category, points) in Points) { if (points.Points <= 0 || points.DefaultMarkings.Count <= 0) { continue; } var index = 0; while (points.Points > 0 || index < points.DefaultMarkings.Count) { if (markingManager.Markings.TryGetValue(points.DefaultMarkings[index], out var prototype)) { var colors = MarkingColoring.GetMarkingLayerColors( prototype, skinColor, eyeColor, this ); var marking = new Marking(points.DefaultMarkings[index], colors); AddBack(category, marking); } index++; } } } /// /// How many points are left in this marking set's category /// /// The category to check /// A number equal or greater than zero if the category exists, -1 otherwise. public int PointsLeft(MarkingCategories category) { if (!Points.TryGetValue(category, out var points)) { return -1; } return points.Points; } /// /// Add a marking to the front of the category's list of markings. /// /// Category to add the marking to. /// The marking instance in question. public void AddFront(MarkingCategories category, Marking marking) { if (!marking.Forced && Points.TryGetValue(category, out var points)) { if (points.Points <= 0) { return; } points.Points--; } if (!Markings.TryGetValue(category, out var markings)) { markings = new(); Markings[category] = markings; } markings.Insert(0, marking); } /// /// Add a marking to the back of the category's list of markings. /// /// /// public void AddBack(MarkingCategories category, Marking marking) { if (!marking.Forced && Points.TryGetValue(category, out var points)) { if (points.Points <= 0) { return; } points.Points--; } if (!Markings.TryGetValue(category, out var markings)) { markings = new(); Markings[category] = markings; } markings.Add(marking); } /// /// Adds a category to this marking set. /// /// /// public List AddCategory(MarkingCategories category) { var markings = new List(); Markings.Add(category, markings); return markings; } /// /// Replace a marking at a given index in a marking category with another marking. /// /// The category to replace the marking in. /// The index of the marking. /// The marking to insert. public void Replace(MarkingCategories category, int index, Marking marking) { if (index < 0 || !Markings.TryGetValue(category, out var markings) || index >= markings.Count) { return; } markings[index] = marking; } /// /// Remove a marking by category and ID. /// /// The category that contains the marking. /// The marking's ID. /// True if removed, false otherwise. public bool Remove(MarkingCategories category, string id) { if (!Markings.TryGetValue(category, out var markings)) { return false; } for (var i = 0; i < markings.Count; i++) { if (markings[i].MarkingId != id) { continue; } if (!markings[i].Forced && Points.TryGetValue(category, out var points)) { points.Points++; } markings.RemoveAt(i); return true; } return false; } /// /// Remove a marking by category and index. /// /// The category that contains the marking. /// The marking's index. /// True if removed, false otherwise. public void Remove(MarkingCategories category, int idx) { if (!Markings.TryGetValue(category, out var markings)) { return; } if (idx < 0 || idx >= markings.Count) { return; } if (!markings[idx].Forced && Points.TryGetValue(category, out var points)) { points.Points++; } markings.RemoveAt(idx); } /// /// Remove an entire category from this marking set. /// /// The category to remove. /// True if removed, false otherwise. public bool RemoveCategory(MarkingCategories category) { if (!Markings.TryGetValue(category, out var markings)) { return false; } if (Points.TryGetValue(category, out var points)) { foreach (var marking in markings) { if (marking.Forced) { continue; } points.Points++; } } Markings.Remove(category); return true; } /// /// Clears all markings from this marking set. /// public void Clear() { foreach (var category in Enum.GetValues()) { RemoveCategory(category); } } /// /// Attempt to find the index of a marking in a category by ID. /// /// The category to search in. /// The ID to search for. /// The index of the marking, otherwise a negative number. public int FindIndexOf(MarkingCategories category, string id) { if (!Markings.TryGetValue(category, out var markings)) { return -1; } return markings.FindIndex(m => m.MarkingId == id); } /// /// Tries to get an entire category from this marking set. /// /// The category to fetch. /// A read only list of the all markings in that category. /// True if successful, false otherwise. public bool TryGetCategory(MarkingCategories category, [NotNullWhen(true)] out IReadOnlyList? markings) { markings = null; if (Markings.TryGetValue(category, out var list)) { markings = list; return true; } return false; } /// /// Tries to get a marking from this marking set, by category. /// /// The category to search in. /// The ID to search for. /// The marking, if it was retrieved. /// True if successful, false otherwise. public bool TryGetMarking(MarkingCategories category, string id, [NotNullWhen(true)] out Marking? marking) { marking = null; if (!Markings.TryGetValue(category, out var markings)) { return false; } foreach (var m in markings) { if (m.MarkingId == id) { marking = m; return true; } } return false; } /// /// Shifts a marking's rank towards the front of the list /// /// The category to shift in. /// Index of the marking. public void ShiftRankUp(MarkingCategories category, int idx) { if (!Markings.TryGetValue(category, out var markings)) { return; } if (idx < 0 || idx >= markings.Count || idx - 1 < 0) { return; } (markings[idx - 1], markings[idx]) = (markings[idx], markings[idx - 1]); } /// /// Shifts a marking's rank upwards from the end of the list /// /// The category to shift in. /// Index of the marking from the end public void ShiftRankUpFromEnd(MarkingCategories category, int idx) { if (!Markings.TryGetValue(category, out var markings)) { return; } ShiftRankUp(category, markings.Count - idx - 1); } /// /// Shifts a marking's rank towards the end of the list /// /// The category to shift in. /// Index of the marking. public void ShiftRankDown(MarkingCategories category, int idx) { if (!Markings.TryGetValue(category, out var markings)) { return; } if (idx < 0 || idx >= markings.Count || idx + 1 >= markings.Count) { return; } (markings[idx + 1], markings[idx]) = (markings[idx], markings[idx + 1]); } /// /// Shifts a marking's rank downwards from the end of the list /// /// The category to shift in. /// Index of the marking from the end public void ShiftRankDownFromEnd(MarkingCategories category, int idx) { if (!Markings.TryGetValue(category, out var markings)) { return; } ShiftRankDown(category, markings.Count - idx - 1); } /// /// Gets all markings in this set as an enumerator. Lists will be organized, but categories may be in any order. /// /// An enumerator of s. public ForwardMarkingEnumerator GetForwardEnumerator() { var markings = new List(); foreach (var (_, list) in Markings) { markings.AddRange(list); } return new ForwardMarkingEnumerator(markings); } /// /// Gets an enumerator of markings in this set, but only for one category. /// /// The category to fetch. /// An enumerator of s in that category. public ForwardMarkingEnumerator GetForwardEnumerator(MarkingCategories category) { var markings = new List(); if (Markings.TryGetValue(category, out var listing)) { markings = new(listing); } return new ForwardMarkingEnumerator(markings); } /// /// Gets all markings in this set as an enumerator, but in reverse order. Lists will be in reverse order, but categories may be in any order. /// /// An enumerator of s in reverse. public ReverseMarkingEnumerator GetReverseEnumerator() { var markings = new List(); foreach (var (_, list) in Markings) { markings.AddRange(list); } return new ReverseMarkingEnumerator(markings); } /// /// Gets an enumerator of markings in this set in reverse order, but only for one category. /// /// The category to fetch. /// An enumerator of s in that category, in reverse order. public ReverseMarkingEnumerator GetReverseEnumerator(MarkingCategories category) { var markings = new List(); if (Markings.TryGetValue(category, out var listing)) { markings = new(listing); } return new ReverseMarkingEnumerator(markings); } public bool CategoryEquals(MarkingCategories category, MarkingSet other) { if (!Markings.TryGetValue(category, out var markings) || !other.Markings.TryGetValue(category, out var markingsOther)) { return false; } return markings.SequenceEqual(markingsOther); } public bool Equals(MarkingSet other) { foreach (var (category, _) in Markings) { if (!CategoryEquals(category, other)) { return false; } } return true; } /// /// Gets a difference of marking categories between two marking sets /// /// The other marking set. /// Enumerator of marking categories that were different between the two. public IEnumerable CategoryDifference(MarkingSet other) { foreach (var (category, _) in Markings) { if (!CategoryEquals(category, other)) { yield return category; } } } } public sealed class ForwardMarkingEnumerator : IEnumerable { private List _markings; public ForwardMarkingEnumerator(List markings) { _markings = markings; } public IEnumerator GetEnumerator() { return new MarkingsEnumerator(_markings, false); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public sealed class ReverseMarkingEnumerator : IEnumerable { private List _markings; public ReverseMarkingEnumerator(List markings) { _markings = markings; } public IEnumerator GetEnumerator() { return new MarkingsEnumerator(_markings, true); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } public sealed class MarkingsEnumerator : IEnumerator { private List _markings; private bool _reverse; int position; public MarkingsEnumerator(List markings, bool reverse) { _markings = markings; _reverse = reverse; if (_reverse) { position = _markings.Count; } else { position = -1; } } public bool MoveNext() { if (_reverse) { position--; return (position >= 0); } else { position++; return (position < _markings.Count); } } public void Reset() { if (_reverse) { position = _markings.Count; } else { position = -1; } } public void Dispose() {} object IEnumerator.Current { get => _markings[position]; } public Marking Current { get => _markings[position]; } }