[Wiki] Обновление ботов (#3719)

This commit is contained in:
Pok 2026-09-03 23:09:31 +03:00 • committed by GitHub
parent eacc62bbaf
commit 943d1af8a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 661 additions and 571 deletions

View file

@ -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<IPrototypeManager>();
var ser = IoCManager.Resolve<ISerializationManager>();
var compFactory = IoCManager.Resolve<IComponentFactory>();
var entMan = IoCManager.Resolve<IEntityManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var componentFactory = IoCManager.Resolve<IComponentFactory>();
var destinationRoot = new ResPath("component").ToRootedPath();
// Map: component name -> (entity id -> component fields)
var output = new Dictionary<string, Dictionary<string, object?>>();
foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype)))
foreach (var entityPrototype in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
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<MappingDataNode>(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<string, object?>();
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<string, object?>();
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<MappingDataNode>(compInstance.GetType(), compInstance, true);
FieldEntry.NormalizeFlagsToSequences(compInstance, node);
defaultObj = FieldEntry.DataNodeToObject(node);
}
catch
{
defaultObj = new Dictionary<string, object?>();
}
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<string, object?>
{
["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<string, object?> GetOrCreateEntry(Dictionary<string, Dictionary<string, object?>> output, string key)
{
if (!output.TryGetValue(key, out var map))
{
map = new Dictionary<string, object?>();
output[key] = map;
}
return map;
}
public static Dictionary<string, object?> BuildEntityComponentMap(EntityPrototype entProto, IPrototypeManager proto, ISerializationManager ser, IComponentFactory compFactory)
{
var components = new Dictionary<string, object?>(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;
}
}

View file

@ -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<IPrototypeManager>();
// Map: entity id -> list of component names.
// Map: component name -> list of entity ids.
var output = new Dictionary<string, List<string>>();
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<string>();
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<string> GetOrCreateEntry(Dictionary<string, List<string>> output, string key)
{
if (!output.TryGetValue(key, out var ids))
{
ids = new List<string>();
output[key] = ids;
}
return ids;
}
}

View file

@ -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<IPrototypeManager>();
@ -98,6 +90,6 @@ public static class ComponentStoreGenerator
normalized[refEntId] = compMap;
}
JsonSerializer.Serialize(stream, normalized, SerializeOptions);
GuideJson.Write(stream, normalized);
}
}

View file

@ -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<IPrototypeManager>();
var prototypes =
prototype
.EnumeratePrototypes<EntityPrototype>()
.Where(x => !x.Abstract)
.Select(x => new EntityJsonGenerator(x))
.ToDictionary(x => x.Id, x => x);
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var prototypes = prototypeManager
.EnumeratePrototypes<EntityPrototype>()
.Where(x => !x.Abstract)
.Select(x => new EntityJsonGenerator(x))
.ToDictionary(x => x.Id, x => x);
JsonSerializer.Serialize(stream, prototypes, SerializeOptions);
GuideJson.Write(stream, prototypes);
}
}

View file

@ -67,7 +67,7 @@ public static class EntityNameDuplicatesJsonGenerator
{
var name = TextTools.CapitalizeString(TextTools.GetDisplayName(p, prototypeManager, loc));
if (p.TryGetComponent<ActionComponent>(out _, compFactory))
if (p.TryComp<ActionComponent>(out _, compFactory))
name = $"{name} {AbilitySuffix}";
var label = GetLabel(p);

View file

@ -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);
}
}

View file

@ -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<string> 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);
}
}

View file

@ -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<string, object?>();
var component = factory.GetComponent(registration.Type);
return SerializeDefault(component, component.GetType(), serializationManager, removeId: false);
}
catch
{
return new Dictionary<string, object?>();
}
}
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<MappingDataNode>(type, value, alwaysWrite);
return true;
}
catch
{
node = new MappingDataNode();
return false;
}
}
public static Dictionary<string, object?> DeduplicateAgainstDefault(
object? defaultObject,
Dictionary<string, object?> values)
{
if (defaultObject is Dictionary<string, object?> defaults)
{
foreach (var fields in values.Values.OfType<Dictionary<string, object?>>())
{
dict[kv.Key] = DataNodeToObject(kv.Value);
RemoveDefaults(defaults, fields);
}
if (node.Tag != null)
{
var wrapped = new Dictionary<string, object?>
{
[node.Tag] = dict
};
return wrapped;
}
return dict;
}
if (node is SequenceDataNode sequence)
return new Dictionary<string, object?> { [DefaultField] = defaultObject, [PrototypeId] = values };
}
public static object? ConvertNode(DataNode node)
{
return node switch
{
var items = new List<object?>();
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<string, object?>();
var canRewrite = true;
foreach (var obj in items)
{
if (obj is not Dictionary<string, object?> 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<string, object?> { [node.Tag] = result };
}
var key = $"type:{typeVal}";
var cloned = new Dictionary<string, object?>(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<string, object?> map || !map.ContainsKey(ComponentType)))
return values;
if (canRewrite && typedMap.Count > 0)
return typedMap;
return items;
var result = new Dictionary<string, object?>();
foreach (var value in values.Cast<Dictionary<string, object?>>())
{
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<string, object?>
{
[node.Tag] = string.IsNullOrEmpty(raw)
? new Dictionary<string, object?>()
: 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<FlagsAttribute>() == 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<FlagsAttribute>(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<string>();
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<object>()
.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<string, object?>();
// Fields
if (removeId)
node.Remove(PrototypeId);
NormalizeFlagsToSequences(instance, node);
return ConvertNode(node);
}
catch
{
return new Dictionary<string, object?>();
}
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<string, object?> { [node.Tag] = new Dictionary<string, object?>() };
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<string, object?> { [node.Tag] = value };
}
private static void RemoveDefaults(Dictionary<string, object?> defaults, Dictionary<string, object?> 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<string, object?> defaultMap &&
values[key] is Dictionary<string, object?> valueMap)
{
RemoveDefaults(defaultMap, valueMap);
}
}
}
private static bool Equal(object? first, object? second)
{
if (first is IDictionary<string, object?> firstMap &&
second is IDictionary<string, object?> 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<Type, SerializedMember[]> MemberCache = new();
private static IEnumerable<SerializedMember> 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<SerializedMember>();
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<DataFieldAttribute>() 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<DataFieldAttribute>() 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;
}
}
}
}

