diff --git a/.github/workflows/update-wiki.yml b/.github/workflows/update-wiki.yml index ddd21f2f0b..c407c15b34 100644 --- a/.github/workflows/update-wiki.yml +++ b/.github/workflows/update-wiki.yml @@ -6,15 +6,11 @@ on: branches: [ master, jsondump ] paths: - '.github/workflows/update-wiki.yml' - - 'Content.Shared/Chemistry/**.cs' - - 'Content.Server/Chemistry/**.cs' - - 'Content.Server/GuideGenerator/**.cs' - - 'Content.Server/Corvax/GuideGenerator/**.cs' - - 'Resources/Prototypes/Reagents/**.yml' - - 'Resources/Prototypes/Chemistry/**.yml' - - 'Resources/Prototypes/Recipes/Reactions/**.yml' + - 'Content.Shared/' + - 'Content.Server/' + - 'Content.Clietn/' + - 'Resources/' - 'RobustToolbox/' - - 'Resources/Locale/**.ftl' jobs: update-wiki: @@ -52,52 +48,50 @@ jobs: run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json continue-on-error: true - - name: Upload chem_prototypes.json to wiki - uses: jtmullen/mediawiki-edit-action@v0.1.1 - with: - wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json - edit_summary: Update chem_prototypes.json via GitHub Actions - page_name: "${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json" - api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php - username: ${{ secrets.WIKI_BOT_USER }} - password: ${{ secrets.WIKI_BOT_PASS }} + # Проходит по всем JSON-файлам в директории BASE и загружает каждый файл как страницу в MediaWiki. + # Имя страницы формируется из относительного пути к файлу. + - name: Upload JSON files to wiki + shell: bash + run: | + set -euo pipefail - - name: Upload react_prototypes.json to wiki - uses: jtmullen/mediawiki-edit-action@v0.1.1 - with: - wiki_text_file: ./bin/Content.Server/data/react_prototypes.json - edit_summary: Update react_prototypes.json via GitHub Actions - page_name: "${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json" - api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php - username: ${{ secrets.WIKI_BOT_USER }} - password: ${{ secrets.WIKI_BOT_PASS }} + BASE="./bin/Content.Server/data" + ROOT="${{ secrets.WIKI_PAGE_ROOT }}" + API="${{ secrets.WIKI_ROOT_URL }}/api.php" + USER="${{ secrets.WIKI_BOT_USER }}" + PASS="${{ secrets.WIKI_BOT_PASS }}" - - name: Upload entity_prototypes.json to wiki - uses: jtmullen/mediawiki-edit-action@v0.1.1 - with: - wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json - edit_summary: Update entity_prototypes.json via GitHub Actions - page_name: "${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json" - api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php - username: ${{ secrets.WIKI_BOT_USER }} - password: ${{ secrets.WIKI_BOT_PASS }} + API="$(printf "%s" "$API" | tr -d '\r\n' | sed 's/[[:space:]]*$//')" + USER="$(printf "%s" "$USER" | tr -d '\r\n')" + PASS="$(printf "%s" "$PASS" | tr -d '\r\n')" + ROOT="$(printf "%s" "$ROOT" | tr -d '\r\n' | sed 's/[[:space:]]*$//')" - - name: Upload mealrecipes_prototypes.json to wiki - uses: jtmullen/mediawiki-edit-action@v0.1.1 - with: - wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json - edit_summary: Update mealrecipes_prototypes.json via GitHub Actions - page_name: "${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json" - api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php - username: ${{ secrets.WIKI_BOT_USER }} - password: ${{ secrets.WIKI_BOT_PASS }} + cookiejar="$(mktemp)" + trap 'rm -f "$cookiejar"' EXIT - - name: Upload loc.json to wiki - uses: jtmullen/mediawiki-edit-action@v0.1.1 - with: - wiki_text_file: ./bin/Content.Server/data/loc.json - edit_summary: Update loc.json via GitHub Actions - page_name: "${{ secrets.WIKI_PAGE_ROOT }}/loc.json" - api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php - username: ${{ secrets.WIKI_BOT_USER }} - password: ${{ secrets.WIKI_BOT_PASS }} + login_token=$(curl -sS -c "$cookiejar" --data "action=query&meta=tokens&type=login&format=json" "$API" | jq -r '.query.tokens.logintoken') + curl -sS -c "$cookiejar" -b "$cookiejar" \ + --data-urlencode "action=login" \ + --data-urlencode "lgname=$USER" \ + --data-urlencode "lgpassword=$PASS" \ + --data-urlencode "lgtoken=$login_token" \ + --data-urlencode "format=json" \ + "$API" > /dev/null + + find "$BASE" -type f -name '*.json' | while IFS= read -r file; do + rel="${file#$BASE/}" + rel="$(printf "%s" "$rel" | tr -d '\r\n' | sed 's/:/_/g')" + page="$ROOT/$rel" + echo "Uploading $rel → $page" + + token=$(curl -sS -b "$cookiejar" --data "action=query&meta=tokens&format=json" "$API" | jq -r '.query.tokens.csrftoken') + + curl -sS -b "$cookiejar" \ + --data-urlencode "action=edit" \ + --data-urlencode "title=$page" \ + --data-urlencode "summary=Update $rel via GitHub Actions" \ + --data-urlencode "text@${file}" \ + --data-urlencode "token=$token" \ + --data-urlencode "format=json" \ + "$API" | jq -r '.' + done diff --git a/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs new file mode 100644 index 0000000000..9c21ab2d4d --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/ComponentJsonGenerator.cs @@ -0,0 +1,90 @@ +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 +{ + public static void PublishAll(IResourceManager res, ResPath destRoot) + { + var proto = IoCManager.Resolve(); + var ser = IoCManager.Resolve(); + var compFactory = IoCManager.Resolve(); + var entMan = IoCManager.Resolve(); + + // Map: component name -> (entity id -> component fields) + var output = new Dictionary>(); + + foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype))) + { + if (p is not EntityPrototype entProto) + continue; + + foreach (var (compName, entry) in entProto.Components) + { + var node = ser.WriteValueAs(entry.Component.GetType(), entry.Component); + var compFields = FieldEntry.DataNodeToObject(node); + + if (!output.TryGetValue(compName, out var map)) + { + map = new Dictionary(); + output[compName] = map; + } + + map[entProto.ID] = compFields; + } + } + + if (output.Count == 0) + return; + + foreach (var (compName, map) in output) + { + // Determine default field for this component. + object? defaultObj = null; + if (compFactory.TryGetRegistration(compName, out var registration)) + { + var uid = entMan.CreateEntityUninitialized(null); + try + { + var compInstance = compFactory.GetComponent(registration.Type); + FieldEntry.EnsureFieldsCollectionsInitialized(compInstance); + entMan.AddComponent(uid, compInstance); + var node = ser.WriteValueAs(compInstance.GetType(), compInstance, true); + defaultObj = FieldEntry.DataNodeToObject(node); + } + catch + { + defaultObj = new Dictionary(); + } + finally + { + entMan.DeleteEntity(uid); + } + } + + var outObj = new Dictionary + { + ["default"] = defaultObj, + ["id"] = map + }; + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + res.UserData.CreateDir(destRoot); + var fileName = PrototypeUtility.CalculatePrototypeName(compName) + ".json"; + var file = res.UserData.OpenWriteText(destRoot / fileName); + file.Write(JsonSerializer.Serialize(outObj, serializeOptions)); + file.Flush(); + } + } +} diff --git a/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs b/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs new file mode 100644 index 0000000000..6f414d01d3 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/ComponentListGenerator.cs @@ -0,0 +1,43 @@ +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 +{ + public static void PublishJson(StreamWriter file) + { + var proto = IoCManager.Resolve(); + + // Map: entity id -> list of component names. + var output = new Dictionary>(); + + foreach (var p in proto.EnumeratePrototypes(typeof(EntityPrototype))) + { + if (p is not EntityPrototype entityProto) + continue; + + var componentNames = new List(); + foreach (var (compName, _) in entityProto.Components) + { + componentNames.Add(compName); + } + + if (componentNames.Count > 0) + output[entityProto.ID] = componentNames; + } + + if (output.Count == 0) + return; + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + file.Write(JsonSerializer.Serialize(output, serializeOptions)); + } +} diff --git a/Content.Server/Corvax/GuideGenerator/FieldEntry.cs b/Content.Server/Corvax/GuideGenerator/FieldEntry.cs new file mode 100644 index 0000000000..65479d7750 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/FieldEntry.cs @@ -0,0 +1,220 @@ +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text.RegularExpressions; +using Robust.Shared.Serialization.Markdown; +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 FieldEntry +{ + public static object? DataNodeToObject(DataNode node) + { + if (node is MappingDataNode mapping) + { + var dict = new Dictionary(); + + foreach (var kv in mapping) + { + dict[kv.Key] = DataNodeToObject(kv.Value); + } + + if (node.Tag != null) + { + var wrapped = new Dictionary + { + [node.Tag] = dict + }; + return wrapped; + } + + return dict; + } + + if (node is SequenceDataNode sequence) + { + var list = new List(); + foreach (var item in sequence) + { + list.Add(DataNodeToObject(item)); + } + + return list; + } + + if (node is ValueDataNode value) + { + if (value.IsNull) + return null; + + var raw = value.Value; + + if (bool.TryParse(raw, out var boolRes)) + return boolRes; + + if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intRes)) + return intRes; + + // Accept decimals only in a strict single-dot format. + if (Regex.IsMatch(raw, @"^[+-]?\d+\.\d+$") && + double.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleRes)) + return doubleRes; + + return raw; + } + + return node.ToString(); + } + + public static void EnsureFieldsCollectionsInitialized(object instance) + { + var type = instance.GetType(); + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + // Fields + 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 + } + } + + // Properties + foreach (var prop 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 + } + } + } + + private static Type? FindConcreteAssignableType(Type target) + { + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + var types = asm.GetTypes(); + + foreach (var t in types) + { + if (t.IsAbstract || t.IsInterface) + continue; + if (!target.IsAssignableFrom(t)) + continue; + if (t.GetConstructor(Type.EmptyTypes) == null) + continue; + return t; + } + } + + return null; + } +} diff --git a/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs b/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs new file mode 100644 index 0000000000..015757760a --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/MetaLicenseGenerator.cs @@ -0,0 +1,53 @@ +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; + +namespace Content.Server.Corvax.GuideGenerator; + +public static class MetaLicenseGenerator +{ + public static void PublishJson(StreamWriter file) + { + var workingDir = Directory.GetCurrentDirectory(); + var resourcesRoot = Path.Combine(workingDir, "Resources"); + if (!Directory.Exists(resourcesRoot)) + return; + + var output = new Dictionary>(); + + 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; + + 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('\\', '/'); + + output[relativeResourcePath] = new Dictionary + { + { "license", license }, + { "copyright", copyright } + }; + } + + if (output.Count == 0) + return; + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + file.Write(JsonSerializer.Serialize(output, serializeOptions)); + } +} + diff --git a/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs b/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs new file mode 100644 index 0000000000..09d8d56762 --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/PrototypeJsonGenerator.cs @@ -0,0 +1,75 @@ +using System.Linq; +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 +{ + public static void PublishAll(IResourceManager res, ResPath destRoot) + { + var proto = IoCManager.Resolve(); + var ser = IoCManager.Resolve(); + + foreach (var kind in proto.EnumeratePrototypeKinds().OrderBy(t => t.Name)) + { + // The entity prototype has its own generator due to its size . + if (kind == typeof(EntityPrototype)) + continue; + + // Map: entity id -> prototype fields + var map = new Dictionary(); + + foreach (var p in proto.EnumeratePrototypes(kind)) + { + var node = ser.WriteValueAs(kind, p); + node.Remove("id"); + map[p.ID] = FieldEntry.DataNodeToObject(node); + } + + if (map.Count == 0) + continue; + + // Determine default field for this prototype. + object? defaultObj = null; + try + { + var instance = Activator.CreateInstance(kind); + if (instance != null) + { + FieldEntry.EnsureFieldsCollectionsInitialized(instance); + var defaultNode = ser.WriteValueAs(kind, instance, true); + defaultNode.Remove("id"); + defaultObj = FieldEntry.DataNodeToObject(defaultNode); + } + } + catch + { + defaultObj = new Dictionary(); + } + + var outObj = new Dictionary + { + ["default"] = defaultObj, + ["id"] = map + }; + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + res.UserData.CreateDir(destRoot); + var fileName = PrototypeUtility.CalculatePrototypeName(kind.Name) + ".json"; + var file = res.UserData.OpenWriteText(destRoot / fileName); + file.Write(JsonSerializer.Serialize(outObj, serializeOptions)); + file.Flush(); + } + } +} diff --git a/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs b/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs new file mode 100644 index 0000000000..0d8e71fc2a --- /dev/null +++ b/Content.Server/Corvax/GuideGenerator/PrototypeListGenerator.cs @@ -0,0 +1,196 @@ +using System.IO; +using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Markdown; +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 PrototypeListGenerator +{ + public static void PublishJson(StreamWriter file) + { + var proto = IoCManager.Resolve(); + + // Map: entity id -> (prototype kind name -> list of prototype ids that reference it). + var output = new Dictionary>>(); + + foreach (var kindType in proto.EnumeratePrototypeKinds()) + { + var kindName = PrototypeUtility.CalculatePrototypeName(kindType.Name); + foreach (var p in proto.EnumeratePrototypes(kindType)) + { + if (!proto.TryGetMapping(kindType, p.ID, out var mapping)) + continue; + + var referencedEntityIds = new HashSet(); + + InspectTypeForEntityRefs(kindType, mapping, referencedEntityIds); + + if (referencedEntityIds.Count == 0) + continue; + + foreach (var entId in referencedEntityIds) + { + if (!output.TryGetValue(entId, out var byKind)) + { + byKind = new Dictionary>(); + output[entId] = byKind; + } + + if (!byKind.TryGetValue(kindName, out var list)) + { + list = new List(); + byKind[kindName] = list; + } + + list.Add(p.ID); + } + } + } + + if (output.Count == 0) + return; + + var serializeOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + file.Write(JsonSerializer.Serialize(output, serializeOptions)); + } + + private static void InspectTypeForEntityRefs(Type prototypeType, MappingDataNode mapping, HashSet outIds) + { + // Check fields. + foreach (var field in prototypeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + var attr = field.GetCustomAttribute(); + if (attr == null) + continue; + + var tag = attr.Tag ?? LowerFirst(field.Name); + if (!mapping.TryGet(tag, out var node)) + continue; + + ExtractIdsFromNode(field.FieldType, node, outIds); + } + + // Check properties. + foreach (var prop in prototypeType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + var attr = prop.GetCustomAttribute(); + if (attr == null) + continue; + + var tag = attr.Tag ?? LowerFirst(prop.Name); + if (!mapping.TryGet(tag, out var node)) + continue; + + ExtractIdsFromNode(prop.PropertyType, node, outIds); + } + } + + private static void ExtractIdsFromNode(Type memberType, DataNode node, HashSet outIds) + { + var underlying = Nullable.GetUnderlyingType(memberType) ?? memberType; + + if (IsEntProtoIdType(underlying)) + { + if (node is ValueDataNode v) + { + if (!string.IsNullOrWhiteSpace(v.Value)) + outIds.Add(v.Value); + } + else if (node is MappingDataNode m && m.TryGet("id", out var idNode) && idNode is ValueDataNode idVal) + { + if (!string.IsNullOrWhiteSpace(idVal.Value)) + outIds.Add(idVal.Value); + } + return; + } + + if (node is SequenceDataNode seq) + { + var elemType = GetElementType(underlying); + if (elemType == null) + return; + + foreach (var child in seq.Sequence) + { + ExtractIdsFromNode(elemType, child, outIds); + } + + return; + } + + if (node is MappingDataNode map) + { + foreach (var field in underlying.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + var attr = field.GetCustomAttribute(); + if (attr == null) + continue; + + var tag = attr.Tag ?? LowerFirst(field.Name); + if (!map.TryGet(tag, out var childNode)) + continue; + + ExtractIdsFromNode(field.FieldType, childNode, outIds); + } + + foreach (var prop in underlying.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + var attr = prop.GetCustomAttribute(); + if (attr == null) + continue; + + var tag = attr.Tag ?? LowerFirst(prop.Name); + if (!map.TryGet(tag, out var childNode)) + continue; + + ExtractIdsFromNode(prop.PropertyType, childNode, outIds); + } + } + } + + private static bool IsEntProtoIdType(Type t) + { + if (t == typeof(EntProtoId)) + return true; + + if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(EntProtoId<>)) + return true; + + return false; + } + + private static Type? GetElementType(Type t) + { + if (t.IsArray) + return t.GetElementType(); + + if (t.IsGenericType) + { + var genDef = t.GetGenericTypeDefinition(); + if (genDef == typeof(List<>) || genDef == typeof(IEnumerable<>) || genDef == typeof(IReadOnlyList<>) || genDef == typeof(ICollection<>)) + return t.GetGenericArguments()[0]; + } + + return null; + } + + private static string LowerFirst(string s) + { + if (string.IsNullOrEmpty(s)) + return s; + if (s.Length == 1) + return s.ToLowerInvariant(); + return char.ToLowerInvariant(s[0]) + s.Substring(1); + } +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 350e8e8b91..0335c0c36b 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -165,6 +165,19 @@ namespace Content.Server.Entry file = _res.UserData.OpenWriteText(resPath.WithName("loc.json")); LocJsonGenerator.PublishJson(file); file.Flush(); + file = _res.UserData.OpenWriteText(resPath.WithName("meta_license.json")); + MetaLicenseGenerator.PublishJson(file); + file.Flush(); + file = _res.UserData.OpenWriteText(resPath.WithName("prototype.json")); + PrototypeListGenerator.PublishJson(file); + file.Flush(); + file = _res.UserData.OpenWriteText(resPath.WithName("component.json")); + ComponentListGenerator.PublishJson(file); + file.Flush(); + PrototypeJsonGenerator.PublishAll(_res, new ResPath("prototype").ToRootedPath()); + file.Flush(); + ComponentJsonGenerator.PublishAll(_res, new ResPath("component").ToRootedPath()); + file.Flush(); // Corvax-Wiki-End Dependencies.Resolve().Shutdown("Data generation done"); return;