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 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;

View file

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32210.308
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
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}"
ProjectSection(SolutionItems) = preProject
@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\Pen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "..\Penumbra\OtterGui\OtterGui.csproj", "{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
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;
public partial class Glamourer : IDalamudPlugin
public class Item : IDalamudPlugin
{
public string Name
=> "Glamourer";
@ -20,26 +20,22 @@ public partial class Glamourer : IDalamudPlugin
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 static readonly Logger Log = new();
public static ChatService Chat { get; private set; } = null!;
public Glamourer(DalamudPluginInterface pluginInterface)
private readonly ServiceProvider _services;
public Item(DalamudPluginInterface pluginInterface)
{
try
{
_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 = ServiceManager.CreateProvider(pluginInterface, Log);
Chat = _services.GetRequiredService<ChatService>();
_services.GetRequiredService<BackupService>(); // call backup service.
_services.GetRequiredService<GlamourerWindowSystem>(); // initialize ui.
_services.GetRequiredService<CommandService>(); // initialize commands.
_services.GetRequiredService<VisorService>();
_services.GetRequiredService<WeaponService>();
_services.GetRequiredService<RedrawManager>();
}
catch
{
@ -53,137 +49,4 @@ public partial class Glamourer : IDalamudPlugin
{
_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>
<ProjectReference Include="..\..\Penumbra\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" />
@ -119,6 +120,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Designs\" />
</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)" />

View file

@ -8,9 +8,9 @@ public class GlamourerWindowSystem : IDisposable
{
private readonly WindowSystem _windowSystem = new("Glamourer");
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;
_ui = ui;
@ -24,7 +24,4 @@ public class GlamourerWindowSystem : IDisposable
_uiBuilder.Draw -= _windowSystem.Draw;
_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 FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Structs;
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 ChangeCustomizeService()
=> 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)]
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 _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.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -9,8 +10,11 @@ namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable
{
public UpdateSlotService()
public readonly UpdatedSlot Event;
public UpdateSlotService(UpdatedSlot updatedSlot)
{
Event = updatedSlot;
SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable();
}
@ -19,59 +23,41 @@ public unsafe class UpdateSlotService : IDisposable
=> _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)
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
{
InvokeFlagSlotEvent(drawObject, slot, ref data);
if (!drawObject.IsCharacterBase)
return;
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 };
UpdateSlot(drawObject, slot, armor);
if (!drawObject.IsCharacterBase)
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)
{
var slot = slotIdx.ToEquipSlot();
InvokeFlagSlotEvent(drawObject, slot, ref *data);
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
var slot = slotIdx.ToEquipSlot();
var returnValue = ulong.MaxValue;
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)
{
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}.");
}
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
}

View file

@ -1,16 +1,19 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Structs;
using Glamourer.Events;
using Glamourer.Interop.Structs;
namespace Glamourer.Interop;
public class VisorService : IDisposable
{
public VisorService()
public readonly VisorStateChanged Event;
public VisorService(VisorStateChanged visorStateChanged)
{
Event = visorStateChanged;
SignatureHelper.Initialise(this);
_setupVisorHook.Enable();
}
@ -18,61 +21,67 @@ public class VisorService : IDisposable
public void 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;
var data = (Human*)humanPtr;
var flags = &data->CharacterBase.UnkFlags_01;
return (*flags & Offsets.DrawObjectVisorStateFlag) != 0;
// TODO: use client structs.
return (characterBase.AsCharacterBase->UnkFlags_01 & 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)
return;
if (!human.IsHuman)
return false;
var data = (Human*)humanPtr;
_setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on);
var oldState = GetVisorState(human);
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);
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!;
public event UpdateVisorDelegate? VisorUpdate;
private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on)
private void SetupVisorDetour(nint human, ushort modelId, bool on)
{
InvokeVisorEvent(humanPtr, modelId, ref on);
_setupVisorHook.Original(humanPtr, modelId, on);
var callOriginal = true;
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)
{
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}.");
// TODO: use client structs.
human.AsCharacterBase->UnkFlags_01 = (byte)(on
? human.AsCharacterBase->UnkFlags_01 | Offsets.DrawObjectVisorStateFlag
: human.AsCharacterBase->UnkFlags_01 & ~Offsets.DrawObjectVisorStateFlag);
_setupVisorHook.Original(human.Address, modelId, on);
}
}

View file

@ -1,8 +1,8 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -13,6 +13,7 @@ public unsafe class WeaponService : IDisposable
public WeaponService()
{
SignatureHelper.Initialise(this);
_loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_loadWeaponHook.Enable();
}
@ -21,68 +22,18 @@ public unsafe class WeaponService : IDisposable
_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,
byte skipGameObject,
byte unk4);
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook;
// 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,
private void LoadWeaponDetour(DrawDataContainer* drawData, 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}");
//}
var actor = (Actor) (nint)drawData->Unk8;
// 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);
_loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
Item.Log.Information($"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
}
// Load a specific weapon for a character by its data and slot.
@ -91,14 +42,14 @@ public unsafe class WeaponService : IDisposable
switch (slot)
{
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;
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;
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);
LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
return;
// 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.
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);
LoadWeaponDetour(&character.AsCharacter->DrawData, 0, main.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)
{
var weapon = slot == EquipSlot.OffHand ? character.OffHand : character.MainHand;
weapon.Stain = stain;
var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel;
var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value };
LoadWeapon(character, slot, weapon);
}
}

View file

@ -1,6 +1,7 @@
using System;
using Dalamud.Game.Command;
using Glamourer.Gui;
using Glamourer.Gui.Tabs;
namespace Glamourer.Services;
@ -11,12 +12,12 @@ public class CommandService : IDisposable
private const string ApplyCommandString = "/glamour";
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;
_interface = ui;
_commands = commands;
_mainWindow = mainWindow;
_commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." });
_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)
=> _interface.Toggle();
=> _mainWindow.Toggle();
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.IO;
using Dalamud.Plugin;
using Glamourer.Designs;
namespace Glamourer.Services;
@ -32,9 +30,6 @@ public class FilenameService
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

@ -22,7 +22,7 @@ public class ItemManager : IDisposable
private readonly Configuration _config;
public readonly IdentifierService IdentifierService;
public readonly ExcelSheet<Item> ItemSheet;
public readonly ExcelSheet<Lumina.Excel.GeneratedSheets.Item> ItemSheet;
public readonly StainData Stains;
public readonly ItemService ItemService;
public readonly RestrictedGear RestrictedGear;
@ -30,7 +30,7 @@ public class ItemManager : IDisposable
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config)
{
_config = config;
ItemSheet = gameData.GetExcelSheet<Item>()!;
ItemSheet = gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>()!;
IdentifierService = identifierService;
Stains = new StainData(pi, gameData, gameData.Language);
ItemService = itemService;
@ -52,7 +52,7 @@ public class ItemManager : IDisposable
return (false, armor);
}
public readonly Item DefaultSword;
public readonly Lumina.Excel.GeneratedSheets.Item DefaultSword;
public static uint NothingId(EquipSlot slot)
=> 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));
}
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();
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));
}
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)
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,
FullEquipType mainType, Item? item = null)
FullEquipType mainType, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var offType = mainType.Offhand();
if (itemId == NothingId(offType))

View file

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

View file

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