Add support for .dat files.

This commit is contained in:
Ottermandias 2023-07-15 19:53:05 +02:00
parent edb05946dd
commit 6a94626a13
10 changed files with 416 additions and 14 deletions

View file

@ -141,6 +141,7 @@ public partial class CustomizationOptions
// Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizationSet(race, gender)
{
Voices = row.Voices,
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,

View file

@ -0,0 +1,148 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Memory;
namespace Glamourer.Customization;
[StructLayout(LayoutKind.Explicit, Size = Size)]
public unsafe struct DatCharacterFile
{
public const int Size = 4 + 4 + 4 + 4 + Penumbra.GameData.Structs.CustomizeData.Size + 2 + 4 + 41 * 4; // 212
[FieldOffset(0)]
private fixed byte _data[Size];
[FieldOffset(0)]
public readonly uint Magic = 0x2013FF14;
[FieldOffset(4)]
public readonly uint Version = 0x05;
[FieldOffset(8)]
private uint _checksum;
[FieldOffset(12)]
private readonly uint _padding = 0;
[FieldOffset(16)]
private Penumbra.GameData.Structs.CustomizeData _customize;
[FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size)]
private ushort _voice;
[FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size + 2)]
private uint _timeStamp;
[FieldOffset(Size - 41 * 4)]
private fixed byte _description[41 * 4];
public readonly void Write(Stream stream)
{
for (var i = 0; i < Size; ++i)
stream.WriteByte(_data[i]);
}
public static bool Read(Stream stream, out DatCharacterFile file)
{
if (stream.Length - stream.Position != Size)
{
file = default;
return false;
}
file = new DatCharacterFile(stream);
return true;
}
private DatCharacterFile(Stream stream)
{
for (var i = 0; i < Size; ++i)
_data[i] = (byte)stream.ReadByte();
}
public DatCharacterFile(in Customize customize, byte voice, string text)
{
SetCustomize(customize);
SetVoice(voice);
SetTime(DateTimeOffset.UtcNow);
SetDescription(text);
_checksum = CalculateChecksum();
}
public readonly uint CalculateChecksum()
{
var ret = 0u;
for (var i = 16; i < Size; i++)
ret ^= (uint)(_data[i] << ((i - 16) % 24));
return ret;
}
public readonly uint Checksum
=> _checksum;
public Customize Customize
{
readonly get => new(_customize);
set
{
SetCustomize(value);
_checksum = CalculateChecksum();
}
}
public ushort Voice
{
readonly get => _voice;
set
{
SetVoice(value);
_checksum = CalculateChecksum();
}
}
public string Description
{
readonly get
{
fixed (byte* ptr = _description)
{
return MemoryHelper.ReadStringNullTerminated((nint)ptr);
}
}
set
{
SetDescription(value);
_checksum = CalculateChecksum();
}
}
public DateTimeOffset Time
{
readonly get => DateTimeOffset.FromUnixTimeSeconds(_timeStamp);
set
{
SetTime(value);
_checksum = CalculateChecksum();
}
}
private void SetTime(DateTimeOffset time)
=> _timeStamp = (uint)time.ToUnixTimeSeconds();
private void SetCustomize(in Customize customize)
=> _customize = customize.Data.Clone();
private void SetVoice(ushort voice)
=> _voice = voice;
private void SetDescription(string text)
{
fixed (byte* ptr = _description)
{
var span = new Span<byte>(ptr, 41 * 4);
Encoding.UTF8.GetBytes(text.AsSpan(0, Math.Min(40, text.Length)), span);
}
}
}

View file

