feat: add WIP scratchpad

This commit is contained in:
goat 2021-05-01 18:27:10 +02:00
parent 3ecbadfde8
commit 9c35be2916
No known key found for this signature in database
GPG key ID: F18F057873895461
8 changed files with 557 additions and 1 deletions

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target">
<PlatformTarget>AnyCPU</PlatformTarget>
<TargetFramework>net472</TargetFramework>
@ -46,6 +46,7 @@
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
<PackageReference Include="Lumina" Version="3.1.0" />
<PackageReference Include="Lumina.Excel" Version="5.50.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="PropertyChanged.Fody" Version="2.6.1" />
<PackageReference Include="Serilog" Version="2.6.0" />

View file

@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
using CheapLoc;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Scratchpad;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using ImGuiNET;
@ -33,6 +34,7 @@ namespace Dalamud.Interface
private readonly DalamudChangelogWindow changelogWindow;
private readonly ComponentDemoWindow componentDemoWindow;
private readonly ColorDemoWindow colorDemoWindow;
private readonly ScratchpadWindow scratchpadWindow;
private readonly WindowSystem windowSystem = new WindowSystem("DalamudCore");
@ -108,6 +110,12 @@ namespace Dalamud.Interface
};
this.windowSystem.AddWindow(this.colorDemoWindow);
this.scratchpadWindow = new ScratchpadWindow(this.dalamud)
{
IsOpen = false,
};
this.windowSystem.AddWindow(this.scratchpadWindow);
Log.Information("[DUI] Windows added");
if (dalamud.Configuration.LogOpenAtStartup)
@ -305,6 +313,21 @@ namespace Dalamud.Interface
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Scratchpad"))
{
if (ImGui.MenuItem("Open Scratchpad"))
{
this.OpenScratchpadWindow();
}
if (ImGui.MenuItem("Dispose all scratches"))
{
this.scratchpadWindow.Execution.DisposeAllScratches();
}
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Localization"))
{
if (ImGui.MenuItem("Export localizable"))
@ -441,6 +464,14 @@ namespace Dalamud.Interface
this.colorDemoWindow.IsOpen = true;
}
/// <summary>
/// Open the colors test window.
/// </summary>
internal void OpenScratchpadWindow()
{
this.scratchpadWindow.IsOpen = true;
}
/// <summary>
/// Toggle the Plugin Installer window.
/// </summary>
@ -514,5 +545,13 @@ namespace Dalamud.Interface
{
this.componentDemoWindow.IsOpen ^= true;
}
/// <summary>
/// Toggle the scratchpad window.
/// </summary>
internal void ToggleScratchpadWindow()
{
this.scratchpadWindow.IsOpen ^= true;
}
}
}

