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:
committed by
Matthias Mailänder
parent
c21bf31ebc
commit
a522457bb6
6
.github/workflows/documentation.yml
vendored
6
.github/workflows/documentation.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
var json = GenerateJson(version, weaponTypes, objectCreator);
|
||||
Console.WriteLine(json);
|
||||
}
|
||||
|
||||
static string GenerateJson(string version, IEnumerable<Type> 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<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;
|
||||
|
||||
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<T>
|
||||
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<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)
|
||||
{
|
||||
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);
|
||||
|
||||
doc.AppendLine($"| {info.YamlName} | {defaultValue} | {fieldType} | {string.Join(" ", fieldDescLines)} |");
|
||||
}
|
||||
}
|
||||
|
||||
Console.Write(doc.ToString());
|
||||
return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
94
packaging/format-docs.py
Normal file
94
packaging/format-docs.py
Normal 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]])
|
||||
Reference in New Issue
Block a user