Shit's fucked, yo

This commit is contained in:
Ottermandias 2022-07-18 18:29:01 +02:00
parent 7af38aa2ce
commit a160276cc5
12 changed files with 507 additions and 429 deletions

View file

@ -16,6 +16,9 @@ public readonly unsafe struct CharacterCustomization
public CharacterCustomization(CustomizationData* data)
=> _data = data;
public void Load(CharacterCustomization other)
=> *_data = *other._data;
public ref Race Race
=> ref _data->Race;

View file

@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Glamourer.Structs;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;

View file

@ -5,6 +5,7 @@ using Dalamud.Logging;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
@ -33,16 +34,15 @@ public class RestrictedGear
}
// Resolve a model given by its model id, variant and slot for your current race and gender.
public (bool Replaced, SetId ModelId, byte Variant) ResolveRestricted(SetId modelId, byte variant, EquipSlot slot, Race race,
Gender gender)
public (bool Replaced, CharacterArmor Armor) ResolveRestricted(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
{
var quad = modelId.Value | ((uint)variant << 16);
var quad = armor.Set.Value | ((uint)armor.Variant << 16);
// Check racial gear, this does not need slots.
if (RaceGenderGroup.Contains(quad))
{
var idx = ((int)race - 1) * 2 + (gender is Gender.Female or Gender.FemaleNpc ? 1 : 0);
var value = RaceGenderGroup[idx];
return (value != quad, (ushort)value, (byte)(value >> 16));
return (value != quad, new CharacterArmor((ushort)value, (byte)(value >> 16), armor.Stain));
}
// Check gender slots. If current gender is female, check if anything needs to be changed from male to female,
@ -52,10 +52,10 @@ public class RestrictedGear
var needle = quad | ((uint)slot.ToSlot() << 24);
if (gender is Gender.Female or Gender.FemaleNpc && _maleToFemale.TryGetValue(needle, out var newValue)
|| gender is Gender.Male or Gender.MaleNpc && _femaleToMale.TryGetValue(needle, out newValue))
return (quad != newValue, (ushort)newValue, (byte)(newValue >> 16));
return (quad != newValue, new CharacterArmor((ushort)newValue, (byte)(newValue >> 16), armor.Stain));
// The gear is not restricted.
return (false, modelId, variant);
return (false, armor);
}
// Add all unknown restricted gear and pair it with emperor's new gear on start up.

87
Glamourer/Actor.cs Normal file
View file

@ -0,0 +1,87 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.ByteString;
namespace Glamourer;
public unsafe struct Actor : IEquatable<Actor>
{
public record struct Identifier(Utf8String Name, uint Id, ushort HomeWorld, ushort Index);
public static readonly Actor Null = new() { Pointer = null };
public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator Actor(IntPtr? pointer)
=> new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)pointer.GetValueOrDefault(IntPtr.Zero) };
public static implicit operator IntPtr(Actor actor)
=> actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer;
public Identifier GetIdentifier()
{
if (Pointer == null)
return new Identifier(Utf8String.Empty, 0, 0, 0);
return new Identifier(Utf8Name, Pointer->GameObject.ObjectID, Pointer->HomeWorld, Pointer->GameObject.ObjectIndex);
}
public Character? Character
=> Pointer == null ? null : Dalamud.Objects[Pointer->GameObject.ObjectIndex] as Character;
public bool IsAvailable
=> Pointer->GameObject.GetIsTargetable();
public bool IsHuman
=> Pointer != null && Pointer->ModelCharaId == 0;
public int ModelId
=> Pointer->ModelCharaId;
public ObjectKind ObjectKind
=> (ObjectKind)Pointer->GameObject.ObjectKind;
public Utf8String Utf8Name
=> new(Pointer->GameObject.Name);
public Human* DrawObject
=> (Human*)Pointer->GameObject.DrawObject;
public void SetModelId(int value)
{
if (Pointer != null)
Pointer->ModelCharaId = value;
}
public static implicit operator bool(Actor actor)
=> actor.Pointer != null;
public static bool operator true(Actor actor)
=> actor.Pointer != null;
public static bool operator false(Actor actor)
=> actor.Pointer == null;
public static bool operator !(Actor actor)
=> actor.Pointer == null;
public bool Equals(Actor other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> ((ulong)Pointer).GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Pointer != rhs.Pointer;
}

View file

