#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 { // 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() .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().OrderBy(t => t.Name); WriteGlobals(globalTables); var actorProperties = utility.ModData.ObjectCreator.GetTypesImplementing(); WriteScriptProperties(typeof(Actor), actorProperties); var playerProperties = utility.ModData.ObjectCreator.GetTypesImplementing(); 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 actorInits, out IEnumerable usedEnums) { Console.WriteLine("---A list of ActorInit implementations that can be used by Lua scripts."); Console.WriteLine("---@class initTable"); var localEnums = new HashSet(); 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).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 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 globalTables) { foreach (var t in globalTables) { var name = Utility.GetCustomAttributes(t, true).First().Name; Console.WriteLine("---Global variable provided by the game scripting engine."); foreach (var obsolete in t.GetCustomAttributes(false).OfType()) { 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(member)) { var lines = Utility.GetCustomAttributes(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()) 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()) 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 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()) 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(memberInfo); if (Utility.HasAttribute(memberInfo)) { var lines = Utility.GetCustomAttributes(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 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() .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(); 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); } } }