This commit is contained in:
Ottermandias 2022-08-23 18:06:28 +02:00
parent cb2e2f0128
commit 941bba1518
39 changed files with 2569 additions and 1579 deletions

View file

@ -6,59 +6,59 @@ namespace Glamourer.Customization;
public unsafe struct Customize public unsafe struct Customize
{ {
private readonly CustomizeData* _data; public readonly CustomizeData* Data;
public Customize(CustomizeData* data) public Customize(CustomizeData* data)
=> _data = data; => Data = data;
public Race Race public Race Race
{ {
get => (Race)_data->Data[0]; get => (Race)Data->Data[0];
set => _data->Data[0] = (byte)value; set => Data->Data[0] = (byte)value;
} }
// Skip Unknown Gender // Skip Unknown Gender
public Gender Gender public Gender Gender
{ {
get => (Gender)(_data->Data[1] + 1); get => (Gender)(Data->Data[1] + 1);
set => _data->Data[1] = (byte)(value - 1); set => Data->Data[1] = (byte)(value - 1);
} }
public ref byte BodyType public ref byte BodyType
=> ref _data->Data[2]; => ref Data->Data[2];
public ref byte Height public ref byte Height
=> ref _data->Data[3]; => ref Data->Data[3];
public SubRace Clan public SubRace Clan
{ {
get => (SubRace)_data->Data[4]; get => (SubRace)Data->Data[4];
set => _data->Data[4] = (byte)value; set => Data->Data[4] = (byte)value;
} }
public ref byte Face public ref byte Face
=> ref _data->Data[5]; => ref Data->Data[5];
public ref byte Hairstyle public ref byte Hairstyle
=> ref _data->Data[6]; => ref Data->Data[6];
public bool HighlightsOn public bool HighlightsOn
{ {
get => _data->Data[7] >> 7 == 1; get => Data->Data[7] >> 7 == 1;
set => _data->Data[7] = (byte)(value ? _data->Data[7] | 0x80 : _data->Data[7] & 0x7F); set => Data->Data[7] = (byte)(value ? Data->Data[7] | 0x80 : Data->Data[7] & 0x7F);
} }
public ref byte SkinColor public ref byte SkinColor
=> ref _data->Data[8]; => ref Data->Data[8];
public ref byte EyeColorRight public ref byte EyeColorRight
=> ref _data->Data[9]; => ref Data->Data[9];
public ref byte HairColor public ref byte HairColor
=> ref _data->Data[10]; => ref Data->Data[10];
public ref byte HighlightsColor public ref byte HighlightsColor
=> ref _data->Data[11]; => ref Data->Data[11];
public readonly ref struct FacialFeatureStruct public readonly ref struct FacialFeatureStruct
{ {
@ -84,73 +84,73 @@ public unsafe struct Customize
} }
public FacialFeatureStruct FacialFeatures public FacialFeatureStruct FacialFeatures
=> new(_data->Data + 12); => new(Data->Data + 12);
public ref byte TattooColor public ref byte TattooColor
=> ref _data->Data[13]; => ref Data->Data[13];
public ref byte Eyebrows public ref byte Eyebrows
=> ref _data->Data[14]; => ref Data->Data[14];
public ref byte EyeColorLeft public ref byte EyeColorLeft
=> ref _data->Data[15]; => ref Data->Data[15];
public byte EyeShape public byte EyeShape
{ {
get => (byte)(_data->Data[16] & 0x7F); get => (byte)(Data->Data[16] & 0x7F);
set => _data->Data[16] = (byte)((value & 0x7F) | (_data->Data[16] & 0x80)); set => Data->Data[16] = (byte)((value & 0x7F) | (Data->Data[16] & 0x80));
} }
public bool SmallIris public bool SmallIris
{ {
get => _data->Data[16] >> 7 == 1; get => Data->Data[16] >> 7 == 1;
set => _data->Data[16] = (byte)(value ? _data->Data[16] | 0x80 : _data->Data[16] & 0x7F); set => Data->Data[16] = (byte)(value ? Data->Data[16] | 0x80 : Data->Data[16] & 0x7F);
} }
public ref byte Nose public ref byte Nose
=> ref _data->Data[17]; => ref Data->Data[17];
public ref byte Jaw public ref byte Jaw
=> ref _data->Data[18]; => ref Data->Data[18];
public byte Mouth public byte Mouth
{ {
get => (byte)(_data->Data[19] & 0x7F); get => (byte)(Data->Data[19] & 0x7F);
set => _data->Data[19] = (byte)((value & 0x7F) | (_data->Data[19] & 0x80)); set => Data->Data[19] = (byte)((value & 0x7F) | (Data->Data[19] & 0x80));
} }
public bool Lipstick public bool Lipstick
{ {
get => _data->Data[19] >> 7 == 1; get => Data->Data[19] >> 7 == 1;
set => _data->Data[19] = (byte)(value ? _data->Data[19] | 0x80 : _data->Data[19] & 0x7F); set => Data->Data[19] = (byte)(value ? Data->Data[19] | 0x80 : Data->Data[19] & 0x7F);
} }
public ref byte LipColor public ref byte LipColor
=> ref _data->Data[20]; => ref Data->Data[20];
public ref byte MuscleMass public ref byte MuscleMass
=> ref _data->Data[21]; => ref Data->Data[21];
public ref byte TailShape public ref byte TailShape
=> ref _data->Data[22]; => ref Data->Data[22];
public ref byte BustSize public ref byte BustSize
=> ref _data->Data[23]; => ref Data->Data[23];
public byte FacePaint public byte FacePaint
{ {
get => (byte)(_data->Data[24] & 0x7F); get => (byte)(Data->Data[24] & 0x7F);
set => _data->Data[24] = (byte)((value & 0x7F) | (_data->Data[24] & 0x80)); set => Data->Data[24] = (byte)((value & 0x7F) | (Data->Data[24] & 0x80));
} }
public bool FacePaintReversed public bool FacePaintReversed
{ {
get => _data->Data[24] >> 7 == 1; get => Data->Data[24] >> 7 == 1;
set => _data->Data[24] = (byte)(value ? _data->Data[24] | 0x80 : _data->Data[24] & 0x7F); set => Data->Data[24] = (byte)(value ? Data->Data[24] | 0x80 : Data->Data[24] & 0x7F);
} }
public ref byte FacePaintColor public ref byte FacePaintColor
=> ref _data->Data[25]; => ref Data->Data[25];
public static readonly CustomizeData Default = GenerateDefault(); public static readonly CustomizeData Default = GenerateDefault();
public static readonly CustomizeData Empty = new(); public static readonly CustomizeData Empty = new();
@ -165,12 +165,12 @@ public unsafe struct Customize
CustomizationId.Clan => (byte)Clan, CustomizationId.Clan => (byte)Clan,
CustomizationId.Face => Face, CustomizationId.Face => Face,
CustomizationId.Hairstyle => Hairstyle, CustomizationId.Hairstyle => Hairstyle,
CustomizationId.HighlightsOnFlag => _data->Data[7], CustomizationId.HighlightsOnFlag => Data->Data[7],
CustomizationId.SkinColor => SkinColor, CustomizationId.SkinColor => SkinColor,
CustomizationId.EyeColorR => EyeColorRight, CustomizationId.EyeColorR => EyeColorRight,
CustomizationId.HairColor => HairColor, CustomizationId.HairColor => HairColor,
CustomizationId.HighlightColor => HighlightsColor, CustomizationId.HighlightColor => HighlightsColor,
CustomizationId.FacialFeaturesTattoos => _data->Data[12], CustomizationId.FacialFeaturesTattoos => Data->Data[12],
CustomizationId.TattooColor => TattooColor, CustomizationId.TattooColor => TattooColor,
CustomizationId.Eyebrows => Eyebrows, CustomizationId.Eyebrows => Eyebrows,
CustomizationId.EyeColorL => EyeColorLeft, CustomizationId.EyeColorL => EyeColorLeft,
@ -204,7 +204,7 @@ public unsafe struct Customize
case CustomizationId.EyeColorR: EyeColorRight = value; break; case CustomizationId.EyeColorR: EyeColorRight = value; break;
case CustomizationId.HairColor: HairColor = value; break; case CustomizationId.HairColor: HairColor = value; break;
case CustomizationId.HighlightColor: HighlightsColor = value; break; case CustomizationId.HighlightColor: HighlightsColor = value; break;
case CustomizationId.FacialFeaturesTattoos: _data->Data[12] = value; break; case CustomizationId.FacialFeaturesTattoos: Data->Data[12] = value; break;
case CustomizationId.TattooColor: TattooColor = value; break; case CustomizationId.TattooColor: TattooColor = value; break;
case CustomizationId.Eyebrows: Eyebrows = value; break; case CustomizationId.Eyebrows: Eyebrows = value; break;
case CustomizationId.EyeColorL: EyeColorLeft = value; break; case CustomizationId.EyeColorL: EyeColorLeft = value; break;
@ -224,7 +224,7 @@ public unsafe struct Customize
} }
public bool Equals(Customize other) public bool Equals(Customize other)
=> throw new NotImplementedException(); => CustomizeData.Equals(Data, other.Data);
public byte this[CustomizationId id] public byte this[CustomizationId id]
{ {
@ -268,5 +268,8 @@ public unsafe struct Customize
} }
public void Load(Customize other) public void Load(Customize other)
=> _data->Read(other._data); => Data->Read(other.Data);
public void Write(IntPtr target)
=> Data->Write((void*)target);
} }

View file

@ -138,7 +138,7 @@ public partial class CustomizationOptions
{ {
var (skin, hair) = GetColors(race, gender); var (skin, hair) = GetColors(race, gender);
var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!; var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var hrothgar = race.ToRace() == Race.Hrothgar;
// Create the initial set with all the easily accessible parameters available for anyone. // Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizationSet(race, gender) var set = new CustomizationSet(race, gender)
{ {
@ -148,8 +148,8 @@ public partial class CustomizationOptions
EyeColors = _eyeColorPicker, EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker, HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker, TattooColors = _tattooColorPicker,
LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark, LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight, LipColorsLight = hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark, FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight, FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row), Faces = GetFaces(row),
@ -164,10 +164,10 @@ public partial class CustomizationOptions
SetAvailability(set, row); SetAvailability(set, row);
SetFacialFeatures(set, row); SetFacialFeatures(set, row);
SetHairByFace(set);
SetMenuTypes(set, row); SetMenuTypes(set, row);
SetNames(set, row); SetNames(set, row);
return set; return set;
} }
@ -218,6 +218,32 @@ public partial class CustomizationOptions
.Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i))) .Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
.ToArray(); .ToArray();
private void SetHairByFace(CustomizationSet set)
{
if (set.Race != Race.Hrothgar)
{
set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray();
return;
}
var tmp = new IReadOnlyList<Customization>[set.Faces.Count + 1];
tmp[0] = set.HairStyles;
for (var i = 1; i <= set.Faces.Count; ++i)
{
bool Valid(Customization c)
{
var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0;
return data == 0 || data == i + set.Faces.Count;
}
tmp[i] = set.HairStyles.Where(Valid).ToArray();
}
set.HairByFace = tmp;
}
private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row) private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row)
{ {
// Set up the menu types for all customizations. // Set up the menu types for all customizations.
@ -270,6 +296,7 @@ public partial class CustomizationOptions
{ {
var count = set.Faces.Count; var count = set.Faces.Count;
var featureDict = new List<IReadOnlyList<Customization>>(count); var featureDict = new List<IReadOnlyList<Customization>>(count);
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8)); var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8));

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.VisualBasic;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
namespace Glamourer.Customization; namespace Glamourer.Customization;
@ -52,6 +53,7 @@ public class CustomizationSet
public IReadOnlyList<string> OptionName { get; internal set; } = null!; public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public IReadOnlyList<Customization> Faces { get; internal init; } = null!; public IReadOnlyList<Customization> Faces { get; internal init; } = null!;
public IReadOnlyList<Customization> HairStyles { get; internal init; } = null!; public IReadOnlyList<Customization> HairStyles { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> HairByFace { get; internal set; } = null!;
public IReadOnlyList<Customization> TailEarShapes { get; internal init; } = null!; public IReadOnlyList<Customization> TailEarShapes { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!; public IReadOnlyList<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaints { get; internal init; } = null!; public IReadOnlyList<Customization> FacePaints { get; internal init; } = null!;
@ -74,13 +76,22 @@ public class CustomizationSet
=> OptionName[(int)id]; => OptionName[(int)id];
public Customization FacialFeature(int faceIdx, int idx) public Customization FacialFeature(int faceIdx, int idx)
=> FeaturesTattoos[faceIdx - 1][idx]; {
faceIdx = HrothgarFaceHack((byte) faceIdx) - 1;
if (faceIdx < FeaturesTattoos.Count)
return FeaturesTattoos[HrothgarFaceHack((byte)faceIdx)][idx];
return FeaturesTattoos[0][idx];
}
private byte HrothgarFaceHack(byte value)
=> value is > 4 and < 9 && Clan.ToRace() == Race.Hrothgar ? (byte)(value - 4) : value;
public int DataByValue(CustomizationId id, byte value, out Customization? custom) public int DataByValue(CustomizationId id, byte value, out Customization? custom)
{ {
var type = id.ToType(); var type = id.ToType();
custom = null; custom = null;
if (type == CharaMakeParams.MenuType.Percentage || type == CharaMakeParams.MenuType.ListSelector) if (type is CharaMakeParams.MenuType.Percentage or CharaMakeParams.MenuType.ListSelector)
{ {
if (value < Count(id)) if (value < Count(id))
{ {
@ -91,9 +102,9 @@ public class CustomizationSet
return -1; return -1;
} }
int Get(IEnumerable<Customization> list, ref Customization? output) int Get(IEnumerable<Customization> list, byte v, ref Customization? output)
{ {
var (val, idx) = list.Cast<Customization?>().Select((c, i) => (c, i)).FirstOrDefault(c => c.c!.Value.Value == value); var (val, idx) = list.Cast<Customization?>().Select((c, i) => (c, i)).FirstOrDefault(c => c.c!.Value.Value == v);
if (val == null) if (val == null)
return -1; return -1;
@ -103,27 +114,27 @@ public class CustomizationSet
return id switch return id switch
{ {
CustomizationId.SkinColor => Get(SkinColors, ref custom), CustomizationId.SkinColor => Get(SkinColors, value, ref custom),
CustomizationId.EyeColorL => Get(EyeColors, ref custom), CustomizationId.EyeColorL => Get(EyeColors, value, ref custom),
CustomizationId.EyeColorR => Get(EyeColors, ref custom), CustomizationId.EyeColorR => Get(EyeColors, value, ref custom),
CustomizationId.HairColor => Get(HairColors, ref custom), CustomizationId.HairColor => Get(HairColors, value, ref custom),
CustomizationId.HighlightColor => Get(HighlightColors, ref custom), CustomizationId.HighlightColor => Get(HighlightColors, value, ref custom),
CustomizationId.TattooColor => Get(TattooColors, ref custom), CustomizationId.TattooColor => Get(TattooColors, value, ref custom),
CustomizationId.LipColor => Get(LipColorsDark.Concat(LipColorsLight), ref custom), CustomizationId.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, ref custom),
CustomizationId.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), ref custom), CustomizationId.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, ref custom),
CustomizationId.Face => Get(Faces, ref custom), CustomizationId.Face => Get(Faces, HrothgarFaceHack(value), ref custom),
CustomizationId.Hairstyle => Get(HairStyles, ref custom), CustomizationId.Hairstyle => Get(HairStyles, value, ref custom),
CustomizationId.TailEarShape => Get(TailEarShapes, ref custom), CustomizationId.TailEarShape => Get(TailEarShapes, value, ref custom),
CustomizationId.FacePaint => Get(FacePaints, ref custom), CustomizationId.FacePaint => Get(FacePaints, value, ref custom),
CustomizationId.FacialFeaturesTattoos => Get(FeaturesTattoos[0], ref custom), CustomizationId.FacialFeaturesTattoos => Get(FeaturesTattoos[0], value, ref custom),
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null), _ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
}; };
} }
public Customization Data(CustomizationId id, int idx) public Customization Data(CustomizationId id, int idx, byte face = 0)
{ {
if (idx > Count(id)) if (idx > Count(id, face = HrothgarFaceHack(face)))
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
switch (id.ToType()) switch (id.ToType())
@ -135,7 +146,7 @@ public class CustomizationSet
return id switch return id switch
{ {
CustomizationId.Face => Faces[idx], CustomizationId.Face => Faces[idx],
CustomizationId.Hairstyle => HairStyles[idx], CustomizationId.Hairstyle => face < HairByFace.Count ? HairByFace[face][idx] : HairStyles[idx],
CustomizationId.TailEarShape => TailEarShapes[idx], CustomizationId.TailEarShape => TailEarShapes[idx],
CustomizationId.FacePaint => FacePaints[idx], CustomizationId.FacePaint => FacePaints[idx],
CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx], CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx],
@ -162,10 +173,13 @@ public class CustomizationSet
ret[(int)CustomizationId.EyeColorL] = CustomizationId.EyeColorR; ret[(int)CustomizationId.EyeColorL] = CustomizationId.EyeColorR;
ret[(int)CustomizationId.EyeColorR] = CustomizationId.TattooColor; ret[(int)CustomizationId.EyeColorR] = CustomizationId.TattooColor;
return ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray()); var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
foreach (var type in Enum.GetValues<CharaMakeParams.MenuType>())
dict.TryAdd(type, Array.Empty<CustomizationId>());
return dict;
} }
public int Count(CustomizationId id) public int Count(CustomizationId id, byte face = 0)
{ {
if (!IsAvailable(id)) if (!IsAvailable(id))
return 0; return 0;
@ -176,7 +190,7 @@ public class CustomizationSet
return id switch return id switch
{ {
CustomizationId.Face => Faces.Count, CustomizationId.Face => Faces.Count,
CustomizationId.Hairstyle => HairStyles.Count, CustomizationId.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face].Count : 0,
CustomizationId.HighlightsOnFlag => 2, CustomizationId.HighlightsOnFlag => 2,
CustomizationId.SkinColor => SkinColors.Count, CustomizationId.SkinColor => SkinColors.Count,
CustomizationId.EyeColorR => EyeColors.Count, CustomizationId.EyeColorR => EyeColors.Count,

View file

@ -1,12 +1,12 @@
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using OtterGui.Raii;
using Companion = Lumina.Excel.GeneratedSheets.Companion; using Companion = Lumina.Excel.GeneratedSheets.Companion;
namespace Glamourer; namespace Glamourer;
@ -26,16 +26,37 @@ public class ModelData
FirstName = $"{name} #{model.RowId:D4}"; FirstName = $"{name} #{model.RowId:D4}";
AllNames = $"#{model.RowId:D4}\n{name}"; AllNames = $"#{model.RowId:D4}\n{name}";
} }
public uint Id
=> Model.RowId;
} }
private readonly SortedList<uint, Data> _models; private readonly SortedList<uint, Data> _models;
private readonly Dictionary<ulong, Data> _modelByData;
public IReadOnlyDictionary<uint, Data> Models public IReadOnlyDictionary<uint, Data> Models
=> _models; => _models;
public unsafe ulong KeyFromCharacterBase(CharacterBase* drawObject)
{
var type = (*(delegate* unmanaged<CharacterBase*, uint>**)drawObject)[50](drawObject);
var unk = (ulong)*((byte*)drawObject + 0x8E8) << 8;
return type switch
{
1 => type | unk,
2 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + 0x908) << 16),
3 => type | unk | ((ulong)*(ushort*)((byte*)drawObject + 0x8F0) << 16) | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 32) | ((ulong)**(ushort**)((byte*)drawObject + 0x910) << 40),
_ => 0u,
};
}
public unsafe bool FromCharacterBase(CharacterBase* drawObject, out Data data)
=> _modelByData.TryGetValue(KeyFromCharacterBase(drawObject), out data);
public ModelData(DataManager dataManager) public ModelData(DataManager dataManager)
{ {
var modelSheet = dataManager.GetExcelSheet<ModelChara>(); var modelSheet = dataManager.GetExcelSheet<ModelChara>()!;
_models = new SortedList<uint, Data>(NpcNames.ModelCharas.Count); _models = new SortedList<uint, Data>(NpcNames.ModelCharas.Count);
@ -71,5 +92,20 @@ public class ModelData
UpdateData(model, name); UpdateData(model, name);
} }
} }
_modelByData = new Dictionary<ulong, Data>((int)modelSheet.RowCount);
foreach (var mdl in modelSheet)
{
var unk5 = (ulong)mdl.Unknown5 << 8;
var key = mdl.Type switch
{
1 => mdl.Type | unk5,
2 => mdl.Type | unk5 | ((ulong)mdl.Model << 16),
3 => mdl.Type | unk5 | ((ulong)mdl.Model << 16) | ((ulong)mdl.Base << 32) | ((ulong)mdl.Base << 40),
_ => 0u,
};
if (key != 0)
_modelByData.TryAdd(key, _models.TryGetValue(mdl.RowId, out var d) ? d : new Data(mdl, string.Empty));
}
} }
} }

View file

@ -5,7 +5,6 @@ using Dalamud.Logging;
using Dalamud.Utility; using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race; using Race = Penumbra.GameData.Enums.Race;
@ -356,16 +355,6 @@ public class RestrictedGear
AddItem(37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves AddItem(37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves
AddItem(37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights AddItem(37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights
AddItem(37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots AddItem(37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots
AddItem(23003, 23008); // Mun'gaek Hat <-> Eastern Socialite's Hat
AddItem(23004, 23009); // Mun'gaek Uibok <-> Eastern Socialite's Cheongsam
AddItem(23005, 23010); // Mun'gaek Cuffs <-> Eastern Socialite's Gloves
AddItem(23006, 23011); // Mun'gaek Trousers <-> Eastern Socialite's Skirt
AddItem(23007, 23012); // Mun'gaek Boots <-> Eastern Socialite's Boots
AddItem(24148, 24153); // Far Eastern Officer's Hat <-> Far Eastern Maiden's Hat
AddItem(24149, 24154); // Far Eastern Officer's Robe <-> Far Eastern Maiden's Tunic
AddItem(24150, 24155); // Far Eastern Officer's Armband <-> Far Eastern Maiden's Armband
AddItem(24151, 24156); // Far Eastern Officer's Bottoms <-> Far Eastern Maiden's Bottoms
AddItem(24152, 24157); // Far Eastern Officer's Boots <-> Far Eastern Maiden's Boots
AddItem(13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica AddItem(13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica
AddItem(13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves AddItem(13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves
AddItem(13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses AddItem(13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses

View file

@ -1,45 +0,0 @@
using System;
using Penumbra.GameData.Enums;
namespace Glamourer.Structs;
// Turn EquipSlot into a bitfield flag enum.
[Flags]
public enum CharacterEquipMask : ushort
{
None = 0,
MainHand = 0b000000000001,
OffHand = 0b000000000010,
Head = 0b000000000100,
Body = 0b000000001000,
Hands = 0b000000010000,
Legs = 0b000000100000,
Feet = 0b000001000000,
Ears = 0b000010000000,
Neck = 0b000100000000,
Wrists = 0b001000000000,
RFinger = 0b010000000000,
LFinger = 0b100000000000,
All = 0b111111111111,
}
public static class CharacterEquipMaskExtensions
{
public static bool Fits(this CharacterEquipMask mask, EquipSlot slot)
=> slot switch
{
EquipSlot.Unknown => false,
EquipSlot.Head => mask.HasFlag(CharacterEquipMask.Head),
EquipSlot.Body => mask.HasFlag(CharacterEquipMask.Body),
EquipSlot.Hands => mask.HasFlag(CharacterEquipMask.Hands),
EquipSlot.Legs => mask.HasFlag(CharacterEquipMask.Legs),
EquipSlot.Feet => mask.HasFlag(CharacterEquipMask.Feet),
EquipSlot.Ears => mask.HasFlag(CharacterEquipMask.Ears),
EquipSlot.Neck => mask.HasFlag(CharacterEquipMask.Neck),
EquipSlot.Wrists => mask.HasFlag(CharacterEquipMask.Wrists),
EquipSlot.RFinger => mask.HasFlag(CharacterEquipMask.RFinger),
EquipSlot.LFinger => mask.HasFlag(CharacterEquipMask.LFinger),
_ => false,
};
}

View file

@ -21,6 +21,9 @@ public readonly struct Item
public bool HasSubModel public bool HasSubModel
=> Base.ModelSub != 0; => Base.ModelSub != 0;
public bool IsBothHand
=> (EquipSlot)Base.EquipSlotCategory.Row == EquipSlot.BothHand;
// Create a new item from its sheet list with the given name and either the inferred equip slot or the given one. // Create a new item from its sheet list with the given name and either the inferred equip slot or the given one.
public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown) public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown)
{ {

View file

@ -1,241 +0,0 @@
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 interface IIdentifier : IEquatable<IIdentifier>
{
Utf8String Name { get; }
public IIdentifier CreatePermanent();
}
public class InvalidIdentifier : IIdentifier
{
public Utf8String Name
=> Utf8String.Empty;
public bool Equals(IIdentifier? other)
=> false;
public override int GetHashCode()
=> 0;
public override string ToString()
=> "Invalid";
public IIdentifier CreatePermanent()
=> this;
}
public class PlayerIdentifier : IIdentifier
{
public Utf8String Name { get; }
public readonly ushort HomeWorld;
public PlayerIdentifier(Utf8String name, ushort homeWorld)
{
Name = name;
HomeWorld = homeWorld;
}
public bool Equals(IIdentifier? other)
=> other is PlayerIdentifier p && p.HomeWorld == HomeWorld && p.Name.Equals(Name);
public override int GetHashCode()
=> HashCode.Combine(Name.Crc32, HomeWorld);
public override string ToString()
=> $"{Name} ({HomeWorld})";
public IIdentifier CreatePermanent()
=> new PlayerIdentifier(Name.Clone(), HomeWorld);
}
public class OwnedIdentifier : IIdentifier
{
public Utf8String Name { get; }
public readonly Utf8String OwnerName;
public readonly uint DataId;
public readonly ushort OwnerHomeWorld;
public readonly ObjectKind Kind;
public OwnedIdentifier(Utf8String name, Utf8String ownerName, ushort ownerHomeWorld, uint dataId, ObjectKind kind)
{
Name = name;
OwnerName = ownerName;
OwnerHomeWorld = ownerHomeWorld;
DataId = dataId;
Kind = kind;
}
public bool Equals(IIdentifier? other)
=> other is OwnedIdentifier p
&& p.DataId == DataId
&& p.OwnerHomeWorld == OwnerHomeWorld
&& p.Kind == Kind
&& p.OwnerName.Equals(OwnerName);
public override int GetHashCode()
=> HashCode.Combine(OwnerName.Crc32, OwnerHomeWorld, DataId, Kind);
public override string ToString()
=> $"{OwnerName}s {Name}";
public IIdentifier CreatePermanent()
=> new OwnedIdentifier(Name.Clone(), OwnerName.Clone(), OwnerHomeWorld, DataId, Kind);
}
public class NpcIdentifier : IIdentifier
{
public Utf8String Name { get; }
public readonly uint DataId;
public readonly ushort ObjectIndex;
public NpcIdentifier(Utf8String actorName, ushort objectIndex = ushort.MaxValue, uint dataId = uint.MaxValue)
{
Name = actorName;
ObjectIndex = objectIndex;
DataId = dataId;
}
public bool Equals(IIdentifier? other)
=> other is NpcIdentifier p
&& p.Name.Equals(Name)
&& (p.DataId == uint.MaxValue || DataId == uint.MaxValue || p.DataId == DataId)
&& (p.ObjectIndex == ushort.MaxValue || ObjectIndex == ushort.MaxValue || p.ObjectIndex == ObjectIndex);
public override int GetHashCode()
=> Name.Crc32;
public override string ToString()
=> DataId == uint.MaxValue ? ObjectIndex == ushort.MaxValue ? Name.ToString() : $"{Name} at {ObjectIndex}" :
ObjectIndex == ushort.MaxValue ? $"{Name} ({DataId})" : $"{Name} ({DataId}) at {ObjectIndex}";
public IIdentifier CreatePermanent()
=> new NpcIdentifier(Name.Clone(), ObjectIndex, DataId);
}
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 IIdentifier GetIdentifier()
=> CreateIdentifier(this);
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 ref int ModelId
=> ref Pointer->ModelCharaId;
public ObjectKind ObjectKind
{
get => (ObjectKind) Pointer->GameObject.ObjectKind;
set => Pointer->GameObject.ObjectKind = (byte)value;
}
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;
private static IIdentifier CreateIdentifier(Actor actor)
{
switch (actor.ObjectKind)
{
case ObjectKind.Player: return new PlayerIdentifier(actor.Utf8Name, actor.Pointer->HomeWorld);
case ObjectKind.BattleNpc:
{
var ownerId = actor.Pointer->GameObject.OwnerID;
if (ownerId != 0xE0000000)
{
var owner = (Actor)Dalamud.Objects.SearchById(ownerId)?.Address;
if (!owner)
return new InvalidIdentifier();
return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld,
actor.Pointer->GameObject.DataID, ObjectKind.BattleNpc);
}
return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex,
actor.Pointer->GameObject.DataID);
}
case ObjectKind.Retainer:
case ObjectKind.EventNpc:
return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex,
actor.Pointer->GameObject.DataID);
case ObjectKind.MountType:
case ObjectKind.Companion:
{
var idx = actor.Pointer->GameObject.ObjectIndex;
if (idx % 2 == 0)
return new InvalidIdentifier();
var owner = (Actor)Dalamud.Objects[idx - 1]?.Address;
if (!owner)
return new InvalidIdentifier();
return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld,
actor.Pointer->GameObject.DataID, actor.ObjectKind);
}
default: return new InvalidIdentifier();
}
}
}

View file

@ -27,17 +27,17 @@ public class GlamourerIpc : IDisposable
private readonly ObjectTable _objectTable; private readonly ObjectTable _objectTable;
private readonly DalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
internal ICallGateProvider<string, string?>? ProviderGetAllCustomization; //internal ICallGateProvider<string, string?>? ProviderGetAllCustomization;
internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter; //internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter;
internal ICallGateProvider<string, string, object>? ProviderApplyAll; //internal ICallGateProvider<string, string, object>? ProviderApplyAll;
internal ICallGateProvider<string, Character?, object>? ProviderApplyAllToCharacter; //internal ICallGateProvider<string, Character?, object>? ProviderApplyAllToCharacter;
internal ICallGateProvider<string, string, object>? ProviderApplyOnlyCustomization; //internal ICallGateProvider<string, string, object>? ProviderApplyOnlyCustomization;
internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyCustomizationToCharacter; //internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyCustomizationToCharacter;
internal ICallGateProvider<string, string, object>? ProviderApplyOnlyEquipment; //internal ICallGateProvider<string, string, object>? ProviderApplyOnlyEquipment;
internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyEquipmentToCharacter; //internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyEquipmentToCharacter;
internal ICallGateProvider<string, object>? ProviderRevert; //internal ICallGateProvider<string, object>? ProviderRevert;
internal ICallGateProvider<Character?, object>? ProviderRevertCharacter; //internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
internal ICallGateProvider<int>? ProviderGetApiVersion; //internal ICallGateProvider<int>? ProviderGetApiVersion;
public GlamourerIpc(ClientState clientState, ObjectTable objectTable, DalamudPluginInterface pluginInterface) public GlamourerIpc(ClientState clientState, ObjectTable objectTable, DalamudPluginInterface pluginInterface)
{ {

View file

@ -2,15 +2,18 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using Glamourer.Interop;
using Glamourer.Structs;
using ImGuiNET; using ImGuiNET;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Api; namespace Glamourer.Api;
public class PenumbraAttach : IDisposable public class PenumbraAttach : IDisposable
{ {
public const int RequiredPenumbraBreakingVersion = 4; public const int RequiredPenumbraBreakingVersion = 4;
public const int RequiredPenumbraFeatureVersion = 0; public const int RequiredPenumbraFeatureVersion = 12;
private ICallGateSubscriber<ChangedItemType, uint, object>? _tooltipSubscriber; private ICallGateSubscriber<ChangedItemType, uint, object>? _tooltipSubscriber;
private ICallGateSubscriber<MouseButton, ChangedItemType, uint, object>? _clickSubscriber; private ICallGateSubscriber<MouseButton, ChangedItemType, uint, object>? _clickSubscriber;
@ -18,11 +21,14 @@ public class PenumbraAttach : IDisposable
private ICallGateSubscriber<GameObject, int, object>? _redrawSubscriberObject; private ICallGateSubscriber<GameObject, int, object>? _redrawSubscriberObject;
private ICallGateSubscriber<IntPtr, (IntPtr, string)>? _drawObjectInfo; private ICallGateSubscriber<IntPtr, (IntPtr, string)>? _drawObjectInfo;
private ICallGateSubscriber<IntPtr, string, IntPtr, IntPtr, IntPtr, object?>? _creatingCharacterBase; private ICallGateSubscriber<IntPtr, string, IntPtr, IntPtr, IntPtr, object?>? _creatingCharacterBase;
private ICallGateSubscriber<IntPtr, string, IntPtr, object?>? _createdCharacterBase;
private ICallGateSubscriber<int, int>? _cutsceneParent;
private readonly ICallGateSubscriber<object?> _initializedEvent; private readonly ICallGateSubscriber<object?> _initializedEvent;
private readonly ICallGateSubscriber<object?> _disposedEvent; private readonly ICallGateSubscriber<object?> _disposedEvent;
public event Action<IntPtr, IntPtr, IntPtr, IntPtr>? CreatingCharacterBase; public event Action<IntPtr, IntPtr, IntPtr, IntPtr>? CreatingCharacterBase;
public event Action<IntPtr, IntPtr>? CreatedCharacterBase;
public PenumbraAttach(bool attach) public PenumbraAttach(bool attach)
{ {
@ -51,6 +57,7 @@ public class PenumbraAttach : IDisposable
_redrawSubscriberName = Dalamud.PluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName"); _redrawSubscriberName = Dalamud.PluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
_redrawSubscriberObject = Dalamud.PluginInterface.GetIpcSubscriber<GameObject, int, object>("Penumbra.RedrawObject"); _redrawSubscriberObject = Dalamud.PluginInterface.GetIpcSubscriber<GameObject, int, object>("Penumbra.RedrawObject");
_drawObjectInfo = Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, (IntPtr, string)>("Penumbra.GetDrawObjectInfo"); _drawObjectInfo = Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, (IntPtr, string)>("Penumbra.GetDrawObjectInfo");
_cutsceneParent = Dalamud.PluginInterface.GetIpcSubscriber<int, int>("Penumbra.GetCutsceneParentIndex");
if (!attach) if (!attach)
return; return;
@ -60,9 +67,12 @@ public class PenumbraAttach : IDisposable
Dalamud.PluginInterface.GetIpcSubscriber<MouseButton, ChangedItemType, uint, object>("Penumbra.ChangedItemClick"); Dalamud.PluginInterface.GetIpcSubscriber<MouseButton, ChangedItemType, uint, object>("Penumbra.ChangedItemClick");
_creatingCharacterBase = _creatingCharacterBase =
Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, string, IntPtr, IntPtr, IntPtr, object?>("Penumbra.CreatingCharacterBase"); Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, string, IntPtr, IntPtr, IntPtr, object?>("Penumbra.CreatingCharacterBase");
_createdCharacterBase =
Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, string, IntPtr, object?>("Penumbra.CreatedCharacterBase");
_tooltipSubscriber.Subscribe(PenumbraTooltip); _tooltipSubscriber.Subscribe(PenumbraTooltip);
_clickSubscriber.Subscribe(PenumbraRightClick); _clickSubscriber.Subscribe(PenumbraRightClick);
_creatingCharacterBase.Subscribe(SubscribeCharacterBase); _creatingCharacterBase.Subscribe(SubscribeCreatingCharacterBase);
_createdCharacterBase.Subscribe(SubscribeCreatedCharacterBase);
PluginLog.Debug("Glamourer attached to Penumbra."); PluginLog.Debug("Glamourer attached to Penumbra.");
} }
catch (Exception e) catch (Exception e)
@ -71,14 +81,18 @@ public class PenumbraAttach : IDisposable
} }
} }
private void SubscribeCharacterBase(IntPtr gameObject, string _, IntPtr modelId, IntPtr customize, IntPtr equipment) private void SubscribeCreatingCharacterBase(IntPtr gameObject, string _, IntPtr modelId, IntPtr customize, IntPtr equipment)
=> CreatingCharacterBase?.Invoke(gameObject, modelId, customize, equipment); => CreatingCharacterBase?.Invoke(gameObject, modelId, customize, equipment);
private void SubscribeCreatedCharacterBase(IntPtr gameObject, string _, IntPtr drawObject)
=> CreatedCharacterBase?.Invoke(gameObject, drawObject);
public void Unattach() public void Unattach()
{ {
_tooltipSubscriber?.Unsubscribe(PenumbraTooltip); _tooltipSubscriber?.Unsubscribe(PenumbraTooltip);
_clickSubscriber?.Unsubscribe(PenumbraRightClick); _clickSubscriber?.Unsubscribe(PenumbraRightClick);
_creatingCharacterBase?.Unsubscribe(SubscribeCharacterBase); _creatingCharacterBase?.Unsubscribe(SubscribeCreatingCharacterBase);
_createdCharacterBase?.Unsubscribe(SubscribeCreatedCharacterBase);
_tooltipSubscriber = null; _tooltipSubscriber = null;
_clickSubscriber = null; _clickSubscriber = null;
_creatingCharacterBase = null; _creatingCharacterBase = null;
@ -109,25 +123,54 @@ public class PenumbraAttach : IDisposable
if (button != MouseButton.Right || type != ChangedItemType.Item) if (button != MouseButton.Right || type != ChangedItemType.Item)
return; return;
//var gPose = ObjectManager.GPosePlayer; var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(id)!;
//var player = ObjectManager.Player; var writeItem = new Item(item, string.Empty);
//var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(id)!;
//var writeItem = new Item(item, string.Empty); UpdateItem(ObjectManager.GPosePlayer, writeItem);
//if (gPose != null) UpdateItem(ObjectManager.Player, writeItem);
//{ }
// writeItem.Write(gPose.Address);
// UpdateCharacters(gPose, player); private static void UpdateItem(Actor actor, Item item)
//} {
//else if (player != null) if (!actor || !actor.DrawObject)
//{ return;
// writeItem.Write(player.Address);
// UpdateCharacters(player); switch (item.EquippableTo)
//} {
case EquipSlot.MainHand:
{
var off = item.HasSubModel
? new CharacterWeapon(item.SubModel.id, item.SubModel.type, item.SubModel.variant, actor.DrawObject.OffHand.Stain)
: item.IsBothHand
? CharacterWeapon.Empty
: actor.OffHand;
var main = new CharacterWeapon(item.MainModel.id, item.MainModel.type, item.MainModel.variant, actor.DrawObject.MainHand.Stain);
Glamourer.RedrawManager.LoadWeapon(actor, main, off);
return;
}
case EquipSlot.OffHand:
{
var off = new CharacterWeapon(item.MainModel.id, item.MainModel.type, item.MainModel.variant, actor.DrawObject.OffHand.Stain);
var main = actor.MainHand;
Glamourer.RedrawManager.LoadWeapon(actor, main, off);
return;
}
default:
{
var current = actor.DrawObject.Equip[item.EquippableTo];
var armor = new CharacterArmor(item.MainModel.id, (byte)item.MainModel.variant, current.Stain);
Glamourer.RedrawManager.ChangeEquip(actor.DrawObject, item.EquippableTo, armor);
return;
}
}
} }
public Actor GameObjectFromDrawObject(IntPtr drawObject) public Actor GameObjectFromDrawObject(IntPtr drawObject)
=> _drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero; => _drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero;
public int CutsceneParent(int idx)
=> _cutsceneParent?.InvokeFunc(idx) ?? -1;
public void RedrawObject(GameObject? actor, RedrawType settings, bool repeat) public void RedrawObject(GameObject? actor, RedrawType settings, bool repeat)
{ {
if (actor == null) if (actor == null)

View file

@ -1,221 +0,0 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
using Functions = Penumbra.GameData.Util.Functions;
namespace Glamourer;
public class CharacterSaveConverter : JsonConverter<CharacterSave>
{
public override void WriteJson(JsonWriter writer, CharacterSave? value, JsonSerializer serializer)
{
var s = value?.ToBase64() ?? string.Empty;
serializer.Serialize(writer, s);
}
public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave? existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var token = JToken.Load(reader);
var s = token.ToObject<string>();
return CharacterSave.FromString(s!);
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct CharacterData
{
[Flags]
public enum SaveFlags : byte
{
WriteCustomizations = 0x01,
IsWet = 0x02,
SetHatState = 0x04,
SetWeaponState = 0x08,
SetVisorState = 0x10,
HatState = 0x20,
WeaponState = 0x40,
VisorState = 0x80,
}
public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + CustomizeData.Size;
public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CustomizeData.Size + 4 + 1;
public const byte TotalSizeVersion3 = 1 + 1 + 2 + 7 + 7 + 2 + 40 + CustomizeData.Size + 4;
public const byte CurrentVersion = 3;
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 CustomizeData _customizeData;
public float Alpha;
public Customize Customize
{
get
{
fixed (CustomizeData* ptr = &_customizeData)
{
return new Customize(ptr);
}
}
}
public CharacterEquip Equipment
{
get
{
fixed (CharacterArmor* ptr = &Head)
{
return new CharacterEquip(ptr);
}
}
}
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,
_customizeData = Customize.Default,
Alpha = 1f,
};
public void Load(Actor actor)
{
if (!actor.IsHuman || actor.Pointer->GameObject.DrawObject == null)
return;
var human = (Human*)actor.Pointer->GameObject.DrawObject;
_customizeData.Read(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(CharacterData)));
}
}
private static void CheckSize(int length, int requiredLength)
{
if (length != requiredLength)
throw new Exception(
$"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}.");
}
private static void CheckRange(int idx, byte value, byte min, byte max)
{
if (value < min || value > max)
throw new Exception(
$"Can not parse Base64 string into CharacterSave:\n\tInvalid value {value} in byte {idx}, should be in [{min},{max}].");
}
public static CharacterData FromString(string data)
{
var bytes = Convert.FromBase64String(data);
var ret = new CharacterData();
fixed (byte* ptr = bytes)
{
switch (bytes[0])
{
case 1:
CheckSize(bytes.Length, TotalSizeVersion1);
CheckRange(2, bytes[1], 0, 1);
Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion1);
ret.Version = CurrentVersion;
ret.Alpha = 1f;
break;
case 2:
CheckSize(bytes.Length, TotalSizeVersion2);
CheckRange(2, bytes[1], 0, 0x3F);
Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion2 - 1);
ret.Flags &= ~SaveFlags.HatState;
if ((bytes.Last() & 0x01) != 0)
ret.Flags |= SaveFlags.HatState;
if ((bytes.Last() & 0x02) != 0)
ret.Flags |= SaveFlags.WeaponState;
if ((bytes.Last() & 0x04) != 0)
ret.Flags |= SaveFlags.VisorState;
break;
case 3:
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]}.");
}
}
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 Customize Customize
=> _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

@ -1,149 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer;
public class CurrentManipulations
{
private readonly RestrictedGear _restrictedGear = GameData.RestrictedGear(Dalamud.GameData);
private readonly Dictionary<Actor.IIdentifier, CharacterSave> _characterSaves = new();
public CharacterSave CreateSave(Actor actor)
{
var id = actor.GetIdentifier();
if (_characterSaves.TryGetValue(id, out var save))
return save;
save = new CharacterSave(actor);
_characterSaves.Add(id.CreatePermanent(), save);
return save;
}
public bool TryGetDesign(Actor.IIdentifier 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.Customize.Race, save.Customize.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, Customize customize)
{
var save = CreateSave(actor);
FixRestrictedGear(save, customize.Gender, customize.Race);
save.Customize.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.Customize;
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.Customize.Gender == gender)
return false;
var customize = save.Customize;
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.Customize.Clan == clan)
return false;
var customize = save.Customize;
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(Customize 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.Customize.Race && gender == save.Customize.Gender)
return;
var equip = save.Equipment;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
(_, equip[slot]) = _restrictedGear.ResolveRestricted(equip[slot], slot, race, gender);
}
}

View file

@ -1,4 +1,8 @@
using System; using System;
using System.Runtime.InteropServices;
using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -14,3 +18,9 @@ public class Design
public override string ToString() public override string ToString()
=> Name; => Name;
} }
public struct ArmorData
{
public CharacterArmor Model;
public bool Ignore;
}

