Files
Craftimizer/Craftimizer/Utils/MacroImport.cs
T
JonKazama-Hellion b598c03e9e Apply csharpier reflow across source tree
Reformats the entire Craftimizer source tree with dotnet csharpier 1.2.6
to match the Hellion Forge house style (matches what HellionChat enforces
in its pre-push pipeline). Pure whitespace + using-block sorting; no
semantic changes.

This is a one-time noisy commit. Future code edits in this fork should
land csharpier-clean because the pre-push hook (introduced in the next
commit) runs `dotnet csharpier check Craftimizer/` as Block C of the
preflight gate.

Trade-off acknowledged: this widens the merge gap with upstream
Craftimizer should Asriel ever resume maintenance. Given the upstream
has been dormant since FFXIV 7.4 and the fork is light-rename only
(internal namespaces unchanged), the marginal cost is acceptable.
2026-05-26 20:21:21 +02:00

184 lines
5.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Dalamud.Networking.Http;
using static Craftimizer.Utils.CommunityMacros;
namespace Craftimizer.Utils;
public static class MacroImport
{
public static IReadOnlyList<ActionType>? TryParseMacro(string inputMacro)
{
var actions = new List<ActionType>();
foreach (var line in inputMacro.ReplaceLineEndings("\n").Split("\n"))
{
if (TryParseLine(line) is { } action)
actions.Add(action);
}
return actions.Count > 0 ? actions : null;
}
private static ActionType? TryParseLine(string line)
{
if (line.StartsWith("/ac", StringComparison.OrdinalIgnoreCase))
line = line[3..];
else if (line.StartsWith("/action", StringComparison.OrdinalIgnoreCase))
line = line[7..];
else
return null;
line = line.TrimStart();
// get first word
if (line.StartsWith('"'))
{
line = line[1..];
var end = line.IndexOf('"', 1);
if (end != -1)
line = line[..end];
}
else
{
var end = line.IndexOf(' ', 1);
if (end != -1)
line = line[..end];
}
foreach (var action in Enum.GetValues<ActionType>())
{
if (line.Equals(action.GetName(ClassJob.Carpenter), StringComparison.OrdinalIgnoreCase))
return action;
}
return null;
}
public static bool TryParseUrl(string url, out Uri uri)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out uri!))
return false;
if (
!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
)
return false;
if (!uri.IsDefaultPort)
return false;
return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app";
}
public static Task<CommunityMacro> RetrieveUrl(string url, CancellationToken token)
{
if (!TryParseUrl(url, out var uri))
throw new ArgumentException("Unsupported url", nameof(url));
return uri.DnsSafeHost switch
{
"ffxivteamcraft.com" => RetrieveTeamcraftUrl(uri, token),
"craftingway.app" => RetrieveCraftingwayUrl(uri, token),
_ => throw new UnreachableException(
"TryParseUrl should handle miscellaneous edge cases"
),
};
}
private static async Task<CommunityMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token)
{
using var heCallback = new HappyEyeballsCallback();
using var client = new HttpClient(
new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = heCallback.ConnectCallback,
}
);
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
if (!path.StartsWith("simulator/", StringComparison.Ordinal))
throw new ArgumentException(
"Teamcraft macro url should start with /simulator",
nameof(uri)
);
path = path[10..];
var lastSlash = path.LastIndexOf('/');
if (lastSlash == -1)
throw new ArgumentException(
"Teamcraft macro url is not in the right format",
nameof(uri)
);
var id = path[(lastSlash + 1)..];
var resp = await client
.GetFromJsonAsync<TeamcraftMacro>(
$"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}",
token
)
.ConfigureAwait(false);
if (resp is null)
throw new Exception("Internal error; failed to retrieve macro");
if (resp.Error is { } error)
throw new Exception($"Internal server error ({error.Status}); {error.Message}");
return new(resp);
}
private static async Task<CommunityMacro> RetrieveCraftingwayUrl(
Uri uri,
CancellationToken token
)
{
using var heCallback = new HappyEyeballsCallback();
using var client = new HttpClient(
new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = heCallback.ConnectCallback,
}
);
// https://craftingway.app/rotation/variable-blueprint-KmrvS
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
if (!path.StartsWith("rotation/", StringComparison.Ordinal))
throw new ArgumentException(
"Craftingway macro url should start with /rotation",
nameof(uri)
);
path = path[9..];
var lastSlash = path.LastIndexOf('/');
if (lastSlash != -1)
throw new ArgumentException(
"Craftingway macro url is not in the right format",
nameof(uri)
);
var id = path;
var resp = await client
.GetFromJsonAsync<CraftingwayMacro>($"https://servingway.fly.dev/rotation/{id}", token)
.ConfigureAwait(false);
if (resp is null)
throw new Exception("Internal error; failed to retrieve macro");
if (resp.Error is { } error)
throw new Exception($"Internal server error; {error}");
return new(resp);
}
}