This commit is contained in:
Ottermandias 2023-06-09 17:57:40 +02:00
parent 7710cfadfa
commit 2d6fd6015d
88 changed files with 2304 additions and 383 deletions

View file

@ -5,22 +5,6 @@ public static class Offsets
public static class Character public static class Character
{ {
public const int ClassJobContainer = 0x1A8; public const int ClassJobContainer = 0x1A8;
public const int Wetness = 0x1ADA;
public const int HatVisible = 0x84E;
public const int VisorToggled = 0x84F;
public const int WeaponHidden1 = 0x84F;
public const int WeaponHidden2 = 0x72C;
public const int Alpha = 0x19E0;
public static class Flags
{
public const byte IsHatHidden = 0x01;
public const byte IsVisorToggled = 0x08;
public const byte IsWet = 0x80;
public const byte IsWeaponHidden1 = 0x01;
public const byte IsWeaponHidden2 = 0x02;
}
} }
public const byte DrawObjectVisorStateFlag = 0x40; public const byte DrawObjectVisorStateFlag = 0x40;

View file

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.2.32210.308 VisualStudioVersion = 17.2.32210.308
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{A5439F6B-83C1-4078-9371-354A147FF554}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlamourerOld", "GlamourerOld\GlamourerOld.csproj", "{A5439F6B-83C1-4078-9371-354A147FF554}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\Pen
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "..\Penumbra\OtterGui\OtterGui.csproj", "{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "..\Penumbra\OtterGui\OtterGui.csproj", "{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -39,6 +41,10 @@ Global
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.Build.0 = Release|Any CPU {6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Release|Any CPU.Build.0 = Release|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -0,0 +1,35 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a model flags an equipment slot for an update.
/// <list type="number">
/// <item>Parameter is the model with a flagged slot. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the model values to change the equipment piece to. </item>
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
/// </list>
/// </summary>
public sealed class UpdatedSlot : EventWrapper<Action<Model, EquipSlot, Ref<CharacterArmor>, Ref<ulong>>, UpdatedSlot.Priority>
{
public enum Priority
{ }
public UpdatedSlot()
: base(nameof(UpdatedSlot))
{ }
public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue)
{
var value = new Ref<CharacterArmor>(armor);
var @return = new Ref<ulong>(returnValue);
Invoke(this, model, slot, value, @return);
armor = value;
returnValue = @return;
}
}

View file

@ -0,0 +1,32 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the state of a visor for any draw object is changed.
/// <list type="number">
/// <item>Parameter is the model with a changed visor state. </item>
/// <item>Parameter is the new state. </item>
/// <item>Parameter is whether to call the original function. </item>
/// </list>
/// </summary>
public sealed class VisorStateChanged : EventWrapper<Action<Model, Ref<bool>, Ref<bool>>, VisorStateChanged.Priority>
{
public enum Priority
{ }
public VisorStateChanged()
: base(nameof(VisorStateChanged))
{ }
public void Invoke(Model model, ref bool state, ref bool callOriginal)
{
var value = new Ref<bool>(state);
var original = new Ref<bool>(callOriginal);
Invoke(this, model, value, original);
state = value;
callOriginal = original;
}
}

View file

@ -9,7 +9,7 @@ using OtterGui.Log;
namespace Glamourer; namespace Glamourer;
public partial class Glamourer : IDalamudPlugin public class Item : IDalamudPlugin
{ {
public string Name public string Name
=> "Glamourer"; => "Glamourer";
@ -21,25 +21,21 @@ public partial class Glamourer : IDalamudPlugin
public static readonly Logger Log = new(); public static readonly Logger Log = new();
public static ChatService ChatService { get; private set; } = null!; public static ChatService Chat { get; private set; } = null!;
private readonly ServiceProvider _services; private readonly ServiceProvider _services;
public Glamourer(DalamudPluginInterface pluginInterface) public Item(DalamudPluginInterface pluginInterface)
{ {
try try
{ {
_services = ServiceManager.CreateProvider(pluginInterface, Log); _services = ServiceManager.CreateProvider(pluginInterface, Log);
ChatService = _services.GetRequiredService<ChatService>(); Chat = _services.GetRequiredService<ChatService>();
_services.GetRequiredService<BackupService>(); _services.GetRequiredService<BackupService>(); // call backup service.
_services.GetRequiredService<GlamourerWindowSystem>(); _services.GetRequiredService<GlamourerWindowSystem>(); // initialize ui.
_services.GetRequiredService<CommandService>(); _services.GetRequiredService<CommandService>(); // initialize commands.
_services.GetRequiredService<GlamourerIpc>();
_services.GetRequiredService<ChangeCustomizeService>();
_services.GetRequiredService<JobService>();
_services.GetRequiredService<UpdateSlotService>();
_services.GetRequiredService<VisorService>(); _services.GetRequiredService<VisorService>();
_services.GetRequiredService<WeaponService>();
_services.GetRequiredService<RedrawManager>();
} }
catch catch
{ {
@ -53,137 +49,4 @@ public partial class Glamourer : IDalamudPlugin
{ {
_services?.Dispose(); _services?.Dispose();
} }
//private static GameObject? GetPlayer(string name)
//{
// var lowerName = name.ToLowerInvariant();
// return lowerName switch
// {
// "" => null,
// "<me>" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
// "self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
// "<t>" => Dalamud.Targets.Target,
// "target" => Dalamud.Targets.Target,
// "<f>" => Dalamud.Targets.FocusTarget,
// "focus" => Dalamud.Targets.FocusTarget,
// "<mo>" => Dalamud.Targets.MouseOverTarget,
// "mouseover" => Dalamud.Targets.MouseOverTarget,
// _ => Dalamud.Objects.LastOrDefault(
// a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)),
// };
//}
//
//public void CopyToClipboard(Character player)
//{
// var save = new CharacterSave();
// save.LoadCharacter(player);
// ImGui.SetClipboardText(save.ToBase64());
//}
//
//public void ApplyCommand(Character player, string target)
//{
// CharacterSave? save = null;
// if (target.ToLowerInvariant() == "clipboard")
// try
// {
// save = CharacterSave.FromString(ImGui.GetClipboardText());
// }
// catch (Exception)
// {
// Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string.");
// }
// else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d)
// Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design.");
// else
// save = d.Data;
//
// save?.Apply(player);
// Penumbra.UpdateCharacters(player);
//}
//
//public void SaveCommand(Character player, string path)
//{
// var save = new CharacterSave();
// save.LoadCharacter(player);
// try
// {
// var (folder, name) = Designs.FileSystem.CreateAllFolders(path);
// var design = new Design(folder, name) { Data = save };
// folder.FindOrAddChild(design);
// Designs.Designs.Add(design.FullName(), design.Data);
// Designs.SaveToFile();
// }
// catch (Exception e)
// {
// Dalamud.Chat.PrintError("Could not save file:");
// Dalamud.Chat.PrintError($" {e.Message}");
// }
//}
//
public void OnGlamour(string command, string arguments)
{
//static void PrintHelp()
//{
// Dalamud.Chat.Print("Usage:");
// Dalamud.Chat.Print($" {HelpString}");
//}
//arguments = arguments.Trim();
//if (!arguments.Any())
//{
// PrintHelp();
// return;
//}
//
//var split = arguments.Split(new[]
//{
// ',',
//}, 3, StringSplitOptions.RemoveEmptyEntries);
//
//if (split.Length < 2)
//{
// PrintHelp();
// return;
//}
//
//var player = GetPlayer(split[1]) as Character;
//if (player == null)
//{
// Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character.");
// return;
//}
//
//switch (split[0].ToLowerInvariant())
//{
// case "copy":
// CopyToClipboard(player);
// return;
// case "apply":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'.");
// return;
// }
//
// ApplyCommand(player, split[2]);
//
// return;
// }
// case "save":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Saving requires a name for the save.");
// return;
// }
//
// SaveCommand(player, split[2]);
// return;
// }
// default:
// PrintHelp();
// return;
//}
}
} }

View file

@ -83,6 +83,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Penumbra\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" /> <ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.Api\Penumbra.Api.csproj" /> <ProjectReference Include="..\..\Penumbra\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" /> <ProjectReference Include="..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" />
@ -119,6 +120,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Designs\" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.String.dll $(SolutionDir)$(SolutionName).zip" /> <Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.String.dll $(SolutionDir)$(SolutionName).zip" />
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" /> <Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />

View file

@ -8,9 +8,9 @@ public class GlamourerWindowSystem : IDisposable
{ {
private readonly WindowSystem _windowSystem = new("Glamourer"); private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly UiBuilder _uiBuilder; private readonly UiBuilder _uiBuilder;
private readonly Interface _ui; private readonly MainWindow _ui;
public GlamourerWindowSystem(UiBuilder uiBuilder, Interface ui) public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui)
{ {
_uiBuilder = uiBuilder; _uiBuilder = uiBuilder;
_ui = ui; _ui = ui;
@ -24,7 +24,4 @@ public class GlamourerWindowSystem : IDisposable
_uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.Draw -= _windowSystem.Draw;
_uiBuilder.OpenConfigUi -= _ui.Toggle; _uiBuilder.OpenConfigUi -= _ui.Toggle;
} }
public void Toggle()
=> _ui.Toggle();
} }

View file

@ -0,0 +1,39 @@
using System;
using System.Numerics;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Glamourer.Gui.Tabs;
using ImGuiNET;
using OtterGui.Widgets;
namespace Glamourer.Gui;
public class MainWindow : Window
{
private readonly ITab[] _tabs;
public MainWindow(DalamudPluginInterface pi, DebugTab debugTab)
: base(GetLabel())
{
pi.UiBuilder.DisableGposeUiHide = true;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(675, 675),
MaximumSize = ImGui.GetIO().DisplaySize,
};
_tabs = new ITab[]
{
debugTab,
};
}
public override void Draw()
{
TabBar.Draw("##tabs", ImGuiTabBarFlags.None, ReadOnlySpan<byte>.Empty, out var currentTab, () => { }, _tabs);
}
private static string GetLabel()
=> Item.Version.Length == 0
? "Glamourer###GlamourerMainWindow"
: $"Glamourer v{Item.Version}###GlamourerMainWindow";
}

View file

@ -0,0 +1,491 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs;
public unsafe class DebugTab : ITab
{
private readonly VisorService _visorService;
private readonly ChangeCustomizeService _changeCustomizeService;
private readonly UpdateSlotService _updateSlotService;
private readonly WeaponService _weaponService;
private readonly PenumbraService _penumbra;
private readonly ObjectTable _objects;
private readonly IdentifierService _identifier;
private readonly ActorService _actors;
private readonly ItemService _items;
private readonly CustomizationService _customization;
private int _gameObjectIndex;
public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects,
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier,
ActorService actors, ItemService items, CustomizationService customization)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
_objects = objects;
_updateSlotService = updateSlotService;
_weaponService = weaponService;
_penumbra = penumbra;
_identifier = identifier;
_actors = actors;
_items = items;
_customization = customization;
}
public ReadOnlySpan<byte> Label
=> "Debug"u8;
public void DrawContent()
{
DrawInteropHeader();
DrawGameDataHeader();
DrawPenumbraHeader();
}
#region Interop
private void DrawInteropHeader()
{
if (!ImGui.CollapsingHeader("Interop"))
return;
ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0);
var actor = (Actor)_objects.GetObjectAddress(_gameObjectIndex);
var model = actor.Model;
using var table = ImRaii.Table("##interopTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableHeader("Actor");
ImGui.TableNextColumn();
ImGui.TableHeader("Model");
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Address");
ImGui.TableNextColumn();
if (ImGui.Selectable($"0x{model.Address:X}"))
ImGui.SetClipboardText($"0x{model.Address:X}");
ImGui.TableNextColumn();
if (ImGui.Selectable($"0x{model.Address:X}"))
ImGui.SetClipboardText($"0x{model.Address:X}");
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Mainhand");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetMainhand().ToString() : "No Character");
ImGui.TableNextColumn();
var weapon = model.AsDrawObject->Object.ChildObject;
if (ImGui.Selectable($"0x{(ulong)weapon:X}"))
ImGui.SetClipboardText($"0x{(ulong)weapon:X}");
ImGuiUtil.DrawTableColumn("Offhand");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetOffhand().ToString() : "No Character");
if (weapon != null && ImGui.Selectable($"0x{(ulong)weapon->NextSiblingObject:X}"))
ImGui.SetClipboardText($"0x{(ulong)weapon->NextSiblingObject:X}");
DrawVisor(actor, model);
DrawHatState(actor, model);
DrawWeaponState(actor, model);
DrawWetness(actor, model);
DrawEquip(actor, model);
DrawCustomize(actor, model);
}
private void DrawVisor(Actor actor, Model model)
{
using var id = ImRaii.PushId("Visor");
ImGuiUtil.DrawTableColumn("Visor State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman ? _visorService.GetVisorState(model).ToString() : "No Human");
ImGui.TableNextColumn();
if (!model.IsHuman)
return;
if (ImGui.SmallButton("Set True"))
_visorService.SetVisorState(model, true);
ImGui.SameLine();
if (ImGui.SmallButton("Set False"))
_visorService.SetVisorState(model, false);
ImGui.SameLine();
if (ImGui.SmallButton("Toggle"))
_visorService.SetVisorState(model, !_visorService.GetVisorState(model));
}
private void DrawHatState(Actor actor, Model model)
{
using var id = ImRaii.PushId("HatState");
ImGuiUtil.DrawTableColumn("Hat State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter
? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString()
: "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman
? model.AsHuman->Head.Value == 0 ? "No Hat" : model.GetArmor(EquipSlot.Head).ToString()
: "No Human");
ImGui.TableNextColumn();
if (!model.IsHuman)
return;
if (ImGui.SmallButton("Hide"))
_updateSlotService.UpdateSlot(model, EquipSlot.Head, CharacterArmor.Empty);
ImGui.SameLine();
if (ImGui.SmallButton("Show"))
_updateSlotService.UpdateSlot(model, EquipSlot.Head, actor.GetArmor(EquipSlot.Head));
ImGui.SameLine();
if (ImGui.SmallButton("Toggle"))
_updateSlotService.UpdateSlot(model, EquipSlot.Head,
model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty);
}
private void DrawWeaponState(Actor actor, Model model)
{
using var id = ImRaii.PushId("WeaponState");
ImGuiUtil.DrawTableColumn("Weapon State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter
? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible"
: "No Character");
var text = string.Empty;
// TODO
if (!model.IsHuman)
{
text = "No Model";
}
else if (model.AsDrawObject->Object.ChildObject == null)
{
text = "No Weapon";
}
else
{
var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject;
if ((weapon->Flags & 0x09) == 0x09)
text = "Visible";
else
text = "Hidden";
}
ImGuiUtil.DrawTableColumn(text);
ImGui.TableNextColumn();
if (!model.IsHuman)
return;
}
private void DrawWetness(Actor actor, Model model)
{
using var id = ImRaii.PushId("Wetness");
ImGuiUtil.DrawTableColumn("Wetness");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character");
var modelString = model.IsCharacterBase
? $"{model.AsCharacterBase->SwimmingWetness:F4} Swimming\n"
+ $"{model.AsCharacterBase->WeatherWetness:F4} Weather\n"
+ $"{model.AsCharacterBase->ForcedWetness:F4} Forced\n"
+ $"{model.AsCharacterBase->WetnessDepth:F4} Depth\n"
: "No CharacterBase";
ImGuiUtil.DrawTableColumn(modelString);
ImGui.TableNextColumn();
if (!actor.IsCharacter)
return;
if (ImGui.SmallButton("GPose On"))
actor.AsCharacter->IsGPoseWet = true;
ImGui.SameLine();
if (ImGui.SmallButton("GPose Off"))
actor.AsCharacter->IsGPoseWet = false;
ImGui.SameLine();
if (ImGui.SmallButton("GPose Toggle"))
actor.AsCharacter->IsGPoseWet = !actor.AsCharacter->IsGPoseWet;
}
private void DrawEquip(Actor actor, Model model)
{
using var id = ImRaii.PushId("Equipment");
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
using var id2 = ImRaii.PushId((int)slot);
ImGuiUtil.DrawTableColumn(slot.ToName());
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human");
ImGui.TableNextColumn();
if (!model.IsHuman)
continue;
if (ImGui.SmallButton("Change Piece"))
_updateSlotService.UpdateArmor(model, slot,
new CharacterArmor((SetId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0));
ImGui.SameLine();
if (ImGui.SmallButton("Change Stain"))
_updateSlotService.UpdateStain(model, slot, 5);
ImGui.SameLine();
if (ImGui.SmallButton("Reset"))
_updateSlotService.UpdateSlot(model, slot, actor.GetArmor(slot));
}
}
private void DrawCustomize(Actor actor, Model model)
{
using var id = ImRaii.PushId("Customize");
var actorCustomize = new Customize(actor.IsCharacter
? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData
: new Penumbra.GameData.Structs.CustomizeData());
var modelCustomize = new Customize(model.IsHuman
? *(Penumbra.GameData.Structs.CustomizeData*)model.AsHuman->CustomizeData
: new Penumbra.GameData.Structs.CustomizeData());
foreach (var type in Enum.GetValues<CustomizeIndex>())
{
using var id2 = ImRaii.PushId((int)type);
ImGuiUtil.DrawTableColumn(type.ToDefaultName());
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human");
ImGui.TableNextColumn();
if (!model.IsHuman || type.ToFlag().RequiresRedraw())
continue;
if (ImGui.SmallButton("++"))
{
modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value + 1));
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
}
ImGui.SameLine();
if (ImGui.SmallButton("--"))
{
modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value - 1));
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
}
ImGui.SameLine();
if (ImGui.SmallButton("Reset"))
{
modelCustomize.Set(type, actorCustomize[type]);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
}
}
}
#endregion
#region Penumbra
private Model _drawObject = Model.Null;
private void DrawPenumbraHeader()
{
if (!ImGui.CollapsingHeader("Penumbra"))
return;
using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
ImGuiUtil.DrawTableColumn("Available");
ImGuiUtil.DrawTableColumn(_penumbra.Available.ToString());
ImGui.TableNextColumn();
if (ImGui.SmallButton("Unattach"))
_penumbra.Unattach();
ImGui.SameLine();
if (ImGui.SmallButton("Reattach"))
_penumbra.Reattach();
ImGuiUtil.DrawTableColumn("Draw Object");
ImGui.TableNextColumn();
var address = _drawObject.Address;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
if (ImGui.InputScalar("##drawObjectPtr", ImGuiDataType.U64, (nint)(&address), IntPtr.Zero, IntPtr.Zero, "%llx",
ImGuiInputTextFlags.CharsHexadecimal))
_drawObject = address;
ImGuiUtil.DrawTableColumn(_penumbra.Available
? $"0x{_penumbra.GameObjectFromDrawObject(_drawObject).Address:X}"
: "Penumbra Unavailable");
ImGuiUtil.DrawTableColumn("Cutscene Object");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0);
ImGuiUtil.DrawTableColumn(_penumbra.Available
? _penumbra.CutsceneParent(_gameObjectIndex).ToString()
: "Penumbra Unavailable");
ImGuiUtil.DrawTableColumn("Redraw Object");
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0);
ImGui.TableNextColumn();
using (var disabled = ImRaii.Disabled(!_penumbra.Available))
{
if (ImGui.SmallButton("Redraw"))
_penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw);
}
}
#endregion
#region GameData
private void DrawGameDataHeader()
{
if (!ImGui.CollapsingHeader("Game Data"))
return;
DrawIdentifierService();
DrawActorService();
DrawItemService();
DrawCustomizationService();
}
private string _gamePath = string.Empty;
private int _setId;
private int _secondaryId;
private int _variant;
private void DrawIdentifierService()
{
using var disabled = ImRaii.Disabled(!_identifier.Valid);
using var tree = ImRaii.TreeNode("Identifier Service");
if (!tree || !_identifier.Valid)
return;
disabled.Dispose();
static void Text(string text)
{
if (text.Length > 0)
ImGui.TextUnformatted(text);
}
ImGui.TextUnformatted("Parse Game Path");
ImGui.SameLine();
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
ImGui.InputTextWithHint("##gamePath", "Enter game path...", ref _gamePath, 256);
var fileInfo = _identifier.AwaitedService.GamePathParser.GetFileInfo(_gamePath);
ImGui.TextUnformatted(
$"{fileInfo.ObjectType} {fileInfo.EquipSlot} {fileInfo.PrimaryId} {fileInfo.SecondaryId} {fileInfo.Variant} {fileInfo.BodySlot} {fileInfo.CustomizationType}");
Text(string.Join("\n", _identifier.AwaitedService.Identify(_gamePath).Keys));
ImGui.Separator();
ImGui.TextUnformatted("Identify Model");
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##SetId", ref _setId, 0, 0);
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##TypeId", ref _secondaryId, 0, 0);
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##Variant", ref _variant, 0, 0);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var identified = _identifier.AwaitedService.Identify((SetId)_setId, (ushort)_variant, slot);
Text(string.Join("\n", identified.Select(i => i.Name.ToDalamudString().TextValue)));
}
var main = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.MainHand);
Text(string.Join("\n", main.Select(i => i.Name.ToDalamudString().TextValue)));
var off = _identifier.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (ushort)_variant, EquipSlot.OffHand);
Text(string.Join("\n", off.Select(i => i.Name.ToDalamudString().TextValue)));
}
private string _bnpcFilter = string.Empty;
private string _enpcFilter = string.Empty;
private string _companionFilter = string.Empty;
private string _mountFilter = string.Empty;
private string _ornamentFilter = string.Empty;
private string _worldFilter = string.Empty;
private void DrawActorService()
{
using var disabled = ImRaii.Disabled(!_actors.Valid);
using var tree = ImRaii.TreeNode("Actor Service");
if (!tree || !_actors.Valid)
return;
disabled.Dispose();
DrawNameTable("BNPCs", ref _bnpcFilter, _actors.AwaitedService.Data.BNpcs.Select(kvp => (kvp.Key, kvp.Value)));
DrawNameTable("ENPCs", ref _enpcFilter, _actors.AwaitedService.Data.ENpcs.Select(kvp => (kvp.Key, kvp.Value)));
DrawNameTable("Companions", ref _companionFilter, _actors.AwaitedService.Data.Companions.Select(kvp => (kvp.Key, kvp.Value)));
DrawNameTable("Mounts", ref _mountFilter, _actors.AwaitedService.Data.Mounts.Select(kvp => (kvp.Key, kvp.Value)));
DrawNameTable("Ornaments", ref _ornamentFilter, _actors.AwaitedService.Data.Ornaments.Select(kvp => (kvp.Key, kvp.Value)));
DrawNameTable("Worlds", ref _worldFilter, _actors.AwaitedService.Data.Worlds.Select(kvp => ((uint)kvp.Key, kvp.Value)));
}
private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names)
{
using var _ = ImRaii.PushId(label);
using var tree = ImRaii.TreeNode(label);
if (!tree)
return;
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256);
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
new Vector2(-1, 10 * height));
if (!table)
return;
if (resetScroll)
ImGui.SetScrollY(0);
ImGui.TableSetupColumn("1", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(height);
ImGui.TableNextColumn();
var f = filter;
var remainder = ImGuiClip.FilteredClippedDraw(names.Select(p => (p.Item1.ToString("D5"), p.Item2)), skips,
p => p.Item1.Contains(f) || p.Item2.Contains(f, StringComparison.OrdinalIgnoreCase),
p =>
{
ImGuiUtil.DrawTableColumn(p.Item1);
ImGuiUtil.DrawTableColumn(p.Item2);
});
ImGuiClip.DrawEndDummy(remainder, height);
}
private void DrawItemService()
{
using var disabled = ImRaii.Disabled(!_items.Valid);
using var tree = ImRaii.TreeNode("Item Manager");
if (!tree || !_items.Valid)
return;
disabled.Dispose();
}
private void DrawCustomizationService()
{
using var id = ImRaii.PushId("Customization");
ImGuiUtil.DrawTableColumn("Customization Service");
ImGui.TableNextColumn();
if (!_customization.Valid)
{
ImGui.TextUnformatted("Unavailable");
ImGui.TableNextColumn();
return;
}
using var tree = ImRaii.TreeNode("Available###Customization", ImGuiTreeNodeFlags.NoTreePushOnOpen);
ImGui.TableNextColumn();
if (!tree)
return;
}
#endregion
}

