1
Fork 0
mirror of https://github.com/redstrate/Auracite.git synced 2025-04-25 13:57:45 +00:00

Begin exporting adventurer plates

This creates a new HTML file called plate.html in the archive, which is
your recreated adventurer plate.
This commit is contained in:
Joshua Goins 2024-11-01 18:58:38 -04:00
parent 517995d49d
commit 23b9bf5532
6 changed files with 261 additions and 9 deletions

View file

@ -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<Bgra32>(pixelDataStream.ToArray(), desc.Width, desc.Height);
}
public Image GetImage(string path)
{
var tex = Plugin.DataManager.GetFile<TexFile>(path);
tex.LoadFile();
var imageData = tex.GetRgbaImageData();
return Image.LoadPixelData<Rgba32>(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<CharaCardBase>()?.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<CharaCardDecoration>()?.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<CharaCardHeader>()?.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<CharaCardHeader>()?.GetRow(rowIndex);
return $"ui/icon/{row.BottomImage.ToString().Substring(0, 3)}000/{row.BottomImage}_hr1.tex";
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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()
}

View file

@ -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<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>,
// 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()?;

28
templates/plate.html Normal file
View file

@ -0,0 +1,28 @@
<html>
<head>
<title>{{ name }}'s Adventurer Plate</title>
</head>
<body>
<div style="width:100%; height:auto; aspect-ratio: 1.846153846153846; display: inline-block; position: relative">
<img style="position: absolute; width: 50%" src="backing.png"/>
<img style="position:absolute; left: 10%; top: 10%; width: 80%; z-index: 1" src="base-plate.png"/>
<img style="position:absolute; left: 10%; top: 10%; width: 80%; z-index: 2" src="pattern-overlay.png"/>
<img style="position:absolute;z-index:4; width:100%" src="plate-frame.png"/>
<img style="position:absolute;z-index:3; width:25%; left: 15%; top: 15%" src="plate-portrait.png"/>
<img style="position:absolute;z-index:5; height:135%; top: -14%; left: 5%" src="portrait-frame.png"/>
<img style="position:absolute;z-index:5; width:100%; top: 0" src="top-border.png"/>
<img style="position:absolute;z-index:5; width:100%; bottom: 0" src="bottom-border.png"/>
<div style="position: absolute; top: 30%; left: 50%; color: white; z-index: 99">
<p>{{ title }}</p>
<p>{{ name }}</p>
<p>{{ world }} [{{ data_center }}]</p>
<p>LEVEL {{ level }}</p>
<p>{{ class }}</p>
<p>{{ search_comment }}</p>
</div>
</div>
<footer>
<p>Generated by <a href="http://xiv.zone/software/auracite">Auracite</a>.</p>
</footer>
</body>
</html>