Moved documentation utility commands to a folder

This commit is contained in:
Pavel Penev
2025-01-07 21:57:15 +02:00
committed by Gustas
parent e903baf680
commit f2e582ce83
8 changed files with 9 additions and 8 deletions

View File

@@ -0,0 +1,477 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using OpenRA.Mods.Common.Scripting;
using OpenRA.Scripting;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
// See https://emmylua.github.io/annotation.html for reference
sealed class ExtractEmmyLuaAPI : IUtilityCommand
{
string IUtilityCommand.Name => "--emmy-lua-api";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[Desc("Generate EmmyLua API annotations for use in IDEs.")]
void IUtilityCommand.Run(Utility utility, string[] args)
{
var version = utility.ModData.Manifest.Metadata.Version;
Console.WriteLine($"-- This is an automatically generated Lua API definition generated for {version} of OpenRA.");
Console.WriteLine("-- https://wiki.openra.net/Utility was used with the --emmy-lua-api parameter.");
Console.WriteLine("-- See https://docs.openra.net/en/release/lua/ for human readable documentation.");
Console.WriteLine();
WriteDiagnosticsDisabling();
Console.WriteLine();
Console.WriteLine();
WriteManual();
Console.WriteLine();
Console.WriteLine();
var actorInits = utility.ModData.ObjectCreator.GetTypesImplementing<ActorInit>()
.Where(x => !x.IsAbstract && !x.GetInterfaces().Contains(typeof(ISuppressInitExport)));
WriteActorInits(actorInits, out var usedEnums);
Console.WriteLine();
Console.WriteLine();
WriteEnums(usedEnums);
Console.WriteLine();
Console.WriteLine();
var globalTables = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>().OrderBy(t => t.Name);
WriteGlobals(globalTables);
var actorProperties = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptActorProperties>();
WriteScriptProperties(typeof(Actor), actorProperties);
var playerProperties = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptPlayerProperties>();
WriteScriptProperties(typeof(Player), playerProperties);
}
static void WriteDiagnosticsDisabling()
{
Console.WriteLine(
"--- This file only lists function \"signatures\", causing Lua Diagnostics errors: " +
"\"Annotations specify that a return value is required here.\"");
Console.WriteLine("--- and Lua Diagnostics warnings \"Unused local\" for the functions' parameters.");
Console.WriteLine("--- Disable those specific errors for the entire file.");
Console.WriteLine("---@diagnostic disable: missing-return");
Console.WriteLine("---@diagnostic disable: unused-local");
}
static void WriteManual()
{
Console.WriteLine("--- This function is triggered once, after the map is loaded.");
Console.WriteLine("function WorldLoaded() end");
Console.WriteLine();
Console.WriteLine("--- This function will hit every game tick which by default is every 40 ms.");
Console.WriteLine("function Tick() end");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("--- Base engine types.");
Console.WriteLine();
Console.WriteLine("---@class cpos");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@field Layer integer");
Console.WriteLine("---@operator add(cvec): cpos");
Console.WriteLine("---@operator sub(cvec): cpos");
Console.WriteLine("---@operator sub(cpos): cvec");
Console.WriteLine();
Console.WriteLine("---@class wpos");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@field Z integer");
Console.WriteLine("---@operator add(wvec): wpos");
Console.WriteLine("---@operator sub(wvec): wpos");
Console.WriteLine("---@operator sub(wpos): wvec");
Console.WriteLine();
Console.WriteLine("---@class wangle");
Console.WriteLine("---@field Angle integer");
Console.WriteLine("---@operator add(wangle): wangle");
Console.WriteLine("---@operator sub(wangle): wangle");
Console.WriteLine();
Console.WriteLine("---@class wdist");
Console.WriteLine("---@field Length integer");
Console.WriteLine("---@operator add(wdist): wdist");
Console.WriteLine("---@operator sub(wdist): wdist");
Console.WriteLine("---@operator unm(wdist): wdist");
Console.WriteLine("---@operator mul(integer): wdist");
Console.WriteLine("---@operator div(integer): wdist");
Console.WriteLine();
Console.WriteLine("---@class wvec");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@field Z integer");
Console.WriteLine("---@field Facing wangle");
Console.WriteLine("---@operator add(wvec): wvec");
Console.WriteLine("---@operator sub(wvec): wvec");
Console.WriteLine("---@operator unm(wvec): wvec");
Console.WriteLine("---@operator mul(integer): wvec");
Console.WriteLine("---@operator div(integer): wvec");
Console.WriteLine();
Console.WriteLine("---@class cvec");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@field Length integer");
Console.WriteLine("---@operator add(cvec): cvec");
Console.WriteLine("---@operator sub(cvec): cvec");
Console.WriteLine("---@operator unm(cvec): cvec");
Console.WriteLine("---@operator mul(integer): cvec");
Console.WriteLine("---@operator div(integer): cvec");
Console.WriteLine();
Console.WriteLine("---@class color");
Console.WriteLine("local color = { };");
}
static void WriteActorInits(IEnumerable<Type> actorInits, out IEnumerable<Type> usedEnums)
{
Console.WriteLine("---A list of ActorInit implementations that can be used by Lua scripts.");
Console.WriteLine("---@class initTable");
var localEnums = new HashSet<Type>();
foreach (var init in actorInits)
{
var name = init.Name[..^4];
var parameters = init.GetConstructors().Select(ci => ci.GetParameters());
var parameterString = string.Join(" | ",
parameters
.Select(cp => string.Join(", ",
cp
.Where(p => !p.HasDefaultValue && p.ParameterType != typeof(TraitInfo)
&& p.ParameterType.Name != typeof(Func<int>).Name)
.Select(p =>
{
if (p.ParameterType.IsEnum)
localEnums.Add(p.ParameterType);
return p.EmmyLuaString($"{init.Name}").TypeDeclaration;
})))
.Where(s => !s.Contains(", "))
.Distinct());
if (!string.IsNullOrEmpty(parameterString))
{
// OwnerInit is special as it is the only "required" init. All others are optional.
if (init.Name != nameof(OwnerInit))
parameterString += '?';
Console.WriteLine($"---@field {name} {parameterString}");
}
}
usedEnums = localEnums;
}
static void WriteEnums(IEnumerable<Type> enumTypes)
{
foreach (var enumType in enumTypes)
{
Console.WriteLine($"---@enum {enumType.Name}");
Console.WriteLine(enumType.Name + " = {");
foreach (var value in Enum.GetValues(enumType))
Console.WriteLine($" {value} = {Convert.ChangeType(value, typeof(int), NumberFormatInfo.InvariantInfo)},");
Console.WriteLine("}");
Console.WriteLine();
}
}
static void WriteGlobals(IEnumerable<Type> globalTables)
{
foreach (var t in globalTables)
{
var name = Utility.GetCustomAttributes<ScriptGlobalAttribute>(t, true).First().Name;
Console.WriteLine("---Global variable provided by the game scripting engine.");
foreach (var obsolete in t.GetCustomAttributes(false).OfType<ObsoleteAttribute>())
{
Console.WriteLine("---@deprecated");
Console.WriteLine($"--- {obsolete.Message}");
}
Console.WriteLine(name + " = {");
var members = ScriptMemberWrapper.WrappableMembers(t);
foreach (var member in members.OrderBy(m => m.Name))
{
Console.WriteLine();
var body = "";
if (Utility.HasAttribute<DescAttribute>(member))
{
var lines = Utility.GetCustomAttributes<DescAttribute>(member, true).First().Lines;
foreach (var line in lines)
Console.WriteLine($" --- {line}");
}
else
throw new NotSupportedException($"Missing {nameof(DescAttribute)} on {t.Name} {member.Name}");
if (member is PropertyInfo propertyInfo)
{
var attributes = propertyInfo.GetCustomAttributes(false);
foreach (var obsolete in attributes.OfType<ObsoleteAttribute>())
Console.WriteLine($" ---@deprecated {obsolete.Message}");
Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString($"{t.Name} {member.Name}")}");
body = propertyInfo.Name + " = nil;";
}
if (member is MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
var luaParameters = parameters
.Select(parameter => parameter.NameAndEmmyLuaString($"{t.Name} {member.Name}"))
.ToArray();
foreach (var generic in luaParameters.Select(p => p.Generic).Where(g => !string.IsNullOrEmpty(g)).Distinct())
Console.WriteLine($" ---@generic {generic}");
foreach (var nameAndType in luaParameters.Select(p => p.NameAndType))
Console.WriteLine($" ---@param {nameAndType}");
var parameterString = parameters.Select(p => p.Name).JoinWith(", ");
var attributes = methodInfo.GetCustomAttributes(false);
foreach (var obsolete in attributes.OfType<ObsoleteAttribute>())
Console.WriteLine($" ---@deprecated {obsolete.Message}");
if (methodInfo.ReturnType != typeof(void))
Console.WriteLine($" ---@return {methodInfo.ReturnTypeEmmyLuaString($"{t.Name} {member.Name}")}");
body = member.Name + $" = function({parameterString}) end;";
}
Console.WriteLine($" {body}");
}
Console.WriteLine("}");
Console.WriteLine();
}
}
static void WriteScriptProperties(Type type, IEnumerable<Type> implementingTypes)
{
var className = type.Name.ToLowerInvariant();
var tableName = $"__{className}";
Console.WriteLine($"---@class {className}");
var members = implementingTypes.SelectMany(t =>
{
var requiredTraits = ScriptMemberWrapper.RequiredTraitNames(t);
return ScriptMemberWrapper.WrappableMembers(t).Select(memberInfo => (memberInfo, requiredTraits));
});
var duplicateMembers = members
.GroupBy(x => x.memberInfo.Name)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToHashSet();
foreach (var (memberInfo, requiredTraits) in members)
{
// Properties are supposed to be defined as @fields on the class.
// They can be defined as keys inside the tables, but then are treated as readonly by the Lua extension.
if (memberInfo is PropertyInfo propertyInfo && propertyInfo.CanWrite)
{
WriteMemberDescription(memberInfo, requiredTraits, 0);
if (duplicateMembers.Contains(memberInfo.Name))
Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index");
Console.WriteLine($"---@field {propertyInfo.Name} {propertyInfo.PropertyType.EmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")}");
}
}
Console.WriteLine("local " + tableName + " = {");
foreach (var (memberInfo, requiredTraits) in members)
{
// Properties are supposed to be defined as @fields on the class,
// but if they are defined as keys inside the table, they are treated as readonly by the Lua extension.
if (memberInfo is PropertyInfo propertyInfo && !propertyInfo.CanWrite)
{
Console.WriteLine();
WriteMemberDescription(memberInfo, requiredTraits, 1);
if (duplicateMembers.Contains(memberInfo.Name))
Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index");
Console.WriteLine($" ---@type {propertyInfo.PropertyType.EmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")}");
Console.WriteLine($" {propertyInfo.Name} = nil;");
}
// Functions are defined as keys inside the table.
if (memberInfo is MethodInfo methodInfo)
{
Console.WriteLine();
WriteMemberDescription(memberInfo, requiredTraits, 1);
var attributes = methodInfo.GetCustomAttributes(false);
foreach (var obsolete in attributes.OfType<ObsoleteAttribute>())
Console.WriteLine($" ---@deprecated {obsolete.Message}");
var parameters = methodInfo.GetParameters();
var luaParameters = parameters
.Select(parameter => parameter.NameAndEmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}"))
.ToArray();
foreach (var generic in luaParameters.Select(p => p.Generic).Where(g => !string.IsNullOrEmpty(g)).Distinct())
Console.WriteLine($" ---@generic {generic}");
foreach (var nameAndType in luaParameters.Select(p => p.NameAndType))
Console.WriteLine($" ---@param {nameAndType}");
var parameterString = parameters.Select(p => p.Name).JoinWith(", ");
if (methodInfo.ReturnType != typeof(void))
Console.WriteLine($" ---@return {methodInfo.ReturnTypeEmmyLuaString($"{memberInfo.DeclaringType.Name} {memberInfo.Name}")}");
if (duplicateMembers.Contains(methodInfo.Name))
Console.WriteLine(" ---@diagnostic disable-next-line: duplicate-index");
Console.WriteLine($" {methodInfo.Name} = function({parameterString}) end;");
}
}
Console.WriteLine("}");
Console.WriteLine();
static void WriteMemberDescription(MemberInfo memberInfo, string[] requiredTraits, int indentation)
{
var isActivity = Utility.HasAttribute<ScriptActorPropertyActivityAttribute>(memberInfo);
if (Utility.HasAttribute<DescAttribute>(memberInfo))
{
var lines = Utility.GetCustomAttributes<DescAttribute>(memberInfo, true).First().Lines;
foreach (var line in lines)
Console.WriteLine($"{new string(' ', indentation * 4)}--- {line}");
}
else
throw new NotSupportedException($"Missing {nameof(DescAttribute)} on {memberInfo.DeclaringType.Name} {memberInfo.Name}");
if (isActivity)
Console.WriteLine(
$"{new string(' ', indentation * 4)}--- *Queued Activity*");
if (requiredTraits.Length != 0)
Console.WriteLine(
$"{new string(' ', indentation * 4)}--- **Requires {(requiredTraits.Length == 1 ? "Trait" : "Traits")}:** " +
$"{requiredTraits.Select(GetDocumentationUrl).JoinWith(", ")}");
}
}
static string GetDocumentationUrl(string trait)
{
return $"[{trait}](https://docs.openra.net/en/release/traits/#{trait.ToLowerInvariant()})";
}
}
public static class EmmyLuaExts
{
static readonly Dictionary<string, string> LuaTypeNameReplacements = new()
{
// These are weak type mappings, don't add these.
// Instead, use ScriptEmmyTypeOverrideAttribute to provide a specific type.
////{ "Object", "any" },
////{ "LuaValue", "any" },
////{ "LuaTable", "table" },
////{ "LuaFunction", "function" },
{ "Byte", "integer" },
{ "UInt32", "integer" },
{ "Int32", "integer" },
{ "String", "string" },
{ "Boolean", "boolean" },
{ "Double", "number" },
{ "WVec", "wvec" },
{ "CVec", "cvec" },
{ "CPos", "cpos" },
{ "WPos", "wpos" },
{ "WAngle", "wangle" },
{ "WDist", "wdist" },
{ "Color", "color" },
{ "Actor", "actor" },
{ "Player", "player" },
};
public static string EmmyLuaString(this Type type, string notSupportedExceptionContext)
{
if (type.IsArray)
return EmmaLuaStringInner(type.GetElementType(), notSupportedExceptionContext) + "[]";
return EmmaLuaStringInner(type, notSupportedExceptionContext);
static string EmmaLuaStringInner(Type type, string context)
{
if (LuaTypeNameReplacements.TryGetValue(type.Name, out var replacement))
return replacement;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var argument = type.GetGenericArguments()[0].Name;
if (LuaTypeNameReplacements.TryGetValue(argument, out var genericReplacement))
return $"{genericReplacement}?";
}
if (type.IsEnum)
return type.Name;
// This may indicate we are trying to export a type we have not added support for yet.
// Consider adding support for this type.
// This may mean updating WriteManual and adding IScriptBindable to the type.
// Or adding an entry to LuaTypeNameReplacements.
// Or use ScriptEmmyTypeOverride to provide a custom type for a parameter.
// Or consider using ISuppressInitExport if the parameter is coming from an init we don't want to expose to Lua.
// Or, it may need a different approach than the ones listed above.
throw new NotSupportedException(
$"Command lacks support for exposing type to Lua: `{type}` required by `{context}`. " +
$"Consider applying {nameof(ScriptEmmyTypeOverrideAttribute)} or {nameof(ISuppressInitExport)}");
}
}
public static string ReturnTypeEmmyLuaString(this MethodInfo methodInfo, string notSupportedExceptionContext)
{
var overrideAttr = methodInfo.ReturnTypeCustomAttributes
.GetCustomAttributes(typeof(ScriptEmmyTypeOverrideAttribute), false)
.Cast<ScriptEmmyTypeOverrideAttribute>()
.SingleOrDefault();
if (overrideAttr != null)
return overrideAttr.TypeDeclaration;
return methodInfo.ReturnType.EmmyLuaString(notSupportedExceptionContext);
}
public static (string TypeDeclaration, string GenericTypeDeclaration) EmmyLuaString(this ParameterInfo parameterInfo, string notSupportedExceptionContext)
{
var overrideAttr = parameterInfo.GetCustomAttribute<ScriptEmmyTypeOverrideAttribute>();
if (overrideAttr != null)
return (overrideAttr.TypeDeclaration, overrideAttr.GenericTypeDeclaration);
return (parameterInfo.ParameterType.EmmyLuaString(notSupportedExceptionContext), null);
}
public static (string NameAndType, string Generic) NameAndEmmyLuaString(this ParameterInfo parameterInfo, string notSupportedExceptionContext)
{
var optional = parameterInfo.IsOptional ? "?" : "";
var (typeDeclaration, genericTypeDeclaration) = parameterInfo.EmmyLuaString(notSupportedExceptionContext);
return ($"{parameterInfo.Name}{optional} {typeDeclaration}", genericTypeDeclaration);
}
}
}

View File

@@ -0,0 +1,210 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Linq;
using OpenRA;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
sealed class ExtractLuaDocsCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--lua-docs";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[Desc("Generate Lua API documentation in MarkDown format.")]
void IUtilityCommand.Run(Utility utility, string[] args)
{
// HACK: The engine code assumes that Game.modData is set.
Game.ModData = utility.ModData;
var version = utility.ModData.Manifest.Metadata.Version;
if (args.Length > 1)
version = args[1];
Console.WriteLine("This is an automatically generated listing of the Lua map scripting API for version {0} of OpenRA.", version);
Console.WriteLine();
Console.WriteLine("OpenRA allows custom maps and missions to be scripted using Lua 5.1.");
Console.WriteLine("These scripts run in a sandbox that prevents access to unsafe functions (e.g. OS or file access), " +
"and limits the memory and CPU usage of the scripts.");
Console.WriteLine();
Console.WriteLine("You can access this interface by adding the [LuaScript](../traits/#luascript) trait to the world actor in your map rules " +
"(note, you must replace the spaces in the snippet below with a single tab for each level of indentation):");
Console.WriteLine("```\nRules:\n\tWorld:\n\t\tLuaScript:\n\t\t\tScripts: myscript.lua\n```");
Console.WriteLine();
Console.WriteLine("Map scripts can interact with the game engine in three ways:");
Console.WriteLine();
Console.WriteLine("* Global tables provide functions for interacting with the global world state, or performing general helper tasks.");
Console.WriteLine("They exist in the global namespace, and can be called directly using ```<table name>.<function name>```.");
Console.WriteLine("* Individual actors expose a collection of properties and commands that query information or modify their state.");
Console.WriteLine(" * Some commands, marked as <em>queued activity</em>, are asynchronous. Activities are queued on the actor, and will run in " +
"sequence until the queue is empty or the Stop command is called. Actors that are not performing an activity are Idle " +
"(actor.IsIdle will return true). The properties and commands available on each actor depends on the traits that the actor " +
"specifies in its rule definitions.");
Console.WriteLine("* Individual players expose a collection of properties and commands that query information or modify their state.");
Console.WriteLine("The properties and commands available on each actor depends on the traits that the actor specifies in its rule definitions.");
Console.WriteLine();
Console.WriteLine("For a basic guide about map scripts see the [`Map Scripting` wiki page](https://github.com/OpenRA/OpenRA/wiki/Map-scripting).");
Console.WriteLine();
var tables = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>()
.OrderBy(t => t.Name);
Console.WriteLine("## Global Tables");
foreach (var t in tables)
{
var name = Utility.GetCustomAttributes<ScriptGlobalAttribute>(t, true).First().Name;
var members = ScriptMemberWrapper.WrappableMembers(t);
Console.WriteLine();
Console.WriteLine("### " + name);
Console.WriteLine();
Console.WriteLine("| Function | Description |");
Console.WriteLine("|---------:|-------------|");
foreach (var m in members.OrderBy(m => m.Name))
{
var memberString = m.LuaDocString();
var desc = Utility.HasAttribute<DescAttribute>(m) ? Utility.GetCustomAttributes<DescAttribute>(m, true).First().Lines.JoinWith("<br />") : "";
if (Utility.HasAttribute<ObsoleteAttribute>(m))
{
memberString = $"<s>{memberString}</s>";
desc += $"<br />**Deprecated: {Utility.GetCustomAttributes<ObsoleteAttribute>(m, true).First().Message}**";
}
Console.WriteLine($"| **{memberString}** | {desc} |");
}
}
Console.WriteLine();
Console.WriteLine("## Actor Properties / Commands");
var actorCategories = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptActorProperties>().SelectMany(cg =>
{
var catAttr = Utility.GetCustomAttributes<ScriptPropertyGroupAttribute>(cg, false).FirstOrDefault();
var category = catAttr != null ? catAttr.Category : "Unsorted";
var required = ScriptMemberWrapper.RequiredTraitNames(cg);
return ScriptMemberWrapper.WrappableMembers(cg).Select(mi => (category, mi, required));
}).GroupBy(g => g.category).OrderBy(g => g.Key);
foreach (var kv in actorCategories)
{
Console.WriteLine();
Console.WriteLine("### " + kv.Key);
Console.WriteLine();
Console.WriteLine("| Function | Description |");
Console.WriteLine("|---------:|-------------|");
foreach (var property in kv.OrderBy(p => p.mi.Name))
{
var mi = property.mi;
var memberString = mi.LuaDocString();
var required = property.required;
var hasDesc = Utility.HasAttribute<DescAttribute>(mi);
var hasRequires = required.Length > 0;
var isActivity = Utility.HasAttribute<ScriptActorPropertyActivityAttribute>(mi);
var isDeprecated = Utility.HasAttribute<ObsoleteAttribute>(mi);
if (isDeprecated)
Console.Write($"| **<s>{memberString}</s>**");
else
Console.Write($"| **{memberString}**");
if (isActivity)
Console.Write("<br />*Queued Activity*");
Console.Write(" | ");
if (hasDesc)
Console.Write(Utility.GetCustomAttributes<DescAttribute>(mi, false).First().Lines.JoinWith("<br />"));
if (hasDesc && hasRequires)
Console.Write("<br />");
if (hasRequires)
Console.Write($"**Requires {(required.Length == 1 ? "Trait" : "Traits")}:** {required.JoinWith(", ")}");
if (isDeprecated)
Console.Write($"**Deprecated: {Utility.GetCustomAttributes<ObsoleteAttribute>(mi, true).First().Message}**");
Console.WriteLine(" |");
}
}
Console.WriteLine();
Console.WriteLine("## Player Properties / Commands");
var playerCategories = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptPlayerProperties>().SelectMany(cg =>
{
var catAttr = Utility.GetCustomAttributes<ScriptPropertyGroupAttribute>(cg, false).FirstOrDefault();
var category = catAttr != null ? catAttr.Category : "Unsorted";
var required = ScriptMemberWrapper.RequiredTraitNames(cg);
return ScriptMemberWrapper.WrappableMembers(cg).Select(mi => (category, mi, required));
}).GroupBy(g => g.category).OrderBy(g => g.Key);
foreach (var kv in playerCategories)
{
Console.WriteLine();
Console.WriteLine("### " + kv.Key);
Console.WriteLine();
Console.WriteLine("| Function | Description |");
Console.WriteLine("|---------:|-------------|");
foreach (var property in kv.OrderBy(p => p.mi.Name))
{
var mi = property.mi;
var memberString = mi.LuaDocString();
var required = property.required;
var hasDesc = Utility.HasAttribute<DescAttribute>(mi);
var hasRequires = required.Length > 0;
var isActivity = Utility.HasAttribute<ScriptActorPropertyActivityAttribute>(mi);
var isDeprecated = Utility.HasAttribute<ObsoleteAttribute>(mi);
if (isDeprecated)
Console.Write($"| **<s>{memberString}</s>**");
else
Console.Write($"| **{memberString}**");
if (isActivity)
Console.Write("<br />*Queued Activity*");
Console.Write(" | ");
if (hasDesc)
Console.Write(Utility.GetCustomAttributes<DescAttribute>(mi, false).First().Lines.JoinWith("<br />"));
if (hasDesc && hasRequires)
Console.Write("<br />");
if (hasRequires)
Console.Write($"**Requires {(required.Length == 1 ? "Trait" : "Traits")}:** {required.JoinWith(", ")}");
if (isDeprecated)
Console.Write($"**Deprecated: {Utility.GetCustomAttributes<ObsoleteAttribute>(mi, true).First().Message}**");
Console.WriteLine(" |");
}
Console.WriteLine();
}
}
}
}

View File

@@ -0,0 +1,100 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Linq;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
sealed class ExtractSettingsDocsCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--settings-docs";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[Desc("[VERSION]", "Generate settings documentation in markdown format.")]
void IUtilityCommand.Run(Utility utility, string[] args)
{
Game.ModData = utility.ModData;
var version = utility.ModData.Manifest.Metadata.Version;
if (args.Length > 1)
version = args[1];
Console.WriteLine(
"This documentation displays annotated settings with default values and description. " +
"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();
Console.WriteLine("All settings can be changed by starting the game via a command-line parameter like `Game.Mod=ra`.");
Console.WriteLine();
Console.WriteLine("## Location");
Console.WriteLine("* Windows: `%APPDATA%\\OpenRA\\settings.yaml`");
Console.WriteLine("* Mac OS X: `~/Library/Application Support/OpenRA/settings.yaml`");
Console.WriteLine("* Linux `~/.config/openra/settings.yaml`");
Console.WriteLine();
Console.WriteLine(
"Older releases (before playtest-20190825) used different locations, " +
"which newer versions may continue to use in some circumstances:");
Console.WriteLine("* Windows: `%USERPROFILE%\\Documents\\OpenRA\\settings.yaml`");
Console.WriteLine("* Linux `~/.openra/settings.yaml`");
Console.WriteLine();
Console.WriteLine(
"If you create the folder `Support` relative to the OpenRA main directory, everything " +
"including settings gets stored there to aid portable installations.");
Console.WriteLine();
var sections = new Settings(null, new Arguments()).Sections;
sections.Add("Launch", new LaunchArguments(new Arguments(Array.Empty<string>())));
foreach (var section in sections.OrderBy(s => s.Key))
{
var fields = Utility.GetFields(section.Value.GetType());
if (fields.Any(field => Utility.GetCustomAttributes<DescAttribute>(field, false).Length > 0))
{
Console.WriteLine($"## {section.Key}");
if (section.Key == "Launch")
{
Console.WriteLine("These are runtime parameters which can't be defined in `settings.yaml`.");
Console.WriteLine();
}
}
foreach (var field in fields)
{
if (!Utility.HasAttribute<DescAttribute>(field))
continue;
Console.WriteLine($"### {field.Name}");
var lines = Utility.GetCustomAttributes<DescAttribute>(field, false).SelectMany(d => d.Lines);
foreach (var line in lines)
{
Console.WriteLine(line);
Console.WriteLine();
}
var value = field.GetValue(section.Value);
if (value != null && !value.ToString().StartsWith("System.", StringComparison.Ordinal))
{
Console.WriteLine($"**Default Value:** {value}");
Console.WriteLine();
Console.WriteLine("```miniyaml");
Console.WriteLine($"{section.Key}: ");
Console.WriteLine($"\t{field.Name}: {value}");
Console.WriteLine("```");
}
}
}
}
}
}