View file

@ -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<Stream> 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);
}
}

View file

@ -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;
}

View file

@ -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<string, string>
{
{ "license", license },
{ "copyright", copyright }
};
output[relativeResourcePath] = new Dictionary<string, string>
{
{ "license", license },
{ "copyright", copyright }
};
}
catch
{
// Ignore
}
}
if (output.Count == 0)
return;
JsonSerializer.Serialize(stream, output, SerializeOptions);
GuideJson.Write(stream, output);
}
}

View file

@ -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<IPrototypeManager>();
var serializationManager = IoCManager.Resolve<ISerializationManager>();
var componentFactory = IoCManager.Resolve<IComponentFactory>();
var destinationRoot = new ResPath("prototype").ToRootedPath();
public static void PublishAll(IResourceManager res, ResPath destRoot)
{
var proto = IoCManager.Resolve<IPrototypeManager>();
var ser = IoCManager.Resolve<ISerializationManager>();
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 <see cref="EntityJsonGenerator"/>.
if (kind == typeof(EntityPrototype))
continue;
var isEntityPrototype = kind == typeof(EntityPrototype);
if (HasUnsafeSerializedDataField(kind))
if (HasUnsafeSerializedDataField(kind, new HashSet<Type>()))
continue;
// Map: entity id -> prototype fields
var map = new Dictionary<string, object?>();
foreach (var p in proto.EnumeratePrototypes(kind))
foreach (var prototype in prototypeManager.EnumeratePrototypes(kind))
{
var node = ser.WriteValueAs<MappingDataNode>(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<MappingDataNode>(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<string, object?>();
}
var defaultObject = FieldEntry.ComputePrototypeDefault(kind, serializationManager);
var output = FieldEntry.DeduplicateAgainstDefault(defaultObject, map);
res.UserData.CreateDir(destinationRoot);
var outObj = new Dictionary<string, object?>
{
["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<string, object?> 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<Type>());
if (fields is not Dictionary<string, object?> 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<Type> 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<MemberInfo>()
.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);
}
}

View file

@ -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<IPrototypeManager>();
@ -42,6 +34,6 @@ public static class PrototypeListGenerator
if (output.Count == 0)
return;
JsonSerializer.Serialize(stream, output, SerializeOptions);
GuideJson.Write(stream, output);
}
}

View file

@ -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<IPrototypeManager>();
@ -60,7 +52,6 @@ public static class PrototypeStoreGenerator
if (output.Count == 0)
return;
JsonSerializer.Serialize(stream, output, SerializeOptions);
GuideJson.Write(stream, output);
}
}

View file

@ -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<IPrototypeManager>();
var ser = IoCManager.Resolve<ISerializationManager>();
var compFactory = IoCManager.Resolve<IComponentFactory>();
var output = new Dictionary<string, List<string>>();
foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype)))
{
if (p is not EntityPrototype entProto)
continue;
if (entProto.Abstract)
continue;
var tags = new HashSet<string>();
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<string> GetOrCreateEntry(Dictionary<string, List<string>> output, string key)
{
if (!output.TryGetValue(key, out var ids))
{
ids = new List<string>();
output[key] = ids;
}
return ids;
}
private static void ExtractTagsFromMapping(
Dictionary<string, MappingDataNode> composed,
string compName,
HashSet<string> 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);
}
}
}

View file

@ -151,25 +151,7 @@ namespace Content.Server.Entry
{
var resPath = new ResPath(dest).ToRootedPath();
// Corvax-Wiki-Start
void WriteFile(string name, Action<Stream> 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<IBaseServer>().Shutdown("Data generation done");
return;

View file

@ -17,13 +17,13 @@ public sealed partial class CCVars
CVarDef.Create("autogen.entity_screenshot.output_path", "/Textures/Entities", CVar.CLIENTONLY);
/// <summary>
/// Prefix for prototype folders included in entity_project generation.
/// Prefixes for prototype folders included in entity_project generation. Comma-separated.
/// </summary>
public static readonly CVarDef<string> EntityProjectFolderPrefix =
CVarDef.Create("autogen.entity_project.folder_prefix", string.Empty, CVar.ARCHIVE);
/// <summary>
/// Prototype folder name to exclude from entity_project generation.
/// Prototype folder names to exclude from entity_project generation. Comma-separated.
/// </summary>
public static readonly CVarDef<string> EntityProjectExcludedCoreProjectFolder =
CVarDef.Create("autogen.entity_project.excluded_core_project_folder", string.Empty, CVar.ARCHIVE);

View file

@ -17,10 +17,12 @@ public static class EntityProjectHelper
{
var cfg = IoCManager.Resolve<IConfigurationManager>();
var res = IoCManager.Resolve<IResourceManager>();
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<string>();
@ -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;