diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9f68ad754..00ab2b567 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -1,4 +1,4 @@ - + AnyCPU net472 @@ -46,6 +46,7 @@ + diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/DalamudInterface.cs index 056cca695..9283ff79e 100644 --- a/Dalamud/Interface/DalamudInterface.cs +++ b/Dalamud/Interface/DalamudInterface.cs @@ -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; } + /// + /// Open the colors test window. + /// + internal void OpenScratchpadWindow() + { + this.scratchpadWindow.IsOpen = true; + } + /// /// Toggle the Plugin Installer window. /// @@ -514,5 +545,13 @@ namespace Dalamud.Interface { this.componentDemoWindow.IsOpen ^= true; } + + /// + /// Toggle the scratchpad window. + /// + internal void ToggleScratchpadWindow() + { + this.scratchpadWindow.IsOpen ^= true; + } } } diff --git a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs b/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs new file mode 100644 index 000000000..3ef4524ea --- /dev/null +++ b/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs @@ -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 loadedScratches = new Dictionary(); + + 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("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; + } + } + } +} diff --git a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs b/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs new file mode 100644 index 000000000..ac67492a4 --- /dev/null +++ b/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs @@ -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 + { + } +} diff --git a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs b/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs new file mode 100644 index 000000000..1baf6d280 --- /dev/null +++ b/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs @@ -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, + } +} diff --git a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs b/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs new file mode 100644 index 000000000..ad29affae --- /dev/null +++ b/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs @@ -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(); + + 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() + .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}Inst;\n"; + + hookInit += $"var addrH{i} = pi.TargetModuleScanner.ScanText(\"{hook.Sig}\");\n"; + hookInit += + $"this.hook{i}Inst = new Hook(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; + } + } +} diff --git a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs b/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs new file mode 100644 index 000000000..07f66b63b --- /dev/null +++ b/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs @@ -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; + } +} diff --git a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs b/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs new file mode 100644 index 000000000..ba719d55d --- /dev/null +++ b/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs @@ -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 documents = new List(); + + 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(); + } + } + } +}