Add importing of ffxivteamcraft macros
This commit is contained in:
@@ -144,7 +144,7 @@ public static class MacroCopy
|
|||||||
|
|
||||||
var module = RaptureMacroModule.Instance();
|
var module = RaptureMacroModule.Instance();
|
||||||
var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx);
|
var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx);
|
||||||
var text = Utf8String.FromString(macroText.Replace(Environment.NewLine, "\n"));
|
var text = Utf8String.FromString(macroText.ReplaceLineEndings("\n"));
|
||||||
module->ReplaceMacroLines(macro, text);
|
module->ReplaceMacroLines(macro, text);
|
||||||
text->Dtor();
|
text->Dtor();
|
||||||
IMemorySpace.Free(text);
|
IMemorySpace.Free(text);
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using Dalamud.Networking.Http;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class MacroImport
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<ActionType>? TryParseMacro(string inputMacro)
|
||||||
|
{
|
||||||
|
var actions = new List<ActionType>();
|
||||||
|
foreach(var line in inputMacro.ReplaceLineEndings("\n").Split("\n"))
|
||||||
|
{
|
||||||
|
if (TryParseLine(line) is { } action)
|
||||||
|
actions.Add(action);
|
||||||
|
}
|
||||||
|
return actions.Count > 0 ? actions : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionType? TryParseLine(string line)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("/ac", StringComparison.OrdinalIgnoreCase))
|
||||||
|
line = line[3..];
|
||||||
|
else if (line.StartsWith("/action", StringComparison.OrdinalIgnoreCase))
|
||||||
|
line = line[7..];
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
|
||||||
|
line = line.TrimStart();
|
||||||
|
|
||||||
|
// get first word
|
||||||
|
if (line.StartsWith('"'))
|
||||||
|
{
|
||||||
|
line = line[1..];
|
||||||
|
|
||||||
|
var end = line.IndexOf('"', 1);
|
||||||
|
if (end != -1)
|
||||||
|
line = line[..end];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var end = line.IndexOf(' ', 1);
|
||||||
|
if (end != -1)
|
||||||
|
line = line[..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var action in Enum.GetValues<ActionType>())
|
||||||
|
{
|
||||||
|
if (line.Equals(action.GetName(ClassJob.Carpenter), StringComparison.OrdinalIgnoreCase))
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseUrl(string url, out Uri uri)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out uri!))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!uri.IsDefaultPort)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<RetrievedMacro> RetrieveUrl(string url, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!TryParseUrl(url, out var uri))
|
||||||
|
throw new ArgumentException("Unsupported url", nameof(url));
|
||||||
|
|
||||||
|
switch (uri.DnsSafeHost)
|
||||||
|
{
|
||||||
|
case "ffxivteamcraft.com":
|
||||||
|
return RetrieveTeamcraftUrl(uri, token);
|
||||||
|
case "craftingway.app":
|
||||||
|
return RetrieveCraftingwayUrl(uri, token);
|
||||||
|
default:
|
||||||
|
throw new UnreachableException("TryParseUrl should handle miscellaneous edge cases");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record TeamcraftMacro
|
||||||
|
{
|
||||||
|
public sealed record StringValue
|
||||||
|
{
|
||||||
|
[JsonPropertyName("stringValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required string Value { get; set; }
|
||||||
|
|
||||||
|
public static implicit operator string(StringValue v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record IntegerValue
|
||||||
|
{
|
||||||
|
[JsonPropertyName("integerValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required int Value { get; set; }
|
||||||
|
|
||||||
|
public static implicit operator int(IntegerValue v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record MapValue<T>
|
||||||
|
{
|
||||||
|
public sealed record ValueData
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required T Fields { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("mapValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required ValueData Data { get; set; }
|
||||||
|
|
||||||
|
public T Value => Data.Fields;
|
||||||
|
public static implicit operator T(MapValue<T> v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ArrayValue<T>
|
||||||
|
{
|
||||||
|
public sealed record ValueData
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required T[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("arrayValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required ValueData Data { get; set; }
|
||||||
|
|
||||||
|
public T[] Value => Data.Values;
|
||||||
|
public static implicit operator T[](ArrayValue<T> v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record RecipeFieldData
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required IntegerValue RLvl { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
public required IntegerValue Durability { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record FieldData
|
||||||
|
{
|
||||||
|
public StringValue? Name { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
public required ArrayValue<StringValue> Rotation { get; set; }
|
||||||
|
public MapValue<RecipeFieldData>? Recipe { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ErrorData
|
||||||
|
{
|
||||||
|
public required int Code { get; set; }
|
||||||
|
public required string Message { get; set; }
|
||||||
|
public required string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldData? Fields { get; set; }
|
||||||
|
|
||||||
|
public ErrorData? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RetrievedMacro(string Name, IReadOnlyList<ActionType> Actions);
|
||||||
|
|
||||||
|
private static async Task<RetrievedMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token)
|
||||||
|
{
|
||||||
|
using var heCallback = new HappyEyeballsCallback();
|
||||||
|
using var client = new HttpClient(new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
|
ConnectCallback = heCallback.ConnectCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
|
||||||
|
if (!path.StartsWith("simulator/", StringComparison.Ordinal))
|
||||||
|
throw new ArgumentException("Teamcraft macro url should start with /simulator", nameof(uri));
|
||||||
|
path = path[10..];
|
||||||
|
|
||||||
|
var lastSlash = path.LastIndexOf('/');
|
||||||
|
if (lastSlash == -1)
|
||||||
|
throw new ArgumentException("Teamcraft macro url is not in the right format", nameof(uri));
|
||||||
|
|
||||||
|
var id = path[(lastSlash + 1)..];
|
||||||
|
|
||||||
|
var resp = await client.GetFromJsonAsync<TeamcraftMacro>($"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}", token).ConfigureAwait(false);
|
||||||
|
if (resp is null)
|
||||||
|
throw new Exception("Internal error; failed to retrieve macro");
|
||||||
|
if (resp.Error is { } error)
|
||||||
|
throw new Exception($"Internal server error ({error.Status}); {error.Message}");
|
||||||
|
if (resp.Fields is not { } rotation)
|
||||||
|
throw new Exception($"Internal error; No fields or error was returned");
|
||||||
|
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49
|
||||||
|
var name = rotation.Name?.Value ??
|
||||||
|
(rotation.Recipe is { Value: var recipe } ?
|
||||||
|
$"rlvl{recipe.RLvl.Value} - {rotation.Rotation.Value.Length} steps, {recipe.Durability.Value} dur" :
|
||||||
|
"New Teamcraft Rotation");
|
||||||
|
var actions = new List<ActionType>();
|
||||||
|
foreach(var action in rotation.Rotation.Value)
|
||||||
|
{
|
||||||
|
ActionType? actionType = action.Value switch
|
||||||
|
{
|
||||||
|
"BasicSynthesis" => ActionType.BasicSynthesis,
|
||||||
|
"CarefulSynthesis" => ActionType.CarefulSynthesis,
|
||||||
|
"PrudentSynthesis" => ActionType.PrudentSynthesis,
|
||||||
|
"RapidSynthesis" => ActionType.RapidSynthesis,
|
||||||
|
"Groundwork" => ActionType.Groundwork,
|
||||||
|
"FocusedSynthesis" => ActionType.FocusedSynthesis,
|
||||||
|
"MuscleMemory" => ActionType.MuscleMemory,
|
||||||
|
"IntensiveSynthesis" => ActionType.IntensiveSynthesis,
|
||||||
|
"BasicTouch" => ActionType.BasicTouch,
|
||||||
|
"StandardTouch" => ActionType.StandardTouch,
|
||||||
|
"AdvancedTouch" => ActionType.AdvancedTouch,
|
||||||
|
"HastyTouch" => ActionType.HastyTouch,
|
||||||
|
"ByregotsBlessing" => ActionType.ByregotsBlessing,
|
||||||
|
"PreciseTouch" => ActionType.PreciseTouch,
|
||||||
|
"FocusedTouch" => ActionType.FocusedTouch,
|
||||||
|
"PrudentTouch" => ActionType.PrudentTouch,
|
||||||
|
"TrainedEye" => ActionType.TrainedEye,
|
||||||
|
"PreparatoryTouch" => ActionType.PreparatoryTouch,
|
||||||
|
"Reflect" => ActionType.Reflect,
|
||||||
|
"TrainedFinesse" => ActionType.TrainedFinesse,
|
||||||
|
"TricksOfTheTrade" => ActionType.TricksOfTheTrade,
|
||||||
|
"MastersMend" => ActionType.MastersMend,
|
||||||
|
"Manipulation" => ActionType.Manipulation,
|
||||||
|
"WasteNot" => ActionType.WasteNot,
|
||||||
|
"WasteNotII" => ActionType.WasteNot2,
|
||||||
|
"GreatStrides" => ActionType.GreatStrides,
|
||||||
|
"Innovation" => ActionType.Innovation,
|
||||||
|
"Veneration" => ActionType.Veneration,
|
||||||
|
"FinalAppraisal" => ActionType.FinalAppraisal,
|
||||||
|
"Observe" => ActionType.Observe,
|
||||||
|
"HeartAndSoul" => ActionType.HeartAndSoul,
|
||||||
|
"CarefulObservation" => ActionType.CarefulObservation,
|
||||||
|
"DelicateSynthesis" => ActionType.DelicateSynthesis,
|
||||||
|
"RemoveFinalAppraisal" => throw new Exception("Removing Final Appraisal is an unsupported action"),
|
||||||
|
null => null,
|
||||||
|
{ } actionValue => throw new Exception($"Unknown action {actionValue}"),
|
||||||
|
};
|
||||||
|
if (actionType.HasValue)
|
||||||
|
actions.Add(actionType.Value);
|
||||||
|
}
|
||||||
|
return new(name, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<RetrievedMacro> RetrieveCraftingwayUrl(Uri uri, CancellationToken token)
|
||||||
|
{
|
||||||
|
using var heCallback = new HappyEyeballsCallback();
|
||||||
|
using var client = new HttpClient(new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
|
ConnectCallback = heCallback.ConnectCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ using Craftimizer.Utils;
|
|||||||
using Dalamud.Game.ClientState.Statuses;
|
using Dalamud.Game.ClientState.Statuses;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.GameFonts;
|
using Dalamud.Interface.GameFonts;
|
||||||
using Dalamud.Interface.Internal;
|
using Dalamud.Interface.Internal;
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
@@ -104,6 +106,14 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
private IDalamudTextureWrap EatFromTheHandBadge { get; }
|
private IDalamudTextureWrap EatFromTheHandBadge { get; }
|
||||||
private GameFontHandle AxisFont { get; }
|
private GameFontHandle AxisFont { get; }
|
||||||
|
|
||||||
|
private string popupSaveAsMacroName = string.Empty;
|
||||||
|
|
||||||
|
private string popupImportText = string.Empty;
|
||||||
|
private string popupImportUrl = string.Empty;
|
||||||
|
private string popupImportError = string.Empty;
|
||||||
|
private CancellationTokenSource? popupImportUrlTokenSource;
|
||||||
|
private MacroImport.RetrievedMacro? popupImportUrlMacro;
|
||||||
|
|
||||||
public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) : base("Craftimizer Macro Editor", WindowFlags, false)
|
public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) : base("Craftimizer Macro Editor", WindowFlags, false)
|
||||||
{
|
{
|
||||||
CharacterStats = characterStats;
|
CharacterStats = characterStats;
|
||||||
@@ -168,7 +178,8 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
using (var table = ImRaii.Table("macroInfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) {
|
using (var table = ImRaii.Table("macroInfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame))
|
||||||
|
{
|
||||||
if (table)
|
if (table)
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2);
|
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2);
|
||||||
@@ -1138,7 +1149,7 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
{
|
{
|
||||||
var height = ImGui.GetFrameHeight();
|
var height = ImGui.GetFrameHeight();
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var width = availWidth - (spacing + height) * (DefaultActions.Length > 0 ? 3 : 2); // small buttons at the end
|
var width = availWidth - ((spacing + height) * (3 + (DefaultActions.Length > 0 ? 1 : 0))); // small buttons at the end
|
||||||
var halfWidth = (width - spacing) / 2f;
|
var halfWidth = (width - spacing) / 2f;
|
||||||
var quarterWidth = (halfWidth - spacing) / 2f;
|
var quarterWidth = (halfWidth - spacing) / 2f;
|
||||||
|
|
||||||
@@ -1191,6 +1202,15 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip("Copy to Clipboard");
|
ImGui.SetTooltip("Copy to Clipboard");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
using (var _disabled = ImRaii.Disabled(SolverRunning))
|
||||||
|
{
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.FileImport, new(height)))
|
||||||
|
ShowImportPopup();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
ImGui.SetTooltip("Import Macro");
|
||||||
|
DrawImportPopup();
|
||||||
|
ImGui.SameLine();
|
||||||
if (DefaultActions.Length > 0)
|
if (DefaultActions.Length > 0)
|
||||||
{
|
{
|
||||||
using (var _disabled = ImRaii.Disabled(SolverRunning))
|
using (var _disabled = ImRaii.Disabled(SolverRunning))
|
||||||
@@ -1219,11 +1239,10 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
ImGui.SetTooltip("Clear");
|
ImGui.SetTooltip("Clear");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string popupMacroName = string.Empty;
|
|
||||||
private void ShowSaveAsPopup()
|
private void ShowSaveAsPopup()
|
||||||
{
|
{
|
||||||
ImGui.OpenPopup($"##saveAsPopup");
|
ImGui.OpenPopup($"##saveAsPopup");
|
||||||
popupMacroName = string.Empty;
|
popupSaveAsMacroName = string.Empty;
|
||||||
ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2));
|
ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,11 +1254,11 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
if (ImGui.IsWindowAppearing())
|
if (ImGui.IsWindowAppearing())
|
||||||
ImGui.SetKeyboardFocusHere();
|
ImGui.SetKeyboardFocusHere();
|
||||||
ImGui.SetNextItemWidth(ImGui.CalcItemWidth());
|
ImGui.SetNextItemWidth(ImGui.CalcItemWidth());
|
||||||
if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue))
|
if (ImGui.InputTextWithHint($"##setName", "Name", ref popupSaveAsMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue))
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(popupMacroName))
|
if (!string.IsNullOrWhiteSpace(popupSaveAsMacroName))
|
||||||
{
|
{
|
||||||
var newMacro = new Macro() { Name = popupMacroName, Actions = Macro.Select(s => s.Action).ToArray() };
|
var newMacro = new Macro() { Name = popupSaveAsMacroName, Actions = Macro.Select(s => s.Action).ToArray() };
|
||||||
Service.Configuration.AddMacro(newMacro);
|
Service.Configuration.AddMacro(newMacro);
|
||||||
MacroSetter = actions =>
|
MacroSetter = actions =>
|
||||||
{
|
{
|
||||||
@@ -1252,6 +1271,152 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowImportPopup()
|
||||||
|
{
|
||||||
|
ImGui.OpenPopup($"##importPopup");
|
||||||
|
popupImportText = string.Empty;
|
||||||
|
popupImportUrl = string.Empty;
|
||||||
|
popupImportError = string.Empty;
|
||||||
|
popupImportUrlMacro = null;
|
||||||
|
popupImportUrlTokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawImportPopup()
|
||||||
|
{
|
||||||
|
const string ExampleMacro = "/mlock\n/ac \"Muscle Memory\" <wait.3>\n/ac Manipulation <wait.2>\n/ac Veneration <wait.2>\n/ac \"Waste Not II\" <wait.2>\n/ac Groundwork <wait.3>\n/ac Innovation <wait.2>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Preparatory Touch\" <wait.3>\n/ac \"Great Strides\" <wait.2>\n/ac \"Byregot's Blessing\" <wait.3>\n/ac \"Careful Synthesis\" <wait.3>";
|
||||||
|
const string ExampleUrl = "https://ffxivteamcraft.com/simulator/39630/35499/9XOZDZKhbVXJUIPXjM63";
|
||||||
|
|
||||||
|
ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, new Vector2(0.5f));
|
||||||
|
ImGui.SetNextWindowSizeConstraints(new(400, 0), new(float.PositiveInfinity));
|
||||||
|
using var popup = ImRaii.Popup($"##importPopup", ImGuiWindowFlags.Modal | ImGuiWindowFlags.NoMove);
|
||||||
|
if (popup)
|
||||||
|
{
|
||||||
|
bool submittedText, submittedUrl;
|
||||||
|
|
||||||
|
using (var panel = ImGuiUtils.GroupPanel("##text", -1, out var availWidth))
|
||||||
|
{
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGuiUtils.TextCentered("Paste your macro here");
|
||||||
|
{
|
||||||
|
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||||
|
ImGuiUtils.InputTextMultilineWithHint("", ExampleMacro, ref popupImportText, 2048, new(availWidth, ImGui.GetTextLineHeight() * 15 + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.AutoSelectAll);
|
||||||
|
}
|
||||||
|
using (var _disabled = ImRaii.Disabled(popupImportUrlTokenSource != null))
|
||||||
|
submittedText = ImGui.Button("Import", new(availWidth, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var panel = ImGuiUtils.GroupPanel("##url", -1, out var availWidth))
|
||||||
|
{
|
||||||
|
var availOffset = ImGui.GetContentRegionAvail().X - availWidth;
|
||||||
|
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGuiUtils.TextCentered("or provide a url to it");
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
||||||
|
{
|
||||||
|
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||||
|
ImGuiUtils.TextRight(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetContentRegionAvail().X - availOffset);
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
using var t = ImRaii.Tooltip();
|
||||||
|
ImGui.Text("Supported sites:");
|
||||||
|
ImGui.BulletText("ffxivteamcraft.com");
|
||||||
|
ImGui.BulletText("craftingway.app");
|
||||||
|
ImGui.Text("More suggestions are appreciated!");
|
||||||
|
}
|
||||||
|
ImGui.SetNextItemWidth(availWidth);
|
||||||
|
submittedUrl = ImGui.InputTextWithHint("", ExampleUrl, ref popupImportUrl, 2048, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue);
|
||||||
|
using (var _disabled = ImRaii.Disabled(popupImportUrlTokenSource != null))
|
||||||
|
submittedUrl = ImGui.Button("Import", new(availWidth, 0)) || submittedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(default);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(popupImportError))
|
||||||
|
{
|
||||||
|
using (var c = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
||||||
|
ImGui.TextWrapped(popupImportError);
|
||||||
|
ImGui.Dummy(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGuiUtils.ButtonCentered("Nevermind", new(ImGui.GetContentRegionAvail().X / 2f, 0)))
|
||||||
|
{
|
||||||
|
popupImportUrlTokenSource?.Cancel();
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (popupImportUrlTokenSource == null)
|
||||||
|
{
|
||||||
|
if (submittedText)
|
||||||
|
{
|
||||||
|
if (MacroImport.TryParseMacro(popupImportText) is { } parsedActions)
|
||||||
|
{
|
||||||
|
popupImportUrlTokenSource?.Cancel();
|
||||||
|
Macro.Clear();
|
||||||
|
foreach (var action in parsedActions)
|
||||||
|
AddStep(action);
|
||||||
|
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification($"Imported macro with {parsedActions.Count} step{(parsedActions.Count != 1 ? "s" : "")}", "Craftimizer Macro Imported", NotificationType.Success);
|
||||||
|
popupImportUrlTokenSource?.Cancel();
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
popupImportError = "Could not find any actions to import. Is it a valid macro?";
|
||||||
|
}
|
||||||
|
if (submittedUrl)
|
||||||
|
{
|
||||||
|
if (MacroImport.TryParseUrl(popupImportUrl, out _))
|
||||||
|
{
|
||||||
|
popupImportUrlTokenSource = new();
|
||||||
|
popupImportUrlMacro = null;
|
||||||
|
var token = popupImportUrlTokenSource.Token;
|
||||||
|
var url = popupImportUrl;
|
||||||
|
|
||||||
|
var task = Task.Run(() => MacroImport.RetrieveUrl(url, token), token);
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (token == popupImportUrlTokenSource.Token)
|
||||||
|
popupImportUrlTokenSource = null;
|
||||||
|
});
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
|
||||||
|
}
|
||||||
|
catch (AggregateException e)
|
||||||
|
{
|
||||||
|
popupImportError = e.Message;
|
||||||
|
Log.Error(e, "Retrieving macro failed");
|
||||||
|
}
|
||||||
|
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
_ = task.ContinueWith(t => popupImportUrlMacro = t.Result, TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
popupImportError = "The url is not in the right format for any supported sites.";
|
||||||
|
}
|
||||||
|
if (popupImportUrlMacro is { Name: var name, Actions: var actions })
|
||||||
|
{
|
||||||
|
Macro.Clear();
|
||||||
|
foreach(var action in actions)
|
||||||
|
AddStep(action);
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification($"Imported macro \"{name}\"", "Craftimizer Macro Imported", NotificationType.Success);
|
||||||
|
|
||||||
|
popupImportUrlTokenSource?.Cancel();
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
popupImportUrlTokenSource?.Cancel();
|
||||||
|
popupImportUrlTokenSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
private void CalculateBestMacro()
|
private void CalculateBestMacro()
|
||||||
{
|
{
|
||||||
SolverTokenSource?.Cancel();
|
SolverTokenSource?.Cancel();
|
||||||
@@ -1294,7 +1459,7 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
|
|
||||||
var solver = new Solver.Solver(config, state) { Token = token };
|
var solver = new Solver.Solver(config, state) { Token = token };
|
||||||
solver.OnLog += Log.Debug;
|
solver.OnLog += Log.Debug;
|
||||||
solver.OnNewAction += a => AddStep(a, isMacro: true);
|
solver.OnNewAction += a => AddStep(a, isSolver: true);
|
||||||
solver.Start();
|
solver.Start();
|
||||||
_ = solver.GetTask().GetAwaiter().GetResult();
|
_ = solver.GetTask().GetAwaiter().GetResult();
|
||||||
|
|
||||||
@@ -1321,11 +1486,11 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
lastState = ((step.Response, step.State) = sim.Execute(lastState, step.Action)).State;
|
lastState = ((step.Response, step.State) = sim.Execute(lastState, step.Action)).State;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddStep(ActionType action, int index = -1, bool isMacro = false)
|
private void AddStep(ActionType action, int index = -1, bool isSolver = false)
|
||||||
{
|
{
|
||||||
if (index < -1 || index >= Macro.Count)
|
if (index < -1 || index >= Macro.Count)
|
||||||
throw new ArgumentOutOfRangeException(nameof(index));
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
if (!isMacro && SolverRunning)
|
if (!isSolver && SolverRunning)
|
||||||
throw new InvalidOperationException("Cannot add steps while solver is running");
|
throw new InvalidOperationException("Cannot add steps while solver is running");
|
||||||
if (!SolverRunning)
|
if (!SolverRunning)
|
||||||
SolverStartStepCount = null;
|
SolverStartStepCount = null;
|
||||||
@@ -1336,7 +1501,8 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
var resp = sim.Execute(State, action);
|
var resp = sim.Execute(State, action);
|
||||||
Macro.Add(new() { Action = action, Response = resp.Response, State = resp.NewState });
|
Macro.Add(new() { Action = action, Response = resp.Response, State = resp.NewState });
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
|
{
|
||||||
var state = index == 0 ? InitialState : Macro[index - 1].State;
|
var state = index == 0 ? InitialState : Macro[index - 1].State;
|
||||||
var sim = new Sim(state);
|
var sim = new Sim(state);
|
||||||
var resp = sim.Execute(state, action);
|
var resp = sim.Execute(state, action);
|
||||||
|
|||||||
Reference in New Issue
Block a user