Initial Commit

This commit is contained in:
Ottermandias 2021-07-30 17:23:15 +02:00
commit 164f304cf6
38 changed files with 2796 additions and 0 deletions

23
Glamourer/CmpFile.cs Normal file
View file

@ -0,0 +1,23 @@
using Dalamud.Plugin;
namespace Glamourer
{
public class CmpFile
{
public readonly Lumina.Data.FileResource File;
public readonly uint[] RgbaColors;
public CmpFile(DalamudPluginInterface pi)
{
File = pi.Data.GetFile("chara/xls/charamake/human.cmp");
RgbaColors = new uint[File.Data.Length >> 2];
for (var i = 0; i < File.Data.Length; i += 4)
{
RgbaColors[i >> 2] = File.Data[i]
| (uint) (File.Data[i + 1] << 8)
| (uint) (File.Data[i + 2] << 16)
| (uint) (File.Data[i + 3] << 24);
}
}
}
}

View file

@ -0,0 +1,98 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.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>
</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>
<Reference Include="Dalamud">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
</Reference>
<Reference Include="SDL2-CS">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\SDL2-CS.dll</HintPath>
</Reference>
<Reference Include="Lumina">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
<Reference Include="Penumbra.Api">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.Api.dll</HintPath>
</Reference>
<Reference Include="Penumbra.PlayerWatch">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.PlayerWatch.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="System.Memory" Version="4.5.3" />
</ItemGroup>
<ItemGroup>
<None Include="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll $(SolutionDir)$(SolutionName).zip" />
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
</Target>
</Project>

11
Glamourer/Glamourer.json Normal file
View file

@ -0,0 +1,11 @@
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
"InternalName": "Glamourer",
"AssemblyVersion": "1.0.0.0",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 3,
"LoadPriority": -100
}

View file

@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using ImGuiNET;
namespace Glamourer.Gui
{
public class ComboWithFilter<T>
{
private readonly string _label;
private readonly string _filterLabel;
private readonly string _listLabel;
private string _currentFilter = string.Empty;
private string _currentFilterLower = string.Empty;
private bool _focus;
private readonly float _size;
private readonly IReadOnlyList<T> _items;
private readonly IReadOnlyList<string> _itemNamesLower;
private readonly Func<T, string> _itemToName;
public Action? PrePreview = null;
public Action? PostPreview = null;
public Func<T, bool>? CreateSelectable = null;
public Action? PreList = null;
public Action? PostList = null;
public float? HeightPerItem = null;
private float _heightPerItem;
public ImGuiComboFlags Flags { get; set; } = ImGuiComboFlags.None;
public int ItemsAtOnce { get; set; } = 12;
public ComboWithFilter(string label, float size, IReadOnlyList<T> items, Func<T, string> itemToName)
{
_label = label;
_filterLabel = $"##_{label}_filter";
_listLabel = $"##_{label}_list";
_itemToName = itemToName;
_items = items;
_size = size;
_itemNamesLower = _items.Select(i => _itemToName(i).ToLowerInvariant()).ToList();
}
public ComboWithFilter(string label, ComboWithFilter<T> other)
{
_label = label;
_filterLabel = $"##_{label}_filter";
_listLabel = $"##_{label}_list";
_itemToName = other._itemToName;
_items = other._items;
_itemNamesLower = other._itemNamesLower;
_size = other._size;
PrePreview = other.PrePreview;
PostPreview = other.PostPreview;
CreateSelectable = other.CreateSelectable;
PreList = other.PreList;
PostList = other.PostList;
HeightPerItem = other.HeightPerItem;
Flags = other.Flags;
}
private bool DrawList(string currentName, out int numItems, out int nodeIdx, ref T? value)
{
numItems = ItemsAtOnce;
nodeIdx = -1;
if (!ImGui.BeginChild(_listLabel, new Vector2(_size, ItemsAtOnce * _heightPerItem)))
return false;
var ret = false;
try
{
if (!_focus)
{
ImGui.SetScrollY(0);
_focus = true;
}
var scrollY = Math.Max((int) (ImGui.GetScrollY() / _heightPerItem) - 1, 0);
var restHeight = scrollY * _heightPerItem;
numItems = 0;
nodeIdx = 0;
if (restHeight > 0)
ImGui.Dummy(Vector2.UnitY * restHeight);
for (var i = scrollY; i < _items.Count; ++i)
{
if (!_itemNamesLower[i].Contains(_currentFilterLower))
continue;
++numItems;
if (numItems <= ItemsAtOnce + 2)
{
nodeIdx = i;
var item = _items[i]!;
var success = false;
if (CreateSelectable != null)
{
success = CreateSelectable(item);
}
else
{
var name = _itemToName(item);
success = ImGui.Selectable(name, name == currentName);
}
if (success)
{
value = item;
ImGui.CloseCurrentPopup();
ret = true;
}
}
}
if (numItems > ItemsAtOnce + 2)
ImGui.Dummy(Vector2.UnitY * (numItems - ItemsAtOnce - 2) * _heightPerItem);
}
finally
{
ImGui.EndChild();
}
return ret;
}
public bool Draw(string currentName, out T? value)
{
value = default;
ImGui.SetNextItemWidth(_size);
PrePreview?.Invoke();
if (!ImGui.BeginCombo(_label, currentName, Flags))
{
_focus = false;
_currentFilter = string.Empty;
_currentFilterLower = string.Empty;
PostPreview?.Invoke();
return false;
}
PostPreview?.Invoke();
_heightPerItem = HeightPerItem ?? ImGui.GetTextLineHeightWithSpacing();
var ret = false;
try
{
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint(_filterLabel, "Filter...", ref _currentFilter, 255))
_currentFilterLower = _currentFilter.ToLowerInvariant();
var isFocused = ImGui.IsItemActive();
if (!_focus)
ImGui.SetKeyboardFocusHere();
PreList?.Invoke();
ret = DrawList(currentName, out var numItems, out var nodeIdx, ref value);
PostList?.Invoke();
if (!isFocused && numItems <= 1 && nodeIdx >= 0)
{
value = _items[nodeIdx];
ret = true;
ImGui.CloseCurrentPopup();
}
}
finally
{
ImGui.EndCombo();
}
return ret;
}
}
}

154
Glamourer/Gui/ImGuiRaii.cs Normal file
View file

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using ImGuiNET;
namespace Glamourer.Gui
{
public sealed class ImGuiRaii : IDisposable
{
private int _colorStack = 0;
private int _fontStack = 0;
private int _styleStack = 0;
private float _indentation = 0f;
private Stack<Action>? _onDispose = null;
public ImGuiRaii()
{ }
public static ImGuiRaii NewGroup()
=> new ImGuiRaii().Group();
public ImGuiRaii Group()
=> Begin(ImGui.BeginGroup, ImGui.EndGroup);
public static ImGuiRaii NewTooltip()
=> new ImGuiRaii().Tooltip();
public ImGuiRaii Tooltip()
=> Begin(ImGui.BeginTooltip, ImGui.EndTooltip);
public ImGuiRaii PushColor(ImGuiCol which, uint color)
{
ImGui.PushStyleColor(which, color);
++_colorStack;
return this;
}
public ImGuiRaii PushColor(ImGuiCol which, Vector4 color)
{
ImGui.PushStyleColor(which, color);
++_colorStack;
return this;
}
public ImGuiRaii PopColors(int n = 1)
{
var actualN = Math.Min(n, _colorStack);
if (actualN > 0)
{
ImGui.PopStyleColor(actualN);
_colorStack -= actualN;
}
return this;
}
public ImGuiRaii PushStyle(ImGuiStyleVar style, Vector2 value)
{
ImGui.PushStyleVar(style, value);
++_styleStack;
return this;
}
public ImGuiRaii PushStyle(ImGuiStyleVar style, float value)
{
ImGui.PushStyleVar(style, value);
++_styleStack;
return this;
}
public ImGuiRaii PopStyles(int n = 1)
{
var actualN = Math.Min(n, _styleStack);
if (actualN > 0)
{
ImGui.PopStyleVar(actualN);
_styleStack -= actualN;
}
return this;
}
public ImGuiRaii PushFont(ImFontPtr font)
{
ImGui.PushFont(font);
++_fontStack;
return this;
}
public ImGuiRaii PopFonts(int n = 1)
{
var actualN = Math.Min(n, _fontStack);
while (actualN-- > 0)
{
ImGui.PopFont();
--_fontStack;
}
return this;
}
public ImGuiRaii Indent(float width)
{
if (width != 0)
{
ImGui.Indent(width);
_indentation += width;
}
return this;
}
public ImGuiRaii Unindent(float width)
=> Indent(-width);
public bool Begin(Func<bool> begin, Action end)
{
if (begin())
{
_onDispose ??= new Stack<Action>();
_onDispose.Push(end);
return true;
}
return false;
}
public ImGuiRaii Begin(Action begin, Action end)
{
begin();
_onDispose ??= new Stack<Action>();
_onDispose.Push(end);
return this;
}
public void End(int n = 1)
{
var actualN = Math.Min(n, _onDispose?.Count ?? 0);
while(actualN-- > 0)
_onDispose!.Pop()();
}
public void Dispose()
{
Unindent(_indentation);
PopColors(_colorStack);
PopStyles(_styleStack);
PopFonts(_fontStack);
if (_onDispose != null)
{
End(_onDispose.Count);
_onDispose = null;
}
}
}
}

326
Glamourer/Gui/Interface.cs Normal file
View file

@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Data.LuminaExtensions;
using Dalamud.Game.ClientState.Actors;
using Dalamud.Game.ClientState.Actors.Types;
using Glamourer.Customization;
using ImGuiNET;
using Penumbra.Api;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.PlayerWatch;
using SDL2;
namespace Glamourer.Gui
{
internal class Interface : IDisposable
{
public const int GPoseActorId = 201;
private const string PluginName = "Glamourer";
private readonly string _glamourerHeader;
private const int ColorButtonWidth = 140;
private readonly IReadOnlyDictionary<byte, Stain> _stains;
private readonly IReadOnlyDictionary<EquipSlot, List<Item>> _equip;
private readonly ActorTable _actors;
private readonly IObjectIdentifier _identifier;
private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
private readonly IPlayerWatcher _playerWatcher;
private bool _visible = false;
private Actor? _player;
private static readonly Vector2 FeatureIconSize = new(80, 80);
public Interface()
{
_glamourerHeader = GlamourerPlugin.Version.Length > 0
? $"{PluginName} v{GlamourerPlugin.Version}###{PluginName}Main"
: $"{PluginName}###{PluginName}Main";
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi += Draw;
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility;
_stains = GameData.Stains(GlamourerPlugin.PluginInterface);
_equip = GameData.ItemsBySlot(GlamourerPlugin.PluginInterface);
_identifier = Penumbra.GameData.GameData.GetIdentifier(GlamourerPlugin.PluginInterface);
_actors = GlamourerPlugin.PluginInterface.ClientState.Actors;
_playerWatcher = PlayerWatchFactory.Create(GlamourerPlugin.PluginInterface);
var stainCombo = new ComboWithFilter<Stain>("##StainCombo", ColorButtonWidth, _stains.Values.ToArray(),
s => s.Name.ToString())
{
Flags = ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge,
PreList = () =>
{
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
},
PostList = () => { ImGui.PopStyleVar(3); },
CreateSelectable = s =>
{
var push = PushColor(s);
var ret = ImGui.Button($"{s.Name}##Stain{(byte) s.RowIndex}",
Vector2.UnitX * (ColorButtonWidth - ImGui.GetStyle().ScrollbarSize));
ImGui.PopStyleColor(push);
return ret;
},
ItemsAtOnce = 12,
};
_combos = _equip.ToDictionary(kvp => kvp.Key,
kvp => (new ComboWithFilter<Item>($"{kvp.Key}##Equip", 300, kvp.Value, i => i.Name) { Flags = ImGuiComboFlags.HeightLarge }
, new ComboWithFilter<Stain>($"##{kvp.Key}Stain", stainCombo))
);
}
public void ToggleVisibility(object _, object _2)
=> _visible = !_visible;
public void Dispose()
{
_playerWatcher?.Dispose();
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi -= Draw;
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility;
}
private string _currentActorName = "";
private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button)
{
ImGui.PushStyleColor(type, stain.RgbaColor);
if (stain.Intensity > 127)
{
ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010);
return 2;
}
return 1;
}
private bool DrawColorSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx)
{
var name = string.Empty;
stainCombo.PostPreview = null;
if (_stains.TryGetValue((byte) stainIdx, out var stain))
{
name = stain.Name;
var previewPush = PushColor(stain, ImGuiCol.FrameBg);
stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
}
if (stainCombo.Draw(name, out var newStain) && _player != null)
{
newStain.Write(_player.Address, slot);
return true;
}
return false;
}
private bool DrawItemSelector(ComboWithFilter<Item> equipCombo, Lumina.Excel.GeneratedSheets.Item? item)
{
var currentName = item?.Name.ToString() ?? "Nothing";
if (equipCombo.Draw(currentName, out var newItem) && _player != null)
{
newItem.Write(_player.Address);
return true;
}
return false;
}
private bool DrawEquip(EquipSlot slot, ActorArmor equip)
{
var (equipCombo, stainCombo) = _combos[slot];
var ret = false;
ret = DrawColorSelector(stainCombo, slot, equip.Stain);
ImGui.SameLine();
var item = _identifier.Identify(equip.Set, new WeaponType(), equip.Variant, slot);
ret |= DrawItemSelector(equipCombo, item);
return ret;
}
private bool DrawWeapon(EquipSlot slot, ActorWeapon weapon)
{
var (equipCombo, stainCombo) = _combos[slot];
var ret = DrawColorSelector(stainCombo, slot, weapon.Stain);
ImGui.SameLine();
var item = _identifier.Identify(weapon.Set, weapon.Type, weapon.Variant, slot);
ret |= DrawItemSelector(equipCombo, item);
return ret;
}
public void UpdateActors(Actor actor)
{
var newEquip = _playerWatcher.UpdateActorWithoutEvent(actor);
GlamourerPlugin.Penumbra?.RedrawActor(actor, RedrawType.WithSettings);
var gPose = _actors[GPoseActorId];
var player = _actors[0];
if (gPose != null && actor.Address == gPose.Address && player != null)
newEquip.Write(player.Address);
}
private SubRace _currentSubRace = SubRace.Midlander;
private Gender _currentGender = Gender.Male;
private CustomizationId _currentCustomization = CustomizationId.Hairstyle;
private static readonly string[]
SubRaceNames = ((SubRace[]) Enum.GetValues(typeof(SubRace))).Skip(1).Select(s => s.ToName()).ToArray();
private void DrawStuff()
{
if (ImGui.BeginCombo("SubRace", _currentSubRace.ToString()))
{
for (var i = 0; i < SubRaceNames.Length; ++i)
{
if (ImGui.Selectable(SubRaceNames[i], (int) _currentSubRace == i + 1))
_currentSubRace = (SubRace) (i + 1);
}
ImGui.EndCombo();
}
if (ImGui.BeginCombo("Gender", _currentGender.ToName()))
{
if (ImGui.Selectable(Gender.Male.ToName(), _currentGender == Gender.Male))
_currentGender = Gender.Male;
if (ImGui.Selectable(Gender.Female.ToName(), _currentGender == Gender.Female))
_currentGender = Gender.Female;
ImGui.EndCombo();
}
var set = GlamourerPlugin.Customization.GetList(_currentSubRace, _currentGender);
if (ImGui.BeginCombo("Customization", _currentCustomization.ToString()))
{
foreach (CustomizationId customizationId in Enum.GetValues(typeof(CustomizationId)))
{
if (!set.IsAvailable(customizationId))
continue;
if (ImGui.Selectable(customizationId.ToString(), customizationId == _currentCustomization))
_currentCustomization = customizationId;
}
ImGui.EndCombo();
}
var count = set.Count(_currentCustomization);
var tmp = 0;
switch (_currentCustomization.ToType(_currentSubRace.ToRace() == Race.Hrothgar))
{
case CharaMakeParams.MenuType.ColorPicker:
{
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.PushStyle(ImGuiStyleVar.FrameRounding, 0f);
for (var i = 0; i < count; ++i)
{
var data = set.Data(_currentCustomization, i);
ImGui.ColorButton($"{data.Value}", ImGui.ColorConvertU32ToFloat4(data.Color));
if (i % 8 != 7)
ImGui.SameLine();
}
}
break;
case CharaMakeParams.MenuType.Percentage:
ImGui.SliderInt("Percentage", ref tmp, 0, 100);
break;
case CharaMakeParams.MenuType.ListSelector:
ImGui.Combo("List", ref tmp, Enumerable.Range(0, count).Select(i => $"{_currentCustomization} #{i}").ToArray(), count);
break;
case CharaMakeParams.MenuType.IconSelector:
case CharaMakeParams.MenuType.MultiIconSelector:
{
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.PushStyle(ImGuiStyleVar.FrameRounding, 0f);
for (var i = 0; i < count; ++i)
{
var data = set.Data(_currentCustomization, i);
var texture = GlamourerPlugin.Customization.GetIcon(data.IconId);
ImGui.ImageButton(texture.ImGuiHandle, FeatureIconSize * ImGui.GetIO().FontGlobalScale);
if (ImGui.IsItemHovered())
{
using var tooltip = ImGuiRaii.NewTooltip();
ImGui.Image(texture.ImGuiHandle, new Vector2(texture.Width, texture.Height));
}
if (i % 4 != 3)
ImGui.SameLine();
}
}
break;
}
}
private void Draw()
{
ImGui.SetNextWindowSizeConstraints(Vector2.One * 600, Vector2.One * 5000);
if (!_visible || !ImGui.Begin(_glamourerHeader))
return;
try
{
if (ImGui.BeginCombo("Actor", _currentActorName))
{
var idx = 0;
foreach (var actor in GlamourerPlugin.PluginInterface.ClientState.Actors.Where(a => a.ObjectKind == ObjectKind.Player))
{
if (ImGui.Selectable($"{actor.Name}##{idx++}"))
_currentActorName = actor.Name;
}
ImGui.EndCombo();
}
_player = _actors[GPoseActorId] ?? _actors[0];
if (_player == null || !GlamourerPlugin.PluginInterface.ClientState.Condition.Any())
{
ImGui.TextColored(new Vector4(0.4f, 0.1f, 0.1f, 1f),
"No player character available.");
}
else
{
var equip = new ActorEquipment(_player);
var changes = false;
changes |= DrawWeapon(EquipSlot.MainHand, equip.MainHand);
changes |= DrawWeapon(EquipSlot.OffHand, equip.OffHand);
changes |= DrawEquip(EquipSlot.Head, equip.Head);
changes |= DrawEquip(EquipSlot.Body, equip.Body);
changes |= DrawEquip(EquipSlot.Hands, equip.Hands);
changes |= DrawEquip(EquipSlot.Legs, equip.Legs);
changes |= DrawEquip(EquipSlot.Feet, equip.Feet);
changes |= DrawEquip(EquipSlot.Ears, equip.Ears);
changes |= DrawEquip(EquipSlot.Neck, equip.Neck);
changes |= DrawEquip(EquipSlot.Wrists, equip.Wrists);
changes |= DrawEquip(EquipSlot.RFinger, equip.RFinger);
changes |= DrawEquip(EquipSlot.LFinger, equip.LFinger);
if (changes)
UpdateActors(_player);
}
DrawStuff();
}
finally
{
ImGui.End();
}
}
}
}

170
Glamourer/Main.cs Normal file
View file

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Gui;
using ImGuiNET;
using Penumbra.Api;
using CommandManager = Glamourer.Managers.CommandManager;
namespace Glamourer
{
internal class Glamourer
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly CommandManager _commands;
public Glamourer(DalamudPluginInterface pi)
{
_pluginInterface = pi;
_commands = new CommandManager(_pluginInterface);
}
}
public class GlamourerPlugin : IDalamudPlugin
{
public const int RequiredPenumbraShareVersion = 1;
public string Name
=> "Glamourer";
public static DalamudPluginInterface PluginInterface = null!;
private Glamourer _glamourer = null!;
private Interface _interface = null!;
public static ICustomizationManager Customization = null!;
public static string Version = string.Empty;
public static IPenumbraApi? Penumbra;
private Dalamud.Dalamud _dalamud = null!;
private List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> _plugins = null!;
private void SetDalamud(DalamudPluginInterface pi)
{
var dalamud = (Dalamud.Dalamud?) pi.GetType()
?.GetField("dalamud", BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(pi);
_dalamud = dalamud ?? throw new Exception("Could not obtain Dalamud.");
}
private void PenumbraTooltip(object? it)
{
if (it is Lumina.Excel.GeneratedSheets.Item)
ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]");
}
private void PenumbraRightClick(MouseButton button, object? it)
{
if (button == MouseButton.Right && it is Lumina.Excel.GeneratedSheets.Item item)
{
var actors = PluginInterface.ClientState.Actors;
var player = actors[Interface.GPoseActorId] ?? actors[0];
if (player != null)
{
var writeItem = new Item(item, string.Empty);
writeItem.Write(player.Address);
_interface.UpdateActors(player);
}
}
}
private void RegisterFunctions()
{
if (Penumbra == null || !Penumbra.Valid)
return;
Penumbra!.ChangedItemTooltip += PenumbraTooltip;
Penumbra!.ChangedItemClicked += PenumbraRightClick;
}
private void UnregisterFunctions()
{
if (Penumbra == null || !Penumbra.Valid)
return;
Penumbra!.ChangedItemTooltip -= PenumbraTooltip;
Penumbra!.ChangedItemClicked -= PenumbraRightClick;
}
private void SetPlugins(DalamudPluginInterface pi)
{
var pluginManager = _dalamud?.GetType()
?.GetProperty("PluginManager", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(_dalamud);
if (pluginManager == null)
throw new Exception("Could not obtain plugin manager.");
var pluginsList =
(List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>?) pluginManager
?.GetType()
?.GetProperty("Plugins", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(pluginManager);
_plugins = pluginsList ?? throw new Exception("Could not obtain Dalamud.");
}
private bool GetPenumbra()
{
if (Penumbra?.Valid ?? false)
return true;
var plugin = _plugins.Find(p
=> p.Definition.InternalName == "Penumbra"
&& string.Compare(p.Definition.AssemblyVersion, "0.4.0.3", StringComparison.Ordinal) >= 0).Plugin;
var penumbra = (IPenumbraApiBase?) plugin?.GetType().GetProperty("Api", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(plugin);
if (penumbra != null && penumbra.Valid && penumbra.ApiVersion >= RequiredPenumbraShareVersion)
{
Penumbra = (IPenumbraApi) penumbra!;
RegisterFunctions();
}
else
{
Penumbra = null;
}
return Penumbra != null;
}
public void Initialize(DalamudPluginInterface pluginInterface)
{
Version = Assembly.GetExecutingAssembly()?.GetName().Version.ToString() ?? "";
PluginInterface = pluginInterface;
Customization = CustomizationManager.Create(PluginInterface);
SetDalamud(PluginInterface);
SetPlugins(PluginInterface);
GetPenumbra();
PluginInterface.CommandManager.AddHandler("/glamour", new CommandInfo(OnCommand)
{
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
});
_glamourer = new Glamourer(PluginInterface);
_interface = new Interface();
}
public void OnCommand(string command, string arguments)
{
if (GetPenumbra())
Penumbra!.RedrawAll(RedrawType.WithSettings);
else
PluginLog.Information("Could not get Penumbra.");
}
public void Dispose()
{
UnregisterFunctions();
_interface?.Dispose();
PluginInterface.CommandManager.RemoveHandler("/glamour");
PluginInterface.Dispose();
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Plugin;
using Glamourer.SeFunctions;
namespace Glamourer.Managers
{
public class CommandManager
{
private readonly ProcessChatBox _processChatBox;
private readonly Dalamud.Game.Command.CommandManager _dalamudCommands;
private readonly IntPtr _uiModulePtr;
public CommandManager(DalamudPluginInterface pi, BaseUiObject baseUiObject, GetUiModule getUiModule, ProcessChatBox processChatBox)
{
_dalamudCommands = pi.CommandManager;
_processChatBox = processChatBox;
_uiModulePtr = getUiModule.Invoke(Marshal.ReadIntPtr(baseUiObject.Address));
}
public CommandManager(DalamudPluginInterface pi)
: this(pi, new BaseUiObject(pi.TargetModuleScanner), new GetUiModule(pi.TargetModuleScanner),
new ProcessChatBox(pi.TargetModuleScanner))
{ }
public bool Execute(string message)
{
// First try to process the command through Dalamud.
if (_dalamudCommands.ProcessCommand(message))
{
PluginLog.Verbose("Executed Dalamud command \"{Message:l}\".", message);
return true;
}
if (_uiModulePtr == IntPtr.Zero)
{
PluginLog.Error("Can not execute \"{Message:l}\" because no uiModulePtr is available.", message);
return false;
}
// Then prepare a string to send to the game itself.
var (text, length) = PrepareString(message);
var payload = PrepareContainer(text, length);
_processChatBox.Invoke(_uiModulePtr, payload, IntPtr.Zero, (byte) 0);
Marshal.FreeHGlobal(payload);
Marshal.FreeHGlobal(text);
return false;
}
private static (IntPtr, long) PrepareString(string message)
{
var bytes = Encoding.UTF8.GetBytes(message);
var mem = Marshal.AllocHGlobal(bytes.Length + 30);
Marshal.Copy(bytes, 0, mem, bytes.Length);
Marshal.WriteByte(mem + bytes.Length, 0);
return (mem, bytes.Length + 1);
}
private static IntPtr PrepareContainer(IntPtr message, long length)
{
var mem = Marshal.AllocHGlobal(400);
Marshal.WriteInt64(mem, message.ToInt64());
Marshal.WriteInt64(mem + 0x8, 64);
Marshal.WriteInt64(mem + 0x10, length);
Marshal.WriteInt64(mem + 0x18, 0);
return mem;
}
}
}

View file

@ -0,0 +1,11 @@
using Dalamud.Game;
namespace Glamourer.SeFunctions
{
public sealed class BaseUiObject : SeAddressBase
{
public BaseUiObject(SigScanner sigScanner)
: base(sigScanner, "48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8")
{ }
}
}

View file

@ -0,0 +1,14 @@
using System;
using Dalamud.Game;
namespace Glamourer.SeFunctions
{
public delegate IntPtr GetUiModuleDelegate(IntPtr baseUiObj);
public sealed class GetUiModule : SeFunctionBase<GetUiModuleDelegate>
{
public GetUiModule(SigScanner sigScanner)
: base(sigScanner, "E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0")
{ }
}
}

View file

@ -0,0 +1,14 @@
using System;
using Dalamud.Game;
namespace Glamourer.SeFunctions
{
public delegate IntPtr ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unk1, byte unk2);
public sealed class ProcessChatBox : SeFunctionBase<ProcessChatBoxDelegate>
{
public ProcessChatBox(SigScanner sigScanner)
: base(sigScanner, "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9")
{ }
}
}

View file

@ -0,0 +1,20 @@
using System;
using Dalamud.Game;
using Dalamud.Plugin;
namespace Glamourer.SeFunctions
{
public class SeAddressBase
{
public readonly IntPtr Address;
public SeAddressBase(SigScanner sigScanner, string signature, int offset = 0)
{
Address = sigScanner.GetStaticAddressFromSig(signature);
if (Address != IntPtr.Zero)
Address += offset;
var baseOffset = (ulong) Address.ToInt64() - (ulong) sigScanner.Module.BaseAddress.ToInt64();
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{baseOffset:X16}.");
}
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Hooking;
using Dalamud.Plugin;
namespace Glamourer.SeFunctions
{
public class SeFunctionBase<T> where T : Delegate
{
public IntPtr Address;
protected T? FuncDelegate;
public SeFunctionBase(SigScanner sigScanner, int offset)
{
Address = sigScanner.Module.BaseAddress + offset;
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{offset:X16}.");
}
public SeFunctionBase(SigScanner sigScanner, string signature, int offset = 0)
{
Address = sigScanner.ScanText(signature);
if (Address != IntPtr.Zero)
Address += offset;
var baseOffset = (ulong) Address.ToInt64() - (ulong) sigScanner.Module.BaseAddress.ToInt64();
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{baseOffset:X16}.");
}
public T? Delegate()
{
if (FuncDelegate != null)
return FuncDelegate;
if (Address != IntPtr.Zero)
{
FuncDelegate = Marshal.GetDelegateForFunctionPointer<T>(Address);
return FuncDelegate;
}
PluginLog.Error($"Trying to generate delegate for {GetType().Name}, but no pointer available.");
return null;
}
public dynamic? Invoke(params dynamic[] parameters)
{
if (FuncDelegate != null)
return FuncDelegate.DynamicInvoke(parameters);
if (Address != IntPtr.Zero)
{
FuncDelegate = Marshal.GetDelegateForFunctionPointer<T>(Address);
return FuncDelegate!.DynamicInvoke(parameters);
}
else
{
PluginLog.Error($"Trying to call {GetType().Name}, but no pointer available.");
return null;
}
}
public Hook<T>? CreateHook(T detour)
{
if (Address != IntPtr.Zero)
{
var hook = new Hook<T>(Address, detour);
hook.Enable();
PluginLog.Debug($"Hooked onto {GetType().Name} at address 0x{Address.ToInt64():X16}.");
return hook;
}
PluginLog.Error($"Trying to create Hook for {GetType().Name}, but no pointer available.");
return null;
}
}
}