@ -33,6 +33,7 @@ public class ActorPanel
private readonly DesignConverter _converter;
private readonly ObjectManager _objects;
private readonly DesignManager _designManager;
private readonly DatFileService _datFileService;
private ActorIdentifier _identifier;
private string _actorName = string.Empty;
@ -42,7 +43,7 @@ public class ActorPanel
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer,
EquipmentDrawer equipmentDrawer, IdentifierService identification, AutoDesignApplier autoDesignApplier,
Configuration config, DesignConverter converter, ObjectManager objects, DesignManager designManager)
Configuration config, DesignConverter converter, ObjectManager objects, DesignManager designManager, DatFileService datFileService)
{
_selector = selector;
_stateManager = stateManager;
@ -54,6 +55,7 @@ public class ActorPanel
_converter = converter;
_objects = objects;
_designManager = designManager;
_datFileService = datFileService;
}
public void Draw()
@ -63,6 +65,13 @@ public class ActorPanel
(_actorName, _actor) = GetHeaderName();
DrawHeader();
DrawPanel();
if (_state is not { IsLocked: false })
return;
if (_datFileService.CreateImGuiTarget(out var dat))
_stateManager.ChangeCustomize(_state!, dat.Customize, CustomizeFlagExtensions.AllRelevant, StateChanged.Source.Manual);
_datFileService.CreateSource();
}
private void DrawHeader()
@ -113,7 +122,7 @@ public class ActorPanel
private void DrawCustomizationsHeader()
{
if (!ImGui.CollapsingHeader("Customizations"))
if (!ImGui.CollapsingHeader("Customization"))
return;
if (_customizationDrawer.Draw(_state!.ModelData.Customize, _state.IsLocked, _identifier.Type is IdentifierType.Special))
@ -372,7 +381,8 @@ public class ActorPanel
? "Apply the current state to your current target."
: "The current target can not be manipulated."
: "No valid target selected.";
if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid || id == _identifier || !_state!.ModelData.IsHuman || _objects.IsInGPose))
if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt,
!data.Valid || id == _identifier || !_state!.ModelData.IsHuman || _objects.IsInGPose))
return;
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))

View file

