Add community macro support
This commit is contained in:
@@ -93,6 +93,8 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool DisableSynthHelperOnMacro { get; set; } = true;
|
public bool DisableSynthHelperOnMacro { get; set; } = true;
|
||||||
public bool ShowOptimalMacroStat { get; set; } = true;
|
public bool ShowOptimalMacroStat { get; set; } = true;
|
||||||
public bool SuggestMacroAutomatically { get; set; }
|
public bool SuggestMacroAutomatically { get; set; }
|
||||||
|
public bool ShowCommunityMacros { get; set; } = true;
|
||||||
|
public bool SearchCommunityMacroAutomatically { get; set; }
|
||||||
public int SynthHelperStepCount { get; set; } = 5;
|
public int SynthHelperStepCount { get; set; } = 5;
|
||||||
|
|
||||||
public bool PinSynthHelperToWindow { get; set; } = true;
|
public bool PinSynthHelperToWindow { get; set; } = true;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public Hooks Hooks { get; }
|
public Hooks Hooks { get; }
|
||||||
public Chat Chat { get; }
|
public Chat Chat { get; }
|
||||||
public IconManager IconManager { get; }
|
public IconManager IconManager { get; }
|
||||||
|
public CommunityMacros CommunityMacros { get; }
|
||||||
|
|
||||||
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
|
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
Hooks = new();
|
Hooks = new();
|
||||||
Chat = new();
|
Chat = new();
|
||||||
IconManager = new();
|
IconManager = new();
|
||||||
|
CommunityMacros = new();
|
||||||
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0];
|
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0];
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public sealed class Service
|
|||||||
public static WindowSystem WindowSystem => Plugin.WindowSystem;
|
public static WindowSystem WindowSystem => Plugin.WindowSystem;
|
||||||
public static Chat Chat => Plugin.Chat;
|
public static Chat Chat => Plugin.Chat;
|
||||||
public static IconManager IconManager => Plugin.IconManager;
|
public static IconManager IconManager => Plugin.IconManager;
|
||||||
|
public static CommunityMacros CommunityMacros => Plugin.CommunityMacros;
|
||||||
#pragma warning restore CS8618
|
#pragma warning restore CS8618
|
||||||
|
|
||||||
internal static void Initialize(Plugin plugin, DalamudPluginInterface iface)
|
internal static void Initialize(Plugin plugin, DalamudPluginInterface iface)
|
||||||
|
|||||||
@@ -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<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
|
||||||
|
{
|
||||||
|
public T[]? Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("arrayValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required ValueData Data { get; set; }
|
||||||
|
|
||||||
|
public T[] Value => Data.Values ?? Array.Empty<T>();
|
||||||
|
public static implicit operator T[](ArrayValue<T> 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<CollectionSelector> From { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
public required Filter Where { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
public required List<Order> 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<Filter> 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<StringValue> Rotation { get; set; }
|
||||||
|
public MapValue<RecipeFieldData>? 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<ActionType> 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<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" => 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<ActionType>();
|
||||||
|
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<ActionType> 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<int, List<CommunityMacro>> CachedRotations { get; } = new();
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<CommunityMacro>> 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<List<TeamcraftMacro>> 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<CollectionSelector>
|
||||||
|
{
|
||||||
|
new() { CollectionId = "rotations" }
|
||||||
|
},
|
||||||
|
Where = new Filter
|
||||||
|
{
|
||||||
|
CompositeFilter = new CompositeFilter
|
||||||
|
{
|
||||||
|
Op = CompositeOperator.AND,
|
||||||
|
Filters = new List<Filter>
|
||||||
|
{
|
||||||
|
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<Order>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Field = new FieldReference { FieldPath = "xivVersion" },
|
||||||
|
Direction = Direction.DESCENDING
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Field = new FieldReference { FieldPath = "__name__" },
|
||||||
|
Direction = Direction.DESCENDING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var resp = await PostFromJsonAsync<RunQueryRequest, List<QueriedTeamcraftMacro>>(
|
||||||
|
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<TResponse?> PostFromJsonAsync<TRequest, TResponse>(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<TResponse>(options, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static Craftimizer.Utils.CommunityMacros;
|
||||||
|
|
||||||
namespace Craftimizer.Utils;
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ public static class MacroImport
|
|||||||
return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app";
|
return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<RetrievedMacro> RetrieveUrl(string url, CancellationToken token)
|
public static Task<CommunityMacro> RetrieveUrl(string url, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (!TryParseUrl(url, out var uri))
|
if (!TryParseUrl(url, out var uri))
|
||||||
throw new ArgumentException("Unsupported url", nameof(url));
|
throw new ArgumentException("Unsupported url", nameof(url));
|
||||||
@@ -93,112 +94,7 @@ public static class MacroImport
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record TeamcraftMacro
|
private static async Task<CommunityMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token)
|
||||||
{
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
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<ActionType> Actions);
|
|
||||||
|
|
||||||
private static async Task<RetrievedMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token)
|
|
||||||
{
|
{
|
||||||
using var heCallback = new HappyEyeballsCallback();
|
using var heCallback = new HappyEyeballsCallback();
|
||||||
using var client = new HttpClient(new SocketsHttpHandler
|
using var client = new HttpClient(new SocketsHttpHandler
|
||||||
@@ -226,62 +122,10 @@ public static class MacroImport
|
|||||||
throw new Exception("Internal error; failed to retrieve macro");
|
throw new Exception("Internal error; failed to retrieve macro");
|
||||||
if (resp.Error is { } error)
|
if (resp.Error is { } error)
|
||||||
throw new Exception($"Internal server error ({error.Status}); {error.Message}");
|
throw new Exception($"Internal server error ({error.Status}); {error.Message}");
|
||||||
if (resp.Fields is not { } rotation)
|
return new(resp);
|
||||||
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)
|
private static async Task<CommunityMacro> RetrieveCraftingwayUrl(Uri uri, CancellationToken token)
|
||||||
{
|
{
|
||||||
using var heCallback = new HappyEyeballsCallback();
|
using var heCallback = new HappyEyeballsCallback();
|
||||||
using var client = new HttpClient(new SocketsHttpHandler
|
using var client = new HttpClient(new SocketsHttpHandler
|
||||||
@@ -311,48 +155,7 @@ public static class MacroImport
|
|||||||
throw new Exception("Internal error; failed to retrieve macro");
|
throw new Exception("Internal error; failed to retrieve macro");
|
||||||
if (resp.Error is { } error)
|
if (resp.Error is { } error)
|
||||||
throw new Exception($"Internal server error; {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");
|
return new(resp);
|
||||||
// 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<ActionType>();
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public sealed class MacroEditor : Window, IDisposable
|
|||||||
private string popupImportUrl = string.Empty;
|
private string popupImportUrl = string.Empty;
|
||||||
private string popupImportError = string.Empty;
|
private string popupImportError = string.Empty;
|
||||||
private CancellationTokenSource? popupImportUrlTokenSource;
|
private CancellationTokenSource? popupImportUrlTokenSource;
|
||||||
private MacroImport.RetrievedMacro? popupImportUrlMacro;
|
private CommunityMacros.CommunityMacro? popupImportUrlMacro;
|
||||||
|
|
||||||
public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) : base("Craftimizer Macro Editor", WindowFlags)
|
public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) : base("Craftimizer Macro Editor", WindowFlags)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -83,10 +83,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
{
|
{
|
||||||
var token = TokenSource.Token;
|
var token = TokenSource.Token;
|
||||||
var task = Task.Run(() => Result = Func(token), token);
|
var task = Task.Run(() => Result = Func(token), token);
|
||||||
_ = task.ContinueWith(t =>
|
_ = task.ContinueWith(t => Completed = true);
|
||||||
{
|
|
||||||
Completed = true;
|
|
||||||
});
|
|
||||||
_ = task.ContinueWith(t =>
|
_ = task.ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
@@ -113,6 +110,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
|
|
||||||
private BackgroundTask<(Macro?, SimulationState?)>? SavedMacroTask { get; set; }
|
private BackgroundTask<(Macro?, SimulationState?)>? SavedMacroTask { get; set; }
|
||||||
private BackgroundTask<SolverSolution>? SuggestedMacroTask { get; set; }
|
private BackgroundTask<SolverSolution>? SuggestedMacroTask { get; set; }
|
||||||
|
private BackgroundTask<(CommunityMacros.CommunityMacro?, SimulationState?)>? CommunityMacroTask { get; set; }
|
||||||
|
|
||||||
private Solver.Solver? BestMacroSolver { get; set; }
|
private Solver.Solver? BestMacroSolver { get; set; }
|
||||||
public bool HasSavedMacro { get; private set; }
|
public bool HasSavedMacro { get; private set; }
|
||||||
@@ -168,18 +166,33 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
{
|
{
|
||||||
SavedMacroTask?.Cancel();
|
SavedMacroTask?.Cancel();
|
||||||
SuggestedMacroTask?.Cancel();
|
SuggestedMacroTask?.Cancel();
|
||||||
|
CommunityMacroTask?.Cancel();
|
||||||
}
|
}
|
||||||
else if (CraftStatus == CraftableStatus.OK)
|
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))
|
if (SavedMacroTask?.Result == null && (SavedMacroTask?.Completed ?? true))
|
||||||
CalculateSavedMacro();
|
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))
|
if (Service.Configuration.SuggestMacroAutomatically && SuggestedMacroTask?.Result == null && (SuggestedMacroTask?.Completed ?? true))
|
||||||
CalculateSuggestedMacro();
|
CalculateSuggestedMacro();
|
||||||
|
// If we don't want to suggest automatically, we should cancel and clean out the task
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SuggestedMacroTask?.Cancel();
|
SuggestedMacroTask?.Cancel();
|
||||||
SuggestedMacroTask = null;
|
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)
|
if (StatsChanged && CraftStatus == CraftableStatus.OK)
|
||||||
{
|
{
|
||||||
|
// Stats changed and we are still craftable, so we need to recalculate
|
||||||
CalculateSavedMacro();
|
CalculateSavedMacro();
|
||||||
|
|
||||||
|
// If we want to suggest automatically, we should recalculate
|
||||||
if (Service.Configuration.SuggestMacroAutomatically)
|
if (Service.Configuration.SuggestMacroAutomatically)
|
||||||
CalculateSuggestedMacro();
|
CalculateSuggestedMacro();
|
||||||
|
// Otherwise, we should cancel and clean out the task
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SuggestedMacroTask?.Cancel();
|
SuggestedMacroTask?.Cancel();
|
||||||
SuggestedMacroTask = null;
|
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;
|
return true;
|
||||||
@@ -324,25 +351,53 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2;
|
var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2;
|
||||||
using (var panel = ImRaii2.GroupPanel("Best Saved Macro", panelWidth, out _))
|
|
||||||
{
|
{
|
||||||
var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth;
|
var macroTaskResult = SavedMacroTask?.Result;
|
||||||
if (SavedMacroTask?.Result is { } savedMacro && savedMacro.Item1 != null && savedMacro.Item2 != null)
|
var state = new MacroTaskState()
|
||||||
{
|
{
|
||||||
ImGuiUtils.TextCentered(savedMacro.Item1.Name, panelWidth);
|
Type = MacroTaskType.Saved,
|
||||||
DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2.Value), SavedMacroTask.Exception, null, a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsPanelWidthOffset, true);
|
Exception = SavedMacroTask?.Exception,
|
||||||
}
|
Started = SavedMacroTask != null,
|
||||||
else
|
Completed = SavedMacroTask?.Completed ?? false,
|
||||||
DrawMacro(null, SavedMacroTask?.Exception, null, null, stepsPanelWidthOffset, true);
|
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;
|
var macroTaskResult = SuggestedMacroTask?.Result;
|
||||||
if (SuggestedMacroTask?.Result is { } suggestedMacro)
|
var state = new MacroTaskState()
|
||||||
DrawMacro((suggestedMacro.Actions, suggestedMacro.State), SuggestedMacroTask.Exception, null, null, stepsPanelWidthOffset, false);
|
{
|
||||||
else
|
Type = MacroTaskType.Suggested,
|
||||||
DrawMacro(null, SuggestedMacroTask?.Exception, BestMacroSolver, null, stepsPanelWidthOffset, false);
|
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);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
@@ -647,18 +702,87 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMacro((IReadOnlyList<ActionType> Actions, SimulationState State)? macroValue, Exception? exception, Solver.Solver? solver, Action<IEnumerable<ActionType>>? 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<ActionType>? Actions;
|
||||||
|
public string? MacroName;
|
||||||
|
public SimulationState? State;
|
||||||
|
public Solver.Solver? Solver;
|
||||||
|
public Action<IEnumerable<ActionType>>? 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();
|
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
if (macroValue is not { } macro)
|
if (!state.Started)
|
||||||
{
|
{
|
||||||
if (isSavedMacro && !HasSavedMacro)
|
switch (state.Type)
|
||||||
ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
|
|
||||||
else if (exception == null)
|
|
||||||
{
|
{
|
||||||
if (solver != null && SuggestedMacroTask != null)
|
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 if (!state.Completed)
|
||||||
|
{
|
||||||
|
switch (state.Type)
|
||||||
|
{
|
||||||
|
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 calcTextSize = ImGui.CalcTextSize("Calculating...");
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var fraction = Math.Clamp((float)solver.ProgressValue / solver.ProgressMax, 0, 1);
|
var fraction = Math.Clamp((float)solver.ProgressValue / solver.ProgressMax, 0, 1);
|
||||||
@@ -669,7 +793,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
|
|
||||||
ImGuiUtils.ArcProgress(
|
ImGuiUtils.ArcProgress(
|
||||||
fraction,
|
fraction,
|
||||||
windowHeight / 2f,
|
windowHeight / 2f + 2,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.ColorConvertFloat4ToU32(progressColors.Background),
|
ImGui.ColorConvertFloat4ToU32(progressColors.Background),
|
||||||
ImGui.ColorConvertFloat4ToU32(progressColors.Foreground));
|
ImGui.ColorConvertFloat4ToU32(progressColors.Foreground));
|
||||||
@@ -681,33 +805,43 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
ImGuiUtils.AlignMiddle(calcTextSize, new(calcTextSize.X, windowHeight));
|
ImGuiUtils.AlignMiddle(calcTextSize, new(calcTextSize.X, windowHeight));
|
||||||
ImGui.Text("Calculating...");
|
ImGui.Text("Calculating...");
|
||||||
ImGui.SetCursorPos(c + new Vector2(0, windowHeight + ImGui.GetStyle().ItemSpacing.Y - 1));
|
ImGui.SetCursorPos(c + new Vector2(0, windowHeight + ImGui.GetStyle().ItemSpacing.Y - 1));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else
|
case MacroTaskType.Community:
|
||||||
{
|
ImGuiUtils.TextMiddleNewLine("Searching...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
|
||||||
using var _padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, ImGui.GetStyle().FramePadding * 2);
|
break;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (state.Exception != null)
|
||||||
{
|
{
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
||||||
ImGuiUtils.TextCentered("An exception occurred");
|
ImGuiUtils.TextCentered("An exception occurred");
|
||||||
if (ImGuiUtils.ButtonCentered("Copy Error Message"))
|
if (ImGuiUtils.ButtonCentered("Copy Error Message"))
|
||||||
ImGui.SetClipboardText(exception.ToString());
|
ImGui.SetClipboardText(state.Exception.ToString());
|
||||||
}
|
}
|
||||||
return;
|
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;
|
||||||
}
|
}
|
||||||
if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo))
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (actions.Any(a => a.Category() == ActionCategory.Combo))
|
||||||
throw new InvalidOperationException("Combo actions should be sanitized away");
|
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);
|
using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
|
||||||
if (table)
|
if (table)
|
||||||
{
|
{
|
||||||
@@ -725,78 +859,77 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
if (Service.Configuration.ShowOptimalMacroStat)
|
if (Service.Configuration.ShowOptimalMacroStat)
|
||||||
{
|
{
|
||||||
var progressHeight = windowHeight;
|
var progressHeight = windowHeight;
|
||||||
if (macro.State.Progress >= macro.State.Input.Recipe.MaxProgress && macro.State.Input.Recipe.MaxQuality > 0)
|
if (simState.Progress >= simState.Input.Recipe.MaxProgress && simState.Input.Recipe.MaxQuality > 0)
|
||||||
{
|
{
|
||||||
ImGuiUtils.ArcProgress(
|
ImGuiUtils.ArcProgress(
|
||||||
(float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality,
|
(float)simState.Quality / simState.Input.Recipe.MaxQuality,
|
||||||
progressHeight / 2f,
|
progressHeight / 2f,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
ImGui.GetColorU32(Colors.Quality));
|
ImGui.GetColorU32(Colors.Quality));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
|
ImGuiUtils.Tooltip($"Quality: {simState.Quality} / {simState.Input.Recipe.MaxQuality}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGuiUtils.ArcProgress(
|
ImGuiUtils.ArcProgress(
|
||||||
(float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress,
|
(float)simState.Progress / simState.Input.Recipe.MaxProgress,
|
||||||
progressHeight / 2f,
|
progressHeight / 2f,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
ImGui.GetColorU32(Colors.Progress));
|
ImGui.GetColorU32(Colors.Progress));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
|
ImGuiUtils.Tooltip($"Progress: {simState.Progress} / {simState.Input.Recipe.MaxProgress}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGuiUtils.ArcProgress(
|
ImGuiUtils.ArcProgress(
|
||||||
(float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress,
|
(float)simState.Progress / simState.Input.Recipe.MaxProgress,
|
||||||
miniRowHeight / 2f,
|
miniRowHeight / 2f,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
ImGui.GetColorU32(Colors.Progress));
|
ImGui.GetColorU32(Colors.Progress));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
|
ImGuiUtils.Tooltip($"Progress: {simState.Progress} / {simState.Input.Recipe.MaxProgress}");
|
||||||
|
|
||||||
ImGui.SameLine(0, spacing);
|
ImGui.SameLine(0, spacing);
|
||||||
ImGuiUtils.ArcProgress(
|
ImGuiUtils.ArcProgress(
|
||||||
(float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality,
|
(float)simState.Quality / simState.Input.Recipe.MaxQuality,
|
||||||
miniRowHeight / 2f,
|
miniRowHeight / 2f,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
ImGui.GetColorU32(Colors.Quality));
|
ImGui.GetColorU32(Colors.Quality));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
|
ImGuiUtils.Tooltip($"Quality: {simState.Quality} / {simState.Input.Recipe.MaxQuality}");
|
||||||
|
ImGuiUtils.ArcProgress((float)simState.Durability / simState.Input.Recipe.MaxDurability,
|
||||||
ImGuiUtils.ArcProgress((float)macro.State.Durability / macro.State.Input.Recipe.MaxDurability,
|
|
||||||
miniRowHeight / 2f,
|
miniRowHeight / 2f,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
ImGui.GetColorU32(Colors.Durability));
|
ImGui.GetColorU32(Colors.Durability));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}");
|
ImGuiUtils.Tooltip($"Remaining Durability: {simState.Durability} / {simState.Input.Recipe.MaxDurability}");
|
||||||
|
|
||||||
ImGui.SameLine(0, spacing);
|
ImGui.SameLine(0, spacing);
|
||||||
ImGuiUtils.ArcProgress(
|
ImGuiUtils.ArcProgress(
|
||||||
(float)macro.State.CP / macro.State.Input.Stats.CP,
|
(float)simState.CP / simState.Input.Stats.CP,
|
||||||
miniRowHeight / 2f,
|
miniRowHeight / 2f,
|
||||||
.5f,
|
.5f,
|
||||||
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
ImGui.GetColorU32(Colors.CP));
|
ImGui.GetColorU32(Colors.CP));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}");
|
ImGuiUtils.Tooltip($"Remaining CP: {simState.CP} / {simState.Input.Stats.CP}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
{
|
{
|
||||||
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Edit, miniRowHeight))
|
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Edit, miniRowHeight))
|
||||||
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), macro.Actions, setter);
|
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), actions, state.MacroEditorSetter);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip("Open in Simulator");
|
ImGuiUtils.Tooltip("Open in Simulator");
|
||||||
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste, miniRowHeight))
|
if (ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste, miniRowHeight))
|
||||||
Service.Plugin.CopyMacro(macro.Actions);
|
Service.Plugin.CopyMacro(actions);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip("Copy to Clipboard");
|
ImGuiUtils.Tooltip("Copy to Clipboard");
|
||||||
}
|
}
|
||||||
@@ -804,7 +937,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
{
|
{
|
||||||
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
|
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
|
||||||
var itemCount = macro.Actions.Count;
|
var itemCount = actions.Count;
|
||||||
for (var i = 0; i < itemsPerRow * 2; i++)
|
for (var i = 0; i < itemsPerRow * 2; i++)
|
||||||
{
|
{
|
||||||
if (i % itemsPerRow != 0)
|
if (i % itemsPerRow != 0)
|
||||||
@@ -814,17 +947,17 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
|
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
|
||||||
if (!shouldShowMore)
|
if (!shouldShowMore)
|
||||||
{
|
{
|
||||||
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
|
ImGui.Image(actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
|
ImGuiUtils.Tooltip(actions[i].GetName(RecipeData!.ClassJob));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var amtMore = itemCount - itemsPerRow * 2;
|
var amtMore = itemCount - itemsPerRow * 2;
|
||||||
var pos = ImGui.GetCursorPos();
|
var pos = ImGui.GetCursorPos();
|
||||||
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f));
|
ImGui.Image(actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGuiUtils.Tooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more");
|
ImGuiUtils.Tooltip($"{actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more");
|
||||||
ImGui.SetCursorPos(pos);
|
ImGui.SetCursorPos(pos);
|
||||||
ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f);
|
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);
|
ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null);
|
||||||
@@ -836,6 +969,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void DrawRequiredStatsTable(int current, int required)
|
private static void DrawRequiredStatsTable(int current, int required)
|
||||||
{
|
{
|
||||||
@@ -947,20 +1081,12 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
HasSavedMacro = macros.Count > 0;
|
if (macros.Count == 0)
|
||||||
if (!HasSavedMacro)
|
|
||||||
return (null, null);
|
return (null, null);
|
||||||
var bestSaved = macros
|
var bestSaved = macros
|
||||||
.Select(macro =>
|
.Select(macro =>
|
||||||
{
|
{
|
||||||
var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions);
|
var (score, outState) = CommunityMacros.CommunityMacro.CalculateScore(macro.Actions, simulator, in state, in mctsConfig);
|
||||||
outState.ActionCount = macro.Actions.Count;
|
|
||||||
var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0;
|
|
||||||
if (resp != ActionResponse.SimulationComplete)
|
|
||||||
{
|
|
||||||
if (failedIdx != -1)
|
|
||||||
score /= 2;
|
|
||||||
}
|
|
||||||
return (macro, outState, score);
|
return (macro, outState, score);
|
||||||
})
|
})
|
||||||
.MaxBy(m => m.score);
|
.MaxBy(m => m.score);
|
||||||
@@ -996,10 +1122,42 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
SuggestedMacroTask.Start();
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SavedMacroTask?.Cancel();
|
SavedMacroTask?.Dispose();
|
||||||
SuggestedMacroTask?.Cancel();
|
SuggestedMacroTask?.Dispose();
|
||||||
|
CommunityMacroTask?.Dispose();
|
||||||
Service.WindowSystem.RemoveWindow(this);
|
Service.WindowSystem.RemoveWindow(this);
|
||||||
AxisFont?.Dispose();
|
AxisFont?.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,30 @@ public sealed class Settings : Window, IDisposable
|
|||||||
ref isDirty
|
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(
|
DrawOption(
|
||||||
"Show Only One Macro Stat in Crafting Log",
|
"Show Only One Macro Stat in Crafting Log",
|
||||||
"Only one stat will be shown for a macro. If a craft will be finished, quality " +
|
"Only one stat will be shown for a macro. If a craft will be finished, quality " +
|
||||||
|
|||||||
Reference in New Issue
Block a user