#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() .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("---@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 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.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 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}"); } 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()}"); 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()) 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 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()) 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(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}"); } 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() { { "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()}"; } } }