Reworked weapon documentation generation

Switched the Utility's ExtractWeaponDocsCommand output to JSON.
Added a Python script to generate documentation Markdown from JSON.
This commit is contained in:
penev92
2022-03-10 20:02:17 +02:00
committed by Matthias Mailänder
parent c21bf31ebc
commit a522457bb6
4 changed files with 170 additions and 55 deletions

View File

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

View File

@@ -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<CustomAttributeTypedArgument>).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);

View File

@@ -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<IWarhead>().OrderBy(t => t.Namespace);
var projectiles = objectCreator.GetTypesImplementing<IProjectileInfo>().OrderBy(t => t.Namespace);
var weaponTypes = weaponInfo.Concat(projectiles).Concat(warheads);
foreach (var t in weaponTypes)
{
// skip helpers like TraitInfo<T>
if (t.ContainsGenericParameters || t.IsAbstract)
continue;
if (currentNamespace != t.Namespace)
{
currentNamespace = t.Namespace;
doc.AppendLine();
doc.AppendLine($"## {currentNamespace}");
var json = GenerateJson(version, weaponTypes, objectCreator);
Console.WriteLine(json);
}
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<DescAttribute>(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)
static string GenerateJson(string version, IEnumerable<Type> weaponTypes, ObjectCreator objectCreator)
{
var defaultValue = FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value;
var fieldType = Util.FriendlyTypeName(info.Field.FieldType);
var fieldDescLines = info.Field.GetCustomAttributes<DescAttribute>(true).SelectMany(d => d.Lines);
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<DescAttribute>(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<DescAttribute>(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;
doc.AppendLine($"| {info.YamlName} | {defaultValue} | {fieldType} | {string.Join(" ", fieldDescLines)} |");
}
}
return new
{
Name = name,
Parameters = a.Constructor.GetParameters()
.Select(pi => new
{
pi.Name,
Value = Util.GetAttributeParameterValue(a.ConstructorArguments[pi.Position])
})
};
})
})
});
Console.Write(doc.ToString());
var result = new
{
Version = version,
WeaponTypes = weaponTypesInfo
};
return JsonConvert.SerializeObject(result);
}
}
}

94
packaging/format-docs.py Normal file
View File

@@ -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]])