diff --git a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs index 5912b7a84f..657892e216 100644 --- a/Content.Client/Construction/UI/ConstructionMenuPresenter.cs +++ b/Content.Client/Construction/UI/ConstructionMenuPresenter.cs @@ -404,7 +404,7 @@ namespace Content.Client.Construction.UI return; } - if (_selected == null || _selected.Mirror == String.Empty) + if (_selected == null || _selected.Mirror == null) { return; } diff --git a/Content.Client/Decals/DecalPlacementSystem.cs b/Content.Client/Decals/DecalPlacementSystem.cs index bc05c39c3d..937e10ab4c 100644 --- a/Content.Client/Decals/DecalPlacementSystem.cs +++ b/Content.Client/Decals/DecalPlacementSystem.cs @@ -197,7 +197,7 @@ public sealed class DecalPlacementSystem : EntitySystem public sealed class PlaceDecalActionEvent : WorldTargetActionEvent { - [DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer))] + [DataField("decalId", customTypeSerializer:typeof(PrototypeIdSerializer), required:true)] public string DecalId = string.Empty; [DataField("color")] diff --git a/Content.Client/Salvage/SalvageMagnetComponent.cs b/Content.Client/Salvage/SalvageMagnetComponent.cs index 44b8b9d958..a681c00d7b 100644 --- a/Content.Client/Salvage/SalvageMagnetComponent.cs +++ b/Content.Client/Salvage/SalvageMagnetComponent.cs @@ -1,5 +1,7 @@ using Content.Shared.Salvage; using Robust.Shared.GameStates; +namespace Content.Client.Salvage; + [NetworkedComponent, RegisterComponent] public sealed class SalvageMagnetComponent : SharedSalvageMagnetComponent {} diff --git a/Content.Server/Advertise/AdvertiseComponent.cs b/Content.Server/Advertise/AdvertiseComponent.cs index 0b49040bdf..027c90f7e5 100644 --- a/Content.Server/Advertise/AdvertiseComponent.cs +++ b/Content.Server/Advertise/AdvertiseComponent.cs @@ -24,7 +24,7 @@ namespace Content.Server.Advertise /// /// The identifier for the advertisements pack prototype. /// - [DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer))] + [DataField("pack", customTypeSerializer:typeof(PrototypeIdSerializer), required: true)] public string PackPrototypeId { get; } = string.Empty; /// diff --git a/Content.Server/Ame/Components/AmePartComponent.cs b/Content.Server/Ame/Components/AmePartComponent.cs index 3642da3913..9c9af18544 100644 --- a/Content.Server/Ame/Components/AmePartComponent.cs +++ b/Content.Server/Ame/Components/AmePartComponent.cs @@ -1,5 +1,5 @@ +using Content.Shared.Tools; using Robust.Shared.Audio; -using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Ame.Components; @@ -19,6 +19,6 @@ public sealed class AmePartComponent : Component /// /// The tool quality required to deploy the packaged AME shielding. /// - [DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer))] public string QualityNeeded = "Pulsing"; } diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs index c32a8569cb..b2f63bd1ff 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs @@ -1,3 +1,6 @@ +using Content.Shared.Roles; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + namespace Content.Server.GameTicking.Rules.Components; /// @@ -8,12 +11,12 @@ namespace Content.Server.GameTicking.Rules.Components; [RegisterComponent] public sealed class NukeOperativeSpawnerComponent : Component { - [DataField("name")] - public string OperativeName = ""; + [DataField("name", required:true)] + public string OperativeName = default!; - [DataField("rolePrototype")] - public string OperativeRolePrototype = ""; + [DataField("rolePrototype", customTypeSerializer:typeof(PrototypeIdSerializer), required:true)] + public string OperativeRolePrototype = default!; - [DataField("startingGearPrototype")] - public string OperativeStartingGear = ""; + [DataField("startingGearPrototype", customTypeSerializer:typeof(PrototypeIdSerializer), required:true)] + public string OperativeStartingGear = default!; } diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index f8f8b42ab6..185146cffd 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -6,6 +6,7 @@ using Content.Shared.Roles; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; @@ -43,19 +44,19 @@ public sealed class NukeopsRuleComponent : Component [DataField("spawnOutpost")] public bool SpawnOutpost = true; - [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] public string SpawnPointPrototype = "SpawnPointNukies"; - [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("ghostSpawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer))] public string GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; - [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("commanderRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] public string CommanderRolePrototype = "NukeopsCommander"; - [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] public string OperativeRoleProto = "Nukeops"; - [DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("medicRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] public string MedicRoleProto = "NukeopsMedic"; [DataField("commanderStartingGearProto", customTypeSerializer: typeof(PrototypeIdSerializer))] diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index 904ce00967..b72433cfc1 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -574,6 +574,15 @@ public sealed class NukeopsRuleSystem : GameRuleSystem // todo: this is kinda awful for multi-nukies foreach (var nukeops in EntityQuery()) { + 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); nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.OperativeRolePrototype); diff --git a/Content.Server/Objectives/Requirements/NotRoleRequirement.cs b/Content.Server/Objectives/Requirements/NotRoleRequirement.cs index ac27f4cb6d..9e9c4dfde2 100644 --- a/Content.Server/Objectives/Requirements/NotRoleRequirement.cs +++ b/Content.Server/Objectives/Requirements/NotRoleRequirement.cs @@ -8,8 +8,8 @@ namespace Content.Server.Objectives.Requirements [DataDefinition] public sealed class NotRoleRequirement : IObjectiveRequirement { - [DataField("roleId", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string roleId = ""; + [DataField("roleId", customTypeSerializer:typeof(PrototypeIdSerializer), required:true)] + private string _roleId = default!; /// /// 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 return true; - return (mind.CurrentJob.Prototype.ID != roleId); + return (mind.CurrentJob.Prototype.ID != _roleId); } } } diff --git a/Content.Shared/Body/Prototypes/BodyPrototype.cs b/Content.Shared/Body/Prototypes/BodyPrototype.cs index 867b24b284..05defb3f6e 100644 --- a/Content.Shared/Body/Prototypes/BodyPrototype.cs +++ b/Content.Shared/Body/Prototypes/BodyPrototype.cs @@ -34,6 +34,10 @@ public sealed record BodyPrototypeSlot public readonly HashSet Connections = new(); public readonly Dictionary Organs = new(); + public BodyPrototypeSlot() : this(null, null, null) + { + } + public BodyPrototypeSlot(string? part, HashSet? connections, Dictionary? organs) { Part = part; diff --git a/Content.Shared/Cargo/CargoBountyData.cs b/Content.Shared/Cargo/CargoBountyData.cs index 8b5abd1412..fb1d953d12 100644 --- a/Content.Shared/Cargo/CargoBountyData.cs +++ b/Content.Shared/Cargo/CargoBountyData.cs @@ -20,7 +20,8 @@ public readonly record struct CargoBountyData(int Id, string Bounty, TimeSpan En /// /// The prototype containing information about the bounty. /// - [DataField("bounty", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadWrite)] + [DataField("bounty", customTypeSerializer: typeof(PrototypeIdSerializer), required:true)] public readonly string Bounty = Bounty; /// @@ -28,4 +29,8 @@ public readonly record struct CargoBountyData(int Id, string Bounty, TimeSpan En /// [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer))] public readonly TimeSpan EndTime = EndTime; + + public CargoBountyData() : this(default, string.Empty, default) + { + } } diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs index cc01b8f3b1..629acca6c9 100644 --- a/Content.Shared/Chemistry/Components/Solution.cs +++ b/Content.Shared/Chemistry/Components/Solution.cs @@ -676,9 +676,9 @@ namespace Content.Shared.Chemistry.Components [DataDefinition] public readonly struct ReagentQuantity: IComparable { - [DataField("ReagentId", customTypeSerializer:typeof(PrototypeIdSerializer))] + [DataField("ReagentId", customTypeSerializer:typeof(PrototypeIdSerializer), required:true)] public readonly string ReagentId; - [DataField("Quantity")] + [DataField("Quantity", required:true)] public readonly FixedPoint2 Quantity; public ReagentQuantity(string reagentId, FixedPoint2 quantity) @@ -687,6 +687,10 @@ namespace Content.Shared.Chemistry.Components Quantity = quantity; } + public ReagentQuantity() : this(string.Empty, default) + { + } + [ExcludeFromCodeCoverage] public override string ToString() { diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index e8b694602c..1c69f1227c 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -14,6 +14,9 @@ public abstract class ClothingSystem : EntitySystem [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly TagSystem _tagSystem = default!; + [ValidatePrototypeId] + private const string HairTag = "HidesHair"; + public override void Initialize() { base.Initialize(); @@ -27,14 +30,14 @@ public abstract class ClothingSystem : EntitySystem protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args) { 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); } protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args) { 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); } diff --git a/Content.Shared/Construction/Prototypes/ConstructionPrototype.cs b/Content.Shared/Construction/Prototypes/ConstructionPrototype.cs index 7fde68b6d7..e249c1d11b 100644 --- a/Content.Shared/Construction/Prototypes/ConstructionPrototype.cs +++ b/Content.Shared/Construction/Prototypes/ConstructionPrototype.cs @@ -31,7 +31,7 @@ public sealed class ConstructionPrototype : IPrototype /// /// The this construction will be using. /// - [DataField("graph", customTypeSerializer:typeof(PrototypeIdSerializer))] + [DataField("graph", customTypeSerializer:typeof(PrototypeIdSerializer), required: true)] public string Graph = string.Empty; /// @@ -85,7 +85,7 @@ public sealed class ConstructionPrototype : IPrototype /// Construction to replace this construction with when the current one is 'flipped' /// [DataField("mirror", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string Mirror = string.Empty; + public string? Mirror; public IReadOnlyList Conditions => _conditions; public IReadOnlyList Layers => _layers ?? new List{Icon}; diff --git a/Content.Shared/Devour/Components/DevourerComponent.cs b/Content.Shared/Devour/Components/DevourerComponent.cs index 97085ca593..d2831683b0 100644 --- a/Content.Shared/Devour/Components/DevourerComponent.cs +++ b/Content.Shared/Devour/Components/DevourerComponent.cs @@ -14,9 +14,6 @@ namespace Content.Server.Devour.Components; [Access(typeof(SharedDevourSystem))] public sealed class DevourerComponent : Component { - [DataField("devourActionId", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string DevourActionId = "Devour"; - [DataField("devourAction")] public EntityTargetAction? DevourAction; diff --git a/Content.Shared/Roles/StartingGearPrototype.cs b/Content.Shared/Roles/StartingGearPrototype.cs index 164df05a9e..c515dbc250 100644 --- a/Content.Shared/Roles/StartingGearPrototype.cs +++ b/Content.Shared/Roles/StartingGearPrototype.cs @@ -14,13 +14,13 @@ namespace Content.Shared.Roles /// if empty, there is no skirt override - instead the uniform provided in equipment is added. /// [DataField("innerclothingskirt", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string _innerClothingSkirt = string.Empty; + private string? _innerClothingSkirt; [DataField("satchel", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string _satchel = string.Empty; + private string? _satchel; [DataField("duffelbag", customTypeSerializer:typeof(PrototypeIdSerializer))] - private string _duffelbag = string.Empty; + private string? _duffelbag; public IReadOnlyDictionary Inhand => _inHand; /// diff --git a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs index c30c2aa29a..06d2c6d4b1 100644 --- a/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs +++ b/Content.Shared/Salvage/Expeditions/Modifiers/SalvageDungeonMod.cs @@ -23,6 +23,6 @@ public sealed class SalvageDungeonMod : IPrototype, IBiomeSpecificMod /// /// The config to use for spawning the dungeon. /// - [DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)] public string Proto = string.Empty; } diff --git a/Content.Shared/Traits/TraitPrototype.cs b/Content.Shared/Traits/TraitPrototype.cs index 10645d0eda..33f092338e 100644 --- a/Content.Shared/Traits/TraitPrototype.cs +++ b/Content.Shared/Traits/TraitPrototype.cs @@ -49,6 +49,6 @@ namespace Content.Shared.Traits /// Gear that is given to the player, when they pick this trait. /// [DataField("traitGear", required: false, customTypeSerializer:typeof(PrototypeIdSerializer))] - public string TraitGear = string.Empty; + public string? TraitGear; } } diff --git a/Content.Shared/VendingMachines/VendingMachineComponent.cs b/Content.Shared/VendingMachines/VendingMachineComponent.cs index b648d36f4e..7be06d1d1c 100644 --- a/Content.Shared/VendingMachines/VendingMachineComponent.cs +++ b/Content.Shared/VendingMachines/VendingMachineComponent.cs @@ -14,7 +14,7 @@ namespace Content.Shared.VendingMachines /// /// PrototypeID for the vending machine's inventory, see /// - [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField("pack", customTypeSerializer: typeof(PrototypeIdSerializer), required: true)] public string PackPrototypeId = string.Empty; /// diff --git a/Content.YAMLLinter/Program.cs b/Content.YAMLLinter/Program.cs index a9932f8c86..a10ad7d75c 100644 --- a/Content.YAMLLinter/Program.cs +++ b/Content.YAMLLinter/Program.cs @@ -7,6 +7,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Robust.UnitTesting; namespace Content.YAMLLinter { @@ -17,9 +18,11 @@ namespace Content.YAMLLinter var stopwatch = new Stopwatch(); 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."); 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; } - private static async Task>> ValidateClient() + private static async Task<(Dictionary> YamlErrors, List FieldErrors)> + ValidateClient() { await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Disconnected = true }); var client = pairTracker.Pair.Client; - - var cPrototypeManager = client.ResolveDependency(); - var clientErrors = new Dictionary>(); - - await client.WaitPost(() => - { - clientErrors = cPrototypeManager.ValidateDirectory(new ResPath("/Prototypes")); - }); - + var result = await ValidateInstance(client); await pairTracker.CleanReturnAsync(); - - return clientErrors; + return result; } - private static async Task>> ValidateServer() + private static async Task<(Dictionary> YamlErrors, List 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 sPrototypeManager = server.ResolveDependency(); - var serverErrors = new Dictionary>(); - - await server.WaitPost(() => - { - serverErrors = sPrototypeManager.ValidateDirectory(new ResPath("/Prototypes")); - }); - + var result = await ValidateInstance(server); await pairTracker.CleanReturnAsync(); - - return serverErrors; + return result; } - public static async Task>> RunValidation() + private static async Task<(Dictionary>, List)> ValidateInstance( + RobustIntegrationTest.IntegrationInstance instance) { - var allErrors = new Dictionary>(); + var protoMan = instance.ResolveDependency(); + Dictionary> yamlErrors = default!; + List 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> YamlErrors , List FieldErrors)> + RunValidation() + { + var yamlErrors = new Dictionary>(); var serverErrors = await ValidateServer(); 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 var newErrors = val.Where(n => n.AlwaysRelevant).ToHashSet(); // 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)); if (newErrors.Count != 0) - allErrors[key] = newErrors; + yamlErrors[key] = newErrors; } - // Finally add any always-relevant client errors. - foreach (var (key, val) in clientErrors) + // Next add any always-relevant client errors. + foreach (var (key, val) in clientErrors.YamlErrors) { var newErrors = val.Where(n => n.AlwaysRelevant).ToHashSet(); if (newErrors.Count == 0) continue; - if (allErrors.TryGetValue(key, out var errors)) + if (yamlErrors.TryGetValue(key, out var errors)) errors.UnionWith(val.Where(n => n.AlwaysRelevant)); 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); } } } diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index 6f16b4db60..2defae76ab 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -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. - type: GhostRoleMobSpawner prototype: MobHumanNukeOp - - type: NukeOperativeSpawner - type: Sprite sprite: Markers/jobs.rsi layers: