mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
[Wiki] Обновление ботов (#3719)
This commit is contained in:
parent
eacc62bbaf
commit
943d1af8a5
18 changed files with 661 additions and 571 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
Content.Server/Corvax/GuideGenerator/GuideGenerator.cs
Normal file
57
Content.Server/Corvax/GuideGenerator/GuideGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
90
Content.Server/Corvax/GuideGenerator/TagJsonGenerator.cs
Normal file
90
Content.Server/Corvax/GuideGenerator/TagJsonGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue