fish-station/Content.Shared/Humanoid/Markings/MarkingsSet.cs
Vigers Ray d0f5e8d361 Merge remote-tracking branch 'space-wizards/master'
# Conflicts:
#	.github/workflows/validate-rgas.yml
#	.github/workflows/validate-rsis.yml
#	Content.Client/Changelog/ChangelogTab.xaml.cs
#	Content.Client/Humanoid/HumanoidAppearanceSystem.cs
#	Content.Client/Options/UI/OptionsMenu.xaml.cs
#	Content.Client/SubFloor/SubFloorHideSystem.cs
#	Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
#	Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
#	Content.Server/Antag/AntagSelectionSystem.cs
#	Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs
#	Content.Server/Explosion/EntitySystems/TriggerSystem.cs
#	Content.Server/Mindshield/MindShieldSystem.cs
#	Content.Server/Objectives/Systems/KillPersonConditionSystem.cs
#	Content.Server/Standing/StandingStateSystem.cs
#	Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs
#	Content.Server/VendingMachines/VendingMachineSystem.cs
#	Content.Server/Zombies/ZombieSystem.Transform.cs
#	Content.Server/Zombies/ZombieSystem.cs
#	Content.Shared/Damage/Systems/DamageableSystem.cs
#	Content.Shared/Humanoid/HumanoidAppearanceComponent.cs
#	Content.Shared/Humanoid/Markings/MarkingManager.cs
#	Content.Shared/Humanoid/NamingSystem.cs
#	Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
#	Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
#	Content.Shared/Implants/Components/SubdermalImplantComponent.cs
#	Content.Shared/Standing/StandingStateSystem.cs
#	Content.Shared/VendingMachines/SharedVendingMachineSystem.cs
#	Content.Shared/VendingMachines/VendingMachineComponent.cs
#	Resources/Audio/Lobby/attributions.yml
#	Resources/Locale/en-US/_strings/administration/commands/forceghost.ftl
#	Resources/Locale/en-US/_strings/clothing/components/fire-protection-component.ftl
#	Resources/Locale/en-US/_strings/components/storage-voice-control-component.ftl
#	Resources/Locale/en-US/_strings/prototypes/roles/antags.ftl
#	Resources/Locale/en-US/_strings/reagents/meta/physical-desc.ftl
#	Resources/Locale/en-US/lathe/lathe-categories.ftl
#	Resources/Prototypes/Access/command.yml
#	Resources/Prototypes/Catalog/Fills/Books/bookshelf.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/misc.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/chemvend.yml
#	Resources/Prototypes/Catalog/thief_toolbox_sets.yml
#	Resources/Prototypes/Datasets/Names/Operation_prefix.yml
#	Resources/Prototypes/Datasets/Names/Operation_suffix.yml
#	Resources/Prototypes/Datasets/Names/arachnid_first.yml
#	Resources/Prototypes/Datasets/Names/arachnid_last.yml
#	Resources/Prototypes/Datasets/Names/borg.yml
#	Resources/Prototypes/Datasets/Names/cargo_shuttle.yml
#	Resources/Prototypes/Datasets/Names/clown.yml
#	Resources/Prototypes/Datasets/Names/diona.yml
#	Resources/Prototypes/Datasets/Names/fake_human_first.yml
#	Resources/Prototypes/Datasets/Names/fake_human_last.yml
#	Resources/Prototypes/Datasets/Names/first.yml
#	Resources/Prototypes/Datasets/Names/first_female.yml
#	Resources/Prototypes/Datasets/Names/first_male.yml
#	Resources/Prototypes/Datasets/Names/fortunes.yml
#	Resources/Prototypes/Datasets/Names/hologram.yml
#	Resources/Prototypes/Datasets/Names/last.yml
#	Resources/Prototypes/Datasets/Names/military.yml
#	Resources/Prototypes/Datasets/Names/moth_first_female.yml
#	Resources/Prototypes/Datasets/Names/moth_first_male.yml
#	Resources/Prototypes/Datasets/Names/moth_last.yml
#	Resources/Prototypes/Datasets/Names/mushman_first.yml
#	Resources/Prototypes/Datasets/Names/mushman_last.yml
#	Resources/Prototypes/Datasets/Names/ninja.yml
#	Resources/Prototypes/Datasets/Names/ninja_title.yml
#	Resources/Prototypes/Datasets/Names/regalrat.yml
#	Resources/Prototypes/Datasets/Names/reptilian_female.yml
#	Resources/Prototypes/Datasets/Names/reptilian_male.yml
#	Resources/Prototypes/Datasets/Names/revenant.yml
#	Resources/Prototypes/Datasets/Names/skeleton_first.yml
#	Resources/Prototypes/Datasets/Names/syndicate.yml
#	Resources/Prototypes/Datasets/Names/vox.yml
#	Resources/Prototypes/Datasets/Names/wizard_first.yml
#	Resources/Prototypes/Datasets/Names/wizard_last.yml
#	Resources/Prototypes/Datasets/adjectives.yml
#	Resources/Prototypes/Datasets/corporations.yml
#	Resources/Prototypes/Datasets/criminal_records.yml
#	Resources/Prototypes/Datasets/rat_king_commands.yml
#	Resources/Prototypes/Datasets/verbs.yml
#	Resources/Prototypes/Entities/Clothing/Head/specific.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/specific.yml
#	Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml
#	Resources/Prototypes/Entities/Mobs/Player/human.yml
#	Resources/Prototypes/Entities/Mobs/Player/humanoid.yml
#	Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
#	Resources/Prototypes/Entities/Mobs/Species/moth.yml
#	Resources/Prototypes/Entities/Mobs/Species/vox.yml
#	Resources/Prototypes/Entities/Objects/Devices/pda.yml
#	Resources/Prototypes/Entities/Objects/Misc/paper.yml
#	Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
#	Resources/Prototypes/Entities/Stations/nanotrasen.yml
#	Resources/Prototypes/Entities/Structures/Furniture/carpets.yml
#	Resources/Prototypes/Entities/Structures/Storage/Closets/wall_lockers.yml
#	Resources/Prototypes/GameRules/roundstart.yml
#	Resources/Prototypes/Procedural/salvage_loot.yml
#	Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml
#	Resources/Prototypes/Recipes/Lathes/biogen.yml
#	Resources/Prototypes/Recipes/Lathes/categories.yml
#	Resources/Prototypes/Roles/Jobs/Security/warden.yml
#	Resources/Prototypes/Species/arachnid.yml
#	Resources/Prototypes/Species/diona.yml
#	Resources/Prototypes/Species/moth.yml
#	Resources/Prototypes/ai_factions.yml
#	Resources/Prototypes/game_presets.yml
#	Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml
#	Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml
#	Resources/ServerInfo/Guidebook/Service/Botany.xml
#	Resources/Textures/Clothing/Eyes/Glasses/glasses.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/jamjar.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/mercglasses.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/meson.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/outlawglasses.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/science.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/secglasses.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/sunglasses.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Glasses/thermal.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/inhand-left.png
#	Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/inhand-right.png
#	Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Hud/synd.rsi/meta.json
#	Resources/Textures/Clothing/Eyes/Hud/syndagent.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hats/capcap.rsi/inhand-left.png
#	Resources/Textures/Clothing/Head/Hats/capcap.rsi/inhand-right.png
#	Resources/Textures/Clothing/Head/Hats/capcap.rsi/meta.json
#	Resources/Textures/Interface/Misc/program_icons.rsi/meta.json
#	Resources/Textures/Objects/Misc/Lights/lights.rsi/meta.json
#	Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/aloe.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/apple.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/carrot.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/chanterelle.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/chili.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/eggplant.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/gatfruit.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/lemon.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/onion_red.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/potato.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/sugarcane.rsi/meta.json
#	Resources/Textures/Objects/Specific/Hydroponics/tomato.rsi/meta.json
#	Resources/Textures/Objects/Specific/Robotics/borgmodule.rsi/meta.json
#	Resources/Textures/Objects/Specific/Robotics/cyborg_parts.rsi/meta.json
#	Resources/Textures/Objects/Tools/spray_painter.rsi/meta.json
#	Resources/Textures/Objects/Weapons/Grenades/flashbang.rsi/meta.json
#	Resources/Textures/Objects/Weapons/Grenades/grenade.rsi/meta.json
#	Resources/Textures/Objects/Weapons/Grenades/nukenade.rsi/meta.json
#	Resources/migration.yml
#	Tools/actions_changelogs_since_last_run.py
2025-03-25 11:30:01 +03:00

869 lines
26 KiB
C#

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
/// <summary>
/// Marking set. For humanoid markings.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataDefinition]
[Serializable, NetSerializable]
public sealed partial class MarkingSet
{
/// <summary>
/// Every single marking in this set.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField("markings")]
public Dictionary<MarkingCategories, List<Marking>> Markings = new();
/// <summary>
/// Marking points for each category.
/// </summary>
[DataField("points")]
public Dictionary<MarkingCategories, MarkingPoints> Points = new();
public MarkingSet()
{}
/// <summary>
/// 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.
/// </summary>
/// <param name="markings">The lists of markings to use.</param>
/// <param name="pointsPrototype">The ID of the points dictionary prototype.</param>
public MarkingSet(List<Marking> 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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="markings">The list of markings to use.</param>
public MarkingSet(List<Marking> 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);
}
}
/// <summary>
/// Construct a MarkingSet only with a points dictionary.
/// </summary>
/// <param name="pointsPrototype">The ID of the points dictionary prototype.</param>
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);
}
/// <summary>
/// Construct a MarkingSet by deep cloning another set.
/// </summary>
/// <param name="other">The other marking set.</param>
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);
}
/// <summary>
/// Filters and colors markings based on species and it's restrictions in the marking's prototype from this marking set.
/// </summary>
/// <param name="species">The species to filter.</param>
/// <param name="skinColor">The skin color for recoloring (i.e. slimes). Use null if you want only filter markings</param>
/// <param name="markingManager">Marking manager.</param>
/// <param name="prototypeManager">Prototype manager.</param>
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<SpeciesPrototype>(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));
}
}
}
}
}
// Sunrise-Sponsors-Start
public void FilterSponsor(string[] sponsorMarkings, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null)
{
IoCManager.Resolve(ref markingManager);
IoCManager.Resolve(ref prototypeManager);
var toRemove = new List<(MarkingCategories category, string id)>();
foreach (var (category, list) in Markings)
{
foreach (var marking in list)
{
if (prototypeManager.TryIndex<MarkingPrototype>(marking.MarkingId, out var proto) && !proto.SponsorOnly)
{
return;
}
var allowedToHave = sponsorMarkings.Contains(marking.MarkingId);
if (!allowedToHave)
{
toRemove.Add((category, marking.MarkingId));
}
}
}
foreach (var marking in toRemove)
{
Remove(marking.category, marking.id);
}
}
// Sunrise-Sponsors-End
/// <summary>
/// Filters markings based on sex and it's restrictions in the marking's prototype from this marking set.
/// </summary>
/// <param name="sex">The species to filter.</param>
/// <param name="markingManager">Marking manager.</param>
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);
}
}
/// <summary>
/// Ensures that all markings in this set are valid.
/// </summary>
/// <param name="markingManager">Marking manager.</param>
public void EnsureValid(MarkingManager? markingManager = null)
{
IoCManager.Resolve(ref markingManager);
var toRemove = new List<int>();
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);
}
}
}
/// <summary>
/// Ensures that the default markings as defined by the marking point set in this marking set are applied.
/// </summary>
/// <param name="skinColor">Skin color for marking coloring.</param>
/// <param name="eyeColor">Eye color for marking coloring.</param>
/// <param name="hairColor">Hair color for marking coloring.</param>
/// <param name="markingManager">Marking manager.</param>
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++;
}
}
}
/// <summary>
/// How many points are left in this marking set's category
/// </summary>
/// <param name="category">The category to check</param>
/// <returns>A number equal or greater than zero if the category exists, -1 otherwise.</returns>
public int PointsLeft(MarkingCategories category)
{
if (!Points.TryGetValue(category, out var points))
{
return -1;
}
return points.Points;
}
/// <summary>
/// Add a marking to the front of the category's list of markings.
/// </summary>
/// <param name="category">Category to add the marking to.</param>
/// <param name="marking">The marking instance in question.</param>
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);
}
/// <summary>
/// Add a marking to the back of the category's list of markings.
/// </summary>
/// <param name="category"></param>
/// <param name="marking"></param>
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);
}
/// <summary>
/// Adds a category to this marking set.
/// </summary>
/// <param name="category"></param>
/// <returns></returns>
public List<Marking> AddCategory(MarkingCategories category)
{
var markings = new List<Marking>();
Markings.Add(category, markings);
return markings;
}
/// <summary>
/// Replace a marking at a given index in a marking category with another marking.
/// </summary>
/// <param name="category">The category to replace the marking in.</param>
/// <param name="index">The index of the marking.</param>
/// <param name="marking">The marking to insert.</param>
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;
}
/// <summary>
/// Remove a marking by category and ID.
/// </summary>
/// <param name="category">The category that contains the marking.</param>
/// <param name="id">The marking's ID.</param>
/// <returns>True if removed, false otherwise.</returns>
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;
}
/// <summary>
/// Remove a marking by category and index.
/// </summary>
/// <param name="category">The category that contains the marking.</param>
/// <param name="idx">The marking's index.</param>
/// <returns>True if removed, false otherwise.</returns>
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);
}
/// <summary>
/// Remove an entire category from this marking set.
/// </summary>
/// <param name="category">The category to remove.</param>
/// <returns>True if removed, false otherwise.</returns>
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;
}
/// <summary>
/// Clears all markings from this marking set.
/// </summary>
public void Clear()
{
foreach (var category in Enum.GetValues<MarkingCategories>())
{
RemoveCategory(category);
}
}
/// <summary>
/// Attempt to find the index of a marking in a category by ID.
/// </summary>
/// <param name="category">The category to search in.</param>
/// <param name="id">The ID to search for.</param>
/// <returns>The index of the marking, otherwise a negative number.</returns>
public int FindIndexOf(MarkingCategories category, string id)
{
if (!Markings.TryGetValue(category, out var markings))
{
return -1;
}
return markings.FindIndex(m => m.MarkingId == id);
}
/// <summary>
/// Tries to get an entire category from this marking set.
/// </summary>
/// <param name="category">The category to fetch.</param>
/// <param name="markings">A read only list of the all markings in that category.</param>
/// <returns>True if successful, false otherwise.</returns>
public bool TryGetCategory(MarkingCategories category, [NotNullWhen(true)] out IReadOnlyList<Marking>? markings)
{
markings = null;
if (Markings.TryGetValue(category, out var list))
{
markings = list;
return true;
}
return false;
}
/// <summary>
/// Tries to get a marking from this marking set, by category.
/// </summary>
/// <param name="category">The category to search in.</param>
/// <param name="id">The ID to search for.</param>
/// <param name="marking">The marking, if it was retrieved.</param>
/// <returns>True if successful, false otherwise.</returns>
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;
}
/// <summary>
/// Shifts a marking's rank towards the front of the list
/// </summary>
/// <param name="category">The category to shift in.</param>
/// <param name="idx">Index of the marking.</param>
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]);
}
/// <summary>
/// Shifts a marking's rank upwards from the end of the list
/// </summary>
/// <param name="category">The category to shift in.</param>
/// <param name="idx">Index of the marking from the end</param>
public void ShiftRankUpFromEnd(MarkingCategories category, int idx)
{
if (!Markings.TryGetValue(category, out var markings))
{
return;
}
ShiftRankUp(category, markings.Count - idx - 1);
}
/// <summary>
/// Shifts a marking's rank towards the end of the list
/// </summary>
/// <param name="category">The category to shift in.</param>
/// <param name="idx">Index of the marking.</param>
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]);
}
/// <summary>
/// Shifts a marking's rank downwards from the end of the list
/// </summary>
/// <param name="category">The category to shift in.</param>
/// <param name="idx">Index of the marking from the end</param>
public void ShiftRankDownFromEnd(MarkingCategories category, int idx)
{
if (!Markings.TryGetValue(category, out var markings))
{
return;
}
ShiftRankDown(category, markings.Count - idx - 1);
}
/// <summary>
/// Gets all markings in this set as an enumerator. Lists will be organized, but categories may be in any order.
/// </summary>
/// <returns>An enumerator of <see cref="Marking"/>s.</returns>
public ForwardMarkingEnumerator GetForwardEnumerator()
{
var markings = new List<Marking>();
foreach (var (_, list) in Markings)
{
markings.AddRange(list);
}
return new ForwardMarkingEnumerator(markings);
}
/// <summary>
/// Gets an enumerator of markings in this set, but only for one category.
/// </summary>
/// <param name="category">The category to fetch.</param>
/// <returns>An enumerator of <see cref="Marking"/>s in that category.</returns>
public ForwardMarkingEnumerator GetForwardEnumerator(MarkingCategories category)
{
var markings = new List<Marking>();
if (Markings.TryGetValue(category, out var listing))
{
markings = new(listing);
}
return new ForwardMarkingEnumerator(markings);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>An enumerator of <see cref="Marking"/>s in reverse.</returns>
public ReverseMarkingEnumerator GetReverseEnumerator()
{
var markings = new List<Marking>();
foreach (var (_, list) in Markings)
{
markings.AddRange(list);
}
return new ReverseMarkingEnumerator(markings);
}
/// <summary>
/// Gets an enumerator of markings in this set in reverse order, but only for one category.
/// </summary>
/// <param name="category">The category to fetch.</param>
/// <returns>An enumerator of <see cref="Marking"/>s in that category, in reverse order.</returns>
public ReverseMarkingEnumerator GetReverseEnumerator(MarkingCategories category)
{
var markings = new List<Marking>();
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;
}
/// <summary>
/// Gets a difference of marking categories between two marking sets
/// </summary>
/// <param name="other">The other marking set.</param>
/// <returns>Enumerator of marking categories that were different between the two.</returns>
public IEnumerable<MarkingCategories> CategoryDifference(MarkingSet other)
{
foreach (var (category, _) in Markings)
{
if (!CategoryEquals(category, other))
{
yield return category;
}
}
}
}
public sealed class ForwardMarkingEnumerator : IEnumerable<Marking>
{
private List<Marking> _markings;
public ForwardMarkingEnumerator(List<Marking> markings)
{
_markings = markings;
}
public IEnumerator<Marking> GetEnumerator()
{
return new MarkingsEnumerator(_markings, false);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public sealed class ReverseMarkingEnumerator : IEnumerable<Marking>
{
private List<Marking> _markings;
public ReverseMarkingEnumerator(List<Marking> markings)
{
_markings = markings;
}
public IEnumerator<Marking> GetEnumerator()
{
return new MarkingsEnumerator(_markings, true);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public sealed class MarkingsEnumerator : IEnumerator<Marking>
{
private List<Marking> _markings;
private bool _reverse;
int position;
public MarkingsEnumerator(List<Marking> 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];
}
}