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 }}
|
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}" > "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"
|
./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/playtest/lua.md"
|
||||||
|
|
||||||
- name: Update docs.openra.net (Release)
|
- name: Update docs.openra.net (Release)
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
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}" > "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"
|
./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/release/lua.md"
|
||||||
|
|
||||||
- name: Push docs.openra.net
|
- name: Push docs.openra.net
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
cd docs
|
cd docs
|
||||||
git config --local user.email "actions@github.com"
|
git config --local user.email "actions@github.com"
|
||||||
git config --local user.name "GitHub Actions"
|
git config --local user.name "GitHub Actions"
|
||||||
git add --all
|
git add *.md
|
||||||
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using OpenRA.GameRules;
|
using OpenRA.GameRules;
|
||||||
using OpenRA.Mods.Common.Traits;
|
using OpenRA.Mods.Common.Traits;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
@@ -298,6 +299,23 @@ namespace OpenRA.Mods.Common
|
|||||||
return t.Name;
|
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)
|
public static int GetProjectileInaccuracy(int baseInaccuracy, InaccuracyType inaccuracyType, ProjectileArgs args)
|
||||||
{
|
{
|
||||||
var inaccuracy = ApplyPercentageModifiers(baseInaccuracy, args.InaccuracyModifiers);
|
var inaccuracy = ApplyPercentageModifiers(baseInaccuracy, args.InaccuracyModifiers);
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using Newtonsoft.Json;
|
||||||
using OpenRA.GameRules;
|
using OpenRA.GameRules;
|
||||||
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.UtilityCommands
|
namespace OpenRA.Mods.Common.UtilityCommands
|
||||||
@@ -26,7 +28,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
return true;
|
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)
|
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.
|
||||||
@@ -36,64 +38,65 @@ namespace OpenRA.Mods.Common.UtilityCommands
|
|||||||
if (args.Length > 1)
|
if (args.Length > 1)
|
||||||
version = args[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 objectCreator = utility.ModData.ObjectCreator;
|
||||||
var weaponInfo = new[] { typeof(WeaponInfo) };
|
var weaponInfo = new[] { typeof(WeaponInfo) };
|
||||||
var warheads = objectCreator.GetTypesImplementing<IWarhead>().OrderBy(t => t.Namespace);
|
var warheads = objectCreator.GetTypesImplementing<IWarhead>().OrderBy(t => t.Namespace);
|
||||||
var projectiles = objectCreator.GetTypesImplementing<IProjectileInfo>().OrderBy(t => t.Namespace);
|
var projectiles = objectCreator.GetTypesImplementing<IProjectileInfo>().OrderBy(t => t.Namespace);
|
||||||
|
|
||||||
var weaponTypes = weaponInfo.Concat(projectiles).Concat(warheads);
|
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>
|
Version = version,
|
||||||
if (t.ContainsGenericParameters || t.IsAbstract)
|
WeaponTypes = weaponTypesInfo
|
||||||
continue;
|
};
|
||||||
|
|
||||||
if (currentNamespace != t.Namespace)
|
return JsonConvert.SerializeObject(result);
|
||||||
{
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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