Validate prototype ids in c# fields (#18224)

This commit is contained in:
Leon Friedrich
2023-07-30 05:34:51 +12:00
committed by GitHub
parent d4a85afb88
commit 385b587cfc
21 changed files with 116 additions and 74 deletions

View File

@@ -404,7 +404,7 @@ namespace Content.Client.Construction.UI
return; return;
} }
if (_selected == null || _selected.Mirror == String.Empty) if (_selected == null || _selected.Mirror == null)
{ {
return; return;
} }

View File

@@ -197,7 +197,7 @@ public sealed class DecalPlacementSystem : EntitySystem
public sealed class PlaceDecalActionEvent : WorldTargetActionEvent public sealed class PlaceDecalActionEvent : WorldTargetActionEvent
{ {
[DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer<DecalPrototype>))] [DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer<DecalPrototype>), required:true)]
public string DecalId = string.Empty; public string DecalId = string.Empty;
[DataField("color")] [DataField("color")]

View File

@@ -1,5 +1,7 @@
using Content.Shared.Salvage; using Content.Shared.Salvage;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
namespace Content.Client.Salvage;
[NetworkedComponent, RegisterComponent] [NetworkedComponent, RegisterComponent]
public sealed class SalvageMagnetComponent : SharedSalvageMagnetComponent {} public sealed class SalvageMagnetComponent : SharedSalvageMagnetComponent {}

View File

@@ -24,7 +24,7 @@ namespace Content.Server.Advertise
/// <summary> /// <summary>
/// The identifier for the advertisements pack prototype. /// The identifier for the advertisements pack prototype.
/// </summary> /// </summary>
[DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer<AdvertisementsPackPrototype>))] [DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer<AdvertisementsPackPrototype>), required: true)]
public string PackPrototypeId { get; } = string.Empty; public string PackPrototypeId { get; } = string.Empty;
/// <summary> /// <summary>

View File

@@ -1,5 +1,5 @@
using Content.Shared.Tools;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Ame.Components; namespace Content.Server.Ame.Components;
@@ -19,6 +19,6 @@ public sealed class AmePartComponent : Component
/// <summary> /// <summary>
/// The tool quality required to deploy the packaged AME shielding. /// The tool quality required to deploy the packaged AME shielding.
/// </summary> /// </summary>
[DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string QualityNeeded = "Pulsing"; public string QualityNeeded = "Pulsing";
} }

View File

@@ -1,3 +1,6 @@
using Content.Shared.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components; namespace Content.Server.GameTicking.Rules.Components;
/// <summary> /// <summary>
@@ -8,12 +11,12 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent] [RegisterComponent]
public sealed class NukeOperativeSpawnerComponent : Component public sealed class NukeOperativeSpawnerComponent : Component
{ {
[DataField("name")] [DataField("name", required:true)]
public string OperativeName = ""; public string OperativeName = default!;
[DataField("rolePrototype")] [DataField("rolePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<AntagPrototype>), required:true)]
public string OperativeRolePrototype = ""; public string OperativeRolePrototype = default!;
[DataField("startingGearPrototype")] [DataField("startingGearPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<StartingGearPrototype>), required:true)]
public string OperativeStartingGear = ""; public string OperativeStartingGear = default!;
} }

View File

@@ -6,6 +6,7 @@ using Content.Shared.Roles;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
@@ -43,19 +44,19 @@ public sealed class NukeopsRuleComponent : Component
[DataField("spawnOutpost")] [DataField("spawnOutpost")]
public bool SpawnOutpost = true; public bool SpawnOutpost = true;
[DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))] [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnPointPrototype = "SpawnPointNukies"; public string SpawnPointPrototype = "SpawnPointNukies";
[DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))] [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative";
[DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))] [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string CommanderRolePrototype = "NukeopsCommander"; public string CommanderRolePrototype = "NukeopsCommander";
[DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))] [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string OperativeRoleProto = "Nukeops"; public string OperativeRoleProto = "Nukeops";
[DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))] [DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string MedicRoleProto = "NukeopsMedic"; public string MedicRoleProto = "NukeopsMedic";
[DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))] [DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]

View File

