diff --git a/Content.Client/Corvax/ExportSprites/EntityScreenshotGenerator.cs b/Content.Client/Corvax/ExportSprites/EntityScreenshotGenerator.cs index b64a962604..de427a1e43 100644 --- a/Content.Client/Corvax/ExportSprites/EntityScreenshotGenerator.cs +++ b/Content.Client/Corvax/ExportSprites/EntityScreenshotGenerator.cs @@ -1,17 +1,23 @@ using System.Linq; using System.Threading.Tasks; +using Content.Client.Gameplay; using Content.Shared.CCVar; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Corvax.GuideGenerator; -using Content.Client.Gameplay; +using Content.Shared.Prototypes; using Robust.Client; +using Robust.Client.GameObjects; using Robust.Client.State; using Robust.Client.Timing; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Map; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Utility; namespace Content.Client.Corvax.ExportSprites; @@ -29,6 +35,7 @@ public sealed class EntityScreenshotGenerator [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly IStateManager _stateManager = default!; private ISawmill _sawmill = default!; @@ -120,7 +127,7 @@ public sealed class EntityScreenshotGenerator var prototypes = _prototypeManager.EnumeratePrototypes() .Where(proto => !proto.Abstract && - proto.Components.ContainsKey("Sprite") && + HasExportableSprite(proto) && EntityProjectHelper.MatchesAllowedIds(proto.ID, allowedIds)) .OrderBy(proto => proto.ID) .ToList(); @@ -136,11 +143,18 @@ public sealed class EntityScreenshotGenerator try { - entity = _entityManager.SpawnEntity(proto.ID, new EntityCoordinates(previewGrid.Owner, default)); + if (proto.HasComponent(_entityManager.ComponentFactory)) + { + entity = _entityManager.SpawnEntity(proto.ID, new EntityCoordinates(previewGrid.Owner, default)); - await WaitForEntityAppearanceAsync(entity); - ApplyPrototypeAppearance(entity, proto); - await WaitForEntityAppearanceAsync(entity, 1); + await WaitForEntityAppearanceAsync(entity); + ApplyPrototypeAppearance(entity, proto); + await WaitForEntityAppearanceAsync(entity, 1); + } + else + { + entity = SpawnIconEntity(proto); + } await _renderService.Export(entity, Direction.South, outputDir / $"{proto.ID}.png"); exported++; @@ -241,4 +255,128 @@ public sealed class EntityScreenshotGenerator if (solution.GetPrimaryReagentId() is { } reagent) appearanceSystem.SetData(entity, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearance); } + + private bool HasExportableSprite(EntityPrototype prototype) + { + if (prototype.HasComponent(_entityManager.ComponentFactory)) + return true; + + return TryGetPrototypeIcon(prototype, out _); + } + + private EntityUid SpawnIconEntity(EntityPrototype prototype) + { + if (!TryGetPrototypeIcon(prototype, out var icon) || icon == null) + throw new InvalidOperationException($"Prototype {prototype.ID} has no exportable icon."); + + var entity = _entityManager.SpawnEntity(null, MapCoordinates.Nullspace); + var sprite = _entityManager.EnsureComponent(entity); + var spriteSystem = _entitySystemManager.GetEntitySystem(); + + spriteSystem.AddBlankLayer((entity, sprite), 0); + if (icon is SpriteSpecifier.EntityPrototype entityIcon) + spriteSystem.LayerSetTexture((entity, sprite), 0, spriteSystem.Frame0(new SpriteSpecifier.EntityPrototype(entityIcon.EntityPrototypeId))); + else + spriteSystem.LayerSetSprite((entity, sprite), 0, icon); + sprite.LayerSetShader(0, "unshaded"); + spriteSystem.LayerSetVisible((entity, sprite), 0, true); + + return entity; + } + + private bool TryGetPrototypeIcon(EntityPrototype prototype, out SpriteSpecifier? icon) + { + icon = null; + + foreach (var (_, entry) in prototype.Components) + { + if (TryExtractSpriteSpecifier(entry.Component.GetType(), entry.Mapping, out icon)) + return true; + } + + return false; + } + + private bool TryExtractSpriteSpecifier(Type? expectedType, DataNode? node, out SpriteSpecifier? icon) + { + icon = null; + + if (node == null) + return false; + + if (expectedType != null && + typeof(SpriteSpecifier).IsAssignableFrom(expectedType) && + TryParseSpriteSpecifier(node, out icon)) + { + return true; + } + + if (node is MappingDataNode mapping) + { + foreach (var (key, child) in mapping.Children) + { + Type? childType = null; + + if (expectedType != null && + _serialization.TryGetVariableType(expectedType, key, out var resolvedType)) + { + childType = resolvedType; + } + + if (TryExtractSpriteSpecifier(childType, child, out icon)) + return true; + } + + return false; + } + + if (node is SequenceDataNode sequence) + { + var elementType = GetSequenceElementType(expectedType); + foreach (var child in sequence.Sequence) + { + if (TryExtractSpriteSpecifier(elementType, child, out icon)) + return true; + } + } + + return false; + } + + private static Type? GetSequenceElementType(Type? type) + { + if (type == null) + return null; + + if (type.IsArray) + return type.GetElementType(); + + var genericArguments = type.GenericTypeArguments; + if (genericArguments.Length == 1) + return genericArguments[0]; + + return null; + } + + private bool TryParseSpriteSpecifier(DataNode node, out SpriteSpecifier? icon) + { + icon = null; + + try + { + icon = _serialization.Read(node, notNullableOverride: true); + if (icon == SpriteSpecifier.Invalid) + { + icon = null; + return false; + } + + return true; + } + catch + { + icon = null; + return false; + } + } } diff --git a/Content.Client/Corvax/ExportSprites/EntityScreenshotRenderService.cs b/Content.Client/Corvax/ExportSprites/EntityScreenshotRenderService.cs index 848279b9db..cd05548469 100644 --- a/Content.Client/Corvax/ExportSprites/EntityScreenshotRenderService.cs +++ b/Content.Client/Corvax/ExportSprites/EntityScreenshotRenderService.cs @@ -3,8 +3,8 @@ using System.Threading; using System.Threading.Tasks; using Robust.Client.GameObjects; using Robust.Client.Graphics; -using Robust.Client.Utility; using Robust.Client.UserInterface; +using Robust.Client.Utility; using Robust.Shared.ContentPack; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -27,6 +27,7 @@ public sealed class EntityScreenshotRenderService private EntityScreenshotRenderControl? _control; private bool _initialized; private readonly Dictionary<(ResPath Path, string State), Image> _rsiStateImageCache = new(); + private readonly Dictionary> _textureImageCache = new(); private ISawmill _sawmill = default!; public void Initialize() @@ -48,6 +49,13 @@ public sealed class EntityScreenshotRenderService _rsiStateImageCache.Clear(); + foreach (var image in _textureImageCache.Values) + { + image.Dispose(); + } + + _textureImageCache.Clear(); + if (_control == null) return; @@ -360,7 +368,7 @@ public sealed class EntityScreenshotRenderService private static int ToDelayMilliseconds(float seconds) { - return Math.Max(1, (int)MathF.Round(seconds * 1000f)); + return Math.Max(1, (int) MathF.Round(seconds * 1000f)); } private void WriteAnimationMetadata(ResPath animationDir, IReadOnlyList animationFrames) @@ -388,7 +396,9 @@ public sealed class EntityScreenshotRenderService return false; // Keep the old render-target path for uncommon transformed sprites. - if (spriteComp.Scale != Vector2.One || spriteComp.Rotation != Angle.Zero) + if (spriteComp.Scale != Vector2.One || + spriteComp.Rotation != Angle.Zero || + spriteComp.EnableDirectionOverride) return false; var size = renderBounds.Size; @@ -407,7 +417,7 @@ public sealed class EntityScreenshotRenderService return false; if (!TryGetLayerImage(spriteLayer, direction, out var sourceImage, out var sourceRect)) - continue; + return false; var drawColor = spriteComp.Color * spriteLayer.Color; var drawOffset = ToPixelOffset(spriteComp.Offset + spriteLayer.Offset) - renderBounds.Min; @@ -429,7 +439,7 @@ public sealed class EntityScreenshotRenderService private static void BlitImage( Image sourceImage, - Rectangle sourceRect, + PixelRect sourceRect, Color modulation, Span destination, Vector2i destinationSize, @@ -519,14 +529,17 @@ public sealed class EntityScreenshotRenderService SpriteComponent.Layer layer, Direction direction, out Image image, - out Rectangle sourceRect) + out PixelRect sourceRect) { image = default!; sourceRect = default; - // Raw texture layers need a separate cache path. Use render target fallback for them. if (layer.Texture != null) - return false; + { + image = GetTextureImage(layer.Texture); + sourceRect = new PixelRect(0, 0, image.Width, image.Height); + return true; + } var rsi = layer.ActualRsi; var stateId = ((ISpriteLayer) layer).RsiState; @@ -569,10 +582,33 @@ public sealed class EntityScreenshotRenderService var target = (int) rsiDirection * framesPerDirection + frame; var targetY = target / statesX; var targetX = target % statesX; - sourceRect = new Rectangle(targetX * frameWidth, targetY * frameHeight, frameWidth, frameHeight); + sourceRect = new PixelRect(targetX * frameWidth, targetY * frameHeight, frameWidth, frameHeight); return true; } + private Image GetTextureImage(Texture texture) + { + if (_textureImageCache.TryGetValue(texture, out var cached)) + return cached; + + var image = new Image(texture.Width, texture.Height); + var pixels = image.GetPixelSpan(); + + for (var y = 0; y < texture.Height; y++) + { + for (var x = 0; x < texture.Width; x++) + { + var color = texture.GetPixel(x, y); + pixels[y * texture.Width + x] = new Rgba32(color.RByte, color.GByte, color.BByte, color.AByte); + } + } + + _textureImageCache[texture] = image; + return image; + } + + private readonly record struct PixelRect(int Left, int Top, int Width, int Height); + private sealed class EntityScreenshotRenderControl : Control { private static readonly Color ExportBackgroundColor = new(128, 128, 128, 0); diff --git a/Content.Server/Corvax/GuideGenerator/EntityEntry.cs b/Content.Server/Corvax/GuideGenerator/EntityEntry.cs deleted file mode 100644 index 9ddd167328..0000000000 --- a/Content.Server/Corvax/GuideGenerator/EntityEntry.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Linq; -using System.Text.Json.Serialization; -using Content.Shared.Labels.Components; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Sequence; -using Robust.Shared.Serialization.Markdown.Value; - -namespace Content.Server.Corvax.GuideGenerator; - -public sealed class EntityEntry -{ - private static string[]? GetRootParents(EntityPrototype proto, IPrototypeManager prototypeManager) - { - if (proto.Parents is not { Length: > 0 }) - return null; - - var roots = new HashSet(StringComparer.Ordinal); - var visited = new HashSet(StringComparer.Ordinal); - - static IEnumerable GetParentIds(MappingDataNode mapping) - { - if (mapping.TryGet("parent", out ValueDataNode? parentValue)) - { - if (!string.IsNullOrWhiteSpace(parentValue.Value)) - yield return parentValue.Value; - - yield break; - } - - if (!mapping.TryGet("parent", out SequenceDataNode? parentSequence)) - yield break; - - foreach (var parentNode in parentSequence) - { - if (parentNode is not ValueDataNode valueNode) - continue; - - if (!string.IsNullOrWhiteSpace(valueNode.Value)) - yield return valueNode.Value; - } - } - - void Visit(string id) - { - if (!visited.Add(id)) - return; - - if (!YAMLEntry.TryGetRawMapping(prototypeManager, typeof(EntityPrototype), id, out var mapping) || - mapping == null) - { - roots.Add(id); - return; - } - - var parents = GetParentIds(mapping).ToArray(); - if (parents.Length == 0) - { - roots.Add(id); - return; - } - - foreach (var parent in parents) - { - Visit(parent); - } - } - - foreach (var parent in proto.Parents) - { - Visit(parent); - } - - return roots.Count > 0 ? roots.OrderBy(x => x, StringComparer.Ordinal).ToArray() : null; - } - - [JsonPropertyName("id")] - public string Id { get; } - - [JsonPropertyName("name")] - public string Name { get; } - - [JsonPropertyName("desc")] - public string Description { get; } - - [JsonPropertyName("suffix")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Suffix { get; } - - [JsonPropertyName("label")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Label { get; } - - [JsonPropertyName("parents")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string[]? Parents { get; } - - public EntityEntry(EntityPrototype proto) - { - var prototypeManager = IoCManager.Resolve(); - var loc = IoCManager.Resolve(); - - Id = proto.ID; - Name = TextTools.CapitalizeString(TextTools.GetDisplayName(proto, prototypeManager, loc)); - Description = proto.Description; - Suffix = string.IsNullOrWhiteSpace(proto.EditorSuffix) ? null : proto.EditorSuffix; - Parents = GetRootParents(proto, prototypeManager); - - Label = proto.Components.Values - .Select(x => x.Component) - .OfType() - .Select(lc => lc.CurrentLabel) - .Where(label => !string.IsNullOrEmpty(label)) - .Select(label => Loc.GetString(label!)) - .FirstOrDefault(); - } -} diff --git a/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs index 24c21cd229..d7562222d3 100644 --- a/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs @@ -1,12 +1,53 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Text.Encodings.Web; +using System.Text.Json.Serialization; +using Content.Shared.Labels.Components; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.GuideGenerator; public sealed class EntityJsonGenerator { + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("name")] + public string Name { get; } + + [JsonPropertyName("desc")] + public string Description { get; } + + [JsonPropertyName("suffix")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Suffix { get; } + + [JsonPropertyName("label")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Label { get; } + + public EntityJsonGenerator(EntityPrototype proto) + { + var prototypeManager = IoCManager.Resolve(); + var loc = IoCManager.Resolve(); + + Id = proto.ID; + Name = TextTools.CapitalizeString(TextTools.GetDisplayName(proto, prototypeManager, loc)); + Description = proto.Description; + + if (!string.IsNullOrWhiteSpace(proto.EditorSuffix)) + Suffix = TextTools.GetEditorSuffix(proto.EditorSuffix, EntityNameDuplicatesJsonGenerator.IgnoredSuffixTokens, TextTools.NormalizeSuffixToken); + + Label = proto.Components.Values + .Select(x => x.Component) + .OfType() + .Select(lc => lc.CurrentLabel) + .Where(label => !string.IsNullOrEmpty(label)) + .Select(label => Loc.GetString(label!)) + .FirstOrDefault(); + } + public static void PublishJson(StreamWriter file) { var prototype = IoCManager.Resolve(); @@ -14,12 +55,13 @@ public sealed class EntityJsonGenerator prototype .EnumeratePrototypes() .Where(x => !x.Abstract) - .Select(x => new EntityEntry(x)) + .Select(x => new EntityJsonGenerator(x)) .ToDictionary(x => x.Id, x => x); var serializeOptions = new JsonSerializerOptions { WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; file.Write(JsonSerializer.Serialize(prototypes, serializeOptions)); diff --git a/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs index 117936e32b..cabbfff030 100644 --- a/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs @@ -2,19 +2,28 @@ using System.IO; using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; +using Content.Shared.Actions.Components; using Content.Shared.Corvax.GuideGenerator; using Content.Shared.Labels.Components; -using Robust.Shared.Physics; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.GuideGenerator; public static class EntityNameDuplicatesJsonGenerator { + private const string AbilitySuffix = "(способность)"; + + public static readonly string[] AllowedNameComponents = + [ + "Fixtures", + "Physics", + "Action" + ]; + // Suffix parts that should be ignored when building display names. - private static readonly HashSet IgnoredSuffixTokens = new(StringComparer.OrdinalIgnoreCase) + public static readonly HashSet IgnoredSuffixTokens = new(StringComparer.OrdinalIgnoreCase) { - "DO NOT MAP", + "do not map", "не маппить", }; @@ -31,10 +40,11 @@ public static class EntityNameDuplicatesJsonGenerator public static bool MatchesEntityNameFilter(EntityPrototype proto, IReadOnlySet allowedIds) { - var compFactory = IoCManager.Resolve(); + var hasAllowedComponent = AllowedNameComponents.Any(proto.Components.ContainsKey); + return !proto.Abstract && - proto.TryGetComponent(out _, compFactory) && - EntityProjectHelper.MatchesAllowedIds(proto.ID, allowedIds); + hasAllowedComponent && + EntityProjectHelper.MatchesAllowedIds(proto.ID, allowedIds); } private static Dictionary> GetDuplicatesName( @@ -42,6 +52,7 @@ public static class EntityNameDuplicatesJsonGenerator bool duplicatesOnly) { var loc = IoCManager.Resolve(); + var compFactory = IoCManager.Resolve(); var allowedIds = EntityProjectGenerator.GetProjectEntityIds(); return prototypeManager .EnumeratePrototypes() @@ -50,24 +61,17 @@ public static class EntityNameDuplicatesJsonGenerator { var name = TextTools.CapitalizeString(TextTools.GetDisplayName(p, prototypeManager, loc)); + if (p.TryGetComponent(out _, compFactory)) + name = $"{name} {AbilitySuffix}"; + var label = GetLabel(p); var rawSuffix = p.EditorSuffix; - var suffix = string.Empty; - if (!string.IsNullOrWhiteSpace(rawSuffix)) - { - var parts = rawSuffix - .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) - .Select(part => part.Trim()) - .Where(part => !IgnoredSuffixTokens.Contains(part)) - .ToArray(); - - if (parts.Length > 0) - suffix = string.Join(", ", parts).ToLowerInvariant(); - } + var suffix = TextTools.GetEditorSuffix(rawSuffix, IgnoredSuffixTokens, TextTools.NormalizeSuffixToken); return (Name: name, Label: label, Suffix: suffix); }) + .Where(g => !string.IsNullOrWhiteSpace(g.Key.Name)) .Where(g => !duplicatesOnly || g.Count() > 1) .ToDictionary( g => diff --git a/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs new file mode 100644 index 0000000000..d7064185ce --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs @@ -0,0 +1,101 @@ +using System.Linq; +using System.Text.Json.Serialization; +using System.IO; +using System.Text.Json; +using System.Text.Encodings.Web; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Value; + +namespace Content.Server.Corvax.GuideGenerator; + +public sealed class EntityParentJsonGenerator +{ + [JsonPropertyName("id")] + public string Id { get; } + + [JsonPropertyName("parents")] + public string[] Parents { get; } + + public EntityParentJsonGenerator(EntityPrototype proto) + { + var prototypeManager = IoCManager.Resolve(); + + Id = proto.ID; + Parents = GetParents(proto, prototypeManager); + } + + public static string[] GetParents(EntityPrototype proto, IPrototypeManager prototypeManager) + { + var parents = new HashSet(StringComparer.Ordinal); + var visited = new HashSet(StringComparer.Ordinal); + + void Visit(string id) + { + if (string.IsNullOrWhiteSpace(id) || !visited.Add(id)) + return; + + parents.Add(id); + + if (!YAMLEntry.TryGetRawMapping(prototypeManager, typeof(EntityPrototype), id, out var mapping) || + mapping == null) + { + return; + } + + foreach (var parent in GetParentIds(mapping)) + { + Visit(parent); + } + } + + foreach (var parent in proto.Parents ?? []) + { + Visit(parent); + } + + return parents.OrderBy(x => x, StringComparer.Ordinal).ToArray(); + } + + private static IEnumerable GetParentIds(MappingDataNode mapping) + { + if (mapping.TryGet("parent", out ValueDataNode? parentValue)) + { + if (!string.IsNullOrWhiteSpace(parentValue.Value)) + yield return parentValue.Value; + + yield break; + } + + if (!mapping.TryGet("parent", out SequenceDataNode? parentSequence)) + yield break; + + foreach (var parentNode in parentSequence) + { + if (parentNode is not ValueDataNode valueNode) + continue; + + if (!string.IsNullOrWhiteSpace(valueNode.Value)) + yield return valueNode.Value; + } + } + + public static void PublishJson(StreamWriter file) + { + var prototypeManager = IoCManager.Resolve(); + var prototypes = prototypeManager + .EnumeratePrototypes() + .Where(x => !x.Abstract) + .Select(x => new EntityParentJsonGenerator(x)) + .ToDictionary(x => x.Id, x => x.Parents); + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + file.Write(JsonSerializer.Serialize(prototypes, serializeOptions)); + } +} diff --git a/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs index 2549b4c5ae..e4d07d2e4c 100644 --- a/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs @@ -3,9 +3,7 @@ using System.Reflection; using System.Text.Encodings.Web; using System.Text.Json; using Robust.Shared.ContentPack; -using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Utility; @@ -154,8 +152,9 @@ public static class PrototypeJsonGenerator } } - return type.GetCustomAttributes(inherit: true).Any(attr => + return type.GetCustomAttributes(inherit: true) + .Any(attr => attr.GetType().Name is nameof(DataDefinitionAttribute) or nameof(SerializableAttribute)) - && HasUnsafeSerializedDataField(type, visited); + && HasUnsafeSerializedDataField(type, visited); } } diff --git a/Content.Server/Corvax/GuideGenerator/TextTools.cs b/Content.Server/Corvax/GuideGenerator/TextTools.cs index 021261ebb4..e1da52f623 100644 --- a/Content.Server/Corvax/GuideGenerator/TextTools.cs +++ b/Content.Server/Corvax/GuideGenerator/TextTools.cs @@ -1,3 +1,5 @@ +using System.Linq; +using System.Text.RegularExpressions; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.GuideGenerator; @@ -60,4 +62,33 @@ public sealed class TextTools return proto.Name; } + + private static readonly Regex SuffixTokenEdgeGarbageRegex = + new(@"^[\p{P}\p{S}\s]+|[\p{P}\p{S}\s]+$", RegexOptions.Compiled); + + public static string NormalizeSuffixToken(string token) + { + return string.IsNullOrWhiteSpace(token) + ? string.Empty + : SuffixTokenEdgeGarbageRegex.Replace(token, string.Empty); + } + + public static string GetEditorSuffix( + string? editorSuffix, + IReadOnlySet ignoredTokens, + Func normalizeToken) + { + if (string.IsNullOrWhiteSpace(editorSuffix)) + return string.Empty; + + var parts = editorSuffix + .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries) + .Select(part => part.Trim()) + .Where(part => !ignoredTokens.Contains(normalizeToken(part))) + .ToArray(); + + return parts.Length > 0 + ? string.Join(", ", parts).ToLowerInvariant() + : string.Empty; + } } diff --git a/Content.Server/Corvax/GuideGenerator/YAMLEntry.cs b/Content.Server/Corvax/GuideGenerator/YAMLEntry.cs index b5578164b1..a8a75f825a 100644 --- a/Content.Server/Corvax/GuideGenerator/YAMLEntry.cs +++ b/Content.Server/Corvax/GuideGenerator/YAMLEntry.cs @@ -36,7 +36,7 @@ public static class YAMLEntry { mapping = null; - if (KindsField?.GetValue(proto) is not object kinds) + if (KindsField?.GetValue(proto) is not { } kinds) return false; var itemProperty = kinds.GetType().GetProperty("Item"); diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index c43ec62571..cea568ce15 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -161,7 +161,8 @@ namespace Content.Server.Entry write(file); file.Flush(); } - WriteFile("entity_" + dest, EntityJsonGenerator.PublishJson); + WriteFile("entity_prototypes.json", EntityJsonGenerator.PublishJson); + WriteFile("entity_parent.json", EntityParentJsonGenerator.PublishJson); WriteFile("loc.json", LocJsonGenerator.PublishJson); WriteFile("meta_license.json", MetaLicenseGenerator.PublishJson); WriteFile("prototype.json", PrototypeListGenerator.PublishJson);