View file

@ -1,24 +1,34 @@
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Interop; namespace Glamourer.Interop;
/// <summary>
/// Access the function the game uses to update customize data on the character screen.
/// Changes in Race, body type or Gender are probably ignored.
/// This operates on draw objects, not game objects.
/// </summary>
public unsafe class ChangeCustomizeService public unsafe class ChangeCustomizeService
{ {
public ChangeCustomizeService() public ChangeCustomizeService()
=> SignatureHelper.Initialise(this); => SignatureHelper.Initialise(this);
public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment); private delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature(Sigs.ChangeCustomize)] [Signature(Sigs.ChangeCustomize)]
private readonly ChangeCustomizeDelegate _changeCustomize = null!; private readonly ChangeCustomizeDelegate _changeCustomize = null!;
public bool UpdateCustomize(Actor actor, CustomizeData customize) public bool UpdateCustomize(Model model, CustomizeData customize)
{ {
if (customize.Data == null || !actor.Valid || !actor.DrawObject.Valid) if (!model.IsHuman)
return false; return false;
return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1); Item.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
return _changeCustomize(model.AsHuman, customize.Data, 1);
} }
public bool UpdateCustomize(Actor actor, CustomizeData customize)
=> UpdateCustomize(actor.Model, customize);
} }