@@ -574,6 +574,15 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
// todo: this is kinda awful for multi-nukies // todo: this is kinda awful for multi-nukies
foreach (var nukeops in EntityQuery<NukeopsRuleComponent>()) foreach (var nukeops in EntityQuery<NukeopsRuleComponent>())
{ {
if (nukeOpSpawner.OperativeName == null
|| nukeOpSpawner.OperativeStartingGear == null
|| nukeOpSpawner.OperativeRolePrototype == null)
{
// I have no idea what is going on with nuke ops code, but I'm pretty sure this shouldn't be possible.
Log.Error($"Invalid nuke op spawner: {ToPrettyString(spawner)}");
continue;
}
SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile, nukeops); SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.OperativeStartingGear, profile, nukeops);
nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype); nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype);

View File

@@ -8,8 +8,8 @@ namespace Content.Server.Objectives.Requirements
[DataDefinition] [DataDefinition]
public sealed class NotRoleRequirement : IObjectiveRequirement public sealed class NotRoleRequirement : IObjectiveRequirement
{ {
[DataField("roleId", customTypeSerializer:typeof(PrototypeIdSerializer<JobPrototype>))] [DataField("roleId", customTypeSerializer:typeof(PrototypeIdSerializer<JobPrototype>), required:true)]
private string roleId = ""; private string _roleId = default!;
/// <summary> /// <summary>
/// This requirement is met if the traitor is NOT the roleId, and fails if they are. /// This requirement is met if the traitor is NOT the roleId, and fails if they are.
@@ -19,7 +19,7 @@ namespace Content.Server.Objectives.Requirements
if (mind.CurrentJob == null) // no job no problems if (mind.CurrentJob == null) // no job no problems
return true; return true;
return (mind.CurrentJob.Prototype.ID != roleId); return (mind.CurrentJob.Prototype.ID != _roleId);
} }
} }
} }

View File

@@ -34,6 +34,10 @@ public sealed record BodyPrototypeSlot
public readonly HashSet<string> Connections = new(); public readonly HashSet<string> Connections = new();
public readonly Dictionary<string, string> Organs = new(); public readonly Dictionary<string, string> Organs = new();
public BodyPrototypeSlot() : this(null, null, null)
{
}
public BodyPrototypeSlot(string? part, HashSet<string>? connections, Dictionary<string, string>? organs) public BodyPrototypeSlot(string? part, HashSet<string>? connections, Dictionary<string, string>? organs)
{ {
Part = part; Part = part;

View File

@@ -20,7 +20,8 @@ public readonly record struct CargoBountyData(int Id, string Bounty, TimeSpan En
/// <summary> /// <summary>
/// The prototype containing information about the bounty. /// The prototype containing information about the bounty.
/// </summary> /// </summary>
[DataField("bounty", customTypeSerializer: typeof(PrototypeIdSerializer<CargoBountyPrototype>)), ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("bounty", customTypeSerializer: typeof(PrototypeIdSerializer<CargoBountyPrototype>), required:true)]
public readonly string Bounty = Bounty; public readonly string Bounty = Bounty;
/// <summary> /// <summary>
@@ -28,4 +29,8 @@ public readonly record struct CargoBountyData(int Id, string Bounty, TimeSpan En
/// </summary> /// </summary>
[DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))] [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public readonly TimeSpan EndTime = EndTime; public readonly TimeSpan EndTime = EndTime;
public CargoBountyData() : this(default, string.Empty, default)
{
}
} }

View File

@@ -676,9 +676,9 @@ namespace Content.Shared.Chemistry.Components
[DataDefinition] [DataDefinition]
public readonly struct ReagentQuantity: IComparable<ReagentQuantity> public readonly struct ReagentQuantity: IComparable<ReagentQuantity>
{ {
[DataField("ReagentId", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentPrototype>))] [DataField("ReagentId", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentPrototype>), required:true)]
public readonly string ReagentId; public readonly string ReagentId;
[DataField("Quantity")] [DataField("Quantity", required:true)]
public readonly FixedPoint2 Quantity; public readonly FixedPoint2 Quantity;
public ReagentQuantity(string reagentId, FixedPoint2 quantity) public ReagentQuantity(string reagentId, FixedPoint2 quantity)
@@ -687,6 +687,10 @@ namespace Content.Shared.Chemistry.Components
Quantity = quantity; Quantity = quantity;
} }
public ReagentQuantity() : this(string.Empty, default)
{
}
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public override string ToString() public override string ToString()
{ {

View File

@@ -14,6 +14,9 @@ public abstract class ClothingSystem : EntitySystem
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly TagSystem _tagSystem = default!;
[ValidatePrototypeId<TagPrototype>]
private const string HairTag = "HidesHair";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -27,14 +30,14 @@ public abstract class ClothingSystem : EntitySystem
protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args) protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args)
{ {
component.InSlot = args.Slot; component.InSlot = args.Slot;
if (args.Slot == "head" && _tagSystem.HasTag(args.Equipment, "HidesHair")) if (args.Slot == "head" && _tagSystem.HasTag(args.Equipment, HairTag))
_humanoidSystem.SetLayerVisibility(args.Equipee, HumanoidVisualLayers.Hair, false); _humanoidSystem.SetLayerVisibility(args.Equipee, HumanoidVisualLayers.Hair, false);
} }
protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args) protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args)
{ {
component.InSlot = null; component.InSlot = null;
if (args.Slot == "head" && _tagSystem.HasTag(args.Equipment, "HidesHair")) if (args.Slot == "head" && _tagSystem.HasTag(args.Equipment, HairTag))
_humanoidSystem.SetLayerVisibility(args.Equipee, HumanoidVisualLayers.Hair, true); _humanoidSystem.SetLayerVisibility(args.Equipee, HumanoidVisualLayers.Hair, true);
} }

View File

@@ -31,7 +31,7 @@ public sealed class ConstructionPrototype : IPrototype
/// <summary> /// <summary>
/// The <see cref="ConstructionGraphPrototype"/> this construction will be using. /// The <see cref="ConstructionGraphPrototype"/> this construction will be using.
/// </summary> /// </summary>
[DataField("graph", customTypeSerializer:typeof(PrototypeIdSerializer<ConstructionGraphPrototype>))] [DataField("graph", customTypeSerializer:typeof(PrototypeIdSerializer<ConstructionGraphPrototype>), required: true)]
public string Graph = string.Empty; public string Graph = string.Empty;
/// <summary> /// <summary>
@@ -85,7 +85,7 @@ public sealed class ConstructionPrototype : IPrototype
/// Construction to replace this construction with when the current one is 'flipped' /// Construction to replace this construction with when the current one is 'flipped'
/// </summary> /// </summary>
[DataField("mirror", customTypeSerializer:typeof(PrototypeIdSerializer<ConstructionPrototype>))] [DataField("mirror", customTypeSerializer:typeof(PrototypeIdSerializer<ConstructionPrototype>))]
public string Mirror = string.Empty; public string? Mirror;
public IReadOnlyList<IConstructionCondition> Conditions => _conditions; public IReadOnlyList<IConstructionCondition> Conditions => _conditions;
public IReadOnlyList<SpriteSpecifier> Layers => _layers ?? new List<SpriteSpecifier>{Icon}; public IReadOnlyList<SpriteSpecifier> Layers => _layers ?? new List<SpriteSpecifier>{Icon};

View File

