From 70bc8bf38c570f1d8a327f7b17c1ab38c4610080 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 18 Apr 2026 09:15:18 -0400 Subject: [PATCH 1/8] Improve step window display Now it's not closeable and doesn't save its window settings. --- dalamud/Auracite/Auracite.json | 4 ++-- dalamud/Auracite/EndStep.cs | 6 +++--- dalamud/Auracite/Plugin.cs | 4 ++-- dalamud/Auracite/StepWindow.cs | 14 ++++++++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/dalamud/Auracite/Auracite.json b/dalamud/Auracite/Auracite.json index 2a06978..078245a 100644 --- a/dalamud/Auracite/Auracite.json +++ b/dalamud/Auracite/Auracite.json @@ -1,8 +1,8 @@ { "Author": "redstrate", "Name": "Auracite", - "Punchline": "Export your FFXIV character in portable, generic formats", - "Description": "Export your FFXIV character in portable, generic formats.", + "Punchline": "Archive your FFXIV character in a portable, generic format", + "Description": "Archive your FFXIV character in a portable, generic format", "Tags": [], "RepoUrl": "https://github.com/redstrate/Auracite" } diff --git a/dalamud/Auracite/EndStep.cs b/dalamud/Auracite/EndStep.cs index d4159e0..59ac21c 100644 --- a/dalamud/Auracite/EndStep.cs +++ b/dalamud/Auracite/EndStep.cs @@ -22,12 +22,12 @@ public void End() public string StepName() { - return "Waiting for Upload"; + return "Save Archive"; } public string StepDescription() { - return "Run Auracite to archive your character."; + return "Save your archived character ZIP."; } private class Controller : WebApiController @@ -80,4 +80,4 @@ public void Dispose() { ShutdownWebServer(); } -} \ No newline at end of file +} diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs index 06df774..47923ed 100644 --- a/dalamud/Auracite/Plugin.cs +++ b/dalamud/Auracite/Plugin.cs @@ -123,7 +123,7 @@ public Plugin() { CommandManager.AddHandler("/auracite", new CommandInfo(OnAuraciteCommand) { - HelpMessage = "Start the server." + HelpMessage = "Start the archive process." }); StepWindow = new StepWindow(); @@ -152,7 +152,7 @@ public void Dispose() private void OnAuraciteCommand(string command, string arguments) { - if (arguments == "begin" && CurrentStep == null) + if (CurrentStep == null) { _stepIndex = -1; package = new Package(); diff --git a/dalamud/Auracite/StepWindow.cs b/dalamud/Auracite/StepWindow.cs index 41bd942..7511bd8 100644 --- a/dalamud/Auracite/StepWindow.cs +++ b/dalamud/Auracite/StepWindow.cs @@ -4,21 +4,27 @@ namespace Auracite; -public class StepWindow() - : Window("Step Window"), IDisposable +public class StepWindow : Window, IDisposable { + public StepWindow() : base("Auracite", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings) + { + this.ShowCloseButton = false; + } + public void Dispose() { + } public override void Draw() { if (Plugin.CurrentStep != null) { - ImGui.Text(Plugin.CurrentStep.StepName()); + ImGui.Text($"Step: {Plugin.CurrentStep.StepName()}"); + ImGui.Separator(); ImGui.Text(Plugin.CurrentStep.StepDescription()); - ImGui.TextDisabled("Step requires manual user action."); + ImGui.TextDisabled("This step requires manual user action."); if (ImGui.Button("Retry")) { From c4a7560c3a7ab1eb18919dbe850381150eb04c79 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 18 Apr 2026 09:26:22 -0400 Subject: [PATCH 2/8] Add button to download from plugin directly Eventually I want to remove the website portion completely, so it can be self-contained within the plugin. --- dalamud/Auracite/Auracite.json | 4 ++-- dalamud/Auracite/EndStep.cs | 15 ++++++--------- dalamud/Auracite/IStep.cs | 6 +++++- dalamud/Auracite/Plugin.cs | 9 ++++++++- dalamud/Auracite/StepWindow.cs | 35 ++++++++++++++++++++++++++-------- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/dalamud/Auracite/Auracite.json b/dalamud/Auracite/Auracite.json index 078245a..ac9257b 100644 --- a/dalamud/Auracite/Auracite.json +++ b/dalamud/Auracite/Auracite.json @@ -1,8 +1,8 @@ { "Author": "redstrate", "Name": "Auracite", - "Punchline": "Archive your FFXIV character in a portable, generic format", - "Description": "Archive your FFXIV character in a portable, generic format", + "Punchline": "Archive your character in a portable, generic format", + "Description": "Archive your character in a portable, generic format", "Tags": [], "RepoUrl": "https://github.com/redstrate/Auracite" } diff --git a/dalamud/Auracite/EndStep.cs b/dalamud/Auracite/EndStep.cs index 59ac21c..b0a29a0 100644 --- a/dalamud/Auracite/EndStep.cs +++ b/dalamud/Auracite/EndStep.cs @@ -39,7 +39,7 @@ public Controller(EndStep endStep) _endStep = endStep; } - [Route(HttpVerbs.Get, "/package")] + [Route(HttpVerbs.Get, "/download")] public void GetPackage() { Response.Headers.Set(HttpHeaderNames.AccessControlAllowOrigin, "*"); @@ -47,14 +47,6 @@ public void GetPackage() using var writer = HttpContext.OpenResponseText(Encoding.UTF8, true); writer.Write(JsonConvert.SerializeObject(Plugin.package)); } - - // TODO: Make this a POST request? - // This is needed since we don't know when the CORS handshake really stops. This really shouldn't be needed though. - [Route(HttpVerbs.Get, "/stop")] - public void Stop() - { - _endStep.End(); - } } private WebServer? _server; @@ -80,4 +72,9 @@ public void Dispose() { ShutdownWebServer(); } + + public bool IsEnd() + { + return true; + } } diff --git a/dalamud/Auracite/IStep.cs b/dalamud/Auracite/IStep.cs index 2e6dc7d..09b8435 100644 --- a/dalamud/Auracite/IStep.cs +++ b/dalamud/Auracite/IStep.cs @@ -10,6 +10,10 @@ public interface IStep : IDisposable string StepName(); string StepDescription(); + bool IsEnd() + { + return false; + } delegate void CompletedDelegate(); -} \ No newline at end of file +} diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs index 47923ed..d470119 100644 --- a/dalamud/Auracite/Plugin.cs +++ b/dalamud/Auracite/Plugin.cs @@ -126,7 +126,7 @@ public Plugin() HelpMessage = "Start the archive process." }); - StepWindow = new StepWindow(); + StepWindow = new StepWindow(this); WindowSystem.AddWindow(StepWindow); PluginInterface.UiBuilder.Draw += WindowSystem.Draw; @@ -175,4 +175,11 @@ private void NextStep() CurrentStep.Completed += NextStep; CurrentStep.Run(); } + + public void Stop() + { + CurrentStep = null; + StepWindow.IsOpen = false; + package = null; + } } diff --git a/dalamud/Auracite/StepWindow.cs b/dalamud/Auracite/StepWindow.cs index 7511bd8..32f5a79 100644 --- a/dalamud/Auracite/StepWindow.cs +++ b/dalamud/Auracite/StepWindow.cs @@ -1,13 +1,17 @@ using System; using Dalamud.Interface.Windowing; using Dalamud.Bindings.ImGui; +using System.Diagnostics; namespace Auracite; public class StepWindow : Window, IDisposable { - public StepWindow() : base("Auracite", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings) + private Plugin plugin; + + public StepWindow(Plugin plugin) : base("Auracite", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings) { + this.plugin = plugin; this.ShowCloseButton = false; } @@ -20,15 +24,30 @@ public override void Draw() { if (Plugin.CurrentStep != null) { - ImGui.Text($"Step: {Plugin.CurrentStep.StepName()}"); - ImGui.Separator(); - ImGui.Text(Plugin.CurrentStep.StepDescription()); + if (Plugin.CurrentStep.IsEnd()) { + ImGui.Text("Archive created! Please download it below and keep it in a safe place."); + ImGui.Text("The plugin can be disabled once you're done using it."); + + if (ImGui.Button("Download")) + { + Process.Start(new ProcessStartInfo { FileName = "http://localhost:42072/download", UseShellExecute = true }); + } + ImGui.SameLine(); + if (ImGui.Button("Close")) + { + plugin.Stop(); + } + } else { + ImGui.Text($"Step: {Plugin.CurrentStep.StepName()}"); + ImGui.Separator(); + ImGui.Text(Plugin.CurrentStep.StepDescription()); - ImGui.TextDisabled("This step requires manual user action."); + ImGui.TextDisabled("This step requires manual user action."); - if (ImGui.Button("Retry")) - { - Plugin.CurrentStep.Run(); + if (ImGui.Button("Retry")) + { + Plugin.CurrentStep.Run(); + } } } else From fd7bd68dc1ada80930ad21f31080a4f2aeb9c2e6 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 18 Apr 2026 09:32:20 -0400 Subject: [PATCH 3/8] Remove need for manual Retry button in step window --- dalamud/Auracite/AdventurerPlateStep.cs | 5 +++++ dalamud/Auracite/IStep.cs | 4 ++++ dalamud/Auracite/Plugin.cs | 12 +++++++++++- dalamud/Auracite/StepWindow.cs | 5 ----- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/dalamud/Auracite/AdventurerPlateStep.cs b/dalamud/Auracite/AdventurerPlateStep.cs index c2f916e..d9c4dea 100644 --- a/dalamud/Auracite/AdventurerPlateStep.cs +++ b/dalamud/Auracite/AdventurerPlateStep.cs @@ -209,4 +209,9 @@ public string ResolveCardBase(uint rowIndex) var row = Plugin.DataManager.GetExcelSheet()?.GetRow(rowIndex); return $"ui/icon/{row?.BottomImage.ToString().Substring(0, 3)}000/{row?.BottomImage}_hr1.tex"; } + + public bool NeedsUpdateEveryFrame() + { + return true; // So we can wait for the window to open. + } } diff --git a/dalamud/Auracite/IStep.cs b/dalamud/Auracite/IStep.cs index 09b8435..d3eff56 100644 --- a/dalamud/Auracite/IStep.cs +++ b/dalamud/Auracite/IStep.cs @@ -14,6 +14,10 @@ bool IsEnd() { return false; } + bool NeedsUpdateEveryFrame() + { + return false; + } delegate void CompletedDelegate(); } diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs index d470119..03afcb6 100644 --- a/dalamud/Auracite/Plugin.cs +++ b/dalamud/Auracite/Plugin.cs @@ -123,13 +123,14 @@ public Plugin() { CommandManager.AddHandler("/auracite", new CommandInfo(OnAuraciteCommand) { - HelpMessage = "Start the archive process." + HelpMessage = "Start the archival process." }); StepWindow = new StepWindow(this); WindowSystem.AddWindow(StepWindow); PluginInterface.UiBuilder.Draw += WindowSystem.Draw; + Framework.Update += CheckCurrentStep; } [PluginService] internal static IClientState ClientState { get; private set; } = null!; @@ -144,6 +145,8 @@ public Plugin() [PluginService] internal static IDataManager DataManager { get; private set; } = null!; + [PluginService] internal static IFramework Framework { get; private set; } = null!; + public void Dispose() { CurrentStep?.Dispose(); @@ -182,4 +185,11 @@ public void Stop() StepWindow.IsOpen = false; package = null; } + + private void CheckCurrentStep(IFramework framework) + { + if (CurrentStep != null && CurrentStep.NeedsUpdateEveryFrame()) { + CurrentStep.Run(); + } + } } diff --git a/dalamud/Auracite/StepWindow.cs b/dalamud/Auracite/StepWindow.cs index 32f5a79..91c7890 100644 --- a/dalamud/Auracite/StepWindow.cs +++ b/dalamud/Auracite/StepWindow.cs @@ -43,11 +43,6 @@ public override void Draw() ImGui.Text(Plugin.CurrentStep.StepDescription()); ImGui.TextDisabled("This step requires manual user action."); - - if (ImGui.Button("Retry")) - { - Plugin.CurrentStep.Run(); - } } } else From e4f2f4bb4094d439a2c1f174d78c32ead08ce467 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 18 Apr 2026 11:20:24 -0400 Subject: [PATCH 4/8] Save more data in character JSON file This roughly corresponds to what we can store in Kawari today. --- dalamud/Auracite/AppearanceStep.cs | 56 ----- dalamud/Auracite/IStep.cs | 19 ++ dalamud/Auracite/MiscStep.cs | 49 ----- dalamud/Auracite/Package.cs | 208 ++++++++++++++++++ dalamud/Auracite/Plugin.cs | 102 +-------- .../{ => Steps}/AdventurerPlateStep.cs | 0 dalamud/Auracite/Steps/AppearanceStep.cs | 53 +++++ dalamud/Auracite/{ => Steps}/EndStep.cs | 0 dalamud/Auracite/{ => Steps}/InventoryStep.cs | 23 +- dalamud/Auracite/Steps/MiscStep.cs | 144 ++++++++++++ dalamud/Auracite/{ => Steps}/PlaytimeStep.cs | 0 dalamud/Auracite/Steps/TitleStep.cs | 49 +++++ 12 files changed, 491 insertions(+), 212 deletions(-) delete mode 100644 dalamud/Auracite/AppearanceStep.cs delete mode 100644 dalamud/Auracite/MiscStep.cs create mode 100644 dalamud/Auracite/Package.cs rename dalamud/Auracite/{ => Steps}/AdventurerPlateStep.cs (100%) create mode 100644 dalamud/Auracite/Steps/AppearanceStep.cs rename dalamud/Auracite/{ => Steps}/EndStep.cs (100%) rename dalamud/Auracite/{ => Steps}/InventoryStep.cs (77%) create mode 100644 dalamud/Auracite/Steps/MiscStep.cs rename dalamud/Auracite/{ => Steps}/PlaytimeStep.cs (100%) create mode 100644 dalamud/Auracite/Steps/TitleStep.cs diff --git a/dalamud/Auracite/AppearanceStep.cs b/dalamud/Auracite/AppearanceStep.cs deleted file mode 100644 index 136affd..0000000 --- a/dalamud/Auracite/AppearanceStep.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Enums; - -namespace Auracite; - -public class AppearanceStep : IStep -{ - public event IStep.CompletedDelegate? Completed; - - public void Run() - { - if (Plugin.ObjectTable.LocalPlayer != null) - { - Plugin.package.race = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Race]; - Plugin.package.gender = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Gender]; - Plugin.package.model_type = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.ModelType]; - Plugin.package.height = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Height]; - Plugin.package.tribe = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Tribe]; - Plugin.package.face_type = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FaceType]; - Plugin.package.hair_style = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HairStyle]; - Plugin.package.has_highlights = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HasHighlights] == 1; - Plugin.package.skin_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.SkinColor]; - Plugin.package.eye_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.EyeColor]; - Plugin.package.hair_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HairColor]; - Plugin.package.hair_color2 = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HairColor2]; - Plugin.package.face_features = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FaceFeatures]; - Plugin.package.face_features_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FaceFeaturesColor]; - Plugin.package.eyebrows = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Eyebrows]; - Plugin.package.eye_color2 = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.EyeColor2]; - Plugin.package.eye_shape = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.EyeShape]; - Plugin.package.nose_shape = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.NoseShape]; - Plugin.package.jaw_shape = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.JawShape]; - Plugin.package.lip_style = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.LipStyle]; - Plugin.package.lip_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.LipColor]; - Plugin.package.race_feature_size = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.RaceFeatureSize]; - Plugin.package.race_feature_type = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.RaceFeatureType]; - Plugin.package.bust_size = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.BustSize]; - Plugin.package.facepaint = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Facepaint]; - Plugin.package.facepaint_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FacepaintColor]; - } - Completed?.Invoke(); - } - - public string StepName() - { - return "Appearance"; - } - - public string StepDescription() - { - return "No user action required."; - } - - public void Dispose() - { - } -} diff --git a/dalamud/Auracite/IStep.cs b/dalamud/Auracite/IStep.cs index d3eff56..c948d0e 100644 --- a/dalamud/Auracite/IStep.cs +++ b/dalamud/Auracite/IStep.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using InteropGenerator.Runtime; +using Lumina.Excel; +using Lumina.Text.ReadOnly; namespace Auracite; @@ -20,4 +24,19 @@ bool NeedsUpdateEveryFrame() } delegate void CompletedDelegate(); + + public static NameValue SaveNameValue(uint key, Func fieldSelector) where T : struct, IExcelRow { + var newValue = new NameValue(); + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(key); + if (row != null) { + newValue.name = fieldSelector(row.Value).ToString(); + } + newValue.value = key; + + return newValue; + } + + public static unsafe List ConsumeBitArray(BitArray array) { + return new List(new ReadOnlySpan(array.Pointer, array.ByteLength).ToArray()); + } } diff --git a/dalamud/Auracite/MiscStep.cs b/dalamud/Auracite/MiscStep.cs deleted file mode 100644 index 024b960..0000000 --- a/dalamud/Auracite/MiscStep.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using FFXIVClientStructs.FFXIV.Client.Game.UI; -using FFXIVClientStructs.FFXIV.Client.Game.Character; - -namespace Auracite; - -public class MiscStep : IStep -{ - public event IStep.CompletedDelegate? Completed; - - public void Run() - { - unsafe - { - Plugin.package.is_battle_mentor = PlayerState.Instance()->IsBattleMentor(); - Plugin.package.is_trade_mentor = PlayerState.Instance()->IsTradeMentor(); - Plugin.package.is_novice = PlayerState.Instance()->IsNovice(); - Plugin.package.is_returner = PlayerState.Instance()->IsReturner(); - Plugin.package.player_commendations = PlayerState.Instance()->PlayerCommendations; - Plugin.package.unlock_flags = new System.Collections.Generic.List(); // TODO: lol - Plugin.package.unlock_flags.AddRange(new ReadOnlySpan(UIState.Instance()->UnlockLinksBitArray.Pointer, UIState.Instance()->UnlockLinksBitArray.ByteLength).ToArray()); - Plugin.package.unlock_aetherytes = new System.Collections.Generic.List(); // TODO: lol - Plugin.package.unlock_aetherytes.AddRange(new ReadOnlySpan(UIState.Instance()->UnlockedAetherytesBitArray.Pointer, UIState.Instance()->UnlockedAetherytesBitArray.ByteLength).ToArray()); - - var localPlayer = Plugin.ObjectTable.LocalPlayer; - if (localPlayer != null) - { - var gameObject = (Character*)localPlayer.Address; - Plugin.package.voice = gameObject->Vfx.VoiceId; - } - } - - Completed?.Invoke(); - } - - public string StepName() - { - return "Misc Data"; - } - - public string StepDescription() - { - return "No user action required."; - } - - public void Dispose() - { - } -} diff --git a/dalamud/Auracite/Package.cs b/dalamud/Auracite/Package.cs new file mode 100644 index 0000000..4375102 --- /dev/null +++ b/dalamud/Auracite/Package.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Auracite; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class InventoryItem +{ + public int slot; + public uint quantity; + public uint id; + public ulong crafter_content_id; + public byte item_flags; + public ushort condition; + public ushort spiritbond_or_collectability; + public uint glamour_id; + public List materia; + public List materia_grades; + public List stains; +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class InventoryContainer +{ + public List items; +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class NameValue +{ + public string name; + public uint value; +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class DayMonthValue +{ + // TODO: add back datetime name once I can figure out how to calculate it + public int day; + public int month; +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class ClassJobLevel +{ + public string name; + public int level; + public int exp; + public uint value; +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class Appearance +{ + public int model_type; + public int height; + public int face_type; + public int hair_style; + public bool has_highlights; + public int skin_color; + public int eye_color; + public int hair_color; + public int hair_color2; + public int face_features; + public int face_features_color; + public int eyebrows; + public int eye_color2; + public int eye_shape; + public int nose_shape; + public int jaw_shape; + public int lip_style; + public int lip_color; + public int race_feature_size; + public int race_feature_type; + public int bust_size; + public int facepaint; + public int facepaint_color; +} + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class CharacterJson +{ + public string name; + public NameValue world; + public NameValue data_center; + public NameValue city_state; + public DayMonthValue nameday; + public NameValue guardian; + public NameValue gender; + public NameValue tribe; + public NameValue race; + public List classjob_levels = new List(); + public NameValue grand_company; + public int grand_company_rank; // TODO: introduce as a NameValue + public NameValue title; + public string playtime; + public int voice; + + // adventurer plate + public string? portrait; + public string? plate_title; + public bool? plate_title_is_prefix; + public string? plate_class_job; + public int plate_class_job_level; + public string? search_comment; + public string? base_plate; + public string? pattern_overlay; + public string? backing; + public string? top_border; + public string? bottom_border; + public string? portrait_frame; + public string? plate_frame; + public string? accent; + + public bool is_battle_mentor; + public bool is_trade_mentor; + public bool is_novice; + public bool is_returner; + public short player_commendations; + + // Appearance + public Appearance appearance = new Appearance(); + + // inventory + public InventoryContainer inventory1; + public InventoryContainer inventory2; + public InventoryContainer inventory3; + public InventoryContainer inventory4; + + public InventoryContainer equipped; + + public InventoryContainer currency; + + public InventoryContainer armory_off_hand; + public InventoryContainer armory_head; + public InventoryContainer armory_body; + public InventoryContainer armory_hands; + public InventoryContainer armory_waist; + public InventoryContainer armory_legs; + public InventoryContainer armory_ear; + public InventoryContainer armory_neck; + public InventoryContainer armory_wrist; + public InventoryContainer armory_rings; + public InventoryContainer armory_soul_crystal; + public InventoryContainer armory_main_hand; + + // Other stuff useful to Kawari: + + // unlocks + public List unlocks; + public List seen_active_help; + public List minions; + public List mounts; + public List orchestrion_rolls; + public List cutscene_seen; + public List ornaments; + public List caught_fish; + public List caught_spearfish; + public List adventures; + public List triple_triad_cards; + public List glasses_styles; + public List chocobo_taxi_stands; + public List titles; + public List unlocked_companion_equip; + + // aether currents + public List comp_flg_set; + public List unlocked_aether_currents; + + // aetheryte + public List unlocked_aetherytes; + public int hoempoint; + public List favorite_aetherytes; + public int free_aetheryte; + + // classjob + public int current_class; + public int first_class; + public uint rested_exp; + + // content + public List unlocked_special_content; + public List unlocked_raids; + public List unlocked_dungeons; + public List unlocked_guildhests; + public List unlocked_trials; + public List unlocked_crystalline_conflicts; + public List unlocked_frontlines; + public List cleared_raids; + public List cleared_dungeons; + public List cleared_guildhests; + public List cleared_trials; + public List cleared_crystalline_conflicts; + public List cleared_frontlines; + public List cleared_masked_carnivale; + public List unlocked_misc_content; + public List cleared_misc_content; + + // quest + public List completed_quests; + + // volatile + public float position_x; + public float position_y; + public float position_z; + public float rotation; + public ushort zone_id; +} diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs index 03afcb6..f7acbf8 100644 --- a/dalamud/Auracite/Plugin.cs +++ b/dalamud/Auracite/Plugin.cs @@ -15,109 +15,13 @@ public sealed class Plugin : IDalamudPlugin private readonly WindowSystem WindowSystem = new("Auracite"); private readonly List _steps = - [typeof(AppearanceStep), typeof(InventoryStep), typeof(MiscStep), typeof(PlaytimeStep), typeof(AdventurerPlateStep), typeof(EndStep)]; + [typeof(AppearanceStep), typeof(InventoryStep), typeof(MiscStep), typeof(PlaytimeStep), typeof(AdventurerPlateStep), typeof(TitleStep), typeof(EndStep)]; private int _stepIndex; private readonly StepWindow StepWindow; - [SuppressMessage("ReSharper", "InconsistentNaming")] - public class InventoryItem - { - public int slot; - public uint quantity; - public int condition; - public uint id; - public uint glamour_id; - } - - [SuppressMessage("ReSharper", "InconsistentNaming")] - public class InventoryContainer - { - public List items; - } - - [SuppressMessage("ReSharper", "InconsistentNaming")] - public class Package - { - public string? playtime; - public bool is_battle_mentor; - public bool is_trade_mentor; - public bool is_novice; - public bool is_returner; - public short player_commendations; - - // Appearance - public int race; - public int gender; - public int model_type; - public int height; - public int tribe; - public int face_type; - public int hair_style; - public bool has_highlights; - public int skin_color; - public int eye_color; - public int hair_color; - public int hair_color2; - public int face_features; - public int face_features_color; - public int eyebrows; - public int eye_color2; - public int eye_shape; - public int nose_shape; - public int jaw_shape; - public int lip_style; - public int lip_color; - public int race_feature_size; - public int race_feature_type; - public int bust_size; - public int facepaint; - public int facepaint_color; - public string? portrait; - public string? plate_title; - public bool? plate_title_is_prefix; - public string? plate_class_job; - public int plate_class_job_level; - public string? search_comment; - public string? base_plate; - public string? pattern_overlay; - public string? backing; - public string? top_border; - public string? bottom_border; - public string? portrait_frame; - public string? plate_frame; - public string? accent; - - // inventory - public InventoryContainer inventory1; - public InventoryContainer inventory2; - public InventoryContainer inventory3; - public InventoryContainer inventory4; - - public InventoryContainer equipped_items; - - public InventoryContainer currency; - - public InventoryContainer armory_off_hand; - public InventoryContainer armory_head; - public InventoryContainer armory_body; - public InventoryContainer armory_hands; - public InventoryContainer armory_waist; - public InventoryContainer armory_legs; - public InventoryContainer armory_ear; - public InventoryContainer armory_neck; - public InventoryContainer armory_wrist; - public InventoryContainer armory_rings; - public InventoryContainer armory_soul_crystal; - public InventoryContainer armory_main_hand; - - public int voice; - public List unlock_flags; - public List unlock_aetherytes; - } - - public static Package? package; + public static CharacterJson? package; public Plugin() { @@ -158,7 +62,7 @@ private void OnAuraciteCommand(string command, string arguments) if (CurrentStep == null) { _stepIndex = -1; - package = new Package(); + package = new CharacterJson(); NextStep(); StepWindow.IsOpen = true; } diff --git a/dalamud/Auracite/AdventurerPlateStep.cs b/dalamud/Auracite/Steps/AdventurerPlateStep.cs similarity index 100% rename from dalamud/Auracite/AdventurerPlateStep.cs rename to dalamud/Auracite/Steps/AdventurerPlateStep.cs diff --git a/dalamud/Auracite/Steps/AppearanceStep.cs b/dalamud/Auracite/Steps/AppearanceStep.cs new file mode 100644 index 0000000..0760147 --- /dev/null +++ b/dalamud/Auracite/Steps/AppearanceStep.cs @@ -0,0 +1,53 @@ +using Dalamud.Game.ClientState.Objects.Enums; + +namespace Auracite; + +public class AppearanceStep : IStep +{ + public event IStep.CompletedDelegate? Completed; + + public void Run() + { + if (Plugin.ObjectTable.LocalPlayer != null) + { + Plugin.package.appearance.model_type = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.ModelType]; + Plugin.package.appearance.height = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Height]; + Plugin.package.appearance.face_type = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FaceType]; + Plugin.package.appearance.hair_style = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HairStyle]; + Plugin.package.appearance.has_highlights = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HasHighlights] == 1; + Plugin.package.appearance.skin_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.SkinColor]; + Plugin.package.appearance.eye_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.EyeColor]; + Plugin.package.appearance.hair_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HairColor]; + Plugin.package.appearance.hair_color2 = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.HairColor2]; + Plugin.package.appearance.face_features = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FaceFeatures]; + Plugin.package.appearance.face_features_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FaceFeaturesColor]; + Plugin.package.appearance.eyebrows = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Eyebrows]; + Plugin.package.appearance.eye_color2 = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.EyeColor2]; + Plugin.package.appearance.eye_shape = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.EyeShape]; + Plugin.package.appearance.nose_shape = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.NoseShape]; + Plugin.package.appearance.jaw_shape = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.JawShape]; + Plugin.package.appearance.lip_style = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.LipStyle]; + Plugin.package.appearance.lip_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.LipColor]; + Plugin.package.appearance.race_feature_size = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.RaceFeatureSize]; + Plugin.package.appearance.race_feature_type = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.RaceFeatureType]; + Plugin.package.appearance.bust_size = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.BustSize]; + Plugin.package.appearance.facepaint = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.Facepaint]; + Plugin.package.appearance.facepaint_color = Plugin.ObjectTable.LocalPlayer.Customize[(int)CustomizeIndex.FacepaintColor]; + } + Completed?.Invoke(); + } + + public string StepName() + { + return "Appearance"; + } + + public string StepDescription() + { + return "No user action required."; + } + + public void Dispose() + { + } +} diff --git a/dalamud/Auracite/EndStep.cs b/dalamud/Auracite/Steps/EndStep.cs similarity index 100% rename from dalamud/Auracite/EndStep.cs rename to dalamud/Auracite/Steps/EndStep.cs diff --git a/dalamud/Auracite/InventoryStep.cs b/dalamud/Auracite/Steps/InventoryStep.cs similarity index 77% rename from dalamud/Auracite/InventoryStep.cs rename to dalamud/Auracite/Steps/InventoryStep.cs index 4a9c850..291a1b2 100644 --- a/dalamud/Auracite/InventoryStep.cs +++ b/dalamud/Auracite/Steps/InventoryStep.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.Game; namespace Auracite; @@ -18,7 +19,7 @@ public void Run() Plugin.package.inventory3 = ProcessContainer(manager->GetInventoryContainer(InventoryType.Inventory3)); Plugin.package.inventory4 = ProcessContainer(manager->GetInventoryContainer(InventoryType.Inventory4)); - Plugin.package.equipped_items = ProcessContainer(manager->GetInventoryContainer(InventoryType.EquippedItems)); + Plugin.package.equipped = ProcessContainer(manager->GetInventoryContainer(InventoryType.EquippedItems)); Plugin.package.currency = ProcessContainer(manager->GetInventoryContainer(InventoryType.Currency)); @@ -39,9 +40,9 @@ public void Run() Completed?.Invoke(); } - private unsafe Auracite.Plugin.InventoryContainer ProcessContainer(FFXIVClientStructs.FFXIV.Client.Game.InventoryContainer *container) { - var serializedContainer = new Auracite.Plugin.InventoryContainer(); - serializedContainer.items = new System.Collections.Generic.List(); // TODO: lol + private unsafe Auracite.InventoryContainer ProcessContainer(FFXIVClientStructs.FFXIV.Client.Game.InventoryContainer *container) { + var serializedContainer = new Auracite.InventoryContainer(); + serializedContainer.items = new System.Collections.Generic.List(); // TODO: lol for (int i = 0; i < container->Size; i++) { var item = container->GetInventorySlot(i); @@ -50,12 +51,18 @@ private unsafe Auracite.Plugin.InventoryContainer ProcessContainer(FFXIVClientSt continue; } - var serializedItem = new Auracite.Plugin.InventoryItem(); - serializedItem.condition = item->GetCondition(); - serializedItem.id = item->GetBaseItemId(); - serializedItem.quantity = item->GetQuantity(); + var serializedItem = new Auracite.InventoryItem(); serializedItem.slot = item->GetSlot(); + serializedItem.quantity = item->GetQuantity(); + serializedItem.id = item->GetBaseItemId(); + serializedItem.crafter_content_id = item->GetCrafterContentId(); + serializedItem.item_flags = (byte)item->GetFlags(); + serializedItem.condition = item->GetCondition(); + serializedItem.spiritbond_or_collectability = item->GetSpiritbondOrCollectability(); serializedItem.glamour_id = item->GetGlamourId(); + serializedItem.materia = new List(item->Materia.ToArray()); + serializedItem.materia_grades = new List(item->MateriaGrades.ToArray()); + serializedItem.stains = new List(item->Stains.ToArray()); serializedContainer.items.Add(serializedItem); } diff --git a/dalamud/Auracite/Steps/MiscStep.cs b/dalamud/Auracite/Steps/MiscStep.cs new file mode 100644 index 0000000..a33543a --- /dev/null +++ b/dalamud/Auracite/Steps/MiscStep.cs @@ -0,0 +1,144 @@ +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using Lumina.Excel.Sheets; +using System.Collections.Generic; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Auracite; + +public class MiscStep : IStep +{ + public event IStep.CompletedDelegate? Completed; + + public void Run() + { + unsafe + { + var playerState = PlayerState.Instance(); + var uiState = UIState.Instance(); + var questManager = QuestManager.Instance(); + + Plugin.package!.name = playerState->CharacterNameString; + Plugin.package!.world = IStep.SaveNameValue(Plugin.ObjectTable.LocalPlayer!.HomeWorld.RowId, world => world.Name); + Plugin.package!.data_center = IStep.SaveNameValue(Plugin.ObjectTable.LocalPlayer!.HomeWorld.Value.DataCenter.RowId, data_center => data_center.Name); + Plugin.package!.city_state = IStep.SaveNameValue(playerState->StartTown, town => town.Name); + Plugin.package!.nameday = new DayMonthValue(); + Plugin.package!.nameday.day = playerState->BirthDay; + Plugin.package!.nameday.month = playerState->BirthMonth; + Plugin.package!.guardian = IStep.SaveNameValue(playerState->GuardianDeity, guardian => guardian.Name); + Plugin.package!.gender = new NameValue(); + Plugin.package!.gender.value = playerState->Sex; + Plugin.package!.gender.name = playerState->Sex == 1 ? "Female" : "Male"; + Plugin.package!.tribe = IStep.SaveNameValue(playerState->Tribe, tribe => playerState->Sex == 1 ? tribe.Feminine : tribe.Masculine); + Plugin.package!.race = IStep.SaveNameValue(playerState->Race, race => playerState->Sex == 1 ? race.Feminine : race.Masculine); + var classJobSheet = Plugin.DataManager.GetExcelSheet()!; + for (int i = 0; i < playerState->ClassJobLevels.Length; i++) { + var classLevel = new ClassJobLevel(); + foreach (var row in classJobSheet) { + if (row.ExpArrayIndex == i) { + classLevel.name = row.NameEnglish.ToString()!; + classLevel.value = row.RowId; + break; + } + } + classLevel.level = playerState->ClassJobLevels[i]; + classLevel.exp = playerState->ClassJobExperience[i]; + + // Exclude currently unavailable jobs + if (classLevel.name != null) { + Plugin.package!.classjob_levels.Add(classLevel); + } + } + Plugin.package!.grand_company = IStep.SaveNameValue(playerState->GrandCompany, company => company.Name); + Plugin.package!.grand_company_rank = playerState->GetGrandCompanyRank(); + + Plugin.package.is_battle_mentor = PlayerState.Instance()->IsBattleMentor(); + Plugin.package.is_trade_mentor = PlayerState.Instance()->IsTradeMentor(); + Plugin.package.is_novice = PlayerState.Instance()->IsNovice(); + Plugin.package.is_returner = PlayerState.Instance()->IsReturner(); + Plugin.package.player_commendations = PlayerState.Instance()->PlayerCommendations; + + var localPlayer = Plugin.ObjectTable.LocalPlayer; + if (localPlayer != null) + { + var gameObject = (Character*)localPlayer.Address; + Plugin.package.voice = gameObject->Vfx.VoiceId; + } + + // unlocks + Plugin.package.unlocks = IStep.ConsumeBitArray(uiState->UnlockLinksBitArray); + Plugin.package.seen_active_help = IStep.ConsumeBitArray(uiState->UnlockedHowTosBitArray); + Plugin.package.minions = IStep.ConsumeBitArray(uiState->UnlockedCompanionsBitArray); + Plugin.package.mounts = IStep.ConsumeBitArray(playerState->UnlockedMountsBitArray); + Plugin.package.orchestrion_rolls = IStep.ConsumeBitArray(playerState->UnlockedOrchestrionRollsBitArray); + Plugin.package.cutscene_seen = IStep.ConsumeBitArray(uiState->SeenCutscenesBitArray); + Plugin.package.ornaments = IStep.ConsumeBitArray(playerState->UnlockedOrnamentsBitArray); + Plugin.package.caught_fish = IStep.ConsumeBitArray(playerState->CaughtFishBitArray); + Plugin.package.caught_spearfish = IStep.ConsumeBitArray(playerState->CaughtSpearfishBitArray); + Plugin.package.adventures = IStep.ConsumeBitArray(playerState->CompletedAdventuresBitArray); + Plugin.package.triple_triad_cards = IStep.ConsumeBitArray(uiState->UnlockedTripleTriadCardsBitArray); + Plugin.package.glasses_styles = IStep.ConsumeBitArray(playerState->UnlockedGlassesStylesBitArray); + Plugin.package.chocobo_taxi_stands = IStep.ConsumeBitArray(uiState->UnlockedChocoboTaxiStandsBitArray); + Plugin.package.unlocked_companion_equip = new List(uiState->Buddy.CompanionInfo.BuddyEquipUnlockBitmask.ToArray()); + + // aether currents + Plugin.package.comp_flg_set = IStep.ConsumeBitArray(playerState->UnlockedAetherCurrentCompFlgSetsBitArray); + Plugin.package.unlocked_aether_currents = IStep.ConsumeBitArray(playerState->UnlockedAetherCurrentsBitArray); + + // aetherytes + Plugin.package.unlocked_aetherytes = IStep.ConsumeBitArray(uiState->UnlockedAetherytesBitArray); + Plugin.package.hoempoint = playerState->HomeAetheryteId; + Plugin.package.favorite_aetherytes = new List(playerState->FavouriteAetherytes.ToArray()); + Plugin.package.free_aetheryte = playerState->FreeAetheryteId; + + // classjob + Plugin.package.current_class = playerState->CurrentClassJobId; + Plugin.package.first_class = playerState->FirstClass; + Plugin.package.rested_exp = playerState->BaseRestedExperience; + + // content + Plugin.package.unlocked_special_content = IStep.ConsumeBitArray(playerState->UnlockedSpecialContentBitArray); + Plugin.package.unlocked_raids = IStep.ConsumeBitArray(playerState->UnlockedRaidsBitArray); + Plugin.package.unlocked_dungeons = IStep.ConsumeBitArray(playerState->UnlockedDungeonsBitArray); + Plugin.package.unlocked_guildhests = IStep.ConsumeBitArray(playerState->UnlockedGuildOrdersBitArray); + Plugin.package.unlocked_trials = IStep.ConsumeBitArray(playerState->UnlockedTrialsBitArray); + Plugin.package.unlocked_crystalline_conflicts = IStep.ConsumeBitArray(playerState->UnlockedCrystallineConflictsBitArray); + Plugin.package.unlocked_frontlines = IStep.ConsumeBitArray(playerState->UnlockedFrontlinesBitArray); + Plugin.package.cleared_raids = IStep.ConsumeBitArray(playerState->CompletedRaidsBitArray); + Plugin.package.cleared_dungeons = IStep.ConsumeBitArray(playerState->CompletedDungeonsBitArray); + Plugin.package.cleared_guildhests = IStep.ConsumeBitArray(playerState->CompletedGuildOrdersBitArray); + Plugin.package.cleared_trials = IStep.ConsumeBitArray(playerState->CompletedTrialsBitArray); + Plugin.package.cleared_crystalline_conflicts = IStep.ConsumeBitArray(playerState->CompletedCrystallineConflictsBitArray); + Plugin.package.cleared_frontlines = IStep.ConsumeBitArray(playerState->CompletedFrontlinesBitArray); + Plugin.package.cleared_masked_carnivale = IStep.ConsumeBitArray(playerState->CompletedMaskedCarnivaleBitArray); + Plugin.package.unlocked_misc_content = IStep.ConsumeBitArray(playerState->UnlockedMiscContentBitArray); + Plugin.package.cleared_misc_content = IStep.ConsumeBitArray(playerState->CompletedMiscContentBitArray); + + // quest + Plugin.package.completed_quests = IStep.ConsumeBitArray(questManager->CompletedQuestsBitArray); + + // volatile + Plugin.package.position_x = Plugin.ObjectTable.LocalPlayer.Position.X; + Plugin.package.position_y = Plugin.ObjectTable.LocalPlayer.Position.Y; + Plugin.package.position_z = Plugin.ObjectTable.LocalPlayer.Position.Z; + Plugin.package.rotation = Plugin.ObjectTable.LocalPlayer.Rotation; + Plugin.package.zone_id = Plugin.ClientState.TerritoryType; + } + + Completed?.Invoke(); + } + + public string StepName() + { + return "Misc Data"; + } + + public string StepDescription() + { + return "No user action required."; + } + + public void Dispose() + { + } +} diff --git a/dalamud/Auracite/PlaytimeStep.cs b/dalamud/Auracite/Steps/PlaytimeStep.cs similarity index 100% rename from dalamud/Auracite/PlaytimeStep.cs rename to dalamud/Auracite/Steps/PlaytimeStep.cs diff --git a/dalamud/Auracite/Steps/TitleStep.cs b/dalamud/Auracite/Steps/TitleStep.cs new file mode 100644 index 0000000..71d8e4e --- /dev/null +++ b/dalamud/Auracite/Steps/TitleStep.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using Lumina.Excel.Sheets; + +namespace Auracite; + +public class TitleStep : IStep +{ + public TitleStep() + { + } + + public void Dispose() + { + } + + public event IStep.CompletedDelegate? Completed; + + public void Run() + { + unsafe { + var uiState = UIState.Instance(); + var playerState = PlayerState.Instance(); + if (uiState->TitleList.DataReceived) + { + Plugin.package!.titles = new List(uiState->TitleList.TitlesUnlockBitmask.ToArray()); + Plugin.package!.title = IStep.SaveNameValue(((Character*)Plugin.ObjectTable.LocalPlayer!.Address)->TitleId, title => playerState->Sex == 1 ? title.Feminine : title.Masculine); + + Completed?.Invoke(); + } + } + } + + public string StepName() + { + return "Titles"; + } + + public string StepDescription() + { + return "Open the title list."; + } + + public bool NeedsUpdateEveryFrame() + { + return true; // So we can wait for the title window to open. + } +} From 2bce0add9ab702c2ce49bbeeb3441ac2aec6a217 Mon Sep 17 00:00:00 2001 From: Joshua Goins <josh@redstrate.com> Date: Sat, 18 Apr 2026 11:33:53 -0400 Subject: [PATCH 5/8] Export from plugin as a ZIP file Like Auracite did before. --- dalamud/Auracite/Package.cs | 9 ---- dalamud/Auracite/Plugin.cs | 11 ++++- dalamud/Auracite/Steps/AdventurerPlateStep.cs | 28 ++++--------- dalamud/Auracite/Steps/EndStep.cs | 41 +++++++++++++++++-- 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/dalamud/Auracite/Package.cs b/dalamud/Auracite/Package.cs index 4375102..ba789fc 100644 --- a/dalamud/Auracite/Package.cs +++ b/dalamud/Auracite/Package.cs @@ -97,20 +97,11 @@ public class CharacterJson public int voice; // adventurer plate - public string? portrait; public string? plate_title; public bool? plate_title_is_prefix; public string? plate_class_job; public int plate_class_job_level; public string? search_comment; - public string? base_plate; - public string? pattern_overlay; - public string? backing; - public string? top_border; - public string? bottom_border; - public string? portrait_frame; - public string? plate_frame; - public string? accent; public bool is_battle_mentor; public bool is_trade_mentor; diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs index f7acbf8..ea2be56 100644 --- a/dalamud/Auracite/Plugin.cs +++ b/dalamud/Auracite/Plugin.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using SixLabors.ImageSharp; namespace Auracite; @@ -22,6 +22,15 @@ public sealed class Plugin : IDalamudPlugin private readonly StepWindow StepWindow; public static CharacterJson? package; + public static Image? portrait; + public static Image? base_plate; + public static Image? pattern_overlay; + public static Image? backing; + public static Image? top_border; + public static Image? bottom_border; + public static Image? portrait_frame; + public static Image? plate_frame; + public static Image? accent; public Plugin() { diff --git a/dalamud/Auracite/Steps/AdventurerPlateStep.cs b/dalamud/Auracite/Steps/AdventurerPlateStep.cs index d9c4dea..4e24e14 100644 --- a/dalamud/Auracite/Steps/AdventurerPlateStep.cs +++ b/dalamud/Auracite/Steps/AdventurerPlateStep.cs @@ -8,7 +8,6 @@ using SharpDX.Direct3D11; using SharpDX.DXGI; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace Auracite; @@ -34,15 +33,13 @@ public void Run() unsafe { var storage = AgentCharaCard.Instance()->Data; - var image = GetCurrentCharaViewImage(); - Plugin.package.portrait = image.ToBase64String(PngFormat.Instance); + var image = GetCurrentCharaViewImage();; var plateDesign = storage->PlateDesign; if (plateDesign.BasePlate != 0) { - Plugin.package.base_plate = GetImage(ResolveCardBase(plateDesign.BasePlate)) - .ToBase64String(PngFormat.Instance); + Plugin.base_plate = GetImage(ResolveCardBase(plateDesign.BasePlate)); } for (int i = 0; i < plateDesign.NumDecorations; i++) @@ -58,32 +55,27 @@ public void Run() { case AgentCharaCard.DecorationType.PatternOverlay: { - Plugin.package.pattern_overlay = GetImage(ResolveCardDecoration(rowIndex)) - .ToBase64String(PngFormat.Instance); + Plugin.pattern_overlay = GetImage(ResolveCardDecoration(rowIndex)); } break; case AgentCharaCard.DecorationType.Backing: { - Plugin.package.backing = GetImage(ResolveCardDecoration(rowIndex)) - .ToBase64String(PngFormat.Instance); + Plugin.backing = GetImage(ResolveCardDecoration(rowIndex)); } break; case AgentCharaCard.DecorationType.PortraitFrame: { - Plugin.package.portrait_frame = GetImage(ResolveCardDecoration(rowIndex)) - .ToBase64String(PngFormat.Instance); + Plugin.portrait_frame = GetImage(ResolveCardDecoration(rowIndex)); } break; case AgentCharaCard.DecorationType.PlateFrame: { - Plugin.package.plate_frame = GetImage(ResolveCardDecoration(rowIndex)) - .ToBase64String(PngFormat.Instance); + Plugin.plate_frame = GetImage(ResolveCardDecoration(rowIndex)); } break; case AgentCharaCard.DecorationType.Accent: { - Plugin.package.accent = GetImage(ResolveCardDecoration(rowIndex)) - .ToBase64String(PngFormat.Instance); + Plugin.accent = GetImage(ResolveCardDecoration(rowIndex)); } break; } @@ -91,14 +83,12 @@ public void Run() if (plateDesign.TopBorder != 0) { - Plugin.package.top_border = GetImage(ResolveCardHeaderTop(plateDesign.TopBorder)) - .ToBase64String(PngFormat.Instance); + Plugin.top_border = GetImage(ResolveCardHeaderTop(plateDesign.TopBorder)); } if (plateDesign.BottomBorder != 0) { - Plugin.package.bottom_border = GetImage(ResolveCardHeaderBottom(plateDesign.BottomBorder)) - .ToBase64String(PngFormat.Instance); + Plugin.bottom_border = GetImage(ResolveCardHeaderBottom(plateDesign.BottomBorder)); } Plugin.package.plate_title = Title?.Feminine.ToString(); // TODO: Support mascs diff --git a/dalamud/Auracite/Steps/EndStep.cs b/dalamud/Auracite/Steps/EndStep.cs index b0a29a0..69e04f4 100644 --- a/dalamud/Auracite/Steps/EndStep.cs +++ b/dalamud/Auracite/Steps/EndStep.cs @@ -1,8 +1,11 @@ -using System.Text; +using System.IO; +using System.IO.Compression; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; using Newtonsoft.Json; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; namespace Auracite; @@ -43,9 +46,28 @@ public Controller(EndStep endStep) public void GetPackage() { Response.Headers.Set(HttpHeaderNames.AccessControlAllowOrigin, "*"); - Response.ContentType = MimeType.Json; - using var writer = HttpContext.OpenResponseText(Encoding.UTF8, true); - writer.Write(JsonConvert.SerializeObject(Plugin.package)); + Response.ContentType = "application/zip"; + using var writer = HttpContext.OpenResponseStream(true); + + using (var archive = new ZipArchive(writer, ZipArchiveMode.Create, true)) + { + using (var entryStream = archive.CreateEntry("character.json").Open()) + { + using (var streamWriter = new StreamWriter(entryStream)) + { + streamWriter.Write(JsonConvert.SerializeObject(Plugin.package, Formatting.Indented)); + } + } + + WriteImage(archive, Plugin.accent, "accent.png"); + WriteImage(archive, Plugin.backing, "backing.png"); + WriteImage(archive, Plugin.base_plate, "base-plate.png"); + WriteImage(archive, Plugin.pattern_overlay, "pattern-overlay.png"); + WriteImage(archive, Plugin.plate_frame, "plate-frame.png"); + WriteImage(archive, Plugin.portrait, "plate-portrait.png"); + WriteImage(archive, Plugin.top_border, "top-border.png"); + WriteImage(archive, Plugin.bottom_border, "bottom-border.png"); + } } } @@ -77,4 +99,15 @@ public bool IsEnd() { return true; } + + private static void WriteImage(ZipArchive archive, Image? image, string path) + { + if (image != null) + { + using (var entryStream = archive.CreateEntry(path).Open()) + { + image.Save(entryStream, PngFormat.Instance); + } + } + } } From c412447096d6e95f9abbcb637ebd66a659dc0ae8 Mon Sep 17 00:00:00 2001 From: Joshua Goins <josh@redstrate.com> Date: Sat, 18 Apr 2026 12:35:34 -0400 Subject: [PATCH 6/8] Fix-ups while testing import into Kawari --- dalamud/Auracite/Package.cs | 8 +++----- dalamud/Auracite/StepWindow.cs | 2 +- dalamud/Auracite/Steps/EndStep.cs | 2 +- dalamud/Auracite/Steps/MiscStep.cs | 8 +++++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dalamud/Auracite/Package.cs b/dalamud/Auracite/Package.cs index ba789fc..44af4b4 100644 --- a/dalamud/Auracite/Package.cs +++ b/dalamud/Auracite/Package.cs @@ -91,7 +91,7 @@ public class CharacterJson public NameValue race; public List<ClassJobLevel> classjob_levels = new List<ClassJobLevel>(); public NameValue grand_company; - public int grand_company_rank; // TODO: introduce as a NameValue + public List<byte> grand_company_ranks = new List<byte>(); // TODO: introduce as a NameValue public NameValue title; public string playtime; public int voice; @@ -135,8 +135,6 @@ public class CharacterJson public InventoryContainer armory_soul_crystal; public InventoryContainer armory_main_hand; - // Other stuff useful to Kawari: - // unlocks public List<byte> unlocks; public List<byte> seen_active_help; @@ -160,14 +158,14 @@ public class CharacterJson // aetheryte public List<byte> unlocked_aetherytes; - public int hoempoint; + public int homepoint; public List<ushort> favorite_aetherytes; public int free_aetheryte; // classjob public int current_class; public int first_class; - public uint rested_exp; + public int rested_exp; // content public List<byte> unlocked_special_content; diff --git a/dalamud/Auracite/StepWindow.cs b/dalamud/Auracite/StepWindow.cs index 91c7890..3fb0660 100644 --- a/dalamud/Auracite/StepWindow.cs +++ b/dalamud/Auracite/StepWindow.cs @@ -30,7 +30,7 @@ public override void Draw() if (ImGui.Button("Download")) { - Process.Start(new ProcessStartInfo { FileName = "http://localhost:42072/download", UseShellExecute = true }); + Process.Start(new ProcessStartInfo { FileName = "http://localhost:42073/download", UseShellExecute = true }); } ImGui.SameLine(); if (ImGui.Button("Close")) diff --git a/dalamud/Auracite/Steps/EndStep.cs b/dalamud/Auracite/Steps/EndStep.cs index 69e04f4..6b1fa7e 100644 --- a/dalamud/Auracite/Steps/EndStep.cs +++ b/dalamud/Auracite/Steps/EndStep.cs @@ -78,7 +78,7 @@ private void StartWebServer() ShutdownWebServer(); _server = new WebServer(o => o - .WithUrlPrefix("http://localhost:42072/") + .WithUrlPrefix("http://localhost:42073/") .WithMode(HttpListenerMode.EmbedIO)) .WithWebApi("/", m => m.WithController(() => new Controller(this))); _server.RunAsync(); diff --git a/dalamud/Auracite/Steps/MiscStep.cs b/dalamud/Auracite/Steps/MiscStep.cs index a33543a..21e1a7e 100644 --- a/dalamud/Auracite/Steps/MiscStep.cs +++ b/dalamud/Auracite/Steps/MiscStep.cs @@ -50,7 +50,9 @@ public void Run() } } Plugin.package!.grand_company = IStep.SaveNameValue<GrandCompany>(playerState->GrandCompany, company => company.Name); - Plugin.package!.grand_company_rank = playerState->GetGrandCompanyRank(); + Plugin.package!.grand_company_ranks.Add(playerState->GCRankMaelstrom); + Plugin.package!.grand_company_ranks.Add(playerState->GCRankTwinAdders); + Plugin.package!.grand_company_ranks.Add(playerState->GCRankImmortalFlames); Plugin.package.is_battle_mentor = PlayerState.Instance()->IsBattleMentor(); Plugin.package.is_trade_mentor = PlayerState.Instance()->IsTradeMentor(); @@ -87,14 +89,14 @@ public void Run() // aetherytes Plugin.package.unlocked_aetherytes = IStep.ConsumeBitArray(uiState->UnlockedAetherytesBitArray); - Plugin.package.hoempoint = playerState->HomeAetheryteId; + Plugin.package.homepoint = playerState->HomeAetheryteId; Plugin.package.favorite_aetherytes = new List<ushort>(playerState->FavouriteAetherytes.ToArray()); Plugin.package.free_aetheryte = playerState->FreeAetheryteId; // classjob Plugin.package.current_class = playerState->CurrentClassJobId; Plugin.package.first_class = playerState->FirstClass; - Plugin.package.rested_exp = playerState->BaseRestedExperience; + Plugin.package.rested_exp = (int)playerState->BaseRestedExperience; // content Plugin.package.unlocked_special_content = IStep.ConsumeBitArray(playerState->UnlockedSpecialContentBitArray); From f9e5fd5f436e160e59ec23883acc700f912633ec Mon Sep 17 00:00:00 2001 From: Joshua Goins <josh@redstrate.com> Date: Sat, 18 Apr 2026 12:42:39 -0400 Subject: [PATCH 7/8] Remove web interface for Auracite I folded this all into the Dalamud plugin, for the main reason that the Lodestone scraper part is practically useless. We want Auracite to be *the* way for people to archive and import their character and only using the website is a dead end. --- .github/workflows/main.yml | 30 - dalamud/Auracite.sln => Auracite.sln | 0 .../Auracite => Auracite}/Auracite.csproj | 0 {dalamud/Auracite => Auracite}/Auracite.json | 0 {dalamud/Auracite => Auracite}/IStep.cs | 0 {dalamud/Auracite => Auracite}/Package.cs | 0 {dalamud/Auracite => Auracite}/Plugin.cs | 0 {dalamud/Auracite => Auracite}/StepWindow.cs | 0 .../Steps/AdventurerPlateStep.cs | 0 .../Steps/AppearanceStep.cs | 0 .../Auracite => Auracite}/Steps/EndStep.cs | 0 .../Steps/InventoryStep.cs | 0 .../Auracite => Auracite}/Steps/MiscStep.cs | 0 .../Steps/PlaytimeStep.cs | 0 .../Auracite => Auracite}/Steps/TitleStep.cs | 0 .../Auracite => Auracite}/packages.lock.json | 0 Cargo.lock | 2348 ----------------- Cargo.toml | 65 - README.md | 19 +- scripts/build-web.sh | 3 - scripts/copy-web-to-server.sh | 4 - src/data.rs | 172 -- src/downloader.rs | 21 - src/html.rs | 46 - src/lib.rs | 433 --- src/package.rs | 94 - src/parser.rs | 244 -- src/value.rs | 382 --- templates/character.html | 17 - templates/plate.html | 28 - web/auracite.ico | Bin 5558 -> 0 bytes web/auracite.svg | 21 - web/index.html | 182 -- 33 files changed, 4 insertions(+), 4105 deletions(-) rename dalamud/Auracite.sln => Auracite.sln (100%) rename {dalamud/Auracite => Auracite}/Auracite.csproj (100%) rename {dalamud/Auracite => Auracite}/Auracite.json (100%) rename {dalamud/Auracite => Auracite}/IStep.cs (100%) rename {dalamud/Auracite => Auracite}/Package.cs (100%) rename {dalamud/Auracite => Auracite}/Plugin.cs (100%) rename {dalamud/Auracite => Auracite}/StepWindow.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/AdventurerPlateStep.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/AppearanceStep.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/EndStep.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/InventoryStep.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/MiscStep.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/PlaytimeStep.cs (100%) rename {dalamud/Auracite => Auracite}/Steps/TitleStep.cs (100%) rename {dalamud/Auracite => Auracite}/packages.lock.json (100%) delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100755 scripts/build-web.sh delete mode 100755 scripts/copy-web-to-server.sh delete mode 100644 src/data.rs delete mode 100644 src/downloader.rs delete mode 100644 src/html.rs delete mode 100644 src/lib.rs delete mode 100644 src/package.rs delete mode 100644 src/parser.rs delete mode 100644 src/value.rs delete mode 100644 templates/character.html delete mode 100644 templates/plate.html delete mode 100644 web/auracite.ico delete mode 100644 web/auracite.svg delete mode 100644 web/index.html diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ea741e..9bf43cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,33 +17,3 @@ jobs: - uses: actions/checkout@v4 - name: Build Plugin uses: redstrate/build-dalamud-plugin@main - with: - solution-dir: dalamud - - build-webassembly: - name: "Build" - runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name - - steps: - - uses: actions/checkout@v4 - - name: Add WebAssembly Target - run: | - rustup target add wasm32-unknown-unknown - - uses: actions/cache@v4 - id: cache-deps - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Install wasm-pack - if: steps.cache-deps.outputs.cache-hit != 'true' - run: | - cargo install wasm-pack - - name: Build - run: | - ./scripts/build-web.sh diff --git a/dalamud/Auracite.sln b/Auracite.sln similarity index 100% rename from dalamud/Auracite.sln rename to Auracite.sln diff --git a/dalamud/Auracite/Auracite.csproj b/Auracite/Auracite.csproj similarity index 100% rename from dalamud/Auracite/Auracite.csproj rename to Auracite/Auracite.csproj diff --git a/dalamud/Auracite/Auracite.json b/Auracite/Auracite.json similarity index 100% rename from dalamud/Auracite/Auracite.json rename to Auracite/Auracite.json diff --git a/dalamud/Auracite/IStep.cs b/Auracite/IStep.cs similarity index 100% rename from dalamud/Auracite/IStep.cs rename to Auracite/IStep.cs diff --git a/dalamud/Auracite/Package.cs b/Auracite/Package.cs similarity index 100% rename from dalamud/Auracite/Package.cs rename to Auracite/Package.cs diff --git a/dalamud/Auracite/Plugin.cs b/Auracite/Plugin.cs similarity index 100% rename from dalamud/Auracite/Plugin.cs rename to Auracite/Plugin.cs diff --git a/dalamud/Auracite/StepWindow.cs b/Auracite/StepWindow.cs similarity index 100% rename from dalamud/Auracite/StepWindow.cs rename to Auracite/StepWindow.cs diff --git a/dalamud/Auracite/Steps/AdventurerPlateStep.cs b/Auracite/Steps/AdventurerPlateStep.cs similarity index 100% rename from dalamud/Auracite/Steps/AdventurerPlateStep.cs rename to Auracite/Steps/AdventurerPlateStep.cs diff --git a/dalamud/Auracite/Steps/AppearanceStep.cs b/Auracite/Steps/AppearanceStep.cs similarity index 100% rename from dalamud/Auracite/Steps/AppearanceStep.cs rename to Auracite/Steps/AppearanceStep.cs diff --git a/dalamud/Auracite/Steps/EndStep.cs b/Auracite/Steps/EndStep.cs similarity index 100% rename from dalamud/Auracite/Steps/EndStep.cs rename to Auracite/Steps/EndStep.cs diff --git a/dalamud/Auracite/Steps/InventoryStep.cs b/Auracite/Steps/InventoryStep.cs similarity index 100% rename from dalamud/Auracite/Steps/InventoryStep.cs rename to Auracite/Steps/InventoryStep.cs diff --git a/dalamud/Auracite/Steps/MiscStep.cs b/Auracite/Steps/MiscStep.cs similarity index 100% rename from dalamud/Auracite/Steps/MiscStep.cs rename to Auracite/Steps/MiscStep.cs diff --git a/dalamud/Auracite/Steps/PlaytimeStep.cs b/Auracite/Steps/PlaytimeStep.cs similarity index 100% rename from dalamud/Auracite/Steps/PlaytimeStep.cs rename to Auracite/Steps/PlaytimeStep.cs diff --git a/dalamud/Auracite/Steps/TitleStep.cs b/Auracite/Steps/TitleStep.cs similarity index 100% rename from dalamud/Auracite/Steps/TitleStep.cs rename to Auracite/Steps/TitleStep.cs diff --git a/dalamud/Auracite/packages.lock.json b/Auracite/packages.lock.json similarity index 100% rename from dalamud/Auracite/packages.lock.json rename to Auracite/packages.lock.json diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 6b697e9..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,2348 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "auracite" -version = "0.1.0" -dependencies = [ - "ahash", - "base64", - "console_error_panic_hook", - "getrandom 0.3.4", - "minijinja", - "physis", - "regex", - "reqwest", - "scraper", - "serde", - "serde_json", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-time", - "zip", -] - -[[package]] -name = "aws-lc-rs" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.37.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "binrw" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81419ff39e6ed10a92a7f125290859776ced35d9a08a665ae40b23e7ca702f30" -dependencies = [ - "array-init", - "binrw_derive", - "bytemuck", -] - -[[package]] -name = "binrw_derive" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "376404e55ec40d0d6f8b4b7df3f87b87954bd987f0cf9a7207ea3b6ea5c9add4" -dependencies = [ - "either", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "ego-tree" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "html5ever" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6452c4751a24e1b99c3260d505eaeee76a050573e61f30ac2c924ddc7236f01e" -dependencies = [ - "log", - "markup5ever", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "libz-rs-sys" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" -dependencies = [ - "zlib-rs", -] - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "markup5ever" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c3294c4d74d0742910f8c7b466f44dda9eb2d5742c1e430138df290a1e8451c" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minijinja" -version = "2.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c54f3bcc034dd74496b5ca929fd0b710186672d5ff0b0f255a9ceb259042ece" -dependencies = [ - "serde", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "physis" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51018fe6a91cd6292c89de282fa4426465080d56a0d62ae21628edc95af23a93" -dependencies = [ - "binrw", - "bitflags", - "half", - "libz-rs-sys", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scraper" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93cecd86d6259499c844440546d02f55f3e17bd286e529e48d1f9f67e92315cb" -dependencies = [ - "cssparser", - "ego-tree", - "getopts", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "selectors" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" -dependencies = [ - "bitflags", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zip" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72" -dependencies = [ - "crc32fast", - "indexmap", - "memchr", - "typed-path", -] - -[[package]] -name = "zlib-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 2edbf67..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "auracite" -version = "0.1.0" -edition = "2024" -description = "Export your FFXIV character in portable, generic formats" -repository = "https://github.com/redstrate/Auracite" -license = "AGPL-3" - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -debug = ["dep:console_error_panic_hook"] - -[dependencies] -# Used to scrape the Lodestone HTML pages -scraper = { version = "0.25" } - -# Used to serialize the JSON data we export -serde = { version = "1.0", features = ["derive"], default-features = false } - -# Used to do some misc regex operations during scraping -regex = { version = "1.12", default-features = false, features = ["unicode-perl"] } - -# Used to generate the HTML page to easily preview your exported data -minijinja = { version = "2.16", features = ["serde"], default-features = false } - -# Download files -reqwest = { version = "0.13" } - -# Zip the character archive -zip = { version = "8.1", default-features = false } - -# Exporting propietary game data -physis = { version = "0.5" } - -# Encoding the character archive to base64 so the browser can download it and decoding the base64 images from the client -base64 = { version = "0.22", default-features = false } - -# Not used directly by us, but to disable the "std" feature and is used by the scraper crate. -ahash = { version = "0.8", default-features = false } - -[target.'cfg(target_family = "wasm")'.dependencies] -# Used to generate the WebAssembly version -wasm-bindgen = { version = "0.2", default-features = false } -wasm-bindgen-futures = { version = "0.4", default-features = false } - -# For async -tokio = { version = "1.49", features = ["rt", "macros"], default-features = false } - -serde_json = { version = "1.0", default-features = false, features = ["alloc"] } - -console_error_panic_hook = { version = "0.1", optional = true } - -# to fix some dependency that doesn't enable the feature -getrandom = { version = "0.3", features = ["wasm_js"] } - -# Because SystemTime doesn't work on Web -web-time = "1.1" - -[target.'cfg(not(target_family = "wasm"))'.dependencies] -# For async -tokio = { version = "1.49", features = ["rt", "rt-multi-thread", "macros"], default-features = false } - -serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/README.md b/README.md index 3bdf699..6d157ba 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,13 @@ # Auracite -Export your FFXIV character in portable, generic formats. This includes +Archive your FFXIV character in a portable, generic format. This includes data in machine-readable JSON (and can be imported by other programs -like [Kawari](https://github.com/redstrate/Kawari)) and a Lodestone-esque -HTML page which you can display in your browser. +like [Kawari](https://github.com/redstrate/Kawari).) ## Usage -Auracite runs inside your web browser, and can be accessed at [auracite.xiv.zone](https://auracite.xiv.zone/). - -## Building - -Use `wasm-pack` or the helper script `scripts/build-web.sh`. A folder called `pkg/` will be generated, and the HTML files live in `web/`. - -### Dalamud Mode - -Auracite can only collect so much data from the Lodestone, some data can only be collected when logged in. To do this, -we provide a Dalamud plugin to run alongside the tool. The plugin is currently available -[in my personal Dalamud repository](https://github.com/redstrate/DalamudPlugins). The plugin can be -safely removed if you're done using Auracite. +It works by running in Dalamud, and the plugin is currently available +[in my personal Dalamud repository](https://github.com/redstrate/DalamudPlugins). ## License diff --git a/scripts/build-web.sh b/scripts/build-web.sh deleted file mode 100755 index a220df5..0000000 --- a/scripts/build-web.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -RUSTFLAGS='--cfg getrandom_backend="wasm_js"' wasm-pack build --target web --release --no-pack --no-typescript diff --git a/scripts/copy-web-to-server.sh b/scripts/copy-web-to-server.sh deleted file mode 100755 index abb3a68..0000000 --- a/scripts/copy-web-to-server.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -rsync -e "ssh -p 38901 -o StrictHostKeyChecking=no" --recursive web/ ryne.moe:/srv/http/auracite.xiv.zone/ && -rsync -e "ssh -p 38901 -o StrictHostKeyChecking=no" --recursive pkg/ ryne.moe:/srv/http/auracite.xiv.zone/pkg/ diff --git a/src/data.rs b/src/data.rs deleted file mode 100644 index 7d7d3d0..0000000 --- a/src/data.rs +++ /dev/null @@ -1,172 +0,0 @@ -use serde::Serialize; - -use crate::{ - package::InventoryContainer, - value::{ - CityStateValue, ClassJobValue, GenderValue, GrandCompanyValue, GuardianValue, ItemValue, - NamedayValue, RaceValue, TribeValue, WorldValue, - }, -}; - -#[derive(Default, Serialize)] -pub struct Currencies { - pub gil: u32, -} - -#[derive(Default, Serialize)] -pub struct Appearance { - pub race: String, - pub gender: String, - pub model_type: i32, - pub height: i32, - pub tribe: String, - pub face_type: i32, - pub hair_style: i32, - pub has_highlights: bool, - pub skin_color: i32, - pub eye_color: i32, - pub hair_color: i32, - pub hair_color2: i32, - pub face_features: i32, - pub face_features_color: i32, - pub eyebrows: i32, - pub eye_color2: i32, - pub eye_shape: i32, - pub nose_shape: i32, - pub jaw_shape: i32, - pub lip_style: i32, - pub lip_color: i32, - pub race_feature_size: i32, - pub race_feature_type: i32, - pub bust_size: i32, - pub facepaint: i32, - pub facepaint_color: i32, -} - -#[derive(Default, Serialize)] -pub struct EquippedItems { - #[serde(skip_serializing_if = "Option::is_none")] - pub main_hand: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub off_hand: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub head: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub body: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub hands: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub legs: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub feet: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub earrings: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub necklace: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub bracelets: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub left_ring: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub right_ring: Option<ItemValue>, - #[serde(skip_serializing_if = "Option::is_none")] - pub soul_crystal: Option<ItemValue>, -} - -#[derive(Default, Serialize)] -pub struct CharacterData { - pub name: String, - pub world: WorldValue, - pub data_center: String, - pub city_state: CityStateValue, - pub nameday: NamedayValue, - pub guardian: GuardianValue, - pub race: RaceValue, - pub gender: GenderValue, - pub tribe: TribeValue, - pub classjob_levels: Vec<ClassJobValue>, - pub grand_company: GrandCompanyValue, - #[serde(skip_serializing_if = "Option::is_none")] - pub free_company: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option<String>, - pub equipped: EquippedItems, - - #[serde(skip_serializing_if = "Option::is_none")] - pub currencies: Option<Currencies>, - #[serde(skip_serializing_if = "Option::is_none")] - pub playtime: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub appearance: Option<Appearance>, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_battle_mentor: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_trade_mentor: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_novice: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_returner: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub player_commendations: Option<i32>, - #[serde(skip_serializing_if = "Option::is_none")] - pub plate_title: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub plate_classjob: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub plate_classjob_level: Option<i32>, - #[serde(skip_serializing_if = "Option::is_none")] - pub search_comment: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub voice: Option<i32>, - - #[serde(skip)] - pub face_url: String, - #[serde(skip)] - pub portrait_url: String, - - // inventory - #[serde(skip_serializing_if = "Option::is_none")] - pub inventory1: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub inventory2: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub inventory3: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub inventory4: Option<InventoryContainer>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub equipped_items: Option<InventoryContainer>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub currency: Option<InventoryContainer>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_off_hand: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_head: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_body: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_hands: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_waist: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_legs: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_ear: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_neck: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_wrist: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_rings: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_soul_crystal: Option<InventoryContainer>, - #[serde(skip_serializing_if = "Option::is_none")] - pub armory_main_hand: Option<InventoryContainer>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub unlock_flags: Option<Vec<u8>>, - #[serde(skip_serializing_if = "Option::is_none")] - pub unlock_aetherytes: Option<Vec<u8>>, -} diff --git a/src/downloader.rs b/src/downloader.rs deleted file mode 100644 index 8fd4cad..0000000 --- a/src/downloader.rs +++ /dev/null @@ -1,21 +0,0 @@ -use reqwest::Url; - -pub async fn download(url: &Url) -> Result<Vec<u8>, reqwest::Error> { - let client; - - #[cfg(target_family = "wasm")] - { - client = reqwest::Client::builder(); - } - - #[cfg(not(target_family = "wasm"))] - { - client = reqwest::Client::builder().no_proxy(); // This fixes localhost connections... for some reason (https://github.com/seanmonstar/reqwest/issues/913) - } - - let client = client.build()?; - - let body = client.get(url.to_string()).send().await; - - Ok(body?.bytes().await?.to_vec()) -} diff --git a/src/html.rs b/src/html.rs deleted file mode 100644 index ff8532c..0000000 --- a/src/html.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::data::CharacterData; -use minijinja::{Environment, context}; - -/// Writes a visual HTML for `char_data` to `file_path`. -/// This vaguely represents Lodestone and designed to visually check your character data. -pub fn create_character_html(char_data: &CharacterData) -> String { - let mut env = Environment::new(); - env.add_template( - "character.html", - include_str!("../templates/character.html"), - ) - .unwrap(); - let template = env.get_template("character.html").unwrap(); - template - .render(context! { - name => char_data.name, - world => char_data.world.name, - data_center => char_data.data_center, - race => char_data.race.name, - subrace => char_data.tribe.name, - gender => char_data.gender.name, - nameday => char_data.nameday.value, - city_state => char_data.city_state.name - }) - .unwrap() -} - -/// Writes a visual HTML for `char_data` to `file_path`. -/// This vaguely represents Lodestone and designed to visually check your character data. -pub fn create_plate_html(char_data: &CharacterData) -> String { - let mut env = Environment::new(); - env.add_template("plate.html", include_str!("../templates/plate.html")) - .unwrap(); - let template = env.get_template("plate.html").unwrap(); - template - .render(context! { - name => char_data.name, - world => char_data.world.name, - data_center => char_data.data_center, - title => char_data.plate_title, - level => char_data.plate_classjob_level, - class => char_data.plate_classjob, - search_comment => char_data.search_comment, - }) - .unwrap() -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 2f722bd..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,433 +0,0 @@ -pub mod data; -pub mod downloader; -pub mod html; -pub mod package; -pub mod parser; -pub mod value; - -use crate::data::CharacterData; -use crate::downloader::download; -use crate::html::{create_character_html, create_plate_html}; -use crate::parser::parse_search; -use base64::prelude::*; -use data::Appearance; -use package::Package; -use physis::savedata::chardat; -use regex::Regex; -use reqwest::Url; -use std::io::Write; -#[cfg(target_family = "wasm")] -use wasm_bindgen::JsValue; -#[cfg(target_family = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; -use web_time::{SystemTime, UNIX_EPOCH}; -use zip::ZipWriter; -use zip::result::ZipError; -use zip::write::SimpleFileOptions; - -/// The main Lodestone domain -const LODESTONE_HOST: &str = "https://na.finalfantasyxiv.com"; - -/// The Lodestone proxy used in WebAssembly builds. Needed for CORS and cookie injection. -const LODESTONE_TUNNEL_HOST: &str = "https://lodestone-tunnel.ryne.moe"; - -/// The image domain. -const IMAGE_HOST: &str = "img2.finalfantasyxiv.com"; - -/// The image proxy used in WebAssembly builds. Needed for CORS. -const IMAGE_TUNNEL_HOST: &str = "img-tunnel.ryne.moe"; - -#[derive(Debug)] -pub enum ArchiveError { - DownloadFailed(String), - CharacterNotFound, - ParsingError, - CouldNotConnectToDalamud, - UnknownError, -} - -impl From<ZipError> for ArchiveError { - fn from(_: ZipError) -> Self { - ArchiveError::UnknownError - } -} - -impl From<std::io::Error> for ArchiveError { - fn from(_: std::io::Error) -> Self { - ArchiveError::UnknownError - } -} - -impl From<physis::Error> for ArchiveError { - fn from(_: physis::Error) -> Self { - ArchiveError::UnknownError - } -} - -#[cfg(target_family = "wasm")] -impl From<ArchiveError> for JsValue { - fn from(err: ArchiveError) -> Self { - match err { - // TODO: give JS the URL that failed to download - ArchiveError::DownloadFailed(_) => JsValue::from_str(&"download_failed".to_string()), - ArchiveError::CharacterNotFound => { - JsValue::from_str(&"character_not_found".to_string()) - } - ArchiveError::ParsingError => JsValue::from_str(&"parsing_error".to_string()), - ArchiveError::UnknownError => JsValue::from_str(&"unknown_error".to_string()), - ArchiveError::CouldNotConnectToDalamud => { - JsValue::from_str(&"could_not_connect_to_dalamud".to_string()) - } - } - } -} - -/// Searches for the character by their name. -pub async fn search_character(character_name: &str) -> Option<u64> { - let lodestone_host = if cfg!(target_family = "wasm") { - LODESTONE_TUNNEL_HOST - } else { - LODESTONE_HOST - }; - - let search_url = Url::parse_with_params( - &format!("{lodestone_host}/lodestone/character?"), - &[("q", character_name)], - ) - .map_err(|_| ArchiveError::UnknownError) - .ok()?; - let search_page = download(&search_url) - .await - .map_err(|_| ArchiveError::DownloadFailed(search_url.to_string())) - .ok()?; - let search_page = String::from_utf8(search_page) - .map_err(|_| ArchiveError::ParsingError) - .ok()?; - - let href = parse_search(&search_page); - if href.is_empty() { - return None; - } - - let re = Regex::new(r"\/lodestone\/character\/(\d+)").ok()?; - let captures = re.captures(&href)?; - - captures.get(1)?.as_str().parse().ok() -} - -/// Archives the character named `character_name` and gives a ZIP file as bytes that can be written to disk. -pub async fn archive_character(id: u64, use_dalamud: bool) -> Result<Vec<u8>, ArchiveError> { - let lodestone_host = if cfg!(target_family = "wasm") { - LODESTONE_TUNNEL_HOST - } else { - LODESTONE_HOST - }; - - let char_page_url = Url::parse(&format!("{lodestone_host}/lodestone/character/{id}/")) - .map_err(|_| ArchiveError::UnknownError)?; - let char_page = download(&char_page_url) - .await - .map_err(|_| ArchiveError::DownloadFailed(char_page_url.to_string()))?; - let char_page = String::from_utf8(char_page).map_err(|_| ArchiveError::ParsingError)?; - - let mut char_data = CharacterData::default(); - parser::parse_profile(&char_page, &mut char_data); - - let classjob_page_url = Url::parse(&format!( - "{lodestone_host}/lodestone/character/{id}/class_job/" - )) - .map_err(|_| ArchiveError::UnknownError)?; - let classjob_page = download(&classjob_page_url) - .await - .map_err(|_| ArchiveError::DownloadFailed(classjob_page_url.to_string()))?; - let char_page = String::from_utf8(classjob_page).map_err(|_| ArchiveError::ParsingError)?; - - parser::parse_classjob(&char_page, &mut char_data); - - let mut buf = Vec::new(); - let mut zip = ZipWriter::new(std::io::Cursor::new(&mut buf)); - - let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); - - if !char_data.portrait_url.is_empty() { - let portrait_url = if cfg!(target_family = "wasm") { - &char_data - .portrait_url - .replace(IMAGE_HOST, IMAGE_TUNNEL_HOST) - } else { - &char_data.portrait_url - }; - let portrait_url = Url::parse(portrait_url).map_err(|_| ArchiveError::UnknownError)?; - - let portrait = download(&portrait_url) - .await - .map_err(|_| ArchiveError::DownloadFailed(portrait_url.to_string()))?; - - zip.start_file("portrait.jpg", options)?; - zip.write_all(&portrait)?; - } - if !char_data.face_url.is_empty() { - let face_url = if cfg!(target_family = "wasm") { - &char_data.face_url.replace(IMAGE_HOST, IMAGE_TUNNEL_HOST) - } else { - &char_data.face_url - }; - let face_url = Url::parse(face_url).map_err(|_| ArchiveError::UnknownError)?; - - let face = download(&face_url) - .await - .map_err(|_| ArchiveError::DownloadFailed(face_url.to_string()))?; - - zip.start_file("face.jpg", options)?; - zip.write_all(&face)?; - } - - if use_dalamud { - let dalamud_url = - Url::parse("http://localhost:42072/package").map_err(|_| ArchiveError::UnknownError)?; - let package = download(&dalamud_url) - .await - .map_err(|_| ArchiveError::CouldNotConnectToDalamud)?; - let package = String::from_utf8(package).map_err(|_| ArchiveError::ParsingError)?; - // Remove BOM at the start - let package = package.trim_start_matches("\u{feff}"); - let package: Package = - serde_json::from_str(package.trim_start()).map_err(|_| ArchiveError::ParsingError)?; - - // appearance data - char_data.appearance = Some(Appearance { - race: char_data.race.name.clone(), - tribe: char_data.tribe.name.clone(), - gender: char_data.gender.name.clone(), - model_type: package.model_type, - height: package.height, - face_type: package.face_type, - hair_style: package.hair_style, - has_highlights: package.has_highlights, - skin_color: package.skin_color, - eye_color: package.eye_color, - hair_color: package.hair_color, - hair_color2: package.hair_color2, - face_features: package.face_features, - face_features_color: package.face_features_color, - eyebrows: package.eyebrows, - eye_color2: package.eye_color2, - eye_shape: package.eye_color2, - nose_shape: package.nose_shape, - jaw_shape: package.jaw_shape, - lip_style: package.lip_style, - lip_color: package.lip_color, - race_feature_size: package.race_feature_size, - race_feature_type: package.race_feature_type, - bust_size: package.bust_size, - facepaint: package.facepaint, - facepaint_color: package.facepaint_color, - }); - - char_data.inventory1 = Some(package.inventory1); - char_data.inventory2 = Some(package.inventory2); - char_data.inventory3 = Some(package.inventory3); - char_data.inventory4 = Some(package.inventory4); - - char_data.equipped_items = Some(package.equipped_items); - - char_data.currency = Some(package.currency); - - char_data.armory_off_hand = Some(package.armory_off_hand); - char_data.armory_head = Some(package.armory_head); - char_data.armory_body = Some(package.armory_body); - char_data.armory_hands = Some(package.armory_hands); - char_data.armory_waist = Some(package.armory_waist); - char_data.armory_legs = Some(package.armory_legs); - char_data.armory_ear = Some(package.armory_ear); - char_data.armory_neck = Some(package.armory_neck); - char_data.armory_wrist = Some(package.armory_wrist); - char_data.armory_rings = Some(package.armory_rings); - char_data.armory_soul_crystal = Some(package.armory_soul_crystal); - char_data.armory_main_hand = Some(package.armory_main_hand); - - char_data.playtime = Some(package.playtime.parse().unwrap()); - char_data.is_battle_mentor = Some(package.is_battle_mentor); - char_data.is_trade_mentor = Some(package.is_trade_mentor); - char_data.is_novice = Some(package.is_novice); - char_data.is_returner = Some(package.is_returner); - char_data.player_commendations = Some(package.player_commendations); // TODO: fetch from the lodestone? - char_data.plate_title = package.plate_title; - char_data.plate_classjob = package.plate_class_job; - char_data.plate_classjob_level = Some(package.plate_class_job_level); - char_data.search_comment = package.search_comment; - char_data.voice = Some(package.voice); - char_data.unlock_flags = Some(package.unlock_flags); - char_data.unlock_aetherytes = Some(package.unlock_aetherytes); - - if let Some(portrait) = package.portrait { - zip.start_file("plate-portrait.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(portrait.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(base_plate) = package.base_plate { - zip.start_file("base-plate.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(base_plate.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(pattern_overlay) = package.pattern_overlay { - zip.start_file("pattern-overlay.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(pattern_overlay.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(backing) = package.backing { - zip.start_file("backing.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(backing.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(top_border) = package.top_border { - zip.start_file("top-border.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(top_border.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(bottom_border) = package.bottom_border { - zip.start_file("bottom-border.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(bottom_border.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(portrait_frame) = package.portrait_frame { - zip.start_file("portrait-frame.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(portrait_frame.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(plate_frame) = package.plate_frame { - zip.start_file("plate-frame.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(plate_frame.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - if let Some(accent) = package.accent { - zip.start_file("accent.png", options)?; - zip.write_all( - &BASE64_STANDARD - .decode(accent.trim_start_matches("data:image/png;base64,")) - .unwrap(), - )?; - } - - let timestamp: u32 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Failed to get UNIX timestamp!") - .as_secs() - .try_into() - .unwrap(); - - let char_dat = chardat::CharacterData { - version: 7, - customize: chardat::CustomizeData { - race: (package.race as u8).try_into()?, - gender: (package.gender as u8).try_into()?, - age: package.model_type as u8, - height: package.height as u8, - tribe: (package.tribe as u8).try_into()?, - face: package.face_type as u8, - hair: package.hair_style as u8, - enable_highlights: package.has_highlights, - skin_tone: package.skin_color as u8, - right_eye_color: package.eye_color as u8, - hair_tone: package.hair_color as u8, - highlights: package.hair_color2 as u8, - facial_features: package.face_features as u8, - facial_feature_color: package.face_features_color as u8, - eyebrows: package.eyebrows as u8, - left_eye_color: package.eye_color2 as u8, - eyes: package.eye_shape as u8, - nose: package.nose_shape as u8, - jaw: package.jaw_shape as u8, - mouth: package.lip_style as u8, - lips_tone_fur_pattern: package.lip_color as u8, - race_feature_size: package.race_feature_size as u8, - race_feature_type: package.race_feature_type as u8, - bust: package.bust_size as u8, - face_paint: package.facepaint as u8, - face_paint_color: package.facepaint_color as u8, - voice: package.voice as u8, - }, - timestamp, - comment: "Generated by Auracite".to_string(), - }; - - zip.start_file("FFXIV_CHARA_01.dat", options)?; - zip.write_all(&char_dat.write_to_buffer().unwrap())?; - - zip.start_file("plate.html", options)?; - zip.write_all(create_plate_html(&char_data).as_ref())?; - - // Stop the HTTP server - let stop_url = - Url::parse("http://localhost:42072/stop").map_err(|_| ArchiveError::UnknownError)?; - // I'm intentionally ignoring the message because it doesn't matter if it fails - and it usually does - let _ = download(&stop_url).await; - } - - zip.start_file("character.json", options)?; - zip.write_all(serde_json::to_string_pretty(&char_data).unwrap().as_ref())?; - - zip.start_file("character.html", options)?; - zip.write_all(create_character_html(&char_data).as_ref())?; - - zip.finish()?; - - Ok(buf) -} - -/// Archives the character `id` and converts the ZIP file to Base64. Useful for downloading via data URIs. -#[cfg(target_family = "wasm")] -#[wasm_bindgen] -pub async extern "C" fn archive_character_base64( - id: u64, - use_dalamud: bool, -) -> Result<String, ArchiveError> { - #[cfg(feature = "debug")] - console_error_panic_hook::set_once(); - - let buf: String = archive_character(id, use_dalamud) - .await - .map(|x| BASE64_STANDARD.encode(x))?; - return Ok(format!("data:application/octet-stream;charset=utf-16le;base64,{buf}").into()); -} - -#[cfg(target_family = "wasm")] -#[wasm_bindgen] -pub async extern "C" fn search_for_character(name: &str) -> Option<u64> { - #[cfg(feature = "debug")] - console_error_panic_hook::set_once(); - - search_character(name).await -} diff --git a/src/package.rs b/src/package.rs deleted file mode 100644 index ac75b97..0000000 --- a/src/package.rs +++ /dev/null @@ -1,94 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Default, Deserialize, Serialize, Clone)] -pub struct InventoryItem { - pub slot: i32, - pub quantity: u32, - pub condition: i32, - pub id: u32, - pub glamour_id: u32, -} - -#[derive(Default, Deserialize, Serialize, Clone)] -pub struct InventoryContainer { - pub items: Vec<InventoryItem>, -} - -#[derive(Default, Deserialize, Clone)] -pub struct Package { - pub playtime: String, - pub is_battle_mentor: bool, - pub is_trade_mentor: bool, - pub is_novice: bool, - pub is_returner: bool, - pub player_commendations: i32, - pub portrait: Option<String>, - pub plate_title: Option<String>, - pub plate_title_is_prefix: Option<bool>, - pub plate_class_job: Option<String>, - pub plate_class_job_level: i32, - pub search_comment: Option<String>, - pub base_plate: Option<String>, - pub pattern_overlay: Option<String>, - pub backing: Option<String>, - pub top_border: Option<String>, - pub bottom_border: Option<String>, - pub portrait_frame: Option<String>, - pub plate_frame: Option<String>, - pub accent: Option<String>, - pub voice: i32, - - // Appearance - pub race: i32, - pub gender: i32, - pub model_type: i32, - pub height: i32, - pub tribe: i32, - pub face_type: i32, - pub hair_style: i32, - pub has_highlights: bool, - pub skin_color: i32, - pub eye_color: i32, - pub hair_color: i32, - pub hair_color2: i32, - pub face_features: i32, - pub face_features_color: i32, - pub eyebrows: i32, - pub eye_color2: i32, - pub eye_shape: i32, - pub nose_shape: i32, - pub jaw_shape: i32, - pub lip_style: i32, - pub lip_color: i32, - pub race_feature_size: i32, - pub race_feature_type: i32, - pub bust_size: i32, - pub facepaint: i32, - pub facepaint_color: i32, - - // inventory - pub inventory1: InventoryContainer, - pub inventory2: InventoryContainer, - pub inventory3: InventoryContainer, - pub inventory4: InventoryContainer, - - pub equipped_items: InventoryContainer, - - pub currency: InventoryContainer, - - pub armory_off_hand: InventoryContainer, - pub armory_head: InventoryContainer, - pub armory_body: InventoryContainer, - pub armory_hands: InventoryContainer, - pub armory_waist: InventoryContainer, - pub armory_legs: InventoryContainer, - pub armory_ear: InventoryContainer, - pub armory_neck: InventoryContainer, - pub armory_wrist: InventoryContainer, - pub armory_rings: InventoryContainer, - pub armory_soul_crystal: InventoryContainer, - pub armory_main_hand: InventoryContainer, - - pub unlock_flags: Vec<u8>, - pub unlock_aetherytes: Vec<u8>, -} diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index 98fa8f2..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::{ - data::CharacterData, - value::{ - CityStateValue, ClassJobValue, GenderValue, GuardianValue, ItemValue, NamedayValue, - RaceValue, TribeValue, WorldValue, - }, -}; -use regex::Regex; -use scraper::{Html, Selector}; - -const ENTRY_SELECTOR: &str = ".entry"; -const ENTRY_NAME_SELECTOR: &str = ".entry__name"; - -/// Parses the HTML from `data` and returns the relative Lodestone URL for the first search entry. -pub fn parse_search(data: &str) -> String { - let document = Html::parse_document(data); - let mut href = String::new(); - - for element in document.select(&Selector::parse(ENTRY_SELECTOR).unwrap()) { - if let Some(_) = element - .select(&Selector::parse(ENTRY_NAME_SELECTOR).unwrap()) - .next() - && let Some(block_name) = element - .select(&Selector::parse("a.entry__link").unwrap()) - .next() - { - href = block_name.attr("href").unwrap().parse().unwrap(); - } - } - - href -} - -const CHARACTER_NAME_SELECTOR: &str = ".frame__chara__name"; -const WORLD_DATA_CENTER_SELECTOR: &str = ".frame__chara__world"; -const CHARACTER_BLOCK_SELECTOR: &str = ".character-block__box"; -const CHARACTER_BLOCK_TITLE_SELECTOR: &str = ".character-block__title"; -const CHARACTER_BLOCK_NAME_SELECTOR: &str = ".character-block__name"; -const FACE_IMG_SELECTOR: &str = ".frame__chara__face > img"; -const PORTRAIT_IMG_SELECTOR: &str = ".character__detail__image > a > img"; -const NAMEDAY_SELECTOR: &str = ".character-block__birth"; -const FREE_COMPANY_SELECTOR: &str = ".character__freecompany__name > h4 > a"; -const TITLE_SELECTOR: &str = ".frame__chara__title"; - -/// Parses the HTML from `data` and returns `CharacterData`. The data may be incomplete. -pub fn parse_profile(data: &str, char_data: &mut CharacterData) { - let document = Html::parse_document(data); - - for element in document.select(&Selector::parse(CHARACTER_NAME_SELECTOR).unwrap()) { - char_data.name = element.inner_html(); - } - - if let Some(title) = document - .select(&Selector::parse(TITLE_SELECTOR).unwrap()) - .next() - { - char_data.title = Some(title.inner_html().as_str().to_string()); - } - - let world_re = Regex::new(r"(\w+)\s\[(\w+)\]").unwrap(); - for element in document.select(&Selector::parse(WORLD_DATA_CENTER_SELECTOR).unwrap()) { - let inner_html = element.inner_html(); - let captures = world_re.captures(&inner_html).unwrap(); - // TODO: use error - char_data.world = WorldValue::try_from(captures.get(1).unwrap().as_str()).unwrap(); - char_data.data_center = captures.get(2).unwrap().as_str().to_owned(); - } - - let block_re = Regex::new(r"([^<]+)<br>([^\/]+)\s\/\s(\W)").unwrap(); - let grand_re = Regex::new(r"([^\/]+)\s\/\s([^\/]+)").unwrap(); - for element in document.select(&Selector::parse(CHARACTER_BLOCK_SELECTOR).unwrap()) { - if let Some(block_title) = element - .select(&Selector::parse(CHARACTER_BLOCK_TITLE_SELECTOR).unwrap()) - .next() - { - let name = block_title.inner_html(); - if name == "Race/Clan/Gender" { - if let Some(block_name) = element - .select(&Selector::parse(CHARACTER_BLOCK_NAME_SELECTOR).unwrap()) - .next() - { - let inner_html = block_name.inner_html(); - let captures = block_re.captures(&inner_html).unwrap(); - - char_data.race = - RaceValue::try_from(captures.get(1).unwrap().as_str()).unwrap(); - char_data.tribe = - TribeValue::try_from(captures.get(2).unwrap().as_str()).unwrap(); - char_data.gender = - GenderValue::try_from(captures.get(3).unwrap().as_str()).unwrap(); - } - } else if name == "City-state" { - if let Some(block_name) = element - .select(&Selector::parse(CHARACTER_BLOCK_NAME_SELECTOR).unwrap()) - .next() - { - char_data.city_state = - CityStateValue::try_from(block_name.inner_html().as_str()).unwrap(); - } - } else if name == "Nameday" { - for element in element.select(&Selector::parse(NAMEDAY_SELECTOR).unwrap()) { - char_data.nameday = - NamedayValue::try_from(element.inner_html().as_str()).unwrap(); - } - - if let Some(block_name) = element - .select(&Selector::parse(CHARACTER_BLOCK_NAME_SELECTOR).unwrap()) - .next() - { - char_data.guardian = - GuardianValue::try_from(block_name.inner_html().as_str()).unwrap(); - } - } else if name == "Grand Company" - && let Some(block_name) = element - .select(&Selector::parse(CHARACTER_BLOCK_NAME_SELECTOR).unwrap()) - .next() - { - let inner_html = block_name.inner_html(); - let captures = grand_re.captures(&inner_html).unwrap(); - - char_data.grand_company.name = captures.get(1).unwrap().as_str().to_string(); - char_data.grand_company.rank = captures.get(2).unwrap().as_str().to_string(); - } - } - - if let Some(free_company) = element - .select(&Selector::parse(FREE_COMPANY_SELECTOR).unwrap()) - .next() - { - char_data.free_company = Some(free_company.inner_html().as_str().to_string()); - } - } - - for element in document.select(&Selector::parse(FACE_IMG_SELECTOR).unwrap()) { - char_data.face_url = element.attr("src").unwrap().parse().unwrap(); - } - - if let Some(element) = document - .select(&Selector::parse(PORTRAIT_IMG_SELECTOR).unwrap()) - .next() - { - char_data.portrait_url = element.attr("src").unwrap().parse().unwrap(); - } - - // TODO: support facewear - let item_slot_selectors = [ - ".icon-c--0", // Main Hand - ".icon-c--1", // Off hand - ".icon-c--2", // Head - ".icon-c--3", // Body - ".icon-c--4", // Hands - ".icon-c--6", // Legs - ".icon-c--7", // Feet - ".icon-c--8", // Earrings - ".icon-c--9", // Necklace - ".icon-c--10", // Bracelets - ".icon-c--11", // Left Ring - ".icon-c--12", // Right Ring - ".icon-c--13", // Soul Crystal - ]; - - for (i, selector) in item_slot_selectors.iter().enumerate() { - if let Some(slot) = document.select(&Selector::parse(selector).unwrap()).next() - && let Some(item) = slot.select(&Selector::parse(".db-tooltip").unwrap()).next() - { - let parsed_item = parse_item_tooltip(&item); - let slot = match i { - 0 => &mut char_data.equipped.main_hand, - 1 => &mut char_data.equipped.off_hand, - 2 => &mut char_data.equipped.head, - 3 => &mut char_data.equipped.body, - 4 => &mut char_data.equipped.hands, - 5 => &mut char_data.equipped.legs, - 6 => &mut char_data.equipped.feet, - 7 => &mut char_data.equipped.earrings, - 8 => &mut char_data.equipped.necklace, - 9 => &mut char_data.equipped.bracelets, - 10 => &mut char_data.equipped.left_ring, - 11 => &mut char_data.equipped.right_ring, - 12 => &mut char_data.equipped.soul_crystal, - _ => panic!("Unexpected slot!"), - }; - - *slot = parsed_item; - } - } -} - -const CLASSJOB_SELECTOR: &str = ".character__job > li"; -const CLASSJOB_LEVEL_SELECTOR: &str = ".character__job__level"; -const CLASSJOB_NAME_SELECTOR: &str = ".character__job__name"; -const CLASSJOB_EXP_SELECTOR: &str = ".character__job__exp"; - -/// Parses the HTML from `data` and returns `CharacterData`. The data may be incomplete. -pub fn parse_classjob(data: &str, char_data: &mut CharacterData) { - let document = Html::parse_document(data); - - for element in document.select(&Selector::parse(CLASSJOB_SELECTOR).unwrap()) { - let level = element - .select(&Selector::parse(CLASSJOB_LEVEL_SELECTOR).unwrap()) - .next() - .unwrap(); - let name = element - .select(&Selector::parse(CLASSJOB_NAME_SELECTOR).unwrap()) - .next() - .unwrap(); - let exp_element = element - .select(&Selector::parse(CLASSJOB_EXP_SELECTOR).unwrap()) - .next() - .unwrap(); - - let mut exp = None; - let mut max_exp = None; - if let Some((exp_text, max_exp_text)) = exp_element.inner_html().split_once(" / ") { - exp = exp_text.replace(",", "").parse().ok(); - max_exp = max_exp_text.replace(",", "").parse().ok(); - } - - // skip levels that are -, which means they don't even have the classjob - if let Ok(level) = level.inner_html().parse() { - let mut class_job_value = - ClassJobValue::try_from(name.inner_html().as_str()).unwrap_or_default(); - class_job_value.level = level; - class_job_value.exp = exp; - class_job_value.max_exp = max_exp; - char_data.classjob_levels.push(class_job_value); - } - } -} - -fn parse_item_tooltip(element: &scraper::ElementRef<'_>) -> Option<ItemValue> { - if let Some(slot) = element - .select(&Selector::parse(".db-tooltip__item__name").unwrap()) - .next() - { - let mut text: String = slot.text().collect(); - if text.contains("\u{e03c}") { - text = text.strip_suffix("\u{e03c}").unwrap().to_string(); - } - return Some(ItemValue { name: text }); - } - - None -} diff --git a/src/value.rs b/src/value.rs deleted file mode 100644 index 583d19c..0000000 --- a/src/value.rs +++ /dev/null @@ -1,382 +0,0 @@ -use regex::Regex; -use serde::Serialize; - -use crate::ArchiveError; - -// TODO: does it make sense to implement Default? -#[derive(Default, Serialize)] -pub struct WorldValue { - /// Name of the world. - pub name: String, - /// Internal ID of the world. - pub value: i32, -} - -impl TryFrom<&str> for WorldValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let value = match name { - "Adamantoise" => 73, - "Cactuar" => 79, - "Faerie" => 54, - "Gilgamesh" => 63, - "Jenova" => 40, - "Midgardsormr" => 65, - "Sargatanas" => 99, - "Siren" => 57, - "Balmung" => 91, - "Brynhildr" => 34, - "Coeurl" => 74, - "Diabolos" => 62, - "Goblin" => 81, - "Malboro" => 75, - "Mateus" => 37, - "Zalera" => 41, - "Behemoth" => 78, - "Excalibur" => 93, - "Exodus" => 53, - "Famfrit" => 35, - "Hyperion" => 95, - "Lamia" => 55, - "Leviathan" => 64, - "Ultros" => 77, - "Halicarnassus" => 406, - "Maduin" => 407, - "Marilith" => 404, - "Seraph" => 405, - "Cuchulainn" => 408, - "Golem" => 411, - "Kraken" => 409, - "Rafflesia" => 410, - "Cerberus" => 80, - "Louisoix" => 83, - "Moogle" => 71, - "Omega" => 39, - "Phantom" => 401, - "Ragnarok" => 97, - "Sagittarius" => 400, - "Spriggan" => 85, - "Alpha" => 402, - "Lich" => 36, - "Odin" => 66, - "Phoenix" => 56, - "Raiden" => 403, - "Shiva" => 67, - "Twintania" => 33, - "Zodiark" => 42, - "Aegis" => 90, - "Atomos" => 68, - "Carbuncle" => 45, - "Garuda" => 58, - "Gungnir" => 94, - "Kujata" => 49, - "Tonberry" => 72, - "Typhon" => 50, - "Alexander" => 43, - "Bahamut" => 69, - "Durandal" => 92, - "Fenrir" => 46, - "Ifrit" => 59, - "Ridill" => 98, - "Tiamat" => 76, - "Ultima" => 51, - "Anima" => 44, - "Asura" => 23, - "Chocobo" => 70, - "Hades" => 47, - "Ixion" => 48, - "Masamune" => 96, - "Pandaemonium" => 28, - "Titan" => 61, - "Belias" => 24, - "Mandragora" => 82, - "Ramuh" => 60, - "Shinryu" => 29, - "Unicorn" => 30, - "Valefor" => 52, - "Yojimbo" => 31, - "Zeromus" => 32, - "Bismarck" => 22, - "Ravana" => 21, - "Sephirot" => 86, - "Sophia" => 87, - "Zurvan" => 88, - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - }) - } -} - -#[derive(Default, Serialize)] -pub struct CityStateValue { - /// Name of the city-state. - pub name: String, - /// Internal ID of the city-state. - pub value: i32, -} - -impl TryFrom<&str> for CityStateValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let value = match name { - "Limsa Lominsa" => 1, - "Gridania" => 2, - "Ul'dah" => 3, - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - }) - } -} - -#[derive(Default, Serialize)] -pub struct GenderValue { - /// Name of the gender. - pub name: String, - /// Internal ID of the gender. - pub value: i32, -} - -impl TryFrom<&str> for GenderValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let (value, name) = match name { - "♂" => (0, "Male"), - "♀" => (1, "Female"), - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - }) - } -} - -#[derive(Default, Serialize)] -pub struct RaceValue { - /// Name of the race. - pub name: String, - /// Internal ID of the race. - pub value: i32, -} - -impl TryFrom<&str> for RaceValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let value = match name { - "Hyur" => 1, - "Elezen" => 2, - "Lalafell" => 3, - "Miqo'te" => 4, - "Roegadyn" => 5, - "Au Ra" => 6, - "Hrothgar" => 7, - "Viera" => 8, - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - }) - } -} - -#[derive(Default, Serialize)] -pub struct TribeValue { - /// Name of the tribe. - pub name: String, - /// Internal ID of the tribe. - pub value: i32, -} - -impl TryFrom<&str> for TribeValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let value = match name { - "Midlander" => 1, - "Highlander" => 2, - "Wildwood" => 3, - "Duskwight" => 4, - "Plainsfolk" => 5, - "Dunesfolk" => 6, - "Seeker of the Sun" => 7, - "Keeper of the Moon" => 8, - "Sea Wolf" => 9, - "Hellsguard" => 10, - "Raen" => 11, - "Xaela" => 12, - "Hellions" => 13, - "The Lost" => 14, - "Rava" => 15, - "Veena" => 16, - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - }) - } -} - -#[derive(Default, Serialize)] -pub struct GuardianValue { - /// Name of the guardian. - pub name: String, - /// Internal ID of the guardian. - pub value: i32, -} - -impl TryFrom<&str> for GuardianValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let value = match name { - "Halone, the Fury" => 1, - "Menphina, the Lover" => 2, - "Thaliak, the Scholar" => 3, - "Nymeia, the Spinner" => 4, - "Llymlaen, the Navigator" => 5, - "Oschon, the Wanderer" => 6, - "Byregot, the Builder" => 7, - "Rhalgr, the Destroyer" => 8, - "Azeyma, the Warden" => 9, - "Nald'thal, the Traders" => 10, - "Nophica, the Matron" => 11, - "Althyk, the Keeper" => 12, - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - }) - } -} - -#[derive(Default, Serialize)] -pub struct NamedayValue { - /// String represenation of your nameday. - pub value: String, - /// Day part of your nameday. - pub day: i32, - /// Month part of your nameday. - pub month: i32, -} - -impl TryFrom<&str> for NamedayValue { - type Error = ArchiveError; - - fn try_from(value: &str) -> Result<Self, ArchiveError> { - let re = Regex::new(r"(\d{1,2})[^\d]+(\d{1,2})").unwrap(); - let captures = re.captures(value).unwrap(); - - Ok(Self { - value: value.to_string(), - day: captures.get(1).unwrap().as_str().parse::<i32>().unwrap(), - month: captures.get(2).unwrap().as_str().parse::<i32>().unwrap(), - }) - } -} - -#[derive(Default, Serialize)] -pub struct ClassJobValue { - /// Name of the class or job. - pub name: String, - /// Level of the class or job. - pub level: i32, - /// The EXP of the job, can be None to indicate either: the job isn't unlocked or that they have max EXP. - #[serde(skip_serializing_if = "Option::is_none")] - pub exp: Option<i32>, - /// The maximum EXP of the job, can be None to indicate either: the job isn't unlocked or that they have max EXP. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_exp: Option<i32>, - /// Internal ID of the class or job. - pub value: i32, -} - -impl TryFrom<&str> for ClassJobValue { - type Error = ArchiveError; - - fn try_from(name: &str) -> Result<Self, ArchiveError> { - let value = match name { - "Gladiator" => 1, - "Pugilist" => 2, - "Marauder" => 3, - "Lancer" => 4, - "Archer" => 5, - "Conjurer" => 6, - "Thaumaturge" => 7, - "Carpenter" => 8, - "Blacksmith" => 9, - "Armorer" => 10, - "Goldsmith" => 11, - "Leatherworker" => 12, - "Weaver" => 13, - "Alchemist" => 14, - "Culinarian" => 15, - "Miner" => 16, - "Botanist" => 17, - "Fisher" => 18, - "Paladin" => 19, - "Monk" => 20, - "Warrior" => 21, - "Dragoon" => 22, - "Bard" => 23, - "White Mage" => 24, - "Black Mage" => 25, - "Arcanist" => 26, - "Summoner" => 27, - "Scholar" => 28, - "Rogue" => 29, - "Ninja" => 30, - "Machinist" => 31, - "Dark Knight" => 32, - "Astrologian" => 33, - "Samurai" => 34, - "Red Mage" => 35, - "Blue Mage" => 36, - "Gunbreaker" => 37, - "Dancer" => 38, - "Reaper" => 39, - "Sage" => 40, - "Viper" => 41, - "Pictomancer" => 42, - _ => return Err(ArchiveError::ParsingError), - }; - - Ok(Self { - name: name.to_string(), - value, - ..Default::default() - }) - } -} - -#[derive(Default, Serialize)] -pub struct GrandCompanyValue { - /// Name of the grand company. - pub name: String, - /// Name of your rank in the grand company. - pub rank: String, -} - -#[derive(Default, Serialize)] -pub struct ItemValue { - /// Name of the item. - pub name: String, -} diff --git a/templates/character.html b/templates/character.html deleted file mode 100644 index a49ddd5..0000000 --- a/templates/character.html +++ /dev/null @@ -1,17 +0,0 @@ -<html> -<head> - <title>{{ name }} - - -

Name: {{ name }}

-

World/Data Center: {{ world }} [{{ data_center }}]

-

Race/Clan/Gender: {{ race }} {{ subrace }} {{ gender }}

-

Nameday: {{ nameday }}

-

City-state: {{ city_state }}

-Character Face -Character Portrait - - - \ No newline at end of file diff --git a/templates/plate.html b/templates/plate.html deleted file mode 100644 index b9c9667..0000000 --- a/templates/plate.html +++ /dev/null @@ -1,28 +0,0 @@ - - - {{ name }}'s Adventurer Plate - - -
- - - - - - - - -
-

{{ title }}

-

{{ name }}

-

{{ world }} [{{ data_center }}]

-

LEVEL {{ level }}

-

{{ class }}

-

{{ search_comment }}

-
-
- - - \ No newline at end of file diff --git a/web/auracite.ico b/web/auracite.ico deleted file mode 100644 index 972cf08ca9c012b5a22adfbf26034b6d6e8982fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5558 zcmeHLYe=M382;9FyDSuYfo%=JA#4@%5{YOiHIbU;{eIW+ekT<{!3|Oq3|he?>PPlN zO;^N-c9B&uLN*MtWq)ayKSG`HGF}JW8E2e1J@2Qp%zU)-o!MrHpaaJ<=R5EFKIgpW zocDc>f^bgo5H4L3&?=58ogHiPP_Bcnh?jzU1WO^yKE| z-uCzRfA(ptq^GBUu2QKUq5fW6T>N8RUfzI@kI!>Fw!FQ)pD!sXc~D*(mvg@lA$6_xJa&@YsOd<(-|KEn5RdqmgMenwvcOB(t%xag%gx z%l7uRc5iQwj+AV{daprt-&UX2w6L)7sjTvufLg8ol6*(SVzG$r#KeTDprGIuL_Fl| ztgWrR0biI&addQ)O;1l78XFtU+1c4Ga=J7g6BBc#r>Dm-I5=o&Zf-W!)zz`m($b%K z&$XAA*9&wHaE>29Pm9)qv)#diEtLrh3c6NUSU8iKnp!0tbF%-B?)KkdQJhhrDp4sO z{g+E{a4^^TcE=k1{QRzFWMurBl$7*UWMrhjJ1`vCjEahSue`jR)z;Rs%*@O`eSLjj zaU}2RvADRnk8n>|R#uiCRgJqclaiA1E$V0R$%lx|s?gBT*IkWkwdd*Sc`+d&;eJ(B z6{Gw?mF|2^O%0=O6%`c>u|pde80clyab+7F9j&s(Cj9E^YDRm4xFzmOSy`DH@#nfL zamiL#SeOFwW!buJYH2PbBO`2aaglNdo12?s4Gj&N@bGZ1JmkzA;n#61c4ucN>+bHR zSWEdrjQjNG=;(+cH*V|odWAE#M9C-{5?yg>?S z*EgHZq7RUpbLH$hGfsXO8yhQyzs&}N!46Z}p}m3J4CcVa>3HV>ob%Y*@4;K7kaqo( zlM}iR7I2(*d2RE`VgHp_mr=_9>?VK?_Md!kcYc2U0QBEFqYboAR%yCq3UnoBw zfU|r7n=a$Gh_%<@+dnAg+7|1Z#QJS>bCb2Sw1_+bT;*$egUYRqcO0?47w@je?;paX zoH#TzBz~hifP85ro&uimNU2o5&ug1U4)))~J{TzXTFdP0teBheHqwI+XeoCQeqv%G z`QN1l`}%5od%I?Nd70vk$VK}5`*p;Nh^JaP1K!;a-NB3S?z%zmHo^f9G=qPMyvT}6 zEqI{;Z^aasF8s_G;p4q4V%n~kaEYJMSbTi^ZxDCcxtFdZA|fI(Ha500IXU?j&i8lV zOTXZMJQ`FJ%>n)~gO|T0)pJw-9Nr}_g0ChO6%~p54<9TB1O&Y7CcGz_#hyo}rKR1) cn_&V%Zu9>WCvZC8@o&CaW#;z(u#E@)0;`6X%m4rY diff --git a/web/auracite.svg b/web/auracite.svg deleted file mode 100644 index 7a7b65f..0000000 --- a/web/auracite.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/web/index.html b/web/index.html deleted file mode 100644 index e1515b3..0000000 --- a/web/index.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - Auracite - - - - -
-
- -

Auracite

-

Auracite allows you to export your FFXIV character into portable, generic formats. Enter your character's name below, and optionally use the Dalamud plugin to collect even more data like your adventurer plate.

- -
- -
- -
- -
- - -
- -
-

-
-
-
-

Auracite uses the Lodestone for collecting data about your character. However, we can't connect to the - Lodestone directly. It uses a proxy which injects CORS compliance and browser compatibility cookies.

- -

The entire process happens locally, and does not give me any personally identifiable data about you. It only - collects what is already publicly available on the Lodestone, and optionally data from the game client.

- - How does this work? - -
-
-

Depending on how much information is collected, lots of things! A snapshot of your character, or perhaps processing it to graph data over time.

-

For a cooler example, I use Auracite in the FFXIV shrine on my website.

- - What can I use this for? - -
-
-

Auracite can only collect public Lodestone information, so if you have privacy settings preventing that then there's nothing we can do. You will have to switch your profile to public for Auracite to function properly.

- - Why doesn't it work/couldn't collect information? - -
-
-

Auracite can only collect as much data about your character as they make publicly available on the Lodestone.

-

To work around this, I created a Dalamud plugin to collect even more information. It's available in my personal - Dalamud plugin repository.

-

The plugin needs to start a local HTTP server in order to communicate with Auracite. To prevent this from running - all the time, you must type /auracite begin before clicking the "Download" button. Once the process - is complete, the server is shutdown automatically. It's always safe to disable the plugin when you're not using - Auracite.

-

The website connects to your game client locally, and it does not use my server to proxy any data. No data leaves - your device.

- - What is the "Connect to Dalamud Plugin" option? - -
- -
- - From 9de8b7e71eecd73e9fb79bb99ca05fc1e2e249e9 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 18 Apr 2026 12:46:37 -0400 Subject: [PATCH 8/8] Add icon to Dalamud plugin --- Auracite/Auracite.json | 3 ++- images/icon.png | Bin 0 -> 25741 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 images/icon.png diff --git a/Auracite/Auracite.json b/Auracite/Auracite.json index ac9257b..15f45fd 100644 --- a/Auracite/Auracite.json +++ b/Auracite/Auracite.json @@ -4,5 +4,6 @@ "Punchline": "Archive your character in a portable, generic format", "Description": "Archive your character in a portable, generic format", "Tags": [], - "RepoUrl": "https://github.com/redstrate/Auracite" + "RepoUrl": "https://github.com/redstrate/Auracite", + "IconUrl": "https://raw.githubusercontent.com/redstrate/Auracite/refs/heads/main/images/icon.png" } diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd79c5344b80718721aace55b82e122e48a95352 GIT binary patch literal 25741 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 literal 0 HcmV?d00001