Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework trait and weapon docs page generation #19948

Merged
merged 5 commits into from Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/documentation.yml
Expand Up @@ -91,17 +91,17 @@ jobs:
env:
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 --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 --lua-docs "${GIT_TAG}" > "docs/api/playtest/lua.md"

- name: Update docs.openra.net (Release)
if: startsWith(github.event.inputs.tag, 'release-')
env:
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 --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 --lua-docs "${GIT_TAG}" > "docs/api/release/lua.md"

- name: Push docs.openra.net
Expand All @@ -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

5 changes: 5 additions & 0 deletions OpenRA.Game/GameRules/WeaponInfo.cs
Expand Up @@ -127,6 +127,11 @@ public sealed class WeaponInfo
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
public readonly List<IWarhead> Warheads = new List<IWarhead>();

/// <summary>
/// This constructor is used solely for documentation generation!
/// </summary>
public WeaponInfo() { }

public WeaponInfo(MiniYaml content)
{
// Resolve any weapon-level yaml inheritance or removals
Expand Down
27 changes: 26 additions & 1 deletion OpenRA.Mods.Common/Util.cs
Expand Up @@ -13,6 +13,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
Expand Down Expand Up @@ -226,6 +227,13 @@ public static int RandomInRange(MersenneTwister random, int[] range)
return random.Next(range[0], range[1]);
}

public static string InternalTypeName(Type t)
{
return t.IsGenericType
? $"{t.Name.Substring(0, t.Name.IndexOf('`'))}<{string.Join(", ", t.GenericTypeArguments.Select(arg => arg.Name))}>"
: t.Name;
}

public static string FriendlyTypeName(Type t)
{
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(HashSet<>))
Expand All @@ -234,7 +242,7 @@ public static string FriendlyTypeName(Type t)
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var args = t.GetGenericArguments().Select(FriendlyTypeName).ToArray();
return $"Dictionary with Key: {args[0]}, Value {args[1]}";
return $"Dictionary with Key: {args[0]}, Value: {args[1]}";
}

if (t.IsSubclassOf(typeof(Array)))
Expand Down Expand Up @@ -291,6 +299,23 @@ public static string FriendlyTypeName(Type t)
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);
Expand Down
128 changes: 59 additions & 69 deletions OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs
Expand Up @@ -10,8 +10,10 @@
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using OpenRA.Primitives;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.UtilityCommands
Expand All @@ -25,7 +27,7 @@ bool IUtilityCommand.ValidateArguments(string[] args)
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)
{
// HACK: The engine code assumes that Game.modData is set.
Expand All @@ -35,84 +37,72 @@ void IUtilityCommand.Run(Utility utility, string[] args)
if (args.Length > 1)
version = args[1];

Console.WriteLine(
"This documentation is aimed at modders. It displays all traits 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.");
Console.WriteLine();
var objectCreator = utility.ModData.ObjectCreator;
var traitInfos = objectCreator.GetTypesImplementing<TraitInfo>().OrderBy(t => t.Namespace);

var doc = new StringBuilder();
var currentNamespace = "";

foreach (var t in Game.ModData.ObjectCreator.GetTypesImplementing<TraitInfo>().OrderBy(t => t.Namespace))
{
if (t.ContainsGenericParameters || t.IsAbstract)
continue; // skip helpers like TraitInfo<T>

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;
var traitDescLines = t.GetCustomAttributes<DescAttribute>(false).SelectMany(d => d.Lines);
doc.AppendLine();
doc.AppendLine($"### {traitName}");
foreach (var line in traitDescLines)
doc.AppendLine(line);

var requires = RequiredTraitTypes(t);
var reqCount = requires.Length;
if (reqCount > 0)
{
if (t.HasAttribute<DescAttribute>())
doc.AppendLine();

doc.Append($"Requires trait{(reqCount > 1 ? "s" : "")}: ");

var i = 0;
foreach (var require in requires)
{
var n = require.Name;
var name = n.EndsWith("Info") ? n.Remove(n.Length - 4, 4) : n;
doc.Append($"[`{name}`](#{name.ToLowerInvariant()}){(i + 1 == reqCount ? ".\n" : ", ")}");
i++;
}
}
var json = GenerateJson(version, traitInfos, objectCreator);
Console.WriteLine(json);
}

var infos = FieldLoader.GetTypeLoadInfo(t);
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)
static string GenerateJson(string version, IEnumerable<Type> traitTypes, ObjectCreator objectCreator)
{
var traitTypesInfo = traitTypes.Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
.Select(type => new
{
var fieldDescLines = info.Field.GetCustomAttributes<DescAttribute>(true).SelectMany(d => d.Lines);
var fieldType = Util.FriendlyTypeName(info.Field.FieldType);
var loadInfo = info.Field.GetCustomAttributes<FieldLoader.SerializeAttribute>(true).FirstOrDefault();
var defaultValue = loadInfo != null && loadInfo.Required ? "*(required)*" : FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value;
doc.Append($"| {info.YamlName} | {defaultValue} | {fieldType} | ");
foreach (var line in fieldDescLines)
doc.Append(line + " ");
doc.AppendLine("|");
}
}
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)),
RequiresTraits = RequiredTraitTypes(type)
.Select(y => y.Name),
InheritedTypes = type.BaseTypes()
.Select(y => y.Name)
.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.
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
{
Version = version,
TraitInfos = traitTypesInfo
};

Console.Write(doc.ToString());
return JsonConvert.SerializeObject(result);
}

static Type[] RequiredTraitTypes(Type t)
static IEnumerable<Type> RequiredTraitTypes(Type t)
{
return t.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(Requires<>))
.SelectMany(i => i.GetGenericArguments())
.Where(i => !i.IsInterface && !t.IsSubclassOf(i))
.OrderBy(i => i.Name)
.ToArray();
.OrderBy(i => i.Name);
}
}
}
109 changes: 55 additions & 54 deletions OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs
Expand Up @@ -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
Expand All @@ -26,7 +28,7 @@ bool IUtilityCommand.ValidateArguments(string[] args)
return true;
}

[Desc("[VERSION]", "Generate weaponry documentation in MarkDown format.")]
[Desc("[VERSION]", "Generate weaponry documentation in JSON format.")]
penev92 marked this conversation as resolved.
Show resolved Hide resolved
void IUtilityCommand.Run(Utility utility, string[] args)
{
// HACK: The engine code assumes that Game.modData is set.
Expand All @@ -36,66 +38,65 @@ void IUtilityCommand.Run(Utility utility, string[] args)
if (args.Length > 1)
version = args[1];

Console.WriteLine(
"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.");
Console.WriteLine();

var doc = new StringBuilder();

var currentNamespace = "";

var objectCreator = utility.ModData.ObjectCreator;
var weaponInfo = objectCreator.GetTypesImplementing<WeaponInfo>();
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 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;
var weaponTypes = weaponInfo.Concat(projectiles).Concat(warheads);

doc.AppendLine();
doc.AppendLine("| Property | Default Value | Type | Description |");
doc.AppendLine("| -------- | --------------| ---- | ----------- |");
var json = GenerateJson(version, weaponTypes, objectCreator);
Console.WriteLine(json);
}

var liveTraitInfo = t == typeof(WeaponInfo) ? null : objectCreator.CreateBasic(t);
foreach (var info in infos)
static string GenerateJson(string version, IEnumerable<Type> weaponTypes, ObjectCreator objectCreator)
{
var weaponTypesInfo = weaponTypes.Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
.Select(type => new
{
var fieldDescLines = info.Field.GetCustomAttributes<DescAttribute>(true).SelectMany(d => d.Lines);
var fieldType = Util.FriendlyTypeName(info.Field.FieldType);
var defaultValue = liveTraitInfo == null ? "" : FieldSaver.SaveField(liveTraitInfo, info.Field.Name).Value.Value;
doc.Append($"| {info.YamlName} | {defaultValue} | {fieldType} | ");
foreach (var line in fieldDescLines)
doc.Append(line + " ");
doc.AppendLine("|");
}
}
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
{
Version = version,
WeaponTypes = weaponTypesInfo
};

Console.Write(doc.ToString());
return JsonConvert.SerializeObject(result);
}
}
}