View File

@@ -0,0 +1,117 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Primitives;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
sealed class ExtractSpriteSequenceDocsCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--sprite-sequence-docs";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[Desc("[VERSION]", "Generate sprite sequence documentation in JSON format.")]
void IUtilityCommand.Run(Utility utility, string[] args)
{
// HACK: The engine code assumes that Game.modData is set.
Game.ModData = utility.ModData;
var version = utility.ModData.Manifest.Metadata.Version;
if (args.Length > 1)
version = args[1];
var objectCreator = utility.ModData.ObjectCreator;
var spriteSequenceTypes = objectCreator.GetTypesImplementing<ISpriteSequence>().OrderBy(t => t.Namespace).ThenBy(t => t.Name);
var json = GenerateJson(version, spriteSequenceTypes);
Console.WriteLine(json);
}
static string GenerateJson(string version, IEnumerable<Type> sequenceTypes)
{
var relatedEnumTypes = new HashSet<Type>();
var pdbReaderCache = Utilities.CreatePdbReaderCache();
var sequenceTypesInfo = sequenceTypes
.Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
.Select(type => new
{
type.Namespace,
type.Name,
Description = string.Join(" ", Utility.GetCustomAttributes<DescAttribute>(type, false).SelectMany(d => d.Lines)),
Filename = Utilities.GetSourceFilenameFromPdb(type, pdbReaderCache),
InheritedTypes = type.BaseTypes()
.Select(y => y.Name)
.Where(y => y != type.Name && y != "Object"),
Properties = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static)
.Where(fi => fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(SpriteSequenceField<>))
.Select(fi =>
{
var description = string.Join(" ", Utility.GetCustomAttributes<DescAttribute>(fi, false)
.SelectMany(d => d.Lines));
var valueType = fi.FieldType.GetGenericArguments()[0];
var key = (string)fi.FieldType
.GetField(nameof(SpriteSequenceField<bool>.Key))?
.GetValue(fi.GetValue(null));
var defaultValueField = fi.FieldType.GetField(nameof(SpriteSequenceField<bool>.DefaultValue));
var defaultValue = defaultValueField?.GetValue(fi.GetValue(null));
if (defaultValueField != null && defaultValueField.FieldType.IsEnum)
relatedEnumTypes.Add(defaultValueField.FieldType);
return new
{
PropertyName = key,
DefaultValue = defaultValue?.ToString(),
InternalType = Util.InternalTypeName(valueType),
UserFriendlyType = Util.FriendlyTypeName(valueType),
Description = description
};
})
});
var relatedEnums = relatedEnumTypes.OrderBy(t => t.Name).Select(type => new
{
type.Namespace,
type.Name,
Values = Enum.GetNames(type).Select(x => new
{
Key = Convert.ToInt32(Enum.Parse(type, x), NumberFormatInfo.InvariantInfo),
Value = x
})
});
var result = new
{
Version = version,
SpriteSequenceTypes = sequenceTypesInfo,
RelatedEnums = relatedEnums
};
return JsonConvert.SerializeObject(result);
}
}
}

