├── .gitignore ├── Snapper ├── Snapper.json ├── PMP │ ├── PMPManipulationEntry.cs │ ├── PMPDefaultMod.cs │ ├── PMPMetadata.cs │ └── PMPExportManager.cs ├── Interop │ ├── Material.cs │ ├── ResourceHandle.cs │ ├── MtrlResource.cs │ ├── RenderModel.cs │ └── Weapon.cs ├── Models │ ├── SnapshotInfo.cs │ └── FileReplacement.cs ├── Export │ ├── MareCharaFileData.cs │ ├── MareCharaFileHeader.cs │ └── MareCharaFileManager.cs ├── Configuration.cs ├── CharacterFactory.cs ├── Windows │ ├── ConfigWindow.cs │ ├── MainWindow.cs │ ├── ImGuiRaii.cs │ ├── PlayerPanelPart.cs │ └── PlayerSelectorPart.cs ├── Snapper.csproj ├── Utils │ ├── Logger.cs │ └── DalamudUtil.cs ├── Plugin.cs ├── Managers │ ├── IpcManager.cs │ └── SnapshotManager.cs └── packages.lock.json ├── mcdf_pmp_ripper ├── mcdf_pmp_ripper.csproj └── Program.cs ├── .gitmodules ├── README.md ├── Snapper.sln └── .editorconfig /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | obj/ 3 | bin/ 4 | *.user -------------------------------------------------------------------------------- /Snapper/Snapper.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "Eqbot, astrodoobs, Vivi", 3 | "Name": "Snapper", 4 | "Punchline": "Snapshot mods from your friends", 5 | "Description": "/psnap to use", 6 | "InternalName": "snapper", 7 | "ApplicableVersion": "any", 8 | "Tags": [ 9 | "snapper" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Snapper/PMP/PMPManipulationEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Snapper.PMP 8 | { 9 | internal class PMPManipulationEntry 10 | { 11 | public string Type { get; set; } 12 | public object Manipulation { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mcdf_pmp_ripper/mcdf_pmp_ripper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0-windows 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Penumbra.GameData"] 2 | path = Penumbra.GameData 3 | url = https://github.com/Ottermandias/Penumbra.GameData 4 | [submodule "Penumbra.Api"] 5 | path = Penumbra.Api 6 | url = https://github.com/Ottermandias/Penumbra.Api 7 | [submodule "Penumbra.String"] 8 | path = Penumbra.String 9 | url = https://github.com/Ottermandias/Penumbra.String 10 | [submodule "OtterGui"] 11 | path = OtterGui 12 | url = https://github.com/Ottermandias/OtterGui 13 | -------------------------------------------------------------------------------- /Snapper/Interop/Material.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using FFXIVClientStructs.FFXIV.Client.Graphics.Render; 3 | 4 | namespace Penumbra.Interop.Structs; 5 | 6 | [StructLayout( LayoutKind.Explicit )] 7 | public unsafe struct Material 8 | { 9 | [FieldOffset( 0x10 )] 10 | public ResourceHandle* ResourceHandle; 11 | } 12 | 13 | [StructLayout( LayoutKind.Explicit )] 14 | public unsafe struct MaterialData 15 | { 16 | [FieldOffset( 0x0 )] 17 | public byte* Data; 18 | } -------------------------------------------------------------------------------- /Snapper/Models/SnapshotInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Snapper.Models 8 | { 9 | internal class SnapshotInfo 10 | { 11 | public string GlamourerString { get; set; } = string.Empty; 12 | public string CustomizeData { get; set; } = string.Empty; 13 | public string ManipulationString { get; set; } = string.Empty; 14 | public Dictionary> FileReplacements { get; set; } = new(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Snapper/PMP/PMPDefaultMod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Snapper.PMP 8 | { 9 | internal class PMPDefaultMod 10 | { 11 | public string Name { get; } = "Default"; 12 | public string Description { get; } = ""; 13 | public Dictionary Files { get; set; } = new(); 14 | public Dictionary FileSwaps { get; set; } = new(); 15 | public List Manipulations { get; set; } = new(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Snapper/PMP/PMPMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Snapper.PMP 8 | { 9 | internal class PMPMetadata 10 | { 11 | public int FileVersion { get; set; } = 3; 12 | public string Name { get; set; } = ""; 13 | public string Author { get; set; } = ""; 14 | public string Description { get; set; } = "Yoink!"; 15 | public string Version { get; set; } = "1.0.0"; 16 | public string Website { get; set; } = ""; 17 | public string[] ModTags { get; set; } = { }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mcdf_pmp_ripper/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | using Snapper.PMP; 3 | using MareSynchronos.Export; 4 | using Snapper; 5 | 6 | Configuration configuration = new Configuration(); 7 | configuration.WorkingDirectory = "."; 8 | MareCharaFileManager fileManager = new MareCharaFileManager(configuration); 9 | 10 | Console.WriteLine("Enter path to MCDF"); 11 | string mcdf_path = Console.ReadLine(); 12 | 13 | fileManager.LoadMareCharaFile(mcdf_path); 14 | string snapshot_path = fileManager.ExtractMareCharaFile(); 15 | 16 | PMPExportManager pmpManager = new PMPExportManager(configuration); 17 | pmpManager.SnapshotToPMP(snapshot_path); 18 | -------------------------------------------------------------------------------- /Snapper/Interop/ResourceHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using FFXIVClientStructs.FFXIV.Client.System.Resource; 4 | 5 | namespace Penumbra.Interop.Structs; 6 | 7 | [StructLayout( LayoutKind.Explicit )] 8 | public unsafe struct ResourceHandle 9 | { 10 | public const int SsoSize = 15; 11 | 12 | public byte* FileName() 13 | { 14 | if( FileNameLength > SsoSize ) 15 | { 16 | return FileNameData; 17 | } 18 | 19 | fixed( byte** name = &FileNameData ) 20 | { 21 | return ( byte* )name; 22 | } 23 | } 24 | 25 | [FieldOffset( 0x08 )] 26 | public ResourceCategory Category; 27 | 28 | [FieldOffset( 0x48 )] 29 | public byte* FileNameData; 30 | 31 | [FieldOffset( 0x58 )] 32 | public int FileNameLength; 33 | } -------------------------------------------------------------------------------- /Snapper/Interop/MtrlResource.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Penumbra.Interop.Structs; 5 | 6 | [StructLayout( LayoutKind.Explicit )] 7 | public unsafe struct MtrlResource 8 | { 9 | [FieldOffset( 0x00 )] 10 | public ResourceHandle Handle; 11 | 12 | [FieldOffset( 0xD0 )] 13 | public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. 14 | 15 | [FieldOffset( 0xE0 )] 16 | public byte* StringList; 17 | 18 | [FieldOffset( 0xF8 )] 19 | public ushort ShpkOffset; 20 | 21 | [FieldOffset( 0xFA )] 22 | public byte NumTex; 23 | 24 | public byte* ShpkString 25 | => StringList + ShpkOffset; 26 | 27 | public byte* TexString( int idx ) 28 | => StringList + *( TexSpace + 4 + idx * 8 ); 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snapper 2 | A plugin for saving/loading the current appearance of a character and all applied mods 3 | 4 | Get it from: [https://raw.githubusercontent.com/eqbot/plugins/main/PluginMaster.json](https://raw.githubusercontent.com/eqbot/DalamudPluginRepo/master/pluginmaster.json) 5 | 6 | this is probably going to be abandoned after 7.3, so enjoy, i hear 'Snappy' is an alternative but I don't know where the repository for it is 7 | 8 | # Usage 9 | First, configure a working directory in the settings window. 10 | 11 | For best results saving snapshots, take them outside of GPose. 12 | 13 | To load snapshots, enter GPose and select an actor, then hit the Load button to select a folder containing a snapshot. 14 | 15 | For best results loading, use actors spawned by [Brio](https://github.com/AsgardXIV/Brio) to load onto. I validate loading behavior against those actors and likely will be slow to fix any issues stemming from loading onto other actors. 16 | -------------------------------------------------------------------------------- /Snapper/Interop/RenderModel.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using FFXIVClientStructs.FFXIV.Client.Graphics.Render; 3 | using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; 4 | 5 | namespace Penumbra.Interop.Structs; 6 | 7 | [StructLayout( LayoutKind.Explicit )] 8 | public unsafe struct RenderModel 9 | { 10 | [FieldOffset( 0x18 )] 11 | public RenderModel* PreviousModel; 12 | 13 | [FieldOffset( 0x20 )] 14 | public RenderModel* NextModel; 15 | 16 | [FieldOffset( 0x30 )] 17 | public ResourceHandle* ResourceHandle; 18 | 19 | [FieldOffset( 0x40 )] 20 | public Skeleton* Skeleton; 21 | 22 | [FieldOffset( 0x58 )] 23 | public void** BoneList; 24 | 25 | [FieldOffset( 0x60 )] 26 | public int BoneListCount; 27 | 28 | [FieldOffset( 0x70 )] 29 | private void* UnkDXBuffer1; 30 | 31 | [FieldOffset( 0x78 )] 32 | private void* UnkDXBuffer2; 33 | 34 | [FieldOffset( 0x80 )] 35 | private void* UnkDXBuffer3; 36 | 37 | [FieldOffset( 0x98 )] 38 | public void** Materials; 39 | 40 | [FieldOffset( 0xA0 )] 41 | public int MaterialCount; 42 | } 43 | -------------------------------------------------------------------------------- /Snapper/Export/MareCharaFileData.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Objects.Enums; 2 | using Newtonsoft.Json; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace MareSynchronos.Export; 8 | 9 | public record MareCharaFileData 10 | { 11 | public string Description { get; set; } = string.Empty; 12 | public string GlamourerData { get; set; } = string.Empty; 13 | public string CustomizePlusData { get; set; } = string.Empty; 14 | public string ManipulationData { get; set; } = string.Empty; 15 | public List Files { get; set; } = new(); 16 | public List FileSwaps { get; set; } = new(); 17 | 18 | public MareCharaFileData() { } 19 | 20 | public byte[] ToByteArray() 21 | { 22 | return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this)); 23 | } 24 | 25 | public static MareCharaFileData FromByteArray(byte[] data) 26 | { 27 | return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data))!; 28 | } 29 | 30 | public record FileSwap(IEnumerable GamePaths, string FileSwapPath); 31 | 32 | public record FileData(IEnumerable GamePaths, long Length); 33 | } 34 | -------------------------------------------------------------------------------- /Snapper/Interop/Weapon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using FFXIVClientStructs.FFXIV.Client.Game.Character; 4 | using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; 5 | using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; 6 | using Penumbra.Interop.Structs; 7 | 8 | namespace Snapper.Interop; 9 | 10 | [StructLayout(LayoutKind.Explicit)] 11 | public unsafe struct Weapon 12 | { 13 | [FieldOffset(0x18)] public IntPtr Parent; 14 | [FieldOffset(0x20)] public IntPtr NextSibling; 15 | [FieldOffset(0x28)] public IntPtr PreviousSibling; 16 | [FieldOffset(0xA8)] public WeaponDrawObject* WeaponRenderModel; 17 | } 18 | 19 | [StructLayout(LayoutKind.Explicit)] 20 | public unsafe struct WeaponDrawObject 21 | { 22 | [FieldOffset(0x00)] public RenderModel* RenderModel; 23 | } 24 | 25 | [StructLayout(LayoutKind.Explicit)] 26 | public unsafe struct HumanExt 27 | { 28 | [FieldOffset(0x0)] public Human Human; 29 | [FieldOffset(0x9E8)] public Penumbra.Interop.Structs.ResourceHandle* Decal; 30 | [FieldOffset(0x9F0)] public Penumbra.Interop.Structs.ResourceHandle* LegacyBodyDecal; 31 | } 32 | 33 | [StructLayout(LayoutKind.Explicit)] 34 | public unsafe struct CharaExt 35 | { 36 | [FieldOffset(0x0)] public Character Character; 37 | [FieldOffset(0x650)] public Character* Mount; 38 | } 39 | -------------------------------------------------------------------------------- /Snapper/Export/MareCharaFileHeader.cs: -------------------------------------------------------------------------------- 1 | using Lumina.Extensions; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace MareSynchronos.Export; 7 | 8 | public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) 9 | { 10 | public static readonly byte CurrentVersion = 1; 11 | 12 | public byte Version { get; set; } = Version; 13 | public MareCharaFileData CharaFileData { get; set; } = CharaFileData; 14 | public string FilePath { get; private set; } 15 | 16 | public void WriteToStream(BinaryWriter writer) 17 | { 18 | writer.Write('M'); 19 | writer.Write('C'); 20 | writer.Write('D'); 21 | writer.Write('F'); 22 | writer.Write(Version); 23 | var charaFileDataArray = CharaFileData.ToByteArray(); 24 | writer.Write(charaFileDataArray.Length); 25 | writer.Write(charaFileDataArray); 26 | } 27 | 28 | public static MareCharaFileHeader? FromBinaryReader(string path, BinaryReader reader) 29 | { 30 | var chars = new string(reader.ReadChars(4)); 31 | if (!string.Equals(chars, "MCDF", System.StringComparison.Ordinal)) throw new System.Exception("Not a Mare Chara File"); 32 | 33 | MareCharaFileHeader? decoded = null; 34 | 35 | var version = reader.ReadByte(); 36 | if (version == 1) 37 | { 38 | var dataLength = reader.ReadInt32(); 39 | 40 | decoded = new(version, MareCharaFileData.FromByteArray(reader.ReadBytes(dataLength))); 41 | decoded.FilePath = path; 42 | } 43 | 44 | return decoded; 45 | } 46 | 47 | public void AdvanceReaderToData(BinaryReader reader) 48 | { 49 | reader.ReadChars(4); 50 | var version = reader.ReadByte(); 51 | if (version == 1) 52 | { 53 | var length = reader.ReadInt32(); 54 | _ = reader.ReadBytes(length); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Snapper/Configuration.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Configuration; 2 | using Dalamud.Plugin; 3 | using System; 4 | 5 | namespace Snapper 6 | { 7 | [Serializable] 8 | public class Configuration : IPluginConfiguration 9 | { 10 | public int Version { get; set; } = 0; 11 | 12 | public bool SomePropertyToBeSavedAndWithADefault { get; set; } = true; 13 | 14 | public string WorkingDirectory { get; set; } = string.Empty; 15 | 16 | public string FallBackGlamourerString { get; set; } = "Bh+LCAAAAAAAAArEV9ly2jAU/Rc/Mxkv8pa3hoRAJ6QZoMmzAhfQRNiuJSdDM/n3XsmYeMNl2nr6Joujc+7mY/FujBiHR0gFiyPj0hoYNz8yluwgksbluzGlLBrTaKXWEwm7Ca4cYrr2wBimIBCzplzAwPiSJHxvXMo0Kx7mEs9WdqonDr+bh5WNy4+B8W29rusROySh54WW9Rei+U675hhoi6BveaQnwat4tS8L2mHg95UbFlNUuufapK/m3cGmquWYZl9aIwBZ0fLd3vK6oaloGxA36EvxHpYvrYp+X4pPKROyPUuvL83ZiEUbSFtF3d6G9H+Ijqke1vk2fquczR8Q8MhEnMckFvFmw2HVjnsCmiijLriacX5og4ky3ctbToUAvdR7KluzAR9mQsY79hO05ccr4AfcjC713iPlGS6c40ktiydvIVrltTxAauS51y32SZnGqmPGwDZbWUYo36hihpxGJYRb/31UDbWpQVkq5J53gzAOrmIRZZQd1HHzFxYNY553rIApD6/CbvagUbN6ek7QFl6d0fNPx9dQJy0FYZSPgMosBaurRRUkDuyZSOdsJDkb6Z6N9M5G+l1I/GTR5X5BpYzjLlyOqFfdbOn4cxq/Vcbn1FTcwfo3Q4HI+ZYm7a9gMYo7yvkEzbsrrPtYdM79V/rWdXwaZ3LbWUWWCMnyT9bptwZRjbH1m2KZWHKYom9194Pxem0ar8AV+tr8YGsdowIPaNTlVpwGzeAVr8rKnM8AN5JtluQJZASVTKuGnvssAh9oSncgUVxhjxLfX6cZlyzhrOLC1oXZsOrSGbxki/z2VOTQgs/7sIgjXb4HSJf4n4BuTrCrYb5ju2fKJ5GESDC5rx9rE9HO+AfnlAFfs/U6y+d6pjpiXniBjdfbEO8rtymA+gpfuIHphDb+Xbk6JOqaodpyG5TKgZuUvuPbLglLjJ7pB8SxvE9KojlJK+PRsUucrmmFnhmUOImOm5TCdCzfI77ptxYafaFE52hkOWmHWOp48EnntVf+XxAdXPY47EXhGmTHnYLruFFPkSXNRhCsOfaiTGjnncDCF4xYwC882VK9PtGVa1hSXo82tE1H3eaP3MVGwVw8H+ktPVhqDOqv6pTia4qfH/Wmfnz8AgAA//8="; 17 | 18 | // the below exist just to make saving less cumbersome 19 | [NonSerialized] 20 | private IDalamudPluginInterface? PluginInterface; 21 | 22 | public void Initialize(IDalamudPluginInterface pluginInterface) 23 | { 24 | this.PluginInterface = pluginInterface; 25 | } 26 | 27 | public void Save() 28 | { 29 | this.PluginInterface!.SavePluginConfig(this); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Snapper/CharacterFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Dalamud.Game.ClientState.Objects.Enums; 4 | using Dalamud.Game.ClientState.Objects.SubKinds; 5 | using Dalamud.Game.ClientState.Objects.Types; 6 | 7 | namespace Snapper 8 | { 9 | public static class CharacterFactory 10 | { 11 | private static ConstructorInfo? _characterConstructor; 12 | 13 | private static void Initialize() 14 | { 15 | _characterConstructor ??= typeof( ICharacter ).GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new[] 16 | { 17 | typeof( IntPtr ), 18 | }, null )!; 19 | } 20 | 21 | private static ICharacter Character( IntPtr address ) 22 | { 23 | Initialize(); 24 | return (ICharacter)_characterConstructor?.Invoke( new object[] 25 | { 26 | address, 27 | } )!; 28 | } 29 | 30 | public static ICharacter? Convert( IGameObject? actor ) 31 | { 32 | if( actor == null ) 33 | { 34 | return null; 35 | } 36 | 37 | return actor switch 38 | { 39 | IPlayerCharacter p => p, 40 | IBattleChara b => b, 41 | _ => actor.ObjectKind switch 42 | { 43 | ObjectKind.BattleNpc => Character( actor.Address ), 44 | ObjectKind.Companion => Character( actor.Address ), 45 | ObjectKind.Retainer => Character( actor.Address ), 46 | ObjectKind.EventNpc => Character( actor.Address ), 47 | _ => null, 48 | }, 49 | }; 50 | } 51 | } 52 | 53 | public static class GameObjectExtensions 54 | { 55 | private const int ModelTypeOffset = 0x01B4; 56 | 57 | public static unsafe int ModelType( this IGameObject actor ) 58 | => *( int* )( actor.Address + ModelTypeOffset ); 59 | 60 | public static unsafe void SetModelType( this IGameObject actor, int value ) 61 | => *( int* )( actor.Address + ModelTypeOffset ) = value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Snapper/Windows/ConfigWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Numerics; 4 | using Dalamud.Interface; 5 | using Dalamud.Interface.ImGuiFileDialog; 6 | using Dalamud.Interface.Windowing; 7 | using Dalamud.Bindings.ImGui; 8 | 9 | namespace Snapper.Windows; 10 | 11 | public class ConfigWindow : Window, IDisposable 12 | { 13 | private Configuration Configuration; 14 | private FileDialogManager FileDialogManager; 15 | 16 | //ImGuiWindowFlags.NoResize 17 | public ConfigWindow(Plugin plugin) : base( 18 | "Snapper Settings", 19 | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | 20 | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoResize) 21 | { 22 | this.Size = new Vector2(500, 115); 23 | this.SizeCondition = ImGuiCond.Always; 24 | 25 | this.Configuration = plugin.Configuration; 26 | this.FileDialogManager = plugin.FileDialogManager; 27 | } 28 | 29 | public void Dispose() { } 30 | 31 | public override void Draw() 32 | { 33 | var workingDirectory = Configuration.WorkingDirectory; 34 | ImGui.InputText("Working Folder", ref workingDirectory, 255, ImGuiInputTextFlags.ReadOnly); 35 | ImGui.SameLine(); 36 | ImGui.PushFont(UiBuilder.IconFont); 37 | string folderIcon = FontAwesomeIcon.Folder.ToIconString(); 38 | if (ImGui.Button(folderIcon)) 39 | { 40 | FileDialogManager.OpenFolderDialog("Snapper working directory", (status, path) => 41 | { 42 | if (!status) 43 | { 44 | return; 45 | } 46 | 47 | if (Directory.Exists(path)) 48 | { 49 | this.Configuration.WorkingDirectory = path; 50 | this.Configuration.Save(); 51 | } 52 | }); 53 | } 54 | ImGui.PopFont(); 55 | 56 | ImGui.Text("Glamourer design fallback string (Temp until GlamourerAPI workaround)"); 57 | string fallbackString = Configuration.FallBackGlamourerString; 58 | ImGui.InputText("##input-format", ref fallbackString, 2500); 59 | if (fallbackString != Configuration.FallBackGlamourerString) 60 | { 61 | Configuration.FallBackGlamourerString = fallbackString; 62 | Configuration.Save(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Snapper/Models/FileReplacement.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using System.Text.RegularExpressions; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Snapper.Models; 9 | 10 | public class FileReplacement 11 | { 12 | private readonly Plugin plugin; 13 | 14 | public FileReplacement(Plugin plugin) 15 | { 16 | this.plugin = plugin; 17 | } 18 | 19 | public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); 20 | 21 | public List GamePaths { get; set; } = new(); 22 | 23 | public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, System.StringComparison.Ordinal)); 24 | 25 | public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && !string.Equals(GamePaths.First(), ResolvedPath, System.StringComparison.Ordinal); 26 | 27 | public string Hash { get; set; } = string.Empty; 28 | 29 | public string ResolvedPath { get; set; } = string.Empty; 30 | 31 | private void SetResolvedPath(string path) 32 | { 33 | ResolvedPath = path.ToLowerInvariant().Replace('\\', '/'); 34 | if (!HasFileReplacement || IsFileSwap) return; 35 | } 36 | 37 | public bool Verify() 38 | { 39 | ResolvePath(GamePaths.First()); 40 | 41 | var success = IsFileSwap; 42 | 43 | return success; 44 | } 45 | 46 | public override string ToString() 47 | { 48 | StringBuilder builder = new(); 49 | builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); 50 | return builder.ToString(); 51 | } 52 | 53 | internal void ReverseResolvePath(string path) 54 | { 55 | GamePaths = plugin.IpcManager.PenumbraReverseResolvePlayer(path).ToList(); 56 | SetResolvedPath(path); 57 | } 58 | 59 | internal void ResolvePath(string path) 60 | { 61 | GamePaths = new List { path }; 62 | SetResolvedPath(plugin.IpcManager.PenumbraResolvePath(path)); 63 | } 64 | 65 | internal void ResolvePathObject(string path, int objIdx) 66 | { 67 | GamePaths = new List { path }; 68 | SetResolvedPath(plugin.IpcManager.PenumbraResolvePathObject(path, objIdx)); 69 | } 70 | 71 | internal void ReverseResolvePathObject(string path, int objIdx) 72 | { 73 | GamePaths = plugin.IpcManager.PenumbraReverseResolveObject(path, objIdx).ToList(); 74 | SetResolvedPath(path); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Snapper/Windows/MainWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Numerics; 6 | using Dalamud.Game.ClientState.Objects.Enums; 7 | using Dalamud.Game.ClientState.Objects.Types; 8 | using Dalamud.Interface.Windowing; 9 | using Dalamud.Logging; 10 | using Dalamud.Bindings.ImGui; 11 | using ImGuiScene; 12 | using LZ4; 13 | using Snapper.Utils; 14 | 15 | namespace Snapper.Windows; 16 | 17 | public partial class MainWindow : Window, IDisposable 18 | { 19 | private const float SelectorWidth = 200; 20 | 21 | private Plugin Plugin; 22 | 23 | public MainWindow(Plugin plugin) : base( 24 | "Snapper - Fork", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) 25 | { 26 | this.SizeConstraints = new WindowSizeConstraints 27 | { 28 | MinimumSize = new Vector2(375, 330), 29 | MaximumSize = new Vector2(float.MaxValue, float.MaxValue) 30 | }; 31 | 32 | this.Plugin = plugin; 33 | } 34 | 35 | public void Dispose() 36 | { 37 | } 38 | 39 | public override void Draw() 40 | { 41 | if (ImGui.Button("Show Settings")) 42 | { 43 | this.Plugin.DrawConfigUI(); 44 | } 45 | 46 | ImGui.SameLine(); 47 | if(ImGui.Button("Revert snapshots")) 48 | { 49 | this.Plugin.SnapshotManager.RevertAllSnapshots(); 50 | } 51 | 52 | ImGui.SameLine(); 53 | if(ImGui.Button("Import MCDF file")) 54 | { 55 | Plugin.FileDialogManager.OpenFileDialog("Snapshot selection", ".mcdf", (status, path) => 56 | { 57 | if (!status) 58 | { 59 | return; 60 | } 61 | 62 | if (File.Exists(path[0])) 63 | { 64 | this.Plugin.MCDFManager.LoadMareCharaFile(path[0]); 65 | this.Plugin.MCDFManager.ExtractMareCharaFile(); 66 | } 67 | }, 1, Plugin.Configuration.WorkingDirectory); 68 | } 69 | 70 | ImGui.SameLine(); 71 | if(ImGui.Button("Export snapshot as PMP")) 72 | { 73 | Plugin.FileDialogManager.OpenFolderDialog("Snapshot selection", (status, path) => 74 | { 75 | if (!status) 76 | { 77 | return; 78 | } 79 | 80 | if (Directory.Exists(path)) 81 | { 82 | Plugin.PMPExportManager.SnapshotToPMP(path); 83 | } 84 | }, Plugin.Configuration.WorkingDirectory); 85 | } 86 | 87 | ImGui.Spacing(); 88 | 89 | this.DrawPlayerSelector(); 90 | if (!currentLabel.Any()) 91 | return; 92 | 93 | ImGui.SameLine(); 94 | this.DrawActorPanel(); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Snapper/Snapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0.0.0.6 7 | Snapper 8 | 9 | https://github.com/goatcorp/SamplePlugin 10 | 11 | 12 | 13 | net9.0-windows7.0 14 | x64 15 | enable 16 | latest 17 | true 18 | false 19 | false 20 | true 21 | true 22 | 23 | 24 | 25 | $(appdata)\XIVLauncher\addon\Hooks\dev\ 26 | 27 | 28 | 29 | $(DALAMUD_HOME)/ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | $(DalamudLibPath)FFXIVClientStructs.dll 48 | false 49 | 50 | 51 | $(DalamudLibPath)Newtonsoft.Json.dll 52 | false 53 | 54 | 55 | $(DalamudLibPath)Dalamud.dll 56 | false 57 | 58 | 59 | $(DalamudLibPath)Dalamud.Bindings.ImGui.dll 60 | false 61 | 62 | 63 | $(DalamudLibPath)InteropGenerator.Runtime.dll 64 | false 65 | 66 | 67 | $(DalamudLibPath)ImGuiScene.dll 68 | false 69 | 70 | 71 | $(DalamudLibPath)Lumina.dll 72 | false 73 | 74 | 75 | $(DalamudLibPath)Lumina.Excel.dll 76 | false 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Snapper/Utils/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Dalamud.Logging; 4 | using Dalamud.Plugin.Services; 5 | using Dalamud.Utility; 6 | using FFXIVClientStructs.FFXIV.Client.UI.Agent; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Snapper.Utils; 10 | 11 | internal class Logger : ILogger 12 | { 13 | private readonly string name; 14 | 15 | public static void Info(string info) 16 | { 17 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 18 | Plugin.Log.Information($"[{caller}] {info}", ""); 19 | } 20 | 21 | public static void Debug(string debug, string stringToHighlight = "") 22 | { 23 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 24 | if (debug.Contains(stringToHighlight, StringComparison.Ordinal) && !stringToHighlight.IsNullOrEmpty()) 25 | { 26 | Plugin.Log.Warning($"[{caller}] {debug}"); 27 | } 28 | else 29 | { 30 | Plugin.Log.Debug($"[{caller}] {debug}"); 31 | } 32 | } 33 | 34 | public static void Error(string msg, Exception ex) 35 | { 36 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 37 | Plugin.Log.Error($"[{caller}] {msg} {Environment.NewLine} Exception: {ex.Message} {Environment.NewLine} {ex.StackTrace}"); 38 | } 39 | 40 | public static void Warn(string msg, Exception ex) 41 | { 42 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 43 | Plugin.Log.Warning($"[{caller}] {msg} {Environment.NewLine} Exception: {ex.Message} {Environment.NewLine} {ex.StackTrace}"); 44 | } 45 | 46 | public static void Error(string msg) 47 | { 48 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 49 | Plugin.Log.Error($"[{caller}] {msg}"); 50 | } 51 | 52 | public static void Warn(string warn) 53 | { 54 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 55 | Plugin.Log.Warning($"[{caller}] {warn}"); 56 | } 57 | 58 | public static void Verbose(string verbose) 59 | { 60 | var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; 61 | #if DEBUG 62 | Plugin.Log.Debug($"[{caller}] {verbose}"); 63 | #else 64 | Plugin.Log.Verbose($"[{caller}] {verbose}"); 65 | #endif 66 | } 67 | public Logger(string name) 68 | { 69 | this.name = name; 70 | } 71 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 72 | { 73 | if (!IsEnabled(logLevel)) return; 74 | 75 | switch (logLevel) 76 | { 77 | case LogLevel.Debug: 78 | Plugin.Log.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); 79 | break; 80 | case LogLevel.Error: 81 | case LogLevel.Critical: 82 | Plugin.Log.Error($"[{name}] [{eventId}] {formatter(state, exception)}"); 83 | break; 84 | case LogLevel.Information: 85 | Plugin.Log.Information($"[{name}] [{eventId}] {formatter(state, exception)}"); 86 | break; 87 | case LogLevel.Warning: 88 | Plugin.Log.Warning($"[{name}] [{eventId}] {formatter(state, exception)}"); 89 | break; 90 | case LogLevel.Trace: 91 | default: 92 | #if DEBUG 93 | Plugin.Log.Verbose($"[{name}] [{eventId}] {formatter(state, exception)}"); 94 | #else 95 | Plugin.Log.Verbose($"[{name}] {eventId} {state} {formatter(state, exception)}"); 96 | #endif 97 | break; 98 | } 99 | } 100 | 101 | public bool IsEnabled(LogLevel logLevel) 102 | { 103 | return true; 104 | } 105 | 106 | public IDisposable BeginScope(TState state) => default!; 107 | } 108 | -------------------------------------------------------------------------------- /Snapper/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.Command; 2 | using Dalamud.IoC; 3 | using Dalamud.Plugin; 4 | using Dalamud.Plugin.Services; 5 | using System.IO; 6 | using System.Reflection; 7 | using Dalamud.Interface.Windowing; 8 | using Snapper.Windows; 9 | using Dalamud.Interface.ImGuiFileDialog; 10 | using Dalamud.Game.ClientState.Objects; 11 | using Snapper.Utils; 12 | using Dalamud.Game.ClientState; 13 | using Dalamud.Game; 14 | using Dalamud.Game.Gui; 15 | using Dalamud.Game.ClientState.Conditions; 16 | using Snapper.Managers; 17 | using MareSynchronos.Export; 18 | using Snapper.PMP; 19 | 20 | namespace Snapper 21 | { 22 | public sealed class Plugin : IDalamudPlugin 23 | { 24 | public string Name => "Snapper"; 25 | private const string CommandName = "/psnap"; 26 | 27 | public Configuration Configuration { get; init; } 28 | public IObjectTable Objects { get; init; } 29 | public WindowSystem WindowSystem = new("Snapper"); 30 | public FileDialogManager FileDialogManager = new FileDialogManager(); 31 | public DalamudUtil DalamudUtil { get; init; } 32 | public IpcManager IpcManager { get; init; } 33 | public SnapshotManager SnapshotManager { get; init; } 34 | public MareCharaFileManager MCDFManager { get; init; } 35 | public PMPExportManager PMPExportManager { get; init; } 36 | 37 | private ConfigWindow ConfigWindow { get; init; } 38 | private MainWindow MainWindow { get; init; } 39 | 40 | 41 | [PluginService] public static IPluginLog Log { get; private set; } = null!; 42 | [PluginService] public static ICommandManager CommandManager { get; private set; } = null!; 43 | [PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!; 44 | //[PluginService] public static Configuration Configuration { get; private set; } = null!; 45 | 46 | //public IPluginLog PluginLog { get; private set; } = null!; 47 | //public static object Log { get; internal set; } 48 | 49 | public Plugin( 50 | IFramework framework, 51 | IObjectTable objectTable, 52 | IClientState clientState, 53 | ICondition condition, 54 | IChatGui chatGui) 55 | { 56 | this.Objects = objectTable; 57 | 58 | this.DalamudUtil = new DalamudUtil(clientState, objectTable, framework, condition, chatGui); 59 | this.IpcManager = new IpcManager(PluginInterface, this.DalamudUtil); 60 | 61 | Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); 62 | Configuration.Initialize(PluginInterface); 63 | 64 | this.SnapshotManager = new SnapshotManager(this); 65 | this.MCDFManager = new MareCharaFileManager(this); 66 | this.PMPExportManager = new PMPExportManager(this); 67 | 68 | 69 | ConfigWindow = new ConfigWindow(this); 70 | MainWindow = new MainWindow(this); 71 | WindowSystem.AddWindow(ConfigWindow); 72 | WindowSystem.AddWindow(MainWindow); 73 | 74 | CommandManager.AddHandler(CommandName, new CommandInfo(OnCommand) 75 | { 76 | HelpMessage = "Opens main Snapper interface" 77 | }); 78 | 79 | PluginInterface.UiBuilder.Draw += DrawUI; 80 | PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; 81 | PluginInterface.UiBuilder.DisableGposeUiHide = true; 82 | PluginInterface.UiBuilder.OpenMainUi += ToggleMainUI; 83 | } 84 | 85 | public void Dispose() 86 | { 87 | this.WindowSystem.RemoveAllWindows(); 88 | CommandManager.RemoveHandler(CommandName); 89 | this.SnapshotManager.RevertAllSnapshots(); 90 | } 91 | 92 | private void OnCommand(string command, string args) 93 | { 94 | // in response to the slash command, just display our main ui 95 | ToggleMainUI(); 96 | } 97 | 98 | private void DrawUI() 99 | { 100 | this.WindowSystem.Draw(); 101 | this.FileDialogManager.Draw(); 102 | } 103 | 104 | public void DrawConfigUI() 105 | { 106 | ConfigWindow.IsOpen = true; 107 | } 108 | 109 | public void ToggleMainUI() => MainWindow.Toggle(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Snapper/Windows/ImGuiRaii.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Numerics; 4 | using Dalamud.Bindings.ImGui; 5 | 6 | namespace Snapper.Windows 7 | { 8 | public sealed class ImGuiRaii : IDisposable 9 | { 10 | private int _colorStack; 11 | private int _fontStack; 12 | private int _styleStack; 13 | private float _indentation; 14 | 15 | private Stack? _onDispose; 16 | 17 | public static ImGuiRaii NewGroup() 18 | => new ImGuiRaii().Group(); 19 | 20 | public ImGuiRaii Group() 21 | => Begin(ImGui.BeginGroup, ImGui.EndGroup); 22 | 23 | public static ImGuiRaii NewTooltip() 24 | => new ImGuiRaii().Tooltip(); 25 | 26 | public ImGuiRaii Tooltip() 27 | => Begin(ImGui.BeginTooltip, ImGui.EndTooltip); 28 | 29 | public ImGuiRaii PushColor(ImGuiCol which, uint color) 30 | { 31 | ImGui.PushStyleColor(which, color); 32 | ++_colorStack; 33 | return this; 34 | } 35 | 36 | public ImGuiRaii PushColor(ImGuiCol which, Vector4 color) 37 | { 38 | ImGui.PushStyleColor(which, color); 39 | ++_colorStack; 40 | return this; 41 | } 42 | 43 | public ImGuiRaii PopColors(int n = 1) 44 | { 45 | var actualN = Math.Min(n, _colorStack); 46 | if (actualN > 0) 47 | { 48 | ImGui.PopStyleColor(actualN); 49 | _colorStack -= actualN; 50 | } 51 | 52 | return this; 53 | } 54 | 55 | public ImGuiRaii PushStyle(ImGuiStyleVar style, Vector2 value) 56 | { 57 | ImGui.PushStyleVar(style, value); 58 | ++_styleStack; 59 | return this; 60 | } 61 | 62 | public ImGuiRaii PushStyle(ImGuiStyleVar style, float value) 63 | { 64 | ImGui.PushStyleVar(style, value); 65 | ++_styleStack; 66 | return this; 67 | } 68 | 69 | public ImGuiRaii PopStyles(int n = 1) 70 | { 71 | var actualN = Math.Min(n, _styleStack); 72 | if (actualN > 0) 73 | { 74 | ImGui.PopStyleVar(actualN); 75 | _styleStack -= actualN; 76 | } 77 | 78 | return this; 79 | } 80 | 81 | public ImGuiRaii PushFont(ImFontPtr font) 82 | { 83 | ImGui.PushFont(font); 84 | ++_fontStack; 85 | return this; 86 | } 87 | 88 | public ImGuiRaii PopFonts(int n = 1) 89 | { 90 | var actualN = Math.Min(n, _fontStack); 91 | 92 | while (actualN-- > 0) 93 | { 94 | ImGui.PopFont(); 95 | --_fontStack; 96 | } 97 | 98 | return this; 99 | } 100 | 101 | public ImGuiRaii Indent(float width) 102 | { 103 | if (width != 0) 104 | { 105 | ImGui.Indent(width); 106 | _indentation += width; 107 | } 108 | 109 | return this; 110 | } 111 | 112 | public ImGuiRaii Unindent(float width) 113 | => Indent(-width); 114 | 115 | public bool Begin(Func begin, Action end) 116 | { 117 | if (begin()) 118 | { 119 | _onDispose ??= new Stack(); 120 | _onDispose.Push(end); 121 | return true; 122 | } 123 | 124 | return false; 125 | } 126 | 127 | public ImGuiRaii Begin(Action begin, Action end) 128 | { 129 | begin(); 130 | _onDispose ??= new Stack(); 131 | _onDispose.Push(end); 132 | return this; 133 | } 134 | 135 | public void End(int n = 1) 136 | { 137 | var actualN = Math.Min(n, _onDispose?.Count ?? 0); 138 | while (actualN-- > 0) 139 | _onDispose!.Pop()(); 140 | } 141 | 142 | public void Dispose() 143 | { 144 | Unindent(_indentation); 145 | PopColors(_colorStack); 146 | PopStyles(_styleStack); 147 | PopFonts(_fontStack); 148 | if (_onDispose != null) 149 | { 150 | End(_onDispose.Count); 151 | _onDispose = null; 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Snapper/PMP/PMPExportManager.cs: -------------------------------------------------------------------------------- 1 | using Snapper.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Snapper.Utils; 9 | using System.Text.Json; 10 | using System.ComponentModel.Design; 11 | using System.IO.Compression; 12 | 13 | namespace Snapper.PMP 14 | { 15 | public class PMPExportManager 16 | { 17 | private Configuration configuration; 18 | public PMPExportManager(Plugin plugin) 19 | { 20 | this.configuration = plugin.Configuration; 21 | } 22 | public PMPExportManager(Configuration configuration) 23 | { 24 | this.configuration = configuration; 25 | } 26 | 27 | public void SnapshotToPMP(string snapshotPath) 28 | { 29 | Logger.Debug($"Operating on {snapshotPath}"); 30 | //read snapshot 31 | string infoJson = File.ReadAllText(Path.Combine(snapshotPath, "snapshot.json")); 32 | if (infoJson == null) 33 | { 34 | Logger.Warn("No snapshot json found, aborting"); 35 | return; 36 | } 37 | SnapshotInfo? snapshotInfo = JsonSerializer.Deserialize(infoJson); 38 | if (snapshotInfo == null) 39 | { 40 | Logger.Warn("Failed to deserialize snapshot json, aborting"); 41 | return; 42 | } 43 | 44 | //begin building PMP 45 | string snapshotName = new DirectoryInfo(snapshotPath).Name; 46 | string pmpFileName = $"{snapshotName}_{Guid.NewGuid()}"; 47 | 48 | 49 | string workingDirectory = Path.Combine(configuration.WorkingDirectory, $"temp_{pmpFileName}"); 50 | if (!Directory.Exists(workingDirectory)) 51 | { 52 | Directory.CreateDirectory(workingDirectory); 53 | } 54 | 55 | //meta.json 56 | PMPMetadata metadata = new PMPMetadata(); 57 | metadata.Name = snapshotName; 58 | metadata.Author = $"not {snapshotName} anymore :3"; 59 | using(FileStream stream = new FileStream(Path.Combine(workingDirectory, "meta.json"), FileMode.Create)) 60 | { 61 | JsonSerializer.Serialize(stream, metadata); 62 | } 63 | 64 | //default_mod.json 65 | PMPDefaultMod defaultMod = new PMPDefaultMod(); 66 | foreach (var file in snapshotInfo.FileReplacements) 67 | { 68 | foreach(var replacement in file.Value) 69 | { 70 | defaultMod.Files.Add(replacement, file.Key); 71 | } 72 | } 73 | 74 | List? manipulations; 75 | FromCompressedBase64>(snapshotInfo.ManipulationString, out manipulations); 76 | if(manipulations != null) 77 | { 78 | defaultMod.Manipulations = manipulations; 79 | } 80 | using (FileStream stream = new FileStream(Path.Combine(workingDirectory, "default_mod.json"), FileMode.Create)) 81 | { 82 | JsonSerializer.Serialize(stream, defaultMod, new JsonSerializerOptions { WriteIndented = true}); 83 | } 84 | 85 | //mods 86 | foreach(var file in snapshotInfo.FileReplacements) 87 | { 88 | 89 | string modPath = Path.Combine(snapshotPath, file.Key); 90 | string destPath = Path.Combine(workingDirectory, file.Key); 91 | Logger.Debug($"Copying {modPath}"); 92 | Directory.CreateDirectory(Path.GetDirectoryName(destPath) ?? ""); 93 | File.Copy(modPath, destPath); 94 | } 95 | 96 | //zip and make pmp file 97 | ZipFile.CreateFromDirectory(workingDirectory, Path.Combine(configuration.WorkingDirectory, $"{pmpFileName}.pmp")); 98 | 99 | //cleanup 100 | Directory.Delete(workingDirectory, true); 101 | } 102 | 103 | 104 | // Decompress a base64 encoded string to the given type and a prepended version byte if possible. 105 | // On failure, data will be default and version will be byte.MaxValue. 106 | internal static byte FromCompressedBase64(string base64, out T? data) 107 | { 108 | var version = byte.MaxValue; 109 | try 110 | { 111 | var bytes = Convert.FromBase64String(base64); 112 | using var compressedStream = new MemoryStream(bytes); 113 | using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress); 114 | using var resultStream = new MemoryStream(); 115 | zipStream.CopyTo(resultStream); 116 | bytes = resultStream.ToArray(); 117 | version = bytes[0]; 118 | var json = Encoding.UTF8.GetString(bytes, 1, bytes.Length - 1); 119 | data = JsonSerializer.Deserialize(json); 120 | } 121 | catch 122 | { 123 | data = default; 124 | } 125 | 126 | return version; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Snapper/Export/MareCharaFileManager.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using Dalamud.Game.ClientState.Objects.Types; 4 | using LZ4; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | using Snapper.Utils; 10 | using Snapper; 11 | using Snapper.Managers; 12 | using Snapper.Models; 13 | using System.Text.Json; 14 | 15 | namespace MareSynchronos.Export; 16 | public class MareCharaFileManager 17 | { 18 | private readonly Configuration _configuration; 19 | public MareCharaFileHeader? LoadedCharaFile { get; private set; } 20 | 21 | public MareCharaFileManager(Plugin plugin) 22 | { 23 | _configuration = plugin.Configuration; 24 | } 25 | 26 | public MareCharaFileManager(Configuration configuration) 27 | { 28 | _configuration = configuration; 29 | } 30 | 31 | public void ClearMareCharaFile() 32 | { 33 | LoadedCharaFile = null; 34 | } 35 | 36 | public void LoadMareCharaFile(string filePath) 37 | { 38 | try 39 | { 40 | using var unwrapped = File.OpenRead(filePath); 41 | using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); 42 | using var reader = new BinaryReader(lz4Stream); 43 | LoadedCharaFile = MareCharaFileHeader.FromBinaryReader(filePath, reader); 44 | Logger.Debug("Read Mare Chara File"); 45 | Logger.Debug("Version: " + LoadedCharaFile.Version); 46 | 47 | } 48 | catch { throw; } 49 | } 50 | 51 | private Dictionary ExtractFilesFromCharaFile(MareCharaFileHeader charaFileHeader, BinaryReader reader) 52 | { 53 | Dictionary gamePathToFilePath = new(StringComparer.Ordinal); 54 | int i = 0; 55 | foreach (var fileData in charaFileHeader.CharaFileData.Files) 56 | { 57 | var fileName = Path.Combine(_configuration.WorkingDirectory, "mcdf_" + charaFileHeader.CharaFileData.Description, "mare_" + (i++) + ".tmp"); 58 | var length = fileData.Length; 59 | var bufferSize = 4 * 1024 * 1024; 60 | var buffer = new byte[bufferSize]; 61 | using var fs = File.OpenWrite(fileName); 62 | using var wr = new BinaryWriter(fs); 63 | while (length > 0) 64 | { 65 | if (length < bufferSize) bufferSize = (int)length; 66 | buffer = reader.ReadBytes(bufferSize); 67 | wr.Write(length > bufferSize ? buffer : buffer.Take((int)length).ToArray()); 68 | length -= bufferSize; 69 | } 70 | foreach (var path in fileData.GamePaths) 71 | { 72 | gamePathToFilePath[path] = Path.GetFileName(fileName); 73 | Logger.Verbose(path + " => " + fileName); 74 | } 75 | } 76 | 77 | return gamePathToFilePath; 78 | } 79 | 80 | public string? ExtractMareCharaFile() 81 | { 82 | Dictionary extractedFiles = new(); 83 | try 84 | { 85 | if (LoadedCharaFile == null || !File.Exists(LoadedCharaFile.FilePath)) return null; 86 | var path = Path.Combine(_configuration.WorkingDirectory, "mcdf_" + LoadedCharaFile.CharaFileData.Description); 87 | //create directory if needed 88 | if (Directory.Exists(path)) 89 | { 90 | Logger.Warn("Snapshot already existed, deleting"); 91 | Directory.Delete(path, true); 92 | } 93 | Directory.CreateDirectory(path); 94 | 95 | //extract files 96 | using var unwrapped = File.OpenRead(LoadedCharaFile.FilePath); 97 | using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); 98 | using var reader = new BinaryReader(lz4Stream); 99 | LoadedCharaFile.AdvanceReaderToData(reader); 100 | extractedFiles = ExtractFilesFromCharaFile(LoadedCharaFile, reader); 101 | 102 | //generate snapper JSON manifest 103 | SnapshotInfo snapInfo = new SnapshotInfo(); 104 | snapInfo.GlamourerString = LoadedCharaFile.CharaFileData.GlamourerData; 105 | snapInfo.ManipulationString = LoadedCharaFile.CharaFileData.ManipulationData; 106 | snapInfo.CustomizeData = LoadedCharaFile.CharaFileData.CustomizePlusData; 107 | snapInfo.FileReplacements = new(); 108 | foreach(var record in extractedFiles) 109 | { 110 | if (snapInfo.FileReplacements.ContainsKey(record.Value)) 111 | { 112 | snapInfo.FileReplacements[record.Value].Add(record.Key); 113 | } 114 | else 115 | { 116 | snapInfo.FileReplacements.Add(record.Value, new List() { record.Key }); 117 | } 118 | } 119 | 120 | string infoJson = JsonSerializer.Serialize(snapInfo); 121 | File.WriteAllText(Path.Combine(_configuration.WorkingDirectory, "mcdf_" + LoadedCharaFile.CharaFileData.Description, "snapshot.json"), infoJson); 122 | return path; 123 | } 124 | catch { throw; } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Snapper.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32825.248 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snapper", "Snapper\Snapper.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcdf_pmp_ripper", "mcdf_pmp_ripper\mcdf_pmp_ripper.csproj", "{3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{5AA03B81-A0A2-4EE1-8510-A656577F89FB}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{EF6463D8-94E4-432F-9FAA-94160AAD7B27}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterGui.csproj", "{56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{E506688B-ED69-4FFB-9751-E716A005506C}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Release|Any CPU = Release|Any CPU 23 | Release|x64 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 27 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 28 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 29 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 30 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 31 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 32 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 33 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 34 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Debug|x64.ActiveCfg = Debug|Any CPU 37 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Debug|x64.Build.0 = Debug|Any CPU 38 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Release|x64.ActiveCfg = Release|Any CPU 41 | {3D1F6AAF-8825-4CD8-AB9C-E9E891330C5B}.Release|x64.Build.0 = Release|Any CPU 42 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Debug|Any CPU.ActiveCfg = Debug|x64 43 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Debug|Any CPU.Build.0 = Debug|x64 44 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Debug|x64.ActiveCfg = Debug|x64 45 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Debug|x64.Build.0 = Debug|x64 46 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Release|Any CPU.ActiveCfg = Release|x64 47 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Release|Any CPU.Build.0 = Release|x64 48 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Release|x64.ActiveCfg = Release|x64 49 | {5AA03B81-A0A2-4EE1-8510-A656577F89FB}.Release|x64.Build.0 = Release|x64 50 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Debug|Any CPU.ActiveCfg = Debug|x64 51 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Debug|Any CPU.Build.0 = Debug|x64 52 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Debug|x64.ActiveCfg = Debug|x64 53 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Debug|x64.Build.0 = Debug|x64 54 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Release|Any CPU.ActiveCfg = Release|x64 55 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Release|Any CPU.Build.0 = Release|x64 56 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Release|x64.ActiveCfg = Release|x64 57 | {EF6463D8-94E4-432F-9FAA-94160AAD7B27}.Release|x64.Build.0 = Release|x64 58 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Debug|Any CPU.ActiveCfg = Debug|x64 59 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Debug|Any CPU.Build.0 = Debug|x64 60 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Debug|x64.ActiveCfg = Debug|x64 61 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Debug|x64.Build.0 = Debug|x64 62 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Release|Any CPU.ActiveCfg = Release|x64 63 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Release|Any CPU.Build.0 = Release|x64 64 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Release|x64.ActiveCfg = Release|x64 65 | {56ECE7C7-73F4-4E1C-92FB-9015F37BCAFE}.Release|x64.Build.0 = Release|x64 66 | {E506688B-ED69-4FFB-9751-E716A005506C}.Debug|Any CPU.ActiveCfg = Debug|x64 67 | {E506688B-ED69-4FFB-9751-E716A005506C}.Debug|Any CPU.Build.0 = Debug|x64 68 | {E506688B-ED69-4FFB-9751-E716A005506C}.Debug|x64.ActiveCfg = Debug|x64 69 | {E506688B-ED69-4FFB-9751-E716A005506C}.Debug|x64.Build.0 = Debug|x64 70 | {E506688B-ED69-4FFB-9751-E716A005506C}.Release|Any CPU.ActiveCfg = Release|x64 71 | {E506688B-ED69-4FFB-9751-E716A005506C}.Release|Any CPU.Build.0 = Release|x64 72 | {E506688B-ED69-4FFB-9751-E716A005506C}.Release|x64.ActiveCfg = Release|x64 73 | {E506688B-ED69-4FFB-9751-E716A005506C}.Release|x64.Build.0 = Release|x64 74 | EndGlobalSection 75 | GlobalSection(SolutionProperties) = preSolution 76 | HideSolutionNode = FALSE 77 | EndGlobalSection 78 | GlobalSection(ExtensibilityGlobals) = postSolution 79 | SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} 80 | EndGlobalSection 81 | EndGlobal 82 | -------------------------------------------------------------------------------- /Snapper/Windows/PlayerPanelPart.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface; 2 | using Dalamud.Interface.Utility; 3 | using Dalamud.Interface.Colors; 4 | using Dalamud.Plugin.Services; 5 | using Dalamud.Interface.ImGuiFileDialog; 6 | using Dalamud.Bindings.ImGui; 7 | using Snapper.Utils; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Numerics; 13 | using System.Text; 14 | using System.Threading.Tasks; 15 | using static Lumina.Data.Parsing.Layer.LayerCommon; 16 | 17 | namespace Snapper.Windows 18 | { 19 | public partial class MainWindow 20 | { 21 | private const uint RedHeaderColor = 0xFF1818C0; 22 | private const uint GreenHeaderColor = 0xFF18C018; 23 | public bool IsInGpose { get; private set; } = false; 24 | 25 | private void DrawPlayerHeader() 26 | { 27 | var color = player == null ? RedHeaderColor : GreenHeaderColor; 28 | var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); 29 | ImGui.Button($"{currentLabel}##playerHeader", -Vector2.UnitX * 0.0001f); 30 | } 31 | 32 | private void DrawPlayerPanel() 33 | { 34 | ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.ParsedOrange); 35 | ImGui.Text("WARNING:"); 36 | ImGui.PopStyleColor(); 37 | ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); 38 | ImGui.Text("Glamourer API currently does not allow you to get their Glamourer design automatically like before when synced with mare."); 39 | ImGui.Text("As a temporary workaround, copy their Glamourer design to clipboard and edit the file it creates."); 40 | ImGui.PopStyleColor(); 41 | ImGui.Text("Save snapshot of player "); 42 | ImGui.SameLine(); 43 | ImGui.PushFont(UiBuilder.IconFont); 44 | try 45 | { 46 | string saveIcon = FontAwesomeIcon.Save.ToIconString(); 47 | if (ImGui.Button(saveIcon)) 48 | { 49 | //save snapshot 50 | if (player != null) 51 | Plugin.SnapshotManager.SaveSnapshot(player); 52 | } 53 | } 54 | finally 55 | { 56 | ImGui.PopFont(); 57 | } 58 | if (!IsInGpose) 59 | { 60 | ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); 61 | ImGui.Text("Saving snapshots while GPose is active may result in broken/incorrect snapshots. For best results, leave GPose first."); 62 | ImGui.PopStyleColor(); 63 | ImGui.Spacing(); 64 | } 65 | 66 | ImGui.Text("Append to existing snapshot"); 67 | ImGui.SameLine(); 68 | ImGui.PushFont(UiBuilder.IconFont); 69 | try 70 | { 71 | string addIcon = FontAwesomeIcon.Plus.ToIconString(); 72 | if(ImGui.Button(addIcon)) 73 | { 74 | if (player != null) 75 | Plugin.SnapshotManager.AppendSnapshot(player); 76 | } 77 | } 78 | finally 79 | { 80 | ImGui.PopFont(); 81 | } 82 | 83 | if (this.modifiable) 84 | { 85 | ImGui.Text("Load snapshot onto "); 86 | ImGui.SameLine(); 87 | ImGui.PushFont(UiBuilder.IconFont); 88 | try 89 | { 90 | string loadIcon = FontAwesomeIcon.Clipboard.ToIconString(); 91 | if (ImGui.Button(loadIcon)) 92 | { 93 | Plugin.FileDialogManager.OpenFolderDialog("Snapshot selection", (status, path) => 94 | { 95 | if (!status) 96 | { 97 | return; 98 | } 99 | 100 | if (Directory.Exists(path)) 101 | { 102 | if (player != null && objIdxSelected.HasValue) 103 | Plugin.SnapshotManager.LoadSnapshot(player, objIdxSelected.Value, path); 104 | } 105 | }, Plugin.Configuration.WorkingDirectory); 106 | } 107 | } 108 | finally 109 | { 110 | ImGui.PopFont(); 111 | } 112 | } 113 | else 114 | { 115 | ImGui.Text("Loading snapshots can only be done on GPose actors"); 116 | } 117 | } 118 | 119 | private void DrawMonsterPanel() 120 | { 121 | 122 | } 123 | 124 | private void DrawActorPanel() 125 | { 126 | using var raii = ImGuiRaii.NewGroup(); 127 | DrawPlayerHeader(); 128 | if (!ImGui.BeginChild("##playerData", -Vector2.One, true)) 129 | { 130 | ImGui.EndChild(); 131 | return; 132 | } 133 | 134 | if (player != null || player.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) 135 | DrawPlayerPanel(); 136 | else 137 | DrawMonsterPanel(); 138 | 139 | ImGui.EndChild(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Snapper/Windows/PlayerSelectorPart.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Objects.Enums; 2 | using Dalamud.Game.ClientState.Objects.Types; 3 | using Dalamud.Plugin.Services; 4 | using Dalamud.Bindings.ImGui; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Numerics; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using Penumbra.GameData.Structs; 12 | 13 | namespace Snapper.Windows 14 | { 15 | public partial class MainWindow 16 | { 17 | public static int GPoseObjectId = ObjectIndex.GPosePlayer.Index; 18 | public static int CharacterScreenIndex = ObjectIndex.CharacterScreen.Index; 19 | public static int ExamineScreenIndex = ObjectIndex.ExamineScreen.Index; 20 | public static int FittingRoomIndex = ObjectIndex.FittingRoom.Index; 21 | public static int DyePreviewIndex = ObjectIndex.DyePreview.Index; 22 | 23 | private string playerFilter = string.Empty; 24 | private string playerFilterLower = string.Empty; 25 | private string currentLabel = string.Empty; 26 | private ICharacter? player; 27 | private bool modifiable = false; 28 | private int? objIdxSelected; 29 | private ICharacter? playerSelected; 30 | private readonly Dictionary playerNames = new(100); 31 | private readonly Dictionary gPoseActors = new(CharacterScreenIndex - GPoseObjectId); 32 | 33 | private IPluginLog PluginLog { get; init; } 34 | private void DrawPlayerFilter() 35 | { 36 | using var raii = new ImGuiRaii() 37 | .PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) 38 | .PushStyle(ImGuiStyleVar.FrameRounding, 0); 39 | ImGui.SetNextItemWidth(SelectorWidth * ImGui.GetIO().FontGlobalScale); 40 | if (ImGui.InputTextWithHint("##playerFilter", "Filter Players...", ref playerFilter, 32)) 41 | playerFilterLower = playerFilter.ToLowerInvariant(); 42 | } 43 | 44 | private void DrawGPoseSelectable(ICharacter player, int objIdx) 45 | { 46 | var playerName = player.Name.ToString(); 47 | if (!playerName.Any()) 48 | return; 49 | 50 | gPoseActors[playerName] = null; 51 | 52 | DrawSelectable(player, $"{playerName} (GPose)", true, objIdx); 53 | } 54 | 55 | private void DrawSelectable(ICharacter player, string label, bool modifiable, int objIdx) 56 | { 57 | if (!playerFilterLower.Any() || label.ToLowerInvariant().Contains(playerFilterLower)) 58 | if (ImGui.Selectable(label, currentLabel == label)) 59 | { 60 | currentLabel = label; 61 | this.player = player; 62 | this.objIdxSelected = objIdx; 63 | this.modifiable = modifiable; 64 | return; 65 | } 66 | 67 | if (currentLabel != label) 68 | return; 69 | 70 | try 71 | { 72 | this.player = player; 73 | this.objIdxSelected = objIdx; 74 | this.modifiable = modifiable; 75 | } 76 | catch (Exception e) 77 | { 78 | PluginLog.Error($"Could not load character {player.Name}s information:\n{e}"); 79 | } 80 | } 81 | 82 | private void DrawPlayerSelectable(ICharacter player, int idx) 83 | { 84 | string playerName; 85 | bool modifiable = false; 86 | 87 | if(idx == CharacterScreenIndex) 88 | { 89 | playerName = "Character Screen Actor"; 90 | } 91 | else if(idx == ExamineScreenIndex) 92 | { 93 | playerName = "Examine Screen Actor"; 94 | } 95 | else if(idx == FittingRoomIndex) 96 | { 97 | playerName = "Fitting Room Actor"; 98 | } 99 | else if(idx == DyePreviewIndex) 100 | { 101 | playerName = "Dye Preview Actor"; 102 | } 103 | else 104 | { 105 | playerName = player.Name.ToString(); 106 | } 107 | 108 | 109 | if (!playerName.Any()) 110 | return; 111 | 112 | if (playerNames.TryGetValue(playerName, out var num)) 113 | playerNames[playerName] = ++num; 114 | else 115 | playerNames[playerName] = num = 1; 116 | 117 | if (gPoseActors.ContainsKey(playerName)) 118 | { 119 | gPoseActors[playerName] = player; 120 | return; 121 | } 122 | 123 | var label = GetLabel(player, playerName, num); 124 | DrawSelectable(player, label, modifiable, idx); 125 | } 126 | 127 | private static string GetLabel(ICharacter player, string playerName, int num) 128 | { 129 | if (player.ObjectKind == ObjectKind.Player) 130 | return num == 1 ? playerName : $"{playerName} #{num}"; 131 | 132 | if (player.ObjectKind == ObjectKind.EventNpc) 133 | return num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)"; 134 | 135 | return num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)"; 136 | } 137 | 138 | private void DrawPlayerSelector() 139 | { 140 | ImGui.BeginGroup(); 141 | DrawPlayerFilter(); 142 | if (!ImGui.BeginChild("##playerSelector", 143 | new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true)) 144 | { 145 | ImGui.EndChild(); 146 | ImGui.EndGroup(); 147 | return; 148 | } 149 | 150 | playerNames.Clear(); 151 | gPoseActors.Clear(); 152 | 153 | for (var i = GPoseObjectId; i < GPoseObjectId + 48; ++i) 154 | { 155 | var player = CharacterFactory.Convert(Plugin.Objects[i]); 156 | 157 | if (player == null) 158 | continue; 159 | 160 | DrawGPoseSelectable(player, i); 161 | } 162 | 163 | for (var i = 0; i < GPoseObjectId; ++i) 164 | { 165 | var player = CharacterFactory.Convert(Plugin.Objects[i]); 166 | if (player != null) 167 | DrawPlayerSelectable(player, i); 168 | } 169 | 170 | for (var i = CharacterScreenIndex; i < Plugin.Objects.Length; ++i) 171 | { 172 | var player = CharacterFactory.Convert(Plugin.Objects[i]); 173 | if (player != null) 174 | DrawPlayerSelectable(player, i); 175 | } 176 | ImGui.EndChild(); 177 | 178 | 179 | //DrawSelectionButtons(); 180 | ImGui.EndGroup(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | # top-most EditorConfig file 3 | 4 | [*] 5 | charset = utf-8 6 | 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | # 4 space indentation 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # disable redundant style warnings 15 | 16 | # Microsoft .NET properties 17 | csharp_indent_braces = false 18 | csharp_new_line_before_catch = true 19 | csharp_new_line_before_else = true 20 | csharp_new_line_before_finally = true 21 | csharp_new_line_before_members_in_object_initializers = false 22 | csharp_new_line_before_open_brace = all 23 | csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion 24 | csharp_style_var_elsewhere = true:suggestion 25 | csharp_style_var_for_built_in_types = true:suggestion 26 | csharp_style_var_when_type_is_apparent = true:suggestion 27 | dotnet_code_quality_unused_parameters = non_public 28 | dotnet_naming_rule.event_rule.severity = warning 29 | dotnet_naming_rule.event_rule.style = on_upper_camel_case_style 30 | dotnet_naming_rule.event_rule.symbols = event_symbols 31 | dotnet_naming_rule.private_constants_rule.severity = warning 32 | dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style 33 | dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols 34 | dotnet_naming_rule.private_instance_fields_rule.severity = warning 35 | dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style 36 | dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols 37 | dotnet_naming_rule.private_static_fields_rule.severity = warning 38 | dotnet_naming_rule.private_static_fields_rule.style = upper_camel_case_style 39 | dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols 40 | dotnet_naming_rule.private_static_readonly_rule.severity = warning 41 | dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style 42 | dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols 43 | dotnet_naming_style.lower_camel_case_style.capitalization = camel_case 44 | dotnet_naming_style.on_upper_camel_case_style.capitalization = pascal_case 45 | dotnet_naming_style.on_upper_camel_case_style.required_prefix = On 46 | dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case 47 | dotnet_naming_symbols.event_symbols.applicable_accessibilities = * 48 | dotnet_naming_symbols.event_symbols.applicable_kinds = event 49 | dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private 50 | dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field 51 | dotnet_naming_symbols.private_constants_symbols.required_modifiers = const 52 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private 53 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field 54 | dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private 55 | dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field 56 | dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static 57 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private 58 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field 59 | dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly 60 | dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion 61 | dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion 62 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion 63 | dotnet_style_predefined_type_for_member_access = true:suggestion 64 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 65 | dotnet_style_parentheses_in_other_operators=always_for_clarity:silent 66 | dotnet_style_object_initializer = false 67 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 68 | csharp_space_between_method_call_parameter_list_parentheses = false 69 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 70 | csharp_space_between_empty_square_brackets = false 71 | csharp_space_before_semicolon_in_for_statement = false 72 | csharp_space_before_open_square_brackets = false 73 | csharp_space_before_comma = false 74 | csharp_space_after_keywords_in_control_flow_statements = true 75 | csharp_space_after_comma = true 76 | csharp_space_after_cast = false 77 | csharp_space_around_binary_operators = before_and_after 78 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 79 | csharp_space_between_method_declaration_parameter_list_parentheses = false 80 | csharp_space_between_parentheses = none 81 | csharp_space_between_square_brackets = false 82 | 83 | # ReSharper properties 84 | resharper_align_linq_query = true 85 | resharper_align_multiline_argument = true 86 | resharper_align_multiline_calls_chain = true 87 | resharper_align_multiline_expression = true 88 | resharper_align_multiline_extends_list = true 89 | resharper_align_multiline_for_stmt = true 90 | resharper_align_multline_type_parameter_constrains = true 91 | resharper_align_multline_type_parameter_list = true 92 | resharper_apply_on_completion = true 93 | resharper_auto_property_can_be_made_get_only_global_highlighting = none 94 | resharper_auto_property_can_be_made_get_only_local_highlighting = none 95 | resharper_autodetect_indent_settings = true 96 | resharper_braces_for_ifelse = required_for_multiline 97 | resharper_can_use_global_alias = false 98 | resharper_csharp_align_multiline_parameter = true 99 | resharper_csharp_align_multiple_declaration = true 100 | resharper_csharp_empty_block_style = together_same_line 101 | resharper_csharp_int_align_comments = true 102 | resharper_csharp_new_line_before_while = true 103 | resharper_csharp_wrap_after_declaration_lpar = true 104 | resharper_enforce_line_ending_style = true 105 | resharper_member_can_be_private_global_highlighting = none 106 | resharper_member_can_be_private_local_highlighting = none 107 | resharper_new_line_before_finally = false 108 | resharper_place_accessorholder_attribute_on_same_line = false 109 | resharper_place_field_attribute_on_same_line = false 110 | resharper_show_autodetect_configure_formatting_tip = false 111 | resharper_use_indent_from_vs = false 112 | 113 | # ReSharper inspection severities 114 | resharper_arrange_missing_parentheses_highlighting = hint 115 | resharper_arrange_redundant_parentheses_highlighting = hint 116 | resharper_arrange_this_qualifier_highlighting = none 117 | resharper_arrange_type_member_modifiers_highlighting = hint 118 | resharper_arrange_type_modifiers_highlighting = hint 119 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint 120 | resharper_built_in_type_reference_style_highlighting = none 121 | resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting = none 122 | resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting = none 123 | resharper_invert_if_highlighting = none 124 | resharper_loop_can_be_converted_to_query_highlighting = none 125 | resharper_method_has_async_overload_highlighting = none 126 | resharper_private_field_can_be_converted_to_local_variable_highlighting = none 127 | resharper_redundant_base_qualifier_highlighting = none 128 | resharper_suggest_var_or_type_built_in_types_highlighting = hint 129 | resharper_suggest_var_or_type_elsewhere_highlighting = hint 130 | resharper_suggest_var_or_type_simple_types_highlighting = hint 131 | resharper_unused_auto_property_accessor_global_highlighting = none 132 | csharp_style_deconstructed_variable_declaration=true:silent 133 | 134 | [*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] 135 | indent_style = space 136 | indent_size = 4 137 | tab_width = 4 138 | dotnet_style_parentheses_in_other_operators=always_for_clarity:silent 139 | -------------------------------------------------------------------------------- /Snapper/Utils/DalamudUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Dalamud.Game.ClientState; 7 | using Dalamud.Game.ClientState.Conditions; 8 | using Dalamud.Game.ClientState.Objects; 9 | using Dalamud.Game.ClientState.Objects.SubKinds; 10 | using Dalamud.Game.Gui; 11 | using Dalamud.Game.Text.SeStringHandling; 12 | using Dalamud.Plugin.Services; 13 | using FFXIVClientStructs.FFXIV.Client.Game.Character; 14 | using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; 15 | 16 | namespace Snapper.Utils; 17 | 18 | public delegate void PlayerChange(Dalamud.Game.ClientState.Objects.Types.ICharacter actor); 19 | 20 | public delegate void LogIn(); 21 | public delegate void LogOut(); 22 | public delegate void ClassJobChanged(); 23 | 24 | public delegate void FrameworkUpdate(); 25 | public delegate void VoidDelegate(); 26 | 27 | public class DalamudUtil : IDisposable 28 | { 29 | private readonly IClientState _clientState; 30 | private readonly IObjectTable _objectTable; 31 | private readonly IFramework _framework; 32 | private readonly ICondition _condition; 33 | private readonly IChatGui _chatGui; 34 | 35 | public event LogIn? LogIn; 36 | public event LogOut? LogOut; 37 | public event FrameworkUpdate? FrameworkUpdate; 38 | public event FrameworkUpdate? DelayedFrameworkUpdate; 39 | public event VoidDelegate? ZoneSwitchStart; 40 | public event VoidDelegate? ZoneSwitchEnd; 41 | private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; 42 | private bool _sentBetweenAreas = false; 43 | 44 | public unsafe bool IsGameObjectPresent(IntPtr key) 45 | { 46 | foreach (var obj in _objectTable) 47 | { 48 | if (obj.Address == key) 49 | { 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | 57 | public DalamudUtil(IClientState clientState, IObjectTable objectTable, IFramework framework, ICondition condition, IChatGui chatGui) 58 | { 59 | _clientState = clientState; 60 | _objectTable = objectTable; 61 | _framework = framework; 62 | _condition = condition; 63 | _chatGui = chatGui; 64 | _clientState.Login += OnLogin; 65 | //_clientState.Logout += OnLogout; 66 | _framework.Update += FrameworkOnUpdate; 67 | if (IsLoggedIn) 68 | { 69 | OnLogin(); 70 | } 71 | } 72 | 73 | public void PrintInfoChat(string message) 74 | { 75 | SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Info: ").AddItalics(message); 76 | _chatGui.Print(se.BuiltString); 77 | } 78 | 79 | public void PrintWarnChat(string message) 80 | { 81 | SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] ").AddUiForeground("Warning: " + message, 31).AddUiForegroundOff(); 82 | _chatGui.Print(se.BuiltString); 83 | } 84 | 85 | public void PrintErrorChat(string message) 86 | { 87 | SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] ").AddUiForeground("Error: ", 534).AddItalicsOn().AddUiForeground(message, 534).AddUiForegroundOff().AddItalicsOff(); 88 | _chatGui.Print(se.BuiltString); 89 | } 90 | 91 | private void FrameworkOnUpdate(IFramework framework) 92 | { 93 | if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || IsInGpose) 94 | { 95 | if (!_sentBetweenAreas) 96 | { 97 | Logger.Debug("Zone switch/Gpose start"); 98 | _sentBetweenAreas = true; 99 | ZoneSwitchStart?.Invoke(); 100 | } 101 | 102 | return; 103 | } 104 | else if (_sentBetweenAreas) 105 | { 106 | Logger.Debug("Zone switch/Gpose end"); 107 | _sentBetweenAreas = false; 108 | ZoneSwitchEnd?.Invoke(); 109 | } 110 | 111 | foreach (FrameworkUpdate? frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) 112 | { 113 | try 114 | { 115 | frameworkInvocation?.Invoke(); 116 | } 117 | catch (Exception ex) 118 | { 119 | Logger.Warn(ex.Message); 120 | Logger.Warn(ex.StackTrace ?? string.Empty); 121 | } 122 | } 123 | 124 | if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; 125 | 126 | foreach (FrameworkUpdate? frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) 127 | { 128 | try 129 | { 130 | frameworkInvocation?.Invoke(); 131 | } 132 | catch (Exception ex) 133 | { 134 | Logger.Warn(ex.Message); 135 | Logger.Warn(ex.StackTrace ?? string.Empty); 136 | } 137 | } 138 | _delayedFrameworkUpdateCheck = DateTime.Now; 139 | } 140 | 141 | private void OnLogout() 142 | { 143 | LogOut?.Invoke(); 144 | } 145 | 146 | private void OnLogin() 147 | { 148 | LogIn?.Invoke(); 149 | } 150 | 151 | public Dalamud.Game.ClientState.Objects.Types.IGameObject? CreateGameObject(IntPtr reference) 152 | { 153 | return _objectTable.CreateObjectReference(reference); 154 | } 155 | 156 | public bool IsLoggedIn => _clientState.IsLoggedIn; 157 | 158 | public bool IsPlayerPresent => _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); 159 | 160 | public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.IGameObject? obj) 161 | { 162 | return obj != null && obj.IsValid(); 163 | } 164 | 165 | public unsafe IntPtr GetMinion() 166 | { 167 | return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)PlayerPointer)->CompanionObject; 168 | } 169 | 170 | public unsafe IntPtr GetPet(IntPtr? playerPointer = null) 171 | { 172 | var mgr = CharacterManager.Instance(); 173 | if (playerPointer == null) playerPointer = PlayerPointer; 174 | return (IntPtr)mgr->LookupPetByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); 175 | } 176 | 177 | public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) 178 | { 179 | var mgr = CharacterManager.Instance(); 180 | if (playerPointer == null) playerPointer = PlayerPointer; 181 | return (IntPtr)mgr->LookupBuddyByOwnerObject((FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)playerPointer); 182 | } 183 | 184 | public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; 185 | 186 | public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; 187 | 188 | public IPlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; 189 | 190 | public bool IsInGpose => _objectTable[201] != null; 191 | 192 | public List GetPlayerCharacters() 193 | { 194 | return _objectTable.Where(obj => 195 | obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player && 196 | !string.Equals(obj.Name.ToString(), PlayerName, StringComparison.Ordinal)).Select(p => (IPlayerCharacter)p).ToList(); 197 | } 198 | 199 | public Dalamud.Game.ClientState.Objects.Types.ICharacter? GetCharacterFromObjectTableByIndex(int index) 200 | { 201 | var objTableObj = _objectTable[index]; 202 | if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; 203 | return (Dalamud.Game.ClientState.Objects.Types.ICharacter)objTableObj; 204 | } 205 | 206 | public IPlayerCharacter? GetPlayerCharacterFromObjectTableByName(string characterName) 207 | { 208 | foreach (var item in _objectTable) 209 | { 210 | if (item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; 211 | if (string.Equals(item.Name.ToString(), characterName, StringComparison.Ordinal)) return (IPlayerCharacter)item; 212 | } 213 | 214 | return null; 215 | } 216 | 217 | public int? GetIndexFromObjectTableByName(string characterName) 218 | { 219 | for (int i = 0; i < _objectTable.Length; i++) 220 | { 221 | if (_objectTable[i] == null) continue; 222 | if (_objectTable[i]!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; 223 | if (string.Equals(_objectTable[i]!.Name.ToString(), characterName, StringComparison.Ordinal)) return i; 224 | } 225 | 226 | return null; 227 | } 228 | 229 | public async Task RunOnFrameworkThread(Func func) 230 | { 231 | return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false); 232 | } 233 | 234 | public unsafe void WaitWhileCharacterIsDrawing(string name, IntPtr characterAddress, int timeOut = 5000, CancellationToken? ct = null) 235 | { 236 | if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; 237 | 238 | var obj = (GameObject*)characterAddress; 239 | const int tick = 250; 240 | int curWaitTime = 0; 241 | // ReSharper disable once LoopVariableIsNeverChangedInsideLoop 242 | while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something 243 | { 244 | Logger.Verbose($"Waiting for {name} to finish drawing"); 245 | curWaitTime += tick; 246 | Thread.Sleep(tick); 247 | } 248 | 249 | if (ct?.IsCancellationRequested ?? false) return; 250 | // wait quarter a second just in case 251 | Thread.Sleep(tick); 252 | } 253 | 254 | public void Dispose() 255 | { 256 | _clientState.Login -= OnLogin; 257 | //_clientState.Logout -= OnLogout; 258 | _framework.Update -= FrameworkOnUpdate; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Snapper/Managers/IpcManager.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Plugin; 2 | using Dalamud.Plugin.Ipc; 3 | using System; 4 | using System.Collections.Generic; 5 | using Dalamud.Game.ClientState.Objects.Types; 6 | using Action = System.Action; 7 | using System.Collections.Concurrent; 8 | using System.Text; 9 | using Penumbra.Api.Enums; 10 | using Penumbra.Api.Helpers; 11 | using Snapper.Utils; 12 | using System.Security; 13 | using Glamourer.Api.Helpers; 14 | using Penumbra.Api.IpcSubscribers; 15 | using System.Linq; 16 | //using Penumbra.Api.IpcSubscribers.Legacy; 17 | //using Glamourer.Api.IpcSubscribers.Legacy; 18 | using Microsoft.Extensions.Logging; 19 | using Glamourer.Api.IpcSubscribers; 20 | using Glamourer.Api.Enums; 21 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 22 | using Dalamud.Utility; 23 | 24 | namespace Snapper.Managers; 25 | 26 | public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); 27 | public delegate void HeelsOffsetChange(float change); 28 | public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); 29 | public delegate void CustomizePlusScaleChange(string? scale); 30 | 31 | 32 | 33 | 34 | public class IpcManager : IDisposable 35 | { 36 | private readonly IDalamudPluginInterface _pi; 37 | private const string TempCollectionPrefix = "Snap_"; 38 | 39 | private readonly Glamourer.Api.IpcSubscribers.ApiVersion _glamourerApiVersion; 40 | private readonly ApplyState _glamourerApplyAll; 41 | private readonly GetStateBase64 _glamourerGetAllCustomization; 42 | private readonly GetStateBase64Name _glamourerGetAllCustomizationName; 43 | private readonly RevertState _glamourerRevertCustomization; 44 | 45 | private readonly Penumbra.Api.Helpers.EventSubscriber _penumbraInit; 46 | private readonly Penumbra.Api.Helpers.EventSubscriber _penumbraDispose; 47 | private readonly Penumbra.Api.Helpers.EventSubscriber _penumbraObjectIsRedrawn; 48 | private readonly Penumbra.Api.Helpers.EventSubscriber _penumbraGameObjectResourcePathResolved; 49 | private readonly Penumbra.Api.Helpers.EventSubscriber _penumbraModSettingChanged; 50 | 51 | private readonly GetModDirectory _penumbraResolveModDir; 52 | private readonly ResolvePlayerPath _penumbraResolvePlayer; 53 | private readonly ResolveGameObjectPath _penumbraResolvePlayerObject; 54 | private readonly ReverseResolveGameObjectPath _penumbraReverseResolvePlayerObject; 55 | private readonly GetEnabledState _penumbraEnabled; 56 | private readonly RedrawObject _penumbraRedraw; 57 | private readonly Penumbra.Api.IpcSubscribers.Legacy.RedrawObject _penumbraRedrawObject; 58 | private readonly GetMetaManipulations _penumbraGetGameObjectMetaManipulations; 59 | private readonly AddTemporaryMod _penumbraAddTemporaryMod; 60 | private readonly CreateTemporaryCollection _penumbraCreateTemporaryCollection; 61 | //private readonly RemoveTemporaryCollection _penumbraRemoveTemporaryCollection; 62 | private readonly Penumbra.Api.IpcSubscribers.Legacy.RemoveTemporaryMod _penumbraRemoveTemporaryMod; 63 | private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection; 64 | private readonly ReverseResolvePlayerPath _reverseResolvePlayer; 65 | 66 | private readonly ICallGateSubscriber _customizePlusApiVersion; 67 | private readonly ICallGateSubscriber _customizePlusBranch; 68 | private readonly ICallGateSubscriber _customizePlusGetBodyScale; 69 | private readonly ICallGateSubscriber _customizePlusGetBodyScaleFromCharacter; 70 | private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; 71 | private readonly ICallGateSubscriber _customizePlusRevert; 72 | private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; 73 | 74 | private readonly DalamudUtil _dalamudUtil; 75 | private readonly ConcurrentQueue actionQueue = new(); 76 | 77 | private Configuration _configuration; 78 | private string backupBase64 = "Bh+LCAAAAAAAAArEV9ly2jAU/Rc/Mxkv8pa3hoRAJ6QZoMmzAhfQRNiuJSdDM/n3XsmYeMNl2nr6Joujc+7mY/FujBiHR0gFiyPj0hoYNz8yluwgksbluzGlLBrTaKXWEwm7Ca4cYrr2wBimIBCzplzAwPiSJHxvXMo0Kx7mEs9WdqonDr+bh5WNy4+B8W29rusROySh54WW9Rei+U675hhoi6BveaQnwat4tS8L2mHg95UbFlNUuufapK/m3cGmquWYZl9aIwBZ0fLd3vK6oaloGxA36EvxHpYvrYp+X4pPKROyPUuvL83ZiEUbSFtF3d6G9H+Ijqke1vk2fquczR8Q8MhEnMckFvFmw2HVjnsCmiijLriacX5og4ky3ctbToUAvdR7KluzAR9mQsY79hO05ccr4AfcjC713iPlGS6c40ktiydvIVrltTxAauS51y32SZnGqmPGwDZbWUYo36hihpxGJYRb/31UDbWpQVkq5J53gzAOrmIRZZQd1HHzFxYNY553rIApD6/CbvagUbN6ek7QFl6d0fNPx9dQJy0FYZSPgMosBaurRRUkDuyZSOdsJDkb6Z6N9M5G+l1I/GTR5X5BpYzjLlyOqFfdbOn4cxq/Vcbn1FTcwfo3Q4HI+ZYm7a9gMYo7yvkEzbsrrPtYdM79V/rWdXwaZ3LbWUWWCMnyT9bptwZRjbH1m2KZWHKYom9194Pxem0ar8AV+tr8YGsdowIPaNTlVpwGzeAVr8rKnM8AN5JtluQJZASVTKuGnvssAh9oSncgUVxhjxLfX6cZlyzhrOLC1oXZsOrSGbxki/z2VOTQgs/7sIgjXb4HSJf4n4BuTrCrYb5ju2fKJ5GESDC5rx9rE9HO+AfnlAFfs/U6y+d6pjpiXniBjdfbEO8rtymA+gpfuIHphDb+Xbk6JOqaodpyG5TKgZuUvuPbLglLjJ7pB8SxvE9KojlJK+PRsUucrmmFnhmUOImOm5TCdCzfI77ptxYafaFE52hkOWmHWOp48EnntVf+XxAdXPY47EXhGmTHnYLruFFPkSXNRhCsOfaiTGjnncDCF4xYwC882VK9PtGVa1hSXo82tE1H3eaP3MVGwVw8H+ktPVhqDOqv6pTia4qfH/Wmfnz8AgAA//8="; 79 | 80 | public IpcManager(IDalamudPluginInterface pi, DalamudUtil dalamudUtil) 81 | { 82 | Logger.Verbose("Creating " + nameof(IpcManager)); 83 | _configuration = (Configuration?)pi.GetPluginConfig(); 84 | _pi = pi; 85 | 86 | _penumbraInit = Penumbra.Api.IpcSubscribers.Initialized.Subscriber(pi, () => PenumbraInit()); 87 | _penumbraDispose = Penumbra.Api.IpcSubscribers.Disposed.Subscriber(pi, () => PenumbraDispose()); 88 | _penumbraResolvePlayer = new ResolvePlayerPath(pi); 89 | _penumbraResolvePlayerObject = new ResolveGameObjectPath(pi); 90 | _penumbraReverseResolvePlayerObject = new ReverseResolveGameObjectPath(pi); 91 | _penumbraResolveModDir = new GetModDirectory(pi); 92 | _penumbraRedraw = new RedrawObject(pi); 93 | _penumbraRedrawObject = new Penumbra.Api.IpcSubscribers.Legacy.RedrawObject(pi); 94 | _reverseResolvePlayer = new ReverseResolvePlayerPath(pi); 95 | _penumbraObjectIsRedrawn = Penumbra.Api.IpcSubscribers.GameObjectRedrawn.Subscriber(pi, (ptr, idx) => RedrawEvent((IntPtr)ptr, idx)); 96 | _penumbraGetGameObjectMetaManipulations = new GetMetaManipulations(pi); 97 | _penumbraAddTemporaryMod = new AddTemporaryMod(pi); 98 | _penumbraCreateTemporaryCollection = new CreateTemporaryCollection(pi); 99 | //_penumbraRemoveTemporaryCollection = new RemoveTemporaryCollectionByName(pi); 100 | _penumbraRemoveTemporaryMod = new Penumbra.Api.IpcSubscribers.Legacy.RemoveTemporaryMod(pi); 101 | _penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi); 102 | _penumbraEnabled = new GetEnabledState(pi); 103 | 104 | _penumbraGameObjectResourcePathResolved = Penumbra.Api.IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, (ptr, arg1, arg2) => ResourceLoaded((IntPtr)ptr, arg1, arg2)); 105 | _penumbraModSettingChanged = Penumbra.Api.IpcSubscribers.ModSettingChanged.Subscriber(pi, (modsetting, a, b, c) => PenumbraModSettingChangedHandler()); 106 | 107 | _glamourerApiVersion = new Glamourer.Api.IpcSubscribers.ApiVersion(pi); 108 | _glamourerGetAllCustomization = new GetStateBase64(pi); 109 | _glamourerGetAllCustomizationName = new GetStateBase64Name(pi); 110 | 111 | _glamourerApplyAll = new ApplyState(pi); 112 | _glamourerRevertCustomization = new RevertState(pi); 113 | 114 | 115 | _customizePlusApiVersion = pi.GetIpcSubscriber("CustomizePlus.GetApiVersion"); 116 | _customizePlusBranch = pi.GetIpcSubscriber("CustomizePlus.GetBranch"); 117 | _customizePlusGetBodyScale = pi.GetIpcSubscriber("CustomizePlus.GetTemporaryScale"); 118 | _customizePlusGetBodyScaleFromCharacter = pi.GetIpcSubscriber("CustomizePlus.GetBodyScaleFromCharacter"); 119 | _customizePlusRevert = pi.GetIpcSubscriber("CustomizePlus.RevertCharacter"); 120 | _customizePlusSetBodyScaleToCharacter = pi.GetIpcSubscriber("CustomizePlus.SetBodyScaleToCharacter"); 121 | _customizePlusOnScaleUpdate = pi.GetIpcSubscriber("CustomizePlus.OnScaleUpdate"); 122 | 123 | _customizePlusOnScaleUpdate.Subscribe(OnCustomizePlusScaleChange); 124 | 125 | if (Initialized) 126 | { 127 | PenumbraInitialized?.Invoke(); 128 | } 129 | 130 | _dalamudUtil = dalamudUtil; 131 | _dalamudUtil.FrameworkUpdate += HandleActionQueue; 132 | _dalamudUtil.ZoneSwitchEnd += ClearActionQueue; 133 | 134 | } 135 | 136 | private void PenumbraModSettingChangedHandler() 137 | { 138 | PenumbraModSettingChanged?.Invoke(); 139 | } 140 | 141 | private void ClearActionQueue() 142 | { 143 | actionQueue.Clear(); 144 | } 145 | 146 | private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) 147 | { 148 | if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) 149 | { 150 | PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); 151 | } 152 | } 153 | 154 | private void HandleActionQueue() 155 | { 156 | if (actionQueue.TryDequeue(out var action)) 157 | { 158 | if (action == null) return; 159 | Logger.Debug("Execution action in queue: " + action.Method); 160 | action(); 161 | } 162 | } 163 | 164 | public event VoidDelegate? PenumbraModSettingChanged; 165 | public event VoidDelegate? PenumbraInitialized; 166 | public event VoidDelegate? PenumbraDisposed; 167 | public event PenumbraRedrawEvent? PenumbraRedrawEvent; 168 | public event HeelsOffsetChange? HeelsOffsetChangeEvent; 169 | public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; 170 | public event CustomizePlusScaleChange? CustomizePlusScaleChange; 171 | 172 | public bool Initialized => CheckPenumbraApi(); 173 | public bool CheckGlamourerApi() 174 | { 175 | try 176 | { 177 | return _glamourerApiVersion.Invoke() is { Major: 1, Minor: >= 1 }; 178 | } 179 | catch 180 | { 181 | Logger.Warn("Glamourer API was not available"); 182 | return false; 183 | } 184 | } 185 | 186 | public bool CheckPenumbraApi() 187 | { 188 | bool penumbraAvailable = false; 189 | try 190 | { 191 | var penumbraVersion = (_pi.InstalledPlugins 192 | .FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase)) 193 | ?.Version ?? new Version(0, 0, 0, 0)); 194 | penumbraAvailable = penumbraVersion >= new Version(1, 1, 0, 0); 195 | penumbraAvailable &= _penumbraEnabled.Invoke(); 196 | return penumbraAvailable; 197 | } 198 | catch 199 | { 200 | Logger.Warn("Penumbra API was not available"); 201 | return false; 202 | } 203 | } 204 | 205 | public bool CheckCustomizePlusApi() 206 | { 207 | try 208 | { 209 | return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal) && string.Equals(_customizePlusBranch.InvokeFunc(), "eqbot", StringComparison.Ordinal); 210 | } 211 | catch 212 | { 213 | return false; 214 | } 215 | } 216 | public bool CheckCustomizePlusBranch() 217 | { 218 | try 219 | { 220 | return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal); 221 | } 222 | catch 223 | { 224 | return false; 225 | } 226 | } 227 | 228 | public void Dispose() 229 | { 230 | Logger.Verbose("Disposing " + nameof(IpcManager)); 231 | 232 | int totalSleepTime = 0; 233 | while (actionQueue.Count > 0 && totalSleepTime < 2000) 234 | { 235 | Logger.Verbose("Waiting for actionqueue to clear..."); 236 | HandleActionQueue(); 237 | System.Threading.Thread.Sleep(16); 238 | totalSleepTime += 16; 239 | } 240 | 241 | if (totalSleepTime >= 2000) 242 | { 243 | Logger.Verbose("Action queue clear or not, disposing"); 244 | } 245 | 246 | _dalamudUtil.FrameworkUpdate -= HandleActionQueue; 247 | _dalamudUtil.ZoneSwitchEnd -= ClearActionQueue; 248 | actionQueue.Clear(); 249 | 250 | _penumbraGameObjectResourcePathResolved.Dispose(); 251 | _penumbraDispose.Dispose(); 252 | _penumbraInit.Dispose(); 253 | _penumbraObjectIsRedrawn.Dispose(); 254 | _penumbraModSettingChanged.Dispose(); 255 | } 256 | 257 | public string GetCustomizePlusScale() 258 | { 259 | if (!CheckCustomizePlusApi()) return string.Empty; 260 | var scale = _customizePlusGetBodyScale.InvokeFunc(_dalamudUtil.PlayerName); 261 | if (string.IsNullOrEmpty(scale)) return string.Empty; 262 | return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); 263 | } 264 | 265 | public string GetCustomizePlusScaleFromCharacter(ICharacter character) 266 | { 267 | if (!CheckCustomizePlusApi()) return string.Empty; 268 | var scale = _customizePlusGetBodyScale.InvokeFunc(character.Name.ToString()); 269 | if (string.IsNullOrEmpty(scale)) 270 | { 271 | Logger.Debug("C+ returned null"); 272 | return string.Empty; 273 | } 274 | return scale; 275 | } 276 | 277 | public void CustomizePlusSetBodyScale(IntPtr character, string scale) 278 | { 279 | if (!CheckCustomizePlusApi() || string.IsNullOrEmpty(scale)) return; 280 | actionQueue.Enqueue(() => 281 | { 282 | var gameObj = _dalamudUtil.CreateGameObject(character); 283 | if (gameObj is ICharacter c) 284 | { 285 | Logger.Verbose("CustomizePlus applying for " + c.Address.ToString("X")); 286 | _customizePlusSetBodyScaleToCharacter!.InvokeAction(scale, c); 287 | } 288 | }); 289 | } 290 | 291 | public void CustomizePlusRevert(IntPtr character) 292 | { 293 | if (!CheckCustomizePlusApi()) return; 294 | actionQueue.Enqueue(() => 295 | { 296 | var gameObj = _dalamudUtil.CreateGameObject(character); 297 | if (gameObj is ICharacter c) 298 | { 299 | Logger.Verbose("CustomizePlus reverting for " + c.Address.ToString("X")); 300 | _customizePlusRevert!.InvokeAction(c); 301 | } 302 | }); 303 | } 304 | 305 | public void GlamourerApplyAll(string? customization, ICharacter obj) 306 | { 307 | if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; 308 | Logger.Verbose("Glamourer applying for " + obj.Address.ToString("X")); 309 | _glamourerApplyAll!.Invoke(customization, obj.ObjectIndex); 310 | } 311 | 312 | public string GlamourerGetCharacterCustomization(IntPtr character) 313 | { 314 | object temp = ""; 315 | object tempGameObj = ""; 316 | Logger.Debug("Getting character customization"); 317 | if (!CheckGlamourerApi()) return string.Empty; 318 | try 319 | { 320 | var gameObj = _dalamudUtil.CreateGameObject(character); 321 | if (gameObj is ICharacter c) 322 | { 323 | Logger.Debug($"Attempting to get customizations for {c.Name} with ObjectIndex {c.ObjectIndex}"); 324 | (GlamourerApiEc apiec, string glamourerString) = _glamourerGetAllCustomization!.Invoke(c.ObjectIndex); 325 | temp = glamourerString; 326 | tempGameObj = c.Name; 327 | Logger.Debug($"Got glamourer customizations {glamourerString} for {c.Name}"); 328 | if (glamourerString.IsNullOrEmpty()) 329 | { 330 | glamourerString = _configuration.FallBackGlamourerString; 331 | } 332 | 333 | byte[] bytes; 334 | 335 | try 336 | { 337 | bytes = Convert.FromBase64String(glamourerString); 338 | } 339 | catch 340 | { 341 | //if your backup string is not valid, you are getting my shitty lala. 342 | bytes = Convert.FromBase64String(backupBase64); 343 | } 344 | 345 | return Convert.ToBase64String(bytes); 346 | } 347 | Logger.Warn("Game object is not an ICharacter or could not retrieve customization data."); 348 | return string.Empty; 349 | } 350 | catch (Exception ex) 351 | { 352 | Logger.Error($"Error occurred while getting customizations for {tempGameObj}. GlamourerString = '{temp}', Exception: {ex.Message}"); 353 | throw; 354 | return string.Empty; 355 | } 356 | } 357 | 358 | public void GlamourerRevertCharacterCustomization(IGameObject character) 359 | { 360 | if (!CheckGlamourerApi()) return; 361 | if(character is ICharacter c) 362 | { 363 | actionQueue.Enqueue(() => _glamourerRevertCustomization!.Invoke(c.ObjectIndex)); 364 | } 365 | else 366 | { 367 | Logger.Error("Tried to revert a non-Character game object and failed"); 368 | } 369 | } 370 | 371 | public string PenumbraGetGameObjectMetaManipulations(int objIdx) 372 | { 373 | if (!CheckPenumbraApi()) return string.Empty; 374 | return _penumbraGetGameObjectMetaManipulations.Invoke(objIdx); 375 | } 376 | 377 | public void PenumbraRedraw(IntPtr obj) 378 | { 379 | if (!CheckPenumbraApi()) return; 380 | actionQueue.Enqueue(() => 381 | { 382 | var gameObj = _dalamudUtil.CreateGameObject(obj); 383 | if (gameObj != null) 384 | { 385 | Logger.Verbose("Redrawing " + gameObj.ToString()); 386 | _penumbraRedrawObject!.Invoke(gameObj, RedrawType.Redraw); 387 | } 388 | }); 389 | } 390 | 391 | public void PenumbraRedraw(int objIdx) 392 | { 393 | if (!CheckPenumbraApi()) return; 394 | _penumbraRedraw!.Invoke(objIdx, RedrawType.Redraw); 395 | } 396 | 397 | public void PenumbraRemoveTemporaryCollection(string characterName) 398 | { 399 | if (!CheckPenumbraApi()) return; 400 | actionQueue.Enqueue(() => 401 | { 402 | var collName = TempCollectionPrefix + characterName; 403 | Logger.Verbose("Removing temp collection for " + collName); 404 | var ret = _penumbraRemoveTemporaryMod.Invoke("Snap", collName, 0); 405 | Logger.Verbose("RemoveTemporaryMod: " + ret); 406 | //var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collName); 407 | //Logger.Verbose("RemoveTemporaryCollection: " + ret2); 408 | }); 409 | } 410 | 411 | public string PenumbraResolvePath(string path) 412 | { 413 | if (!CheckPenumbraApi()) return path; 414 | var resolvedPath = _penumbraResolvePlayer!.Invoke(path); 415 | return resolvedPath ?? path; 416 | } 417 | 418 | public string[] PenumbraReverseResolvePlayer(string path) 419 | { 420 | if (!CheckPenumbraApi()) return new[] { path }; 421 | var resolvedPaths = _reverseResolvePlayer.Invoke(path); 422 | if (resolvedPaths.Length == 0) 423 | { 424 | resolvedPaths = new[] { path }; 425 | } 426 | return resolvedPaths; 427 | } 428 | 429 | public string PenumbraResolvePathObject(string path, int objIdx) 430 | { 431 | if (!CheckPenumbraApi()) return path; 432 | var resolvedPath = _penumbraResolvePlayerObject!.Invoke(path, objIdx); 433 | return resolvedPath ?? path; 434 | } 435 | 436 | public string[] PenumbraReverseResolveObject(string path, int objIdx) 437 | { 438 | if (!CheckPenumbraApi()) return new[] { path }; 439 | var resolvedPaths = _penumbraReverseResolvePlayerObject.Invoke(path, objIdx); 440 | if (resolvedPaths.Length == 0) 441 | { 442 | resolvedPaths = new[] { path }; 443 | } 444 | return resolvedPaths; 445 | } 446 | 447 | public void PenumbraSetTemporaryMods(ICharacter character, int? idx, Dictionary modPaths, string manipulationData) 448 | { 449 | if (!CheckPenumbraApi()) return; 450 | if (idx == null) 451 | { 452 | return; 453 | } 454 | var collName = TempCollectionPrefix + character.Name.TextValue; 455 | var ret = _penumbraCreateTemporaryCollection.Invoke(collName); 456 | Logger.Verbose("Creating Temp Collection " + collName + ", Success: " + ret); 457 | var retAssign = _penumbraAssignTemporaryCollection.Invoke(ret, idx.Value, true); 458 | Logger.Verbose("Assigning Temp Collection " + collName + " to index " + idx.Value); 459 | Logger.Verbose("Penumbra response" + retAssign); 460 | foreach (var mod in modPaths) 461 | { 462 | Logger.Verbose(mod.Key + " => " + mod.Value); 463 | } 464 | 465 | var ret2 = _penumbraAddTemporaryMod.Invoke("Snap", ret, modPaths, manipulationData, 0); 466 | Logger.Verbose("Setting temp mods for " + collName + ", Success: " + ret2); 467 | } 468 | 469 | private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) 470 | { 471 | PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); 472 | } 473 | 474 | private void PenumbraInit() 475 | { 476 | PenumbraInitialized?.Invoke(); 477 | //_penumbraRedraw!.Invoke("self", RedrawType.Redraw); 478 | } 479 | 480 | private void OnCustomizePlusScaleChange(string? scale) 481 | { 482 | if (scale != null) scale = Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); 483 | CustomizePlusScaleChange?.Invoke(scale); 484 | } 485 | 486 | private void PenumbraDispose() 487 | { 488 | PenumbraDisposed?.Invoke(); 489 | actionQueue.Clear(); 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /Snapper/Managers/SnapshotManager.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.ClientState.Objects.Types; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Penumbra.Api; 8 | using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; 9 | using FFXIVClientStructs.FFXIV.Client.System.Resource; 10 | using Penumbra.Interop.Structs; 11 | using Lumina.Models.Materials; 12 | using Snapper.Models; 13 | using Snapper.Utils; 14 | using System.Threading; 15 | using Dalamud.Game.ClientState.Objects.Enums; 16 | using Penumbra.String; 17 | using FFXIVClientStructs.FFXIV.Client.Game.Object; 18 | using System.IO; 19 | using System.Text.Json; 20 | using Snapper.Interop; 21 | using Dalamud.Utility; 22 | 23 | namespace Snapper.Managers 24 | { 25 | public class SnapshotManager 26 | { 27 | private Plugin Plugin; 28 | private List tempCollections = new(); 29 | 30 | public SnapshotManager(Plugin plugin) 31 | { 32 | 33 | this.Plugin = plugin; 34 | } 35 | 36 | public void RevertAllSnapshots() 37 | { 38 | foreach(var character in tempCollections) 39 | { 40 | Plugin.IpcManager.PenumbraRemoveTemporaryCollection(character.Name.TextValue); 41 | Plugin.IpcManager.GlamourerRevertCharacterCustomization(character); 42 | Plugin.IpcManager.CustomizePlusRevert(character.Address); 43 | } 44 | tempCollections.Clear(); 45 | } 46 | 47 | public bool AppendSnapshot(ICharacter character) 48 | { 49 | var charaName = character.Name.TextValue; 50 | var path = Path.Combine(Plugin.Configuration.WorkingDirectory, charaName); 51 | string infoJson = File.ReadAllText(Path.Combine(path, "snapshot.json")); 52 | if (infoJson == null) 53 | { 54 | Logger.Warn("No snapshot json found, aborting"); 55 | return false; 56 | } 57 | SnapshotInfo? snapshotInfo = JsonSerializer.Deserialize(infoJson); 58 | if (snapshotInfo == null) 59 | { 60 | Logger.Warn("Failed to deserialize snapshot json, aborting"); 61 | return false; 62 | } 63 | 64 | if (!Directory.Exists(path)) 65 | { 66 | //no existing snapshot for character, just use save mode 67 | this.SaveSnapshot(character); 68 | } 69 | 70 | //Merge file replacements 71 | List replacements = GetFileReplacementsForCharacter(character); 72 | 73 | Logger.Debug($"Got {replacements.Count} replacements"); 74 | 75 | foreach (var replacement in replacements) 76 | { 77 | FileInfo replacementFile = new FileInfo(replacement.ResolvedPath); 78 | FileInfo fileToCreate = new FileInfo(Path.Combine(path, replacement.GamePaths[0])); 79 | if (!fileToCreate.Exists) 80 | { 81 | //totally new file 82 | fileToCreate.Directory.Create(); 83 | replacementFile.CopyTo(fileToCreate.FullName); 84 | foreach (var gamePath in replacement.GamePaths) 85 | { 86 | var collisions = snapshotInfo.FileReplacements.Where(src => src.Value.Any(path => path == gamePath)); 87 | //gamepath already exists in snapshot, overwrite with new file 88 | foreach (var collision in collisions) 89 | { 90 | collision.Value.Remove(gamePath); 91 | if (collision.Value.Count == 0) 92 | { 93 | //delete file if it no longer has any references 94 | snapshotInfo.FileReplacements.Remove(collision.Key); 95 | File.Delete(Path.Combine(path, collision.Key)); 96 | } 97 | } 98 | } 99 | snapshotInfo.FileReplacements.Add(replacement.GamePaths[0], replacement.GamePaths); 100 | } 101 | } 102 | 103 | //Merge meta manips 104 | //Meta manipulations seem to be sent containing every mod a character has enabled, regardless of whether it's actively being used. 105 | //This may end up shooting me in the foot, but a newer snapshot should contain the info of an older one. 106 | snapshotInfo.ManipulationString = Plugin.IpcManager.PenumbraGetGameObjectMetaManipulations(character.ObjectIndex); 107 | 108 | string infoJsonWrite = JsonSerializer.Serialize(snapshotInfo); 109 | File.WriteAllText(Path.Combine(path, "snapshot.json"), infoJsonWrite); 110 | 111 | return true; 112 | } 113 | 114 | public bool SaveSnapshot(ICharacter character) 115 | { 116 | var charaName = character.Name.TextValue; 117 | var path = Path.Combine(Plugin.Configuration.WorkingDirectory,charaName); 118 | SnapshotInfo snapshotInfo = new(); 119 | 120 | if (Directory.Exists(path)) 121 | { 122 | Logger.Warn("Snapshot already existed, deleting"); 123 | Directory.Delete(path, true); 124 | } 125 | Directory.CreateDirectory(path); 126 | 127 | //Get glamourer string 128 | snapshotInfo.GlamourerString = Plugin.IpcManager.GlamourerGetCharacterCustomization(character.Address); 129 | Logger.Debug($"Got glamourer string {snapshotInfo.GlamourerString}"); 130 | 131 | //Save all file replacements 132 | 133 | List replacements = GetFileReplacementsForCharacter(character); 134 | 135 | Logger.Debug($"Got {replacements.Count} replacements"); 136 | 137 | foreach(var replacement in replacements) 138 | { 139 | FileInfo replacementFile = new FileInfo(replacement.ResolvedPath); 140 | FileInfo fileToCreate = new FileInfo(Path.Combine(path, replacement.GamePaths[0])); 141 | fileToCreate.Directory.Create(); 142 | replacementFile.CopyTo(fileToCreate.FullName); 143 | snapshotInfo.FileReplacements.Add(replacement.GamePaths[0], replacement.GamePaths); 144 | } 145 | 146 | snapshotInfo.ManipulationString = Plugin.IpcManager.PenumbraGetGameObjectMetaManipulations(character.ObjectIndex); 147 | 148 | //Get customize+ data, if applicable 149 | if (Plugin.IpcManager.CheckCustomizePlusApi()) 150 | { 151 | Logger.Debug("C+ api loaded"); 152 | var data = Plugin.IpcManager.GetCustomizePlusScaleFromCharacter(character); 153 | //Logger.Info(Plugin.DalamudUtil.PlayerName); 154 | //Logger.Info(character.Name.TextValue); 155 | //Logger.Info($"Cust+: {data}"); 156 | if (!data.IsNullOrEmpty()) 157 | { 158 | File.WriteAllText(Path.Combine(path, "customizePlus.json"), data); 159 | } 160 | } 161 | 162 | string infoJson = JsonSerializer.Serialize(snapshotInfo); 163 | File.WriteAllText(Path.Combine(path, "snapshot.json"), infoJson); 164 | 165 | return true; 166 | } 167 | 168 | public bool LoadSnapshot(ICharacter characterApplyTo, int objIdx, string path) 169 | { 170 | Logger.Info($"Applying snapshot to {characterApplyTo.Address}"); 171 | string infoJson = File.ReadAllText(Path.Combine(path,"snapshot.json")); 172 | if (infoJson == null) 173 | { 174 | Logger.Warn("No snapshot json found, aborting"); 175 | return false; 176 | } 177 | SnapshotInfo? snapshotInfo = JsonSerializer.Deserialize(infoJson); 178 | if(snapshotInfo == null) 179 | { 180 | Logger.Warn("Failed to deserialize snapshot json, aborting"); 181 | return false; 182 | } 183 | 184 | //Apply mods 185 | Dictionary moddedPaths = new(); 186 | foreach(var replacement in snapshotInfo.FileReplacements) 187 | { 188 | foreach(var gamePath in replacement.Value) 189 | { 190 | moddedPaths.Add(gamePath, Path.Combine(path, replacement.Key)); 191 | } 192 | } 193 | Logger.Debug($"Applied {moddedPaths.Count} replacements"); 194 | 195 | Plugin.IpcManager.PenumbraRemoveTemporaryCollection(characterApplyTo.Name.TextValue); 196 | Plugin.IpcManager.PenumbraSetTemporaryMods(characterApplyTo, objIdx, moddedPaths, snapshotInfo.ManipulationString); 197 | if (!tempCollections.Contains(characterApplyTo)) 198 | { 199 | tempCollections.Add(characterApplyTo); 200 | } 201 | 202 | //Apply Customize+ if it exists and C+ is installed 203 | if (Plugin.IpcManager.CheckCustomizePlusApi()) 204 | { 205 | if(File.Exists(Path.Combine(path, "customizePlus.json"))) 206 | { 207 | string custPlusData = File.ReadAllText(Path.Combine(path, "customizePlus.json")); 208 | Plugin.IpcManager.CustomizePlusSetBodyScale(characterApplyTo.Address, custPlusData); 209 | } 210 | } 211 | 212 | //Apply glamourer string 213 | Plugin.IpcManager.GlamourerApplyAll(snapshotInfo.GlamourerString, characterApplyTo); 214 | 215 | //Redraw 216 | Plugin.IpcManager.PenumbraRedraw(objIdx); 217 | 218 | return true; 219 | } 220 | 221 | private int? GetObjIDXFromCharacter(ICharacter character) 222 | { 223 | for (var i = 0; i <= Plugin.Objects.Length; i++) 224 | { 225 | global::Dalamud.Game.ClientState.Objects.Types.IGameObject current = Plugin.Objects[i]; 226 | if (!(current == null) && current.GameObjectId == character.GameObjectId) 227 | { 228 | return i; 229 | } 230 | } 231 | return null; 232 | } 233 | 234 | public unsafe List GetFileReplacementsForCharacter(ICharacter character) 235 | { 236 | List replacements = new List(); 237 | var charaPointer = character.Address; 238 | var objectKind = character.ObjectKind; 239 | var charaName = character.Name.TextValue; 240 | int? objIdx = GetObjIDXFromCharacter(character); 241 | 242 | Logger.Debug($"Character name {charaName}"); 243 | if(objIdx == null) 244 | { 245 | Logger.Error("Unable to find character in object table, aborting search for file replacements"); 246 | return replacements; 247 | } 248 | Logger.Debug($"Object IDX {objIdx}"); 249 | 250 | var chara = Plugin.DalamudUtil.CreateGameObject(charaPointer)!; 251 | while (!Plugin.DalamudUtil.IsObjectPresent(chara)) 252 | { 253 | Logger.Verbose("Character is null but it shouldn't be, waiting"); 254 | Thread.Sleep(50); 255 | } 256 | 257 | Plugin.DalamudUtil.WaitWhileCharacterIsDrawing(objectKind.ToString(), charaPointer, 15000); 258 | 259 | var baseCharacter = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(void*)charaPointer; 260 | var human = (Human*)baseCharacter->GameObject.GetDrawObject(); 261 | for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) 262 | { 263 | var mdl = (RenderModel*)human->CharacterBase.Models[mdlIdx]; 264 | if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) 265 | { 266 | continue; 267 | } 268 | 269 | AddReplacementsFromRenderModel(mdl, replacements, objIdx.Value, 0); 270 | } 271 | 272 | AddPlayerSpecificReplacements(replacements, charaPointer, human, objIdx.Value); 273 | 274 | return replacements; 275 | } 276 | 277 | private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, List replacements, int objIdx, int inheritanceLevel = 0) 278 | { 279 | if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) 280 | { 281 | return; 282 | } 283 | 284 | string mdlPath; 285 | try 286 | { 287 | mdlPath = new ByteString(mdl->ResourceHandle->FileName()).ToString(); 288 | } 289 | catch 290 | { 291 | Logger.Warn("Could not get model data"); 292 | return; 293 | } 294 | Logger.Verbose("Checking File Replacement for Model " + mdlPath); 295 | 296 | FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath, objIdx); 297 | //DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel); 298 | 299 | AddFileReplacement(replacements, mdlFileReplacement); 300 | 301 | for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) 302 | { 303 | var mtrl = (Penumbra.Interop.Structs.Material*)mdl->Materials[mtrlIdx]; 304 | if (mtrl == null) continue; 305 | 306 | AddReplacementsFromMaterial(mtrl, replacements, objIdx, inheritanceLevel + 1); 307 | } 308 | } 309 | 310 | private unsafe void AddReplacementsFromMaterial(Penumbra.Interop.Structs.Material* mtrl, List replacements, int objIdx, int inheritanceLevel = 0) 311 | { 312 | string fileName; 313 | try 314 | { 315 | fileName = new ByteString(mtrl->ResourceHandle->FileName()).ToString(); 316 | 317 | } 318 | catch 319 | { 320 | Logger.Warn("Could not get material data"); 321 | return; 322 | } 323 | 324 | Logger.Verbose("Checking File Replacement for Material " + fileName); 325 | var mtrlArray = fileName.Split("|"); 326 | string mtrlPath; 327 | if (mtrlArray.Count() >= 3) 328 | { 329 | mtrlPath = fileName.Split("|")[2]; 330 | } 331 | else 332 | { 333 | Logger.Warn($"Material {fileName} did not split into at least 3 partts"); 334 | return; 335 | } 336 | 337 | if (replacements.Any(c => c.ResolvedPath.Contains(mtrlPath, StringComparison.Ordinal))) 338 | { 339 | return; 340 | } 341 | 342 | var mtrlFileReplacement = CreateFileReplacement(mtrlPath, objIdx); 343 | //DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel); 344 | 345 | AddFileReplacement(replacements, mtrlFileReplacement); 346 | 347 | var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; 348 | for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) 349 | { 350 | string? texPath = null; 351 | try 352 | { 353 | texPath = new ByteString(mtrlResourceHandle->TexString(resIdx)).ToString(); 354 | } 355 | catch 356 | { 357 | Logger.Warn("Could not get Texture data for Material " + fileName); 358 | } 359 | 360 | if (string.IsNullOrEmpty(texPath)) continue; 361 | 362 | Logger.Verbose("Checking File Replacement for Texture " + texPath); 363 | 364 | AddReplacementsFromTexture(texPath, replacements, objIdx, inheritanceLevel + 1); 365 | } 366 | 367 | try 368 | { 369 | var shpkPath = "shader/sm5/shpk/" + new ByteString(mtrlResourceHandle->ShpkString).ToString(); 370 | Logger.Verbose("Checking File Replacement for Shader " + shpkPath); 371 | AddReplacementsFromShader(shpkPath, replacements, objIdx, inheritanceLevel + 1); 372 | } 373 | catch 374 | { 375 | Logger.Verbose("Could not find shpk for Material " + fileName); 376 | } 377 | } 378 | 379 | private void AddReplacementsFromTexture(string texPath, List replacements, int objIdx, int inheritanceLevel = 0, bool doNotReverseResolve = true) 380 | { 381 | Logger.Debug($"Adding replacement for texture {texPath}"); 382 | if (string.IsNullOrEmpty(texPath)) return; 383 | 384 | if(replacements.Any(c => c.GamePaths.Contains(texPath, StringComparer.Ordinal))) 385 | { 386 | Logger.Debug($"Replacements already contain {texPath}, skipping"); 387 | return; 388 | } 389 | 390 | var texFileReplacement = CreateFileReplacement(texPath, objIdx, doNotReverseResolve); 391 | //DebugPrint(texFileReplacement, objectKind, "Texture", inheritanceLevel); 392 | 393 | AddFileReplacement(replacements, texFileReplacement); 394 | 395 | if (texPath.Contains("/--", StringComparison.Ordinal)) return; 396 | 397 | var texDx11Replacement = 398 | CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), objIdx, doNotReverseResolve); 399 | 400 | //DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel); 401 | 402 | AddFileReplacement(replacements, texDx11Replacement); 403 | } 404 | 405 | private void AddReplacementsFromShader(string shpkPath, List replacements, int objIdx, int inheritanceLevel = 0) 406 | { 407 | if (string.IsNullOrEmpty(shpkPath)) return; 408 | 409 | if (replacements.Any(c => c.GamePaths.Contains(shpkPath, StringComparer.Ordinal))) 410 | { 411 | return; 412 | } 413 | 414 | var shpkFileReplacement = CreateFileReplacement(shpkPath, objIdx); 415 | //DebugPrint(shpkFileReplacement, objectKind, "Shader", inheritanceLevel); 416 | AddFileReplacement(replacements, shpkFileReplacement); 417 | } 418 | 419 | private unsafe void AddPlayerSpecificReplacements(List replacements, IntPtr charaPointer, Human* human, int objIdx) 420 | { 421 | var weaponObject = (Interop.Weapon*)((FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object*)human)->ChildObject; 422 | 423 | if ((IntPtr)weaponObject != IntPtr.Zero) 424 | { 425 | var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; 426 | 427 | AddReplacementsFromRenderModel(mainHandWeapon, replacements, objIdx, 0); 428 | 429 | /* 430 | foreach (var item in replacements) 431 | { 432 | _transientResourceManager.RemoveTransientResource(charaPointer, item); 433 | } 434 | */ 435 | /* 436 | foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject)) 437 | { 438 | Logger.Verbose("Found transient weapon resource: " + item); 439 | AddReplacement(item, objectKind, previousData, 1, true); 440 | } 441 | */ 442 | 443 | 444 | if (weaponObject->NextSibling != (IntPtr)weaponObject) 445 | { 446 | var offHandWeapon = ((Interop.Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; 447 | 448 | AddReplacementsFromRenderModel(offHandWeapon, replacements, objIdx, 1); 449 | /* 450 | foreach (var item in replacements) 451 | { 452 | _transientResourceManager.RemoveTransientResource((IntPtr)offHandWeapon, item); 453 | } 454 | 455 | foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) 456 | { 457 | Logger.Verbose("Found transient offhand weapon resource: " + item); 458 | AddReplacement(item, objectKind, previousData, 1, true); 459 | } 460 | */ 461 | } 462 | } 463 | 464 | AddReplacementSkeleton(((Interop.HumanExt*)human)->Human.RaceSexId, objIdx, replacements); 465 | try 466 | { 467 | AddReplacementsFromTexture(new ByteString(((Interop.HumanExt*)human)->Decal->FileName()).ToString(), replacements, objIdx, 0, false); 468 | } 469 | catch 470 | { 471 | Logger.Warn("Could not get Decal data"); 472 | } 473 | try 474 | { 475 | AddReplacementsFromTexture(new ByteString(((Interop.HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), replacements, objIdx, 0, false); 476 | } 477 | catch 478 | { 479 | Logger.Warn("Could not get Legacy Body Decal Data"); 480 | } 481 | /* 482 | foreach (var item in previousData.FileReplacements[objectKind]) 483 | { 484 | _transientResourceManager.RemoveTransientResource(charaPointer, item); 485 | } 486 | */ 487 | } 488 | 489 | private void AddReplacementSkeleton(ushort raceSexId, int objIdx, List replacements) 490 | { 491 | string raceSexIdString = raceSexId.ToString("0000"); 492 | 493 | string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; 494 | 495 | var replacement = CreateFileReplacement(skeletonPath, objIdx, true); 496 | AddFileReplacement(replacements, replacement); 497 | 498 | //DebugPrint(replacement, objectKind, "SKLB", 0); 499 | } 500 | 501 | private void AddFileReplacement(List replacements, FileReplacement newReplacement) 502 | { 503 | if (!newReplacement.HasFileReplacement) 504 | { 505 | Logger.Debug($"Replacement for {newReplacement.ResolvedPath} does not have a file replacement, skipping"); 506 | foreach(var path in newReplacement.GamePaths) 507 | { 508 | Logger.Debug(path); 509 | } 510 | return; 511 | } 512 | 513 | var existingReplacement = replacements.SingleOrDefault(f => string.Equals(f.ResolvedPath, newReplacement.ResolvedPath, System.StringComparison.OrdinalIgnoreCase)); 514 | if (existingReplacement != null) 515 | { 516 | Logger.Debug($"Added replacement for existing path {existingReplacement.ResolvedPath}"); 517 | existingReplacement.GamePaths.AddRange(newReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e, System.StringComparer.OrdinalIgnoreCase))); 518 | } 519 | else 520 | { 521 | Logger.Debug($"Added new replacement {newReplacement.ResolvedPath}"); 522 | replacements.Add(newReplacement); 523 | } 524 | } 525 | 526 | private FileReplacement CreateFileReplacement(string path, int objIdx, bool doNotReverseResolve = false) 527 | { 528 | var fileReplacement = new FileReplacement(Plugin); 529 | 530 | if (!doNotReverseResolve) 531 | { 532 | fileReplacement.ReverseResolvePathObject(path, objIdx); 533 | } 534 | else 535 | { 536 | fileReplacement.ResolvePathObject(path, objIdx); 537 | } 538 | 539 | Logger.Debug($"Created file replacement for resolved path {fileReplacement.ResolvedPath}, hash {fileReplacement.Hash}, gamepath {fileReplacement.GamePaths[0]}"); 540 | return fileReplacement; 541 | } 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /Snapper/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net9.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[13.0.0, )", 8 | "resolved": "13.0.0", 9 | "contentHash": "Mb3cUDSK/vDPQ8gQIeuCw03EMYrej1B4J44a1AvIJ9C759p9XeqdU9Hg4WgOmlnlPe0G7ILTD32PKSUpkQNa8w==" 10 | }, 11 | "Glamourer.Api": { 12 | "type": "Direct", 13 | "requested": "[2.6.0, )", 14 | "resolved": "2.6.0", 15 | "contentHash": "zysCZgNBRm3k3qvibyw/31MmEckX0Uh0ZsT+Sax3ZHnYIRELr9Qhbz3cjJz7u0RHGIrNJiRpktu/LxgHEqDItw==" 16 | }, 17 | "lz4net": { 18 | "type": "Direct", 19 | "requested": "[1.0.15.93, )", 20 | "resolved": "1.0.15.93", 21 | "contentHash": "s92+YYtH4tiw0/0a7cyb+EXAOICtxZgPA1cL19L8paJZe39QViBaNl24o9ETBJxjgbm11OIMjuMUzX86id6SEg==", 22 | "dependencies": { 23 | "NETStandard.Library": "1.6.1", 24 | "lz4net.unsafe.netcore": "[1.0.15.93]" 25 | } 26 | }, 27 | "Microsoft.Extensions.Logging": { 28 | "type": "Direct", 29 | "requested": "[9.0.8, )", 30 | "resolved": "9.0.8", 31 | "contentHash": "Z/7ze+0iheT7FJeZPqJKARYvyC2bmwu3whbm/48BJjdlGVvgDguoCqJIkI/67NkroTYobd5geai1WheNQvWrgA==", 32 | "dependencies": { 33 | "Microsoft.Extensions.DependencyInjection": "9.0.8", 34 | "Microsoft.Extensions.Logging.Abstractions": "9.0.8", 35 | "Microsoft.Extensions.Options": "9.0.8" 36 | } 37 | }, 38 | "FlatSharp.Compiler": { 39 | "type": "Transitive", 40 | "resolved": "7.9.0", 41 | "contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A==" 42 | }, 43 | "FlatSharp.Runtime": { 44 | "type": "Transitive", 45 | "resolved": "7.9.0", 46 | "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==", 47 | "dependencies": { 48 | "System.Memory": "4.5.5" 49 | } 50 | }, 51 | "JetBrains.Annotations": { 52 | "type": "Transitive", 53 | "resolved": "2024.3.0", 54 | "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" 55 | }, 56 | "lz4net.unsafe.netcore": { 57 | "type": "Transitive", 58 | "resolved": "1.0.15.93", 59 | "contentHash": "tuJA5s13/5AF+fUcvERBBXtzK/gARwpJiHrqsCh2UH57e8qNp+WETNrKw63byK0Zle4OVTwuWgN0cY/uSc388Q==", 60 | "dependencies": { 61 | "NETStandard.Library": "1.6.1" 62 | } 63 | }, 64 | "Microsoft.Extensions.DependencyInjection": { 65 | "type": "Transitive", 66 | "resolved": "9.0.8", 67 | "contentHash": "JJjI2Fa+QtZcUyuNjbKn04OjIUX5IgFGFu/Xc+qvzh1rXdZHLcnqqVXhR4093bGirTwacRlHiVg1XYI9xum6QQ==", 68 | "dependencies": { 69 | "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8" 70 | } 71 | }, 72 | "Microsoft.Extensions.DependencyInjection.Abstractions": { 73 | "type": "Transitive", 74 | "resolved": "9.0.8", 75 | "contentHash": "xY3lTjj4+ZYmiKIkyWitddrp1uL5uYiweQjqo4BKBw01ZC4HhcfgLghDpPZcUlppgWAFqFy9SgkiYWOMx365pw==" 76 | }, 77 | "Microsoft.Extensions.Logging.Abstractions": { 78 | "type": "Transitive", 79 | "resolved": "9.0.8", 80 | "contentHash": "pYnAffJL7ARD/HCnnPvnFKSIHnTSmWz84WIlT9tPeQ4lHNiu0Az7N/8itihWvcF8sT+VVD5lq8V+ckMzu4SbOw==", 81 | "dependencies": { 82 | "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8" 83 | } 84 | }, 85 | "Microsoft.Extensions.Options": { 86 | "type": "Transitive", 87 | "resolved": "9.0.8", 88 | "contentHash": "OmTaQ0v4gxGQkehpwWIqPoEiwsPuG/u4HUsbOFoWGx4DKET2AXzopnFe/fE608FIhzc/kcg2p8JdyMRCCUzitQ==", 89 | "dependencies": { 90 | "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", 91 | "Microsoft.Extensions.Primitives": "9.0.8" 92 | } 93 | }, 94 | "Microsoft.Extensions.Primitives": { 95 | "type": "Transitive", 96 | "resolved": "9.0.8", 97 | "contentHash": "tizSIOEsIgSNSSh+hKeUVPK7xmTIjR8s+mJWOu1KXV3htvNQiPMFRMO17OdI1y/4ZApdBVk49u/08QGC9yvLug==" 98 | }, 99 | "Microsoft.NETCore.Platforms": { 100 | "type": "Transitive", 101 | "resolved": "1.1.0", 102 | "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" 103 | }, 104 | "Microsoft.NETCore.Targets": { 105 | "type": "Transitive", 106 | "resolved": "1.1.0", 107 | "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" 108 | }, 109 | "Microsoft.Win32.Primitives": { 110 | "type": "Transitive", 111 | "resolved": "4.3.0", 112 | "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", 113 | "dependencies": { 114 | "Microsoft.NETCore.Platforms": "1.1.0", 115 | "Microsoft.NETCore.Targets": "1.1.0", 116 | "System.Runtime": "4.3.0" 117 | } 118 | }, 119 | "NETStandard.Library": { 120 | "type": "Transitive", 121 | "resolved": "1.6.1", 122 | "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", 123 | "dependencies": { 124 | "Microsoft.NETCore.Platforms": "1.1.0", 125 | "Microsoft.Win32.Primitives": "4.3.0", 126 | "System.AppContext": "4.3.0", 127 | "System.Collections": "4.3.0", 128 | "System.Collections.Concurrent": "4.3.0", 129 | "System.Console": "4.3.0", 130 | "System.Diagnostics.Debug": "4.3.0", 131 | "System.Diagnostics.Tools": "4.3.0", 132 | "System.Diagnostics.Tracing": "4.3.0", 133 | "System.Globalization": "4.3.0", 134 | "System.Globalization.Calendars": "4.3.0", 135 | "System.IO": "4.3.0", 136 | "System.IO.Compression": "4.3.0", 137 | "System.IO.Compression.ZipFile": "4.3.0", 138 | "System.IO.FileSystem": "4.3.0", 139 | "System.IO.FileSystem.Primitives": "4.3.0", 140 | "System.Linq": "4.3.0", 141 | "System.Linq.Expressions": "4.3.0", 142 | "System.Net.Http": "4.3.0", 143 | "System.Net.Primitives": "4.3.0", 144 | "System.Net.Sockets": "4.3.0", 145 | "System.ObjectModel": "4.3.0", 146 | "System.Reflection": "4.3.0", 147 | "System.Reflection.Extensions": "4.3.0", 148 | "System.Reflection.Primitives": "4.3.0", 149 | "System.Resources.ResourceManager": "4.3.0", 150 | "System.Runtime": "4.3.0", 151 | "System.Runtime.Extensions": "4.3.0", 152 | "System.Runtime.Handles": "4.3.0", 153 | "System.Runtime.InteropServices": "4.3.0", 154 | "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", 155 | "System.Runtime.Numerics": "4.3.0", 156 | "System.Security.Cryptography.Algorithms": "4.3.0", 157 | "System.Security.Cryptography.Encoding": "4.3.0", 158 | "System.Security.Cryptography.Primitives": "4.3.0", 159 | "System.Security.Cryptography.X509Certificates": "4.3.0", 160 | "System.Text.Encoding": "4.3.0", 161 | "System.Text.Encoding.Extensions": "4.3.0", 162 | "System.Text.RegularExpressions": "4.3.0", 163 | "System.Threading": "4.3.0", 164 | "System.Threading.Tasks": "4.3.0", 165 | "System.Threading.Timer": "4.3.0", 166 | "System.Xml.ReaderWriter": "4.3.0", 167 | "System.Xml.XDocument": "4.3.0" 168 | } 169 | }, 170 | "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 171 | "type": "Transitive", 172 | "resolved": "4.3.0", 173 | "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" 174 | }, 175 | "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 176 | "type": "Transitive", 177 | "resolved": "4.3.0", 178 | "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" 179 | }, 180 | "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 181 | "type": "Transitive", 182 | "resolved": "4.3.0", 183 | "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" 184 | }, 185 | "runtime.native.System": { 186 | "type": "Transitive", 187 | "resolved": "4.3.0", 188 | "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", 189 | "dependencies": { 190 | "Microsoft.NETCore.Platforms": "1.1.0", 191 | "Microsoft.NETCore.Targets": "1.1.0" 192 | } 193 | }, 194 | "runtime.native.System.IO.Compression": { 195 | "type": "Transitive", 196 | "resolved": "4.3.0", 197 | "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", 198 | "dependencies": { 199 | "Microsoft.NETCore.Platforms": "1.1.0", 200 | "Microsoft.NETCore.Targets": "1.1.0" 201 | } 202 | }, 203 | "runtime.native.System.Net.Http": { 204 | "type": "Transitive", 205 | "resolved": "4.3.0", 206 | "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", 207 | "dependencies": { 208 | "Microsoft.NETCore.Platforms": "1.1.0", 209 | "Microsoft.NETCore.Targets": "1.1.0" 210 | } 211 | }, 212 | "runtime.native.System.Security.Cryptography.Apple": { 213 | "type": "Transitive", 214 | "resolved": "4.3.0", 215 | "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", 216 | "dependencies": { 217 | "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" 218 | } 219 | }, 220 | "runtime.native.System.Security.Cryptography.OpenSsl": { 221 | "type": "Transitive", 222 | "resolved": "4.3.0", 223 | "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", 224 | "dependencies": { 225 | "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 226 | "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 227 | "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 228 | "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 229 | "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 230 | "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 231 | "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 232 | "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 233 | "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", 234 | "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" 235 | } 236 | }, 237 | "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 238 | "type": "Transitive", 239 | "resolved": "4.3.0", 240 | "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" 241 | }, 242 | "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 243 | "type": "Transitive", 244 | "resolved": "4.3.0", 245 | "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" 246 | }, 247 | "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { 248 | "type": "Transitive", 249 | "resolved": "4.3.0", 250 | "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" 251 | }, 252 | "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 253 | "type": "Transitive", 254 | "resolved": "4.3.0", 255 | "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" 256 | }, 257 | "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 258 | "type": "Transitive", 259 | "resolved": "4.3.0", 260 | "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" 261 | }, 262 | "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 263 | "type": "Transitive", 264 | "resolved": "4.3.0", 265 | "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" 266 | }, 267 | "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 268 | "type": "Transitive", 269 | "resolved": "4.3.0", 270 | "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" 271 | }, 272 | "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { 273 | "type": "Transitive", 274 | "resolved": "4.3.0", 275 | "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" 276 | }, 277 | "System.AppContext": { 278 | "type": "Transitive", 279 | "resolved": "4.3.0", 280 | "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", 281 | "dependencies": { 282 | "System.Runtime": "4.3.0" 283 | } 284 | }, 285 | "System.Buffers": { 286 | "type": "Transitive", 287 | "resolved": "4.3.0", 288 | "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", 289 | "dependencies": { 290 | "System.Diagnostics.Debug": "4.3.0", 291 | "System.Diagnostics.Tracing": "4.3.0", 292 | "System.Resources.ResourceManager": "4.3.0", 293 | "System.Runtime": "4.3.0", 294 | "System.Threading": "4.3.0" 295 | } 296 | }, 297 | "System.Collections": { 298 | "type": "Transitive", 299 | "resolved": "4.3.0", 300 | "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", 301 | "dependencies": { 302 | "Microsoft.NETCore.Platforms": "1.1.0", 303 | "Microsoft.NETCore.Targets": "1.1.0", 304 | "System.Runtime": "4.3.0" 305 | } 306 | }, 307 | "System.Collections.Concurrent": { 308 | "type": "Transitive", 309 | "resolved": "4.3.0", 310 | "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", 311 | "dependencies": { 312 | "System.Collections": "4.3.0", 313 | "System.Diagnostics.Debug": "4.3.0", 314 | "System.Diagnostics.Tracing": "4.3.0", 315 | "System.Globalization": "4.3.0", 316 | "System.Reflection": "4.3.0", 317 | "System.Resources.ResourceManager": "4.3.0", 318 | "System.Runtime": "4.3.0", 319 | "System.Runtime.Extensions": "4.3.0", 320 | "System.Threading": "4.3.0", 321 | "System.Threading.Tasks": "4.3.0" 322 | } 323 | }, 324 | "System.Console": { 325 | "type": "Transitive", 326 | "resolved": "4.3.0", 327 | "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", 328 | "dependencies": { 329 | "Microsoft.NETCore.Platforms": "1.1.0", 330 | "Microsoft.NETCore.Targets": "1.1.0", 331 | "System.IO": "4.3.0", 332 | "System.Runtime": "4.3.0", 333 | "System.Text.Encoding": "4.3.0" 334 | } 335 | }, 336 | "System.Diagnostics.Debug": { 337 | "type": "Transitive", 338 | "resolved": "4.3.0", 339 | "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", 340 | "dependencies": { 341 | "Microsoft.NETCore.Platforms": "1.1.0", 342 | "Microsoft.NETCore.Targets": "1.1.0", 343 | "System.Runtime": "4.3.0" 344 | } 345 | }, 346 | "System.Diagnostics.DiagnosticSource": { 347 | "type": "Transitive", 348 | "resolved": "4.3.0", 349 | "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", 350 | "dependencies": { 351 | "System.Collections": "4.3.0", 352 | "System.Diagnostics.Tracing": "4.3.0", 353 | "System.Reflection": "4.3.0", 354 | "System.Runtime": "4.3.0", 355 | "System.Threading": "4.3.0" 356 | } 357 | }, 358 | "System.Diagnostics.Tools": { 359 | "type": "Transitive", 360 | "resolved": "4.3.0", 361 | "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", 362 | "dependencies": { 363 | "Microsoft.NETCore.Platforms": "1.1.0", 364 | "Microsoft.NETCore.Targets": "1.1.0", 365 | "System.Runtime": "4.3.0" 366 | } 367 | }, 368 | "System.Diagnostics.Tracing": { 369 | "type": "Transitive", 370 | "resolved": "4.3.0", 371 | "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", 372 | "dependencies": { 373 | "Microsoft.NETCore.Platforms": "1.1.0", 374 | "Microsoft.NETCore.Targets": "1.1.0", 375 | "System.Runtime": "4.3.0" 376 | } 377 | }, 378 | "System.Globalization": { 379 | "type": "Transitive", 380 | "resolved": "4.3.0", 381 | "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", 382 | "dependencies": { 383 | "Microsoft.NETCore.Platforms": "1.1.0", 384 | "Microsoft.NETCore.Targets": "1.1.0", 385 | "System.Runtime": "4.3.0" 386 | } 387 | }, 388 | "System.Globalization.Calendars": { 389 | "type": "Transitive", 390 | "resolved": "4.3.0", 391 | "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", 392 | "dependencies": { 393 | "Microsoft.NETCore.Platforms": "1.1.0", 394 | "Microsoft.NETCore.Targets": "1.1.0", 395 | "System.Globalization": "4.3.0", 396 | "System.Runtime": "4.3.0" 397 | } 398 | }, 399 | "System.Globalization.Extensions": { 400 | "type": "Transitive", 401 | "resolved": "4.3.0", 402 | "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", 403 | "dependencies": { 404 | "Microsoft.NETCore.Platforms": "1.1.0", 405 | "System.Globalization": "4.3.0", 406 | "System.Resources.ResourceManager": "4.3.0", 407 | "System.Runtime": "4.3.0", 408 | "System.Runtime.Extensions": "4.3.0", 409 | "System.Runtime.InteropServices": "4.3.0" 410 | } 411 | }, 412 | "System.IO": { 413 | "type": "Transitive", 414 | "resolved": "4.3.0", 415 | "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", 416 | "dependencies": { 417 | "Microsoft.NETCore.Platforms": "1.1.0", 418 | "Microsoft.NETCore.Targets": "1.1.0", 419 | "System.Runtime": "4.3.0", 420 | "System.Text.Encoding": "4.3.0", 421 | "System.Threading.Tasks": "4.3.0" 422 | } 423 | }, 424 | "System.IO.Compression": { 425 | "type": "Transitive", 426 | "resolved": "4.3.0", 427 | "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", 428 | "dependencies": { 429 | "Microsoft.NETCore.Platforms": "1.1.0", 430 | "System.Buffers": "4.3.0", 431 | "System.Collections": "4.3.0", 432 | "System.Diagnostics.Debug": "4.3.0", 433 | "System.IO": "4.3.0", 434 | "System.Resources.ResourceManager": "4.3.0", 435 | "System.Runtime": "4.3.0", 436 | "System.Runtime.Extensions": "4.3.0", 437 | "System.Runtime.Handles": "4.3.0", 438 | "System.Runtime.InteropServices": "4.3.0", 439 | "System.Text.Encoding": "4.3.0", 440 | "System.Threading": "4.3.0", 441 | "System.Threading.Tasks": "4.3.0", 442 | "runtime.native.System": "4.3.0", 443 | "runtime.native.System.IO.Compression": "4.3.0" 444 | } 445 | }, 446 | "System.IO.Compression.ZipFile": { 447 | "type": "Transitive", 448 | "resolved": "4.3.0", 449 | "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", 450 | "dependencies": { 451 | "System.Buffers": "4.3.0", 452 | "System.IO": "4.3.0", 453 | "System.IO.Compression": "4.3.0", 454 | "System.IO.FileSystem": "4.3.0", 455 | "System.IO.FileSystem.Primitives": "4.3.0", 456 | "System.Resources.ResourceManager": "4.3.0", 457 | "System.Runtime": "4.3.0", 458 | "System.Runtime.Extensions": "4.3.0", 459 | "System.Text.Encoding": "4.3.0" 460 | } 461 | }, 462 | "System.IO.FileSystem": { 463 | "type": "Transitive", 464 | "resolved": "4.3.0", 465 | "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", 466 | "dependencies": { 467 | "Microsoft.NETCore.Platforms": "1.1.0", 468 | "Microsoft.NETCore.Targets": "1.1.0", 469 | "System.IO": "4.3.0", 470 | "System.IO.FileSystem.Primitives": "4.3.0", 471 | "System.Runtime": "4.3.0", 472 | "System.Runtime.Handles": "4.3.0", 473 | "System.Text.Encoding": "4.3.0", 474 | "System.Threading.Tasks": "4.3.0" 475 | } 476 | }, 477 | "System.IO.FileSystem.Primitives": { 478 | "type": "Transitive", 479 | "resolved": "4.3.0", 480 | "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", 481 | "dependencies": { 482 | "System.Runtime": "4.3.0" 483 | } 484 | }, 485 | "System.Linq": { 486 | "type": "Transitive", 487 | "resolved": "4.3.0", 488 | "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", 489 | "dependencies": { 490 | "System.Collections": "4.3.0", 491 | "System.Diagnostics.Debug": "4.3.0", 492 | "System.Resources.ResourceManager": "4.3.0", 493 | "System.Runtime": "4.3.0", 494 | "System.Runtime.Extensions": "4.3.0" 495 | } 496 | }, 497 | "System.Linq.Expressions": { 498 | "type": "Transitive", 499 | "resolved": "4.3.0", 500 | "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", 501 | "dependencies": { 502 | "System.Collections": "4.3.0", 503 | "System.Diagnostics.Debug": "4.3.0", 504 | "System.Globalization": "4.3.0", 505 | "System.IO": "4.3.0", 506 | "System.Linq": "4.3.0", 507 | "System.ObjectModel": "4.3.0", 508 | "System.Reflection": "4.3.0", 509 | "System.Reflection.Emit": "4.3.0", 510 | "System.Reflection.Emit.ILGeneration": "4.3.0", 511 | "System.Reflection.Emit.Lightweight": "4.3.0", 512 | "System.Reflection.Extensions": "4.3.0", 513 | "System.Reflection.Primitives": "4.3.0", 514 | "System.Reflection.TypeExtensions": "4.3.0", 515 | "System.Resources.ResourceManager": "4.3.0", 516 | "System.Runtime": "4.3.0", 517 | "System.Runtime.Extensions": "4.3.0", 518 | "System.Threading": "4.3.0" 519 | } 520 | }, 521 | "System.Memory": { 522 | "type": "Transitive", 523 | "resolved": "4.5.5", 524 | "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" 525 | }, 526 | "System.Net.Http": { 527 | "type": "Transitive", 528 | "resolved": "4.3.0", 529 | "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", 530 | "dependencies": { 531 | "Microsoft.NETCore.Platforms": "1.1.0", 532 | "System.Collections": "4.3.0", 533 | "System.Diagnostics.Debug": "4.3.0", 534 | "System.Diagnostics.DiagnosticSource": "4.3.0", 535 | "System.Diagnostics.Tracing": "4.3.0", 536 | "System.Globalization": "4.3.0", 537 | "System.Globalization.Extensions": "4.3.0", 538 | "System.IO": "4.3.0", 539 | "System.IO.FileSystem": "4.3.0", 540 | "System.Net.Primitives": "4.3.0", 541 | "System.Resources.ResourceManager": "4.3.0", 542 | "System.Runtime": "4.3.0", 543 | "System.Runtime.Extensions": "4.3.0", 544 | "System.Runtime.Handles": "4.3.0", 545 | "System.Runtime.InteropServices": "4.3.0", 546 | "System.Security.Cryptography.Algorithms": "4.3.0", 547 | "System.Security.Cryptography.Encoding": "4.3.0", 548 | "System.Security.Cryptography.OpenSsl": "4.3.0", 549 | "System.Security.Cryptography.Primitives": "4.3.0", 550 | "System.Security.Cryptography.X509Certificates": "4.3.0", 551 | "System.Text.Encoding": "4.3.0", 552 | "System.Threading": "4.3.0", 553 | "System.Threading.Tasks": "4.3.0", 554 | "runtime.native.System": "4.3.0", 555 | "runtime.native.System.Net.Http": "4.3.0", 556 | "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" 557 | } 558 | }, 559 | "System.Net.Primitives": { 560 | "type": "Transitive", 561 | "resolved": "4.3.0", 562 | "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", 563 | "dependencies": { 564 | "Microsoft.NETCore.Platforms": "1.1.0", 565 | "Microsoft.NETCore.Targets": "1.1.0", 566 | "System.Runtime": "4.3.0", 567 | "System.Runtime.Handles": "4.3.0" 568 | } 569 | }, 570 | "System.Net.Sockets": { 571 | "type": "Transitive", 572 | "resolved": "4.3.0", 573 | "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", 574 | "dependencies": { 575 | "Microsoft.NETCore.Platforms": "1.1.0", 576 | "Microsoft.NETCore.Targets": "1.1.0", 577 | "System.IO": "4.3.0", 578 | "System.Net.Primitives": "4.3.0", 579 | "System.Runtime": "4.3.0", 580 | "System.Threading.Tasks": "4.3.0" 581 | } 582 | }, 583 | "System.ObjectModel": { 584 | "type": "Transitive", 585 | "resolved": "4.3.0", 586 | "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", 587 | "dependencies": { 588 | "System.Collections": "4.3.0", 589 | "System.Diagnostics.Debug": "4.3.0", 590 | "System.Resources.ResourceManager": "4.3.0", 591 | "System.Runtime": "4.3.0", 592 | "System.Threading": "4.3.0" 593 | } 594 | }, 595 | "System.Reflection": { 596 | "type": "Transitive", 597 | "resolved": "4.3.0", 598 | "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", 599 | "dependencies": { 600 | "Microsoft.NETCore.Platforms": "1.1.0", 601 | "Microsoft.NETCore.Targets": "1.1.0", 602 | "System.IO": "4.3.0", 603 | "System.Reflection.Primitives": "4.3.0", 604 | "System.Runtime": "4.3.0" 605 | } 606 | }, 607 | "System.Reflection.Emit": { 608 | "type": "Transitive", 609 | "resolved": "4.3.0", 610 | "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", 611 | "dependencies": { 612 | "System.IO": "4.3.0", 613 | "System.Reflection": "4.3.0", 614 | "System.Reflection.Emit.ILGeneration": "4.3.0", 615 | "System.Reflection.Primitives": "4.3.0", 616 | "System.Runtime": "4.3.0" 617 | } 618 | }, 619 | "System.Reflection.Emit.ILGeneration": { 620 | "type": "Transitive", 621 | "resolved": "4.3.0", 622 | "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", 623 | "dependencies": { 624 | "System.Reflection": "4.3.0", 625 | "System.Reflection.Primitives": "4.3.0", 626 | "System.Runtime": "4.3.0" 627 | } 628 | }, 629 | "System.Reflection.Emit.Lightweight": { 630 | "type": "Transitive", 631 | "resolved": "4.3.0", 632 | "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", 633 | "dependencies": { 634 | "System.Reflection": "4.3.0", 635 | "System.Reflection.Emit.ILGeneration": "4.3.0", 636 | "System.Reflection.Primitives": "4.3.0", 637 | "System.Runtime": "4.3.0" 638 | } 639 | }, 640 | "System.Reflection.Extensions": { 641 | "type": "Transitive", 642 | "resolved": "4.3.0", 643 | "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", 644 | "dependencies": { 645 | "Microsoft.NETCore.Platforms": "1.1.0", 646 | "Microsoft.NETCore.Targets": "1.1.0", 647 | "System.Reflection": "4.3.0", 648 | "System.Runtime": "4.3.0" 649 | } 650 | }, 651 | "System.Reflection.Primitives": { 652 | "type": "Transitive", 653 | "resolved": "4.3.0", 654 | "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", 655 | "dependencies": { 656 | "Microsoft.NETCore.Platforms": "1.1.0", 657 | "Microsoft.NETCore.Targets": "1.1.0", 658 | "System.Runtime": "4.3.0" 659 | } 660 | }, 661 | "System.Reflection.TypeExtensions": { 662 | "type": "Transitive", 663 | "resolved": "4.3.0", 664 | "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", 665 | "dependencies": { 666 | "System.Reflection": "4.3.0", 667 | "System.Runtime": "4.3.0" 668 | } 669 | }, 670 | "System.Resources.ResourceManager": { 671 | "type": "Transitive", 672 | "resolved": "4.3.0", 673 | "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", 674 | "dependencies": { 675 | "Microsoft.NETCore.Platforms": "1.1.0", 676 | "Microsoft.NETCore.Targets": "1.1.0", 677 | "System.Globalization": "4.3.0", 678 | "System.Reflection": "4.3.0", 679 | "System.Runtime": "4.3.0" 680 | } 681 | }, 682 | "System.Runtime": { 683 | "type": "Transitive", 684 | "resolved": "4.3.0", 685 | "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", 686 | "dependencies": { 687 | "Microsoft.NETCore.Platforms": "1.1.0", 688 | "Microsoft.NETCore.Targets": "1.1.0" 689 | } 690 | }, 691 | "System.Runtime.Extensions": { 692 | "type": "Transitive", 693 | "resolved": "4.3.0", 694 | "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", 695 | "dependencies": { 696 | "Microsoft.NETCore.Platforms": "1.1.0", 697 | "Microsoft.NETCore.Targets": "1.1.0", 698 | "System.Runtime": "4.3.0" 699 | } 700 | }, 701 | "System.Runtime.Handles": { 702 | "type": "Transitive", 703 | "resolved": "4.3.0", 704 | "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", 705 | "dependencies": { 706 | "Microsoft.NETCore.Platforms": "1.1.0", 707 | "Microsoft.NETCore.Targets": "1.1.0", 708 | "System.Runtime": "4.3.0" 709 | } 710 | }, 711 | "System.Runtime.InteropServices": { 712 | "type": "Transitive", 713 | "resolved": "4.3.0", 714 | "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", 715 | "dependencies": { 716 | "Microsoft.NETCore.Platforms": "1.1.0", 717 | "Microsoft.NETCore.Targets": "1.1.0", 718 | "System.Reflection": "4.3.0", 719 | "System.Reflection.Primitives": "4.3.0", 720 | "System.Runtime": "4.3.0", 721 | "System.Runtime.Handles": "4.3.0" 722 | } 723 | }, 724 | "System.Runtime.InteropServices.RuntimeInformation": { 725 | "type": "Transitive", 726 | "resolved": "4.3.0", 727 | "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", 728 | "dependencies": { 729 | "System.Reflection": "4.3.0", 730 | "System.Reflection.Extensions": "4.3.0", 731 | "System.Resources.ResourceManager": "4.3.0", 732 | "System.Runtime": "4.3.0", 733 | "System.Runtime.InteropServices": "4.3.0", 734 | "System.Threading": "4.3.0", 735 | "runtime.native.System": "4.3.0" 736 | } 737 | }, 738 | "System.Runtime.Numerics": { 739 | "type": "Transitive", 740 | "resolved": "4.3.0", 741 | "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", 742 | "dependencies": { 743 | "System.Globalization": "4.3.0", 744 | "System.Resources.ResourceManager": "4.3.0", 745 | "System.Runtime": "4.3.0", 746 | "System.Runtime.Extensions": "4.3.0" 747 | } 748 | }, 749 | "System.Security.Cryptography.Algorithms": { 750 | "type": "Transitive", 751 | "resolved": "4.3.0", 752 | "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", 753 | "dependencies": { 754 | "Microsoft.NETCore.Platforms": "1.1.0", 755 | "System.Collections": "4.3.0", 756 | "System.IO": "4.3.0", 757 | "System.Resources.ResourceManager": "4.3.0", 758 | "System.Runtime": "4.3.0", 759 | "System.Runtime.Extensions": "4.3.0", 760 | "System.Runtime.Handles": "4.3.0", 761 | "System.Runtime.InteropServices": "4.3.0", 762 | "System.Runtime.Numerics": "4.3.0", 763 | "System.Security.Cryptography.Encoding": "4.3.0", 764 | "System.Security.Cryptography.Primitives": "4.3.0", 765 | "System.Text.Encoding": "4.3.0", 766 | "runtime.native.System.Security.Cryptography.Apple": "4.3.0", 767 | "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" 768 | } 769 | }, 770 | "System.Security.Cryptography.Cng": { 771 | "type": "Transitive", 772 | "resolved": "4.3.0", 773 | "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", 774 | "dependencies": { 775 | "Microsoft.NETCore.Platforms": "1.1.0", 776 | "System.IO": "4.3.0", 777 | "System.Resources.ResourceManager": "4.3.0", 778 | "System.Runtime": "4.3.0", 779 | "System.Runtime.Extensions": "4.3.0", 780 | "System.Runtime.Handles": "4.3.0", 781 | "System.Runtime.InteropServices": "4.3.0", 782 | "System.Security.Cryptography.Algorithms": "4.3.0", 783 | "System.Security.Cryptography.Encoding": "4.3.0", 784 | "System.Security.Cryptography.Primitives": "4.3.0", 785 | "System.Text.Encoding": "4.3.0" 786 | } 787 | }, 788 | "System.Security.Cryptography.Csp": { 789 | "type": "Transitive", 790 | "resolved": "4.3.0", 791 | "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", 792 | "dependencies": { 793 | "Microsoft.NETCore.Platforms": "1.1.0", 794 | "System.IO": "4.3.0", 795 | "System.Reflection": "4.3.0", 796 | "System.Resources.ResourceManager": "4.3.0", 797 | "System.Runtime": "4.3.0", 798 | "System.Runtime.Extensions": "4.3.0", 799 | "System.Runtime.Handles": "4.3.0", 800 | "System.Runtime.InteropServices": "4.3.0", 801 | "System.Security.Cryptography.Algorithms": "4.3.0", 802 | "System.Security.Cryptography.Encoding": "4.3.0", 803 | "System.Security.Cryptography.Primitives": "4.3.0", 804 | "System.Text.Encoding": "4.3.0", 805 | "System.Threading": "4.3.0" 806 | } 807 | }, 808 | "System.Security.Cryptography.Encoding": { 809 | "type": "Transitive", 810 | "resolved": "4.3.0", 811 | "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", 812 | "dependencies": { 813 | "Microsoft.NETCore.Platforms": "1.1.0", 814 | "System.Collections": "4.3.0", 815 | "System.Collections.Concurrent": "4.3.0", 816 | "System.Linq": "4.3.0", 817 | "System.Resources.ResourceManager": "4.3.0", 818 | "System.Runtime": "4.3.0", 819 | "System.Runtime.Extensions": "4.3.0", 820 | "System.Runtime.Handles": "4.3.0", 821 | "System.Runtime.InteropServices": "4.3.0", 822 | "System.Security.Cryptography.Primitives": "4.3.0", 823 | "System.Text.Encoding": "4.3.0", 824 | "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" 825 | } 826 | }, 827 | "System.Security.Cryptography.OpenSsl": { 828 | "type": "Transitive", 829 | "resolved": "4.3.0", 830 | "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", 831 | "dependencies": { 832 | "System.Collections": "4.3.0", 833 | "System.IO": "4.3.0", 834 | "System.Resources.ResourceManager": "4.3.0", 835 | "System.Runtime": "4.3.0", 836 | "System.Runtime.Extensions": "4.3.0", 837 | "System.Runtime.Handles": "4.3.0", 838 | "System.Runtime.InteropServices": "4.3.0", 839 | "System.Runtime.Numerics": "4.3.0", 840 | "System.Security.Cryptography.Algorithms": "4.3.0", 841 | "System.Security.Cryptography.Encoding": "4.3.0", 842 | "System.Security.Cryptography.Primitives": "4.3.0", 843 | "System.Text.Encoding": "4.3.0", 844 | "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" 845 | } 846 | }, 847 | "System.Security.Cryptography.Primitives": { 848 | "type": "Transitive", 849 | "resolved": "4.3.0", 850 | "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", 851 | "dependencies": { 852 | "System.Diagnostics.Debug": "4.3.0", 853 | "System.Globalization": "4.3.0", 854 | "System.IO": "4.3.0", 855 | "System.Resources.ResourceManager": "4.3.0", 856 | "System.Runtime": "4.3.0", 857 | "System.Threading": "4.3.0", 858 | "System.Threading.Tasks": "4.3.0" 859 | } 860 | }, 861 | "System.Security.Cryptography.X509Certificates": { 862 | "type": "Transitive", 863 | "resolved": "4.3.0", 864 | "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", 865 | "dependencies": { 866 | "Microsoft.NETCore.Platforms": "1.1.0", 867 | "System.Collections": "4.3.0", 868 | "System.Diagnostics.Debug": "4.3.0", 869 | "System.Globalization": "4.3.0", 870 | "System.Globalization.Calendars": "4.3.0", 871 | "System.IO": "4.3.0", 872 | "System.IO.FileSystem": "4.3.0", 873 | "System.IO.FileSystem.Primitives": "4.3.0", 874 | "System.Resources.ResourceManager": "4.3.0", 875 | "System.Runtime": "4.3.0", 876 | "System.Runtime.Extensions": "4.3.0", 877 | "System.Runtime.Handles": "4.3.0", 878 | "System.Runtime.InteropServices": "4.3.0", 879 | "System.Runtime.Numerics": "4.3.0", 880 | "System.Security.Cryptography.Algorithms": "4.3.0", 881 | "System.Security.Cryptography.Cng": "4.3.0", 882 | "System.Security.Cryptography.Csp": "4.3.0", 883 | "System.Security.Cryptography.Encoding": "4.3.0", 884 | "System.Security.Cryptography.OpenSsl": "4.3.0", 885 | "System.Security.Cryptography.Primitives": "4.3.0", 886 | "System.Text.Encoding": "4.3.0", 887 | "System.Threading": "4.3.0", 888 | "runtime.native.System": "4.3.0", 889 | "runtime.native.System.Net.Http": "4.3.0", 890 | "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" 891 | } 892 | }, 893 | "System.Text.Encoding": { 894 | "type": "Transitive", 895 | "resolved": "4.3.0", 896 | "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", 897 | "dependencies": { 898 | "Microsoft.NETCore.Platforms": "1.1.0", 899 | "Microsoft.NETCore.Targets": "1.1.0", 900 | "System.Runtime": "4.3.0" 901 | } 902 | }, 903 | "System.Text.Encoding.Extensions": { 904 | "type": "Transitive", 905 | "resolved": "4.3.0", 906 | "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", 907 | "dependencies": { 908 | "Microsoft.NETCore.Platforms": "1.1.0", 909 | "Microsoft.NETCore.Targets": "1.1.0", 910 | "System.Runtime": "4.3.0", 911 | "System.Text.Encoding": "4.3.0" 912 | } 913 | }, 914 | "System.Text.RegularExpressions": { 915 | "type": "Transitive", 916 | "resolved": "4.3.0", 917 | "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", 918 | "dependencies": { 919 | "System.Runtime": "4.3.0" 920 | } 921 | }, 922 | "System.Threading": { 923 | "type": "Transitive", 924 | "resolved": "4.3.0", 925 | "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", 926 | "dependencies": { 927 | "System.Runtime": "4.3.0", 928 | "System.Threading.Tasks": "4.3.0" 929 | } 930 | }, 931 | "System.Threading.Tasks": { 932 | "type": "Transitive", 933 | "resolved": "4.3.0", 934 | "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", 935 | "dependencies": { 936 | "Microsoft.NETCore.Platforms": "1.1.0", 937 | "Microsoft.NETCore.Targets": "1.1.0", 938 | "System.Runtime": "4.3.0" 939 | } 940 | }, 941 | "System.Threading.Tasks.Extensions": { 942 | "type": "Transitive", 943 | "resolved": "4.3.0", 944 | "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", 945 | "dependencies": { 946 | "System.Collections": "4.3.0", 947 | "System.Runtime": "4.3.0", 948 | "System.Threading.Tasks": "4.3.0" 949 | } 950 | }, 951 | "System.Threading.Timer": { 952 | "type": "Transitive", 953 | "resolved": "4.3.0", 954 | "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", 955 | "dependencies": { 956 | "Microsoft.NETCore.Platforms": "1.1.0", 957 | "Microsoft.NETCore.Targets": "1.1.0", 958 | "System.Runtime": "4.3.0" 959 | } 960 | }, 961 | "System.Xml.ReaderWriter": { 962 | "type": "Transitive", 963 | "resolved": "4.3.0", 964 | "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", 965 | "dependencies": { 966 | "System.Collections": "4.3.0", 967 | "System.Diagnostics.Debug": "4.3.0", 968 | "System.Globalization": "4.3.0", 969 | "System.IO": "4.3.0", 970 | "System.IO.FileSystem": "4.3.0", 971 | "System.IO.FileSystem.Primitives": "4.3.0", 972 | "System.Resources.ResourceManager": "4.3.0", 973 | "System.Runtime": "4.3.0", 974 | "System.Runtime.Extensions": "4.3.0", 975 | "System.Runtime.InteropServices": "4.3.0", 976 | "System.Text.Encoding": "4.3.0", 977 | "System.Text.Encoding.Extensions": "4.3.0", 978 | "System.Text.RegularExpressions": "4.3.0", 979 | "System.Threading.Tasks": "4.3.0", 980 | "System.Threading.Tasks.Extensions": "4.3.0" 981 | } 982 | }, 983 | "System.Xml.XDocument": { 984 | "type": "Transitive", 985 | "resolved": "4.3.0", 986 | "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", 987 | "dependencies": { 988 | "System.Collections": "4.3.0", 989 | "System.Diagnostics.Debug": "4.3.0", 990 | "System.Diagnostics.Tools": "4.3.0", 991 | "System.Globalization": "4.3.0", 992 | "System.IO": "4.3.0", 993 | "System.Reflection": "4.3.0", 994 | "System.Resources.ResourceManager": "4.3.0", 995 | "System.Runtime": "4.3.0", 996 | "System.Runtime.Extensions": "4.3.0", 997 | "System.Text.Encoding": "4.3.0", 998 | "System.Threading": "4.3.0", 999 | "System.Xml.ReaderWriter": "4.3.0" 1000 | } 1001 | }, 1002 | "ottergui": { 1003 | "type": "Project", 1004 | "dependencies": { 1005 | "JetBrains.Annotations": "[2024.3.0, )", 1006 | "Microsoft.Extensions.DependencyInjection": "[9.0.2, )" 1007 | } 1008 | }, 1009 | "penumbra.api": { 1010 | "type": "Project" 1011 | }, 1012 | "penumbra.gamedata": { 1013 | "type": "Project", 1014 | "dependencies": { 1015 | "FlatSharp.Compiler": "[7.9.0, )", 1016 | "FlatSharp.Runtime": "[7.9.0, )", 1017 | "OtterGui": "[1.0.0, )", 1018 | "Penumbra.Api": "[5.10.0, )", 1019 | "Penumbra.String": "[1.0.6, )" 1020 | } 1021 | }, 1022 | "penumbra.string": { 1023 | "type": "Project" 1024 | } 1025 | } 1026 | } 1027 | } --------------------------------------------------------------------------------