diff --git a/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs index c465d01858..b5d6906b46 100644 --- a/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs @@ -1,110 +1,86 @@ -using System.Text.Encodings.Web; -using System.Text.Json; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Utility; namespace Content.Server.Corvax.GuideGenerator; public static class ComponentJsonGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() + public static void PublishAll(IResourceManager res, ResPath destination) { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - - public static void PublishAll(IResourceManager res, ResPath destRoot) - { - var proto = IoCManager.Resolve(); - var ser = IoCManager.Resolve(); - var compFactory = IoCManager.Resolve(); - var entMan = IoCManager.Resolve(); + var prototypeManager = IoCManager.Resolve(); + var serializationManager = IoCManager.Resolve(); + var componentFactory = IoCManager.Resolve(); + var destinationRoot = new ResPath("component").ToRootedPath(); // Map: component name -> (entity id -> component fields) var output = new Dictionary>(); - - foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype))) + foreach (var entityPrototype in prototypeManager.EnumeratePrototypes()) { - if (p is not EntityPrototype entProto) - continue; - - var composedComponents = YAMLEntry.GetComposedComponentMappings(entProto, proto, ser, compFactory); - - foreach (var (compName, entry) in entProto.Components) + foreach (var (componentName, componentFields) in BuildEntityComponentMap( + entityPrototype, + prototypeManager, + serializationManager, + componentFactory)) { - var node = ser.WriteValueAs(entry.Component.GetType(), entry.Component); - FieldEntry.NormalizeFlagsToSequences(entry.Component, node); - - var compFields = FieldEntry.DataNodeToObject(node); - - if (!output.TryGetValue(compName, out var map)) - { - map = new Dictionary(); - output[compName] = map; - } - - map[entProto.ID] = compFields; - } - - foreach (var (compName, node) in composedComponents) - { - if (entProto.Components.ContainsKey(compName)) - continue; - - var compFields = FieldEntry.DataNodeToObject(node); - - if (!output.TryGetValue(compName, out var map)) - { - map = new Dictionary(); - output[compName] = map; - } - - map[entProto.ID] = compFields; + GetOrCreateEntry(output, componentName)[entityPrototype.ID] = componentFields; } } if (output.Count == 0) return; - foreach (var (compName, map) in output) + res.UserData.CreateDir(destinationRoot); + foreach (var (componentName, fieldsByEntity) in output) { - // Determine default field for this component. - object? defaultObj = null; - if (compFactory.TryGetRegistration(compName, out var registration)) - { - var uid = entMan.CreateEntityUninitialized(null); - try - { - var compInstance = compFactory.GetComponent(registration.Type); - FieldEntry.EnsureFieldsCollectionsInitialized(compInstance); - entMan.AddComponent(uid, compInstance); - var node = ser.WriteValueAs(compInstance.GetType(), compInstance, true); - FieldEntry.NormalizeFlagsToSequences(compInstance, node); - defaultObj = FieldEntry.DataNodeToObject(node); - } - catch - { - defaultObj = new Dictionary(); - } - finally - { - entMan.DeleteEntity(uid); - } - } + var defaultObject = FieldEntry.ComputeComponentDefault( + componentName, + componentFactory, + serializationManager); + var componentOutput = FieldEntry.DeduplicateAgainstDefault(defaultObject, fieldsByEntity); + var directoryName = TextTools.CapitalizeString(componentName); + var componentRoot = destinationRoot / directoryName; - var outObj = new Dictionary - { - ["default"] = defaultObj, - ["id"] = map - }; - - res.UserData.CreateDir(destRoot); - var fileName = TextTools.DecapitalizeString(compName) + ".json"; - using var stream = res.UserData.OpenWrite(destRoot / fileName); - JsonSerializer.Serialize(stream, outObj, SerializeOptions); + res.UserData.CreateDir(componentRoot); + GuideJson.WriteFile(res, destinationRoot / $"{directoryName}.json", componentOutput); + GuideJson.WriteFile(res, componentRoot / "defaultFields.json", defaultObject); } } + + private static Dictionary GetOrCreateEntry(Dictionary> output, string key) + { + if (!output.TryGetValue(key, out var map)) + { + map = new Dictionary(); + output[key] = map; + } + + return map; + } + + public static Dictionary BuildEntityComponentMap(EntityPrototype entProto, IPrototypeManager proto, ISerializationManager ser, IComponentFactory compFactory) + { + var components = new Dictionary(StringComparer.Ordinal); + var composedComponents = YAMLEntry.GetComposedComponentMappings(entProto, proto, ser, compFactory); + + foreach (var (compName, entry) in entProto.Components) + { + if (!FieldEntry.TryWriteValueAsMapping(ser, entry.Component.GetType(), entry.Component, out var node)) + continue; + + composedComponents.TryGetValue(compName, out var composedNode); + components[compName] = FieldEntry.ProcessNode(entry.Component, node, composedNode); + } + + foreach (var (compName, node) in composedComponents) + { + if (entProto.Components.ContainsKey(compName)) + continue; + + components[compName] = FieldEntry.ConvertNode(node); + } + + return components; + } } diff --git a/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs b/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs index 53e687afd5..acdc77b550 100644 --- a/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs @@ -1,23 +1,15 @@ using System.IO; -using System.Text.Encodings.Web; -using System.Text.Json; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.GuideGenerator; public static class ComponentListGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public static void PublishJson(Stream stream) { var proto = IoCManager.Resolve(); - // Map: entity id -> list of component names. + // Map: component name -> list of entity ids. var output = new Dictionary>(); foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype))) @@ -25,19 +17,31 @@ public static class ComponentListGenerator if (p is not EntityPrototype entityProto) continue; - var componentNames = new List(); foreach (var (compName, _) in entityProto.Components) { - componentNames.Add(compName); + GetOrCreateEntry(output, compName).Add(entityProto.ID); } - - if (componentNames.Count > 0) - output[entityProto.ID] = componentNames; } if (output.Count == 0) return; - JsonSerializer.Serialize(stream, output, SerializeOptions); + foreach (var ids in output.Values) + { + ids.Sort(); + } + + GuideJson.Write(stream, output); + } + + private static List GetOrCreateEntry(Dictionary> output, string key) + { + if (!output.TryGetValue(key, out var ids)) + { + ids = new List(); + output[key] = ids; + } + + return ids; } } diff --git a/Content.Server/Corvax/GuideGenerator/ComponentStoreGenerator.cs b/Content.Server/Corvax/GuideGenerator/ComponentStoreGenerator.cs index 813a93bb40..edaa4b6c9e 100644 --- a/Content.Server/Corvax/GuideGenerator/ComponentStoreGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/ComponentStoreGenerator.cs @@ -1,7 +1,5 @@ using System.IO; using System.Linq; -using System.Text.Encodings.Web; -using System.Text.Json; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; @@ -11,12 +9,6 @@ namespace Content.Server.Corvax.GuideGenerator; public static class ComponentStoreGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public static void PublishJson(Stream stream) { var proto = IoCManager.Resolve(); @@ -98,6 +90,6 @@ public static class ComponentStoreGenerator normalized[refEntId] = compMap; } - JsonSerializer.Serialize(stream, normalized, SerializeOptions); + GuideJson.Write(stream, normalized); } } diff --git a/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs index 6328729287..d510033446 100644 --- a/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/EntityJsonGenerator.cs @@ -1,7 +1,5 @@ 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; @@ -10,13 +8,7 @@ namespace Content.Server.Corvax.GuideGenerator; public sealed class EntityJsonGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - - [JsonPropertyName("id")] + [JsonIgnore] public string Id { get; } [JsonPropertyName("name")] @@ -56,14 +48,13 @@ public sealed class EntityJsonGenerator public static void PublishJson(Stream stream) { - var prototype = IoCManager.Resolve(); - var prototypes = - prototype - .EnumeratePrototypes() - .Where(x => !x.Abstract) - .Select(x => new EntityJsonGenerator(x)) - .ToDictionary(x => x.Id, x => x); + var prototypeManager = IoCManager.Resolve(); + var prototypes = prototypeManager + .EnumeratePrototypes() + .Where(x => !x.Abstract) + .Select(x => new EntityJsonGenerator(x)) + .ToDictionary(x => x.Id, x => x); - JsonSerializer.Serialize(stream, prototypes, SerializeOptions); + GuideJson.Write(stream, prototypes); } } diff --git a/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs index 403605823c..3845a6bdac 100644 --- a/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/EntityNameDuplicatesJsonGenerator.cs @@ -67,7 +67,7 @@ public static class EntityNameDuplicatesJsonGenerator { var name = TextTools.CapitalizeString(TextTools.GetDisplayName(p, prototypeManager, loc)); - if (p.TryGetComponent(out _, compFactory)) + if (p.TryComp(out _, compFactory)) name = $"{name} {AbilitySuffix}"; var label = GetLabel(p); diff --git a/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs index 0555da8cb6..dc22725da2 100644 --- a/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/EntityParentJsonGenerator.cs @@ -1,8 +1,6 @@ -using System.Linq; -using System.Text.Json.Serialization; using System.IO; -using System.Text.Json; -using System.Text.Encodings.Web; +using System.Linq; +using System.Text.Json.Serialization; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; @@ -12,12 +10,6 @@ namespace Content.Server.Corvax.GuideGenerator; public sealed class EntityParentJsonGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - [JsonPropertyName("id")] public string Id { get; } @@ -44,11 +36,8 @@ public sealed class EntityParentJsonGenerator parents.Add(id); - if (!YAMLEntry.TryGetRawMapping(prototypeManager, typeof(EntityPrototype), id, out var mapping) || - mapping == null) - { + if (!YAMLEntry.TryGetRawMapping(prototypeManager, typeof(EntityPrototype), id, out var mapping) || mapping == null) return; - } foreach (var parent in GetParentIds(mapping)) { @@ -79,10 +68,7 @@ public sealed class EntityParentJsonGenerator foreach (var parentNode in parentSequence) { - if (parentNode is not ValueDataNode valueNode) - continue; - - if (!string.IsNullOrWhiteSpace(valueNode.Value)) + if (parentNode is ValueDataNode valueNode && !string.IsNullOrWhiteSpace(valueNode.Value)) yield return valueNode.Value; } } @@ -96,6 +82,6 @@ public sealed class EntityParentJsonGenerator .Select(x => new EntityParentJsonGenerator(x)) .ToDictionary(x => x.Id, x => x.Parents); - JsonSerializer.Serialize(stream, prototypes, SerializeOptions); + GuideJson.Write(stream, prototypes); } } diff --git a/Content.Server/Corvax/GuideGenerator/EntityProjectGenerator.cs b/Content.Server/Corvax/GuideGenerator/EntityProjectGenerator.cs index 0b46a3e1aa..0281c266bb 100644 --- a/Content.Server/Corvax/GuideGenerator/EntityProjectGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/EntityProjectGenerator.cs @@ -1,19 +1,11 @@ using System.IO; using System.Linq; -using System.Text.Encodings.Web; -using System.Text.Json; using Content.Shared.Corvax.GuideGenerator; namespace Content.Server.Corvax.GuideGenerator; public static class EntityProjectGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public static HashSet GetProjectEntityIds() { return EntityProjectHelper.GetProjectEntityIds(); @@ -28,6 +20,6 @@ public static class EntityProjectGenerator var sorted = ids.ToList(); sorted.Sort(StringComparer.Ordinal); - JsonSerializer.Serialize(stream, sorted, SerializeOptions); + GuideJson.Write(stream, sorted); } } diff --git a/Content.Server/Corvax/GuideGenerator/FieldEntry.cs b/Content.Server/Corvax/GuideGenerator/FieldEntry.cs index 4657b8e79b..16b6b487e9 100644 --- a/Content.Server/Corvax/GuideGenerator/FieldEntry.cs +++ b/Content.Server/Corvax/GuideGenerator/FieldEntry.cs @@ -1,8 +1,10 @@ using System.Collections; +using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; @@ -12,279 +14,348 @@ namespace Content.Server.Corvax.GuideGenerator; public static class FieldEntry { - private static readonly Regex DoubleEntryRegex = new(@"^[+-]?\d+\.\d+$"); + public const string PrototypeId = "id"; + public const string DefaultField = "default"; + public const string ComponentType = "type"; + public const string ComponentTypePrefix = "type:"; - public static object? DataNodeToObject(DataNode node) + private static readonly Regex DecimalValueRegex = new(@"^[+-]?\d+\.\d+$"); + + public static object? ProcessNode(object instance, MappingDataNode node, MappingDataNode? composed = null) { - if (node is MappingDataNode mapping) + NormalizeFlagsToSequences(instance, node); + AddReadOnlyFields(instance.GetType(), node, composed); + return ConvertNode(node); + } + + public static object? ComputePrototypeDefault(Type type, ISerializationManager serializationManager) + { + return TryCreate(type) is { } instance + ? SerializeDefault(instance, type, serializationManager, removeId: true) + : null; + } + + public static object? ComputeComponentDefault( + string name, + IComponentFactory factory, + ISerializationManager serializationManager) + { + if (!factory.TryGetRegistration(name, out var registration)) + return null; + + try { - var dict = new Dictionary(); + var component = factory.GetComponent(registration.Type); + return SerializeDefault(component, component.GetType(), serializationManager, removeId: false); + } + catch + { + return new Dictionary(); + } + } - foreach (var kv in mapping) + public static bool TryWriteValueAsMapping( + ISerializationManager manager, + Type type, + object value, + out MappingDataNode node, + bool alwaysWrite = false) + { + try + { + node = manager.WriteValueAs(type, value, alwaysWrite); + return true; + } + catch + { + node = new MappingDataNode(); + return false; + } + } + + public static Dictionary DeduplicateAgainstDefault( + object? defaultObject, + Dictionary values) + { + if (defaultObject is Dictionary defaults) + { + foreach (var fields in values.Values.OfType>()) { - dict[kv.Key] = DataNodeToObject(kv.Value); + RemoveDefaults(defaults, fields); } - - if (node.Tag != null) - { - var wrapped = new Dictionary - { - [node.Tag] = dict - }; - return wrapped; - } - - return dict; } - if (node is SequenceDataNode sequence) + return new Dictionary { [DefaultField] = defaultObject, [PrototypeId] = values }; + } + + public static object? ConvertNode(DataNode node) + { + return node switch { - var items = new List(); - foreach (var item in sequence) - { - items.Add(DataNodeToObject(item)); - } + MappingDataNode mapping => ConvertMapping(mapping), + SequenceDataNode sequence => ConvertSequence(sequence), + ValueDataNode value => ConvertValue(value), + _ => node.ToString() + }; + } - var typedMap = new Dictionary(); - var canRewrite = true; - foreach (var obj in items) - { - if (obj is not Dictionary dict || - !dict.TryGetValue("type", out var typeVal) || - typeVal is null) - { - canRewrite = false; - break; - } + private static object ConvertMapping(MappingDataNode node) + { + var result = node.ToDictionary(pair => pair.Key, pair => ConvertNode(pair.Value)); + return node.Tag == null ? result : new Dictionary { [node.Tag] = result }; + } - var key = $"type:{typeVal}"; - var cloned = new Dictionary(dict); - cloned.Remove("type"); - typedMap[key] = cloned; - } + private static object ConvertSequence(SequenceDataNode node) + { + var values = node.Select(ConvertNode).ToList(); + if (values.Any(value => value is not Dictionary map || !map.ContainsKey(ComponentType))) + return values; - if (canRewrite && typedMap.Count > 0) - return typedMap; - - return items; + var result = new Dictionary(); + foreach (var value in values.Cast>()) + { + var type = value[ComponentType]; + result[$"{ComponentTypePrefix}{type}"] = value + .Where(pair => pair.Key != ComponentType) + .ToDictionary(pair => pair.Key, pair => pair.Value); } - if (node is ValueDataNode value) + return result; + } + + public static Type GetMemberType(MemberInfo member) + { + return member switch { - if (value.IsNull) - return null; - - var raw = value.Value; - object? parsed; - - if (bool.TryParse(raw, out var boolRes)) - parsed = boolRes; - else if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intRes)) - parsed = intRes; - else if (DoubleEntryRegex.IsMatch(raw) && - double.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleRes)) - parsed = doubleRes; - else - parsed = raw; - - if (node.Tag == null) - return parsed; - - return new Dictionary - { - [node.Tag] = string.IsNullOrEmpty(raw) - ? new Dictionary() - : parsed - }; - } - - return node.ToString(); + FieldInfo field => field.FieldType, + PropertyInfo property => property.PropertyType, + _ => throw new ArgumentException("Unsupported member type", nameof(member)) + }; } public static void NormalizeFlagsToSequences(object instance, MappingDataNode node) { - var type = instance.GetType(); - const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.IgnoreCase; - - foreach (var key in node.Keys.ToList()) + foreach (var key in node.Keys.ToArray()) { - var prop = type.GetProperty(key, flags); - MemberInfo? member = prop != null && prop.GetGetMethod(true) != null ? prop : type.GetField(key, flags); - if (member == null) + var member = SerializedMembers(instance.GetType()).FirstOrDefault(m => string.Equals(m.Tag, key, StringComparison.OrdinalIgnoreCase)); + if (member == null || !member.Type.IsEnum || + member.Type.GetCustomAttribute() == null || member.Get(instance) is not { } value) continue; - var memberType = member is PropertyInfo p ? p.PropertyType : ((FieldInfo) member).FieldType; - if (!memberType.IsEnum) - continue; - - if (memberType.GetCustomAttribute(false) == null) - continue; - - var value = member is PropertyInfo p2 ? p2.GetValue(instance) : ((FieldInfo) member).GetValue(instance); - if (value == null) - continue; - - var intVal = Convert.ToInt64(value); - var names = new List(); - foreach (var v in Enum.GetValues(memberType)) - { - var i = Convert.ToInt64(v); - if (i == 0) - continue; - if ((i & (i - 1)) == 0 && (intVal & i) != 0) - names.Add(Enum.GetName(memberType, v)!); - } - - node[key] = new SequenceDataNode(names.ToArray()); + var number = Convert.ToInt64(value); + var flags = Enum.GetValues(member.Type) + .Cast() + .Where(flag => + { + var numberFlag = Convert.ToInt64(flag); + return numberFlag != 0 && (numberFlag & (numberFlag - 1)) == 0 && (number & numberFlag) != 0; + }) + .Select(flag => Enum.GetName(member.Type, flag)!); + node[key] = new SequenceDataNode(flags.ToArray()); } } - public static void EnsureFieldsCollectionsInitialized(object instance) + private static object? SerializeDefault( + object instance, + Type type, + ISerializationManager manager, + bool removeId) { - var type = instance.GetType(); - const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + try + { + EnsureCollections(instance); + if (!TryWriteValueAsMapping(manager, type, instance, out var node, alwaysWrite: true)) + return new Dictionary(); - // Fields + if (removeId) + node.Remove(PrototypeId); + + NormalizeFlagsToSequences(instance, node); + return ConvertNode(node); + } + catch + { + return new Dictionary(); + } + finally + { + (instance as IDisposable)?.Dispose(); + } + } + + private static void AddReadOnlyFields(Type type, MappingDataNode target, MappingDataNode? composed) + { + if (composed == null) + return; + + foreach (var member in SerializedMembers(type)) + { + if (!member.Attribute.ReadOnly || target.ContainsKey(member.Tag) || !composed.TryGetValue(member.Tag, out var value)) + continue; + + target[member.Tag] = value.Copy(); + } + } + + private static object? ConvertValue(ValueDataNode node) + { + if (node.IsNull) + return null; + + if (string.IsNullOrEmpty(node.Value)) + return node.Tag == null ? string.Empty : new Dictionary { [node.Tag] = new Dictionary() }; + + object value; + if (bool.TryParse(node.Value, out var boolean)) + { + value = boolean; + } + else if (int.TryParse(node.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var integer)) + { + value = integer; + } + else if (DecimalValueRegex.IsMatch(node.Value) && + double.TryParse(node.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) + { + value = number; + } + else + { + value = node.Value; + } + + return node.Tag == null ? value : new Dictionary { [node.Tag] = value }; + } + + private static void RemoveDefaults(Dictionary defaults, Dictionary values) + { + foreach (var key in values.Keys.ToArray()) + { + if (!defaults.TryGetValue(key, out var defaultValue)) + continue; + + if (Equal(defaultValue, values[key])) + { + values.Remove(key); + } + else if (defaultValue is Dictionary defaultMap && + values[key] is Dictionary valueMap) + { + RemoveDefaults(defaultMap, valueMap); + } + } + } + + private static bool Equal(object? first, object? second) + { + if (first is IDictionary firstMap && + second is IDictionary secondMap) + { + return firstMap.Count == secondMap.Count && + firstMap.All(pair => secondMap.TryGetValue(pair.Key, out var value) && Equal(pair.Value, value)); + } + + if (first is IList firstList && second is IList secondList) + { + return firstList.Count == secondList.Count && + Enumerable.Range(0, firstList.Count).All(i => Equal(firstList[i], secondList[i])); + } + + return Equals(first, second); + } + + // The serializer omits null object graphs, so initialize only serialized fields. + // Runtime fields such as ItemSlotsComponent.Slots must come from composition. + private static void EnsureCollections(object instance) + { + foreach (var member in SerializedMembers(instance.GetType())) + { + if (member.Get(instance) != null || !member.Type.IsClass || member.Type == typeof(string) || + member.Type.IsAbstract || member.Type.GetConstructor(Type.EmptyTypes) == null) + continue; + try + { + if (Activator.CreateInstance(member.Type) is { } value) + member.Set(instance, value); + } + catch + { + // Some serializers intentionally expose read-only runtime members. + } + } + } + + private static object? TryCreate(Type type) + { + var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, + Type.EmptyTypes, + null); + + return constructor == null ? null : Activator.CreateInstance(type, true); + } + + private static readonly ConcurrentDictionary MemberCache = new(); + + private static IEnumerable SerializedMembers(Type type) + { + return MemberCache.GetOrAdd(type, FindSerializedMembers); + } + + private static SerializedMember[] FindSerializedMembers(Type type) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + var members = new List(); foreach (var field in type.GetFields(flags)) { - if (field.IsInitOnly) - continue; - - try - { - var value = field.GetValue(instance); - if (value != null) - continue; - - var ft = field.FieldType; - if (ft == typeof(string)) - { - field.SetValue(instance, string.Empty); - } - else if ((typeof(IDictionary).IsAssignableFrom(ft) || typeof(IList).IsAssignableFrom(ft) || ft.IsGenericType && ft.GetGenericTypeDefinition() == typeof(List<>) || ft.IsArray) && ft is { IsAbstract: false, IsInterface: false }) - { - object? created = null; - if (ft.IsArray) - { - var elemType = ft.GetElementType(); - if (elemType != null) - created = Array.CreateInstance(elemType, 0); - } - else if (ft.GetConstructor(Type.EmptyTypes) != null) - { - created = Activator.CreateInstance(ft); - } - - if (created != null) - field.SetValue(instance, created); - } - else if (ft.IsClass && ft != typeof(string) && !ft.IsAbstract) - { - var created = Activator.CreateInstance(ft, true); - if (created != null) - { - field.SetValue(instance, created); - EnsureFieldsCollectionsInitialized(created); - } - } - else if ((ft.IsAbstract || ft.IsInterface) && ft != typeof(string)) - { - var concrete = FindConcreteAssignableType(ft); - if (concrete != null) - { - var created = Activator.CreateInstance(concrete); - if (created != null) - { - field.SetValue(instance, created); - EnsureFieldsCollectionsInitialized(created); - } - } - } - } - catch - { - // ignore - } + if (field.GetCustomAttribute() is { } attribute) + members.Add(new SerializedMember(field, attribute, attribute.Tag ?? LowerFirst(field.Name), field.FieldType)); } - // Properties - foreach (var prop in type.GetProperties(flags)) + foreach (var property in type.GetProperties(flags)) { - if (!prop.CanWrite || prop.GetIndexParameters().Length != 0) - continue; - - try - { - var value = prop.GetValue(instance); - if (value != null) - continue; - - var pt = prop.PropertyType; - if ((typeof(IDictionary).IsAssignableFrom(pt) || typeof(IList).IsAssignableFrom(pt) || pt.IsGenericType && pt.GetGenericTypeDefinition() == typeof(List<>) || pt.IsArray) && pt is { IsAbstract: false, IsInterface: false }) - { - object? created = null; - if (pt.IsArray) - { - var elemType = pt.GetElementType(); - if (elemType != null) - created = Array.CreateInstance(elemType, 0); - } - else if (pt.GetConstructor(Type.EmptyTypes) != null) - { - created = Activator.CreateInstance(pt); - } - - if (created != null) - prop.SetValue(instance, created); - } - else if (pt.IsClass && pt != typeof(string) && !pt.IsAbstract) - { - var created = Activator.CreateInstance(pt, true); - if (created != null) - { - prop.SetValue(instance, created); - EnsureFieldsCollectionsInitialized(created); - } - } - else if ((pt.IsAbstract || pt.IsInterface) && pt != typeof(string)) - { - var concrete = FindConcreteAssignableType(pt); - if (concrete != null) - { - var created = Activator.CreateInstance(concrete); - if (created != null) - { - prop.SetValue(instance, created); - EnsureFieldsCollectionsInitialized(created); - } - } - } - } - catch - { - // ignore - } + if (property.GetCustomAttribute() is { } attribute && property.GetGetMethod(true) != null) + members.Add(new SerializedMember(property, attribute, attribute.Tag ?? LowerFirst(property.Name), property.PropertyType)); } + + return [.. members]; } - private static Type? FindConcreteAssignableType(Type target) + private static string LowerFirst(string value) { - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - var types = asm.GetTypes(); + return string.IsNullOrEmpty(value) ? value : char.ToLowerInvariant(value[0]) + value[1..]; + } - foreach (var t in types) + private sealed class SerializedMember( + MemberInfo info, + DataFieldAttribute attribute, + string tag, + Type type) + { + public readonly DataFieldAttribute Attribute = attribute; + public readonly string Tag = tag; + public readonly Type Type = type; + public object? Get(object instance) + { + return info switch { - if (t.IsAbstract || t.IsInterface) - continue; - if (!target.IsAssignableFrom(t)) - continue; - if (t.GetConstructor(Type.EmptyTypes) == null) - continue; - return t; - } + FieldInfo field => field.GetValue(instance), + PropertyInfo property => property.GetValue(instance), + _ => null + }; } - return null; + public void Set(object instance, object value) + { + switch (info) + { + case FieldInfo { IsInitOnly: false } field: + field.SetValue(instance, value); + break; + case PropertyInfo { CanWrite: true } property: + property.SetValue(instance, value); + break; + } + } } } diff --git a/Content.Server/Corvax/GuideGenerator/GuideGenerator.cs b/Content.Server/Corvax/GuideGenerator/GuideGenerator.cs new file mode 100644 index 0000000000..fbcbf6997f --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/GuideGenerator.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; + +namespace Content.Server.Corvax.GuideGenerator; + +internal static class GuideDataGenerator +{ + internal static void Generate(IResourceManager resourceManager, ResPath destination) + { + WriteFile(resourceManager, destination, "entity_prototypes.json", EntityJsonGenerator.PublishJson); + WriteFile(resourceManager, destination, "entity_parent.json", EntityParentJsonGenerator.PublishJson); + WriteFile(resourceManager, destination, "loc.json", LocJsonGenerator.PublishJson); + WriteFile(resourceManager, destination, "meta_license.json", MetaLicenseGenerator.PublishJson); + WriteFile(resourceManager, destination, "prototype.json", PrototypeListGenerator.PublishJson); + WriteFile(resourceManager, destination, "component.json", ComponentListGenerator.PublishJson); + WriteFile(resourceManager, destination, "prototype_store.json", PrototypeStoreGenerator.PublishJson); + WriteFile(resourceManager, destination, "component_store.json", ComponentStoreGenerator.PublishJson); + WriteFile(resourceManager, destination, "entity_project.json", EntityProjectGenerator.PublishJson); + WriteFile(resourceManager, destination, "entity_name.json", EntityNameDuplicatesJsonGenerator.PublishNameJson); + WriteFile(resourceManager, destination, "entity_name_wiki.json", stream => WikiEntityNameGenerator.PublishJson(stream, resourceManager, destination)); + WriteFile(resourceManager, destination, "entity_name_duplicates.json", EntityNameDuplicatesJsonGenerator.PublishDuplicatesJson); + WriteFile(resourceManager, destination, "tag.json", TagJsonGenerator.PublishJson); + + PrototypeJsonGenerator.PublishAll(resourceManager, destination); + ComponentJsonGenerator.PublishAll(resourceManager, destination); + } + + private static void WriteFile( + IResourceManager resourceManager, + ResPath destination, + string name, + Action write) + { + using var stream = resourceManager.UserData.OpenWrite(destination.WithName(name)); + write(stream); + } +} + +internal static class GuideJson +{ + internal static readonly JsonSerializerOptions Options = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + internal static void Write(Stream stream, object? value) => JsonSerializer.Serialize(stream, value, Options); + + internal static void WriteFile(IResourceManager resources, ResPath path, object? value) + { + using var stream = resources.UserData.OpenWrite(path); + Write(stream, value); + } +} diff --git a/Content.Server/Corvax/GuideGenerator/LocJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/LocJsonGenerator.cs index d7a90bc6ad..dbfbfb61d1 100644 --- a/Content.Server/Corvax/GuideGenerator/LocJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/LocJsonGenerator.cs @@ -106,7 +106,9 @@ public static class LocJsonGenerator }; foreach (var attr in attrs.OrderBy(a => a)) { - obj[attr] = GetStringSafe($"{id}.{attr}"); + var value = GetStringSafe($"{id}.{attr}"); + if (!string.IsNullOrEmpty(value)) + obj[attr] = value; } output[id] = obj; } diff --git a/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs b/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs index 6f0c9fbc63..0c1048e9a4 100644 --- a/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs @@ -1,17 +1,10 @@ using System.IO; -using System.Text.Encodings.Web; using System.Text.Json; namespace Content.Server.Corvax.GuideGenerator; public static class MetaLicenseGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public static void PublishJson(Stream stream) { var workingDir = Directory.GetCurrentDirectory(); @@ -23,31 +16,37 @@ public static class MetaLicenseGenerator foreach (var metaPath in Directory.EnumerateFiles(resourcesRoot, "meta.json", SearchOption.AllDirectories)) { - var json = File.ReadAllText(metaPath); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; + try + { + var json = File.ReadAllText(metaPath); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; - var license = root.TryGetProperty("license", out var licEl) && licEl.ValueKind == JsonValueKind.String - ? licEl.GetString() ?? string.Empty - : string.Empty; + var license = root.TryGetProperty("license", out var licEl) && licEl.ValueKind == JsonValueKind.String + ? licEl.GetString() ?? string.Empty + : string.Empty; - var copyright = root.TryGetProperty("copyright", out var copyEl) && copyEl.ValueKind == JsonValueKind.String - ? copyEl.GetString() ?? string.Empty - : string.Empty; - var resourceDir = Path.GetDirectoryName(metaPath) ?? metaPath; - var relativeResourcePath = Path.GetRelativePath(workingDir, resourceDir).Replace('\\', '/'); + var copyright = root.TryGetProperty("copyright", out var copyEl) && copyEl.ValueKind == JsonValueKind.String + ? copyEl.GetString() ?? string.Empty + : string.Empty; + var resourceDir = Path.GetDirectoryName(metaPath) ?? metaPath; + var relativeResourcePath = Path.GetRelativePath(workingDir, resourceDir).Replace('\\', '/'); - output[relativeResourcePath] = new Dictionary - { - { "license", license }, - { "copyright", copyright } - }; + output[relativeResourcePath] = new Dictionary + { + { "license", license }, + { "copyright", copyright } + }; + } + catch + { + // Ignore + } } if (output.Count == 0) return; - JsonSerializer.Serialize(stream, output, SerializeOptions); + GuideJson.Write(stream, output); } } - diff --git a/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs index 219c35dba4..5c2b0196ce 100644 --- a/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs @@ -1,96 +1,87 @@ using System.Linq; using System.Reflection; -using System.Text.Encodings.Web; -using System.Text.Json; using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; -using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Utility; namespace Content.Server.Corvax.GuideGenerator; public static class PrototypeJsonGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() + public static void PublishAll(IResourceManager res, ResPath destination) { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; + var prototypeManager = IoCManager.Resolve(); + var serializationManager = IoCManager.Resolve(); + var componentFactory = IoCManager.Resolve(); + var destinationRoot = new ResPath("prototype").ToRootedPath(); - public static void PublishAll(IResourceManager res, ResPath destRoot) - { - var proto = IoCManager.Resolve(); - var ser = IoCManager.Resolve(); - - foreach (var kind in proto.EnumeratePrototypeKinds().OrderBy(t => t.Name)) + foreach (var kind in prototypeManager.EnumeratePrototypeKinds().OrderBy(type => type.Name)) { // The entity prototype has its own generator due to its size . - if (kind == typeof(EntityPrototype)) - continue; + var isEntityPrototype = kind == typeof(EntityPrototype); - if (HasUnsafeSerializedDataField(kind)) + if (HasUnsafeSerializedDataField(kind, new HashSet())) continue; // Map: entity id -> prototype fields var map = new Dictionary(); - - foreach (var p in proto.EnumeratePrototypes(kind)) + foreach (var prototype in prototypeManager.EnumeratePrototypes(kind)) { - var node = ser.WriteValueAs(kind, p); - node.Remove("id"); - map[p.ID] = FieldEntry.DataNodeToObject(node); + if (!FieldEntry.TryWriteValueAsMapping(serializationManager, kind, prototype, out var node)) + continue; + + node.Remove(FieldEntry.PrototypeId); + var fields = FieldEntry.ProcessNode(prototype, node); + if (isEntityPrototype && prototype is EntityPrototype entityPrototype) + fields = ProcessEntityPrototype(entityPrototype, prototypeManager, serializationManager, componentFactory, fields); + + map[prototype.ID] = fields; } if (map.Count == 0) continue; - // Determine default field for this prototype. - object? defaultObj = null; - try - { - var instance = Activator.CreateInstance(kind); - if (instance != null) - { - try - { - FieldEntry.EnsureFieldsCollectionsInitialized(instance); - var defaultNode = ser.WriteValueAs(kind, instance, true); - defaultNode.Remove("id"); - FieldEntry.NormalizeFlagsToSequences(instance, defaultNode); - defaultObj = FieldEntry.DataNodeToObject(defaultNode); - } - finally - { - if (instance is IDisposable disposable) - disposable.Dispose(); - } - } - } - catch - { - defaultObj = new Dictionary(); - } + var defaultObject = FieldEntry.ComputePrototypeDefault(kind, serializationManager); + var output = FieldEntry.DeduplicateAgainstDefault(defaultObject, map); + res.UserData.CreateDir(destinationRoot); - var outObj = new Dictionary - { - ["default"] = defaultObj, - ["id"] = map - }; - - res.UserData.CreateDir(destRoot); - var kindName = proto.TryGetKindFrom(kind, out var actualKindName) + var kindName = prototypeManager.TryGetKindFrom(kind, out var actualKindName) ? actualKindName : kind.Name; - var fileName = TextTools.DecapitalizeString(kindName) + ".json"; - using var stream = res.UserData.OpenWrite(destRoot / fileName); - JsonSerializer.Serialize(stream, outObj, SerializeOptions); + var directoryName = TextTools.CapitalizeString(kindName); + + if (!isEntityPrototype) + { + GuideJson.WriteFile(res, destinationRoot / $"{directoryName}.json", output); + continue; + } + + var entityRoot = destinationRoot / directoryName; + res.UserData.CreateDir(entityRoot); + var entityPrototypes = output.TryGetValue(FieldEntry.PrototypeId, out var idValue) && idValue is Dictionary em + ? em + : output; + + foreach (var (id, fields) in entityPrototypes) + { + GuideJson.WriteFile(res, entityRoot / $"{id}.json", fields); + } } } - private static bool HasUnsafeSerializedDataField(Type type) + private static object? ProcessEntityPrototype(EntityPrototype entProto, IPrototypeManager proto, ISerializationManager ser, IComponentFactory compFactory, object? fields) { - return HasUnsafeSerializedDataField(type, new HashSet()); + if (fields is not Dictionary fieldMap) + return fields; + + var componentMap = ComponentJsonGenerator.BuildEntityComponentMap(entProto, proto, ser, compFactory); + if (componentMap.Count == 0) + fieldMap.Remove("components"); + else + fieldMap["components"] = componentMap; + + return fieldMap; } private static bool HasUnsafeSerializedDataField(Type type, HashSet visited) @@ -100,25 +91,10 @@ public static class PrototypeJsonGenerator if (!visited.Add(type)) return false; - foreach (var field in type.GetFields(flags)) - { - if (!HasDataField(field)) - continue; - - if (IsUnsafeSerializedType(field.FieldType, visited)) - return true; - } - - foreach (var property in type.GetProperties(flags)) - { - if (!HasDataField(property)) - continue; - - if (IsUnsafeSerializedType(property.PropertyType, visited)) - return true; - } - - return false; + return type.GetFields(flags) + .Cast() + .Concat(type.GetProperties(flags)) + .Any(m => HasDataField(m) && IsUnsafeSerializedType(FieldEntry.GetMemberType(m), visited)); } private static bool HasDataField(MemberInfo member) @@ -131,37 +107,20 @@ public static class PrototypeJsonGenerator { type = Nullable.GetUnderlyingType(type) ?? type; - if (type == typeof(object) || - type == typeof(EntityUid) || - type == typeof(NetEntity)) - { + if (type == typeof(EntityUid) || type == typeof(NetEntity)) return true; - } - if (type.IsPrimitive || - type.IsEnum || - type == typeof(string) || - type == typeof(decimal) || - type == typeof(TimeSpan)) - { + if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(TimeSpan)) return false; - } if (type.IsArray) return IsUnsafeSerializedType(type.GetElementType()!, visited); if (type.IsGenericType) - { - foreach (var argument in type.GetGenericArguments()) - { - if (IsUnsafeSerializedType(argument, visited)) - return true; - } - } + return type.GetGenericArguments().Any(arg => IsUnsafeSerializedType(arg, visited)); return type.GetCustomAttributes(inherit: true) - .Any(attr => - attr.GetType().Name is nameof(DataDefinitionAttribute) or nameof(SerializableAttribute)) - && HasUnsafeSerializedDataField(type, visited); + .Any(attr => attr.GetType().Name is nameof(DataDefinitionAttribute) or nameof(SerializableAttribute)) + && HasUnsafeSerializedDataField(type, visited); } } diff --git a/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs b/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs index c4c44377e6..cfc086aba0 100644 --- a/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs @@ -1,18 +1,10 @@ using System.IO; -using System.Text.Encodings.Web; -using System.Text.Json; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.GuideGenerator; public static class PrototypeListGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public static void PublishJson(Stream stream) { var proto = IoCManager.Resolve(); @@ -42,6 +34,6 @@ public static class PrototypeListGenerator if (output.Count == 0) return; - JsonSerializer.Serialize(stream, output, SerializeOptions); + GuideJson.Write(stream, output); } } diff --git a/Content.Server/Corvax/GuideGenerator/PrototypeStoreGenerator.cs b/Content.Server/Corvax/GuideGenerator/PrototypeStoreGenerator.cs index b7cfeecb82..45d747b6bd 100644 --- a/Content.Server/Corvax/GuideGenerator/PrototypeStoreGenerator.cs +++ b/Content.Server/Corvax/GuideGenerator/PrototypeStoreGenerator.cs @@ -1,18 +1,10 @@ using System.IO; -using System.Text.Encodings.Web; -using System.Text.Json; using Robust.Shared.Prototypes; namespace Content.Server.Corvax.GuideGenerator; public static class PrototypeStoreGenerator { - private static readonly JsonSerializerOptions SerializeOptions = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; - public static void PublishJson(Stream stream) { var proto = IoCManager.Resolve(); @@ -60,7 +52,6 @@ public static class PrototypeStoreGenerator if (output.Count == 0) return; - JsonSerializer.Serialize(stream, output, SerializeOptions); + GuideJson.Write(stream, output); } - } diff --git a/Content.Server/Corvax/GuideGenerator/TagJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/TagJsonGenerator.cs new file mode 100644 index 0000000000..80dc84e62f --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/TagJsonGenerator.cs @@ -0,0 +1,90 @@ +using System.IO; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Value; + +namespace Content.Server.Corvax.GuideGenerator; + +public static class TagJsonGenerator +{ + public static void PublishJson(Stream stream) + { + var proto = IoCManager.Resolve(); + var ser = IoCManager.Resolve(); + var compFactory = IoCManager.Resolve(); + + var output = new Dictionary>(); + + foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype))) + { + if (p is not EntityPrototype entProto) + continue; + + if (entProto.Abstract) + continue; + + var tags = new HashSet(); + + if (entProto.Components.TryGetValue("Tag", out var tagEntry)) + { + if (tagEntry.Component is TagComponent tagComp) + { + foreach (var tag in tagComp.Tags) + { + tags.Add(tag.Id); + } + } + } + + var composed = YAMLEntry.GetComposedComponentMappings(entProto, proto, ser, compFactory); + ExtractTagsFromMapping(composed, "Tag", tags); + + foreach (var tag in tags) + { + GetOrCreateEntry(output, tag).Add(entProto.ID); + } + } + + if (output.Count == 0) + return; + + foreach (var ids in output.Values) + { + ids.Sort(); + } + + GuideJson.Write(stream, output); + } + + private static List GetOrCreateEntry(Dictionary> output, string key) + { + if (!output.TryGetValue(key, out var ids)) + { + ids = new List(); + output[key] = ids; + } + + return ids; + } + + private static void ExtractTagsFromMapping( + Dictionary composed, + string compName, + HashSet tags) + { + if (!composed.TryGetValue(compName, out var mapping)) + return; + + if (!mapping.TryGet("tags", out SequenceDataNode? tagsSeq)) + return; + + foreach (var node in tagsSeq) + { + if (node is ValueDataNode valueNode && !string.IsNullOrEmpty(valueNode.Value)) + tags.Add(valueNode.Value); + } + } +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 110070b395..e07ed73b25 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -151,25 +151,7 @@ namespace Content.Server.Entry { var resPath = new ResPath(dest).ToRootedPath(); // Corvax-Wiki-Start - void WriteFile(string name, Action write) - { - using var stream = _res.UserData.OpenWrite(resPath.WithName(name)); - write(stream); - } - 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); - WriteFile("component.json", ComponentListGenerator.PublishJson); - WriteFile("prototype_store.json", PrototypeStoreGenerator.PublishJson); - WriteFile("component_store.json", ComponentStoreGenerator.PublishJson); - WriteFile("entity_project.json", EntityProjectGenerator.PublishJson); - WriteFile("entity_name.json", EntityNameDuplicatesJsonGenerator.PublishNameJson); - WriteFile("entity_name_wiki.json", file => WikiEntityNameGenerator.PublishJson(file, _res, resPath)); - WriteFile("entity_name_duplicates.json", EntityNameDuplicatesJsonGenerator.PublishDuplicatesJson); - PrototypeJsonGenerator.PublishAll(_res, new ResPath("prototype").ToRootedPath()); - ComponentJsonGenerator.PublishAll(_res, new ResPath("component").ToRootedPath()); + GuideDataGenerator.Generate(_res, resPath); // Corvax-Wiki-End Dependencies.Resolve().Shutdown("Data generation done"); return; diff --git a/Content.Shared/Corvax/CCVars/CCVars.Wiki.cs b/Content.Shared/Corvax/CCVars/CCVars.Wiki.cs index 7d5dd4b410..8bd4730476 100644 --- a/Content.Shared/Corvax/CCVars/CCVars.Wiki.cs +++ b/Content.Shared/Corvax/CCVars/CCVars.Wiki.cs @@ -17,13 +17,13 @@ public sealed partial class CCVars CVarDef.Create("autogen.entity_screenshot.output_path", "/Textures/Entities", CVar.CLIENTONLY); /// - /// Prefix for prototype folders included in entity_project generation. + /// Prefixes for prototype folders included in entity_project generation. Comma-separated. /// public static readonly CVarDef EntityProjectFolderPrefix = CVarDef.Create("autogen.entity_project.folder_prefix", string.Empty, CVar.ARCHIVE); /// - /// Prototype folder name to exclude from entity_project generation. + /// Prototype folder names to exclude from entity_project generation. Comma-separated. /// public static readonly CVarDef EntityProjectExcludedCoreProjectFolder = CVarDef.Create("autogen.entity_project.excluded_core_project_folder", string.Empty, CVar.ARCHIVE); diff --git a/Content.Shared/Corvax/GuideGenerator/EntityProjectHelper.cs b/Content.Shared/Corvax/GuideGenerator/EntityProjectHelper.cs index a6a2460d8a..1ddeea885c 100644 --- a/Content.Shared/Corvax/GuideGenerator/EntityProjectHelper.cs +++ b/Content.Shared/Corvax/GuideGenerator/EntityProjectHelper.cs @@ -17,10 +17,12 @@ public static class EntityProjectHelper { var cfg = IoCManager.Resolve(); var res = IoCManager.Resolve(); - var projectFolderPrefix = cfg.GetCVar(CCVars.EntityProjectFolderPrefix); - var excludedCoreProjectFolder = cfg.GetCVar(CCVars.EntityProjectExcludedCoreProjectFolder); + var projectFolderPrefixes = cfg.GetCVar(CCVars.EntityProjectFolderPrefix) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var excludedCoreProjectFolders = cfg.GetCVar(CCVars.EntityProjectExcludedCoreProjectFolder) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (string.IsNullOrWhiteSpace(projectFolderPrefix)) + if (projectFolderPrefixes.Length == 0) return []; var ids = new HashSet(); @@ -30,7 +32,7 @@ public static class EntityProjectHelper if (!IsPrototypeFile(path)) continue; - if (!IsInIncludedProjectFolder(path, projectFolderPrefix, excludedCoreProjectFolder)) + if (!IsInIncludedProjectFolder(path, projectFolderPrefixes, excludedCoreProjectFolders)) continue; ExtractIdsFromYaml(res, path, ids); @@ -52,23 +54,27 @@ public static class EntityProjectHelper private static bool IsInIncludedProjectFolder( ResPath path, - string projectFolderPrefix, - string excludedCoreProjectFolder) + string[] projectFolderPrefixes, + string[] excludedCoreProjectFolders) { - var normalizedExcluded = excludedCoreProjectFolder.Trim(); - foreach (var part in GetPathParts(path)) { - if (!part.StartsWith(projectFolderPrefix, StringComparison.Ordinal)) - continue; - - if (!string.IsNullOrWhiteSpace(normalizedExcluded) && - part.Equals(normalizedExcluded, StringComparison.Ordinal)) + foreach (var prefix in projectFolderPrefixes) { - return false; - } + if (!part.StartsWith(prefix, StringComparison.Ordinal)) + continue; - return true; + foreach (var excluded in excludedCoreProjectFolders) + { + if (!string.IsNullOrWhiteSpace(excluded) && + part.Equals(excluded, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } } return false;