@ -126,8 +126,8 @@ public class PenumbraAttach : IDisposable
//}
}
public unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* GameObjectFromDrawObject(IntPtr drawObject)
=> (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(_drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero);
public Actor GameObjectFromDrawObject(IntPtr drawObject)
=> _drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero;
public void RedrawObject(GameObject? actor, RedrawType settings, bool repeat)
{

View file

@ -13,13 +13,13 @@ namespace Glamourer;
public class CharacterSaveConverter : JsonConverter<CharacterSave>
{
public override void WriteJson(JsonWriter writer, CharacterSave value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, CharacterSave? value, JsonSerializer serializer)
{
var s = value.ToBase64();
var s = value?.ToBase64() ?? string.Empty;
serializer.Serialize(writer, s);
}
public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave existingValue, bool hasExistingValue,
public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave? existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var token = JToken.Load(reader);
@ -28,9 +28,8 @@ public class CharacterSaveConverter : JsonConverter<CharacterSave>
}
}
[JsonConverter(typeof(CharacterSaveConverter))]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct CharacterSave
public unsafe struct CharacterData
{
[Flags]
public enum SaveFlags : byte
@ -47,42 +46,27 @@ public unsafe struct CharacterSave
public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + CustomizationData.CustomizationBytes;
public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CustomizationData.CustomizationBytes + 4 + 1;
public const byte TotalSizeVersion3 = 1 + 1 + 2 + 7 + 7 + 2 + 40 + CustomizationData.CustomizationBytes + 4;
public const byte CurrentVersion = 3;
public byte Version = CurrentVersion;
public SaveFlags Flags = 0;
public CharacterEquipMask Equip = 0;
public CharacterWeapon MainHand = default;
public CharacterWeapon OffHand = default;
public ushort Padding = 0;
public CharacterArmor Head = default;
public CharacterArmor Body = default;
public CharacterArmor Hands = default;
public CharacterArmor Legs = default;
public CharacterArmor Feet = default;
public CharacterArmor Ears = default;
public CharacterArmor Neck = default;
public CharacterArmor Wrist = default;
public CharacterArmor RFinger = default;
public CharacterArmor LFinger = default;
private CustomizationData CustomizationData = CustomizationData.Default;
public float Alpha = 1f;
public CharacterSave()
{ }
public void Load(Actor actor)
{
if (!actor.IsHuman || actor.Pointer->GameObject.DrawObject == null)
return;
var human = (Human*)actor.Pointer->GameObject.DrawObject;
CustomizationData = *(CustomizationData*)human->CustomizeData;
fixed (void* equip = &Head)
{
Functions.MemCpyUnchecked(equip, human->EquipSlotData, sizeof(CharacterArmor) * 10);
}
}
public byte Version;
public SaveFlags Flags;
public CharacterEquipMask Equip;
public CharacterWeapon MainHand;
public CharacterWeapon OffHand;
public ushort Padding;
public CharacterArmor Head;
public CharacterArmor Body;
public CharacterArmor Hands;
public CharacterArmor Legs;
public CharacterArmor Feet;
public CharacterArmor Ears;
public CharacterArmor Neck;
public CharacterArmor Wrist;
public CharacterArmor RFinger;
public CharacterArmor LFinger;
private CustomizationData CustomizationData;
public float Alpha;
public CharacterCustomization Customize
{
@ -106,11 +90,47 @@ public unsafe struct CharacterSave
}
}
public static readonly CharacterData Default
= new()
{
Version = CurrentVersion,
Flags = SaveFlags.WriteCustomizations,
Equip = CharacterEquipMask.All,
MainHand = CharacterWeapon.Empty,
OffHand = CharacterWeapon.Empty,
Padding = 0,
Head = CharacterArmor.Empty,
Body = CharacterArmor.Empty,
Hands = CharacterArmor.Empty,
Legs = CharacterArmor.Empty,
Feet = CharacterArmor.Empty,
Ears = CharacterArmor.Empty,
Neck = CharacterArmor.Empty,
Wrist = CharacterArmor.Empty,
RFinger = CharacterArmor.Empty,
LFinger = CharacterArmor.Empty,
CustomizationData = CustomizationData.Default,
Alpha = 1f,
};
public void Load(Actor actor)
{
if (!actor.IsHuman || actor.Pointer->GameObject.DrawObject == null)
return;
var human = (Human*)actor.Pointer->GameObject.DrawObject;
CustomizationData = *(CustomizationData*)human->CustomizeData;
fixed (void* equip = &Head)
{
Functions.MemCpyUnchecked(equip, human->EquipSlotData, sizeof(CharacterArmor) * 10);
}
}
public string ToBase64()
{
fixed (void* ptr = &this)
{
return Convert.ToBase64String(new ReadOnlySpan<byte>(ptr, sizeof(CharacterSave)));
return Convert.ToBase64String(new ReadOnlySpan<byte>(ptr, sizeof(CharacterData)));
}
}
@ -128,10 +148,10 @@ public unsafe struct CharacterSave
$"Can not parse Base64 string into CharacterSave:\n\tInvalid value {value} in byte {idx}, should be in [{min},{max}].");
}
public static CharacterSave FromString(string data)
public static CharacterData FromString(string data)
{
var bytes = Convert.FromBase64String(data);
var ret = new CharacterSave();
var ret = new CharacterData();
fixed (byte* ptr = bytes)
{
switch (bytes[0])
@ -156,8 +176,8 @@ public unsafe struct CharacterSave
ret.Flags |= SaveFlags.VisorState;
break;
case 3:
CheckSize(bytes.Length, sizeof(CharacterSave));
Functions.MemCpyUnchecked(&ret, ptr, sizeof(CharacterSave));
CheckSize(bytes.Length, TotalSizeVersion3);
Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion3);
break;
default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}.");
}
@ -166,3 +186,36 @@ public unsafe struct CharacterSave
return ret;
}
}
[JsonConverter(typeof(CharacterSaveConverter))]
public class CharacterSave
{
private CharacterData _data;
public CharacterSave()
=> _data = CharacterData.Default;
public CharacterSave(Actor actor)
=> _data.Load(actor);
public void Load(Actor actor)
=> _data.Load(actor);
public string ToBase64()
=> _data.ToBase64();
public CharacterCustomization Customization
=> _data.Customize;
public CharacterEquip Equipment
=> _data.Equipment;
public ref CharacterWeapon MainHand
=> ref _data.MainHand;
public ref CharacterWeapon OffHand
=> ref _data.OffHand;
public static CharacterSave FromString(string data)
=> new() { _data = CharacterData.FromString(data) };
}

View file

@ -54,71 +54,4 @@ public static unsafe class CustomizeExtensions
public static string ClanName(this CharacterCustomization customize)
=> ClanName(customize.Clan, customize.Gender);
// Change a gender and fix up all required customizations afterwards.
public static bool ChangeGender(this CharacterCustomization customize, Gender gender, CharacterEquip equip)
{
if (customize.Gender == gender)
return false;
customize.Gender = gender;
FixUpAttributes(customize, equip);
return true;
}
// Change a race and fix up all required customizations afterwards.
public static bool ChangeRace(this CharacterCustomization customize, SubRace clan, CharacterEquip equip)
{
if (customize.Clan == clan)
return false;
var race = clan.ToRace();
customize.Race = race;
customize.Clan = clan;
// TODO: Female Hrothgar
if (race == Race.Hrothgar)
customize.Gender = Gender.Male;
FixUpAttributes(customize, equip);
return true;
}
// Go through a whole customization struct and fix up all settings that need fixing.
private static void FixUpAttributes(CharacterCustomization customize, CharacterEquip equip)
{
var set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId)))
{
switch (id)
{
case CustomizationId.Race: break;
case CustomizationId.Clan: break;
case CustomizationId.BodyType: break;
case CustomizationId.Gender: break;
case CustomizationId.FacialFeaturesTattoos: break;
case CustomizationId.HighlightsOnFlag: break;
case CustomizationId.Face: break;
default:
var count = set.Count(id);
if (set.DataByValue(id, customize[id], out _) < 0)
customize[id] = count == 0 ? (byte)0 : set.Data(id, 0).Value;
break;
}
}
if (!equip)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var item = equip[slot];
var (replaced, newSet, newVariant) =
Glamourer.RestrictedGear.ResolveRestricted(item.Set, item.Variant, slot, customize.Race, customize.Gender);
if (replaced)
equip[slot] = new CharacterArmor(newSet, newVariant, item.Stain);
}
}
}

View file

@ -48,7 +48,6 @@ public class Glamourer : IDalamudPlugin
public static ICustomizationManager Customization = null!;
public static RedrawManager RedrawManager = null!;
public static RestrictedGear RestrictedGear = null!;
private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly Interface _interface;
@ -61,8 +60,6 @@ public class Glamourer : IDalamudPlugin
{
Dalamud.Initialize(pluginInterface);
Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
RestrictedGear = GameData.RestrictedGear(Dalamud.GameData);
var m = global::Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
Config = GlamourerConfig.Load();
Penumbra = new PenumbraAttach(Config.AttachToPenumbra);
@ -83,7 +80,6 @@ public class Glamourer : IDalamudPlugin
_interface = new Interface(this);
_windowSystem.AddWindow(_interface);
Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw;
var x = 0x00011000u;
//FixedDesignManager.Flag((Human*)((Actor)Dalamud.ClientState.LocalPlayer?.Address).Pointer->GameObject.DrawObject, 0, &x);
}

View file

@ -1,16 +1,6 @@
using System;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
@ -24,8 +14,7 @@ internal partial class Interface
{
private class ActorTab
{
private ObjectManager.ActorData _data = new(string.Empty, string.Empty, Actor.Null, false, Actor.Null);
private CharacterSave _character = new();
private ObjectManager.ActorData _data = new(string.Empty, new Actor.Identifier(), Actor.Null, false, Actor.Null);
private Actor _nextSelect = Actor.Null;
public void Draw()
@ -49,9 +38,12 @@ internal partial class Interface
private void DrawActorPanel()
{
using var group = ImRaii.Group();
if (DrawCustomization(_character.Customize, _character.Equipment, !_data.Modifiable))
if (!Glamourer.RedrawManager.CurrentManipulations.GetSave(_data.Actor, out var save))
return;
if (DrawCustomization(save.Customization, save.Equipment, !_data.Modifiable))
{
Glamourer.RedrawManager.Set(_data.Actor.Address, _character);
//Glamourer.RedrawManager.Set(_data.Actor.Address, _character);
Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
}
@ -59,6 +51,11 @@ internal partial class Interface
{
Glamourer.RedrawManager.ChangeEquip(_data.Actor.Address, EquipSlot.Head, new CharacterArmor(265, 1, 0));
}
if (ImGui.Button("Set Weapon"))
{
Glamourer.RedrawManager.LoadWeapon(_data.Actor.Address, new CharacterWeapon(0x00C9, 0x004E, 0x0001, 0x00), new CharacterWeapon(0x0065, 0x003D, 0x0001, 0x00));
}
}
private void DrawMonsterPanel()
@ -115,7 +112,7 @@ internal partial class Interface
private void UpdateSelection(ObjectManager.ActorData data)
{
_data = data;
_character.Load(_data.Actor);
//_character.Load(_data.Actor);
}
private bool CheckFilter(ObjectManager.ActorData data)

View file

@ -80,7 +80,7 @@ internal partial class Interface
return false;
var gender = customize.Gender == Gender.Male ? Gender.Female : Gender.Male;
return customize.ChangeGender(gender, locked ? CharacterEquip.Null : equip);
return false; //customize.ChangeGender(gender, locked ? CharacterEquip.Null : equip);
}
private static bool DrawRaceCombo(CharacterCustomization customize, CharacterEquip equip, bool locked)
@ -98,7 +98,7 @@ internal partial class Interface
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
{
if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, customize.Gender), subRace == customize.Clan))
ret |= customize.ChangeRace(subRace, equip);
ret |= false; //customize.ChangeRace(subRace, equip);
}
return ret;

View file

@ -1,71 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.GameData.ByteString;
namespace Glamourer;
public unsafe struct Actor : IEquatable<Actor>
{
public static readonly Actor Null = new() { Pointer = null };
public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator Actor(IntPtr? pointer)
=> new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)pointer.GetValueOrDefault(IntPtr.Zero) };
public static implicit operator IntPtr(Actor actor)
=> actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer;
public Character? Character
=> Pointer == null ? null : Dalamud.Objects[Pointer->GameObject.ObjectIndex] as Character;
public bool IsAvailable
=> Pointer->GameObject.GetIsTargetable();
public bool IsHuman
=> Pointer != null && Pointer->ModelCharaId == 0;
public int ModelId
=> Pointer != null ? Pointer->ModelCharaId : 0;
public void SetModelId(int value)
{
if (Pointer != null)
Pointer->ModelCharaId = value;
}
public static implicit operator bool(Actor actor)
=> actor.Pointer != null;
public static bool operator true(Actor actor)
=> actor.Pointer != null;
public static bool operator false(Actor actor)
=> actor.Pointer == null;
public static bool operator !(Actor actor)
=> actor.Pointer == null;
public bool Equals(Actor other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> ((ulong)Pointer).GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Pointer != rhs.Pointer;
}
public static class ObjectManager
{
private const int GPosePlayerIndex = 201;
@ -73,8 +12,9 @@ public static class ObjectManager
private const int ExamineScreenIndex = 241;
private const int FittingRoomIndex = 242;
private const int DyePreviewIndex = 243;
private static readonly Dictionary<string, int> _nameCounters = new();
private static readonly Dictionary<string, Actor> _gPoseActors = new(CharacterScreenIndex - GPosePlayerIndex);
private static readonly Dictionary<Utf8String, int> NameCounters = new();
private static readonly Dictionary<Actor.Identifier, Actor> GPoseActors = new(CharacterScreenIndex - GPosePlayerIndex);
public static bool IsInGPose()
=> Dalamud.Objects[GPosePlayerIndex] != null;
@ -85,88 +25,88 @@ public static class ObjectManager
public static Actor Player
=> Dalamud.ClientState.LocalPlayer?.Address;
public record struct ActorData(string Label, string Name, Actor Actor, bool Modifiable, Actor GPose);
public record struct ActorData(string Label, Actor.Identifier Identifier, Actor Actor, bool Modifiable, Actor GPose);
public static IEnumerable<ActorData> GetEnumerator()
{
_nameCounters.Clear();
_gPoseActors.Clear();
NameCounters.Clear();
GPoseActors.Clear();
for (var i = GPosePlayerIndex; i < CharacterScreenIndex; ++i)
{
var character = Dalamud.Objects[i];
if (character == null)
Actor character = Dalamud.Objects[i]?.Address;
if (!character)
break;
var name = character.Name.TextValue;
_gPoseActors[name] = character.Address;
yield return new ActorData(GetLabel(character, name, 0, true), name, character.Address, true, Actor.Null);
var identifier = character.GetIdentifier();
GPoseActors[identifier] = character.Address;
yield return new ActorData(GetLabel(character, identifier.Name.ToString(), 0, true), identifier, character.Address, true,
Actor.Null);
}
var actor = Dalamud.Objects[CharacterScreenIndex];
if (actor != null)
yield return new ActorData("Character Screen Actor", string.Empty, actor.Address, false, Actor.Null);
Actor actor = Dalamud.Objects[CharacterScreenIndex]?.Address;
if (actor)
yield return new ActorData("Character Screen Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null);
actor = Dalamud.Objects[ExamineScreenIndex];
if (actor != null)
yield return new ActorData("Examine Screen Actor", string.Empty, actor.Address, false, Actor.Null);
actor = Dalamud.Objects[ExamineScreenIndex]?.Address;
if (actor)
yield return new ActorData("Examine Screen Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null);
actor = Dalamud.Objects[FittingRoomIndex];
if (actor != null)
yield return new ActorData("Fitting Room Actor", string.Empty, actor.Address, false, Actor.Null);
actor = Dalamud.Objects[FittingRoomIndex]?.Address;
if (actor)
yield return new ActorData("Fitting Room Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null);
actor = Dalamud.Objects[DyePreviewIndex];
if (actor != null)
yield return new ActorData("Dye Preview Actor", string.Empty, actor.Address, false, Actor.Null);
actor = Dalamud.Objects[DyePreviewIndex]?.Address;
if (actor)
yield return new ActorData("Dye Preview Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null);
for (var i = 0; i < GPosePlayerIndex; ++i)
{
var character = Dalamud.Objects[i];
if (character == null
|| character.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion
actor = Dalamud.Objects[i]?.Address;
if (!actor
|| actor.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion
or ObjectKind.Retainer))
continue;
var name = character.Name.TextValue;
if (name.Length == 0)
var identifier = actor.GetIdentifier();
if (identifier.Name.Length == 0)
continue;
if (_nameCounters.TryGetValue(name, out var num))
_nameCounters[name] = ++num;
if (NameCounters.TryGetValue(identifier.Name, out var num))
NameCounters[identifier.Name] = ++num;
else
_nameCounters[name] = num = 1;
NameCounters[identifier.Name] = num = 1;
if (!_gPoseActors.TryGetValue(name, out var gPose))
if (!GPoseActors.TryGetValue(identifier, out var gPose))
gPose = Actor.Null;
yield return new ActorData(GetLabel(character, name, num, false), name, character.Address, true, gPose);
yield return new ActorData(GetLabel(actor, identifier.Name.ToString(), num, false), identifier, actor.Address, true, gPose);
}
for (var i = DyePreviewIndex + 1; i < Dalamud.Objects.Length; ++i)
{
var character = Dalamud.Objects[i];
if (character == null
|| !((Actor)character.Address).IsAvailable
|| character.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion
actor = Dalamud.Objects[i]?.Address;
if (!actor
|| actor.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion
or ObjectKind.Retainer))
continue;
var name = character.Name.TextValue;
if (name.Length == 0)
var identifier = actor.GetIdentifier();
if (identifier.Name.Length == 0)
continue;
if (_nameCounters.TryGetValue(name, out var num))
_nameCounters[name] = ++num;
if (NameCounters.TryGetValue(identifier.Name, out var num))
NameCounters[identifier.Name] = ++num;
else
_nameCounters[name] = num = 1;
NameCounters[identifier.Name] = num = 1;
if (!_gPoseActors.TryGetValue(name, out var gPose))
if (!GPoseActors.TryGetValue(identifier, out var gPose))
gPose = Actor.Null;
yield return new ActorData(GetLabel(character, name, num, false), name, character.Address, true, gPose);
yield return new ActorData(GetLabel(actor, identifier.Name.ToString(), num, false), identifier, actor.Address, true, gPose);
}
}
private static unsafe string GetLabel(GameObject player, string playerName, int num, bool gPose)
private static unsafe string GetLabel(Actor player, string playerName, int num, bool gPose)
{
if (player.ObjectKind == ObjectKind.Player)
return gPose ? $"{playerName} (GPose)" : num == 1 ? playerName : $"{playerName} #{num}";

View file

@ -1,211 +1,283 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer;
public unsafe class RedrawManager : IDisposable
public class CurrentManipulations
{
private readonly RestrictedGear _restrictedGear = GameData.RestrictedGear(Dalamud.GameData);
private readonly Dictionary<Actor.Identifier, CharacterSave> _characterSaves = new();
public CharacterSave CreateSave(Actor actor)
{
var id = actor.GetIdentifier();
if (_characterSaves.TryGetValue(actor.GetIdentifier(), out var save))
return save;
save = new CharacterSave(actor);
_characterSaves.Add(id with { Name = id.Name.Clone() }, save);
return save;
}
public bool GetSave(Actor actor, [NotNullWhen(true)] out CharacterSave? save)
{
save = null;
return actor && _characterSaves.TryGetValue(actor.GetIdentifier(), out save);
}
public bool GetSave(Actor.Identifier identifier, [NotNullWhen(true)] out CharacterSave? save)
=> _characterSaves.TryGetValue(identifier, out save);
public CharacterArmor? ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data)
{
var save = CreateSave(actor);
(_, data) = _restrictedGear.ResolveRestricted(data, slot, save.Customization.Race, save.Customization.Gender);
if (save.Equipment[slot] == data)
return null;
save.Equipment[slot] = data;
return data;
}
public bool ChangeWeapon(Actor actor, CharacterWeapon main)
{
var save = CreateSave(actor);
if (save.MainHand == main)
return false;
save.MainHand = main;
return true;
}
public bool ChangeWeapon(Actor actor, CharacterWeapon main, CharacterWeapon off)
{
var save = CreateSave(actor);
if (main == save.MainHand && off == save.OffHand)
return false;
save.MainHand = main;
save.OffHand = off;
return true;
}
public void ChangeCustomization(Actor actor, CharacterCustomization customize)
{
var save = CreateSave(actor);
FixRestrictedGear(save, customize.Gender, customize.Race);
save.Customization.Load(customize);
}
public bool ChangeCustomization(Actor actor, CustomizationId id, byte value)
{
if (id == CustomizationId.Race)
return ChangeRace(actor, (SubRace)value);
if (id == CustomizationId.Gender)
return ChangeGender(actor, (Gender)value);
var save = CreateSave(actor);
var customize = save.Customization;
if (customize[id] != value)
return false;
customize[id] = value;
return true;
}
// Change a gender and fix up all required customizations afterwards.
public bool ChangeGender(Actor actor, Gender gender)
{
var save = CreateSave(actor);
if (save.Customization.Gender == gender)
return false;
var customize = save.Customization;
FixRestrictedGear(save, gender, customize.Race);
FixUpAttributes(customize);
return true;
}
// Change a race and fix up all required customizations afterwards.
public bool ChangeRace(Actor actor, SubRace clan)
{
var save = CreateSave(actor);
if (save.Customization.Clan == clan)
return false;
var customize = save.Customization;
var race = clan.ToRace();
var gender = race == Race.Hrothgar ? Gender.Male : customize.Gender; // TODO Female Hrothgar
FixRestrictedGear(save, gender, race);
customize.Gender = gender;
customize.Race = race;
customize.Clan = clan;
FixUpAttributes(customize);
return true;
}
// Go through a whole customization struct and fix up all settings that need fixing.
private void FixUpAttributes(CharacterCustomization customize)
{
var set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId)))
{
switch (id)
{
case CustomizationId.Race: break;
case CustomizationId.Clan: break;
case CustomizationId.BodyType: break;
case CustomizationId.Gender: break;
case CustomizationId.FacialFeaturesTattoos: break;
case CustomizationId.HighlightsOnFlag: break;
case CustomizationId.Face: break;
default:
var count = set.Count(id);
if (set.DataByValue(id, customize[id], out _) < 0)
customize[id] = count == 0 ? (byte)0 : set.Data(id, 0).Value;
break;
}
}
}
private void FixRestrictedGear(CharacterSave save, Gender gender, Race race)
{
if (race == save.Customization.Race && gender == save.Customization.Gender)
return;
var equip = save.Equipment;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
(_, equip[slot]) = _restrictedGear.ResolveRestricted(equip[slot], slot, race, gender);
}
}
public unsafe partial class RedrawManager
{
public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data);
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A")]
public Hook<FlagSlotForUpdateDelegate> FlagSlotForUpdateHook = null!;
// This gets called when one of the ten equip items of an existing draw object gets changed.
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A", DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegate>? _flagSlotForUpdateHook;
private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slot, CharacterArmor* data)
{
return FlagSlotForUpdateHook.Original(drawObject, slot, data);
//try
//{
// var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
// if (actor && CurrentManipulations.GetSave(actor, out _))
// // TODO fixed design
//
// *data = CurrentManipulations.ChangeEquip(actor, slot.ToEquipSlot(), *data) ?? *data;
//}
//catch
//{
// // ignored
//}
return _flagSlotForUpdateHook!.Original(drawObject, slot, data);
}
public delegate void LoadWeaponDelegate(IntPtr characterOffset, uint slot, ulong data, byte unk);
[Signature("E8 ?? ?? ?? ?? 44 8B 9F")]
public Hook<LoadWeaponDelegate> LoadWeaponHook = null!;
private void LoadWeaponDetour(IntPtr characterOffset, uint slot, ulong data, byte unk)
public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data)
{
const int offset = 0xD8 * 8;
PluginLog.Information($"0x{characterOffset:X}, 0x{characterOffset - offset:X}, {slot}, {data:16X}, {unk}");
LoadWeaponHook.Original(characterOffset, slot, data, unk);
if (actor && CurrentManipulations.ChangeEquip(actor, slot, data).HasValue && actor.DrawObject != null)
return _flagSlotForUpdateHook?.Original(actor.DrawObject, slot.ToIndex(), &data) != 0;
return false;
}
}
public unsafe partial class RedrawManager
{
// The character weapon object manipulated is inside the actual character.
public const int CharacterWeaponOffset = 0xD8 * 8;
public delegate void LoadWeaponDelegate(IntPtr offsetCharacter, uint slot, CharacterWeapon weapon, byte unk1, byte unk2, byte unk3,
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.
// unk1 seems to be 0 when re-equipping and 1 when redrawing the entire actor.
// unk2 seemed to always be 1.
// unk3 seemed to always be 0.
// unk4 seemed to be the same as unk1.
[Signature("E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate>? _loadWeaponHook;
private void LoadWeaponDetour(IntPtr characterOffset, uint slot, CharacterWeapon weapon, byte unk1, byte unk2, byte unk3, byte unk4)
{
_loadWeaponHook!.Original(characterOffset, slot, weapon, unk1, unk2, unk3, unk4);
}
//[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A",
// DetourName = nameof(FlagSlotForUpdateDetour))]
//public Hook<FlagSlotForUpdateDelegate>? FlagSlotForUpdateHook;
//
// public readonly FixedDesigns FixedDesigns;
//
// Load a specific weapon for a character by its data and slot.
public void LoadWeapon(IntPtr character, EquipSlot slot, CharacterWeapon weapon)
{
switch (slot)
{
case EquipSlot.MainHand:
LoadWeaponDetour(character + CharacterWeaponOffset, 0, weapon, 1, 1, 0, 1);
return;
case EquipSlot.OffHand:
LoadWeaponDetour(character + CharacterWeaponOffset, 1, weapon, 1, 1, 0, 1);
return;
case EquipSlot.BothHand:
LoadWeaponDetour(character + CharacterWeaponOffset, 0, weapon, 1, 1, 0, 1);
LoadWeaponDetour(character + CharacterWeaponOffset, 1, CharacterWeapon.Empty, 1, 1, 0, 1);
return;
// function can also be called with '2', but does not seem to ever be.
}
}
private readonly Dictionary<string, CharacterSave> _currentRedraws = new(32);
public void LoadWeapon(Character* character, EquipSlot slot, CharacterWeapon weapon)
=> LoadWeapon((IntPtr)character, slot, weapon);
// Load specific Main- and Offhand weapons.
public void LoadWeapon(IntPtr character, CharacterWeapon main, CharacterWeapon off)
{
LoadWeaponDetour(character + CharacterWeaponOffset, 0, main, 1, 1, 0, 1);
LoadWeaponDetour(character + CharacterWeaponOffset, 1, off, 1, 1, 0, 1);
}
public void LoadWeapon(Character* character, CharacterWeapon main, CharacterWeapon off)
=> LoadWeapon((IntPtr)character, main, off);
}
public unsafe partial class RedrawManager : IDisposable
{
internal readonly CurrentManipulations CurrentManipulations = new();
public RedrawManager()
{
SignatureHelper.Initialise(this);
// FixedDesigns = new FixedDesigns(designs);
Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw;
FlagSlotForUpdateHook.Enable();
LoadWeaponHook.Enable();
//
// if (Glamourer.Config.ApplyFixedDesigns)
// Enable();
//_flagSlotForUpdateHook?.Enable();
//_loadWeaponHook?.Enable();
}
public void Dispose()
{
FlagSlotForUpdateHook.Dispose();
LoadWeaponHook.Dispose();
_flagSlotForUpdateHook?.Dispose();
_loadWeaponHook?.Dispose();
Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw;
//FlagSlotForUpdateHook?.Dispose();
}
public void Set(Character* actor, CharacterSave save)
{
var name = GetName(actor);
if (name.Length == 0)
return;
_currentRedraws[name] = save;
}
public void Set(IntPtr actor, CharacterSave save)
=> Set((Character*)actor, save);
public void Revert(Character* actor)
=> _currentRedraws.Remove(GetName(actor));
public void Revert(IntPtr actor)
=> Revert((Character*)actor);
private static string GetName(Character* actor)
{
return string.Concat(new Utf8String(actor->GameObject.Name)
.Select(c => (char)c)
.Append(actor->GameObject.ObjectKind == (byte)ObjectKind.Pc ? (char)actor->HomeWorld : (char)actor->GameObject.ObjectIndex));
}
private void Cleanup(object? _, ushort _1)
=> _currentRedraws.Clear();
public void ChangeEquip(Human* actor, EquipSlot slot, CharacterArmor item)
=> Flag(actor, slot.ToIndex(), &item);
public void ChangeEquip(Character* character, EquipSlot slot, CharacterArmor item)
=> ChangeEquip((Human*)character->GameObject.DrawObject, slot, item);
public void ChangeEquip(IntPtr character, EquipSlot slot, CharacterArmor item)
=> ChangeEquip((Character*)character, slot, item);
private void OnCharacterRedraw(IntPtr addr, IntPtr modelId, IntPtr customize, IntPtr equipData)
{
var name = GetName((Character*)addr);
if (_currentRedraws.TryGetValue(name, out var save))
{
*(CustomizationData*)customize = *(CustomizationData*)save.Customize.Address;
var equip = (CharacterEquip)equipData;
var newEquip = save.Equipment;
for (var i = 0; i < 10; ++i)
equip[i] = newEquip[i];
}
//*(uint*)modelId = 0;
//var human = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)addr;
//if (human->GameObject.ObjectKind is (byte)ObjectKind.EventNpc or (byte)ObjectKind.BattleNpc or (byte)ObjectKind.Player
// && human->ModelCharaId == 0)
//if (CurrentManipulations.GetSave(addr, out var save))
//{
// var name = new Utf8String(human->GameObject.Name).ToString();
// if (FixedDesigns.EnabledDesigns.TryGetValue(name, out var designs))
// {
// var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(human->ClassJob));
// if (design != null)
// {
// if (design.Design.Data.WriteCustomizations)
// *(CharacterCustomization*)customize = design.Design.Data.Customizations;
//
// var data = (uint*)equipData;
// for (var i = 0u; i < 10; ++i)
// {
// var slot = i.ToEquipSlot();
// if (design.Design.Data.WriteEquipment.Fits(slot))
// data[i] = slot switch
// {
// EquipSlot.Head => design.Design.Data.Equipment.Head.Value,
// EquipSlot.Body => design.Design.Data.Equipment.Body.Value,
// EquipSlot.Hands => design.Design.Data.Equipment.Hands.Value,
// EquipSlot.Legs => design.Design.Data.Equipment.Legs.Value,
// EquipSlot.Feet => design.Design.Data.Equipment.Feet.Value,
// EquipSlot.Ears => design.Design.Data.Equipment.Ears.Value,
// EquipSlot.Neck => design.Design.Data.Equipment.Neck.Value,
// EquipSlot.Wrists => design.Design.Data.Equipment.Wrists.Value,
// EquipSlot.RFinger => design.Design.Data.Equipment.RFinger.Value,
// EquipSlot.LFinger => design.Design.Data.Equipment.LFinger.Value,
// _ => 0,
// };
// }
// }
// }
// *(CustomizationData*)customize = *(CustomizationData*)save.Customization.Address;
// var equip = (CharacterEquip)equipData;
// var newEquip = save.Equipment;
// for (var i = 0; i < 10; ++i)
// equip[i] = newEquip[i];
//}
}
//
// private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, uint* data)
// {
// ulong ret;
// var slot = slotIdx.ToEquipSlot();
// try
// {
// if (slot != EquipSlot.Unknown)
// {
// var gameObject =
// (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
// if (gameObject != null)
// {
// var name = new Utf8String(gameObject->GameObject.Name).ToString();
// if (FixedDesigns.EnabledDesigns.TryGetValue(name, out var designs))
// {
// var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(gameObject->ClassJob));
// if (design != null && design.Design.Data.WriteEquipment.Fits(slot))
// *data = slot switch
// {
// EquipSlot.Head => design.Design.Data.Equipment.Head.Value,
// EquipSlot.Body => design.Design.Data.Equipment.Body.Value,
// EquipSlot.Hands => design.Design.Data.Equipment.Hands.Value,
// EquipSlot.Legs => design.Design.Data.Equipment.Legs.Value,
// EquipSlot.Feet => design.Design.Data.Equipment.Feet.Value,
// EquipSlot.Ears => design.Design.Data.Equipment.Ears.Value,
// EquipSlot.Neck => design.Design.Data.Equipment.Neck.Value,
// EquipSlot.Wrists => design.Design.Data.Equipment.Wrists.Value,
// EquipSlot.RFinger => design.Design.Data.Equipment.RFinger.Value,
// EquipSlot.LFinger => design.Design.Data.Equipment.LFinger.Value,
// _ => 0,
// };
// }
// }
// }
// }
// finally
// {
// ret = FlagSlotForUpdateHook!.Original(drawObject, slotIdx, data);
// }
//
// return ret;
// }
//
// public void UpdateSlot(Human* drawObject, EquipSlot slot, CharacterArmor data)
// {
// var idx = slot.ToIndex();
// if (idx >= 10)
// return;
//
// FlagSlotForUpdateDetour(drawObject, idx, (uint*)&data);
// }
}