Files
OpenRA/OpenRA.Mods.Common/UtilityCommands/Documentation/ExtractEmmyLuaAPI.cs
RoosterDragon 9e548fd0f0 Freeze non-mutable readonly static dictionaries.
Since these will live for the lifetime of the process, it is worth freezing them on creation to improve lookup performance. If the dictionaries were iterated, change to an immutable array instead to preserve the order.
2025-12-12 09:51:27 +02:00

479 lines
18 KiB
C#

#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.Frozen;
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 FrozenDictionary<string, string> LuaTypeNameReplacements = new Dictionary<string, string>
{
// 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" },
}.ToFrozenDictionary();
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);
}
}
}