diff --git a/.gitignore b/.gitignore index db41fce..37fd804 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /target .idea/ -config.json \ No newline at end of file +config.json +.vs/ +obj/ +bin/ +*.user \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 52532dd..3415466 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,7 @@ dependencies = [ "scraper", "serde", "serde_json", + "touche", ] [[package]] @@ -123,6 +124,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -135,6 +142,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -230,6 +246,25 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cssparser" version = "0.31.2" @@ -264,6 +299,16 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "downloader" version = "0.2.8" @@ -452,6 +497,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getopts" version = "0.2.21" @@ -478,6 +533,30 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + [[package]] name = "heck" version = "0.5.0" @@ -504,6 +583,17 @@ dependencies = [ "syn", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.1.0" @@ -522,7 +612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.1.0", ] [[package]] @@ -533,7 +623,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "pin-project-lite", ] @@ -544,6 +634,12 @@ version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.4.1" @@ -553,7 +649,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.1.0", "http-body", "httparse", "itoa", @@ -588,7 +684,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.1.0", "http-body", "hyper", "pin-project-lite", @@ -748,6 +844,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.4" @@ -1051,11 +1157,11 @@ version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "hyper", @@ -1109,7 +1215,7 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ - "base64", + "base64 0.22.1", "rustls-pki-types", ] @@ -1251,6 +1357,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1390,6 +1507,15 @@ dependencies = [ "syn", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1430,6 +1556,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "touche" +version = "0.0.10" +source = "git+https://github.com/redstrate/touche#8979a3367dcf79d605d818157ea187a52608fed9" +dependencies = [ + "headers", + "http 0.2.12", + "httparse", + "thiserror", + "threadpool", +] + [[package]] name = "tower-service" version = "0.3.3" @@ -1461,6 +1599,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index 7bd33c3..ffc8ec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,9 @@ clap_derive = "4.5" downloader = "0.2" # Used to generate the HTML page to easily preview your exported data -minijinja = "2.0" \ No newline at end of file +minijinja = "2.0" + +# Used to communicate with the Dalamud plugin +# Needs my fork for allowing server shutdown +# TODO: upstream this or poke upstream to add this +touche = { git = "https://github.com/redstrate/touche" } \ No newline at end of file diff --git a/dalamud/Auracite.sln b/dalamud/Auracite.sln new file mode 100644 index 0000000..2369872 --- /dev/null +++ b/dalamud/Auracite.sln @@ -0,0 +1,29 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Auracite", "Auracite\Auracite.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Debug|x64.ActiveCfg = Debug|x64 + {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Debug|x64.Build.0 = Debug|x64 + {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Release|x64.ActiveCfg = Release|x64 + {4FEC9558-EB25-419F-B86E-51B8CFDA32B7}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} + EndGlobalSection +EndGlobal diff --git a/dalamud/Auracite/Auracite.csproj b/dalamud/Auracite/Auracite.csproj new file mode 100644 index 0000000..43f8a48 --- /dev/null +++ b/dalamud/Auracite/Auracite.csproj @@ -0,0 +1,9 @@ + + + + + + 1.0.0.0 + NeoVARC + + diff --git a/dalamud/Auracite/Auracite.json b/dalamud/Auracite/Auracite.json new file mode 100644 index 0000000..2a06978 --- /dev/null +++ b/dalamud/Auracite/Auracite.json @@ -0,0 +1,8 @@ +{ + "Author": "redstrate", + "Name": "Auracite", + "Punchline": "Export your FFXIV character in portable, generic formats", + "Description": "Export your FFXIV character in portable, generic formats.", + "Tags": [], + "RepoUrl": "https://github.com/redstrate/Auracite" +} diff --git a/dalamud/Auracite/Dalamud.Plugin.Bootstrap.targets b/dalamud/Auracite/Dalamud.Plugin.Bootstrap.targets new file mode 100644 index 0000000..d9fe281 --- /dev/null +++ b/dalamud/Auracite/Dalamud.Plugin.Bootstrap.targets @@ -0,0 +1,12 @@ + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + $(HOME)/.xlcore/dalamud/Hooks/dev/ + $(HOME)/.local/share/astra/dalamud/local/ + $(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/ + $(DALAMUD_HOME)/ + + + + diff --git a/dalamud/Auracite/IStep.cs b/dalamud/Auracite/IStep.cs new file mode 100644 index 0000000..79245a6 --- /dev/null +++ b/dalamud/Auracite/IStep.cs @@ -0,0 +1,13 @@ +using System; + +namespace Auracite; + +public interface IStep +{ + public event CompletedDelegate Completed; + + string StepName(); + string StepDescription(); + + delegate void CompletedDelegate(); +} \ No newline at end of file diff --git a/dalamud/Auracite/PlaytimeStep.cs b/dalamud/Auracite/PlaytimeStep.cs new file mode 100644 index 0000000..c910f46 --- /dev/null +++ b/dalamud/Auracite/PlaytimeStep.cs @@ -0,0 +1,41 @@ +using System; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +namespace Auracite; + +public class PlaytimeStep : IStep, IDisposable +{ + public PlaytimeStep() + { + Plugin.ChatGui.ChatMessage += OnChatMessage; + } + + public void Dispose() + { + Plugin.ChatGui.ChatMessage -= OnChatMessage; + } + + public event IStep.CompletedDelegate? Completed; + + public string StepName() + { + return "Playtime"; + } + + public string StepDescription() + { + return "Type /playtime into the chat window."; + } + + private void OnChatMessage(XivChatType type, int timestamp, ref SeString sender, ref SeString message, + ref bool ishandled) + { + var msgString = message.ToString(); + if (msgString.Contains("Total Play Time:") && type == XivChatType.SystemMessage) + { + Plugin.package.playtime = msgString.Split(": ")[1]; // TODO: lol + Completed?.Invoke(); + } + } +} \ No newline at end of file diff --git a/dalamud/Auracite/Plugin.cs b/dalamud/Auracite/Plugin.cs new file mode 100644 index 0000000..4c62bbf --- /dev/null +++ b/dalamud/Auracite/Plugin.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Dalamud.Game.Command; +using Dalamud.Interface.Windowing; +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using Newtonsoft.Json; + +namespace Auracite; + +public sealed class Plugin : IDalamudPlugin +{ + public static IStep? CurrentStep; + private readonly WindowSystem WindowSystem = new("Auracite"); + + private readonly List _steps = + [typeof(PlaytimeStep)]; + + private int _stepIndex; + + private readonly StepWindow StepWindow; + + public class Package + { + public string playtime; + } + + public static Package? package; + + public Plugin() + { + CommandManager.AddHandler("/auracite", new CommandInfo(OnAuraciteCommand) + { + HelpMessage = "Start the server." + }); + + StepWindow = new StepWindow(); + WindowSystem.AddWindow(StepWindow); + + PluginInterface.UiBuilder.Draw += WindowSystem.Draw; + } + + [PluginService] internal static IClientState ClientState { get; private set; } = null!; + + [PluginService] internal static IDalamudPluginInterface PluginInterface { get; private set; } = null!; + + [PluginService] internal static IChatGui ChatGui { get; private set; } = null!; + + [PluginService] internal static ICommandManager CommandManager { get; private set; } = null!; + + public void Dispose() + { + WindowSystem.RemoveAllWindows(); + } + + private void OnAuraciteCommand(string command, string arguments) + { + if (arguments == "begin" && CurrentStep == null) + { + _stepIndex = -1; + package = new Package(); + NextStep(); + StepWindow.IsOpen = true; + } + } + + private void NextStep() + { + _stepIndex++; + if (_stepIndex >= _steps.Count) + { + CurrentStep = null; + StepWindow.IsOpen = false; + SendPackage(); + return; + } + CurrentStep = (IStep)Activator.CreateInstance(_steps[_stepIndex])!; + CurrentStep.Completed += NextStep; + } + + private void SendPackage() + { + var client = new HttpClient(); + client.PostAsync("http://127.0.0.1:8000/package", new StringContent(JsonConvert.SerializeObject(package))); + package = null; + } +} \ No newline at end of file diff --git a/dalamud/Auracite/StepWindow.cs b/dalamud/Auracite/StepWindow.cs new file mode 100644 index 0000000..377cbc6 --- /dev/null +++ b/dalamud/Auracite/StepWindow.cs @@ -0,0 +1,28 @@ +using System; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Auracite; + +public class StepWindow() + : Window("Step Window"), IDisposable +{ + public void Dispose() + { + } + + public override void Draw() + { + if (Plugin.CurrentStep != null) + { + ImGui.Text(Plugin.CurrentStep.StepName()); + ImGui.Text(Plugin.CurrentStep.StepDescription()); + + ImGui.TextDisabled("Step requires manual user action."); + } + else + { + ImGui.Text("Auracite is not running."); + } + } +} \ No newline at end of file diff --git a/dalamud/Auracite/packages.lock.json b/dalamud/Auracite/packages.lock.json new file mode 100644 index 0000000..19fcea9 --- /dev/null +++ b/dalamud/Auracite/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net8.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[2.1.13, )", + "resolved": "2.1.13", + "contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ==" + } + } + } +} \ No newline at end of file diff --git a/dalamud/global.json b/dalamud/global.json new file mode 100644 index 0000000..9e5e1fd --- /dev/null +++ b/dalamud/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/src/data.rs b/src/data.rs index dae535c..f854e53 100644 --- a/src/data.rs +++ b/src/data.rs @@ -17,6 +17,7 @@ pub struct CharacterData { pub nameday: String, pub guardian: String, pub currencies: Currencies, + pub playtime: String, #[serde(skip)] pub face_url: String, diff --git a/src/main.rs b/src/main.rs index ffed92d..984f768 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,13 @@ use crate::downloader::download; use crate::html::write_html; use crate::parser::{parse_lodestone, parse_search}; use clap::Parser; +use serde::Deserialize; +use std::convert::Infallible; use std::fs::{read, write}; use std::path::Path; +use std::sync::{Arc, Mutex}; +use touche::server::Service; +use touche::{Body, HttpBody, Request, Response, Server, StatusCode}; const LODESTONE_HOST: &str = "https://na.finalfantasyxiv.com"; @@ -17,6 +22,41 @@ const LODESTONE_HOST: &str = "https://na.finalfantasyxiv.com"; struct Args { #[arg(short, long, help = "The character's name.")] name: String, + + #[arg(short, long, help = "Whether to import more data from the Auracite Dalamud plugin.")] + dalamud: bool, +} + +#[derive(Default, Deserialize, Clone)] +struct Package { + playtime: String, +} + +#[derive(Clone)] +struct PackageService<'a> { + wants_stop: Arc>, // TODO: THIS IS TERRIBLE STOP STOP STOP + package: &'a Arc>, +} + +impl Service for PackageService<'_> { + type Body = &'static str; + type Error = Infallible; + + fn call(&self, req: Request) -> Result, Self::Error> { + *self.package.lock().unwrap() = serde_json::from_str(&String::from_utf8(req.into_body().into_bytes().unwrap()).unwrap()).unwrap(); + + *self.wants_stop.lock().unwrap() = true; + + Ok(Response::builder() + .status(StatusCode::OK) + .body("") + .unwrap()) + } + + // TODO: NO NO NO NO + fn wants_stop(&self) -> bool { + *self.wants_stop.lock().unwrap() + } } fn main() { @@ -29,7 +69,7 @@ fn main() { &format!("{LODESTONE_HOST}/lodestone/character/?q={}", args.name), search_page_path, ) - .expect("Failed to download the search page from the Lodestone."); + .expect("Failed to download the search page from the Lodestone."); let href = parse_search(&String::from_utf8(read(search_page_path).unwrap()).unwrap()); if href.is_empty() { @@ -40,7 +80,7 @@ fn main() { download(&format!("{LODESTONE_HOST}{}", href), char_page_path) .expect("Failed to download the character page from the Lodestone."); - let char_data = parse_lodestone(&String::from_utf8(read(char_page_path).unwrap()).unwrap()); + let mut char_data = parse_lodestone(&String::from_utf8(read(char_page_path).unwrap()).unwrap()); let character_folder = Path::new(&args.name); if !character_folder.exists() { @@ -52,13 +92,25 @@ fn main() { &char_data.portrait_url, &character_folder.join("portrait.jpg"), ) - .expect("Failed to download the character portrait image."); + .expect("Failed to download the character portrait image."); } if !char_data.face_url.is_empty() { download(&char_data.face_url, &character_folder.join("face.jpg")) .expect("Failed to download the character face image."); } + if args.dalamud { + println!("Now waiting for the Dalamud plugin. Type /auracite begin in chat."); + + let package = Arc::new(Mutex::new(Package::default())); + + Server::bind("0.0.0.0:8000").serve_single_thread(PackageService { wants_stop: Arc::new(Mutex::new(false)), package: &package }).unwrap(); + + let package = &*package.lock().unwrap(); + + char_data.playtime = package.playtime.parse().unwrap(); + } + let serialized = serde_json::to_string(&char_data).unwrap(); write(character_folder.join("character.json"), serialized) .expect("Failed to write the character JSON file."); @@ -76,5 +128,5 @@ fn main() { .into_string() .unwrap(), ) - .expect("Failed to write the character HTML file."); + .expect("Failed to write the character HTML file."); }