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:
parent
517995d49d
commit
23b9bf5532
6 changed files with 261 additions and 9 deletions
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
25
src/html.rs
25
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()
|
||||
}
|
||||
|
|
71
src/lib.rs
71
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<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
28
templates/plate.html
Normal 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>
|
Loading…
Add table
Reference in a new issue