View file

@ -1,7 +1,172 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection.Metadata.Ecma335;
using Dalamud.Logging;
using System.Runtime;
using System.Text;
using Dalamud.Utility;
using Glamourer.Interop;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public struct FixedCondition
{
private const ulong _territoryFlag = 1ul << 32;
private const ulong _jobFlag = 1ul << 33;
private ulong _data;
public static FixedCondition TerritoryCondition(ushort territoryType)
=> new() { _data = territoryType | _territoryFlag };
public static FixedCondition JobCondition(JobGroup group)
=> new() { _data = group.Id | _jobFlag };
public bool Check(Actor actor)
{
if ((_data & (_territoryFlag | _jobFlag)) == 0)
return true;
if ((_data & _territoryFlag) != 0)
return Dalamud.ClientState.TerritoryType == (ushort)_data;
if (actor && GameData.JobGroups(Dalamud.GameData).TryGetValue((ushort)_data, out var group) && group.Fits(actor.Job))
return true;
return true;
}
public override string ToString()
=> _data.ToString();
}
public class FixedDesign
{
public const int CurrentVersion = 0;
public string Name { get; private set; }
public bool Enabled;
public List<Actor.IIdentifier> Actors;
public List<(FixedCondition, Design)> Customization;
public List<(FixedCondition, Design)> Equipment;
public List<(FixedCondition, Design)> Weapons;
public FixedDesign(string name)
{
Name = name;
Actors = new List<Actor.IIdentifier>();
Customization = new List<(FixedCondition, Design)>();
Equipment = new List<(FixedCondition, Design)>();
Weapons = new List<(FixedCondition, Design)>();
}
public static FixedDesign? Load(JObject j)
{
try
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var version = j["Version"]?.Value<int>();
if (version == null)
return null;
return version switch
{
CurrentVersion => LoadCurrentVersion(j, name),
_ => null,
};
}
catch (Exception e)
{
PluginLog.Error($"Error loading fixed design:\n{e}");
return null;
}
}
private static FixedDesign? LoadCurrentVersion(JObject j, string name)
{
var enabled = j[nameof(Enabled)]?.Value<bool>() ?? false;
var ret = new FixedDesign(name)
{
Enabled = enabled,
};
var actors = j[nameof(Actors)];
//foreach(var pair in actors?.Children().)
return null;
}
public void Save(FileInfo file)
{
try
{
using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew);
using var w = new StreamWriter(s, Encoding.UTF8);
using var j = new JsonTextWriter(w)
{
Formatting = Formatting.Indented,
};
j.WriteStartObject();
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName("Version");
j.WriteValue(CurrentVersion);
j.WritePropertyName(nameof(Enabled));
j.WriteValue(Enabled);
j.WritePropertyName(nameof(Actors));
j.WriteStartArray();
foreach (var actor in Actors)
actor.ToJson(j);
j.WriteEndArray();
j.WritePropertyName(nameof(Customization));
j.WriteStartArray();
foreach (var (condition, design) in Customization)
{
j.WritePropertyName(condition.ToString());
j.WriteValue(design.Name);
}
j.WriteEndArray();
j.WritePropertyName(nameof(Equipment));
j.WriteStartArray();
foreach (var (condition, design) in Equipment)
{
j.WritePropertyName(condition.ToString());
j.WriteValue(design.Name);
}
j.WriteEndArray();
j.WritePropertyName(nameof(Weapons));
j.WriteStartArray();
foreach (var (condition, design) in Weapons)
{
j.WritePropertyName(condition.ToString());
j.WriteValue(design.Name);
}
j.WriteEndArray();
}
catch (Exception e)
{
PluginLog.Error($"Could not save collection {Name}:\n{e}");
}
}
public static bool Load(FileInfo path, [NotNullWhen(true)] out FixedDesign? result)
{
result = null;
return true;
}
}
public class FixedDesigns : IDisposable public class FixedDesigns : IDisposable
{ {
//public class FixedDesign //public class FixedDesign

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.State;
namespace Glamourer.Designs; namespace Glamourer.Designs;

View file

@ -1,10 +1,18 @@
using System.Reflection; using System;
using System.Reflection;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Hooking;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Api; using Glamourer.Api;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.State;
using Penumbra.GameData;
namespace Glamourer; namespace Glamourer;
@ -25,14 +33,18 @@ public class Glamourer : IDalamudPlugin
public static GlamourerConfig Config = null!; public static GlamourerConfig Config = null!;
public static IObjectIdentifier Identifier = null!;
public static PenumbraAttach Penumbra = null!; public static PenumbraAttach Penumbra = null!;
public static ICustomizationManager Customization = null!; public static ICustomizationManager Customization = null!;
public static RestrictedGear RestrictedGear = null!;
public static ModelData Models = null!;
public static RedrawManager RedrawManager = null!; public static RedrawManager RedrawManager = null!;
private readonly WindowSystem _windowSystem = new("Glamourer"); public readonly FixedDesigns FixedDesigns;
private readonly FixedDesigns _fixedDesigns; public readonly CurrentManipulations CurrentManipulations;
private readonly CurrentManipulations _currentManipulations;
private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly Interface _interface; private readonly Interface _interface;
//public readonly DesignManager Designs; //public readonly DesignManager Designs;
//public static RevertableDesigns RevertableDesigns = new(); //public static RevertableDesigns RevertableDesigns = new();
@ -42,14 +54,17 @@ public class Glamourer : IDalamudPlugin
{ {
Dalamud.Initialize(pluginInterface); Dalamud.Initialize(pluginInterface);
Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage); Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
RestrictedGear = GameData.RestrictedGear(Dalamud.GameData);
Models = GameData.Models(Dalamud.GameData);
Identifier = global::Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData);
Config = GlamourerConfig.Load(); Config = GlamourerConfig.Load();
Penumbra = new PenumbraAttach(Config.AttachToPenumbra); Penumbra = new PenumbraAttach(Config.AttachToPenumbra);
_fixedDesigns = new FixedDesigns(); FixedDesigns = new FixedDesigns();
CurrentManipulations = new CurrentManipulations();
//Designs = new DesignManager(); //Designs = new DesignManager();
//GlamourerIpc = new GlamourerIpc(Dalamud.ClientState, Dalamud.Objects, Dalamud.PluginInterface); //GlamourerIpc = new GlamourerIpc(Dalamud.ClientState, Dalamud.Objects, Dalamud.PluginInterface);
RedrawManager = new RedrawManager(_fixedDesigns, _currentManipulations); RedrawManager = new RedrawManager(FixedDesigns, CurrentManipulations);
Dalamud.Commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) Dalamud.Commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer)
{ {
@ -149,11 +164,11 @@ public class Glamourer : IDalamudPlugin
// //
public void OnGlamour(string command, string arguments) public void OnGlamour(string command, string arguments)
{ {
static void PrintHelp() //static void PrintHelp()
{ //{
Dalamud.Chat.Print("Usage:"); // Dalamud.Chat.Print("Usage:");
Dalamud.Chat.Print($" {HelpString}"); // Dalamud.Chat.Print($" {HelpString}");
} //}
//arguments = arguments.Trim(); //arguments = arguments.Trim();
//if (!arguments.Any()) //if (!arguments.Any())

View file

@ -118,9 +118,7 @@
</None> </None>
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec <Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll $(SolutionDir)$(SolutionName).zip" />
Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll $(SolutionDir)$(SolutionName).zip" /> <Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
<Exec
Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
</Target> </Target>
</Project> </Project>

View file

@ -1,12 +1,14 @@
using System; using System;
using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui; namespace Glamourer.Gui;
@ -14,8 +16,15 @@ internal partial class Interface
{ {
private class ActorTab private class ActorTab
{ {
private ObjectManager.ActorData _data = new(string.Empty, new Actor.InvalidIdentifier(), Actor.Null, false, Actor.Null); private readonly CurrentManipulations _manipulations;
private Actor _nextSelect = Actor.Null;
public ActorTab(CurrentManipulations manipulations)
=> _manipulations = manipulations;
private Actor.IIdentifier _identifier = Actor.IIdentifier.Invalid;
private ObjectManager.ActorData _currentData = ObjectManager.ActorData.Invalid;
private string _currentLabel = string.Empty;
private CurrentDesign? _currentSave;
public void Draw() public void Draw()
{ {
@ -24,62 +33,107 @@ internal partial class Interface
return; return;
DrawActorSelector(); DrawActorSelector();
if (_data.Label.Length == 0) if (!ObjectManager.Actors.TryGetValue(_identifier, out _currentData))
return; _currentData = ObjectManager.ActorData.Invalid;
else
_currentLabel = _currentData.Label;
ImGui.SameLine(); ImGui.SameLine();
if (_data.Actor.IsHuman) DrawPanel();
DrawActorPanel();
else
DrawMonsterPanel();
} }
private void DrawActorPanel() private unsafe void DrawPanel()
{ {
using var group = ImRaii.Group(); if (_identifier == Actor.IIdentifier.Invalid)
if (!Glamourer.RedrawManager.CurrentManipulations.GetSave(_data.Actor, out var save))
return; return;
if (DrawCustomization(save.Customize, save.Equipment, !_data.Modifiable))
{
//Glamourer.RedrawManager.Set(_data.Actor.Address, _character);
Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
}
if (ImGui.Button("Set Machinist Goggles"))
{
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()
{
using var group = ImRaii.Group(); using var group = ImRaii.Group();
var currentModel = (uint)_data.Actor.ModelId; DrawPanelHeader();
var models = GameData.Models(Dalamud.GameData); using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true);
var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}"; if (!child || _currentSave == null)
using var combo = ImRaii.Combo("Model Id", currentData);
if (!combo)
return; return;
foreach (var (id, data) in models.Models) if (_currentData.Valid)
_currentSave.Update(_currentData.Objects[0]);
var d = _currentData.Objects[0].DrawObject.Pointer;
var x = (*(delegate* unmanaged<Human*, byte>**)d)[50](d);
ImGui.Text($"{x} {_currentData.Objects[0].ModelId}");
if (x == 1)
CustomizationDrawer.Draw(_currentSave.Data.Customize, _currentSave.Data.Equipment, _currentData.Objects,
_identifier is Actor.SpecialIdentifier);
}
private const uint RedHeaderColor = 0xFF1818C0;
private const uint GreenHeaderColor = 0xFF18C018;
private void DrawPanelHeader()
{ {
if (ImGui.Selectable(data.FirstName, id == currentModel) && id != currentModel) var color = _currentData.Valid ? GreenHeaderColor : RedHeaderColor;
{ var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
_data.Actor.SetModelId((int)id); using var c = ImRaii.PushColor(ImGuiCol.Text, color)
_data.Actor.ObjectKind = .Push(ImGuiCol.Button, buttonColor)
Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true); .Push(ImGuiCol.ButtonHovered, buttonColor)
} .Push(ImGuiCol.ButtonActive, buttonColor);
ImGuiUtil.HoverTooltip(data.AllNames); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
} .Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX);
} }
//private void DrawActorPanel()
//{
// using var group = ImRaii.Group();
// if (!_data.Identifier.IsValid)
// return;
//
// if (DrawCustomization(_currentSave.Customize, _currentSave.Equipment, !_data.Modifiable))
// //Glamourer.RedrawManager.Set(_data.Actor.Address, _character);
// Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
//
// if (ImGui.Button("Set Machinist Goggles"))
// Glamourer.RedrawManager.ChangeEquip(_data.Actor, 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));
//
// if (ImGui.Button("Set Customize"))
// {
// unsafe
// {
// var data = _data.Actor.Customize.Data->Clone();
// Glamourer.RedrawManager.UpdateCustomize(_data.Actor.DrawObject, new Customize(&data)
// {
// SkinColor = 154,
// });
// }
// }
//}
//
//private void DrawMonsterPanel()
//{
// using var group = ImRaii.Group();
// var currentModel = (uint)_data.Actor.ModelId;
// var models = GameData.Models(Dalamud.GameData);
// var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}";
// using var combo = ImRaii.Combo("Model Id", currentData);
// if (!combo)
// return;
//
// foreach (var (id, data) in models.Models)
// {
// if (ImGui.Selectable(data.FirstName, id == currentModel) && id != currentModel)
// {
// _data.Actor.SetModelId((int)id);
// Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
// }
//
// ImGuiUtil.HoverTooltip(data.AllNames);
// }
//}
private LowerString _actorFilter = LowerString.Empty; private LowerString _actorFilter = LowerString.Empty;
@ -91,63 +145,55 @@ internal partial class Interface
.Push(ImGuiStyleVar.FrameRounding, 0); .Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(_actorSelectorWidth); ImGui.SetNextItemWidth(_actorSelectorWidth);
LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64); LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64);
using (var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true))
{
if (!child)
return;
_data.Actor = Actor.Null;
_data.GPose = Actor.Null;
_data.Modifiable = false;
style.Push(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(ObjectManager.GetEnumerator(), skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
style.Pop();
}
DrawSelector(oldSpacing);
DrawSelectionButtons(); DrawSelectionButtons();
} }
private void UpdateSelection(ObjectManager.ActorData data) private void DrawSelector(Vector2 oldSpacing)
{ {
_data = data; using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true);
//_character.Load(_data.Actor); if (!child)
return;
ObjectManager.Update();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(ObjectManager.List, skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
} }
private bool CheckFilter(ObjectManager.ActorData data) private bool CheckFilter((Actor.IIdentifier, ObjectManager.ActorData) pair)
{ => _actorFilter.IsEmpty || pair.Item2.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase);
if (_nextSelect && _nextSelect == data.Actor || data.Label == _data.Label)
UpdateSelection(data);
return data.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase);
}
private void DrawSelectable(ObjectManager.ActorData data) private void DrawSelectable((Actor.IIdentifier, ObjectManager.ActorData) pair)
{ {
var equal = data.Label == _data.Label; var equal = pair.Item1.Equals(_identifier);
if (ImGui.Selectable(data.Label, equal) && !equal) if (ImGui.Selectable(pair.Item2.Label, equal) && !equal)
UpdateSelection(data); {
_identifier = pair.Item1.CreatePermanent();
_currentData = pair.Item2;
_currentSave = _currentData.Valid ? _manipulations.GetOrCreateSave(_currentData.Objects[0]) : null;
}
} }
private void DrawSelectionButtons() private void DrawSelectionButtons()
{ {
_nextSelect = Actor.Null;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0); .Push(ImGuiStyleVar.FrameRounding, 0);
var buttonWidth = new Vector2(_actorSelectorWidth / 2, 0); var buttonWidth = new Vector2(_actorSelectorWidth / 2, 0);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth
, "Select the local player character.", !ObjectManager.Player, true)) , "Select the local player character.", !ObjectManager.Player, true))
_nextSelect = _inGPose ? ObjectManager.GPosePlayer : ObjectManager.Player; _identifier = ObjectManager.Player.GetIdentifier();
ImGui.SameLine(); ImGui.SameLine();
Actor targetActor = Dalamud.Targets.Target?.Address; Actor targetActor = Dalamud.Targets.Target?.Address;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth,
"Select the current target, if it is in the list.", _inGPose || !targetActor, true)) "Select the current target, if it is in the list.", ObjectManager.IsInGPose || !targetActor, true))
_nextSelect = targetActor; _identifier = targetActor.GetIdentifier();
} }
} }
private readonly ActorTab _actorTab = new();
} }
//internal partial class Interface //internal partial class Interface

View file

@ -1,9 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Logging; using Dalamud.Logging;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Util;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
@ -15,224 +18,221 @@ namespace Glamourer.Gui;
internal partial class Interface internal partial class Interface
{ {
private static byte _tempStorage; private class CustomizationDrawer
private static CustomizationId _tempType;
private static bool DrawCustomization(Customize customize, CharacterEquip equip, bool locked)
{ {
private Customize _customize;
private CharacterEquip _equip;
private IReadOnlyCollection<Actor> _actors = Array.Empty<Actor>();
private CustomizationSet _set = null!;
public static void Draw(Customize customize, CharacterEquip equip, IReadOnlyCollection<Actor> actors, bool locked)
{
var d = new CustomizationDrawer()
{
_customize = customize,
_equip = equip,
_actors = actors,
};
if (!ImGui.CollapsingHeader("Character Customization")) if (!ImGui.CollapsingHeader("Character Customization"))
return false; return;
var ret = DrawRaceGenderSelector(customize, equip, locked); using var disabled = ImRaii.Disabled(locked);
var set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
foreach (var id in set.Order[CharaMakeParams.MenuType.Percentage]) d.DrawRaceGenderSelector();
ret |= PercentageSelector(set, id, customize, locked);
Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.IconSelector], c => DrawIconSelector(set, c, customize, locked), d._set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
ImGui.SameLine);
ret |= DrawMultiIconSelector(set, customize, locked); foreach (var id in d._set.Order[CharaMakeParams.MenuType.Percentage])
d.PercentageSelector(id);
foreach (var id in set.Order[CharaMakeParams.MenuType.ListSelector]) Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.IconSelector], d.DrawIconSelector, ImGui.SameLine);
ret |= DrawListSelector(set, id, customize, locked);
Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.ColorPicker], c => DrawColorPicker(set, c, customize, locked), d.DrawMultiIconSelector();
ImGui.SameLine);
ret |= Checkbox(set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b, locked); foreach (var id in d._set.Order[CharaMakeParams.MenuType.ListSelector])
d.DrawListSelector(id);
Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.ColorPicker], d.DrawColorPicker, ImGui.SameLine);
d.Checkbox(d._set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b);
var xPos = _inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X; var xPos = _inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
ImGui.SameLine(xPos); ImGui.SameLine(xPos);
ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}", d.Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {d._set.Option(CustomizationId.FacePaint)}",
customize.FacePaintReversed, b => customize.FacePaintReversed = b, locked); customize.FacePaintReversed, b => customize.FacePaintReversed = b);
ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}", d.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}",
customize.SmallIris, b => customize.SmallIris = b, locked); customize.SmallIris, b => customize.SmallIris = b);
if (customize.Race != Race.Hrothgar) if (customize.Race != Race.Hrothgar)
{ {
ImGui.SameLine(xPos); ImGui.SameLine(xPos);
ret |= Checkbox(set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b, locked); d.Checkbox(d._set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b);
}
} }
return ret; private void DrawRaceGenderSelector()
}
private static bool DrawRaceGenderSelector(Customize customize, CharacterEquip equip, bool locked)
{ {
var ret = DrawGenderSelector(customize, equip, locked); DrawGenderSelector();
ImGui.SameLine(); ImGui.SameLine();
using var group = ImRaii.Group(); using var group = ImRaii.Group();
ret |= DrawRaceCombo(customize, equip, locked); DrawRaceCombo();
var gender = Glamourer.Customization.GetName(CustomName.Gender); var gender = Glamourer.Customization.GetName(CustomName.Gender);
var clan = Glamourer.Customization.GetName(CustomName.Clan); var clan = Glamourer.Customization.GetName(CustomName.Clan);
ImGui.TextUnformatted($"{gender} & {clan}"); ImGui.TextUnformatted($"{gender} & {clan}");
return ret;
} }
private static bool DrawGenderSelector(Customize customize, CharacterEquip equip, bool locked) private void DrawGenderSelector()
{ {
using var font = ImRaii.PushFont(UiBuilder.IconFont); using var font = ImRaii.PushFont(UiBuilder.IconFont);
var icon = customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus; var icon = _customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
var restricted = customize.Race == Race.Hrothgar; var restricted = _customize.Race == Race.Hrothgar;
if (restricted) if (restricted)
icon = FontAwesomeIcon.MarsDouble; icon = FontAwesomeIcon.MarsDouble;
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted || locked, true)) if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted, true))
return false; return;
var gender = customize.Gender == Gender.Male ? Gender.Female : Gender.Male; var gender = _customize.Gender == Gender.Male ? Gender.Female : Gender.Male;
return false; //customize.ChangeGender(gender, locked ? CharacterEquip.Null : equip); if (!_customize.ChangeGender(_equip, gender))
return;
foreach (var actor in _actors.Where(a => a))
Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false);
} }
private static bool DrawRaceCombo(Customize customize, CharacterEquip equip, bool locked) private void DrawRaceCombo()
{ {
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
ImGui.SetNextItemWidth(_raceSelectorWidth); ImGui.SetNextItemWidth(_raceSelectorWidth);
using var combo = ImRaii.Combo("##subRaceCombo", customize.ClanName()); using var combo = ImRaii.Combo("##subRaceCombo", _customize.ClanName());
if (!combo) if (!combo)
return false; return;
if (locked)
ImGui.CloseCurrentPopup();
var ret = false;
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
{ {
if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, customize.Gender), subRace == customize.Clan)) if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, _customize.Gender), subRace == _customize.Clan)
ret |= false; //customize.ChangeRace(subRace, equip); && _customize.ChangeRace(_equip, subRace))
foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw, false);
}
} }
return ret; private void Checkbox(string label, bool current, Action<bool> setter)
}
private static bool Checkbox(string label, bool current, Action<bool> setter, bool locked)
{ {
var tmp = current; var tmp = current;
var ret = false; if (ImGui.Checkbox($"##{label}", ref tmp) && tmp != current)
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
if (ImGui.Checkbox($"##{label}", ref tmp) && tmp == current && !locked)
{ {
setter(tmp); setter(tmp);
ret = true; foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
} }
alpha.Pop();
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(label); ImGui.TextUnformatted(label);
return ret;
} }
private static bool PercentageSelector(CustomizationSet set, CustomizationId id, Customize customization, bool locked) private void PercentageSelector(CustomizationId id)
{ {
using var bigGroup = ImRaii.Group(); using var bigGroup = ImRaii.Group();
using var _ = ImRaii.PushId((int)id); using var _ = ImRaii.PushId((int)id);
int value = id == _tempType ? _tempStorage : customization[id]; int value = _customize[id];
var count = set.Count(id); var count = _set.Count(id);
ImGui.SetNextItemWidth(_comboSelectorSize); ImGui.SetNextItemWidth(_comboSelectorSize);
var (min, max) = locked ? (value, value) : (0, count - 1); void OnChange(int v)
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
if (ImGui.SliderInt("##slider", ref value, min, max, string.Empty, ImGuiSliderFlags.AlwaysClamp) && !locked)
{ {
_tempStorage = (byte)value; _customize[id] = (byte)v;
_tempType = id; foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
} }
var ret = ImGui.IsItemDeactivatedAfterEdit(); if (ImGui.SliderInt("##slider", ref value, 0, count - 1, "%i", ImGuiSliderFlags.AlwaysClamp))
OnChange(value);
ImGui.SameLine(); ImGui.SameLine();
ret |= InputInt("##input", id, --value, min, max, locked); InputInt("##input", --value, 0, count - 1, OnChange);
alpha.Pop();
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(set.OptionName[(int)id]); ImGui.TextUnformatted(_set.OptionName[(int)id]);
if (ret)
customization[id] = _tempStorage;
return ret;
} }
private static bool InputInt(string label, CustomizationId id, int startValue, int minValue, int maxValue, bool locked) private static void InputInt(string label, int startValue, int minValue, int maxValue, Action<int> setter)
{ {
var tmp = startValue + 1; var tmp = startValue + 1;
ImGui.SetNextItemWidth(_inputIntSize); ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue) if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue)
&& !locked
&& tmp != startValue + 1 && tmp != startValue + 1
&& tmp >= minValue && tmp >= minValue
&& tmp <= maxValue) && tmp <= maxValue)
{ setter(tmp);
_tempType = id;
_tempStorage = (byte)(tmp - 1);
}
var ret = ImGui.IsItemDeactivatedAfterEdit() && !locked;
if (!locked)
ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]"); ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]");
return ret;
} }
private static bool DrawIconSelector(CustomizationSet set, CustomizationId id, Customize customize, bool locked) private void DrawIconSelector(CustomizationId id)
{ {
const string popupName = "Style Picker"; const string popupName = "Style Picker";
using var bigGroup = ImRaii.Group(); using var bigGroup = ImRaii.Group();
using var _ = ImRaii.PushId((int)id); using var _ = ImRaii.PushId((int)id);
var count = set.Count(id); var count = _set.Count(id, _customize.Face);
var label = set.Option(id); var label = _set.Option(id);
var current = set.DataByValue(id, _tempType == id ? _tempStorage : customize[id], out var custom); var current = _set.DataByValue(id, _customize[id], out var custom);
if (current < 0) if (current < 0)
{ {
label = $"{label} (Custom #{customize[id]})"; label = $"{label} (Custom #{_customize[id]})";
current = 0; current = 0;
custom = set.Data(id, 0); custom = _set.Data(id, 0);
} }
var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId); var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize) && !locked)
ImGui.OpenPopup(popupName); ImGui.OpenPopup(popupName);
ImGuiUtil.HoverIconTooltip(icon, _iconSize); ImGuiUtil.HoverIconTooltip(icon, _iconSize);
void OnChange(int v)
{
var value = _set.Data(id, v - 1).Value;
// Hrothgar hack
if (_set.Race == Race.Hrothgar && id == CustomizationId.Face)
value += 4;
if (_customize[id] == value)
return;
_customize[id] = value;
foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
}
ImGui.SameLine(); ImGui.SameLine();
using var group = ImRaii.Group(); using var group = ImRaii.Group();
var (min, max) = locked ? (current, current) : (1, count); InputInt("##text", current, 1, count, OnChange);
var ret = InputInt("##text", id, current, min, max, locked);
if (ret)
customize[id] = set.Data(id, _tempStorage).Value;
ImGui.TextUnformatted($"{label} ({custom.Value.Value})"); ImGui.TextUnformatted($"{label} ({custom.Value.Value})");
ret |= DrawIconPickerPopup(popupName, set, id, customize); DrawIconPickerPopup(popupName, id, OnChange);
return ret;
} }
private static bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, Customize customize) private void DrawIconPickerPopup(string label, CustomizationId id, Action<int> setter)
{ {
using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup) if (!popup)
return false; return;
var ret = false; var count = _set.Count(id, _customize.Face);
var count = set.Count(id);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0); .Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
var custom = set.Data(id, i); var custom = _set.Data(id, i, _customize.Face);
var icon = Glamourer.Customization.GetIcon(custom.IconId); var icon = Glamourer.Customization.GetIcon(custom.IconId);
using var group = ImRaii.Group(); using var group = ImRaii.Group();
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
{ {
customize[id] = custom.Value; setter(custom.Value);
ret = true;
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
@ -247,173 +247,156 @@ internal partial class Interface
if (i % 8 != 7) if (i % 8 != 7)
ImGui.SameLine(); ImGui.SameLine();
} }
return ret;
} }
private static bool DrawColorPicker(CustomizationSet set, CustomizationId id, Customize customize, bool locked) private void DrawColorPicker(CustomizationId id)
{ {
const string popupName = "Color Picker"; const string popupName = "Color Picker";
using var _ = ImRaii.PushId((int)id); using var _ = ImRaii.PushId((int)id);
var ret = false; var count = _set.Count(id);
var count = set.Count(id); var label = _set.Option(id);
var label = set.Option(id); var (current, custom) = GetCurrentCustomization(id);
var (current, custom) = GetCurrentCustomization(set, id, customize);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None,
if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, _framedIconSize) _framedIconSize))
&& !locked)
ImGui.OpenPopup(popupName); ImGui.OpenPopup(popupName);
ImGui.SameLine(); ImGui.SameLine();
using (var group = ImRaii.Group()) void OnChange(int v)
{ {
var (min, max) = locked ? (current, current) : (1, count); _customize[id] = _set.Data(id, v).Value;
if (InputInt("##text", id, current, min, max, locked)) foreach (var actor in _actors.Where(a => a && a.DrawObject))
{ Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
customize[id] = set.Data(id, current).Value;
ret = true;
} }
using (var group = ImRaii.Group())
{
InputInt("##text", current, 1, count, OnChange);
ImGui.TextUnformatted(label); ImGui.TextUnformatted(label);
} }
return ret | DrawColorPickerPopup(popupName, set, id, customize); DrawColorPickerPopup(popupName, id, OnChange);
} }
private static (int, Customization.Customization) GetCurrentCustomization(CustomizationSet set, CustomizationId id, private (int, Customization.Customization) GetCurrentCustomization(CustomizationId id)
Customize customize)
{ {
var current = set.DataByValue(id, customize[id], out var custom); var current = _set.DataByValue(id, _customize[id], out var custom);
if (set.IsAvailable(id) && current < 0) if (_set.IsAvailable(id) && current < 0)
{ {
PluginLog.Warning($"Read invalid customization value {customize[id]} for {id}."); PluginLog.Warning($"Read invalid customization value {_customize[id]} for {id}.");
current = 0; current = 0;
custom = set.Data(id, 0); custom = _set.Data(id, 0);
} }
return (current, custom!.Value); return (current, custom!.Value);
} }
private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, Customize customize) private void DrawColorPickerPopup(string label, CustomizationId id, Action<int> setter)
{ {
using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize); using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup) if (!popup)
return false; return;
var ret = false; var count = _set.Count(id);
var count = set.Count(id);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0); .Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
var custom = set.Data(id, i); var custom = _set.Data(id, i);
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color))) if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
{ {
customize[id] = custom.Value; setter(custom.Value);
ret = true;
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
if (i % 8 != 7) if (i % 8 != 7)
ImGui.SameLine(); ImGui.SameLine();
} }
return ret;
} }
private static bool DrawMultiIconSelector(CustomizationSet set, Customize customize, bool locked) private void DrawMultiIconSelector()
{ {
using var bigGroup = ImRaii.Group(); using var bigGroup = ImRaii.Group();
using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos); using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
var ret = DrawMultiIcons(set, customize, locked); void OnChange(int v)
ImGui.SameLine();
using var group = ImRaii.Group();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2);
int value = customize[CustomizationId.FacialFeaturesTattoos];
var (min, max) = locked ? (value, value) : (1, 256);
if (InputInt(string.Empty, CustomizationId.FacialFeaturesTattoos, value, min, max, locked))
{ {
customize[CustomizationId.FacialFeaturesTattoos] = (byte)value; _customize[CustomizationId.FacialFeaturesTattoos] = (byte)v;
ret = true; foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
} }
ImGui.TextUnformatted(set.Option(CustomizationId.FacialFeaturesTattoos)); DrawMultiIcons();
ImGui.SameLine();
int value = _customize[CustomizationId.FacialFeaturesTattoos];
using var group = ImRaii.Group();
ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y / 2));
InputInt(string.Empty, --value, 0, 255, OnChange);
return ret; ImGui.TextUnformatted(_set.Option(CustomizationId.FacialFeaturesTattoos));
} }
private static bool DrawMultiIcons(CustomizationSet set, Customize customize, bool locked) private void DrawMultiIcons()
{ {
using var _ = ImRaii.Group(); using var _ = ImRaii.Group();
var face = customize.Face; var face = _customize.Face;
if (set.Faces.Count < face)
face = 1;
var ret = false; var ret = false;
var count = set.Count(CustomizationId.FacialFeaturesTattoos); var count = _set.Count(CustomizationId.FacialFeaturesTattoos);
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
var enabled = customize.FacialFeatures[i]; var enabled = _customize.FacialFeatures[i];
var feature = set.FacialFeature(face, i); var feature = _set.FacialFeature(face, i);
var icon = i == count - 1 var icon = i == count - 1
? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId) ? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId)
: Glamourer.Customization.GetIcon(feature.IconId); : Glamourer.Customization.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : RedTint) Vector4.Zero, enabled ? Vector4.One : RedTint))
&& !locked)
{ {
customize.FacialFeatures.Set(i, !enabled); _customize.FacialFeatures.Set(i, !enabled);
ret = true; foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
} }
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 1f, !locked);
ImGuiUtil.HoverIconTooltip(icon, _iconSize); ImGuiUtil.HoverIconTooltip(icon, _iconSize);
if (i % 4 != 3) if (i % 4 != 3)
ImGui.SameLine(); ImGui.SameLine();
} }
return ret;
} }
private static bool DrawListSelector(CustomizationSet set, CustomizationId id, Customize customize, bool locked) private void DrawListSelector(CustomizationId id)
{ {
using var _ = ImRaii.PushId((int)id); using var _ = ImRaii.PushId((int)id);
using var bigGroup = ImRaii.Group(); using var bigGroup = ImRaii.Group();
var ret = false; int current = _customize[id];
int current = customize[id]; var count = _set.Count(id);
var count = set.Count(id);
void OnChange(int v)
{
_customize[id] = (byte)v;
foreach (var actor in _actors.Where(a => a && a.DrawObject))
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
}
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale); ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked); using (var combo = ImRaii.Combo("##combo", $"{_set.Option(id)} #{current + 1}"))
using (var combo = ImRaii.Combo("##combo", $"{set.Option(id)} #{current + 1}"))
{ {
if (combo) if (combo)
for (var i = 0; i < count; ++i) for (var i = 0; i < count; ++i)
{ {
if (!ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) || i == current || locked) if (!ImGui.Selectable($"{_set.Option(id)} #{i + 1}##combo", i == current) || i == current)
continue; continue;
customize[id] = (byte)i; OnChange(i);
ret = true;
} }
} }
ImGui.SameLine(); ImGui.SameLine();
var (min, max) = locked ? (current, current) : (1, count); InputInt("##text", current, 1, count, OnChange);
if (InputInt("##text", id, current, min, max, locked))
{
customize[id] = (byte)current;
ret = true;
}
ImGui.SameLine(); ImGui.SameLine();
alpha.Pop(); ImGui.TextUnformatted(_set.Option(id));
ImGui.TextUnformatted(set.Option(id)); }
return ret;
} }
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Numerics; using System.Numerics;
using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;

View file

@ -3,6 +3,7 @@ using System.Numerics;
using System.Reflection; using System.Reflection;
using Dalamud.Interface; using Dalamud.Interface;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Interop;
using ImGuiNET; using ImGuiNET;
namespace Glamourer.Gui; namespace Glamourer.Gui;
@ -20,13 +21,11 @@ internal partial class Interface
private static float _inputIntSize; private static float _inputIntSize;
private static float _comboSelectorSize; private static float _comboSelectorSize;
private static float _raceSelectorWidth; private static float _raceSelectorWidth;
private static bool _inGPose;
private static void UpdateState() private static void UpdateState()
{ {
// General // General
_inGPose = ObjectManager.IsInGPose();
_spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 }; _spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 };
_actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale; _actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale;

View file

@ -1,17 +1,108 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging; using Dalamud.Logging;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets;
namespace Glamourer.Gui; namespace Glamourer.Gui;
internal partial class Interface : Window, IDisposable internal partial class Interface : Window, IDisposable
{ {
private class DebugStateTab
{
private readonly CurrentManipulations _currentManipulations;
private LowerString _manipulationFilter = LowerString.Empty;
private Actor.IIdentifier _selection = Actor.IIdentifier.Invalid;
private CurrentDesign? _save = null;
private bool _delete = false;
public DebugStateTab(CurrentManipulations currentManipulations)
=> _currentManipulations = currentManipulations;
public void Draw()
{
using var tab = ImRaii.TabItem("Current Manipulations");
if (!tab)
return;
DrawManipulationSelector();
if (_save == null)
return;
ImGui.SameLine();
DrawActorPanel();
if (_delete)
{
_delete = false;
_currentManipulations.DeleteSave(_selection);
_selection = Actor.IIdentifier.Invalid;
}
}
private void DrawSelector(Vector2 oldSpacing)
{
using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -1), true);
if (!child)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(_currentManipulations, skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
}
private void DrawManipulationSelector()
{
using var group = ImRaii.Group();
var oldSpacing = ImGui.GetStyle().ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(_actorSelectorWidth);
LowerString.InputWithHint("##actorFilter", "Filter...", ref _manipulationFilter, 64);
_save = null;
DrawSelector(oldSpacing);
}
private bool CheckFilter(KeyValuePair<Actor.IIdentifier, CurrentDesign> data)
{
if (data.Key.Equals(_selection))
_save = data.Value;
return _manipulationFilter.Length == 0 || _manipulationFilter.IsContained(data.Key.ToString()!);
}
private void DrawSelectable(KeyValuePair<Actor.IIdentifier, CurrentDesign> data)
{
var equal = data.Key.Equals(_selection);
if (ImGui.Selectable(data.Key.ToString(), equal))
{
_selection = data.Key;
_save = data.Value;
}
}
private void DrawActorPanel()
{
using var group = ImRaii.Group();
if (ImGui.Button("Delete"))
_delete = true;
CustomizationDrawer.Draw(_save!.Data.Customize, _save.Data.Equipment, Array.Empty<Actor>(), false);
}
}
private readonly Glamourer _plugin; private readonly Glamourer _plugin;
private readonly ActorTab _actorTab;
private readonly DebugStateTab _debugStateTab;
public Interface(Glamourer plugin) public Interface(Glamourer plugin)
: base(GetLabel()) : base(GetLabel())
{ {
@ -23,6 +114,8 @@ internal partial class Interface : Window, IDisposable
MinimumSize = new Vector2(675, 675), MinimumSize = new Vector2(675, 675),
MaximumSize = ImGui.GetIO().DisplaySize, MaximumSize = ImGui.GetIO().DisplaySize,
}; };
_actorTab = new ActorTab(_plugin.CurrentManipulations);
_debugStateTab = new DebugStateTab(_plugin.CurrentManipulations);
} }
public override void Draw() public override void Draw()
@ -31,14 +124,22 @@ internal partial class Interface : Window, IDisposable
if (!tabBar) if (!tabBar)
return; return;
try
{
UpdateState(); UpdateState();
_actorTab.Draw(); _actorTab.Draw();
DrawSettingsTab(); DrawSettingsTab();
_debugStateTab.Draw();
// DrawSaves(); // DrawSaves();
// DrawFixedDesignsTab(); // DrawFixedDesignsTab();
// DrawRevertablesTab(); // DrawRevertablesTab();
} }
catch (Exception e)
{
PluginLog.Error($"Unexpected Error during Draw:\n{e}");
}
}
public void Dispose() public void Dispose()
{ {

View file

@ -9,8 +9,7 @@ internal partial class Interface
//private bool _holdShift; //private bool _holdShift;
//private bool _holdCtrl; //private bool _holdCtrl;
//private const string DesignNamePopupLabel = "Save Design As..."; //private const string DesignNamePopupLabel = "Save Design As...";
//private const uint RedHeaderColor = 0xFF1818C0;
//private const uint GreenHeaderColor = 0xFF18C018;
// //
//private void DrawPlayerHeader() //private void DrawPlayerHeader()
//{ //{

View file

@ -0,0 +1,350 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;
namespace Glamourer.Interop;
public unsafe partial struct Actor
{
public interface IIdentifier : IEquatable<IIdentifier>
{
Utf8String Name { get; }
public bool IsValid { get; }
public IIdentifier CreatePermanent();
public static readonly InvalidIdentifier Invalid = new();
public void ToJson(JsonTextWriter j);
public static IIdentifier? FromJson(JObject j)
{
switch (j["Type"]?.Value<string>() ?? string.Empty)
{
case nameof(PlayerIdentifier):
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var serverId = j[nameof(PlayerIdentifier.HomeWorld)]?.Value<ushort>() ?? ushort.MaxValue;
return new PlayerIdentifier(Utf8String.FromStringUnsafe(name, false), serverId);
}
case nameof(SpecialIdentifier):
{
var index = j[nameof(SpecialIdentifier.Index)]?.Value<ushort>() ?? ushort.MaxValue;
return new SpecialIdentifier(index);
}
case nameof(OwnedIdentifier):
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var ownerName = j[nameof(OwnedIdentifier.OwnerName)]?.Value<string>();
if (ownerName.IsNullOrEmpty())
return null;
var ownerHomeWorld = j[nameof(OwnedIdentifier.OwnerHomeWorld)]?.Value<ushort>() ?? ushort.MaxValue;
var dataId = j[nameof(OwnedIdentifier.DataId)]?.Value<ushort>() ?? ushort.MaxValue;
var kind = j[nameof(OwnedIdentifier.Kind)]?.Value<ObjectKind>() ?? ObjectKind.Player;
return new OwnedIdentifier(Utf8String.FromStringUnsafe(name, false), Utf8String.FromStringUnsafe(ownerName, false),
ownerHomeWorld, dataId, kind);
}
case nameof(NpcIdentifier):
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var dataId = j[nameof(NpcIdentifier.DataId)]?.Value<uint>() ?? uint.MaxValue;
return new NpcIdentifier(Utf8String.FromStringUnsafe(name, false), ushort.MaxValue, dataId);
}
default: return null;
}
}
}
public class InvalidIdentifier : IIdentifier
{
public Utf8String Name
=> Utf8String.Empty;
public bool IsValid
=> false;
public bool Equals(IIdentifier? other)
=> false;
public override int GetHashCode()
=> 0;
public override string ToString()
=> "Invalid";
public IIdentifier CreatePermanent()
=> this;
public void ToJson(JsonTextWriter j)
{ }
}
public class PlayerIdentifier : IIdentifier, IEquatable<PlayerIdentifier>
{
public Utf8String Name { get; }
public readonly ushort HomeWorld;
public bool IsValid
=> true;
public PlayerIdentifier(Utf8String name, ushort homeWorld)
{
Name = name;
HomeWorld = homeWorld;
}
public bool Equals(IIdentifier? other)
=> Equals(other as PlayerIdentifier);
public bool Equals(PlayerIdentifier? other)
=> other?.HomeWorld == HomeWorld && other.Name.Equals(Name);
public override int GetHashCode()
=> HashCode.Combine(Name.Crc32, HomeWorld);
public override string ToString()
=> $"{Name} ({HomeWorld})";
public IIdentifier CreatePermanent()
=> new PlayerIdentifier(Name.Clone(), HomeWorld);
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName(nameof(HomeWorld));
j.WriteValue(HomeWorld);
j.WriteEndObject();
}
}
public class SpecialIdentifier : IIdentifier, IEquatable<SpecialIdentifier>
{
public Utf8String Name
=> Utf8String.Empty;
public readonly ushort Index;
public bool IsValid
=> true;
public SpecialIdentifier(ushort index)
=> Index = index;
public bool Equals(IIdentifier? other)
=> Equals(other as SpecialIdentifier);
public bool Equals(SpecialIdentifier? other)
=> other?.Index == Index;
public override int GetHashCode()
=> Index;
public override string ToString()
=> $"Special Actor {Index}";
public IIdentifier CreatePermanent()
=> this;
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Index));
j.WriteValue(Index);
j.WriteEndObject();
}
}
public class OwnedIdentifier : IIdentifier, IEquatable<OwnedIdentifier>
{
public Utf8String Name { get; }
public readonly Utf8String OwnerName;
public readonly uint DataId;
public readonly ushort OwnerHomeWorld;
public readonly ObjectKind Kind;
public bool IsValid
=> true;
public OwnedIdentifier(Utf8String name, Utf8String ownerName, ushort ownerHomeWorld, uint dataId, ObjectKind kind)
{
Name = name;
OwnerName = ownerName;
OwnerHomeWorld = ownerHomeWorld;
DataId = dataId;
Kind = kind;
}
public bool Equals(IIdentifier? other)
=> Equals(other as OwnedIdentifier);
public bool Equals(OwnedIdentifier? other)
=> other?.DataId == DataId
&& other.OwnerHomeWorld == OwnerHomeWorld
&& other.Kind == Kind
&& other.OwnerName.Equals(OwnerName);
public override int GetHashCode()
=> HashCode.Combine(OwnerName.Crc32, OwnerHomeWorld, DataId, Kind);
public override string ToString()
=> $"{OwnerName}s {Name}";
public IIdentifier CreatePermanent()
=> new OwnedIdentifier(Name.Clone(), OwnerName.Clone(), OwnerHomeWorld, DataId, Kind);
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName(nameof(OwnerName));
j.WriteValue(OwnerName);
j.WritePropertyName(nameof(OwnerHomeWorld));
j.WriteValue(OwnerHomeWorld);
j.WritePropertyName(nameof(Kind));
j.WriteValue(Kind);
j.WritePropertyName(nameof(DataId));
j.WriteValue(DataId);
j.WriteEndObject();
}
}
public class NpcIdentifier : IIdentifier, IEquatable<NpcIdentifier>
{
public Utf8String Name { get; }
public readonly uint DataId;
public readonly ushort ObjectIndex;
public bool IsValid
=> true;
public NpcIdentifier(Utf8String actorName, ushort objectIndex = ushort.MaxValue, uint dataId = uint.MaxValue)
{
Name = actorName;
ObjectIndex = objectIndex;
DataId = dataId;
}
public bool Equals(IIdentifier? other)
=> Equals(other as NpcIdentifier);
public bool Equals(NpcIdentifier? other)
=> (other?.Name.Equals(Name) ?? false)
&& (other.DataId == uint.MaxValue || DataId == uint.MaxValue || other.DataId == DataId)
&& (other.ObjectIndex == ushort.MaxValue || ObjectIndex == ushort.MaxValue || other.ObjectIndex == ObjectIndex);
public override int GetHashCode()
=> Name.Crc32;
public override string ToString()
=> DataId == uint.MaxValue ? ObjectIndex == ushort.MaxValue ? Name.ToString() : $"{Name} at {ObjectIndex}" :
ObjectIndex == ushort.MaxValue ? $"{Name} ({DataId})" : $"{Name} ({DataId}) at {ObjectIndex}";
public IIdentifier CreatePermanent()
=> new NpcIdentifier(Name.Clone(), ObjectIndex, DataId);
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName(nameof(DataId));
j.WriteValue(DataId);
j.WriteEndObject();
}
}
private static IIdentifier CreateIdentifier(Actor actor)
{
if (!actor.Valid)
return IIdentifier.Invalid;
var objectIdx = actor.Pointer->GameObject.ObjectIndex;
if (objectIdx is >= 200 and < 240)
{
var parentIdx = Glamourer.Penumbra.CutsceneParent(objectIdx);
if (parentIdx >= 0)
{
var parent = (Actor)Dalamud.Objects.GetObjectAddress(parentIdx);
if (!parent)
return IIdentifier.Invalid;
return CreateIdentifier(parent);
}
}
switch (actor.ObjectKind)
{
case ObjectKind.Player:
{
var name = actor.Utf8Name;
if (name.Length > 0 && actor.Pointer->HomeWorld is > 0 and < ushort.MaxValue)
return new PlayerIdentifier(actor.Utf8Name, actor.Pointer->HomeWorld);
return IIdentifier.Invalid;
}
case ObjectKind.BattleNpc:
{
var ownerId = actor.Pointer->GameObject.OwnerID;
if (ownerId != 0xE0000000)
{
var owner = (Actor)Dalamud.Objects.SearchById(ownerId)?.Address;
if (!owner)
return new InvalidIdentifier();
return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld,
actor.Pointer->GameObject.DataID, ObjectKind.BattleNpc);
}
return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex,
actor.Pointer->GameObject.DataID);
}
case ObjectKind.Retainer:
case ObjectKind.EventNpc:
return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex,
actor.Pointer->GameObject.DataID);
case ObjectKind.MountType:
case ObjectKind.Companion:
{
var idx = actor.Pointer->GameObject.ObjectIndex;
if (idx % 2 == 0)
return new InvalidIdentifier();
var owner = (Actor)Dalamud.Objects[idx - 1]?.Address;
if (!owner)
return new InvalidIdentifier();
return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld,
actor.Pointer->GameObject.DataID, actor.ObjectKind);
}
default: return new InvalidIdentifier();
}
}
}

232
Glamourer/Interop/Actor.cs Normal file
View file

@ -0,0 +1,232 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public interface IDesignable
{
public bool Valid { get; }
public uint ModelId { get; }
public Customize Customize { get; }
public CharacterEquip Equip { get; }
public CharacterWeapon MainHand { get; }
public CharacterWeapon OffHand { get; }
public bool VisorEnabled { get; }
public bool WeaponEnabled { get; }
}
public unsafe partial struct DrawObject : IEquatable<DrawObject>, IDesignable
{
public Human* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator DrawObject(IntPtr? pointer)
=> new() { Pointer = (Human*)(pointer ?? IntPtr.Zero) };
public static implicit operator IntPtr(DrawObject drawObject)
=> drawObject.Pointer == null ? IntPtr.Zero : (IntPtr)drawObject.Pointer;
public bool Valid
=> Pointer != null;
public uint ModelId
=> 0;
public uint Type
=> (*(delegate* unmanaged<Human*, uint>**)Pointer)[50](Pointer);
public Customize Customize
=> new((CustomizeData*)Pointer->CustomizeData);
public CharacterEquip Equip
=> new((CharacterArmor*)Pointer->EquipSlotData);
public unsafe CharacterWeapon MainHand
=> CharacterWeapon.Empty;
public unsafe CharacterWeapon OffHand
=> CharacterWeapon.Empty;
public unsafe bool VisorEnabled
=> (*(byte*)(Address + 0x90) & 0x40) != 0;
public unsafe bool WeaponEnabled
=> false;
public static implicit operator bool(DrawObject actor)
=> actor.Pointer != null;
public static bool operator true(DrawObject actor)
=> actor.Pointer != null;
public static bool operator false(DrawObject actor)
=> actor.Pointer == null;
public static bool operator !(DrawObject actor)
=> actor.Pointer == null;
public bool Equals(DrawObject other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is DrawObject other && Equals(other);
public override int GetHashCode()
=> unchecked((int)(long)Pointer);
public static bool operator ==(DrawObject lhs, DrawObject rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(DrawObject lhs, DrawObject rhs)
=> lhs.Pointer != rhs.Pointer;
}
public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
{
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 ?? IntPtr.Zero) };
public static implicit operator IntPtr(Actor actor)
=> actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer;
public IIdentifier GetIdentifier()
=> CreateIdentifier(this);
public bool Identifier(out IIdentifier ident)
{
if (Valid)
{
ident = GetIdentifier();
return true;
}
ident = IIdentifier.Invalid;
return false;
}
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 ObjectKind ObjectKind
{
get => (ObjectKind)Pointer->GameObject.ObjectKind;
set => Pointer->GameObject.ObjectKind = (byte)value;
}
public Utf8String Utf8Name
=> new(Pointer->GameObject.Name);
public byte Job
=> Pointer->ClassJob;
public DrawObject DrawObject
=> (IntPtr)Pointer->GameObject.DrawObject;
public bool Valid
=> Pointer != null;
public uint ModelId
{
get => (uint)Pointer->ModelCharaId;
set => Pointer->ModelCharaId = (int)value;
}
public Customize Customize
=> new((CustomizeData*)Pointer->CustomizeData);
public CharacterEquip Equip
=> new((CharacterArmor*)Pointer->EquipSlotData);
public unsafe CharacterWeapon MainHand
{
get => *(CharacterWeapon*)(Address + 0x06C0 + 0x10);
set => *(CharacterWeapon*)(Address + 0x06C0 + 0x10) = value;
}
public unsafe CharacterWeapon OffHand
{
get => *(CharacterWeapon*)(Address + 0x06C0 + 0x10 + 0x68);
set => *(CharacterWeapon*)(Address + 0x06C0 + 0x10 + 0x68) = value;
}
public unsafe bool VisorEnabled
{
get => (*(byte*)(Address + Offsets.Character.VisorToggled) & Offsets.Character.Flags.IsVisorToggled) != 0;
set => *(byte*)(Address + Offsets.Character.VisorToggled) = (byte)(value
? *(byte*)(Address + Offsets.Character.VisorToggled) | Offsets.Character.Flags.IsVisorToggled
: *(byte*)(Address + Offsets.Character.VisorToggled) & ~Offsets.Character.Flags.IsVisorToggled);
}
public unsafe bool WeaponEnabled
{
get => (*(byte*)(Address + Offsets.Character.WeaponHidden1) & Offsets.Character.Flags.IsWeaponHidden1) == 0;
set
{
ref var w1 = ref *(byte*)(Address + Offsets.Character.WeaponHidden1);
ref var w2 = ref *(byte*)(Address + Offsets.Character.WeaponHidden2);
if (value)
{
w1 = (byte)(w1 & ~Offsets.Character.Flags.IsWeaponHidden1);
w2 = (byte)(w2 & ~Offsets.Character.Flags.IsWeaponHidden2);
}
else
{
w1 = (byte)(w1 | Offsets.Character.Flags.IsWeaponHidden1);
w2 = (byte)(w2 | Offsets.Character.Flags.IsWeaponHidden2);
}
}
}
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

@ -0,0 +1,193 @@
using System.Collections.Generic;
using System.Text;
using Dalamud.Game.ClientState.Objects.Enums;
using Lumina.Excel.GeneratedSheets;
using static Glamourer.Interop.Actor;
namespace Glamourer.Interop;
public static class ObjectManager
{
private const int CutsceneIndex = 200;
private const int GPosePlayerIndex = 201;
private const int CharacterScreenIndex = 240;
private const int ExamineScreenIndex = 241;
private const int FittingRoomIndex = 242;
private const int DyePreviewIndex = 243;
private const int PortraitIndex = 244;
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;
}
}
public static bool IsInGPose { get; private set; }
public static ushort World { get; private set; }
public static IReadOnlyDictionary<IIdentifier, ActorData> Actors
=> Identifiers;
public static IReadOnlyList<(IIdentifier, ActorData)> List
=> ListData;
private static readonly Dictionary<IIdentifier, ActorData> Identifiers = new(200);
private static readonly List<(IIdentifier, ActorData)> ListData = new(Dalamud.Objects.Length);
private static void HandleIdentifier(IIdentifier identifier, Actor character)
{
if (!character.DrawObject)
return;
switch (identifier)
{
case PlayerIdentifier p:
if (!Identifiers.TryGetValue(p, out var data))
{
data = new ActorData(character,
World != p.HomeWorld
? $"{p.Name} ({Dalamud.GameData.GetExcelSheet<World>()!.GetRow(p.HomeWorld)!.Name})"
: p.Name.ToString());
Identifiers[p] = data;
ListData.Add((p, data));
}
else
{
data.Objects.Add(character);
}
break;
case NpcIdentifier n when !n.Name.IsEmpty:
if (!Identifiers.TryGetValue(n, out data))
{
data = new ActorData(character, $"{n.Name} (at {n.ObjectIndex})");
Identifiers[n] = data;
ListData.Add((n, data));
}
else
{
data.Objects.Add(character);
}
break;
case OwnedIdentifier o:
if (!Identifiers.TryGetValue(o, out data))
{
data = new ActorData(character,
World != o.OwnerHomeWorld
? $"{o.OwnerName}s {o.Name} ({Dalamud.GameData.GetExcelSheet<World>()!.GetRow(o.OwnerHomeWorld)!.Name})"
: $"{o.OwnerName}s {o.Name}");
Identifiers[o] = data;
ListData.Add((o, data));
}
else
{
data.Objects.Add(character);
}
break;
}
}
public static void Update()
{
World = (ushort)(Dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0u);
Identifiers.Clear();
ListData.Clear();
for (var i = 0; i < CutsceneIndex; ++i)
{
Actor character = Dalamud.Objects.GetObjectAddress(i);
if (character.Identifier(out var identifier))
HandleIdentifier(identifier, character);
}
for (var i = CutsceneIndex; i < CharacterScreenIndex; ++i)
{
Actor character = Dalamud.Objects.GetObjectAddress(i);
if (!character.Identifier(out var identifier))
break;
HandleIdentifier(identifier, character);
}
void AddSpecial(int idx, string label)
{
Actor actor = Dalamud.Objects.GetObjectAddress(idx);
if (actor.Identifier(out var ident))
{
var data = new ActorData(actor, label);
Identifiers.Add(ident, data);
ListData.Add((ident, data));
}
}
AddSpecial(CharacterScreenIndex, "Character Screen Actor");
AddSpecial(ExamineScreenIndex, "Examine Screen Actor");
AddSpecial(FittingRoomIndex, "Fitting Room Actor");
AddSpecial(DyePreviewIndex, "Dye Preview Actor");
AddSpecial(PortraitIndex, "Portrait Actor");
for (var i = PortraitIndex + 1; i < Dalamud.Objects.Length; ++i)
{
Actor character = Dalamud.Objects.GetObjectAddress(i);
if (character.Identifier(out var identifier))
HandleIdentifier(identifier, character);
}
Actor gPose = Dalamud.Objects.GetObjectAddress(GPosePlayerIndex);
IsInGPose = gPose && gPose.Utf8Name.Length > 0;
}
public static Actor GPosePlayer
=> Dalamud.Objects.GetObjectAddress(GPosePlayerIndex);
public static Actor Player
=> Dalamud.Objects.GetObjectAddress(0);
private static unsafe string GetLabel(Actor player, string playerName, int num, bool gPose)
{
var sb = new StringBuilder(64);
sb.Append(playerName);
if (gPose)
{
sb.Append(" (GPose");
if (player.ObjectKind == ObjectKind.Player)
sb.Append(')');
else
sb.Append(player.ModelId == 0 ? ", NPC)" : ", Monster)");
}
else if (player.ObjectKind != ObjectKind.Player)
{
sb.Append(player.ModelId == 0 ? " (NPC)" : " (Monster)");
}
if (num > 1)
{
sb.Append(" #");
sb.Append(num);
}
return sb.ToString();
}
}