View file

@ -0,0 +1,142 @@
using System;
using Dalamud.Logging;
using Dalamud.Plugin;
using Glamourer.Interop.Structs;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
namespace Glamourer.Interop.Penumbra;
public unsafe class PenumbraService : IDisposable
{
public const int RequiredPenumbraBreakingVersion = 4;
public const int RequiredPenumbraFeatureVersion = 15;
private readonly DalamudPluginInterface _pluginInterface;
private readonly EventSubscriber<ChangedItemType, uint> _tooltipSubscriber;
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
private ActionSubscriber<int, RedrawType> _redrawSubscriber;
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
private FuncSubscriber<int, int> _cutsceneParent;
private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent;
public bool Available { get; private set; }
public PenumbraService(DalamudPluginInterface pi)
{
_pluginInterface = pi;
_initializedEvent = Ipc.Initialized.Subscriber(pi, Reattach);
_disposedEvent = Ipc.Disposed.Subscriber(pi, Unattach);
_tooltipSubscriber = Ipc.ChangedItemTooltip.Subscriber(pi);
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
Reattach();
}
public event Action<MouseButton, ChangedItemType, uint> Click
{
add => _clickSubscriber.Event += value;
remove => _clickSubscriber.Event -= value;
}
public event Action<ChangedItemType, uint> Tooltip
{
add => _tooltipSubscriber.Event += value;
remove => _tooltipSubscriber.Event -= value;
}
public event Action<nint, string, nint, nint, nint> CreatingCharacterBase
{
add => _creatingCharacterBase.Event += value;
remove => _creatingCharacterBase.Event -= value;
}
public event Action<nint, string, nint> CreatedCharacterBase
{
add => _createdCharacterBase.Event += value;
remove => _createdCharacterBase.Event -= value;
}
/// <summary> Obtain the game object corresponding to a draw object. </summary>
public Actor GameObjectFromDrawObject(Model drawObject)
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
/// <summary> Obtain the parent of a cutscene actor if it is known. </summary>
public int CutsceneParent(int idx)
=> Available ? _cutsceneParent.Invoke(idx) : -1;
/// <summary> Try to redraw the given actor. </summary>
public void RedrawObject(Actor actor, RedrawType settings)
{
if (!actor || !Available)
return;
try
{
_redrawSubscriber.Invoke(actor.AsObject->ObjectIndex, settings);
}
catch (Exception e)
{
PluginLog.Debug($"Failure redrawing object:\n{e}");
}
}
/// <summary> Reattach to the currently running Penumbra IPC provider. Unattaches before if necessary. </summary>
public void Reattach()
{
try
{
Unattach();
var (breaking, feature) = Ipc.ApiVersions.Subscriber(_pluginInterface).Invoke();
if (breaking != RequiredPenumbraBreakingVersion || feature < RequiredPenumbraFeatureVersion)
throw new Exception(
$"Invalid Version {breaking}.{feature:D4}, required major Version {RequiredPenumbraBreakingVersion} with feature greater or equal to {RequiredPenumbraFeatureVersion}.");
_tooltipSubscriber.Enable();
_clickSubscriber.Enable();
_creatingCharacterBase.Enable();
_createdCharacterBase.Enable();
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
Available = true;
Item.Log.Debug("Glamourer attached to Penumbra.");
}
catch (Exception e)
{
Item.Log.Debug($"Could not attach to Penumbra:\n{e}");
}
}
/// <summary> Unattach from the currently running Penumbra IPC provider. </summary>
public void Unattach()
{
_tooltipSubscriber.Disable();
_clickSubscriber.Disable();
_creatingCharacterBase.Disable();
_createdCharacterBase.Disable();
if (Available)
{
Available = false;
Item.Log.Debug("Glamourer detached from Penumbra.");
}
}
public void Dispose()
{
Unattach();
_tooltipSubscriber.Dispose();
_clickSubscriber.Dispose();
_creatingCharacterBase.Dispose();
_createdCharacterBase.Dispose();
_initializedEvent.Dispose();
_disposedEvent.Dispose();
}
}

View file

@ -0,0 +1,97 @@
using Penumbra.GameData.Actors;
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop.Structs;
public readonly unsafe struct Actor : IEquatable<Actor>
{
private Actor(nint address)
=> Address = address;
public static readonly Actor Null = new(nint.Zero);
public readonly nint Address;
public GameObject* AsObject
=> (GameObject*)Address;
public Character* AsCharacter
=> (Character*)Address;
public bool Valid
=> Address != nint.Zero;
public bool IsCharacter
=> Valid && AsObject->IsCharacter();
public static implicit operator Actor(nint? pointer)
=> new(pointer ?? nint.Zero);
public static implicit operator Actor(GameObject* pointer)
=> new((nint)pointer);
public static implicit operator Actor(Character* pointer)
=> new((nint)pointer);
public static implicit operator nint(Actor actor)
=> actor.Address;
public ActorIdentifier GetIdentifier(ActorManager actors)
=> actors.FromObject(AsObject, out _, true, true, false);
public bool Identifier(ActorManager actors, out ActorIdentifier ident)
{
if (Valid)
{
ident = GetIdentifier(actors);
return ident.IsValid;
}
ident = ActorIdentifier.Invalid;
return false;
}
public Model Model
=> Valid ? AsObject->DrawObject : null;
public static implicit operator bool(Actor actor)
=> actor.Address != nint.Zero;
public static bool operator true(Actor actor)
=> actor.Address != nint.Zero;
public static bool operator false(Actor actor)
=> actor.Address == nint.Zero;
public static bool operator !(Actor actor)
=> actor.Address == nint.Zero;
public bool Equals(Actor other)
=> Address == other.Address;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> Address.GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Address == rhs.Address;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Address != rhs.Address;
/// <summary> Only valid for characters. </summary>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()];
public CharacterWeapon GetMainhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.MainHandModel;
public CharacterWeapon GetOffhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel;
}

View file

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace Glamourer.Interop.Structs;
public readonly struct ActorData
{
public readonly List<Actor> Objects;
public readonly string Label;
public bool Valid
=> Objects.Count > 0;
public ActorData(Actor actor, string label)
{
Objects = new List<Actor> { actor };
Label = label;
}
public static readonly ActorData Invalid = new(false);
private ActorData(bool _)
{
Objects = new List<Actor>(0);
Label = string.Empty;
}
}

View file

@ -0,0 +1,92 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
namespace Glamourer.Interop.Structs;
public readonly unsafe struct Model : IEquatable<Model>
{
private Model(nint address)
=> Address = address;
public readonly nint Address;
public static readonly Model Null = new(0);
public DrawObject* AsDrawObject
=> (DrawObject*)Address;
public CharacterBase* AsCharacterBase
=> (CharacterBase*)Address;
public Human* AsHuman
=> (Human*)Address;
public static implicit operator Model(nint? pointer)
=> new(pointer ?? nint.Zero);
public static implicit operator Model(DrawObject* pointer)
=> new((nint)pointer);
public static implicit operator Model(Human* pointer)
=> new((nint)pointer);
public static implicit operator Model(CharacterBase* pointer)
=> new((nint)pointer);
public static implicit operator nint(Model model)
=> model.Address;
public bool Valid
=> Address != nint.Zero;
public bool IsCharacterBase
=> Valid && AsDrawObject->Object.GetObjectType() == ObjectType.CharacterBase;
public bool IsHuman
=> IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human;
public static implicit operator bool(Model actor)
=> actor.Address != nint.Zero;
public static bool operator true(Model actor)
=> actor.Address != nint.Zero;
public static bool operator false(Model actor)
=> actor.Address == nint.Zero;
public static bool operator !(Model actor)
=> actor.Address == nint.Zero;
public bool Equals(Model other)
=> Address == other.Address;
public override bool Equals(object? obj)
=> obj is Model other && Equals(other);
public override int GetHashCode()
=> Address.GetHashCode();
public static bool operator ==(Model lhs, Model rhs)
=> lhs.Address == rhs.Address;
public static bool operator !=(Model lhs, Model rhs)
=> lhs.Address != rhs.Address;
/// <summary> Only valid for humans. </summary>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()];
public CharacterWeapon GetMainhand()
{
var weapon = AsDrawObject->Object.ChildObject;
if (weapon == null)
return CharacterWeapon.Empty;
weapon
}
public CharacterWeapon GetOffhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel;
}

View file

@ -1,7 +1,8 @@
using System; using System;
using System.Linq;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -9,8 +10,11 @@ namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable public unsafe class UpdateSlotService : IDisposable
{ {
public UpdateSlotService() public readonly UpdatedSlot Event;
public UpdateSlotService(UpdatedSlot updatedSlot)
{ {
Event = updatedSlot;
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable(); _flagSlotForUpdateHook.Enable();
} }
@ -19,59 +23,41 @@ public unsafe class UpdateSlotService : IDisposable
=> _flagSlotForUpdateHook.Dispose(); => _flagSlotForUpdateHook.Dispose();
private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data); private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
public delegate void FlagSlotForUpdateDelegate(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item);
// This gets called when one of the ten equip items of an existing draw object gets changed.
[Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))] [Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!; private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!;
public event FlagSlotForUpdateDelegate? EquipUpdate; public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
public ulong FlagSlotForUpdateInterop(DrawObject drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
{ {
InvokeFlagSlotEvent(drawObject, slot, ref data); if (!drawObject.IsCharacterBase)
return;
FlagSlotForUpdateInterop(drawObject, slot, data); FlagSlotForUpdateInterop(drawObject, slot, data);
} }
public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain) public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data)
{ {
var armor = drawObject.Equip[slot] with { Stain = stain }; if (!drawObject.IsCharacterBase)
UpdateSlot(drawObject, slot, armor); return;
FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain));
}
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
{
if (!drawObject.IsHuman)
return;
FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain));
} }
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{ {
var slot = slotIdx.ToEquipSlot(); var slot = slotIdx.ToEquipSlot();
InvokeFlagSlotEvent(drawObject, slot, ref *data); var returnValue = ulong.MaxValue;
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data); Event.Invoke(drawObject, slot, ref *data, ref returnValue);
return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
} }
private void InvokeFlagSlotEvent(DrawObject drawObject, EquipSlot slot, ref CharacterArmor armor) private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
{ => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
if (EquipUpdate == null)
{
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}.");
return;
}
var iv = armor;
foreach (var del in EquipUpdate.GetInvocationList().OfType<FlagSlotForUpdateDelegate>())
{
try
{
del(drawObject, slot, ref armor);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(EquipUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}, initial armor was {iv.Set.Value}-{iv.Variant} with stain {iv.Stain.Value}.");
}
} }

View file

@ -1,16 +1,19 @@
using System; using System;
using System.Linq; using System.Runtime.CompilerServices;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Events;
using Penumbra.GameData.Structs; using Glamourer.Interop.Structs;
namespace Glamourer.Interop; namespace Glamourer.Interop;
public class VisorService : IDisposable public class VisorService : IDisposable
{ {
public VisorService() public readonly VisorStateChanged Event;
public VisorService(VisorStateChanged visorStateChanged)
{ {
Event = visorStateChanged;
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_setupVisorHook.Enable(); _setupVisorHook.Enable();
} }
@ -18,61 +21,67 @@ public class VisorService : IDisposable
public void Dispose() public void Dispose()
=> _setupVisorHook.Dispose(); => _setupVisorHook.Dispose();
public static unsafe bool GetVisorState(nint humanPtr) /// <summary> Obtain the current state of the Visor for the given draw object (true: toggled). </summary>
public unsafe bool GetVisorState(Model characterBase)
{ {
if (humanPtr == IntPtr.Zero) if (!characterBase.IsCharacterBase)
return false; return false;
var data = (Human*)humanPtr; // TODO: use client structs.
var flags = &data->CharacterBase.UnkFlags_01; return (characterBase.AsCharacterBase->UnkFlags_01 & Offsets.DrawObjectVisorStateFlag) != 0;
return (*flags & Offsets.DrawObjectVisorStateFlag) != 0;
} }
public unsafe void SetVisorState(nint humanPtr, bool on) /// <summary> Manually set the state of the Visor for the given draw object. </summary>
/// <param name="human"> The draw object. </param>
/// <param name="on"> The desired state (true: toggled). </param>
/// <returns> Whether the state was changed. </returns>
public unsafe bool SetVisorState(Model human, bool on)
{ {
if (humanPtr == IntPtr.Zero) if (!human.IsHuman)
return; return false;
var data = (Human*)humanPtr; var oldState = GetVisorState(human);
_setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on); Item.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}.");
if (oldState == on)
return false;
SetupVisorHook(human, (ushort)human.AsHuman->HeadSetID, on);
return true;
} }
private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on); private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on);
public delegate void UpdateVisorDelegate(DrawObject human, SetId modelId, ref bool on);
[Signature(Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))] [Signature(global::Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
private readonly Hook<UpdateVisorDelegateInternal> _setupVisorHook = null!; private readonly Hook<UpdateVisorDelegateInternal> _setupVisorHook = null!;
public event UpdateVisorDelegate? VisorUpdate; private void SetupVisorDetour(nint human, ushort modelId, bool on)
private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on)
{ {
InvokeVisorEvent(humanPtr, modelId, ref on); var callOriginal = true;
_setupVisorHook.Original(humanPtr, modelId, on); var originalOn = on;
// Invoke an event that can change the requested value
// and also control whether the function should be called at all.
Event.Invoke(human, ref on, ref callOriginal);
Item.Log.Excessive(
$"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn}, call original {callOriginal}).");
if (callOriginal)
SetupVisorHook(human, modelId, on);
} }
private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on) /// <summary>
/// The SetupVisor function does not set the visor state for the draw object itself,
/// it only sets the "visor is changing" state to false.
/// So we wrap a manual change of that flag with the function call.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private unsafe void SetupVisorHook(Model human, ushort modelId, bool on)
{ {
if (VisorUpdate == null) // TODO: use client structs.
{ human.AsCharacterBase->UnkFlags_01 = (byte)(on
Glamourer.Log.Excessive($"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}."); ? human.AsCharacterBase->UnkFlags_01 | Offsets.DrawObjectVisorStateFlag
return; : human.AsCharacterBase->UnkFlags_01 & ~Offsets.DrawObjectVisorStateFlag);
} _setupVisorHook.Original(human.Address, modelId, on);
var initialValue = on;
foreach (var del in VisorUpdate.GetInvocationList().OfType<UpdateVisorDelegate>())
{
try
{
del(drawObject, modelId, ref on);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(VisorUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}.");
} }
} }

View file

@ -1,8 +1,8 @@
using System; using System;
using System.Runtime.InteropServices;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
@ -13,6 +13,7 @@ public unsafe class WeaponService : IDisposable
public WeaponService() public WeaponService()
{ {
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_loadWeaponHook.Enable(); _loadWeaponHook.Enable();
} }
@ -21,68 +22,18 @@ public unsafe class WeaponService : IDisposable
_loadWeaponHook.Dispose(); _loadWeaponHook.Dispose();
} }
public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf<Character>("DrawData"); private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, byte unk4);
public delegate void LoadWeaponDelegate(nint offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, private readonly Hook<LoadWeaponDelegate> _loadWeaponHook;
byte skipGameObject,
byte unk4);
// Weapons for a specific character are reloaded with this function. private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
// The first argument is a pointer to the game object but shifted a bit inside.
// slot is 0 for main hand, 1 for offhand, 2 for unknown (always called with empty data.
// weapon argument is the new weapon data.
// redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one.
// skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change)
// unk4 seemed to be the same as unk1.
[Signature(Penumbra.GameData.Sigs.WeaponReload, DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook = null!;
private void LoadWeaponDetour(nint characterOffset, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
byte unk4) byte unk4)
{ {
//var oldWeapon = weapon; var actor = (Actor) (nint)drawData->Unk8;
//var character = (Actor)(characterOffset - CharacterWeaponOffset);
//try
//{
// var identifier = character.GetIdentifier(_actors.AwaitedService);
// if (_fixedDesignManager.TryGetDesign(identifier, out var save))
// {
// PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
// weapon = slot switch
// {
// 0 => save.WeaponMain.Model.Value,
// 1 => save.WeaponOff.Model.Value,
// _ => weapon,
// };
// }
// else if (redrawOnEquality == 1 && _stateManager.TryGetValue(identifier, out var save2))
// {
// PluginLog.Information($"Loaded weapon from current design for {identifier}.");
// //switch (slot)
// //{
// // case 0:
// // save2.MainHand = new CharacterWeapon(weapon);
// // break;
// // case 1:
// // save2.Data.OffHand = new CharacterWeapon(weapon);
// // break;
// //}
// }
//}
//catch (Exception e)
//{
// PluginLog.Error($"Error on loading new weapon:\n{e}");
//}
// First call the regular function. // First call the regular function.
_loadWeaponHook.Original(characterOffset, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4); _loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
Glamourer.Log.Excessive($"Weapon reloaded for {(Actor)(characterOffset - CharacterWeaponOffset)} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}"); Item.Log.Information($"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
// // If something changed the weapon, call it again with the actual change, not forcing redraws and skipping applying it to the game object.
// if (oldWeapon != weapon)
// _loadWeaponHook.Original(characterOffset, slot, weapon, 0 /* redraw */, unk2, 1 /* skip */, unk4);
// // If we're not actively changing the offhand and the game object has no offhand, redraw an empty offhand to fix animation problems.
// else if (slot != 1 && character.OffHand.Value == 0)
// _loadWeaponHook.Original(characterOffset, 1, 0, 1 /* redraw */, unk2, 1 /* skip */, unk4);
} }
// Load a specific weapon for a character by its data and slot. // Load a specific weapon for a character by its data and slot.
@ -91,14 +42,14 @@ public unsafe class WeaponService : IDisposable
switch (slot) switch (slot)
{ {
case EquipSlot.MainHand: case EquipSlot.MainHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0); LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
return; return;
case EquipSlot.OffHand: case EquipSlot.OffHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, weapon.Value, 0, 0, 1, 0); LoadWeaponDetour(&character.AsCharacter->DrawData, 1, weapon.Value, 0, 0, 1, 0);
return; return;
case EquipSlot.BothHand: case EquipSlot.BothHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0); LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0); LoadWeaponDetour(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
return; return;
// function can also be called with '2', but does not seem to ever be. // function can also be called with '2', but does not seem to ever be.
} }
@ -107,14 +58,14 @@ public unsafe class WeaponService : IDisposable
// Load specific Main- and Offhand weapons. // Load specific Main- and Offhand weapons.
public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off) public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off)
{ {
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, main.Value, 1, 0, 1, 0); LoadWeaponDetour(&character.AsCharacter->DrawData, 0, main.Value, 1, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 1, 0, 1, 0); LoadWeaponDetour(&character.AsCharacter->DrawData, 1, off.Value, 1, 0, 1, 0);
} }
public void LoadStain(Actor character, EquipSlot slot, StainId stain) public void LoadStain(Actor character, EquipSlot slot, StainId stain)
{ {
var weapon = slot == EquipSlot.OffHand ? character.OffHand : character.MainHand; var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel;
weapon.Stain = stain; var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value };
LoadWeapon(character, slot, weapon); LoadWeapon(character, slot, weapon);
} }
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Gui.Tabs;
namespace Glamourer.Services; namespace Glamourer.Services;
@ -11,12 +12,12 @@ public class CommandService : IDisposable
private const string ApplyCommandString = "/glamour"; private const string ApplyCommandString = "/glamour";
private readonly CommandManager _commands; private readonly CommandManager _commands;
private readonly Interface _interface; private readonly MainWindow _mainWindow;
public CommandService(CommandManager commands, Interface ui) public CommandService(CommandManager commands, MainWindow mainWindow)
{ {
_commands = commands; _commands = commands;
_interface = ui; _mainWindow = mainWindow;
_commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." });
_commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" }); _commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" });
@ -29,7 +30,7 @@ public class CommandService : IDisposable
} }
private void OnGlamourer(string command, string arguments) private void OnGlamourer(string command, string arguments)
=> _interface.Toggle(); => _mainWindow.Toggle();
private void OnGlamour(string command, string arguments) private void OnGlamour(string command, string arguments)
{ } { }

View file

@ -0,0 +1,49 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.IoC;
using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection;
namespace Glamourer.Services;
public class DalamudServices
{
public DalamudServices(DalamudPluginInterface pi)
{
pi.Inject(this);
}
public void AddServices(IServiceCollection services)
{
services.AddSingleton(PluginInterface);
services.AddSingleton(Commands);
services.AddSingleton(GameData);
services.AddSingleton(ClientState);
services.AddSingleton(GameGui);
services.AddSingleton(Chat);
services.AddSingleton(Framework);
services.AddSingleton(Targets);
services.AddSingleton(Objects);
services.AddSingleton(KeyState);
services.AddSingleton(this);
services.AddSingleton(PluginInterface.UiBuilder);
}
// @formatter:off
[PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!;
[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!;
// @formatter:on
}

View file

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Dalamud.Plugin; using Dalamud.Plugin;
using Glamourer.Designs;
namespace Glamourer.Services; namespace Glamourer.Services;
@ -32,9 +30,6 @@ public class FilenameService
yield return new FileInfo(file); yield return new FileInfo(file);
} }
public string DesignFile(Design design)
=> DesignFile(design.Identifier.ToString());
public string DesignFile(string identifier) public string DesignFile(string identifier)
=> Path.Combine(DesignDirectory, $"{identifier}.json"); => Path.Combine(DesignDirectory, $"{identifier}.json");
} }

View file

@ -22,7 +22,7 @@ public class ItemManager : IDisposable
private readonly Configuration _config; private readonly Configuration _config;
public readonly IdentifierService IdentifierService; public readonly IdentifierService IdentifierService;
public readonly ExcelSheet<Item> ItemSheet; public readonly ExcelSheet<Lumina.Excel.GeneratedSheets.Item> ItemSheet;
public readonly StainData Stains; public readonly StainData Stains;
public readonly ItemService ItemService; public readonly ItemService ItemService;
public readonly RestrictedGear RestrictedGear; public readonly RestrictedGear RestrictedGear;
@ -30,7 +30,7 @@ public class ItemManager : IDisposable
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config) public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config)
{ {
_config = config; _config = config;
ItemSheet = gameData.GetExcelSheet<Item>()!; ItemSheet = gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>()!;
IdentifierService = identifierService; IdentifierService = identifierService;
Stains = new StainData(pi, gameData, gameData.Language); Stains = new StainData(pi, gameData, gameData.Language);
ItemService = itemService; ItemService = itemService;
@ -52,7 +52,7 @@ public class ItemManager : IDisposable
return (false, armor); return (false, armor);
} }
public readonly Item DefaultSword; public readonly Lumina.Excel.GeneratedSheets.Item DefaultSword;
public static uint NothingId(EquipSlot slot) public static uint NothingId(EquipSlot slot)
=> uint.MaxValue - 128 - (uint)slot.ToSlot(); => uint.MaxValue - 128 - (uint)slot.ToSlot();
@ -81,7 +81,7 @@ public class ItemManager : IDisposable
return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0)); return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0));
} }
public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null) public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{ {
slot = slot.ToSlot(); slot = slot.ToSlot();
if (itemId == NothingId(slot)) if (itemId == NothingId(slot))
@ -100,7 +100,7 @@ public class ItemManager : IDisposable
return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue)); return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue));
} }
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null) public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{ {
if (item == null || item.RowId != itemId) if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId); item = ItemSheet.GetRow(itemId);
@ -117,7 +117,7 @@ public class ItemManager : IDisposable
} }
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId,
FullEquipType mainType, Item? item = null) FullEquipType mainType, Lumina.Excel.GeneratedSheets.Item? item = null)
{ {
var offType = mainType.Offhand(); var offType = mainType.Offhand();
if (itemId == NothingId(offType)) if (itemId == NothingId(offType))

View file

@ -1,9 +1,9 @@
using Dalamud.Plugin; using Dalamud.Plugin;
using Glamourer.Api; using Glamourer.Events;
using Glamourer.Designs;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Gui.Tabs;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.State; using Glamourer.Interop.Penumbra;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Log; using OtterGui.Log;
@ -18,12 +18,10 @@ public static class ServiceManager
.AddSingleton(log) .AddSingleton(log)
.AddDalamud(pi) .AddDalamud(pi)
.AddMeta() .AddMeta()
.AddConfig()
.AddPenumbra()
.AddInterop() .AddInterop()
.AddGameData() .AddEvents()
.AddDesigns() .AddData()
.AddInterface() .AddUi()
.AddApi(); .AddApi();
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
@ -36,45 +34,33 @@ public static class ServiceManager
} }
private static IServiceCollection AddMeta(this IServiceCollection services) private static IServiceCollection AddMeta(this IServiceCollection services)
=> services.AddSingleton<FilenameService>() => services.AddSingleton<ChatService>()
.AddSingleton<SaveService>() .AddSingleton<FilenameService>()
.AddSingleton<FrameworkManager>()
.AddSingleton<ChatService>();
private static IServiceCollection AddConfig(this IServiceCollection services)
=> services.AddSingleton<Configuration>()
.AddSingleton<BackupService>(); .AddSingleton<BackupService>();
private static IServiceCollection AddPenumbra(this IServiceCollection services) private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<PenumbraAttach>(); => services.AddSingleton<VisorStateChanged>()
.AddSingleton<UpdatedSlot>();
private static IServiceCollection AddGameData(this IServiceCollection services) private static IServiceCollection AddData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>() => services.AddSingleton<IdentifierService>()
.AddSingleton<ActorService>()
.AddSingleton<ItemService>() .AddSingleton<ItemService>()
.AddSingleton<ItemManager>() .AddSingleton<ActorService>()
.AddSingleton<CustomizationService>(); .AddSingleton<CustomizationService>();
private static IServiceCollection AddInterop(this IServiceCollection services) private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<ChangeCustomizeService>() => services.AddSingleton<VisorService>()
.AddSingleton<JobService>() .AddSingleton<ChangeCustomizeService>()
.AddSingleton<UpdateSlotService>() .AddSingleton<UpdateSlotService>()
.AddSingleton<VisorService>()
.AddSingleton<WeaponService>() .AddSingleton<WeaponService>()
.AddSingleton<ObjectManager>(); .AddSingleton<PenumbraService>();
private static IServiceCollection AddDesigns(this IServiceCollection services) private static IServiceCollection AddUi(this IServiceCollection services)
=> services.AddSingleton<DesignManager>() => services
.AddSingleton<DesignFileSystem>() .AddSingleton<DebugTab>()
.AddSingleton<ActiveDesign.Manager>() .AddSingleton<MainWindow>()
.AddSingleton<FixedDesignManager>()
.AddSingleton<RedrawManager>();
private static IServiceCollection AddInterface(this IServiceCollection services)
=> services.AddSingleton<Interface>()
.AddSingleton<GlamourerWindowSystem>(); .AddSingleton<GlamourerWindowSystem>();
private static IServiceCollection AddApi(this IServiceCollection services) private static IServiceCollection AddApi(this IServiceCollection services)
=> services.AddSingleton<CommandService>() => services.AddSingleton<CommandService>();
.AddSingleton<Glamourer.GlamourerIpc>();
} }

View file

@ -7,8 +7,8 @@ using Penumbra.GameData.Actors;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game; using Dalamud.Game;
using Glamourer.Api;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Interop.Penumbra;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData; using Penumbra.GameData;
@ -50,7 +50,7 @@ public abstract class AsyncServiceWrapper<T>
else else
{ {
Service = service; Service = service;
Glamourer.Log.Verbose($"[{Name}] Created."); Item.Log.Verbose($"[{Name}] Created.");
_task = null; _task = null;
} }
}); });
@ -70,7 +70,7 @@ public abstract class AsyncServiceWrapper<T>
_task = null; _task = null;
if (Service is IDisposable d) if (Service is IDisposable d)
d.Dispose(); d.Dispose();
Glamourer.Log.Verbose($"[{Name}] Disposed."); Item.Log.Verbose($"[{Name}] Disposed.");
} }
} }
@ -91,7 +91,7 @@ public sealed class ItemService : AsyncServiceWrapper<ItemData>
public sealed class ActorService : AsyncServiceWrapper<ActorManager> public sealed class ActorService : AsyncServiceWrapper<ActorManager>
{ {
public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData, public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData,
GameGui gui, PenumbraAttach penumbra) GameGui gui, PenumbraService penumbra)
: base(nameof(ActorService), : base(nameof(ActorService),
() => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx))) () => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx)))
{ } { }

190
GlamourerOld/Glamourer.cs Normal file
View file

@ -0,0 +1,190 @@
using System.Reflection;
using Dalamud.Plugin;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Services;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer;
public partial class Glamourer : IDalamudPlugin
{
public string Name
=> "Glamourer";
public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
public static readonly string CommitHash =
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
public static readonly Logger Log = new();
public static ChatService ChatService { get; private set; } = null!;
private readonly ServiceProvider _services;
public Glamourer(DalamudPluginInterface pluginInterface)
{
try
{
EventWrapper.ChangeLogger(Log);
_services = ServiceManager.CreateProvider(pluginInterface, Log);
ChatService = _services.GetRequiredService<ChatService>();
_services.GetRequiredService<BackupService>();
_services.GetRequiredService<GlamourerWindowSystem>();
_services.GetRequiredService<CommandService>();
_services.GetRequiredService<GlamourerIpc>();
_services.GetRequiredService<ChangeCustomizeService>();
_services.GetRequiredService<JobService>();
_services.GetRequiredService<UpdateSlotService>();
_services.GetRequiredService<VisorService>();
_services.GetRequiredService<WeaponService>();
_services.GetRequiredService<RedrawManager>();
}
catch
{
Dispose();
throw;
}
}
public void Dispose()
{
_services?.Dispose();
}
//private static GameObject? GetPlayer(string name)
//{
// var lowerName = name.ToLowerInvariant();
// return lowerName switch
// {
// "" => null,
// "<me>" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
// "self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
// "<t>" => Dalamud.Targets.Target,
// "target" => Dalamud.Targets.Target,
// "<f>" => Dalamud.Targets.FocusTarget,
// "focus" => Dalamud.Targets.FocusTarget,
// "<mo>" => Dalamud.Targets.MouseOverTarget,
// "mouseover" => Dalamud.Targets.MouseOverTarget,
// _ => Dalamud.Objects.LastOrDefault(
// a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)),
// };
//}
//
//public void CopyToClipboard(Character player)
//{
// var save = new CharacterSave();
// save.LoadCharacter(player);
// ImGui.SetClipboardText(save.ToBase64());
//}
//
//public void ApplyCommand(Character player, string target)
//{
// CharacterSave? save = null;
// if (target.ToLowerInvariant() == "clipboard")
// try
// {
// save = CharacterSave.FromString(ImGui.GetClipboardText());
// }
// catch (Exception)
// {
// Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string.");
// }
// else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d)
// Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design.");
// else
// save = d.Data;
//
// save?.Apply(player);
// Penumbra.UpdateCharacters(player);
//}
//
//public void SaveCommand(Character player, string path)
//{
// var save = new CharacterSave();
// save.LoadCharacter(player);
// try
// {
// var (folder, name) = Designs.FileSystem.CreateAllFolders(path);
// var design = new Design(folder, name) { Data = save };
// folder.FindOrAddChild(design);
// Designs.Designs.Add(design.FullName(), design.Data);
// Designs.SaveToFile();
// }
// catch (Exception e)
// {
// Dalamud.Chat.PrintError("Could not save file:");
// Dalamud.Chat.PrintError($" {e.Message}");
// }
//}
//
public void OnGlamour(string command, string arguments)
{
//static void PrintHelp()
//{
// Dalamud.Chat.Print("Usage:");
// Dalamud.Chat.Print($" {HelpString}");
//}
//arguments = arguments.Trim();
//if (!arguments.Any())
//{
// PrintHelp();
// return;
//}
//
//var split = arguments.Split(new[]
//{
// ',',
//}, 3, StringSplitOptions.RemoveEmptyEntries);
//
//if (split.Length < 2)
//{
// PrintHelp();
// return;
//}
//
//var player = GetPlayer(split[1]) as Character;
//if (player == null)
//{
// Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character.");
// return;
//}
//
//switch (split[0].ToLowerInvariant())
//{
// case "copy":
// CopyToClipboard(player);
// return;
// case "apply":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'.");
// return;
// }
//
// ApplyCommand(player, split[2]);
//
// return;
// }
// case "save":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Saving requires a name for the save.");
// return;
// }
//
// SaveCommand(player, split[2]);
// return;
// }
// default:
// PrintHelp();
// return;
//}
}
}

View file

@ -0,0 +1,126 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>GlamourerOld</AssemblyName>
<FileVersion>0.2.0.0</FileVersion>
<AssemblyVersion>0.2.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
<None Remove="LegacyTattoo.raw" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="LegacyTattoo.raw" />
</ItemGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" />
</Exec>
<PropertyGroup>
<InformationalVersion>$(GitCommitHash)</InformationalVersion>
</PropertyGroup>
</Target>
<ItemGroup>
<None Update="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.String.dll $(SolutionDir)$(SolutionName).zip" />
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
</Target>
</Project>

View file

@ -0,0 +1,30 @@
using System;
using Dalamud.Interface;
using Dalamud.Interface.Windowing;
namespace Glamourer.Gui;
public class GlamourerWindowSystem : IDisposable
{
private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly UiBuilder _uiBuilder;
private readonly Interface _ui;
public GlamourerWindowSystem(UiBuilder uiBuilder, Interface ui)
{
_uiBuilder = uiBuilder;
_ui = ui;
_windowSystem.AddWindow(ui);
_uiBuilder.Draw += _windowSystem.Draw;
_uiBuilder.OpenConfigUi += _ui.Toggle;
}
public void Dispose()
{
_uiBuilder.Draw -= _windowSystem.Draw;
_uiBuilder.OpenConfigUi -= _ui.Toggle;
}
public void Toggle()
=> _ui.Toggle();
}

View file

@ -0,0 +1,24 @@
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class ChangeCustomizeService
{
public ChangeCustomizeService()
=> SignatureHelper.Initialise(this);
public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature(Sigs.ChangeCustomize)]
private readonly ChangeCustomizeDelegate _changeCustomize = null!;
public bool UpdateCustomize(Actor actor, CustomizeData customize)
{
if (customize.Data == null || !actor.Valid || !actor.DrawObject.Valid)
return false;
return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1);
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable
{
public UpdateSlotService()
{
SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable();
}
public void Dispose()
=> _flagSlotForUpdateHook.Dispose();
private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
public delegate void FlagSlotForUpdateDelegate(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item);
// This gets called when one of the ten equip items of an existing draw object gets changed.
[Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!;
public event FlagSlotForUpdateDelegate? EquipUpdate;
public ulong FlagSlotForUpdateInterop(DrawObject drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
{
InvokeFlagSlotEvent(drawObject, slot, ref data);
FlagSlotForUpdateInterop(drawObject, slot, data);
}
public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain)
{
var armor = drawObject.Equip[slot] with { Stain = stain };
UpdateSlot(drawObject, slot, armor);
}
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
InvokeFlagSlotEvent(drawObject, slot, ref *data);
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
}
private void InvokeFlagSlotEvent(DrawObject drawObject, EquipSlot slot, ref CharacterArmor armor)
{
if (EquipUpdate == null)
{
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}.");
return;
}
var iv = armor;
foreach (var del in EquipUpdate.GetInvocationList().OfType<FlagSlotForUpdateDelegate>())
{
try
{
del(drawObject, slot, ref armor);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(EquipUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}, initial armor was {iv.Set.Value}-{iv.Variant} with stain {iv.Stain.Value}.");
}
}

View file

@ -0,0 +1,78 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public class VisorService : IDisposable
{
public VisorService()
{
SignatureHelper.Initialise(this);
_setupVisorHook.Enable();
}
public void Dispose()
=> _setupVisorHook.Dispose();
public static unsafe bool GetVisorState(nint humanPtr)
{
if (humanPtr == IntPtr.Zero)
return false;
var data = (Human*)humanPtr;
var flags = &data->CharacterBase.UnkFlags_01;
return (*flags & Offsets.DrawObjectVisorStateFlag) != 0;
}
public unsafe void SetVisorState(nint humanPtr, bool on)
{
if (humanPtr == IntPtr.Zero)
return;
var data = (Human*)humanPtr;
_setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on);
}
private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on);
public delegate void UpdateVisorDelegate(DrawObject human, SetId modelId, ref bool on);
[Signature(Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
private readonly Hook<UpdateVisorDelegateInternal> _setupVisorHook = null!;
public event UpdateVisorDelegate? VisorUpdate;
private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on)
{
InvokeVisorEvent(humanPtr, modelId, ref on);
_setupVisorHook.Original(humanPtr, modelId, on);
}
private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on)
{
if (VisorUpdate == null)
{
Glamourer.Log.Excessive($"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}.");
return;
}
var initialValue = on;
foreach (var del in VisorUpdate.GetInvocationList().OfType<UpdateVisorDelegate>())
{
try
{
del(drawObject, modelId, ref on);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(VisorUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}.");
}
}

View file

@ -0,0 +1,120 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable
{
public WeaponService()
{
SignatureHelper.Initialise(this);
_loadWeaponHook.Enable();
}
public void Dispose()
{
_loadWeaponHook.Dispose();
}
public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf<Character>("DrawData");
public delegate void LoadWeaponDelegate(nint offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2,
byte skipGameObject,
byte unk4);
// Weapons for a specific character are reloaded with this function.
// The first argument is a pointer to the game object but shifted a bit inside.
// slot is 0 for main hand, 1 for offhand, 2 for unknown (always called with empty data.
// weapon argument is the new weapon data.
// redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one.
// skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change)
// unk4 seemed to be the same as unk1.
[Signature(Penumbra.GameData.Sigs.WeaponReload, DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook = null!;
private void LoadWeaponDetour(nint characterOffset, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
byte unk4)
{
//var oldWeapon = weapon;
//var character = (Actor)(characterOffset - CharacterWeaponOffset);
//try
//{
// var identifier = character.GetIdentifier(_actors.AwaitedService);
// if (_fixedDesignManager.TryGetDesign(identifier, out var save))
// {
// PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
// weapon = slot switch
// {
// 0 => save.WeaponMain.Model.Value,
// 1 => save.WeaponOff.Model.Value,
// _ => weapon,
// };
// }
// else if (redrawOnEquality == 1 && _stateManager.TryGetValue(identifier, out var save2))
// {
// PluginLog.Information($"Loaded weapon from current design for {identifier}.");
// //switch (slot)
// //{
// // case 0:
// // save2.MainHand = new CharacterWeapon(weapon);
// // break;
// // case 1:
// // save2.Data.OffHand = new CharacterWeapon(weapon);
// // break;
// //}
// }
//}
//catch (Exception e)
//{
// PluginLog.Error($"Error on loading new weapon:\n{e}");
//}
// First call the regular function.
_loadWeaponHook.Original(characterOffset, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
Glamourer.Log.Excessive($"Weapon reloaded for {(Actor)(characterOffset - CharacterWeaponOffset)} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
// // If something changed the weapon, call it again with the actual change, not forcing redraws and skipping applying it to the game object.
// if (oldWeapon != weapon)
// _loadWeaponHook.Original(characterOffset, slot, weapon, 0 /* redraw */, unk2, 1 /* skip */, unk4);
// // If we're not actively changing the offhand and the game object has no offhand, redraw an empty offhand to fix animation problems.
// else if (slot != 1 && character.OffHand.Value == 0)
// _loadWeaponHook.Original(characterOffset, 1, 0, 1 /* redraw */, unk2, 1 /* skip */, unk4);
}
// Load a specific weapon for a character by its data and slot.
public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon)
{
switch (slot)
{
case EquipSlot.MainHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.OffHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.BothHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
return;
// function can also be called with '2', but does not seem to ever be.
}
}
// Load specific Main- and Offhand weapons.
public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off)
{
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, main.Value, 1, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 1, 0, 1, 0);
}
public void LoadStain(Actor character, EquipSlot slot, StainId stain)
{
var weapon = slot == EquipSlot.OffHand ? character.OffHand : character.MainHand;
weapon.Stain = stain;
LoadWeapon(character, slot, weapon);
}
}

Binary file not shown.

View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.IO;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
public class BackupService
{
public BackupService(Logger logger, FilenameService fileNames)
{
var files = GlamourerFiles(fileNames);
Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
}
/// <summary> Collect all relevant files for glamourer configuration. </summary>
private static IReadOnlyList<FileInfo> GlamourerFiles(FilenameService fileNames)
{
var list = new List<FileInfo>(16)
{
new(fileNames.ConfigFile),
new(fileNames.DesignFileSystem),
new(fileNames.MigrationDesignFile),
};
list.AddRange(fileNames.Designs());
return list;
}
}

View file

@ -0,0 +1,36 @@
using System;
using Dalamud.Game.Command;
using Glamourer.Gui;
namespace Glamourer.Services;
public class CommandService : IDisposable
{
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
private const string MainCommandString = "/glamourer";
private const string ApplyCommandString = "/glamour";
private readonly CommandManager _commands;
private readonly Interface _interface;
public CommandService(CommandManager commands, Interface ui)
{
_commands = commands;
_interface = ui;
_commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." });
_commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" });
}
public void Dispose()
{
_commands.RemoveHandler(MainCommandString);
_commands.RemoveHandler(ApplyCommandString);
}
private void OnGlamourer(string command, string arguments)
=> _interface.Toggle();
private void OnGlamour(string command, string arguments)
{ }
}

View file

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Plugin;
using Glamourer.Designs;
namespace Glamourer.Services;
public class FilenameService
{
public readonly string ConfigDirectory;
public readonly string ConfigFile;
public readonly string DesignFileSystem;
public readonly string MigrationDesignFile;
public readonly string DesignDirectory;
public FilenameService(DalamudPluginInterface pi)
{
ConfigDirectory = pi.ConfigDirectory.FullName;
ConfigFile = pi.ConfigFile.FullName;
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
}
public IEnumerable<FileInfo> Designs()
{
if (!Directory.Exists(DesignDirectory))
yield break;
foreach (var file in Directory.EnumerateFiles(DesignDirectory, "*.json", SearchOption.TopDirectoryOnly))
yield return new FileInfo(file);
}
public string DesignFile(Design design)
=> DesignFile(design.Identifier.ToString());
public string DesignFile(string identifier)
=> Path.Combine(DesignDirectory, $"{identifier}.json");
}

View file

@ -0,0 +1,189 @@
using System;
using System.Diagnostics;
using System.Linq;
using Dalamud.Data;
using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Lumina.Text;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Services;
public class ItemManager : IDisposable
{
public const string Nothing = "Nothing";
public const string SmallClothesNpc = "Smallclothes (NPC)";
public const ushort SmallClothesNpcModel = 9903;
private readonly Configuration _config;
public readonly IdentifierService IdentifierService;
public readonly ExcelSheet<Item> ItemSheet;
public readonly StainData Stains;
public readonly ItemService ItemService;
public readonly RestrictedGear RestrictedGear;
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config)
{
_config = config;
ItemSheet = gameData.GetExcelSheet<Item>()!;
IdentifierService = identifierService;
Stains = new StainData(pi, gameData, gameData.Language);
ItemService = itemService;
RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData);
DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword
}
public void Dispose()
{
Stains.Dispose();
RestrictedGear.Dispose();
}
public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
{
if (_config.UseRestrictedGearProtection)
return RestrictedGear.ResolveRestricted(armor, slot, race, gender);
return (false, armor);
}
public readonly Item DefaultSword;
public static uint NothingId(EquipSlot slot)
=> uint.MaxValue - 128 - (uint)slot.ToSlot();
public static uint SmallclothesId(EquipSlot slot)
=> uint.MaxValue - 256 - (uint)slot.ToSlot();
public static uint NothingId(FullEquipType type)
=> uint.MaxValue - 384 - (uint)type;
public static Designs.Item NothingItem(EquipSlot slot)
{
Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(NothingItem)} on {slot}.");
return new Designs.Item(Nothing, NothingId(slot), CharacterArmor.Empty);
}
public static Designs.Weapon NothingItem(FullEquipType type)
{
Debug.Assert(type.ToSlot() == EquipSlot.OffHand, $"Called {nameof(NothingItem)} on {type}.");
return new Designs.Weapon(Nothing, NothingId(type), CharacterWeapon.Empty, type);
}
public static Designs.Item SmallClothesItem(EquipSlot slot)
{
Debug.Assert(slot.IsEquipment(), $"Called {nameof(SmallClothesItem)} on {slot}.");
return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0));
}
public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null)
{
slot = slot.ToSlot();
if (itemId == NothingId(slot))
return (true, 0, 0, Nothing);
if (itemId == SmallclothesId(slot))
return (true, SmallClothesNpcModel, 1, SmallClothesNpc);
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, string.Intern($"Unknown #{itemId}"));
if (item.ToEquipType().ToSlot() != slot)
return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"));
return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue));
}
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null)
{
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown);
var type = item.ToEquipType();
if (type.ToSlot() != EquipSlot.MainHand)
return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type);
return (true, (SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32),
string.Intern(item.Name.ToDalamudString().TextValue), type);
}
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId,
FullEquipType mainType, Item? item = null)
{
var offType = mainType.Offhand();
if (itemId == NothingId(offType))
return (true, 0, 0, 0, Nothing, offType);
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown);
var type = item.ToEquipType();
if (offType != type)
return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type);
var (m, w, v) = offType.ToSlot() == EquipSlot.MainHand
? ((SetId)item.ModelSub, (WeaponType)(item.ModelSub >> 16), (byte)(item.ModelSub >> 32))
: ((SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32));
return (true, m, w, v, string.Intern(item.Name.ToDalamudString().TextValue), type);
}
public (bool Valid, uint ItemId, string ItemName) Identify(EquipSlot slot, SetId id, byte variant)
{
slot = slot.ToSlot();
if (!slot.IsEquipmentPiece())
return (false, 0, string.Intern($"Unknown ({id.Value}-{variant})"));
switch (id.Value)
{
case 0: return (true, NothingId(slot), Nothing);
case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc);
default:
var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault();
return item == null
? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})"))
: (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue));
}
}
public (bool Valid, uint ItemId, string ItemName, FullEquipType Type) Identify(EquipSlot slot, SetId id, WeaponType type, byte variant,
FullEquipType mainhandType = FullEquipType.Unknown)
{
switch (slot)
{
case EquipSlot.MainHand:
{
var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault();
return item != null
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType())
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), mainhandType);
}
case EquipSlot.OffHand:
{
var weaponType = mainhandType.Offhand();
if (id.Value == 0)
return (true, NothingId(weaponType), Nothing, weaponType);
var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault();
return item != null
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType())
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"),
weaponType);
}
default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown);
}
}
}

View file

@ -0,0 +1,80 @@
using Dalamud.Plugin;
using Glamourer.Api;
using Glamourer.Designs;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.State;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
public static class ServiceManager
{
public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger log)
{
var services = new ServiceCollection()
.AddSingleton(log)
.AddDalamud(pi)
.AddMeta()
.AddConfig()
.AddPenumbra()
.AddInterop()
.AddGameData()
.AddDesigns()
.AddInterface()
.AddApi();
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
}
private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi)
{
new DalamudServices(pi).AddServices(services);
return services;
}
private static IServiceCollection AddMeta(this IServiceCollection services)
=> services.AddSingleton<FilenameService>()
.AddSingleton<SaveService>()
.AddSingleton<FrameworkManager>()
.AddSingleton<ChatService>();
private static IServiceCollection AddConfig(this IServiceCollection services)
=> services.AddSingleton<Configuration>()
.AddSingleton<BackupService>();
private static IServiceCollection AddPenumbra(this IServiceCollection services)
=> services.AddSingleton<PenumbraAttach>();
private static IServiceCollection AddGameData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>()
.AddSingleton<ActorService>()
.AddSingleton<ItemService>()
.AddSingleton<ItemManager>()
.AddSingleton<CustomizationService>();
private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<ChangeCustomizeService>()
.AddSingleton<JobService>()
.AddSingleton<UpdateSlotService>()
.AddSingleton<VisorService>()
.AddSingleton<WeaponService>()
.AddSingleton<ObjectManager>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()
.AddSingleton<DesignFileSystem>()
.AddSingleton<ActiveDesign.Manager>()
.AddSingleton<FixedDesignManager>()
.AddSingleton<RedrawManager>();
private static IServiceCollection AddInterface(this IServiceCollection services)
=> services.AddSingleton<Interface>()
.AddSingleton<GlamourerWindowSystem>();
private static IServiceCollection AddApi(this IServiceCollection services)
=> services.AddSingleton<CommandService>()
.AddSingleton<Glamourer.GlamourerIpc>();
}

View file

@ -0,0 +1,105 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using Dalamud.Plugin;
using Penumbra.GameData.Actors;
using System;
using System.Threading.Tasks;
using Dalamud.Game;
using Glamourer.Api;
using Glamourer.Customization;
using Penumbra.GameData.Data;
using Penumbra.GameData;
namespace Glamourer.Services;
public abstract class AsyncServiceWrapper<T>
{
public string Name { get; }
public T? Service { get; private set; }
public T AwaitedService
{
get
{
_task?.Wait();
return Service!;
}
}
public bool Valid
=> Service != null && !_isDisposed;
public event Action? FinishedCreation;
private Task? _task;
private bool _isDisposed;
protected AsyncServiceWrapper(string name, Func<T> factory)
{
Name = name;
_task = Task.Run(() =>
{
var service = factory();
if (_isDisposed)
{
if (service is IDisposable d)
d.Dispose();
}
else
{
Service = service;
Glamourer.Log.Verbose($"[{Name}] Created.");
_task = null;
}
});
_task.ContinueWith((t, x) =>
{
if (!_isDisposed)
FinishedCreation?.Invoke();
}, null);
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_task = null;
if (Service is IDisposable d)
d.Dispose();
Glamourer.Log.Verbose($"[{Name}] Disposed.");
}
}
public sealed class IdentifierService : AsyncServiceWrapper<IObjectIdentifier>
{
public IdentifierService(DalamudPluginInterface pi, DataManager data)
: base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data))
{ }
}
public sealed class ItemService : AsyncServiceWrapper<ItemData>
{
public ItemService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(ItemService), () => new ItemData(pi, gameData, gameData.Language))
{ }
}
public sealed class ActorService : AsyncServiceWrapper<ActorManager>
{
public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData,
GameGui gui, PenumbraAttach penumbra)
: base(nameof(ActorService),
() => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx)))
{ }
}
public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationManager>
{
public CustomizationService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
{ }
}