@ -27,7 +27,7 @@ public sealed class DesignCombo : FilterComboCache<Design>
if (_fileSystem.FindLeaf(Items[globalIdx], out var leaf))
{
var fullName = leaf.FullName();
if (fullName != Items[globalIdx].Name)
if (!fullName.StartsWith(Items[globalIdx].Name))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled));
ImGui.SameLine();

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
@ -46,6 +47,7 @@ public unsafe class DebugTab : ITab
private readonly ObjectManager _objectManager;
private readonly GlamourerIpc _ipc;
private readonly CodeService _code;
private readonly DatFileService _datFileService;
private readonly ItemManager _items;
private readonly ActorService _actors;
@ -74,7 +76,7 @@ public unsafe class DebugTab : ITab
DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config,
PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface,
AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks,
ItemUnlockManager itemUnlocks, DesignConverter designConverter)
ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
@ -100,6 +102,7 @@ public unsafe class DebugTab : ITab
_customizeUnlocks = customizeUnlocks;
_itemUnlocks = itemUnlocks;
_designConverter = designConverter;
_datFileService = datFileService;
}
public ReadOnlySpan<byte> Label
@ -130,6 +133,7 @@ public unsafe class DebugTab : ITab
DrawModelEvaluation();
DrawObjectManager();
DrawDatFiles();
}
private void DrawModelEvaluation()
@ -258,6 +262,34 @@ public unsafe class DebugTab : ITab
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeightWithSpacing());
}
private string _datFilePath = string.Empty;
private DatCharacterFile? _datFile = null;
private void DrawDatFiles()
{
using var tree = ImRaii.TreeNode("Character Dat File");
if (!tree)
return;
ImGui.InputTextWithHint("##datFilePath", "Dat File Path...", ref _datFilePath, 256);
var exists = _datFilePath.Length > 0 && File.Exists(_datFilePath);
if (ImGuiUtil.DrawDisabledButton("Load##Dat", Vector2.Zero, string.Empty, !exists))
_datFile = _datFileService.LoadDesign(_datFilePath, out var tmp) ? tmp : null;
if (ImGuiUtil.DrawDisabledButton("Save##Dat", Vector2.Zero, string.Empty, _datFilePath.Length == 0 || _datFile == null))
_datFileService.SaveDesign(_datFilePath, _datFile!.Value.Customize, _datFile!.Value.Description);
if (_datFile != null)
{
ImGui.TextUnformatted(_datFile.Value.Magic.ToString());
ImGui.TextUnformatted(_datFile.Value.Version.ToString());
ImGui.TextUnformatted(_datFile.Value.Time.LocalDateTime.ToString("g"));
ImGui.TextUnformatted(_datFile.Value.Voice.ToString());
ImGui.TextUnformatted(_datFile.Value.Customize.Data.ToString());
ImGui.TextUnformatted(_datFile.Value.Description);
}
}
private void DrawVisor(Actor actor, Model model)
{
using var id = ImRaii.PushId("Visor");

View file

@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
@ -11,7 +13,6 @@ using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
@ -34,10 +35,12 @@ public class DesignPanel
private readonly ModAssociationsTab _modAssociations;
private readonly DesignDetailTab _designDetails;
private readonly DesignConverter _converter;
private readonly DatFileService _datFileService;
private readonly FileDialogManager _fileDialog = new();
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
StateManager state, EquipmentDrawer equipmentDrawer, CustomizationService customizationService, PenumbraService penumbra,
ModAssociationsTab modAssociations, DesignDetailTab designDetails, DesignConverter converter)
StateManager state, EquipmentDrawer equipmentDrawer, CustomizationService customizationService, ModAssociationsTab modAssociations,
DesignDetailTab designDetails, DesignConverter converter, DatFileService datFileService)
{
_selector = selector;
_customizationDrawer = customizationDrawer;
@ -49,6 +52,7 @@ public class DesignPanel
_modAssociations = modAssociations;
_designDetails = designDetails;
_converter = converter;
_datFileService = datFileService;
}
private HeaderDrawer.Button LockButton()
@ -199,7 +203,7 @@ public class DesignPanel
if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3))
{
var newFlags = flags == 3;
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, newFlags);
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, newFlags);
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, newFlags);
foreach (var index in CustomizationExtensions.AllBasic.Where(set.IsAvailable))
_manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags);
@ -291,10 +295,26 @@ public class DesignPanel
{
using var group = ImRaii.Group();
DrawHeader();
DrawPanel();
var design = _selector.Selected;
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || design == null)
if (_selector.Selected == null || _selector.Selected.WriteProtected())
return;
if (_datFileService.CreateImGuiTarget(out var dat))
{
_manager.ChangeCustomize(_selector.Selected!, CustomizeIndex.Clan, dat.Customize[CustomizeIndex.Clan]);
_manager.ChangeCustomize(_selector.Selected!, CustomizeIndex.Gender, dat.Customize[CustomizeIndex.Gender]);
foreach (var idx in CustomizationExtensions.AllBasic)
_manager.ChangeCustomize(_selector.Selected!, idx, dat.Customize[idx]);
}
_datFileService.CreateSource();
}
private void DrawPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || _selector.Selected == null)
return;
DrawButtonRow();
@ -310,6 +330,8 @@ public class DesignPanel
DrawApplyToSelf();
ImGui.SameLine();
DrawApplyToTarget();
ImGui.SameLine();
DrawSaveToDat();
}
private void SetFromClipboard()
@ -367,6 +389,25 @@ public class DesignPanel
_state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual);
}
private void DrawSaveToDat()
{
var verified = _datFileService.Verify(_selector.Selected!.DesignData.Customize, out var voice);
var tt = verified
? "Export the currently configured customizations of this design to a character creation data file."
: "The current design contains customizations that can not be applied during character creation.";
var startPath = GetUserPath();
if (startPath.Length == 0)
startPath = null;
if (ImGuiUtil.DrawDisabledButton("Export to Dat", Vector2.Zero, tt, !verified))
_fileDialog.SaveFileDialog("Save File...", ".dat", "FFXIV_CHARA_01.dat", ".dat", (v, path) =>
{
if (v && _selector.Selected != null)
_datFileService.SaveDesign(path, _selector.Selected!.DesignData.Customize, _selector.Selected!.Name);
}, startPath);
_fileDialog.Draw();
}
private void ApplyChanges(ActorState.MetaIndex index, DataChange change, bool value, bool apply)
{
switch (change)
@ -383,4 +424,7 @@ public class DesignPanel
break;
}
}
private static unsafe string GetUserPath()
=> Framework.Instance()->UserPath;
}

View file