View file

@ -0,0 +1,23 @@
namespace Glamourer.Interop;
public static class Offsets
{
public static class Character
{
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;
}
}
}

View file

@ -0,0 +1,287 @@
using System;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager
{
private delegate void ChangeJobDelegate(IntPtr data, uint job);
[Signature("88 51 ?? 44 3B CA", DetourName = nameof(ChangeJobDetour))]
private readonly Hook<ChangeJobDelegate> _changeJobHook = null!;
private void ChangeJobDetour(IntPtr data, uint job)
{
_changeJobHook.Original(data, job);
JobChanged?.Invoke(data - 0x1A8, GameData.Jobs(Dalamud.GameData)[(byte)job]);
}
public event Action<Actor, Job>? JobChanged;
}
public unsafe partial class RedrawManager
{
public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data);
// 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 = null!;
private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
try
{
var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
var identifier = actor.GetIdentifier();
if (_fixedDesigns.TryGetDesign(identifier, out var save))
{
PluginLog.Information($"Loaded {slot} from fixed design for {identifier}.");
(var replaced, *data) =
Glamourer.RestrictedGear.ResolveRestricted(save.Equipment[slot], slot, (Race)drawObject->Race, (Gender)drawObject->Sex);
}
else if (_currentManipulations.TryGetDesign(identifier, out var save2))
{
PluginLog.Information($"Updated {slot} from current designs for {identifier}.");
(var replaced, *data) =
Glamourer.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)drawObject->Sex);
save2.Data.Equipment[slot] = *data;
}
}
catch (Exception e)
{
PluginLog.Error($"Error on loading new gear:\n{e}");
}
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
}
public bool ChangeEquip(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
{
if (!drawObject)
return false;
var slotIndex = slot.ToIndex();
if (slotIndex > 9)
return false;
return FlagSlotForUpdateDetour(drawObject.Pointer, slotIndex, &data) != 0;
}
public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data)
=> actor && ChangeEquip(actor.DrawObject, slot, data);
}
public unsafe partial class RedrawManager
{
// The character weapon object manipulated is inside the actual character.
public const int CharacterWeaponOffset = 0x6C0;
public delegate void LoadWeaponDelegate(IntPtr 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("E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook = null!;
private void LoadWeaponDetour(IntPtr 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();
if (_fixedDesigns.TryGetDesign(identifier, out var save))
{
PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
weapon = slot switch
{
0 => save.MainHand.Value,
1 => save.OffHand.Value,
_ => weapon,
};
}
else if (redrawOnEquality == 1 && _currentManipulations.TryGetDesign(identifier, out var save2))
{
PluginLog.Information($"Loaded weapon from current design for {identifier}.");
switch (slot)
{
//case 0:
// save2.Data.MainHand = new CharacterWeapon(weapon);
// break;
//case 1:
// save.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, oldWeapon, 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, 0, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 0, 0, 1, 0);
}
}
public unsafe partial class RedrawManager : IDisposable
{
private readonly FixedDesigns _fixedDesigns;
private readonly CurrentManipulations _currentManipulations;
public RedrawManager(FixedDesigns fixedDesigns, CurrentManipulations currentManipulations)
{
SignatureHelper.Initialise(this);
Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw;
Glamourer.Penumbra.CreatedCharacterBase += OnCharacterRedrawFinished;
_fixedDesigns = fixedDesigns;
_currentManipulations = currentManipulations;
_flagSlotForUpdateHook.Enable();
_loadWeaponHook.Enable();
_changeJobHook.Enable();
}
public void Dispose()
{
_flagSlotForUpdateHook.Dispose();
_loadWeaponHook.Dispose();
_changeJobHook.Dispose();
Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw;
Glamourer.Penumbra.CreatedCharacterBase -= OnCharacterRedrawFinished;
}
private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip)
{
// Do not apply anything if the game object model id does not correspond to the draw object model id.
// This is the case if the actor is transformed to a different creature.
if (actor.ModelId != *modelId)
return;
// Check if we have a current design in use, or if not if the actor has a fixed design.
var identifier = actor.GetIdentifier();
if (!(_currentManipulations.TryGetDesign(identifier, out var save) || _fixedDesigns.TryGetDesign(identifier, out var save2)))
return;
// Compare game object customize data against draw object customize data for transformations.
// Apply customization if they correspond and there is customization to apply.
//var gameObjectCustomize = new Customize((CustomizeData*)actor.Pointer->CustomizeData);
//if (gameObjectCustomize.Equals(customize))
// customize.Load(save.Customize);
//
//// Compare game object equip data against draw object equip data for transformations.
//// Apply each piece of equip that should be applied if they correspond.
//var gameObjectEquip = new CharacterEquip((CharacterArmor*)actor.Pointer->EquipSlotData);
//if (gameObjectEquip.Equals(equip))
//{
// var saveEquip = save.Equipment;
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
// {
// (var _, equip[slot]) =
// Glamourer.RestrictedGear.ResolveRestricted(true ? equip[slot] : saveEquip[slot], slot, customize.Race, customize.Gender);
// }
//}
}
private void OnCharacterRedraw(IntPtr gameObject, IntPtr modelId, IntPtr customize, IntPtr equipData)
{
try
{
OnCharacterRedraw(gameObject, (uint*)modelId, new Customize((CustomizeData*)customize),
new CharacterEquip((CharacterArmor*)equipData));
}
catch (Exception e)
{
PluginLog.Error($"Error on new draw object creation:\n{e}");
}
}
private static void OnCharacterRedrawFinished(IntPtr gameObject, IntPtr drawObject)
{
//SetVisor((Human*)drawObject, true);
if (Glamourer.Models.FromCharacterBase((CharacterBase*)drawObject, out var data))
PluginLog.Information($"Name: {data.FirstName} ({data.Id})");
else
PluginLog.Information($"Key: {Glamourer.Models.KeyFromCharacterBase((CharacterBase*)drawObject):X16}");
}
// Update
public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature("E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86")]
private readonly ChangeCustomizeDelegate _changeCustomize = null!;
public bool UpdateCustomize(DrawObject drawObject, Customize customize)
{
if (!drawObject.Valid)
return false;
return _changeCustomize(drawObject.Pointer, (byte*)customize.Data, 1);
}
public static void SetVisor(Human* data, bool on)
{
if (data == null)
return;
var flags = &data->CharacterBase.UnkFlags_01;
var state = (*flags & 0x40) != 0;
if (state == on)
return;
*flags = (byte)((on ? *flags | 0x40 : *flags & 0xBF) | 0x80);
}
}

View file

@ -1,118 +0,0 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Enums;
using Penumbra.GameData.ByteString;
namespace Glamourer;
public static class ObjectManager
{
private const int GPosePlayerIndex = 201;
private const int CharacterScreenIndex = 240;
private const int ExamineScreenIndex = 241;
private const int FittingRoomIndex = 242;
private const int DyePreviewIndex = 243;
private static readonly Dictionary<Utf8String, int> NameCounters = new();
private static readonly Dictionary<Actor.IIdentifier, Actor> GPoseActors = new(CharacterScreenIndex - GPosePlayerIndex);
public static bool IsInGPose()
=> Dalamud.Objects[GPosePlayerIndex] != null;
public static Actor GPosePlayer
=> Dalamud.Objects[GPosePlayerIndex]?.Address;
public static Actor Player
=> Dalamud.ClientState.LocalPlayer?.Address;
public record struct ActorData(string Label, Actor.IIdentifier Identifier, Actor Actor, bool Modifiable, Actor GPose);
public static IEnumerable<ActorData> GetEnumerator()
{
NameCounters.Clear();
GPoseActors.Clear();
for (var i = GPosePlayerIndex; i < CharacterScreenIndex; ++i)
{
Actor character = Dalamud.Objects[i]?.Address;
if (!character)
break;
var identifier = character.GetIdentifier();
GPoseActors[identifier] = character.Address;
yield return new ActorData(GetLabel(character, character.Utf8Name.ToString(), 0, true), identifier, character.Address, true,
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]?.Address;
if (actor)
yield return new ActorData("Examine Screen Actor", actor.GetIdentifier(), 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]?.Address;
if (actor)
yield return new ActorData("Dye Preview Actor", actor.GetIdentifier(), actor.Address, false, Actor.Null);
for (var i = 0; i < GPosePlayerIndex; ++i)
{
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 identifier = actor.GetIdentifier();
if (actor.Utf8Name.Length == 0)
continue;
if (NameCounters.TryGetValue(identifier.Name, out var num))
NameCounters[identifier.Name] = ++num;
else
NameCounters[identifier.Name] = num = 1;
if (!GPoseActors.TryGetValue(identifier, out var gPose))
gPose = Actor.Null;
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)
{
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 identifier = actor.GetIdentifier();
if (identifier.Name.Length == 0)
continue;
if (NameCounters.TryGetValue(identifier.Name, out var num))
NameCounters[identifier.Name] = ++num;
else
NameCounters[identifier.Name] = num = 1;
if (!GPoseActors.TryGetValue(identifier, out var gPose))
gPose = Actor.Null;
yield return new ActorData(GetLabel(actor, identifier.Name.ToString(), num, false), identifier, actor.Address, true, 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}";
if (((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)player!.Address)->ModelCharaId == 0)
return gPose ? $"{playerName} (GPose, NPC)" : num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)";
return gPose ? $"{playerName} (GPose, Monster)" : num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)";
}
}

View file

@ -1,23 +0,0 @@
namespace Glamourer;
public static class Offsets
{
public static class Character
{
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;
}
}
}

View file

@ -1,162 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using static Glamourer.Actor;
namespace Glamourer;
public unsafe partial class RedrawManager
{
public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data);
// 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)
{
try
{
var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
var identifier = actor.GetIdentifier();
if (_fixedDesigns.TryGetDesign(identifier, out var save))
PluginLog.Information($"Loaded {slot.ToEquipSlot()} from fixed design for {identifier}.");
else if (_currentManipulations.TryGetDesign(identifier, out save))
PluginLog.Information($"Updated {slot.ToEquipSlot()} from current designs for {identifier}.");
}
catch (Exception e)
{
PluginLog.Error($"Error on loading new gear:\n{e}");
}
return _flagSlotForUpdateHook!.Original(drawObject, slot, data);
}
public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data)
{
if (actor && 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)
{
try
{
var character = (Actor)(characterOffset - CharacterWeaponOffset);
var identifier = character.GetIdentifier();
if (_fixedDesigns.TryGetDesign(identifier, out var save))
PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
else if (unk1 == 1 && _currentManipulations.TryGetDesign(identifier, out save))
PluginLog.Information($"Loaded weapon from current design for {identifier}.");
}
catch (Exception e)
{
PluginLog.Error($"Error on loading new weapon:\n{e}");
}
_loadWeaponHook!.Original(characterOffset, slot, weapon, unk1, unk2, unk3, unk4);
}
// 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, 0, 1, 0, 0);
return;
case EquipSlot.OffHand:
LoadWeaponDetour(character + CharacterWeaponOffset, 1, weapon, 0, 1, 0, 0);
return;
case EquipSlot.BothHand:
LoadWeaponDetour(character + CharacterWeaponOffset, 0, weapon, 0, 1, 0, 0);
LoadWeaponDetour(character + CharacterWeaponOffset, 1, CharacterWeapon.Empty, 0, 1, 0, 0);
return;
// function can also be called with '2', but does not seem to ever be.
}
}
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, 0, 1, 0, 0);
LoadWeaponDetour(character + CharacterWeaponOffset, 1, off, 0, 1, 0, 0);
}
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();
private readonly FixedDesigns _fixedDesigns;
private readonly CurrentManipulations _currentManipulations;
public RedrawManager(FixedDesigns fixedDesigns, CurrentManipulations currentManipulations)
{
SignatureHelper.Initialise(this);
Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw;
_fixedDesigns = fixedDesigns;
_currentManipulations = currentManipulations;
//_flagSlotForUpdateHook?.Enable();
//_loadWeaponHook?.Enable();
}
public void Dispose()
{
_flagSlotForUpdateHook?.Dispose();
_loadWeaponHook?.Dispose();
Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw;
}
private void OnCharacterRedraw(IntPtr addr, IntPtr modelId, IntPtr customize, IntPtr equipData)
{
try
{
var actor = (Actor)addr;
var identifier = actor.GetIdentifier();
if (_currentManipulations.TryGetDesign(identifier, out var save))
PluginLog.Information($"Loaded current design for {identifier}.");
else if (_fixedDesigns.TryGetDesign(identifier, out save))
PluginLog.Information($"Loaded fixed design for {identifier}.");
}
catch (Exception e)
{
PluginLog.Error($"Error on new draw object creation:\n{e}");
}
}
}

View file

@ -0,0 +1,208 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using DrawObject = Glamourer.Interop.DrawObject;
using Functions = Penumbra.GameData.Util.Functions;
namespace Glamourer.State;
public class CharacterSaveConverter : JsonConverter<CharacterSave>
{
public override void WriteJson(JsonWriter writer, CharacterSave? value, JsonSerializer serializer)
{
var s = value?.ToBase64() ?? string.Empty;
serializer.Serialize(writer, s);
}
public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave? existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var token = JToken.Load(reader);
var s = token.ToObject<string>();
return CharacterSave.FromString(s!);
}
}
[Flags]
public enum ApplicationFlags : uint
{
Customizations = 0x000001,
MainHand = 0x000002,
OffHand = 0x000004,
Head = 0x000008,
Body = 0x000010,
Hands = 0x000020,
Legs = 0x000040,
Feet = 0x000080,
Ears = 0x000100,
Neck = 0x000200,
Wrist = 0x000400,
RFinger = 0x000800,
LFinger = 0x001000,
SetVisor = 0x002000,
Visor = 0x004000,
SetWeapon = 0x008000,
Weapon = 0x010000,
SetWet = 0x020000,
Wet = 0x040000,
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct CharacterData
{
public const byte CurrentVersion = 3;
public uint ModelId;
public ApplicationFlags Flags;
public CustomizeData CustomizeData;
public CharacterWeapon MainHand;
public CharacterWeapon OffHand;
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;
public unsafe Customize Customize
{
get
{
fixed (CustomizeData* ptr = &CustomizeData)
{
return new Customize(ptr);
}
}
}
public unsafe CharacterEquip Equipment
{
get
{
fixed (CharacterArmor* ptr = &Head)
{
return new CharacterEquip(ptr);
}
}
}
public static readonly CharacterData Default
= new()
{
ModelId = 0,
Flags = 0,
CustomizeData = Customize.Default,
MainHand = CharacterWeapon.Empty,
OffHand = CharacterWeapon.Empty,
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,
};
public unsafe CharacterData Clone()
{
var data = new CharacterData();
fixed (void* ptr = &this)
{
Functions.MemCpyUnchecked(&data, ptr, sizeof(CharacterData));
}
return data;
}
private const ApplicationFlags SaveFlags = ApplicationFlags.Customizations
| ApplicationFlags.Head
| ApplicationFlags.Body
| ApplicationFlags.Hands
| ApplicationFlags.Legs
| ApplicationFlags.Feet
| ApplicationFlags.Ears
| ApplicationFlags.Neck
| ApplicationFlags.Wrist
| ApplicationFlags.RFinger
| ApplicationFlags.LFinger
| ApplicationFlags.MainHand
| ApplicationFlags.OffHand
| ApplicationFlags.SetVisor
| ApplicationFlags.SetWeapon;
public void Load(IDesignable designable)
{
ModelId = designable.ModelId;
Customize.Load(designable.Customize);
Equipment.Load(designable.Equip);
Flags = SaveFlags | (designable.VisorEnabled ? ApplicationFlags.Visor : 0) | (designable.WeaponEnabled ? ApplicationFlags.Weapon : 0);
}
}
public interface ICharacterData
{
public CharacterData Data { get; }
//public bool ApplyModel();
//public bool ApplyCustomize(Customize target);
//public bool ApplyWeapon(ref CharacterWeapon weapon, bool mainHand, bool offHand);
//public bool ApplyGear(ref CharacterArmor armor, EquipSlot slot);
//public unsafe bool ApplyWetness(CharacterBase* drawObject);
//public unsafe bool ApplyVisorState(CharacterBase* drawObject);
//public unsafe bool ApplyWeaponState(CharacterBase* drawObject);
}
[JsonConverter(typeof(CharacterSaveConverter))]
public class CharacterSave
{
private CharacterData _data = CharacterData.Default;
public CharacterSave()
{ }
public CharacterSave(Actor actor)
{
Load(actor);
}
public void Load<T>(T actor) where T : IDesignable
{
_data.Load(actor);
}
public string ToBase64()
=> string.Empty;
public Customize Customize
=> _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();
}

View file

@ -0,0 +1,63 @@
using Glamourer.Interop;
using Penumbra.GameData.Enums;
namespace Glamourer.State;
public unsafe class CurrentDesign : ICharacterData
{
public CharacterData Data
=> _drawData;
private CharacterData _drawData;
private CharacterData _initialData;
public CurrentDesign(Actor actor)
{
_initialData = new CharacterData();
if (!actor)
return;
_initialData.Load(actor);
var drawObject = actor.DrawObject;
if (drawObject.Valid)
_drawData.Load(drawObject);
else
_drawData = _initialData.Clone();
}
public void Update(Actor actor)
{
if (!actor)
return;
if (!_initialData.Customize.Equals(actor.Customize))
{
_initialData.Customize.Load(actor.Customize);
_drawData.Customize.Load(actor.Customize);
}
var initialEquip = _initialData.Equipment;
var currentEquip = actor.Equip;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var current = currentEquip[slot];
if (initialEquip[slot] != current)
{
initialEquip[slot] = current;
_drawData.Equipment[slot] = current;
}
}
if (_initialData.MainHand != actor.MainHand)
{
_initialData.MainHand = actor.MainHand;
_drawData.MainHand = actor.MainHand;
}
if (_initialData.OffHand != actor.OffHand)
{
_initialData.OffHand = actor.OffHand;
_drawData.OffHand = actor.OffHand;
}
}
}

View file

@ -0,0 +1,73 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Glamourer.Interop;
namespace Glamourer.State;
public class CurrentManipulations : IReadOnlyCollection<KeyValuePair<Actor.IIdentifier, CurrentDesign>>
{
private readonly Dictionary<Actor.IIdentifier, CurrentDesign> _characterSaves = new();
public IEnumerator<KeyValuePair<Actor.IIdentifier, CurrentDesign>> GetEnumerator()
=> _characterSaves.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _characterSaves.Count;
public CurrentDesign GetOrCreateSave(Actor actor)
{
var id = actor.GetIdentifier();
if (_characterSaves.TryGetValue(id, out var save))
{
save.Update(actor);
return save;
}
save = new CurrentDesign(actor);
_characterSaves.Add(id.CreatePermanent(), save);
return save;
}
public void DeleteSave(Actor.IIdentifier identifier)
=> _characterSaves.Remove(identifier);
public bool TryGetDesign(Actor.IIdentifier identifier, [NotNullWhen(true)] out CurrentDesign? 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.Customize.Race, save.Customize.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;
//}
//
}

View file

@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Glamourer.Interop;
namespace Glamourer; namespace Glamourer.State;
public class FixedDesigns public class FixedDesigns
{ {

View file

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Configuration; using Dalamud.Configuration;
namespace Glamourer namespace Glamourer.State
{ {
public class GlamourerConfig : IPluginConfiguration public class GlamourerConfig : IPluginConfiguration
{ {

View file

@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Interop;
namespace Glamourer; namespace Glamourer.Util;
public static class CharacterExtensions public static class CharacterExtensions
{ {
@ -27,7 +28,7 @@ public static class CharacterExtensions
public static unsafe bool SetHatVisible(this Character a, bool visible) public static unsafe bool SetHatVisible(this Character a, bool visible)
{ {
var current = IsHatVisible(a); var current = a.IsHatVisible();
if (current == visible) if (current == visible)
return false; return false;
@ -46,7 +47,7 @@ public static class CharacterExtensions
public static unsafe bool SetVisorToggled(this Character a, bool toggled) public static unsafe bool SetVisorToggled(this Character a, bool toggled)
{ {
var current = IsVisorToggled(a); var current = a.IsVisorToggled();
if (current == toggled) if (current == toggled)
return false; return false;
@ -67,7 +68,7 @@ public static class CharacterExtensions
public static unsafe bool SetWeaponHidden(this Character a, bool value) public static unsafe bool SetWeaponHidden(this Character a, bool value)
{ {
var hidden = IsWeaponHidden(a); var hidden = a.IsWeaponHidden();
if (hidden == value) if (hidden == value)
return false; return false;

View file

@ -1,9 +1,11 @@
using System; using System;
using System.Net.Http;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.State;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer; namespace Glamourer.Util;
public static unsafe class CustomizeExtensions public static unsafe class CustomizeExtensions
{ {
@ -54,4 +56,87 @@ public static unsafe class CustomizeExtensions
public static string ClanName(this Customize customize) public static string ClanName(this Customize customize)
=> ClanName(customize.Clan, customize.Gender); => ClanName(customize.Clan, customize.Gender);
// Change a gender and fix up all required customizations afterwards.
public static bool ChangeGender(this Customize customize, CharacterEquip equip, Gender gender)
{
if (customize.Gender == gender)
return false;
FixRestrictedGear(customize, equip, gender, customize.Race);
customize.Gender = gender;
FixUpAttributes(customize);
return true;
}
// Change a race and fix up all required customizations afterwards.
public static bool ChangeRace(this Customize customize, CharacterEquip equip, SubRace clan)
{
if (customize.Clan == clan)
return false;
var race = clan.ToRace();
var gender = race == Race.Hrothgar ? Gender.Male : customize.Gender; // TODO Female Hrothgar
FixRestrictedGear(customize, equip, gender, race);
customize.Gender = gender;
customize.Race = race;
customize.Clan = clan;
FixUpAttributes(customize);
return true;
}
public static void ChangeCustomization(this Customize customize, CharacterEquip equip, Customize newCustomize)
{
FixRestrictedGear(customize, equip, newCustomize.Gender, newCustomize.Race);
customize.Load(newCustomize);
}
public static bool ChangeCustomization(this Customize customize, CharacterEquip equip, CustomizationId id, byte value)
{
switch (id)
{
case CustomizationId.Race: return customize.ChangeRace(equip, (SubRace)value);
case CustomizationId.Gender: return customize.ChangeGender(equip, (Gender)value);
}
if (customize[id] == value)
return false;
customize[id] = value;
return true;
}
// Go through a whole customization struct and fix up all settings that need fixing.
private static void FixUpAttributes(Customize 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 static void FixRestrictedGear(Customize customize, CharacterEquip equip, Gender gender, Race race)
{
if (race == customize.Race && gender == customize.Gender)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
(_, equip[slot]) = Glamourer.RestrictedGear.ResolveRestricted(equip[slot], slot, race, gender);
}
} }