diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index c5208b3..9f6dcc7 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -93,6 +93,8 @@ public class Configuration : IPluginConfiguration public bool DisableSynthHelperOnMacro { get; set; } = true; public bool ShowOptimalMacroStat { get; set; } = true; public bool SuggestMacroAutomatically { get; set; } + public bool ShowCommunityMacros { get; set; } = true; + public bool SearchCommunityMacroAutomatically { get; set; } public int SynthHelperStepCount { get; set; } = 5; public bool PinSynthHelperToWindow { get; set; } = true; diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index 577ed29..58f9464 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -35,6 +35,7 @@ public sealed class Plugin : IDalamudPlugin public Hooks Hooks { get; } public Chat Chat { get; } public IconManager IconManager { get; } + public CommunityMacros CommunityMacros { get; } public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { @@ -45,6 +46,7 @@ public sealed class Plugin : IDalamudPlugin Hooks = new(); Chat = new(); IconManager = new(); + CommunityMacros = new(); var assembly = Assembly.GetExecutingAssembly(); Version = assembly.GetCustomAttribute()!.InformationalVersion.Split('+')[0]; diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index 5ea5fb6..4ec8642 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -30,6 +30,7 @@ public sealed class Service public static WindowSystem WindowSystem => Plugin.WindowSystem; public static Chat Chat => Plugin.Chat; public static IconManager IconManager => Plugin.IconManager; + public static CommunityMacros CommunityMacros => Plugin.CommunityMacros; #pragma warning restore CS8618 internal static void Initialize(Plugin plugin, DalamudPluginInterface iface) diff --git a/Craftimizer/Utils/CommunityMacros.cs b/Craftimizer/Utils/CommunityMacros.cs new file mode 100644 index 0000000..147de11 --- /dev/null +++ b/Craftimizer/Utils/CommunityMacros.cs @@ -0,0 +1,473 @@ +using Dalamud.Networking.Http; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json.Serialization; +using System.Net.Http.Json; +using System.Text.Json; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Craftimizer.Simulator.Actions; +using Craftimizer.Simulator; +using Craftimizer.Solver; + +namespace Craftimizer.Utils; + +public sealed class CommunityMacros +{ + public sealed record BooleanValue + { + [JsonPropertyName("booleanValue")] + [JsonRequired] + public required bool Value { get; set; } + + public static implicit operator bool(BooleanValue v) => v.Value; + } + + 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] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public required int Value { get; set; } + + public static implicit operator int(IntegerValue v) => v.Value; + } + + public sealed record MapValue + { + 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 v) => v.Value; + } + + public sealed record ArrayValue + { + public sealed record ValueData + { + public T[]? Values { get; set; } + } + + [JsonPropertyName("arrayValue")] + [JsonRequired] + public required ValueData Data { get; set; } + + public T[] Value => Data.Values ?? Array.Empty(); + public static implicit operator T[](ArrayValue v) => v.Value; + } + + public sealed record ErrorData + { + public required int Code { get; set; } + public required string Message { get; set; } + public required string Status { get; set; } + } + + public sealed record StructuredQuery + { + [JsonRequired] + public required List From { get; set; } + [JsonRequired] + public required Filter Where { get; set; } + [JsonRequired] + public required List OrderBy { get; set; } + } + + public sealed record CollectionSelector + { + public required string CollectionId { get; set; } + } + + public sealed record Filter + { + public CompositeFilter? CompositeFilter { get; set; } + public FieldFilter? FieldFilter { get; set; } + } + + public sealed record CompositeFilter + { + [JsonRequired] + public required List Filters { get; set; } + [JsonRequired] + public required CompositeOperator Op { get; set; } + } + + public enum CompositeOperator + { + OPERATOR_UNSPECIFIED, + AND, + OR + } + + public sealed record FieldFilter + { + [JsonRequired] + public required FieldReference Field { get; set; } + [JsonRequired] + public required FieldOperator Op { get; set; } + public object? Value { get; set; } + } + + public enum FieldOperator + { + OPERATOR_UNSPECIFIED, + LESS_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + EQUAL, + NOT_EQUAL, + ARRAY_CONTAINS, + IN, + ARRAY_CONTAINS_ANY, + NOT_IN + } + + public sealed record Order + { + [JsonRequired] + public required FieldReference Field { get; set; } + [JsonRequired] + public required Direction Direction { get; set; } + } + + public sealed record FieldReference + { + [JsonRequired] + public required string FieldPath { get; set; } + } + + public enum Direction + { + DIRECTION_UNSPECIFIED, + ASCENDING, + DESCENDING + } + + private sealed record RunQueryRequest + { + [JsonRequired] + public required StructuredQuery StructuredQuery { get; set; } + } + + public sealed record TeamcraftMacro + { + public sealed record RecipeFieldData + { + [JsonRequired] + [JsonPropertyName("rlvl")] + 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 Rotation { get; set; } + public MapValue? Recipe { get; set; } + } + + public FieldData? Fields { get; set; } + + public ErrorData? Error { get; set; } + } + + private sealed record QueriedTeamcraftMacro + { + public TeamcraftMacro? Document { get; set; } + + public DateTimeOffset? ReadTime { get; set; } + + public ErrorData? Error { get; set; } + } + + public sealed record CraftingwayMacro + { + public int Id { get; set; } + public string? Slug { get; set; } + public string? Version { get; set; } + public string? Job { get; set; } + [JsonPropertyName("job_level")] + public int JobLevel { get; set; } + public int Craftsmanship { get; set; } + public int Control { get; set; } + public int CP { get; set; } + public string? Food { get; set; } + public string? Potion { get; set; } + [JsonPropertyName("recipe_job_level")] + public int RecipeJobLevel { get; set; } + public string? Recipe { get; set; } + // HqIngredients + public string? Actions { get; set; } + [JsonPropertyName("created_at")] + public long CreatedAt { get; set; } + public string? Error { get; set; } + } + + public sealed record CommunityMacro + { + public string Name { get; } + public IReadOnlyList Actions { get; } + + public CommunityMacro(TeamcraftMacro macro) + { + if (macro.Fields is not { } rotation) + throw new Exception($"Internal error; No fields were returned"); + + // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49 + 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(); + 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" => null, + // Old actions? + _ => null + }; + if (actionType.HasValue) + actions.Add(actionType.Value); + } + + Actions = actions; + } + + public CommunityMacro(CraftingwayMacro macro) + { + if (macro.Actions is not { } rotation) + throw new Exception($"Internal error; No actions were returned"); + + // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49 + Name = macro.Slug ?? "New Craftinway Rotation"; + var actions = new List(); + foreach (var action in rotation.Split(',')) + { + ActionType? actionType = action switch + { + "BasicSynthesis" => ActionType.BasicSynthesis, + "BasicTouch" => ActionType.BasicTouch, + "MastersMend" => ActionType.MastersMend, + "Observe" => ActionType.Observe, + "WasteNot" => ActionType.WasteNot, + "Veneration" => ActionType.Veneration, + "StandardTouch" => ActionType.StandardTouch, + "GreatStrides" => ActionType.GreatStrides, + "Innovation" => ActionType.Innovation, + "BasicSynthesisTraited" => ActionType.BasicSynthesis, + "WasteNotII" => ActionType.WasteNot2, + "ByregotsBlessing" => ActionType.ByregotsBlessing, + "MuscleMemory" => ActionType.MuscleMemory, + "CarefulSynthesis" => ActionType.CarefulSynthesis, + "Manipulation" => ActionType.Manipulation, + "PrudentTouch" => ActionType.PrudentTouch, + "FocusedSynthesis" => ActionType.FocusedSynthesis, + "FocusedTouch" => ActionType.FocusedTouch, + "Reflect" => ActionType.Reflect, + "PreparatoryTouch" => ActionType.PreparatoryTouch, + "Groundwork" => ActionType.Groundwork, + "DelicateSynthesis" => ActionType.DelicateSynthesis, + "TrainedEye" => ActionType.TrainedEye, + "CarefulSynthesisTraited" => ActionType.CarefulSynthesis, + "AdvancedTouch" => ActionType.AdvancedTouch, + "GroundworkTraited" => ActionType.Groundwork, + "PrudentSynthesis" => ActionType.PrudentSynthesis, + "TrainedFinesse" => ActionType.TrainedFinesse, + { } actionValue => throw new Exception($"Unknown action {actionValue}"), + }; + if (actionType.HasValue) + actions.Add(actionType.Value); + } + Actions = actions; + } + + public (float Score, SimulationState FinalState) CalculateScore(SimulatorNoRandom simulator, in SimulationState startingState, in MCTSConfig mctsConfig) + { + return CalculateScore(Actions, simulator, startingState, mctsConfig); + } + + public static (float Score, SimulationState FinalState) CalculateScore(IReadOnlyCollection actions, SimulatorNoRandom simulator, in SimulationState startingState, in MCTSConfig mctsConfig) + { + var (resp, outState, failedIdx) = simulator.ExecuteMultiple(startingState, actions); + outState.ActionCount = actions.Count; + var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; + if (resp != ActionResponse.SimulationComplete) + { + if (failedIdx != -1) + score /= 2; + } + return (score, outState); + } + } + + private Dictionary> CachedRotations { get; } = new(); + + public async Task> RetrieveRotations(int rlvl, CancellationToken token) + { + lock (CachedRotations) + { + if (CachedRotations.TryGetValue(rlvl, out var cachedMacros)) + return cachedMacros; + } + + var tcMacros = await RetrieveRotationsInternal(rlvl, token).ConfigureAwait(false); + var macros = tcMacros.Select(macro => new CommunityMacro(macro)).ToList(); + lock (CachedRotations) + CachedRotations.TryAdd(rlvl, macros); + return macros; + } + + private static async Task> RetrieveRotationsInternal(int rlvl, CancellationToken token) + { + using var heCallback = new HappyEyeballsCallback(); + using var client = new HttpClient(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = heCallback.ConnectCallback, + }); + + var request = new RunQueryRequest + { + StructuredQuery = new StructuredQuery + { + From = new List + { + new() { CollectionId = "rotations" } + }, + Where = new Filter + { + CompositeFilter = new CompositeFilter + { + Op = CompositeOperator.AND, + Filters = new List + { + new() + { + FieldFilter = new FieldFilter + { + Field = new FieldReference { FieldPath = "public" }, + Op = FieldOperator.EQUAL, + Value = new BooleanValue { Value = true } + } + }, + new() + { + FieldFilter = new FieldFilter + { + Field = new FieldReference { FieldPath = "community.rlvl" }, + Op = FieldOperator.EQUAL, + Value = new IntegerValue { Value = rlvl } + } + } + } + }, + }, + OrderBy = new List + { + new() + { + Field = new FieldReference { FieldPath = "xivVersion" }, + Direction = Direction.DESCENDING + }, + new() + { + Field = new FieldReference { FieldPath = "__name__" }, + Direction = Direction.DESCENDING + } + } + }, + }; + + var resp = await PostFromJsonAsync>( + client, + $"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents:runQuery", + request, new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }, token). + ConfigureAwait(false); + if (resp is null) + throw new Exception("Internal server error; failed to retrieve macro"); + + foreach(var macro in resp) + { + if (macro.Error is { } error) + throw new Exception($"Internal server error ({error.Status}); {error.Message}"); + } + + return resp.Where(macro => macro.Document is not null).Select(macro => macro.Document!).ToList(); + } + + private static async Task PostFromJsonAsync(HttpClient client, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, TRequest value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client is null) + throw new ArgumentNullException(nameof(client)); + + var resp = client.PostAsJsonAsync(requestUri, value, options, cancellationToken); + using var message = await resp.ConfigureAwait(false); + message.EnsureSuccessStatusCode(); + + return await message.Content!.ReadFromJsonAsync(options, cancellationToken).ConfigureAwait(false); + } +} diff --git a/Craftimizer/Utils/MacroImport.cs b/Craftimizer/Utils/MacroImport.cs index 6b9d5b8..785cfde 100644 --- a/Craftimizer/Utils/MacroImport.cs +++ b/Craftimizer/Utils/MacroImport.cs @@ -12,6 +12,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using static Craftimizer.Utils.CommunityMacros; namespace Craftimizer.Utils; @@ -77,7 +78,7 @@ public static class MacroImport return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app"; } - public static Task RetrieveUrl(string url, CancellationToken token) + public static Task RetrieveUrl(string url, CancellationToken token) { if (!TryParseUrl(url, out var uri)) throw new ArgumentException("Unsupported url", nameof(url)); @@ -93,112 +94,7 @@ public static class MacroImport } } - 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 - { - 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 v) => v.Value; - } - - public sealed record ArrayValue - { - 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 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 Rotation { get; set; } - public MapValue? 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; } - } - - private sealed record CraftingwayMacro - { - public int Id { get; set; } - public string? Slug { get; set; } - public string? Version { get; set; } - public string? Job { get; set; } - [JsonPropertyName("job_level")] - public int JobLevel { get; set; } - public int Craftsmanship { get; set; } - public int Control { get; set; } - public int CP { get; set; } - public string? Food { get; set; } - public string? Potion { get; set; } - [JsonPropertyName("recipe_job_level")] - public int RecipeJobLevel { get; set; } - public string? Recipe { get; set; } - // HqIngredients - public string? Actions { get; set; } - [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } - public string? Error { get; set; } - } - - public readonly record struct RetrievedMacro(string Name, IReadOnlyList Actions); - - private static async Task RetrieveTeamcraftUrl(Uri uri, CancellationToken token) + private static async Task RetrieveTeamcraftUrl(Uri uri, CancellationToken token) { using var heCallback = new HappyEyeballsCallback(); using var client = new HttpClient(new SocketsHttpHandler @@ -226,62 +122,10 @@ public static class MacroImport 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(); - 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); + return new(resp); } - private static async Task RetrieveCraftingwayUrl(Uri uri, CancellationToken token) + private static async Task RetrieveCraftingwayUrl(Uri uri, CancellationToken token) { using var heCallback = new HappyEyeballsCallback(); using var client = new HttpClient(new SocketsHttpHandler @@ -311,48 +155,7 @@ public static class MacroImport throw new Exception("Internal error; failed to retrieve macro"); if (resp.Error is { } error) throw new Exception($"Internal server error; {error}"); - if (resp.Actions is not { } rotation) - throw new Exception($"Internal error; No actions 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 = resp.Slug ?? "New Craftinway Rotation"; - var actions = new List(); - foreach (var action in resp.Actions.Split(',')) - { - ActionType? actionType = action switch - { - "BasicSynthesis" => ActionType.BasicSynthesis, - "BasicTouch" => ActionType.BasicTouch, - "MastersMend" => ActionType.MastersMend, - "Observe" => ActionType.Observe, - "WasteNot" => ActionType.WasteNot, - "Veneration" => ActionType.Veneration, - "StandardTouch" => ActionType.StandardTouch, - "GreatStrides" => ActionType.GreatStrides, - "Innovation" => ActionType.Innovation, - "BasicSynthesisTraited" => ActionType.BasicSynthesis, - "WasteNotII" => ActionType.WasteNot2, - "ByregotsBlessing" => ActionType.ByregotsBlessing, - "MuscleMemory" => ActionType.MuscleMemory, - "CarefulSynthesis" => ActionType.CarefulSynthesis, - "Manipulation" => ActionType.Manipulation, - "PrudentTouch" => ActionType.PrudentTouch, - "FocusedSynthesis" => ActionType.FocusedSynthesis, - "FocusedTouch" => ActionType.FocusedTouch, - "Reflect" => ActionType.Reflect, - "PreparatoryTouch" => ActionType.PreparatoryTouch, - "Groundwork" => ActionType.Groundwork, - "DelicateSynthesis" => ActionType.DelicateSynthesis, - "TrainedEye" => ActionType.TrainedEye, - "CarefulSynthesisTraited" => ActionType.CarefulSynthesis, - "AdvancedTouch" => ActionType.AdvancedTouch, - "GroundworkTraited" => ActionType.Groundwork, - "PrudentSynthesis" => ActionType.PrudentSynthesis, - "TrainedFinesse" => ActionType.TrainedFinesse, - { } actionValue => throw new Exception($"Unknown action {actionValue}"), - }; - if (actionType.HasValue) - actions.Add(actionType.Value); - } - return new(name, actions); + + return new(resp); } } diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 499fed6..35ebc80 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -109,7 +109,7 @@ public sealed class MacroEditor : Window, IDisposable private string popupImportUrl = string.Empty; private string popupImportError = string.Empty; private CancellationTokenSource? popupImportUrlTokenSource; - private MacroImport.RetrievedMacro? popupImportUrlMacro; + private CommunityMacros.CommunityMacro? popupImportUrlMacro; public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable actions, Action>? setter) : base("Craftimizer Macro Editor", WindowFlags) { diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 31be226..c2bf45c 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -83,10 +83,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable { var token = TokenSource.Token; var task = Task.Run(() => Result = Func(token), token); - _ = task.ContinueWith(t => - { - Completed = true; - }); + _ = task.ContinueWith(t => Completed = true); _ = task.ContinueWith(t => { if (token.IsCancellationRequested) @@ -113,6 +110,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable private BackgroundTask<(Macro?, SimulationState?)>? SavedMacroTask { get; set; } private BackgroundTask? SuggestedMacroTask { get; set; } + private BackgroundTask<(CommunityMacros.CommunityMacro?, SimulationState?)>? CommunityMacroTask { get; set; } private Solver.Solver? BestMacroSolver { get; set; } public bool HasSavedMacro { get; private set; } @@ -168,18 +166,33 @@ public sealed unsafe class RecipeNote : Window, IDisposable { SavedMacroTask?.Cancel(); SuggestedMacroTask?.Cancel(); + CommunityMacroTask?.Cancel(); } else if (CraftStatus == CraftableStatus.OK) { + // If it didn't exist before or it already ran, we need to recalculate if (SavedMacroTask?.Result == null && (SavedMacroTask?.Completed ?? true)) CalculateSavedMacro(); + + // If it didn't exist before or it already ran, we need to recalculate if (Service.Configuration.SuggestMacroAutomatically && SuggestedMacroTask?.Result == null && (SuggestedMacroTask?.Completed ?? true)) CalculateSuggestedMacro(); + // If we don't want to suggest automatically, we should cancel and clean out the task else { SuggestedMacroTask?.Cancel(); SuggestedMacroTask = null; } + + // If it didn't exist before or it already ran, we need to recalculate + if (Service.Configuration.ShowCommunityMacros && Service.Configuration.SearchCommunityMacroAutomatically && CommunityMacroTask?.Result == null && (CommunityMacroTask?.Completed ?? true)) + CalculateCommunityMacro(); + // If we don't want to search automatically, we should cancel and clean out the task + else + { + CommunityMacroTask?.Cancel(); + CommunityMacroTask = null; + } } } @@ -254,14 +267,28 @@ public sealed unsafe class RecipeNote : Window, IDisposable if (StatsChanged && CraftStatus == CraftableStatus.OK) { + // Stats changed and we are still craftable, so we need to recalculate CalculateSavedMacro(); + + // If we want to suggest automatically, we should recalculate if (Service.Configuration.SuggestMacroAutomatically) CalculateSuggestedMacro(); + // Otherwise, we should cancel and clean out the task else { SuggestedMacroTask?.Cancel(); SuggestedMacroTask = null; } + + // If we want to search automatically, we should recalculate + if (Service.Configuration.ShowCommunityMacros && Service.Configuration.SearchCommunityMacroAutomatically) + CalculateCommunityMacro(); + // Otherwise, we should cancel and clean out the task + else + { + CommunityMacroTask?.Cancel(); + CommunityMacroTask = null; + } } return true; @@ -324,25 +351,53 @@ public sealed unsafe class RecipeNote : Window, IDisposable ImGui.Separator(); var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2; - using (var panel = ImRaii2.GroupPanel("Best Saved Macro", panelWidth, out _)) + { - var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; - if (SavedMacroTask?.Result is { } savedMacro && savedMacro.Item1 != null && savedMacro.Item2 != null) + var macroTaskResult = SavedMacroTask?.Result; + var state = new MacroTaskState() { - ImGuiUtils.TextCentered(savedMacro.Item1.Name, panelWidth); - DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2.Value), SavedMacroTask.Exception, null, a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsPanelWidthOffset, true); - } - else - DrawMacro(null, SavedMacroTask?.Exception, null, null, stepsPanelWidthOffset, true); + Type = MacroTaskType.Saved, + Exception = SavedMacroTask?.Exception, + Started = SavedMacroTask != null, + Completed = SavedMacroTask?.Completed ?? false, + Actions = macroTaskResult?.Item1?.Actions, + MacroName = macroTaskResult?.Item1?.Name, + State = macroTaskResult?.Item2, + }; + if (macroTaskResult is { } macro && macro.Item1 is { } savedMacro) + state.MacroEditorSetter = a => { savedMacro.ActionEnumerable = a; Service.Configuration.Save(); }; + DrawMacro(in state, panelWidth); } - using (var panel = ImRaii2.GroupPanel("Suggested Macro", panelWidth, out _)) { - var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; - if (SuggestedMacroTask?.Result is { } suggestedMacro) - DrawMacro((suggestedMacro.Actions, suggestedMacro.State), SuggestedMacroTask.Exception, null, null, stepsPanelWidthOffset, false); - else - DrawMacro(null, SuggestedMacroTask?.Exception, BestMacroSolver, null, stepsPanelWidthOffset, false); + var macroTaskResult = SuggestedMacroTask?.Result; + var state = new MacroTaskState() + { + Type = MacroTaskType.Suggested, + Exception = SuggestedMacroTask?.Exception, + Started = SuggestedMacroTask != null, + Completed = SuggestedMacroTask?.Completed ?? false, + Actions = macroTaskResult?.Actions, + State = macroTaskResult?.State, + Solver = BestMacroSolver, + }; + DrawMacro(in state, panelWidth); + } + + if (Service.Configuration.ShowCommunityMacros) + { + var macroTaskResult = CommunityMacroTask?.Result; + var state = new MacroTaskState() + { + Type = MacroTaskType.Community, + Exception = CommunityMacroTask?.Exception, + Started = CommunityMacroTask != null, + Completed = CommunityMacroTask?.Completed ?? false, + Actions = macroTaskResult?.Item1?.Actions, + MacroName = macroTaskResult?.Item1?.Name, + State = macroTaskResult?.Item2, + }; + DrawMacro(in state, panelWidth); } ImGuiHelpers.ScaledDummy(5); @@ -647,191 +702,270 @@ public sealed unsafe class RecipeNote : Window, IDisposable } } - private void DrawMacro((IReadOnlyList Actions, SimulationState State)? macroValue, Exception? exception, Solver.Solver? solver, Action>? setter, float stepsAvailWidthOffset, bool isSavedMacro) + private enum MacroTaskType { + Saved, + Suggested, + Community + } + private record struct MacroTaskState + { + public MacroTaskType Type; + public Exception? Exception; + public bool Started; + public bool Completed; + public IReadOnlyList? Actions; + public string? MacroName; + public SimulationState? State; + public Solver.Solver? Solver; + public Action>? MacroEditorSetter; + } + + private void DrawMacro(in MacroTaskState state, float panelWidth) + { + var panelTitle = state.Type switch + { + MacroTaskType.Saved => "Best Saved Macro", + MacroTaskType.Suggested => "Suggested Macro", + MacroTaskType.Community => "Best Community Macro", + _ => throw new ArgumentOutOfRangeException(nameof(state), "state.Type must have a valid type") + }; + + using var panel = ImRaii2.GroupPanel(panelTitle, panelWidth, out _); + if (!panel) + return; + + var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; + var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing(); - if (macroValue is not { } macro) + if (!state.Started) { - if (isSavedMacro && !HasSavedMacro) - ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); - else if (exception == null) + switch (state.Type) { - if (solver != null && SuggestedMacroTask != null) - { - var calcTextSize = ImGui.CalcTextSize("Calculating..."); - var spacing = ImGui.GetStyle().ItemSpacing.X; - var fraction = Math.Clamp((float)solver.ProgressValue / solver.ProgressMax, 0, 1); - var progressColors = Colors.GetSolverProgressColors(solver.ProgressStage); - - var c = ImGui.GetCursorPos(); - ImGuiUtils.AlignCentered(windowHeight + spacing + calcTextSize.X, ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset); - - ImGuiUtils.ArcProgress( - fraction, - windowHeight / 2f, - .5f, - ImGui.ColorConvertFloat4ToU32(progressColors.Background), - ImGui.ColorConvertFloat4ToU32(progressColors.Foreground)); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Solver Progress: {solver.ProgressValue} / {solver.ProgressMax}"); - - ImGui.SameLine(0, spacing); - - ImGuiUtils.AlignMiddle(calcTextSize, new(calcTextSize.X, windowHeight)); - ImGui.Text("Calculating..."); - ImGui.SetCursorPos(c + new Vector2(0, windowHeight + ImGui.GetStyle().ItemSpacing.Y - 1)); - } - else - { - using var _padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ImGui.GetStyle().FramePadding * 2); - var size = ImGui.CalcTextSize("Generate") + ImGui.GetStyle().FramePadding * 2; - var c = ImGui.GetCursorPos(); - var availSize = new Vector2(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight); - ImGuiUtils.AlignMiddle(size, availSize); - using var _disabled = ImRaii.Disabled(!(SuggestedMacroTask?.Completed) ?? false); - if (ImGui.Button("Generate")) - CalculateSuggestedMacro(); - ImGui.SetCursorPos(c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y - 1)); - } + case MacroTaskType.Saved: + throw new InvalidOperationException("Saved macro window should always be started or completed"); + case MacroTaskType.Suggested: + { + using var _padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ImGui.GetStyle().FramePadding * 2); + var size = ImGui.CalcTextSize("Generate") + ImGui.GetStyle().FramePadding * 2; + var c = ImGui.GetCursorPos(); + var availSize = new Vector2(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight); + ImGuiUtils.AlignMiddle(size, availSize); + if (ImGui.Button("Generate")) + CalculateSuggestedMacro(); + ImGui.SetCursorPos(c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y - 1)); + break; + } + case MacroTaskType.Community: + { + using var _padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ImGui.GetStyle().FramePadding * 2); + var size = ImGui.CalcTextSize("Search Online") + ImGui.GetStyle().FramePadding * 2; + var c = ImGui.GetCursorPos(); + var availSize = new Vector2(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight); + ImGuiUtils.AlignMiddle(size, availSize); + if (ImGui.Button("Search Online")) + CalculateCommunityMacro(); + ImGui.SetCursorPos(c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y - 1)); + break; + } } - else - { - ImGui.AlignTextToFramePadding(); - using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) - ImGuiUtils.TextCentered("An exception occurred"); - if (ImGuiUtils.ButtonCentered("Copy Error Message")) - ImGui.SetClipboardText(exception.ToString()); - } - return; } - if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo)) - throw new InvalidOperationException("Combo actions should be sanitized away"); - - using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); - if (table) + else if (!state.Completed) { - ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, 0); - ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0); - ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch); - - ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight); - ImGui.TableNextColumn(); - - var spacing = ImGui.GetStyle().ItemSpacing.Y; - var miniRowHeight = (windowHeight - spacing) / 2f; - + switch (state.Type) { - if (Service.Configuration.ShowOptimalMacroStat) - { - var progressHeight = windowHeight; - if (macro.State.Progress >= macro.State.Input.Recipe.MaxProgress && macro.State.Input.Recipe.MaxQuality > 0) + case MacroTaskType.Saved: + ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); + break; + case MacroTaskType.Suggested: { + if (state.Solver is not { } solver) + throw new ArgumentNullException(nameof(state), "Solver should not be null"); + + var calcTextSize = ImGui.CalcTextSize("Calculating..."); + var spacing = ImGui.GetStyle().ItemSpacing.X; + var fraction = Math.Clamp((float)solver.ProgressValue / solver.ProgressMax, 0, 1); + var progressColors = Colors.GetSolverProgressColors(solver.ProgressStage); + + var c = ImGui.GetCursorPos(); + ImGuiUtils.AlignCentered(windowHeight + spacing + calcTextSize.X, ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset); + ImGuiUtils.ArcProgress( - (float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality, - progressHeight / 2f, - .5f, - ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Colors.Quality)); + fraction, + windowHeight / 2f + 2, + .5f, + ImGui.ColorConvertFloat4ToU32(progressColors.Background), + ImGui.ColorConvertFloat4ToU32(progressColors.Foreground)); if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); - } - else - { - ImGuiUtils.ArcProgress( - (float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress, - progressHeight / 2f, - .5f, - ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Colors.Progress)); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); - } - } - else - { - ImGuiUtils.ArcProgress( - (float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress, - miniRowHeight / 2f, - .5f, - ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Colors.Progress)); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); + ImGuiUtils.Tooltip($"Solver Progress: {solver.ProgressValue} / {solver.ProgressMax}"); - ImGui.SameLine(0, spacing); - ImGuiUtils.ArcProgress( - (float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality, - miniRowHeight / 2f, - .5f, - ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Colors.Quality)); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); - - ImGuiUtils.ArcProgress((float)macro.State.Durability / macro.State.Input.Recipe.MaxDurability, - miniRowHeight / 2f, - .5f, - ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Colors.Durability)); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}"); - - ImGui.SameLine(0, spacing); - ImGuiUtils.ArcProgress( - (float)macro.State.CP / macro.State.Input.Stats.CP, - miniRowHeight / 2f, - .5f, - ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Colors.CP)); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}"); - } - } - - ImGui.TableNextColumn(); - { - if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Edit, miniRowHeight)) - Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), macro.Actions, setter); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip("Open in Simulator"); - if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste, miniRowHeight)) - Service.Plugin.CopyMacro(macro.Actions); - if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip("Copy to Clipboard"); - } - - ImGui.TableNextColumn(); - { - var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing)); - var itemCount = macro.Actions.Count; - for (var i = 0; i < itemsPerRow * 2; i++) - { - if (i % itemsPerRow != 0) ImGui.SameLine(0, spacing); - if (i < itemCount) + + ImGuiUtils.AlignMiddle(calcTextSize, new(calcTextSize.X, windowHeight)); + ImGui.Text("Calculating..."); + ImGui.SetCursorPos(c + new Vector2(0, windowHeight + ImGui.GetStyle().ItemSpacing.Y - 1)); + break; + } + case MacroTaskType.Community: + ImGuiUtils.TextMiddleNewLine("Searching...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); + break; + } + } + else if (state.Exception != null) + { + ImGui.AlignTextToFramePadding(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + ImGuiUtils.TextCentered("An exception occurred"); + if (ImGuiUtils.ButtonCentered("Copy Error Message")) + ImGui.SetClipboardText(state.Exception.ToString()); + } + else if (state.Actions is not { } actions || state.State is not { } simState) + { + switch (state.Type) + { + case MacroTaskType.Saved: + ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); + break; + case MacroTaskType.Suggested: + throw new ArgumentNullException(nameof(state), "Actions or State should not be null"); + case MacroTaskType.Community: + ImGuiUtils.TextMiddleNewLine("No macros found!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); + break; + } + } + else + { + if (actions.Any(a => a.Category() == ActionCategory.Combo)) + throw new InvalidOperationException("Combo actions should be sanitized away"); + + if (state.MacroName is { } macroName) + ImGuiUtils.TextCentered(macroName, panelWidth); + + using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); + if (table) + { + ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight); + ImGui.TableNextColumn(); + + var spacing = ImGui.GetStyle().ItemSpacing.Y; + var miniRowHeight = (windowHeight - spacing) / 2f; + + { + if (Service.Configuration.ShowOptimalMacroStat) { - var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount; - if (!shouldShowMore) + var progressHeight = windowHeight; + if (simState.Progress >= simState.Input.Recipe.MaxProgress && simState.Input.Recipe.MaxQuality > 0) { - ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight)); + ImGuiUtils.ArcProgress( + (float)simState.Quality / simState.Input.Recipe.MaxQuality, + progressHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Quality)); if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); + ImGuiUtils.Tooltip($"Quality: {simState.Quality} / {simState.Input.Recipe.MaxQuality}"); } else { - var amtMore = itemCount - itemsPerRow * 2; - var pos = ImGui.GetCursorPos(); - ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f)); + ImGuiUtils.ArcProgress( + (float)simState.Progress / simState.Input.Recipe.MaxProgress, + progressHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Progress)); if (ImGui.IsItemHovered()) - ImGuiUtils.Tooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"); - ImGui.SetCursorPos(pos); - ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f); - ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null); + ImGuiUtils.Tooltip($"Progress: {simState.Progress} / {simState.Input.Recipe.MaxProgress}"); } } else - ImGui.Dummy(new(miniRowHeight)); + { + ImGuiUtils.ArcProgress( + (float)simState.Progress / simState.Input.Recipe.MaxProgress, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Progress)); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip($"Progress: {simState.Progress} / {simState.Input.Recipe.MaxProgress}"); + + ImGui.SameLine(0, spacing); + ImGuiUtils.ArcProgress( + (float)simState.Quality / simState.Input.Recipe.MaxQuality, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Quality)); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip($"Quality: {simState.Quality} / {simState.Input.Recipe.MaxQuality}"); + ImGuiUtils.ArcProgress((float)simState.Durability / simState.Input.Recipe.MaxDurability, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Durability)); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip($"Remaining Durability: {simState.Durability} / {simState.Input.Recipe.MaxDurability}"); + + ImGui.SameLine(0, spacing); + ImGuiUtils.ArcProgress( + (float)simState.CP / simState.Input.Stats.CP, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.CP)); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip($"Remaining CP: {simState.CP} / {simState.Input.Stats.CP}"); + } + } + + ImGui.TableNextColumn(); + { + if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Edit, miniRowHeight)) + Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), actions, state.MacroEditorSetter); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip("Open in Simulator"); + if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste, miniRowHeight)) + Service.Plugin.CopyMacro(actions); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip("Copy to Clipboard"); + } + + ImGui.TableNextColumn(); + { + var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing)); + var itemCount = actions.Count; + for (var i = 0; i < itemsPerRow * 2; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + if (i < itemCount) + { + var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount; + if (!shouldShowMore) + { + ImGui.Image(actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight)); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip(actions[i].GetName(RecipeData!.ClassJob)); + } + else + { + var amtMore = itemCount - itemsPerRow * 2; + var pos = ImGui.GetCursorPos(); + ImGui.Image(actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f)); + if (ImGui.IsItemHovered()) + ImGuiUtils.Tooltip($"{actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"); + ImGui.SetCursorPos(pos); + ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f); + ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null); + } + } + else + ImGui.Dummy(new(miniRowHeight)); + } } } } @@ -947,20 +1081,12 @@ public sealed unsafe class RecipeNote : Window, IDisposable token.ThrowIfCancellationRequested(); - HasSavedMacro = macros.Count > 0; - if (!HasSavedMacro) + if (macros.Count == 0) return (null, null); var bestSaved = macros .Select(macro => { - var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions); - outState.ActionCount = macro.Actions.Count; - var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; - if (resp != ActionResponse.SimulationComplete) - { - if (failedIdx != -1) - score /= 2; - } + var (score, outState) = CommunityMacros.CommunityMacro.CalculateScore(macro.Actions, simulator, in state, in mctsConfig); return (macro, outState, score); }) .MaxBy(m => m.score); @@ -996,10 +1122,42 @@ public sealed unsafe class RecipeNote : Window, IDisposable SuggestedMacroTask.Start(); } + public void CalculateCommunityMacro() + { + CommunityMacroTask?.Cancel(); + CommunityMacroTask = new(token => + { + var input = new SimulationInput(CharacterStats!, RecipeData!.RecipeInfo); + var state = new SimulationState(input); + var config = Service.Configuration.SimulatorSolverConfig; + var mctsConfig = new MCTSConfig(config); + var simulator = new SimulatorNoRandom(); + var macros = Service.CommunityMacros.RetrieveRotations(input.Recipe.RLvl, token).GetAwaiter().GetResult(); + + token.ThrowIfCancellationRequested(); + + if (macros.Count == 0) + return (null, null); + var bestSaved = macros + .Select(macro => + { + var (score, outState) = CommunityMacros.CommunityMacro.CalculateScore(macro.Actions, simulator, in state, in mctsConfig); + return (macro, outState, score); + }) + .MaxBy(m => m.score); + + token.ThrowIfCancellationRequested(); + + return (bestSaved.macro, bestSaved.outState); + }); + CommunityMacroTask.Start(); + } + public void Dispose() { - SavedMacroTask?.Cancel(); - SuggestedMacroTask?.Cancel(); + SavedMacroTask?.Dispose(); + SuggestedMacroTask?.Dispose(); + CommunityMacroTask?.Dispose(); Service.WindowSystem.RemoveWindow(this); AxisFont?.Dispose(); } diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 635bafc..c40bd15 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -204,6 +204,30 @@ public sealed class Settings : Window, IDisposable ref isDirty ); + DrawOption( + "Enable Community Macros in Crafting Log", + "Use FFXIV Teamcraft's community rotations to search for and find the best possible" + + "crowd-sourced macro for your craft. This sends a request to their servers to retrieve " + + "a list of macros that apply to your craft's rlvl. Requests are only sent once per rlvl " + + "and are always cached to reduce server load.", + Config.ShowCommunityMacros, + v => Config.ShowCommunityMacros = v, + ref isDirty + ); + + if (Config.ShowCommunityMacros) + { + DrawOption( + "Automatically Search for Community Macro", + "When navigating to a new recipe or changing your gear stats, automatically search " + + "online for a new community macro.\n" + + "This is turned off by default so you don't hammer their servers :)", + Config.SearchCommunityMacroAutomatically, + v => Config.SearchCommunityMacroAutomatically = v, + ref isDirty + ); + } + DrawOption( "Show Only One Macro Stat in Crafting Log", "Only one stat will be shown for a macro. If a craft will be finished, quality " +