From 23b9bf5532a9c4f201b88575a503ccb56bfbb63e Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Fri, 1 Nov 2024 18:58:38 -0400 Subject: [PATCH] Begin exporting adventurer plates This creates a new HTML file called plate.html in the archive, which is your recreated adventurer plate. --- dalamud/Auracite/AdventurerPlateStep.cs | 134 ++++++++++++++++++++++++ dalamud/Auracite/Plugin.cs | 8 ++ src/data.rs | 4 + src/html.rs | 25 ++++- src/lib.rs | 71 +++++++++++-- templates/plate.html | 28 +++++ 6 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 templates/plate.html diff --git a/dalamud/Auracite/AdventurerPlateStep.cs b/dalamud/Auracite/AdventurerPlateStep.cs index 2c561c6..a62ed49 100644 --- a/dalamud/Auracite/AdventurerPlateStep.cs +++ b/dalamud/Auracite/AdventurerPlateStep.cs @@ -1,6 +1,12 @@ using System; using System.IO; +using System.Runtime.InteropServices; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using Lumina.Data.Files; using Lumina.Excel.GeneratedSheets; using SharpDX; using SharpDX.Direct3D11; @@ -13,6 +19,52 @@ namespace Auracite; public class AdventurerPlateStep : IStep { + // Won't be needed once https://github.com/aers/FFXIVClientStructs/pull/1139 is merged + [StructLayout(LayoutKind.Explicit, Size = 0x9B0)] + public unsafe struct CustomStorage { + [FieldOffset(0x4)] public uint EntityId; + [FieldOffset(0x8)] public ulong ContentId; + + [FieldOffset(0x1B)] public bool InvertPortraitPlacement; + [FieldOffset(0x1C)] public byte BasePlate; + [FieldOffset(0x22)] public byte Backing; + [FieldOffset(0x1E)] public byte TopBorder; + [FieldOffset(0x1F)] public byte BottomBorder; + [FieldOffset(0x24)] public byte PatternOverlay; + [FieldOffset(0x26)] public byte PortraitFrame; + [FieldOffset(0x28)] public byte PlateFrame; + [FieldOffset(0x2A)] public byte Accent; + + [FieldOffset(0x60)] public Utf8String Name; + [FieldOffset(0xC8)] public ushort WorldId; + [FieldOffset(0xCA)] public byte ClassJobId; + + [FieldOffset(0xCC)] public byte GcRank; + + [FieldOffset(0xD0)] public ushort Level; + [FieldOffset(0xD2)] public ushort TitleId; + + [FieldOffset(0xE0)] public Utf8String FreeCompany; + [FieldOffset(0x148)] public Utf8String SearchComment; + [FieldOffset(0x1B0)] public Utf8String SearchCommentRaw; // contains unresolved AutoTranslatePayloads + + [FieldOffset(0x258)] public uint Activity1IconId; + [FieldOffset(0x260)] public Utf8String Activity1Name; + [FieldOffset(0x2C8)] public uint Activity2IconId; + [FieldOffset(0x2D0)] public Utf8String Activity2Name; + [FieldOffset(0x338)] public uint Activity3IconId; + [FieldOffset(0x340)] public Utf8String Activity3Name; + [FieldOffset(0x3A8)] public uint Activity4IconId; + [FieldOffset(0x3B0)] public Utf8String Activity4Name; + [FieldOffset(0x418)] public uint Activity5IconId; + [FieldOffset(0x420)] public Utf8String Activity5Name; + [FieldOffset(0x488)] public uint Activity6IconId; + [FieldOffset(0x490)] public Utf8String Activity6Name; + + [FieldOffset(0x540)] public CharaViewPortrait CharaView; + [FieldOffset(0x960)] public Texture* PortraitTexture; + } + public AdventurerPlateStep() { @@ -31,8 +83,58 @@ public class AdventurerPlateStep : IStep { unsafe { + var customData = (CustomStorage*)AgentCharaCard.Instance()->Data; var image = GetCurrentCharaViewImage(); Plugin.package.portrait = image.ToBase64String(PngFormat.Instance); + + if (customData->BasePlate != 0) + { + Plugin.package.base_plate = GetImage(ResolveCardBase(customData->BasePlate)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->PatternOverlay != 0) + { + Plugin.package.pattern_overlay = GetImage(ResolveCardDecoration(customData->PatternOverlay)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->Backing != 0) + { + Plugin.package.backing = GetImage(ResolveCardDecoration(customData->Backing)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->TopBorder != 0) + { + Plugin.package.top_border = GetImage(ResolveCardHeaderTop(customData->TopBorder)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->BottomBorder != 0) + { + Plugin.package.bottom_border = GetImage(ResolveCardHeaderBottom(customData->BottomBorder)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->PortraitFrame != 0) + { + Plugin.package.portrait_frame = GetImage(ResolveCardDecoration(customData->PortraitFrame)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->PlateFrame != 0) + { + Plugin.package.plate_frame = GetImage(ResolveCardDecoration(customData->PlateFrame)) + .ToBase64String(PngFormat.Instance); + } + + if (customData->Accent != 0) + { + Plugin.package.accent = GetImage(ResolveCardDecoration(customData->Accent)) + .ToBase64String(PngFormat.Instance); + } + Plugin.package.plate_title = Title?.Feminine; // TODO: Support mascs Plugin.package.plate_title_is_prefix = Title?.IsPrefix; Plugin.package.plate_class_job = ClassJob?.Name; @@ -76,6 +178,14 @@ public class AdventurerPlateStep : IStep return Image.LoadPixelData(pixelDataStream.ToArray(), desc.Width, desc.Height); } + + public Image GetImage(string path) + { + var tex = Plugin.DataManager.GetFile(path); + tex.LoadFile(); + var imageData = tex.GetRgbaImageData(); + return Image.LoadPixelData(imageData, tex.Header.Width, tex.Header.Height); + } private unsafe Title? Title { get { @@ -109,4 +219,28 @@ public class AdventurerPlateStep : IStep { return AgentCharaCard.Instance()->AgentInterface.IsAgentActive(); } + + public string ResolveCardBase(uint rowIndex) + { + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(rowIndex); + return $"ui/icon/{row.Image.ToString().Substring(0, 3)}000/{row.Image}_hr1.tex"; + } + + public string? ResolveCardDecoration(uint rowIndex) + { + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(rowIndex); + return $"ui/icon/{row.Image.ToString().Substring(0, 3)}000/{row.Image}_hr1.tex"; + } + + public string? ResolveCardHeaderTop(uint rowIndex) + { + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(rowIndex); + return $"ui/icon/{row.TopImage.ToString().Substring(0, 3)}000/{row.TopImage}_hr1.tex"; + } + + public string? ResolveCardHeaderBottom(uint rowIndex) + { + var row = Plugin.DataManager.GetExcelSheet()?.GetRow(rowIndex); + return $"ui/icon/{row.BottomImage.ToString().Substring(0, 3)}000/{row.BottomImage}_hr1.tex"; + } } \ No newline at end of file diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs index 773a45c..9c12076 100644 --- a/dalamud/Auracite/Plugin.cs +++ b/dalamud/Auracite/Plugin.cs @@ -65,6 +65,14 @@ public sealed class Plugin : IDalamudPlugin 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 static Package? package; diff --git a/src/data.rs b/src/data.rs index 1286f6c..8291b6c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -51,6 +51,10 @@ pub struct CharacterData { pub is_novice: bool, pub is_returner: bool, pub player_commendations: i32, + pub plate_title: String, + pub plate_classjob: String, + pub plate_classjob_level: i32, + pub search_comment: String, #[serde(skip)] pub face_url: String, diff --git a/src/html.rs b/src/html.rs index 98b742a..b3de328 100644 --- a/src/html.rs +++ b/src/html.rs @@ -3,7 +3,7 @@ use minijinja::{context, Environment}; /// 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_html(char_data: &CharacterData) -> String { +pub fn create_character_html(char_data: &CharacterData) -> String { let mut env = Environment::new(); env.add_template( "character.html", @@ -24,3 +24,26 @@ pub fn create_html(char_data: &CharacterData) -> String { }) .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, + 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 index 640e2eb..72c99c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ use zip::result::ZipError; use zip::write::SimpleFileOptions; use zip::ZipWriter; use crate::downloader::download; -use crate::html::create_html; +use crate::html::{create_character_html, create_plate_html}; use crate::parser::parse_search; use base64::prelude::*; #[cfg(target_family = "wasm")] @@ -47,6 +47,14 @@ struct Package { pub plate_class_job: String, pub plate_class_job_level: i32, pub search_comment: String, + pub base_plate: Option, + pub pattern_overlay: Option, + pub backing: Option, + pub top_border: Option, + pub bottom_border: Option, + pub portrait_frame: Option, + pub plate_frame: Option, + pub accent: Option, // Appearance pub race: i32, @@ -216,13 +224,57 @@ pub async fn archive_character(character_name: &str, use_dalamud: bool) -> Resul char_data.is_novice = package.is_novice; char_data.is_returner = package.is_returner; char_data.player_commendations = 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 = package.plate_class_job_level; + char_data.search_comment = package.search_comment; zip.start_file("plate-portrait.png", options)?; zip.write_all(&*BASE64_STANDARD.decode(package.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())?; + } + // Stop the HTTP server - let stop_url = Url::parse(&"http://localhost:42072/stop").map_err(|_| ArchiveError::UnknownError)?; - download(&stop_url).await; + //let stop_url = Url::parse(&"http://localhost:42072/stop").map_err(|_| ArchiveError::UnknownError)?; + //download(&stop_url).await; } let char_dat = physis::chardat::CharacterData { @@ -263,12 +315,15 @@ pub async fn archive_character(character_name: &str, use_dalamud: bool) -> Resul zip.start_file("character.json", options)?; zip.write_all(serde_json::to_string(&char_data).unwrap().as_ref())?; - let html = create_html( - &char_data - ); - zip.start_file("character.html", options)?; - zip.write_all(html.as_ref())?; + zip.write_all(create_character_html( + &char_data + ).as_ref())?; + + zip.start_file("plate.html", options)?; + zip.write_all(create_plate_html( + &char_data + ).as_ref())?; zip.finish()?; diff --git a/templates/plate.html b/templates/plate.html new file mode 100644 index 0000000..c4dd2b5 --- /dev/null +++ b/templates/plate.html @@ -0,0 +1,28 @@ + + + {{ name }}'s Adventurer Plate + + +
+ + + + + + + + +
+

{{ title }}

+

{{ name }}

+

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

+

LEVEL {{ level }}

+

{{ class }}

+

{{ search_comment }}

+
+
+ + + \ No newline at end of file