Add importing of ffxivteamcraft macros
This commit is contained in:
@@ -144,7 +144,7 @@ public static class MacroCopy
|
||||
|
||||
var module = RaptureMacroModule.Instance();
|
||||
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);
|
||||
text->Dtor();
|
||||
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.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Utility;
|
||||
@@ -104,6 +106,14 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
private IDalamudTextureWrap EatFromTheHandBadge { 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)
|
||||
{
|
||||
CharacterStats = characterStats;
|
||||
@@ -168,7 +178,8 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
|
||||
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)
|
||||
{
|
||||
ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2);
|
||||
@@ -691,7 +702,7 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
ImGui.AlignTextToFramePadding();
|
||||
|
||||
ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize));
|
||||
|
||||
|
||||
ImGui.SameLine(0, 5);
|
||||
|
||||
ushort? newRecipe = null;
|
||||
@@ -983,7 +994,7 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
DrawBars(datas);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
using (var panel = ImGuiUtils.GroupPanel("Buffs", -1, out _))
|
||||
{
|
||||
using var _font = ImRaii.PushFont(AxisFont.ImFont);
|
||||
@@ -995,7 +1006,7 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
ImGui.SameLine(0, 0);
|
||||
|
||||
var effects = State.ActiveEffects;
|
||||
foreach(var effect in Enum.GetValues<EffectType>())
|
||||
foreach (var effect in Enum.GetValues<EffectType>())
|
||||
{
|
||||
if (!effects.HasEffect(effect))
|
||||
continue;
|
||||
@@ -1040,7 +1051,7 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
});
|
||||
var maxSize = (textSize - 2 * spacing - ImGui.CalcTextSize("/").X) / 2;
|
||||
var barSize = totalSize - textSize - spacing;
|
||||
foreach(var bar in bars)
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
using var panel = ImGuiUtils.GroupPanel(bar.Name, totalSize, out _);
|
||||
if (bar.Condition is { } condition)
|
||||
@@ -1138,7 +1149,7 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
{
|
||||
var height = ImGui.GetFrameHeight();
|
||||
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 quarterWidth = (halfWidth - spacing) / 2f;
|
||||
|
||||
@@ -1191,6 +1202,15 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Copy to Clipboard");
|
||||
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)
|
||||
{
|
||||
using (var _disabled = ImRaii.Disabled(SolverRunning))
|
||||
@@ -1219,11 +1239,10 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
ImGui.SetTooltip("Clear");
|
||||
}
|
||||
|
||||
private string popupMacroName = string.Empty;
|
||||
private void ShowSaveAsPopup()
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1235,11 +1254,11 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
if (ImGui.IsWindowAppearing())
|
||||
ImGui.SetKeyboardFocusHere();
|
||||
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);
|
||||
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()
|
||||
{
|
||||
SolverTokenSource?.Cancel();
|
||||
@@ -1294,7 +1459,7 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
|
||||
var solver = new Solver.Solver(config, state) { Token = token };
|
||||
solver.OnLog += Log.Debug;
|
||||
solver.OnNewAction += a => AddStep(a, isMacro: true);
|
||||
solver.OnNewAction += a => AddStep(a, isSolver: true);
|
||||
solver.Start();
|
||||
_ = 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;
|
||||
}
|
||||
|
||||
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)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
if (!isMacro && SolverRunning)
|
||||
if (!isSolver && SolverRunning)
|
||||
throw new InvalidOperationException("Cannot add steps while solver is running");
|
||||
if (!SolverRunning)
|
||||
SolverStartStepCount = null;
|
||||
@@ -1336,13 +1501,14 @@ public sealed class MacroEditor : Window, IDisposable
|
||||
var resp = sim.Execute(State, action);
|
||||
Macro.Add(new() { Action = action, Response = resp.Response, State = resp.NewState });
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
var state = index == 0 ? InitialState : Macro[index - 1].State;
|
||||
var sim = new Sim(state);
|
||||
var resp = sim.Execute(state, action);
|
||||
Macro.Insert(index, new() { Action = action, Response = resp.Response, State = resp.NewState });
|
||||
state = resp.NewState;
|
||||
for(var i = index + 1; i < Macro.Count; i++)
|
||||
for (var i = index + 1; i < Macro.Count; i++)
|
||||
state = ((Macro[i].Response, Macro[i].State) = sim.Execute(state, Macro[i].Action)).State;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user