View File

@@ -0,0 +1,132 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
sealed class ExtractTraitDocsCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--docs";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[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.
Game.ModData = utility.ModData;
var version = utility.ModData.Manifest.Metadata.Version;
if (args.Length > 1)
version = args[1];
var objectCreator = utility.ModData.ObjectCreator;
var traitInfos = objectCreator.GetTypesImplementing<TraitInfo>().OrderBy(t => t.Namespace).ThenBy(t => t.Name);
var json = GenerateJson(version, traitInfos, objectCreator);
Console.WriteLine(json);
}
static string GenerateJson(string version, IEnumerable<Type> traitTypes, ObjectCreator objectCreator)
{
var relatedEnumTypes = new HashSet<Type>();
var pdbReaderCache = Utilities.CreatePdbReaderCache();
var traitTypesInfo = traitTypes
.Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
.Select(type => new
{
type.Namespace,
Name = type.Name.EndsWith("Info", StringComparison.Ordinal) ? type.Name[..^4] : type.Name,
Description = string.Join(" ", Utility.GetCustomAttributes<DescAttribute>(type, false).SelectMany(d => d.Lines)),
Filename = Utilities.GetSourceFilenameFromPdb(type, pdbReaderCache),
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 =>
{
if (fi.Field.FieldType.IsEnum)
relatedEnumTypes.Add(fi.Field.FieldType);
return 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(" ", Utility.GetCustomAttributes<DescAttribute>(fi.Field, 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", StringComparison.Ordinal) ? name[..^9] : name;
return new
{
Name = name,
Parameters = a.Constructor.GetParameters()
.Select(pi => new
{
pi.Name,
Value = Util.GetAttributeParameterValue(a.ConstructorArguments[pi.Position])
})
};
})
};
})
});
var relatedEnums = relatedEnumTypes.OrderBy(t => t.Name).Select(type => new
{
type.Namespace,
type.Name,
Values = Enum.GetNames(type).Select(x => new
{
Key = Convert.ToInt32(Enum.Parse(type, x), NumberFormatInfo.InvariantInfo),
Value = x
})
});
var result = new
{
Version = version,
TraitInfos = traitTypesInfo,
RelatedEnums = relatedEnums
};
return JsonConvert.SerializeObject(result);
}
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);
}
}
}

