Reworked trait documentation generation

Switched the Utility's ExtractTraitDocsCommand output to JSON.
Updated documentation generation to use that and the new Python script to generate the Markdown file, same as the Weapon documentation.
This commit is contained in:
penev92
2022-04-20 14:12:32 +03:00
committed by Matthias Mailänder
parent a522457bb6
commit 737cdd7851
2 changed files with 64 additions and 74 deletions

View File

@@ -91,7 +91,7 @@ jobs:
env: env:
GIT_TAG: ${{ github.event.inputs.tag }} GIT_TAG: ${{ github.event.inputs.tag }}
run: | run: |
./utility.sh all --docs "${GIT_TAG}" > "docs/api/playtest/traits.md" ./utility.sh all --docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/playtest/traits.md"
./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "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" ./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/playtest/lua.md"
@@ -100,7 +100,7 @@ jobs:
env: env:
GIT_TAG: ${{ github.event.inputs.tag }} GIT_TAG: ${{ github.event.inputs.tag }}
run: | run: |
./utility.sh all --docs "${GIT_TAG}" > "docs/api/release/traits.md" ./utility.sh all --docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/release/traits.md"
./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "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" ./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/release/lua.md"

View File

@@ -10,8 +10,10 @@
#endregion #endregion
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using Newtonsoft.Json;
using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA.Mods.Common.UtilityCommands namespace OpenRA.Mods.Common.UtilityCommands
@@ -25,7 +27,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
return true; return true;
} }
[Desc("[VERSION]", "Generate trait documentation in MarkDown format.")] [Desc("[VERSION]", "Generate trait documentation in JSON format.")]
void IUtilityCommand.Run(Utility utility, string[] args) void IUtilityCommand.Run(Utility utility, string[] args)
{ {
// HACK: The engine code assumes that Game.modData is set. // HACK: The engine code assumes that Game.modData is set.
@@ -35,84 +37,72 @@ namespace OpenRA.Mods.Common.UtilityCommands
if (args.Length > 1) if (args.Length > 1)
version = args[1]; version = args[1];
Console.WriteLine( var objectCreator = utility.ModData.ObjectCreator;
"This documentation is aimed at modders. It displays all traits with default values and developer commentary. " + var traitInfos = objectCreator.GetTypesImplementing<TraitInfo>().OrderBy(t => t.Namespace);
"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.");
Console.WriteLine();
var doc = new StringBuilder(); var json = GenerateJson(version, traitInfos, objectCreator);
var currentNamespace = ""; Console.WriteLine(json);
}
foreach (var t in Game.ModData.ObjectCreator.GetTypesImplementing<TraitInfo>().OrderBy(t => t.Namespace)) static string GenerateJson(string version, IEnumerable<Type> traitTypes, ObjectCreator objectCreator)
{ {
if (t.ContainsGenericParameters || t.IsAbstract) var traitTypesInfo = traitTypes.Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
continue; // skip helpers like TraitInfo<T> .Select(type => new
if (currentNamespace != t.Namespace)
{ {
currentNamespace = t.Namespace; type.Namespace,
doc.AppendLine(); Name = type.Name.EndsWith("Info") ? type.Name.Substring(0, type.Name.Length - 4) : type.Name,
doc.AppendLine($"## {currentNamespace}"); Description = string.Join(" ", type.GetCustomAttributes<DescAttribute>(false).SelectMany(d => d.Lines)),
} RequiresTraits = RequiredTraitTypes(type)
.Select(y => y.Name),
var traitName = t.Name.EndsWith("Info") ? t.Name.Substring(0, t.Name.Length - 4) : t.Name; InheritedTypes = type.BaseTypes()
var traitDescLines = t.GetCustomAttributes<DescAttribute>(false).SelectMany(d => d.Lines); .Select(y => y.Name)
doc.AppendLine(); .Where(y => y != type.Name && y != $"{type.Name}Info" && y != "Object" && y != "TraitInfo`1"), // HACK: This is the simplest way to exclude TraitInfo<T>, which doesn't serialize well.
doc.AppendLine($"### {traitName}"); Properties = FieldLoader.GetTypeLoadInfo(type)
foreach (var line in traitDescLines) .Where(fi => fi.Field.IsPublic && fi.Field.IsInitOnly && !fi.Field.IsStatic)
doc.AppendLine(line); .Select(fi => new
var requires = RequiredTraitTypes(t);
var reqCount = requires.Length;
if (reqCount > 0)
{ {
if (t.HasAttribute<DescAttribute>()) PropertyName = fi.YamlName,
doc.AppendLine(); DefaultValue = FieldSaver.SaveField(objectCreator.CreateBasic(type), fi.Field.Name).Value.Value,
InternalType = Util.InternalTypeName(fi.Field.FieldType),
doc.Append($"Requires trait{(reqCount > 1 ? "s" : "")}: "); UserFriendlyType = Util.FriendlyTypeName(fi.Field.FieldType),
Description = string.Join(" ", fi.Field.GetCustomAttributes<DescAttribute>(true).SelectMany(d => d.Lines)),
var i = 0; OtherAttributes = fi.Field.CustomAttributes
foreach (var require in requires) .Where(a => a.AttributeType.Name != nameof(DescAttribute) && a.AttributeType.Name != nameof(FieldLoader.LoadUsingAttribute))
.Select(a =>
{ {
var n = require.Name; var name = a.AttributeType.Name;
var name = n.EndsWith("Info") ? n.Remove(n.Length - 4, 4) : n; name = name.EndsWith("Attribute") ? name.Substring(0, name.Length - 9) : name;
doc.Append($"[`{name}`](#{name.ToLowerInvariant()}){(i + 1 == reqCount ? ".\n" : ", ")}");
i++;
}
}
var infos = FieldLoader.GetTypeLoadInfo(t); return new
if (!infos.Any())
continue;
doc.AppendLine();
doc.AppendLine("| Property | Default Value | Type | Description |");
doc.AppendLine("| -------- | --------------| ---- | ----------- |");
var liveTraitInfo = Game.ModData.ObjectCreator.CreateBasic(t);
foreach (var info in infos)
{ {
var fieldDescLines = info.Field.GetCustomAttributes<DescAttribute>(true).SelectMany(d => d.Lines); Name = name,
var fieldType = Util.FriendlyTypeName(info.Field.FieldType); Parameters = a.Constructor.GetParameters()
var loadInfo = info.Field.GetCustomAttributes<FieldLoader.SerializeAttribute>(true).FirstOrDefault(); .Select(pi => new
var defaultValue = loadInfo != null && loadInfo.Required ? "*(required)*" : FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value; {
doc.Append($"| {info.YamlName} | {defaultValue} | {fieldType} | "); pi.Name,
foreach (var line in fieldDescLines) Value = Util.GetAttributeParameterValue(a.ConstructorArguments[pi.Position])
doc.Append(line + " "); })
doc.AppendLine("|"); };
} })
})
});
var result = new
{
Version = version,
TraitInfos = traitTypesInfo
};
return JsonConvert.SerializeObject(result);
} }
Console.Write(doc.ToString()); static IEnumerable<Type> RequiredTraitTypes(Type t)
}
static Type[] RequiredTraitTypes(Type t)
{ {
return t.GetInterfaces() return t.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>)) .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>))
.SelectMany(i => i.GetGenericArguments()) .SelectMany(i => i.GetGenericArguments())
.Where(i => !i.IsInterface && !t.IsSubclassOf(i)) .Where(i => !i.IsInterface && !t.IsSubclassOf(i))
.OrderBy(i => i.Name) .OrderBy(i => i.Name);
.ToArray();
} }
} }
} }