From a522457bb612920432426bb010e425927fb5969e Mon Sep 17 00:00:00 2001 From: penev92 Date: Thu, 10 Mar 2022 20:02:17 +0200 Subject: [PATCH] Reworked weapon documentation generation Switched the Utility's ExtractWeaponDocsCommand output to JSON. Added a Python script to generate documentation Markdown from JSON. --- .github/workflows/documentation.yml | 6 +- OpenRA.Mods.Common/Util.cs | 18 +++ .../ExtractWeaponDocsCommand.cs | 107 +++++++++--------- packaging/format-docs.py | 94 +++++++++++++++ 4 files changed, 170 insertions(+), 55 deletions(-) create mode 100644 packaging/format-docs.py diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index d398091816..6682f4f4ba 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -92,7 +92,7 @@ jobs: GIT_TAG: ${{ github.event.inputs.tag }} run: | ./utility.sh all --docs "${GIT_TAG}" > "docs/api/playtest/traits.md" - ./utility.sh all --weapon-docs "${GIT_TAG}" > "docs/api/playtest/weapons.md" + ./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/playtest/weapons.md" ./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/playtest/lua.md" - name: Update docs.openra.net (Release) @@ -101,7 +101,7 @@ jobs: GIT_TAG: ${{ github.event.inputs.tag }} run: | ./utility.sh all --docs "${GIT_TAG}" > "docs/api/release/traits.md" - ./utility.sh all --weapon-docs "${GIT_TAG}" > "docs/api/release/weapons.md" + ./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/release/weapons.md" ./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/release/lua.md" - name: Push docs.openra.net @@ -111,7 +111,7 @@ jobs: cd docs git config --local user.email "actions@github.com" git config --local user.name "GitHub Actions" - git add --all + git add *.md git commit -m "Update auto-generated documentation for ${GIT_TAG}" git push origin master diff --git a/OpenRA.Mods.Common/Util.cs b/OpenRA.Mods.Common/Util.cs index 240a6220de..f1358eb23a 100644 --- a/OpenRA.Mods.Common/Util.cs +++ b/OpenRA.Mods.Common/Util.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Reflection; using OpenRA.GameRules; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; @@ -298,6 +299,23 @@ namespace OpenRA.Mods.Common return t.Name; } + public static string GetAttributeParameterValue(CustomAttributeTypedArgument value) + { + if (value.ArgumentType.IsEnum) + return Enum.Parse(value.ArgumentType, value.Value.ToString()).ToString(); + + if (value.ArgumentType == typeof(Type) && value.Value != null) + return (value.Value as Type).Name; + + if (value.ArgumentType.IsArray) + { + var names = (value.Value as IReadOnlyCollection).Select(x => (x.Value as Type).Name); + return string.Join(", ", names); + } + + return value.Value?.ToString(); + } + public static int GetProjectileInaccuracy(int baseInaccuracy, InaccuracyType inaccuracyType, ProjectileArgs args) { var inaccuracy = ApplyPercentageModifiers(baseInaccuracy, args.InaccuracyModifiers); diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs index f3f5229729..f67bb5432c 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs @@ -10,9 +10,11 @@ #endregion using System; +using System.Collections.Generic; using System.Linq; -using System.Text; +using Newtonsoft.Json; using OpenRA.GameRules; +using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.UtilityCommands @@ -26,7 +28,7 @@ namespace OpenRA.Mods.Common.UtilityCommands return true; } - [Desc("[VERSION]", "Generate weaponry documentation in MarkDown format.")] + [Desc("[VERSION]", "Generate weaponry documentation in JSON format.")] void IUtilityCommand.Run(Utility utility, string[] args) { // HACK: The engine code assumes that Game.modData is set. @@ -36,64 +38,65 @@ namespace OpenRA.Mods.Common.UtilityCommands if (args.Length > 1) version = args[1]; - var doc = new StringBuilder(); - - doc.AppendLine( - "This documentation is aimed at modders. It displays a template for weapon definitions " + - "as well as its contained types (warheads and projectiles) with default values and developer commentary. " + - "Please do not edit it directly, but add new `[Desc(\"String\")]` tags to the source code. This file has been " + - $"automatically generated for version {version} of OpenRA."); - doc.AppendLine(); - - var currentNamespace = ""; - var objectCreator = utility.ModData.ObjectCreator; var weaponInfo = new[] { typeof(WeaponInfo) }; var warheads = objectCreator.GetTypesImplementing().OrderBy(t => t.Namespace); var projectiles = objectCreator.GetTypesImplementing().OrderBy(t => t.Namespace); var weaponTypes = weaponInfo.Concat(projectiles).Concat(warheads); - foreach (var t in weaponTypes) + + var json = GenerateJson(version, weaponTypes, objectCreator); + Console.WriteLine(json); + } + + static string GenerateJson(string version, IEnumerable weaponTypes, ObjectCreator objectCreator) + { + var weaponTypesInfo = weaponTypes.Where(x => !x.ContainsGenericParameters && !x.IsAbstract) + .Select(type => new + { + type.Namespace, + Name = type.Name.EndsWith("Info") ? type.Name.Substring(0, type.Name.Length - 4) : type.Name, + Description = string.Join(" ", type.GetCustomAttributes(false).SelectMany(d => d.Lines)), + InheritedTypes = type.BaseTypes() + .Select(y => y.Name) + .Where(y => y != type.Name && y != $"{type.Name}Info" && y != "Object"), + Properties = FieldLoader.GetTypeLoadInfo(type) + .Where(fi => fi.Field.IsPublic && fi.Field.IsInitOnly && !fi.Field.IsStatic) + .Select(fi => new + { + PropertyName = fi.YamlName, + DefaultValue = FieldSaver.SaveField(objectCreator.CreateBasic(type), fi.Field.Name).Value.Value, + InternalType = Util.InternalTypeName(fi.Field.FieldType), + UserFriendlyType = Util.FriendlyTypeName(fi.Field.FieldType), + Description = string.Join(" ", fi.Field.GetCustomAttributes(true).SelectMany(d => d.Lines)), + OtherAttributes = fi.Field.CustomAttributes + .Where(a => a.AttributeType.Name != nameof(DescAttribute) && a.AttributeType.Name != nameof(FieldLoader.LoadUsingAttribute)) + .Select(a => + { + var name = a.AttributeType.Name; + name = name.EndsWith("Attribute") ? name.Substring(0, name.Length - 9) : name; + + return new + { + Name = name, + Parameters = a.Constructor.GetParameters() + .Select(pi => new + { + pi.Name, + Value = Util.GetAttributeParameterValue(a.ConstructorArguments[pi.Position]) + }) + }; + }) + }) + }); + + var result = new { - // skip helpers like TraitInfo - if (t.ContainsGenericParameters || t.IsAbstract) - continue; + Version = version, + WeaponTypes = weaponTypesInfo + }; - if (currentNamespace != t.Namespace) - { - currentNamespace = t.Namespace; - doc.AppendLine(); - doc.AppendLine($"## {currentNamespace}"); - } - - var traitName = t.Name.EndsWith("Info") ? t.Name.Substring(0, t.Name.Length - 4) : t.Name; - doc.AppendLine(); - doc.AppendLine($"### {traitName}"); - - var traitDescLines = t.GetCustomAttributes(false).SelectMany(d => d.Lines); - foreach (var line in traitDescLines) - doc.AppendLine(line); - - var infos = FieldLoader.GetTypeLoadInfo(t); - if (!infos.Any()) - continue; - - doc.AppendLine(); - doc.AppendLine("| Property | Default Value | Type | Description |"); - doc.AppendLine("| -------- | ------------- | ---- | ----------- |"); - - var liveTraitInfo = objectCreator.CreateBasic(t); - foreach (var info in infos) - { - var defaultValue = FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value; - var fieldType = Util.FriendlyTypeName(info.Field.FieldType); - var fieldDescLines = info.Field.GetCustomAttributes(true).SelectMany(d => d.Lines); - - doc.AppendLine($"| {info.YamlName} | {defaultValue} | {fieldType} | {string.Join(" ", fieldDescLines)} |"); - } - } - - Console.Write(doc.ToString()); + return JsonConvert.SerializeObject(result); } } } diff --git a/packaging/format-docs.py b/packaging/format-docs.py new file mode 100644 index 0000000000..2fc7fbee4e --- /dev/null +++ b/packaging/format-docs.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Copyright 2007-2022 The OpenRA Developers (see AUTHORS) +# This file is part of OpenRA, which is free software. It is made +# available to you under the terms of the GNU General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. For more +# information, see COPYING. + +import io +import sys +import json +from collections import OrderedDict + +def format_type_name(typeName, isKnownType): + name = typeName + if name.endswith("Info"): + name = name[0:-4] + + return f'[`{name}`](#{name.lower()})' if isKnownType else f'`{name}`' + +def is_known_type(typeName, types): + name = typeName + if name.endswith("Info"): + name = name[0:-4] + + result = [t for t in types if name == t["Name"]] + return len(result) > 0 + +def format_docs(version, collectionName, types): + typesByNamespace = OrderedDict() + for currentType in types: + if currentType["Namespace"] in typesByNamespace: + typesByNamespace[currentType["Namespace"]].append(currentType) + else: + typesByNamespace[currentType["Namespace"]] = [currentType] + + explanation = "" + if collectionName == "TraitInfos": + explanation = "all traits with their properties and their default values plus developer commentary" + elif collectionName == "WeaponTypes": + explanation = "a template for weapon definitions and the types it can use (warheads and projectiles) with default values and developer commentary" + elif collectionName == "SequenceTypes": + explanation = "all sequence types with their properties and their default values plus developer commentary" + + print(f"This documentation is aimed at modders and has been automatically generated for version `{version}` of OpenRA. " + + "Please do not edit it directly, but instead add new `[Desc(\"String\")]` tags to the source code.\n") + + print(f"Listed below are {explanation}.") + + for namespace in typesByNamespace: + print(f'\n## {namespace}') + + for currentType in typesByNamespace[namespace]: + print(f'\n### {currentType["Name"]}') + + if currentType["Description"]: + print(f'**{currentType["Description"]}**') + + if "InheritedTypes" in currentType and currentType["InheritedTypes"]: + inheritedTypes = [t for t in currentType["InheritedTypes"] if t not in ['TraitInfo', 'Warhead']] # Remove blacklisted types. + if inheritedTypes: + print("###### Inherits from: " + ", ".join([format_type_name(x, is_known_type(x, types)) for x in inheritedTypes]) + '.') + + if "RequiresTraits" in currentType and currentType["RequiresTraits"]: + print("###### Requires trait(s): " + ", ".join([format_type_name(x, is_known_type(x, types)) for x in currentType["RequiresTraits"]]) + '.') + + if len(currentType["Properties"]) > 0: + print() + print(f'| Property | Default Value | Type | Description |') + print(f'| -------- | ------------- | ---- | ----------- |') + + for prop in currentType["Properties"]: + if "OtherAttributes" in prop: + attributes = [] + for attribute in prop["OtherAttributes"]: + attributes.append(attribute["Name"]) + + defaultValue = '' + if prop["DefaultValue"]: + defaultValue = prop["DefaultValue"] + elif 'Require' in attributes: + defaultValue = '*(required)*' + + print(f'| {prop["PropertyName"]} | {defaultValue} | {prop["UserFriendlyType"]} | {prop["Description"]} |') + else: + print(f'| {prop["PropertyName"]} | {prop["DefaultValue"]} | {prop["UserFriendlyType"]} | {prop["Description"]} |') + +if __name__ == "__main__": + input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8-sig') + jsonInfo = json.load(input_stream) + + keys = list(jsonInfo) + if len(keys) == 2 and keys[0] == 'Version': + format_docs(jsonInfo[keys[0]], keys[1], jsonInfo[keys[1]])