View file

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Configuration;
using Dalamud.Plugin;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Serilog;
namespace Dalamud.Interface.Scratchpad
{
class ScratchExecutionManager
{
private readonly Dalamud dalamud;
private Dictionary<Guid, IDalamudPlugin> loadedScratches = new Dictionary<Guid, IDalamudPlugin>();
public ScratchMacroProcessor MacroProcessor { get; private set; } = new ScratchMacroProcessor();
public ScratchExecutionManager(Dalamud dalamud)
{
this.dalamud = dalamud;
}
public void DisposeAllScratches()
{
foreach (var dalamudPlugin in this.loadedScratches)
{
dalamudPlugin.Value.Dispose();
}
this.loadedScratches.Clear();
}
public ScratchLoadStatus RenewScratch(ScratchpadDocument doc)
{
var existingScratch = this.loadedScratches.FirstOrDefault(x => x.Key == doc.Id);
if (existingScratch.Value != null)
{
existingScratch.Value.Dispose();
this.loadedScratches.Remove(existingScratch.Key);
}
var code = doc.IsMacro ? this.MacroProcessor.Process(doc.Content) : doc.Content;
var options = ScriptOptions.Default
.AddReferences(typeof(ImGui).Assembly)
.AddReferences(typeof(Dalamud).Assembly)
.AddReferences(typeof(FFXIVClientStructs.Attributes.Addon)
.Assembly) // FFXIVClientStructs
.AddReferences(typeof(Lumina.GameData).Assembly) // Lumina
.AddReferences(typeof(TerritoryType).Assembly) // Lumina.Excel
//.WithReferences(MetadataReference.CreateFromFile(typeof(ScratchExecutionManager).Assembly.Location))
.AddImports("System")
.AddImports("System.IO")
.AddImports("System.Reflection")
.AddImports("System.Runtime.InteropServices")
.AddImports("Dalamud")
.AddImports("Dalamud.Plugin")
.AddImports("Dalamud.Game.Command")
.AddImports("Dalamud.Hooking")
.AddImports("ImGuiNET");
try
{
var script = CSharpScript.Create(code, options);
var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null, PluginLoadReason.Installer);
var plugin = script.ContinueWith<IDalamudPlugin>("return new ScratchPlugin() as IDalamudPlugin;").RunAsync().GetAwaiter().GetResult()
.ReturnValue;
plugin.Initialize(pi);
this.loadedScratches.Add(doc.Id, plugin);
return ScratchLoadStatus.Success;
}
catch (CompilationErrorException ex)
{
Log.Error(ex, "Compilation error occurred!\n" + string.Join(Environment.NewLine, ex.Diagnostics));
return ScratchLoadStatus.FailureCompile;
}
catch (Exception ex)
{
Log.Error(ex, "Initialization error occured!\n");
return ScratchLoadStatus.FailureInit;
}
}
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Interface.Scratchpad
{
class ScratchFileWatcher
{
}
}

View file

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Interface.Scratchpad
{
enum ScratchLoadStatus
{
Unknown,
FailureCompile,
FailureInit,
Success,
}
}

View file

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Dalamud.Plugin;
namespace Dalamud.Interface.Scratchpad
{
class ScratchMacroProcessor
{
private const string template = @"
public class ScratchPlugin : IDalamudPlugin {
public string Name => ""ScratchPlugin"";
private DalamudPluginInterface pi;
{SETUPBODY}
public void Initialize(DalamudPluginInterface pluginInterface)
{
this.pi = pluginInterface;
this.pi.UiBuilder.OnBuildUi += DrawUI;
{INITBODY}
}
private void DrawUI()
{
{DRAWBODY}
}
{NONEBODY}
public void Dispose()
{
this.pi.UiBuilder.OnBuildUi -= DrawUI;
{DISPOSEBODY}
}
}
";
private enum ParseContext
{
None,
Init,
Draw,
Hook,
Dispose,
}
private class HookInfo
{
public string Body { get; set; }
public string Arguments { get; set; }
public string Invocation { get; set; }
public string RetType { get; set; }
public string Sig { get; set; }
}
public string Process(string input)
{
var lines = input.Split(new[] {'\r', '\n'});
var ctx = ParseContext.None;
var setupBody = string.Empty;
var noneBody = string.Empty;
var initBody = string.Empty;
var disposeBody = string.Empty;
var drawBody = string.Empty;
var tHook = new HookInfo();
var hooks = new List<HookInfo>();
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (line.StartsWith("INITIALIZE:"))
{
ctx = ParseContext.Init;
continue;
}
if (line.StartsWith("DRAW:"))
{
ctx = ParseContext.Draw;
continue;
}
if (line.StartsWith("DISPOSE:"))
{
ctx = ParseContext.Dispose;
continue;
}
if (line.StartsWith("HOOK("))
{
ctx = ParseContext.Hook;
var args = Regex.Match(line, "HOOK\\((.+)+\\):").Groups[0].Captures[0].Value
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(x => x[0] == ' ' ? x[1..] : x).ToArray();
tHook.Sig = args[0].Replace("\"", string.Empty); // Split quotation marks if any
tHook.Sig = tHook.Sig.Replace("HOOK(", string.Empty);
tHook.RetType = args[1];
tHook.Arguments = string.Join(", ", args.Skip(2).ToArray()).Replace("):", string.Empty);
var invocationGroups = Regex.Matches(tHook.Arguments, "\\S+ ([a-zA-Z0-9]+),*").Cast<Match>()
.Select(x => x.Groups[1].Value);
tHook.Invocation = string.Join(", ", invocationGroups);
continue;
}
if (line.StartsWith("END;"))
{
switch (ctx)
{
case ParseContext.None:
throw new Exception("Not in a macro!!!");
case ParseContext.Init:
break;
case ParseContext.Draw:
break;
case ParseContext.Hook:
hooks.Add(tHook);
tHook = new HookInfo();
break;
case ParseContext.Dispose:
break;
default:
throw new ArgumentOutOfRangeException();
}
ctx = ParseContext.None;
continue;
}
switch (ctx)
{
case ParseContext.None:
noneBody += line + "\n";
break;
case ParseContext.Init:
initBody += line + "\n";
break;
case ParseContext.Draw:
drawBody += line + "\n";
break;
case ParseContext.Hook:
tHook.Body += line + "\n";
break;
case ParseContext.Dispose:
disposeBody += line + "\n";
break;
default:
throw new ArgumentOutOfRangeException();
}
}
var hookSetup = string.Empty;
var hookInit = string.Empty;
var hookDetour = string.Empty;
var hookDispose = string.Empty;
for (var i = 0; i < hooks.Count; i++)
{
var hook = hooks[i];
hookSetup +=
$"private delegate {hook.RetType} Hook{i}Delegate({hook.Arguments});\n";
hookSetup +=
$"private Hook<Hook{i}Delegate> hook{i}Inst;\n";
hookInit += $"var addrH{i} = pi.TargetModuleScanner.ScanText(\"{hook.Sig}\");\n";
hookInit +=
$"this.hook{i}Inst = new Hook<Hook{i}Delegate>(addrH{i}, new Hook{i}Delegate(Hook{i}Detour), this);\n";
hookInit +=
$"this.hook{i}Inst.Enable();\n";
var originalCall = $"this.hook{i}Inst.Original({hook.Invocation});\n";
if (hook.RetType != "void")
originalCall = "return " + originalCall;
if (hook.Body.Contains("hook{i}Inst.Original"))
{
PluginLog.Warning($"Attention! A manual call to Original() in Hook #{i} was detected. Original calls will not be managed for you.");
originalCall = string.Empty;
}
hookDetour +=
$"private {hook.RetType} Hook{i}Detour({hook.Arguments}) {{\n" +
$"try {{\n" +
$" {hook.Body}\n" +
$"}} catch(Exception ex) {{\n" +
$" PluginLog.Error(ex, \"Exception in Hook{i}Detour!!\");\n" +
$"}}\n" +
$"{originalCall}" +
$"\n}}\n";
hookDispose += $"this.hook{i}Inst.Dispose();\n";
}
setupBody += "\n" + hookSetup;
initBody = hookInit + "\n" + initBody;
noneBody += "\n" + hookDetour;
disposeBody += "\n" + hookDispose;
var output = template;
output = output.Replace("{SETUPBODY}", setupBody);
output = output.Replace("{INITBODY}", initBody);
output = output.Replace("{DRAWBODY}", drawBody);
output = output.Replace("{NONEBODY}", noneBody);
output = output.Replace("{DISPOSEBODY}", disposeBody);
return output;
}
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Interface.Scratchpad
{
class ScratchpadDocument
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Content = "INITIALIZE:\n\tPluginLog.Information(\"Loaded!\");\nEND;\n\nDISPOSE:\n\tPluginLog.Information(\"Disposed!\");\nEND;\n";
public string Title { get; set; } = "New Document";
public bool HasUnsaved { get; set; }
public bool IsOpen { get; set; }
public ScratchLoadStatus Status { get; set; }
public bool IsMacro = true;
}
}

View file

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.Scratchpad
{
class ScratchpadWindow : Window
{
private readonly Dalamud dalamud;
public ScratchExecutionManager Execution { get; private set; }
private List<ScratchpadDocument> documents = new List<ScratchpadDocument>();
public ScratchpadWindow(Dalamud dalamud) :
base("Plugin Scratchpad")
{
this.dalamud = dalamud;
this.documents.Add(new ScratchpadDocument());
this.SizeConstraintsMin = new Vector2(400, 400);
this.Execution = new ScratchExecutionManager(dalamud);
}
public override void Draw()
{
if (ImGui.BeginMenuBar())
{
if (ImGui.BeginMenu("File"))
{
if (ImGui.MenuItem("Load & Watch"))
{
}
}
}
var flags = ImGuiTabBarFlags.Reorderable | ImGuiTabBarFlags.TabListPopupButton |
ImGuiTabBarFlags.FittingPolicyScroll;
if (ImGui.BeginTabBar("ScratchDocTabBar", flags))
{
if (ImGui.TabItemButton("+", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
this.documents.Add(new ScratchpadDocument());
for (var i = 0; i < this.documents.Count; i++)
{
var isOpen = true;
if (ImGui.BeginTabItem(this.documents[i].Title + (this.documents[i].HasUnsaved ? "*" : string.Empty) + "###ScratchItem" + i, ref isOpen))
{
if (ImGui.InputTextMultiline("###ScratchInput" + i, ref this.documents[i].Content, 20000,
new Vector2(-1, -34), ImGuiInputTextFlags.AllowTabInput))
{
this.documents[i].HasUnsaved = true;
}
ImGuiHelpers.ScaledDummy(3);
if (ImGui.Button("Compile & Reload"))
{
this.documents[i].Status = this.Execution.RenewScratch(this.documents[i]);
}
ImGui.SameLine();
if (ImGui.Button("Dispose all"))
{
this.Execution.DisposeAllScratches();
}
ImGui.SameLine();
if (ImGui.Button("Dump processed code"))
{
try
{
var code = this.Execution.MacroProcessor.Process(this.documents[i].Content);
Log.Information(code);
ImGui.SetClipboardText(code);
}
catch (Exception ex)
{
Log.Error(ex, "Could not process macros");
}
}
ImGui.SameLine();
if (ImGui.Button("Toggle Log"))
{
this.dalamud.DalamudUi.ToggleLog();
}
ImGui.SameLine();
ImGui.Checkbox("Use Macros", ref this.documents[i].IsMacro);
ImGui.SameLine();
switch (this.documents[i].Status)
{
case ScratchLoadStatus.Unknown:
ImGui.TextColored(ImGuiColors.DalamudGrey, "Compile scratch to see status");
break;
case ScratchLoadStatus.FailureCompile:
ImGui.TextColored(ImGuiColors.DalamudRed, "Error during compilation");
break;
case ScratchLoadStatus.FailureInit:
ImGui.TextColored(ImGuiColors.DalamudRed, "Error during init");
break;
case ScratchLoadStatus.Success:
ImGui.TextColored(ImGuiColors.HealerGreen, "OK!");
break;
default:
throw new ArgumentOutOfRangeException();
}
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey, this.documents[i].Id.ToString());
ImGui.EndTabItem();
}
}
ImGui.EndTabBar();
}
}
}
}