Files
OpenRA/OpenRA.Mods.Common/UtilityCommands/ExtractEmmyLuaAPI.cs
2024-07-29 21:56:36 +02:00

420 lines
14 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.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using OpenRA.Scripting;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.UtilityCommands
{
// 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/latest/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("---@class cpos");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@operator add(cvec): cpos");
Console.WriteLine("---@operator sub(cvec): cpos");
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();
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();
Console.WriteLine("---@class wvec");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@field Z integer");
Console.WriteLine("---@operator add(wvec): wvec");
Console.WriteLine("---@operator sub(wvec): wvec");
Console.WriteLine();
Console.WriteLine("---@class cvec");
Console.WriteLine("---@field X integer");
Console.WriteLine("---@field Y integer");
Console.WriteLine("---@operator add(cvec): cvec");
Console.WriteLine("---@operator sub(cvec): 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.ParameterType.EmmyLuaString();
})))
.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}");
}
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()}");
body = propertyInfo.Name + " = nil;";
}
if (member is MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
foreach (var parameter in parameters)
Console.WriteLine($" ---@param {parameter.EmmyLuaString()}");
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}");
var returnType = methodInfo.ReturnType.EmmyLuaString();
if (returnType != "Void")
Console.WriteLine($" ---@return {returnType}");
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()}");
}
}
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()}");
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();
foreach (var parameter in parameters)
Console.WriteLine($" ---@param {parameter.EmmyLuaString()}");
var parameterString = parameters.Select(p => p.Name).JoinWith(", ");
var returnType = methodInfo.ReturnType.EmmyLuaString();
if (returnType != "Void")
Console.WriteLine($" ---@return {returnType}");
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}");
}
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()
{
{ "UInt32", "integer" },
{ "Int32", "integer" },
{ "String", "string" },
{ "String[]", "string[]" },
{ "Boolean", "boolean" },
{ "Double", "number" },
{ "Object", "any" },
{ "LuaTable", "table" },
{ "LuaValue", "any" },
{ "LuaValue[]", "table" },
{ "LuaFunction", "function" },
{ "WVec", "wvec" },
{ "CVec", "cvec" },
{ "CPos", "cpos" },
{ "CPos[]", "cpos[]" },
{ "WPos", "wpos" },
{ "WAngle", "wangle" },
{ "WAngle[]", "wangle[]" },
{ "WDist", "wdist" },
{ "Color", "color" },
{ "Actor", "actor" },
{ "Actor[]", "actor[]" },
{ "Player", "player" },
{ "Player[]", "player[]" },
};
public static string EmmyLuaString(this Type type)
{
if (!LuaTypeNameReplacements.TryGetValue(type.Name, out var replacement))
replacement = type.Name;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var argument = type.GetGenericArguments().Select(p => p.Name).First();
if (LuaTypeNameReplacements.TryGetValue(argument, out var genericReplacement))
replacement = $"{genericReplacement}?";
else
replacement = $"{type.GetGenericArguments().Select(p => p.Name).First()}?";
}
return replacement;
}
public static string EmmyLuaString(this ParameterInfo parameterInfo)
{
var optional = parameterInfo.IsOptional ? "?" : "";
var parameterType = parameterInfo.ParameterType.EmmyLuaString();
// A hack for ActorGlobal.Create().
if (parameterInfo.Name == "initTable")
parameterType = "initTable";
return $"{parameterInfo.Name}{optional} {parameterType}";
}
public static string EmmyLuaString(this PropertyInfo propertyInfo)
{
return $"{propertyInfo.Name} {propertyInfo.PropertyType.EmmyLuaString()}";
}
}
}