@@ -14,9 +14,6 @@ namespace Content.Server.Devour.Components;
[Access(typeof(SharedDevourSystem))] [Access(typeof(SharedDevourSystem))]
public sealed class DevourerComponent : Component public sealed class DevourerComponent : Component
{ {
[DataField("devourActionId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityTargetActionPrototype>))]
public string DevourActionId = "Devour";
[DataField("devourAction")] [DataField("devourAction")]
public EntityTargetAction? DevourAction; public EntityTargetAction? DevourAction;

View File

@@ -14,13 +14,13 @@ namespace Content.Shared.Roles
/// if empty, there is no skirt override - instead the uniform provided in equipment is added. /// if empty, there is no skirt override - instead the uniform provided in equipment is added.
/// </summary> /// </summary>
[DataField("innerclothingskirt", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("innerclothingskirt", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _innerClothingSkirt = string.Empty; private string? _innerClothingSkirt;
[DataField("satchel", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("satchel", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _satchel = string.Empty; private string? _satchel;
[DataField("duffelbag", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("duffelbag", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _duffelbag = string.Empty; private string? _duffelbag;
public IReadOnlyDictionary<string, string> Inhand => _inHand; public IReadOnlyDictionary<string, string> Inhand => _inHand;
/// <summary> /// <summary>

View File

@@ -23,6 +23,6 @@ public sealed class SalvageDungeonMod : IPrototype, IBiomeSpecificMod
/// <summary> /// <summary>
/// The config to use for spawning the dungeon. /// The config to use for spawning the dungeon.
/// </summary> /// </summary>
[DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer<DungeonConfigPrototype>))] [DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer<DungeonConfigPrototype>), required: true)]
public string Proto = string.Empty; public string Proto = string.Empty;
} }

View File

@@ -49,6 +49,6 @@ namespace Content.Shared.Traits
/// Gear that is given to the player, when they pick this trait. /// Gear that is given to the player, when they pick this trait.
/// </summary> /// </summary>
[DataField("traitGear", required: false, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("traitGear", required: false, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string TraitGear = string.Empty; public string? TraitGear;
} }
} }

View File

@@ -14,7 +14,7 @@ namespace Content.Shared.VendingMachines
/// <summary> /// <summary>
/// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/> /// PrototypeID for the vending machine's inventory, see <see cref="VendingMachineInventoryPrototype"/>
/// </summary> /// </summary>
[DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<VendingMachineInventoryPrototype>))] [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer<VendingMachineInventoryPrototype>), required: true)]
public string PackPrototypeId = string.Empty; public string PackPrototypeId = string.Empty;
/// <summary> /// <summary>

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Robust.UnitTesting;
namespace Content.YAMLLinter namespace Content.YAMLLinter
{ {
@@ -17,9 +18,11 @@ namespace Content.YAMLLinter
var stopwatch = new Stopwatch(); var stopwatch = new Stopwatch();
stopwatch.Start(); stopwatch.Start();
var errors = await RunValidation(); var (errors, fieldErrors) = await RunValidation();
if (errors.Count == 0) var count = errors.Count + fieldErrors.Count;
if (count == 0)
{ {
Console.WriteLine($"No errors found in {(int) stopwatch.Elapsed.TotalMilliseconds} ms."); Console.WriteLine($"No errors found in {(int) stopwatch.Elapsed.TotalMilliseconds} ms.");
return 0; return 0;
@@ -33,80 +36,92 @@ namespace Content.YAMLLinter
} }
} }
Console.WriteLine($"{errors.Count} errors found in {(int) stopwatch.Elapsed.TotalMilliseconds} ms."); foreach (var error in fieldErrors)
{
Console.WriteLine(error);
}
Console.WriteLine($"{count} errors found in {(int) stopwatch.Elapsed.TotalMilliseconds} ms.");
return -1; return -1;
} }
private static async Task<Dictionary<string, HashSet<ErrorNode>>> ValidateClient() private static async Task<(Dictionary<string, HashSet<ErrorNode>> YamlErrors, List<string> FieldErrors)>
ValidateClient()
{ {
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Disconnected = true }); await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Disconnected = true });
var client = pairTracker.Pair.Client; var client = pairTracker.Pair.Client;
var result = await ValidateInstance(client);
var cPrototypeManager = client.ResolveDependency<IPrototypeManager>();
var clientErrors = new Dictionary<string, HashSet<ErrorNode>>();
await client.WaitPost(() =>
{
clientErrors = cPrototypeManager.ValidateDirectory(new ResPath("/Prototypes"));
});
await pairTracker.CleanReturnAsync(); await pairTracker.CleanReturnAsync();
return result;
return clientErrors;
} }
private static async Task<Dictionary<string, HashSet<ErrorNode>>> ValidateServer() private static async Task<(Dictionary<string, HashSet<ErrorNode>> YamlErrors, List<string> FieldErrors)>
ValidateServer()
{ {
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Disconnected = true }); await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, NoClient = true });
var server = pairTracker.Pair.Server; var server = pairTracker.Pair.Server;
var result = await ValidateInstance(server);
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var serverErrors = new Dictionary<string, HashSet<ErrorNode>>();
await server.WaitPost(() =>
{
serverErrors = sPrototypeManager.ValidateDirectory(new ResPath("/Prototypes"));
});
await pairTracker.CleanReturnAsync(); await pairTracker.CleanReturnAsync();
return result;
return serverErrors;
} }
public static async Task<Dictionary<string, HashSet<ErrorNode>>> RunValidation() private static async Task<(Dictionary<string, HashSet<ErrorNode>>, List<string>)> ValidateInstance(
RobustIntegrationTest.IntegrationInstance instance)
{ {
var allErrors = new Dictionary<string, HashSet<ErrorNode>>(); var protoMan = instance.ResolveDependency<IPrototypeManager>();
Dictionary<string, HashSet<ErrorNode>> yamlErrors = default!;
List<string> fieldErrors = default!;
await instance.WaitPost(() =>
{
yamlErrors = protoMan.ValidateDirectory(new ResPath("/Prototypes"), out var prototypes);
fieldErrors = protoMan.ValidateFields(prototypes);
});
return (yamlErrors, fieldErrors);
}
public static async Task<(Dictionary<string, HashSet<ErrorNode>> YamlErrors , List<string> FieldErrors)>
RunValidation()
{
var yamlErrors = new Dictionary<string, HashSet<ErrorNode>>();
var serverErrors = await ValidateServer(); var serverErrors = await ValidateServer();
var clientErrors = await ValidateClient(); var clientErrors = await ValidateClient();
foreach (var (key, val) in serverErrors) foreach (var (key, val) in serverErrors.YamlErrors)
{ {
// Include all server errors marked as always relevant // Include all server errors marked as always relevant
var newErrors = val.Where(n => n.AlwaysRelevant).ToHashSet(); var newErrors = val.Where(n => n.AlwaysRelevant).ToHashSet();
// We include sometimes-relevant errors if they exist both for the client & server // We include sometimes-relevant errors if they exist both for the client & server
if (clientErrors.TryGetValue(key, out var clientVal)) if (clientErrors.Item1.TryGetValue(key, out var clientVal))
newErrors.UnionWith(val.Intersect(clientVal)); newErrors.UnionWith(val.Intersect(clientVal));
if (newErrors.Count != 0) if (newErrors.Count != 0)
allErrors[key] = newErrors; yamlErrors[key] = newErrors;
} }
// Finally add any always-relevant client errors. // Next add any always-relevant client errors.
foreach (var (key, val) in clientErrors) foreach (var (key, val) in clientErrors.YamlErrors)
{ {
var newErrors = val.Where(n => n.AlwaysRelevant).ToHashSet(); var newErrors = val.Where(n => n.AlwaysRelevant).ToHashSet();
if (newErrors.Count == 0) if (newErrors.Count == 0)
continue; continue;
if (allErrors.TryGetValue(key, out var errors)) if (yamlErrors.TryGetValue(key, out var errors))
errors.UnionWith(val.Where(n => n.AlwaysRelevant)); errors.UnionWith(val.Where(n => n.AlwaysRelevant));
else else
allErrors[key] = newErrors; yamlErrors[key] = newErrors;
} }
return allErrors; // Finally, combine the prototype ID field errors.
var fieldErrors = serverErrors.FieldErrors
.Concat(clientErrors.FieldErrors)
.Distinct()
.ToList();
return (yamlErrors, fieldErrors);
} }
} }
} }

View File

@@ -66,7 +66,6 @@
rules: You are a syndicate operative tasked with the destruction of the station. As an antagonist, do whatever is required to complete this task. rules: You are a syndicate operative tasked with the destruction of the station. As an antagonist, do whatever is required to complete this task.
- type: GhostRoleMobSpawner - type: GhostRoleMobSpawner
prototype: MobHumanNukeOp prototype: MobHumanNukeOp
- type: NukeOperativeSpawner
- type: Sprite - type: Sprite
sprite: Markers/jobs.rsi sprite: Markers/jobs.rsi
layers: layers: