Skip to content

Commit

Permalink
add a reflection-based documentation generator and partial documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
PrincessRTFM committed Oct 17, 2023
1 parent 136af1e commit 1c84f76
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 11 deletions.
1 change: 1 addition & 0 deletions WoLua/Constants/LogTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace PrincessRTFM.WoLua.Constants;

public static class LogTag {
public const string
GenerateDocs = "LUADOC",
MethodTiming = "TIMER",
DeprecatedApiMember = "DEPRECATION",
CallbackRegistration = "CALLBACK",
Expand Down
2 changes: 2 additions & 0 deletions WoLua/Lua/Api/ApiBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ namespace PrincessRTFM.WoLua.Lua.Api;
using MoonSharp.Interpreter.Serialization.Json;

using PrincessRTFM.WoLua.Constants;
using PrincessRTFM.WoLua.Lua.Docs;
using PrincessRTFM.WoLua.Ui.Chat;

public abstract class ApiBase: IDisposable {
private const BindingFlags allInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

[LuaDoc("If this is true, the API has been disposed of and **MUST NOT** be used. This should never happen in a running script.")]
public bool Disposed { get; protected set; } = false;

protected internal virtual void PreInit() { }
Expand Down
11 changes: 9 additions & 2 deletions WoLua/Lua/Api/GameApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace PrincessRTFM.WoLua.Lua.Api;
using PrincessRTFM.WoLua.Constants;
using PrincessRTFM.WoLua.Game;
using PrincessRTFM.WoLua.Lua.Api.Game;
using PrincessRTFM.WoLua.Lua.Docs;
using PrincessRTFM.WoLua.Ui.Chat;

// This API is for everything pertaining to the actual game, including holding more specific APIs.
Expand All @@ -30,7 +31,9 @@ internal GameApi(ScriptContainer source) : base(source) { }
#endregion

#region Chat
public void PrintMessage(params DynValue[] messages) {

[LuaDoc("Prints a message into the user's local chat log using the normal default colour")]
public void PrintMessage([AsLuaType(LuaType.Any), LuaDoc("Multiple values will be concatenated with a single space")] params DynValue[] messages) {
if (this.Disposed)
return;
if (messages.Length == 0)
Expand All @@ -44,7 +47,8 @@ public void PrintMessage(params DynValue[] messages) {
Service.Plugin.Print(message, null, this.Owner.PrettyName);
}

public void PrintError(params DynValue[] messages) {
[LuaDoc("Prints a message into the user's local chat log in red")]
public void PrintError([AsLuaType(LuaType.Any), LuaDoc("Multiple values will be concatenated with a single space")] params DynValue[] messages) {
if (this.Disposed)
return;

Expand All @@ -56,6 +60,7 @@ public void PrintError(params DynValue[] messages) {
Service.Plugin.Print(message, Foreground.Error, this.Owner.PrettyName);
}

[LuaDoc("Sends text to the game as if the user had typed it into their chat box themselves")]
public void SendChat(string chatline) {
if (this.Disposed)
return;
Expand All @@ -68,6 +73,8 @@ public void SendChat(string chatline) {
}
#endregion

[LuaDoc("Plays one of the sixteen <se.##> sound effects without printing anything to the user's chat")]
[return: LuaDoc("true if the provided sound effect ID was a valid sound, false if it wasn't, or nil if there was an internal error")]
public bool? PlaySoundEffect(int id) {
if (this.Disposed)
return null;
Expand Down
54 changes: 45 additions & 9 deletions WoLua/Lua/Api/ScriptApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace PrincessRTFM.WoLua.Lua.Api;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;

using Dalamud.Interface.Internal.Notifications;

Expand All @@ -15,6 +16,7 @@ namespace PrincessRTFM.WoLua.Lua.Api;
using PrincessRTFM.WoLua.Constants;
using PrincessRTFM.WoLua.Lua.Actions;
using PrincessRTFM.WoLua.Lua.Api.Script;
using PrincessRTFM.WoLua.Lua.Docs;

// This API is for all for everything that doesn't relate to the actual game itself.
// It also contains script-specific and per-script functionality, like persistent storage.
Expand Down Expand Up @@ -56,8 +58,11 @@ protected internal override void PreInit() {

#region Storage

[LuaDoc("The script's persistent storage table")]
public Table Storage { get; protected set; }

[LuaDoc("Completely clears the persistent storage table AND deletes the saved file on disk")]
[return: LuaDoc("false if the disk file couldn't be deleted, otherwise true (whether or not it existed before)")]
public bool DeleteStorage() {
if (this.Disposed)
return false;
Expand All @@ -75,6 +80,8 @@ public bool DeleteStorage() {
}
}

[LuaDoc("Save the current contents of the persistent storage to disk")]
[return: LuaDoc("true if the disk file was written successfully, false if there was an error")]
public bool SaveStorage() {
if (this.Disposed)
return false;
Expand All @@ -91,6 +98,8 @@ public bool SaveStorage() {
}
}

[LuaDoc("Try to load the saved persistent storage from disk, REPLACING the existing contents")]
[return: LuaDoc("true on success, false if no disk file was found, nil if an error occurred")]
public bool? ReloadStorage() {
if (this.Disposed)
return false;
Expand Down Expand Up @@ -119,7 +128,8 @@ public bool SaveStorage() {
}
}

public void SetStorage(Table replacement) {
[LuaDoc("Replace the script's existing persistent storage table with a new one")]
public void SetStorage([LuaDoc("The table to COPY as the new persistent storage")] Table replacement) {
if (this.Disposed)
return;

Expand All @@ -136,35 +146,48 @@ public void SetStorage(Table replacement) {

#region Common strings

public static string PluginCommand => Plugin.Command;
[LuaDoc(Plugin.Name + "'s core chat command")]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Documentation generation only reflects instance members")]
public string PluginCommand => Plugin.Command;

[LuaDoc("The INTERNAL name of this script, used to call it. May or may not be the same as the title.")]
public string Name => this.Owner.InternalName;

[LuaDoc("The PRETTY name of this script, for display purposes **ONLY**. DO NOT attempt to call the script with this name, as it may not be recognised.")]
public string Title => this.Owner.PrettyName;

public string CallSelfCommand => Service.Configuration.RegisterDirectCommands ? $"/{Service.Configuration.DirectInvocationCommandPrefix}{this.Name}" : PluginCommand + " call " + this.Name;
[LuaDoc("The command to call this script, respecting the \"direct script commands\" setting")]
public string CallSelfCommand => Service.Configuration.RegisterDirectCommands ? $"/{Service.Configuration.DirectInvocationCommandPrefix}{this.Name}" : this.PluginCommand + " call " + this.Name;

#endregion

#region Action queueing

[LuaDoc("How many actions are currently in this script's action queue")]
public int QueueSize => this.Owner.ActionQueue.Count;

[LuaDoc("Clears this script's action queue entirely, but does NOT interrupt existing functions being run")]
public void ClearQueue() => this.Owner.ActionQueue.clear();

[LuaDoc("Queues a pause of the given number of milliseconds before following actions are processed")]
public void QueueDelay(uint milliseconds) => this.Owner.ActionQueue.add(new PauseAction(milliseconds));

public void QueueAction(Closure func, params DynValue[] arguments)
=> this.Owner.ActionQueue.add(new CallbackAction(DynValue.NewClosure(func), arguments));
public void QueueAction(CallbackFunction func, params DynValue[] arguments)
=> this.Owner.ActionQueue.add(new CallbackAction(DynValue.NewCallback(func), arguments));
[LuaDoc("Queues the execution of a function with the provided arguments (if any)")]
public void QueueAction(Closure callback, [AsLuaType(LuaType.Any), Optional] params DynValue[] arguments)
=> this.Owner.ActionQueue.add(new CallbackAction(DynValue.NewClosure(callback), arguments));

[SkipDoc("It's a type-only overload of the above")]
public void QueueAction(CallbackFunction callback, params DynValue[] arguments)
=> this.Owner.ActionQueue.add(new CallbackAction(DynValue.NewCallback(callback), arguments));

#endregion

#region Clipboard

[AllowNull]
public static string Clipboard {
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Documentation generation only reflects instance members")]
[LuaDoc("The user's current system clipboard contents")]
public string Clipboard {
get => ImGui.GetClipboardText() ?? string.Empty;
set => ImGui.SetClipboardText(value ?? string.Empty);
}
Expand All @@ -173,6 +196,8 @@ public static string Clipboard {

#region JSON

[LuaDoc("Parses a string containing a JSON object/array and turns it into a lua table")]
[return: LuaDoc("The resulting lua table, or nil if the JSON wasn't a valid object/array")]
public Table? ParseJson(string jsonObject) {
this.Log(jsonObject, LogTag.JsonParse);
try {
Expand All @@ -186,20 +211,25 @@ public static string Clipboard {
}
}

[LuaDoc("Serialises a lua table into a JSON object, or an array if all keys are numeric")]
[return: LuaDoc("The resulting JSON object/array as a string")]
public string SerialiseJson(Table content) {
this.Owner.cleanTable(content);
string json = JsonTableConverter.TableToJson(content);
this.Log(json, LogTag.JsonDump);
return json;
}

[LuaDoc("Serialises a lua table into a JSON object, or an array if all keys are numeric")]
[return: LuaDoc("The resulting JSON object/array as a string")]
public string SerializeJson(Table content) => this.SerialiseJson(content); // american spelling

#endregion

#region Non-game Dalamud access

[Obsolete("Use the DalamudApi version instead")]
[return: LuaDoc("Whether or not a plugin with the given INTERNAL name is installed and loaded")]
public bool HasPlugin(string pluginName) {
this.DeprecationWarning("Game.Dalamud.HasPlugin(string)");
return this.Owner.GameApi.Dalamud.HasPlugin(pluginName);
Expand All @@ -214,13 +244,19 @@ private void showNotification(string content, NotificationType type, double dura
this.Log($"Displaying notification (type {type}) of {content.Length:N} chars for {duration:N}ms ({initialDuration:D}ms x {durationModifier:F2})", LogTag.DebugMessage);
Service.Interface.UiBuilder.AddNotification(content, $"{Plugin.Name}: {this.Title}", type, duration);
}

[LuaDoc("Displays a Dalamud popup debug notification in the lower right corner **IF** the script has debug mode enabled")]
public void NotifyDebug(string content) {
if (this.Debug.Enabled)
this.showNotification(content, NotificationType.None);
}
[LuaDoc("Displays a Dalamud popup informational notification in the lower right corner")]
public void NotifyInfo(string content) => this.showNotification(content, NotificationType.Info);
[LuaDoc("Displays a Dalamud popup success notification in the lower right corner")]
public void NotifySuccess(string content) => this.showNotification(content, NotificationType.Success);
[LuaDoc("Displays a Dalamud popup warning notification in the lower right corner")]
public void NotifyWarning(string content) => this.showNotification(content, NotificationType.Warning, 1.1);
[LuaDoc("Displays a Dalamud popup error notification in the lower right corner")]
public void NotifyError(string content) => this.showNotification(content, NotificationType.Error, 1.2);

#endregion
Expand All @@ -233,7 +269,7 @@ public void NotifyDebug(string content) {
[MoonSharpHidden]
[MoonSharpUserDataMetamethod(Metamethod.FunctionCall)]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Lua __call invocations pass target object as first parameter")]
public void RegisterCallbackFunction(DynValue self, DynValue func) {
public void RegisterCallbackFunction(DynValue self, [AsLuaType(LuaType.Function)] DynValue func) {
if (this.Owner.SetCallback(func)) {
this.Log($"Registered on-execute function [{ToUsefulString(func, true)}]", LogTag.CallbackRegistration);
}
Expand Down
16 changes: 16 additions & 0 deletions WoLua/Lua/Docs/AsLuaTypeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace PrincessRTFM.WoLua.Lua.Docs;

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
internal class AsLuaTypeAttribute: Attribute {
public string LuaName { get; }

public AsLuaTypeAttribute(string luaType) {
this.LuaName = luaType;
}
public AsLuaTypeAttribute(LuaType luaType): this(luaType.LuaName()) { }
}
13 changes: 13 additions & 0 deletions WoLua/Lua/Docs/LuaDocAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace PrincessRTFM.WoLua.Lua.Docs;

using System;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.ReturnValue)]
internal class LuaDocAttribute: Attribute {
public string Description { get; private init; }
public string[] Lines => this.Description.Split('\n', StringSplitOptions.TrimEntries);

public LuaDocAttribute(string help) {
this.Description = help;
}
}
87 changes: 87 additions & 0 deletions WoLua/Lua/Docs/LuaType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
namespace PrincessRTFM.WoLua.Lua.Docs;

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

using MoonSharp.Interpreter;

using PrincessRTFM.WoLua.Constants;

[SuppressMessage("Naming", "CA1720:Identifier contains type name", Justification = "the names are defined externally")]
[Flags]
public enum LuaType: ushort {
Any = 0,
String = 1 << 0,
Boolean = 1 << 1,
Integer = 1 << 2,
Number = 1 << 3,
Table = 1 << 4,
Function = 1 << 5,
Userdata = 1 << 6,
Nil = 1 << 7,
}

public static class LuaTypeExtensions {
public static string LuaName(this LuaType type) {
return type is LuaType.Any
? type.ToString().ToLower()
: string.Join("|", Enum.GetValues<LuaType>()
.Where(t => t is not LuaType.Any && type.HasFlag(t))
.OrderBy(t => (ushort)t)
.Select(t => t.ToString().ToLower())
);
}
public static string LuaName(this LuaType type, string userdataName) {
return type is LuaType.Any
? type.ToString().ToLower()
: string.Join("|", Enum.GetValues<LuaType>()
.Where(t => t is not LuaType.Any && type.HasFlag(t))
.OrderBy(t => (ushort)t)
.Select(t => t is LuaType.Userdata ? userdataName : t.ToString().ToLower())
);
}

public static LuaType FromType(Type originalType) {
LuaType lua = LuaType.Any;
Type realType = originalType;
if (originalType.IsGenericType && realType.GetGenericTypeDefinition() == typeof(Nullable<>)) {
realType = originalType.GenericTypeArguments[0];
lua = LuaType.Nil;
}

// apparently you can't do a switch using typeof() cases, so here we fuckin' are
if (realType == typeof(DynValue))
lua = LuaType.String | LuaType.Boolean | LuaType.Integer | LuaType.Number | LuaType.Table | LuaType.Function | LuaType.Userdata | LuaType.Nil;
else if (realType == typeof(string))
lua |= LuaType.String;
else if (realType == typeof(bool))
lua |= LuaType.Boolean;
else if (realType == typeof(byte) || realType == typeof(sbyte))
lua |= LuaType.Integer;
else if (realType == typeof(short) || realType == typeof(ushort))
lua |= LuaType.Integer;
else if (realType == typeof(int) || realType == typeof(uint))
lua |= LuaType.Integer;
else if (realType == typeof(long) || realType == typeof(ulong))
lua |= LuaType.Integer;
else if (realType == typeof(float) || realType == typeof(double))
lua |= LuaType.Number;
else if (realType == typeof(Closure) || realType == typeof(CallbackFunction))
lua |= LuaType.Function;
else if (realType == typeof(Table))
lua |= LuaType.Table;
else if (realType == typeof(void))
lua |= LuaType.Nil;
else if (realType.IsAssignableTo(typeof(IEnumerable)) || (realType.IsGenericType && realType.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
lua |= LuaType.Table;
else if (realType.IsAssignableTo(typeof(IDictionary)) || (realType.IsGenericType && realType.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
lua |= LuaType.Table;
else
lua |= LuaType.Userdata;

return lua;
}
}
Loading

0 comments on commit 1c84f76

Please sign in to comment.