@ -0,0 +1,163 @@
using System;
using System.IO;
using System.Linq;
using Dalamud.Interface.DragDrop;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Unlocks;
using ImGuiNET;
namespace Glamourer.Interop;
public class DatFileService
{
private readonly CustomizationService _customizations;
private readonly CustomizeUnlockManager _unlocks;
private readonly IDragDropManager _dragDropManager;
public DatFileService(CustomizationService customizations, CustomizeUnlockManager unlocks, IDragDropManager dragDropManager)
{
_customizations = customizations;
_unlocks = unlocks;
_dragDropManager = dragDropManager;
}
public void CreateSource()
{
_dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m =>
{
ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import customizations for Glamourer...");
return true;
});
}
public bool CreateImGuiTarget(out DatCharacterFile file)
{
if (!_dragDropManager.CreateImGuiTarget("DatDragger", out var files, out _) || files.Count != 1)
{
file = default;
return false;
}
return LoadDesign(files[0], out file);
}
public bool LoadDesign(string path, out DatCharacterFile file)
{
if (!File.Exists(path))
{
file = default;
return false;
}
try
{
using var stream = File.OpenRead(path);
if (!DatCharacterFile.Read(stream, out file))
return false;
if (!Verify(file))
return false;
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Could not read character data file {path}.",
$"Could not read character data file {path}", "Failure", NotificationType.Error);
file = default;
}
return true;
}
public bool SaveDesign(string path, in Customize input, string description)
{
if (!Verify(input, out var voice))
return false;
if (description.Length > 40)
return false;
if (path.Length == 0)
return false;
try
{
var file = new DatCharacterFile(input, voice, description);
var directories = Path.GetDirectoryName(path);
if (directories != null)
Directory.CreateDirectory(directories);
using var stream = File.Open(path, File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew);
file.Write(stream);
return true;
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Could not save character data to file {path}.",
$"Could not save character data to file {path}", "Failure", NotificationType.Error);
return false;
}
}
public bool Verify(in Customize input, out byte voice, byte? inputVoice = null)
{
voice = 0;
if (_customizations.ValidateClan(input.Clan, input.Race, out _, out _).Length > 0)
return false;
if (!_customizations.IsGenderValid(input.Race, input.Gender))
return false;
if (input.BodyType.Value != 1)
return false;
var set = _customizations.AwaitedService.GetList(input.Clan, input.Gender);
voice = set.Voices[0];
if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value))
return false;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
switch (index)
{
case CustomizeIndex.Race:
case CustomizeIndex.BodyType:
case CustomizeIndex.Gender:
case CustomizeIndex.Clan:
continue;
case CustomizeIndex.Hairstyle:
case CustomizeIndex.FacePaint:
if (set.DataByValue(index, input[index], out var data, input.Face) < 0
|| data == null
|| _unlocks.Unlockable.ContainsKey(data.Value))
return false;
break;
default:
if (!CustomizationService.IsCustomizationValid(set, input.Face, index, input[index]))
return false;
break;
}
}
if (input[CustomizeIndex.LegacyTattoo].Value != 0)
return false;
return true;
}
public bool Verify(in DatCharacterFile datFile)
{
var customize = datFile.Customize;
if (!Verify(customize, out _, (byte)datFile.Voice))
return false;
if (datFile.Time < DateTimeOffset.UnixEpoch || datFile.Time > DateTimeOffset.UtcNow)
return false;
if (datFile.Checksum != datFile.CalculateChecksum())
return false;
return true;
}
}

View file

@ -277,7 +277,7 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
private static CustomizeFlag FixValues(CustomizationSet set, ref Customize customize)
{
CustomizeFlag flags = 0;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
foreach (var idx in CustomizationExtensions.AllBasic)
{
if (set.IsAvailable(idx))
{

View file

@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Interface.DragDrop;
using Dalamud.IoC;
using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection;
@ -32,6 +33,7 @@ public class DalamudServices
services.AddSingleton(KeyState);
services.AddSingleton(this);
services.AddSingleton(PluginInterface.UiBuilder);
services.AddSingleton(DragDropManager);
}
// @formatter:off
@ -45,5 +47,6 @@ public class DalamudServices
[PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public IDragDropManager DragDropManager { get; private set; } = null!;
// @formatter:on
}

View file

@ -88,7 +88,8 @@ public static class ServiceManager
.AddSingleton<PenumbraAutoRedraw>()
.AddSingleton<JobService>()
.AddSingleton<CustomizeUnlockManager>()
.AddSingleton<ItemUnlockManager>();
.AddSingleton<ItemUnlockManager>()
.AddSingleton<DatFileService>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()