View File

@@ -0,0 +1,126 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using OpenRA.GameRules;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
sealed class ExtractWeaponDocsCommand : IUtilityCommand
{
string IUtilityCommand.Name => "--weapon-docs";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[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.
Game.ModData = utility.ModData;
var version = utility.ModData.Manifest.Metadata.Version;
if (args.Length > 1)
version = args[1];
var objectCreator = utility.ModData.ObjectCreator;
var weaponInfo = new[] { typeof(WeaponInfo) };
var warheads = objectCreator.GetTypesImplementing<IWarhead>().OrderBy(t => t.Namespace).ThenBy(t => t.Name);
var projectiles = objectCreator.GetTypesImplementing<IProjectileInfo>().OrderBy(t => t.Namespace).ThenBy(t => t.Name);
var weaponTypes = weaponInfo.Concat(projectiles).Concat(warheads);
var json = GenerateJson(version, weaponTypes, objectCreator);
Console.WriteLine(json);
}
static string GenerateJson(string version, IEnumerable<Type> weaponTypes, ObjectCreator objectCreator)
{
var relatedEnumTypes = new HashSet<Type>();
var pdbReaderCache = Utilities.CreatePdbReaderCache();
var weaponTypesInfo = weaponTypes
.Where(x => !x.ContainsGenericParameters && !x.IsAbstract)
.Select(type => new
{
type.Namespace,
Name = type.Name.EndsWith("Info", StringComparison.Ordinal) ? type.Name[..^4] : type.Name,
Description = string.Join(" ", Utility.GetCustomAttributes<DescAttribute>(type, false).SelectMany(d => d.Lines)),
Filename = Utilities.GetSourceFilenameFromPdb(type, pdbReaderCache),
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 =>
{
if (fi.Field.FieldType.IsEnum)
relatedEnumTypes.Add(fi.Field.FieldType);
return 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(" ", Utility.GetCustomAttributes<DescAttribute>(fi.Field, 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", StringComparison.Ordinal) ? name[..^9] : name;
return new
{
Name = name,
Parameters = a.Constructor.GetParameters()
.Select(pi => new
{
pi.Name,
Value = Util.GetAttributeParameterValue(a.ConstructorArguments[pi.Position])
})
};
})
};
})
});
var relatedEnums = relatedEnumTypes.OrderBy(t => t.Name).Select(type => new
{
type.Namespace,
type.Name,
Values = Enum.GetNames(type).Select(x => new
{
Key = Convert.ToInt32(Enum.Parse(type, x), NumberFormatInfo.InvariantInfo),
Value = x
})
});
var result = new
{
Version = version,
WeaponTypes = weaponTypesInfo,
RelatedEnums = relatedEnums
};
return JsonConvert.SerializeObject(result);
}
}
}

View File

@@ -0,0 +1,146 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* 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.
*/
#endregion
using System;
using System.Linq;
using System.Reflection;
using OpenRA.Scripting;
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
{
// See https://studio.zerobrane.com/doc-api-auto-complete for reference
sealed class ExtractZeroBraneStudioLuaAPI : IUtilityCommand
{
string IUtilityCommand.Name => "--zbstudio-lua-api";
bool IUtilityCommand.ValidateArguments(string[] args)
{
return true;
}
[Desc("Generate ZeroBrane Studio Lua API and auto-complete descriptions.")]
void IUtilityCommand.Run(Utility utility, string[] args)
{
Console.WriteLine("local interpreter = {");
Console.WriteLine(" name = \"OpenRA\",");
Console.WriteLine(" description = \"OpenRA map scripting Lua API\",");
Console.WriteLine(" api = {\"baselib\", \"openra\"},");
Console.WriteLine(" hasdebugger = false,");
Console.WriteLine(" skipcompile = true,");
Console.WriteLine("}");
Console.WriteLine();
Console.WriteLine("-- This is an automatically generated Lua API definition generated for {0} of OpenRA.", utility.ModData.Manifest.Metadata.Version);
Console.WriteLine("-- https://github.com/OpenRA/OpenRA/wiki/Utility was used with the --zbstudio-lua-api parameter.");
Console.WriteLine("-- See https://github.com/OpenRA/OpenRA/wiki/Lua-API for human readable documentation.");
Console.WriteLine();
Console.WriteLine("local api = {");
var tables = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>().OrderBy(t => t.Name);
foreach (var t in tables)
{
var name = Utility.GetCustomAttributes<ScriptGlobalAttribute>(t, true).First().Name;
Console.WriteLine(" " + name + " = {");
Console.WriteLine(" type = \"class\",");
Console.WriteLine(" childs = {");
var members = ScriptMemberWrapper.WrappableMembers(t);
foreach (var member in members.OrderBy(m => m.Name))
{
Console.WriteLine(" " + member.Name + " = {");
var methodInfo = member as MethodInfo;
if (methodInfo != null)
Console.WriteLine(" type = \"function\",");
var propertyInfo = member as PropertyInfo;
if (propertyInfo != null)
Console.WriteLine(" type = \"value\",");
if (Utility.HasAttribute<DescAttribute>(member))
{
var desc = Utility.GetCustomAttributes<DescAttribute>(member, true).First().Lines.JoinWith("\n");
Console.WriteLine(" description = [[{0}]],", desc);
}
if (methodInfo != null)
{
var parameters = methodInfo.GetParameters().Select(pi => pi.LuaDocString());
Console.WriteLine(" args = \"({0})\",", parameters.JoinWith(", "));
var returnType = methodInfo.ReturnType.LuaDocString();
Console.WriteLine(" returns = \"({0})\",", returnType);
}
Console.WriteLine(" },");
}
Console.WriteLine(" }");
Console.WriteLine(" },");
}
var actorProperties = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptActorProperties>()
.SelectMany(ScriptMemberWrapper.WrappableMembers);
var scriptProperties = utility.ModData.ObjectCreator.GetTypesImplementing<ScriptPlayerProperties>()
.SelectMany(ScriptMemberWrapper.WrappableMembers);
var properties = actorProperties.Concat(scriptProperties);
foreach (var property in properties.OrderBy(m => m.Name))
{
Console.WriteLine(" " + property.Name + " = {");
var methodInfo = property as MethodInfo;
if (methodInfo != null)
Console.WriteLine(" type = \"function\",");
var propertyInfo = property as PropertyInfo;
if (propertyInfo != null)
Console.WriteLine(" type = \"value\",");
if (Utility.HasAttribute<DescAttribute>(property))
{
var desc = Utility.GetCustomAttributes<DescAttribute>(property, true).First().Lines.JoinWith("\n");
Console.WriteLine(" description = [[{0}]],", desc);
}
if (methodInfo != null)
{
var parameters = methodInfo.GetParameters().Select(pi => pi.LuaDocString());
Console.WriteLine(" args = \"({0})\",", parameters.JoinWith(", "));
var returnType = methodInfo.ReturnType.LuaDocString();
Console.WriteLine(" returns = \"({0})\",", returnType);
}
Console.WriteLine(" },");
}
Console.WriteLine("}");
Console.WriteLine();
Console.WriteLine("return {");
Console.WriteLine(" name = \"OpenRA\",");
Console.WriteLine(" description = \"Adds API description for auto-complete and tooltip support for OpenRA.\",");
Console.WriteLine(" author = \"Matthias Mailänder\",");
Console.WriteLine($" version = \"{utility.ModData.Manifest.Metadata.Version.Split('-').LastOrDefault()}\",");
Console.WriteLine();
Console.WriteLine(" onRegister = function(self)");
Console.WriteLine(" ide:AddAPI(\"lua\", \"openra\", api)");
Console.WriteLine(" ide:AddInterpreter(\"openra\", interpreter)");
Console.WriteLine(" end,");
Console.WriteLine();
Console.WriteLine(" onUnRegister = function(self)");
Console.WriteLine(" ide:RemoveAPI(\"lua\", \"openra\")");
Console.WriteLine(" ide:RemoveInterpreter(\"openra\")");
Console.WriteLine(" end,");
Console.WriteLine("}");
}
}
}