mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 10:17:23 +01:00
blep
This commit is contained in:
parent
cb2e2f0128
commit
941bba1518
39 changed files with 2569 additions and 1579 deletions
|
|
@ -6,59 +6,59 @@ namespace Glamourer.Customization;
|
|||
|
||||
public unsafe struct Customize
|
||||
{
|
||||
private readonly CustomizeData* _data;
|
||||
public readonly CustomizeData* Data;
|
||||
|
||||
public Customize(CustomizeData* data)
|
||||
=> _data = data;
|
||||
=> Data = data;
|
||||
|
||||
public Race Race
|
||||
{
|
||||
get => (Race)_data->Data[0];
|
||||
set => _data->Data[0] = (byte)value;
|
||||
get => (Race)Data->Data[0];
|
||||
set => Data->Data[0] = (byte)value;
|
||||
}
|
||||
|
||||
// Skip Unknown Gender
|
||||
public Gender Gender
|
||||
{
|
||||
get => (Gender)(_data->Data[1] + 1);
|
||||
set => _data->Data[1] = (byte)(value - 1);
|
||||
get => (Gender)(Data->Data[1] + 1);
|
||||
set => Data->Data[1] = (byte)(value - 1);
|
||||
}
|
||||
|
||||
public ref byte BodyType
|
||||
=> ref _data->Data[2];
|
||||
=> ref Data->Data[2];
|
||||
|
||||
public ref byte Height
|
||||
=> ref _data->Data[3];
|
||||
=> ref Data->Data[3];
|
||||
|
||||
public SubRace Clan
|
||||
{
|
||||
get => (SubRace)_data->Data[4];
|
||||
set => _data->Data[4] = (byte)value;
|
||||
get => (SubRace)Data->Data[4];
|
||||
set => Data->Data[4] = (byte)value;
|
||||
}
|
||||
|
||||
public ref byte Face
|
||||
=> ref _data->Data[5];
|
||||
=> ref Data->Data[5];
|
||||
|
||||
public ref byte Hairstyle
|
||||
=> ref _data->Data[6];
|
||||
=> ref Data->Data[6];
|
||||
|
||||
public bool HighlightsOn
|
||||
{
|
||||
get => _data->Data[7] >> 7 == 1;
|
||||
set => _data->Data[7] = (byte)(value ? _data->Data[7] | 0x80 : _data->Data[7] & 0x7F);
|
||||
get => Data->Data[7] >> 7 == 1;
|
||||
set => Data->Data[7] = (byte)(value ? Data->Data[7] | 0x80 : Data->Data[7] & 0x7F);
|
||||
}
|
||||
|
||||
public ref byte SkinColor
|
||||
=> ref _data->Data[8];
|
||||
=> ref Data->Data[8];
|
||||
|
||||
public ref byte EyeColorRight
|
||||
=> ref _data->Data[9];
|
||||
=> ref Data->Data[9];
|
||||
|
||||
public ref byte HairColor
|
||||
=> ref _data->Data[10];
|
||||
=> ref Data->Data[10];
|
||||
|
||||
public ref byte HighlightsColor
|
||||
=> ref _data->Data[11];
|
||||
=> ref Data->Data[11];
|
||||
|
||||
public readonly ref struct FacialFeatureStruct
|
||||
{
|
||||
|
|
@ -84,73 +84,73 @@ public unsafe struct Customize
|
|||
}
|
||||
|
||||
public FacialFeatureStruct FacialFeatures
|
||||
=> new(_data->Data + 12);
|
||||
=> new(Data->Data + 12);
|
||||
|
||||
public ref byte TattooColor
|
||||
=> ref _data->Data[13];
|
||||
=> ref Data->Data[13];
|
||||
|
||||
public ref byte Eyebrows
|
||||
=> ref _data->Data[14];
|
||||
=> ref Data->Data[14];
|
||||
|
||||
public ref byte EyeColorLeft
|
||||
=> ref _data->Data[15];
|
||||
=> ref Data->Data[15];
|
||||
|
||||
public byte EyeShape
|
||||
{
|
||||
get => (byte)(_data->Data[16] & 0x7F);
|
||||
set => _data->Data[16] = (byte)((value & 0x7F) | (_data->Data[16] & 0x80));
|
||||
get => (byte)(Data->Data[16] & 0x7F);
|
||||
set => Data->Data[16] = (byte)((value & 0x7F) | (Data->Data[16] & 0x80));
|
||||
}
|
||||
|
||||
public bool SmallIris
|
||||
{
|
||||
get => _data->Data[16] >> 7 == 1;
|
||||
set => _data->Data[16] = (byte)(value ? _data->Data[16] | 0x80 : _data->Data[16] & 0x7F);
|
||||
get => Data->Data[16] >> 7 == 1;
|
||||
set => Data->Data[16] = (byte)(value ? Data->Data[16] | 0x80 : Data->Data[16] & 0x7F);
|
||||
}
|
||||
|
||||
public ref byte Nose
|
||||
=> ref _data->Data[17];
|
||||
=> ref Data->Data[17];
|
||||
|
||||
public ref byte Jaw
|
||||
=> ref _data->Data[18];
|
||||
=> ref Data->Data[18];
|
||||
|
||||
public byte Mouth
|
||||
{
|
||||
get => (byte)(_data->Data[19] & 0x7F);
|
||||
set => _data->Data[19] = (byte)((value & 0x7F) | (_data->Data[19] & 0x80));
|
||||
get => (byte)(Data->Data[19] & 0x7F);
|
||||
set => Data->Data[19] = (byte)((value & 0x7F) | (Data->Data[19] & 0x80));
|
||||
}
|
||||
|
||||
public bool Lipstick
|
||||
{
|
||||
get => _data->Data[19] >> 7 == 1;
|
||||
set => _data->Data[19] = (byte)(value ? _data->Data[19] | 0x80 : _data->Data[19] & 0x7F);
|
||||
get => Data->Data[19] >> 7 == 1;
|
||||
set => Data->Data[19] = (byte)(value ? Data->Data[19] | 0x80 : Data->Data[19] & 0x7F);
|
||||
}
|
||||
|
||||
public ref byte LipColor
|
||||
=> ref _data->Data[20];
|
||||
=> ref Data->Data[20];
|
||||
|
||||
public ref byte MuscleMass
|
||||
=> ref _data->Data[21];
|
||||
=> ref Data->Data[21];
|
||||
|
||||
public ref byte TailShape
|
||||
=> ref _data->Data[22];
|
||||
=> ref Data->Data[22];
|
||||
|
||||
public ref byte BustSize
|
||||
=> ref _data->Data[23];
|
||||
=> ref Data->Data[23];
|
||||
|
||||
public byte FacePaint
|
||||
{
|
||||
get => (byte)(_data->Data[24] & 0x7F);
|
||||
set => _data->Data[24] = (byte)((value & 0x7F) | (_data->Data[24] & 0x80));
|
||||
get => (byte)(Data->Data[24] & 0x7F);
|
||||
set => Data->Data[24] = (byte)((value & 0x7F) | (Data->Data[24] & 0x80));
|
||||
}
|
||||
|
||||
public bool FacePaintReversed
|
||||
{
|
||||
get => _data->Data[24] >> 7 == 1;
|
||||
set => _data->Data[24] = (byte)(value ? _data->Data[24] | 0x80 : _data->Data[24] & 0x7F);
|
||||
get => Data->Data[24] >> 7 == 1;
|
||||
set => Data->Data[24] = (byte)(value ? Data->Data[24] | 0x80 : Data->Data[24] & 0x7F);
|
||||
}
|
||||
|
||||
public ref byte FacePaintColor
|
||||
=> ref _data->Data[25];
|
||||
=> ref Data->Data[25];
|
||||
|
||||
public static readonly CustomizeData Default = GenerateDefault();
|
||||
public static readonly CustomizeData Empty = new();
|
||||
|
|
@ -165,12 +165,12 @@ public unsafe struct Customize
|
|||
CustomizationId.Clan => (byte)Clan,
|
||||
CustomizationId.Face => Face,
|
||||
CustomizationId.Hairstyle => Hairstyle,
|
||||
CustomizationId.HighlightsOnFlag => _data->Data[7],
|
||||
CustomizationId.HighlightsOnFlag => Data->Data[7],
|
||||
CustomizationId.SkinColor => SkinColor,
|
||||
CustomizationId.EyeColorR => EyeColorRight,
|
||||
CustomizationId.HairColor => HairColor,
|
||||
CustomizationId.HighlightColor => HighlightsColor,
|
||||
CustomizationId.FacialFeaturesTattoos => _data->Data[12],
|
||||
CustomizationId.FacialFeaturesTattoos => Data->Data[12],
|
||||
CustomizationId.TattooColor => TattooColor,
|
||||
CustomizationId.Eyebrows => Eyebrows,
|
||||
CustomizationId.EyeColorL => EyeColorLeft,
|
||||
|
|
@ -204,7 +204,7 @@ public unsafe struct Customize
|
|||
case CustomizationId.EyeColorR: EyeColorRight = value; break;
|
||||
case CustomizationId.HairColor: HairColor = 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.Eyebrows: Eyebrows = value; break;
|
||||
case CustomizationId.EyeColorL: EyeColorLeft = value; break;
|
||||
|
|
@ -224,7 +224,7 @@ public unsafe struct Customize
|
|||
}
|
||||
|
||||
public bool Equals(Customize other)
|
||||
=> throw new NotImplementedException();
|
||||
=> CustomizeData.Equals(Data, other.Data);
|
||||
|
||||
public byte this[CustomizationId id]
|
||||
{
|
||||
|
|
@ -268,5 +268,8 @@ public unsafe struct Customize
|
|||
}
|
||||
|
||||
public void Load(Customize other)
|
||||
=> _data->Read(other._data);
|
||||
=> Data->Read(other.Data);
|
||||
|
||||
public void Write(IntPtr target)
|
||||
=> Data->Write((void*)target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ public partial class CustomizationOptions
|
|||
{
|
||||
var (skin, hair) = GetColors(race, 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.
|
||||
var set = new CustomizationSet(race, gender)
|
||||
{
|
||||
|
|
@ -148,8 +148,8 @@ public partial class CustomizationOptions
|
|||
EyeColors = _eyeColorPicker,
|
||||
HighlightColors = _highlightPicker,
|
||||
TattooColors = _tattooColorPicker,
|
||||
LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
|
||||
LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
|
||||
LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
|
||||
LipColorsLight = hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
|
||||
FacePaintColorsDark = _facePaintColorPickerDark,
|
||||
FacePaintColorsLight = _facePaintColorPickerLight,
|
||||
Faces = GetFaces(row),
|
||||
|
|
@ -164,10 +164,10 @@ public partial class CustomizationOptions
|
|||
|
||||
SetAvailability(set, row);
|
||||
SetFacialFeatures(set, row);
|
||||
SetHairByFace(set);
|
||||
SetMenuTypes(set, row);
|
||||
SetNames(set, row);
|
||||
|
||||
|
||||
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)))
|
||||
.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)
|
||||
{
|
||||
// Set up the menu types for all customizations.
|
||||
|
|
@ -270,6 +296,7 @@ public partial class CustomizationOptions
|
|||
{
|
||||
var count = set.Faces.Count;
|
||||
var featureDict = new List<IReadOnlyList<Customization>>(count);
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.VisualBasic;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Customization;
|
||||
|
|
@ -52,6 +53,7 @@ public class CustomizationSet
|
|||
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
|
||||
public IReadOnlyList<Customization> Faces { 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<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!;
|
||||
public IReadOnlyList<Customization> FacePaints { get; internal init; } = null!;
|
||||
|
|
@ -74,13 +76,22 @@ public class CustomizationSet
|
|||
=> OptionName[(int)id];
|
||||
|
||||
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)
|
||||
{
|
||||
var type = id.ToType();
|
||||
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))
|
||||
{
|
||||
|
|
@ -91,9 +102,9 @@ public class CustomizationSet
|
|||
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)
|
||||
return -1;
|
||||
|
||||
|
|
@ -103,27 +114,27 @@ public class CustomizationSet
|
|||
|
||||
return id switch
|
||||
{
|
||||
CustomizationId.SkinColor => Get(SkinColors, ref custom),
|
||||
CustomizationId.EyeColorL => Get(EyeColors, ref custom),
|
||||
CustomizationId.EyeColorR => Get(EyeColors, ref custom),
|
||||
CustomizationId.HairColor => Get(HairColors, ref custom),
|
||||
CustomizationId.HighlightColor => Get(HighlightColors, ref custom),
|
||||
CustomizationId.TattooColor => Get(TattooColors, ref custom),
|
||||
CustomizationId.LipColor => Get(LipColorsDark.Concat(LipColorsLight), ref custom),
|
||||
CustomizationId.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), ref custom),
|
||||
CustomizationId.SkinColor => Get(SkinColors, value, ref custom),
|
||||
CustomizationId.EyeColorL => Get(EyeColors, value, ref custom),
|
||||
CustomizationId.EyeColorR => Get(EyeColors, value, ref custom),
|
||||
CustomizationId.HairColor => Get(HairColors, value, ref custom),
|
||||
CustomizationId.HighlightColor => Get(HighlightColors, value, ref custom),
|
||||
CustomizationId.TattooColor => Get(TattooColors, value, ref custom),
|
||||
CustomizationId.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, ref custom),
|
||||
CustomizationId.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, ref custom),
|
||||
|
||||
CustomizationId.Face => Get(Faces, ref custom),
|
||||
CustomizationId.Hairstyle => Get(HairStyles, ref custom),
|
||||
CustomizationId.TailEarShape => Get(TailEarShapes, ref custom),
|
||||
CustomizationId.FacePaint => Get(FacePaints, ref custom),
|
||||
CustomizationId.FacialFeaturesTattoos => Get(FeaturesTattoos[0], ref custom),
|
||||
CustomizationId.Face => Get(Faces, HrothgarFaceHack(value), ref custom),
|
||||
CustomizationId.Hairstyle => Get(HairStyles, value, ref custom),
|
||||
CustomizationId.TailEarShape => Get(TailEarShapes, value, ref custom),
|
||||
CustomizationId.FacePaint => Get(FacePaints, value, ref custom),
|
||||
CustomizationId.FacialFeaturesTattoos => Get(FeaturesTattoos[0], value, ref custom),
|
||||
_ => 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();
|
||||
|
||||
switch (id.ToType())
|
||||
|
|
@ -135,7 +146,7 @@ public class CustomizationSet
|
|||
return id switch
|
||||
{
|
||||
CustomizationId.Face => Faces[idx],
|
||||
CustomizationId.Hairstyle => HairStyles[idx],
|
||||
CustomizationId.Hairstyle => face < HairByFace.Count ? HairByFace[face][idx] : HairStyles[idx],
|
||||
CustomizationId.TailEarShape => TailEarShapes[idx],
|
||||
CustomizationId.FacePaint => FacePaints[idx],
|
||||
CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx],
|
||||
|
|
@ -162,10 +173,13 @@ public class CustomizationSet
|
|||
ret[(int)CustomizationId.EyeColorL] = CustomizationId.EyeColorR;
|
||||
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))
|
||||
return 0;
|
||||
|
|
@ -176,7 +190,7 @@ public class CustomizationSet
|
|||
return id switch
|
||||
{
|
||||
CustomizationId.Face => Faces.Count,
|
||||
CustomizationId.Hairstyle => HairStyles.Count,
|
||||
CustomizationId.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face].Count : 0,
|
||||
CustomizationId.HighlightsOnFlag => 2,
|
||||
CustomizationId.SkinColor => SkinColors.Count,
|
||||
CustomizationId.EyeColorR => EyeColors.Count,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui.Raii;
|
||||
using Companion = Lumina.Excel.GeneratedSheets.Companion;
|
||||
|
||||
namespace Glamourer;
|
||||
|
|
@ -26,16 +26,37 @@ public class ModelData
|
|||
FirstName = $"{name} #{model.RowId:D4}";
|
||||
AllNames = $"#{model.RowId:D4}\n{name}";
|
||||
}
|
||||
|
||||
public uint Id
|
||||
=> Model.RowId;
|
||||
}
|
||||
|
||||
private readonly SortedList<uint, Data> _models;
|
||||
private readonly Dictionary<ulong, Data> _modelByData;
|
||||
|
||||
public IReadOnlyDictionary<uint, Data> 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)
|
||||
{
|
||||
var modelSheet = dataManager.GetExcelSheet<ModelChara>();
|
||||
var modelSheet = dataManager.GetExcelSheet<ModelChara>()!;
|
||||
|
||||
_models = new SortedList<uint, Data>(NpcNames.ModelCharas.Count);
|
||||
|
||||
|
|
@ -71,5 +92,20 @@ public class ModelData
|
|||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using Dalamud.Logging;
|
|||
using Dalamud.Utility;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Race = Penumbra.GameData.Enums.Race;
|
||||
|
|
@ -356,16 +355,6 @@ public class RestrictedGear
|
|||
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(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(13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves
|
||||
AddItem(13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -21,6 +21,9 @@ public readonly struct Item
|
|||
public bool HasSubModel
|
||||
=> 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.
|
||||
public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,17 +27,17 @@ public class GlamourerIpc : IDisposable
|
|||
private readonly ObjectTable _objectTable;
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
|
||||
internal ICallGateProvider<string, string?>? ProviderGetAllCustomization;
|
||||
internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter;
|
||||
internal ICallGateProvider<string, string, object>? ProviderApplyAll;
|
||||
internal ICallGateProvider<string, Character?, object>? ProviderApplyAllToCharacter;
|
||||
internal ICallGateProvider<string, string, object>? ProviderApplyOnlyCustomization;
|
||||
internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyCustomizationToCharacter;
|
||||
internal ICallGateProvider<string, string, object>? ProviderApplyOnlyEquipment;
|
||||
internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyEquipmentToCharacter;
|
||||
internal ICallGateProvider<string, object>? ProviderRevert;
|
||||
internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
|
||||
internal ICallGateProvider<int>? ProviderGetApiVersion;
|
||||
//internal ICallGateProvider<string, string?>? ProviderGetAllCustomization;
|
||||
//internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter;
|
||||
//internal ICallGateProvider<string, string, object>? ProviderApplyAll;
|
||||
//internal ICallGateProvider<string, Character?, object>? ProviderApplyAllToCharacter;
|
||||
//internal ICallGateProvider<string, string, object>? ProviderApplyOnlyCustomization;
|
||||
//internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyCustomizationToCharacter;
|
||||
//internal ICallGateProvider<string, string, object>? ProviderApplyOnlyEquipment;
|
||||
//internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyEquipmentToCharacter;
|
||||
//internal ICallGateProvider<string, object>? ProviderRevert;
|
||||
//internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
|
||||
//internal ICallGateProvider<int>? ProviderGetApiVersion;
|
||||
|
||||
public GlamourerIpc(ClientState clientState, ObjectTable objectTable, DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Structs;
|
||||
using ImGuiNET;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Api;
|
||||
|
||||
public class PenumbraAttach : IDisposable
|
||||
{
|
||||
public const int RequiredPenumbraBreakingVersion = 4;
|
||||
public const int RequiredPenumbraFeatureVersion = 0;
|
||||
public const int RequiredPenumbraFeatureVersion = 12;
|
||||
|
||||
private ICallGateSubscriber<ChangedItemType, uint, object>? _tooltipSubscriber;
|
||||
private ICallGateSubscriber<MouseButton, ChangedItemType, uint, object>? _clickSubscriber;
|
||||
|
|
@ -18,11 +21,14 @@ public class PenumbraAttach : IDisposable
|
|||
private ICallGateSubscriber<GameObject, int, object>? _redrawSubscriberObject;
|
||||
private ICallGateSubscriber<IntPtr, (IntPtr, string)>? _drawObjectInfo;
|
||||
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?> _disposedEvent;
|
||||
|
||||
public event Action<IntPtr, IntPtr, IntPtr, IntPtr>? CreatingCharacterBase;
|
||||
public event Action<IntPtr, IntPtr>? CreatedCharacterBase;
|
||||
|
||||
public PenumbraAttach(bool attach)
|
||||
{
|
||||
|
|
@ -51,6 +57,7 @@ public class PenumbraAttach : IDisposable
|
|||
_redrawSubscriberName = Dalamud.PluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
|
||||
_redrawSubscriberObject = Dalamud.PluginInterface.GetIpcSubscriber<GameObject, int, object>("Penumbra.RedrawObject");
|
||||
_drawObjectInfo = Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, (IntPtr, string)>("Penumbra.GetDrawObjectInfo");
|
||||
_cutsceneParent = Dalamud.PluginInterface.GetIpcSubscriber<int, int>("Penumbra.GetCutsceneParentIndex");
|
||||
|
||||
if (!attach)
|
||||
return;
|
||||
|
|
@ -60,9 +67,12 @@ public class PenumbraAttach : IDisposable
|
|||
Dalamud.PluginInterface.GetIpcSubscriber<MouseButton, ChangedItemType, uint, object>("Penumbra.ChangedItemClick");
|
||||
_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);
|
||||
_clickSubscriber.Subscribe(PenumbraRightClick);
|
||||
_creatingCharacterBase.Subscribe(SubscribeCharacterBase);
|
||||
_creatingCharacterBase.Subscribe(SubscribeCreatingCharacterBase);
|
||||
_createdCharacterBase.Subscribe(SubscribeCreatedCharacterBase);
|
||||
PluginLog.Debug("Glamourer attached to Penumbra.");
|
||||
}
|
||||
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);
|
||||
|
||||
private void SubscribeCreatedCharacterBase(IntPtr gameObject, string _, IntPtr drawObject)
|
||||
=> CreatedCharacterBase?.Invoke(gameObject, drawObject);
|
||||
|
||||
public void Unattach()
|
||||
{
|
||||
_tooltipSubscriber?.Unsubscribe(PenumbraTooltip);
|
||||
_clickSubscriber?.Unsubscribe(PenumbraRightClick);
|
||||
_creatingCharacterBase?.Unsubscribe(SubscribeCharacterBase);
|
||||
_creatingCharacterBase?.Unsubscribe(SubscribeCreatingCharacterBase);
|
||||
_createdCharacterBase?.Unsubscribe(SubscribeCreatedCharacterBase);
|
||||
_tooltipSubscriber = null;
|
||||
_clickSubscriber = null;
|
||||
_creatingCharacterBase = null;
|
||||
|
|
@ -109,25 +123,54 @@ public class PenumbraAttach : IDisposable
|
|||
if (button != MouseButton.Right || type != ChangedItemType.Item)
|
||||
return;
|
||||
|
||||
//var gPose = ObjectManager.GPosePlayer;
|
||||
//var player = ObjectManager.Player;
|
||||
//var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(id)!;
|
||||
//var writeItem = new Item(item, string.Empty);
|
||||
//if (gPose != null)
|
||||
//{
|
||||
// writeItem.Write(gPose.Address);
|
||||
// UpdateCharacters(gPose, player);
|
||||
//}
|
||||
//else if (player != null)
|
||||
//{
|
||||
// writeItem.Write(player.Address);
|
||||
// UpdateCharacters(player);
|
||||
//}
|
||||
var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(id)!;
|
||||
var writeItem = new Item(item, string.Empty);
|
||||
|
||||
UpdateItem(ObjectManager.GPosePlayer, writeItem);
|
||||
UpdateItem(ObjectManager.Player, writeItem);
|
||||
}
|
||||
|
||||
private static void UpdateItem(Actor actor, Item item)
|
||||
{
|
||||
if (!actor || !actor.DrawObject)
|
||||
return;
|
||||
|
||||
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)
|
||||
=> _drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero;
|
||||
|
||||
public int CutsceneParent(int idx)
|
||||
=> _cutsceneParent?.InvokeFunc(idx) ?? -1;
|
||||
|
||||
public void RedrawObject(GameObject? actor, RedrawType settings, bool repeat)
|
||||
{
|
||||
if (actor == null)
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Glamourer.State;
|
||||
using Glamourer.Structs;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
|
|
@ -14,3 +18,9 @@ public class Design
|
|||
public override string ToString()
|
||||
=> Name;
|
||||
}
|
||||
|
||||
public struct ArmorData
|
||||
{
|
||||
public CharacterArmor Model;
|
||||
public bool Ignore;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,172 @@
|
|||
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;
|
||||
|
||||
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 FixedDesign
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Glamourer.State;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
using System.Reflection;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Glamourer.Api;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Gui;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.State;
|
||||
using Penumbra.GameData;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
|
|
@ -25,14 +33,18 @@ public class Glamourer : IDalamudPlugin
|
|||
|
||||
public static GlamourerConfig Config = null!;
|
||||
|
||||
public static IObjectIdentifier Identifier = null!;
|
||||
public static PenumbraAttach Penumbra = null!;
|
||||
public static ICustomizationManager Customization = null!;
|
||||
public static RestrictedGear RestrictedGear = null!;
|
||||
public static ModelData Models = null!;
|
||||
public static RedrawManager RedrawManager = null!;
|
||||
private readonly WindowSystem _windowSystem = new("Glamourer");
|
||||
private readonly FixedDesigns _fixedDesigns;
|
||||
private readonly CurrentManipulations _currentManipulations;
|
||||
public readonly FixedDesigns FixedDesigns;
|
||||
public readonly CurrentManipulations CurrentManipulations;
|
||||
|
||||
private readonly WindowSystem _windowSystem = new("Glamourer");
|
||||
private readonly Interface _interface;
|
||||
|
||||
//public readonly DesignManager Designs;
|
||||
|
||||
//public static RevertableDesigns RevertableDesigns = new();
|
||||
|
|
@ -42,14 +54,17 @@ public class Glamourer : IDalamudPlugin
|
|||
{
|
||||
Dalamud.Initialize(pluginInterface);
|
||||
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();
|
||||
Penumbra = new PenumbraAttach(Config.AttachToPenumbra);
|
||||
_fixedDesigns = new FixedDesigns();
|
||||
|
||||
FixedDesigns = new FixedDesigns();
|
||||
CurrentManipulations = new CurrentManipulations();
|
||||
//Designs = new DesignManager();
|
||||
|
||||
//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)
|
||||
{
|
||||
|
|
@ -149,11 +164,11 @@ public class Glamourer : IDalamudPlugin
|
|||
//
|
||||
public void OnGlamour(string command, string arguments)
|
||||
{
|
||||
static void PrintHelp()
|
||||
{
|
||||
Dalamud.Chat.Print("Usage:");
|
||||
Dalamud.Chat.Print($" {HelpString}");
|
||||
}
|
||||
//static void PrintHelp()
|
||||
//{
|
||||
// Dalamud.Chat.Print("Usage:");
|
||||
// Dalamud.Chat.Print($" {HelpString}");
|
||||
//}
|
||||
|
||||
//arguments = arguments.Trim();
|
||||
//if (!arguments.Any())
|
||||
|
|
|
|||
|
|
@ -118,9 +118,7 @@
|
|||
</None>
|
||||
</ItemGroup>
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec
|
||||
Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll $(SolutionDir)$(SolutionName).zip" />
|
||||
<Exec
|
||||
Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
|
||||
<Exec 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)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.State;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Gui;
|
||||
|
||||
|
|
@ -14,8 +16,15 @@ internal partial class Interface
|
|||
{
|
||||
private class ActorTab
|
||||
{
|
||||
private ObjectManager.ActorData _data = new(string.Empty, new Actor.InvalidIdentifier(), Actor.Null, false, Actor.Null);
|
||||
private Actor _nextSelect = Actor.Null;
|
||||
private readonly CurrentManipulations _manipulations;
|
||||
|
||||
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()
|
||||
{
|
||||
|
|
@ -24,62 +33,107 @@ internal partial class Interface
|
|||
return;
|
||||
|
||||
DrawActorSelector();
|
||||
if (_data.Label.Length == 0)
|
||||
return;
|
||||
if (!ObjectManager.Actors.TryGetValue(_identifier, out _currentData))
|
||||
_currentData = ObjectManager.ActorData.Invalid;
|
||||
else
|
||||
_currentLabel = _currentData.Label;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (_data.Actor.IsHuman)
|
||||
DrawActorPanel();
|
||||
else
|
||||
DrawMonsterPanel();
|
||||
DrawPanel();
|
||||
}
|
||||
|
||||
private void DrawActorPanel()
|
||||
private unsafe void DrawPanel()
|
||||
{
|
||||
using var group = ImRaii.Group();
|
||||
if (!Glamourer.RedrawManager.CurrentManipulations.GetSave(_data.Actor, out var save))
|
||||
if (_identifier == Actor.IIdentifier.Invalid)
|
||||
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();
|
||||
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)
|
||||
DrawPanelHeader();
|
||||
using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true);
|
||||
if (!child || _currentSave == null)
|
||||
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)
|
||||
{
|
||||
_data.Actor.SetModelId((int)id);
|
||||
_data.Actor.ObjectKind =
|
||||
Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
|
||||
}
|
||||
ImGuiUtil.HoverTooltip(data.AllNames);
|
||||
}
|
||||
var color = _currentData.Valid ? GreenHeaderColor : RedHeaderColor;
|
||||
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
|
||||
using var c = ImRaii.PushColor(ImGuiCol.Text, color)
|
||||
.Push(ImGuiCol.Button, buttonColor)
|
||||
.Push(ImGuiCol.ButtonHovered, buttonColor)
|
||||
.Push(ImGuiCol.ButtonActive, buttonColor);
|
||||
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;
|
||||
|
||||
|
|
@ -91,63 +145,55 @@ internal partial class Interface
|
|||
.Push(ImGuiStyleVar.FrameRounding, 0);
|
||||
ImGui.SetNextItemWidth(_actorSelectorWidth);
|
||||
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();
|
||||
}
|
||||
|
||||
private void UpdateSelection(ObjectManager.ActorData data)
|
||||
private void DrawSelector(Vector2 oldSpacing)
|
||||
{
|
||||
_data = data;
|
||||
//_character.Load(_data.Actor);
|
||||
using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true);
|
||||
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)
|
||||
{
|
||||
if (_nextSelect && _nextSelect == data.Actor || data.Label == _data.Label)
|
||||
UpdateSelection(data);
|
||||
return data.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
private bool CheckFilter((Actor.IIdentifier, ObjectManager.ActorData) pair)
|
||||
=> _actorFilter.IsEmpty || pair.Item2.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;
|
||||
if (ImGui.Selectable(data.Label, equal) && !equal)
|
||||
UpdateSelection(data);
|
||||
var equal = pair.Item1.Equals(_identifier);
|
||||
if (ImGui.Selectable(pair.Item2.Label, equal) && !equal)
|
||||
{
|
||||
_identifier = pair.Item1.CreatePermanent();
|
||||
_currentData = pair.Item2;
|
||||
_currentSave = _currentData.Valid ? _manipulations.GetOrCreateSave(_currentData.Objects[0]) : null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSelectionButtons()
|
||||
{
|
||||
_nextSelect = Actor.Null;
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||
.Push(ImGuiStyleVar.FrameRounding, 0);
|
||||
var buttonWidth = new Vector2(_actorSelectorWidth / 2, 0);
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth
|
||||
, "Select the local player character.", !ObjectManager.Player, true))
|
||||
_nextSelect = _inGPose ? ObjectManager.GPosePlayer : ObjectManager.Player;
|
||||
_identifier = ObjectManager.Player.GetIdentifier();
|
||||
|
||||
ImGui.SameLine();
|
||||
Actor targetActor = Dalamud.Targets.Target?.Address;
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth,
|
||||
"Select the current target, if it is in the list.", _inGPose || !targetActor, true))
|
||||
_nextSelect = targetActor;
|
||||
"Select the current target, if it is in the list.", ObjectManager.IsInGPose || !targetActor, true))
|
||||
_identifier = targetActor.GetIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ActorTab _actorTab = new();
|
||||
}
|
||||
|
||||
//internal partial class Interface
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Logging;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Util;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
|
|
@ -15,224 +18,221 @@ namespace Glamourer.Gui;
|
|||
|
||||
internal partial class Interface
|
||||
{
|
||||
private static byte _tempStorage;
|
||||
private static CustomizationId _tempType;
|
||||
|
||||
private static bool DrawCustomization(Customize customize, CharacterEquip equip, bool locked)
|
||||
private class CustomizationDrawer
|
||||
{
|
||||
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"))
|
||||
return false;
|
||||
return;
|
||||
|
||||
var ret = DrawRaceGenderSelector(customize, equip, locked);
|
||||
var set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
|
||||
using var disabled = ImRaii.Disabled(locked);
|
||||
|
||||
foreach (var id in set.Order[CharaMakeParams.MenuType.Percentage])
|
||||
ret |= PercentageSelector(set, id, customize, locked);
|
||||
d.DrawRaceGenderSelector();
|
||||
|
||||
Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.IconSelector], c => DrawIconSelector(set, c, customize, locked),
|
||||
ImGui.SameLine);
|
||||
d._set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
|
||||
|
||||
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])
|
||||
ret |= DrawListSelector(set, id, customize, locked);
|
||||
Functions.IteratePairwise(d._set.Order[CharaMakeParams.MenuType.IconSelector], d.DrawIconSelector, ImGui.SameLine);
|
||||
|
||||
Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.ColorPicker], c => DrawColorPicker(set, c, customize, locked),
|
||||
ImGui.SameLine);
|
||||
d.DrawMultiIconSelector();
|
||||
|
||||
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;
|
||||
ImGui.SameLine(xPos);
|
||||
ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}",
|
||||
customize.FacePaintReversed, b => customize.FacePaintReversed = b, locked);
|
||||
ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}",
|
||||
customize.SmallIris, b => customize.SmallIris = b, locked);
|
||||
d.Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {d._set.Option(CustomizationId.FacePaint)}",
|
||||
customize.FacePaintReversed, b => customize.FacePaintReversed = b);
|
||||
d.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}",
|
||||
customize.SmallIris, b => customize.SmallIris = b);
|
||||
|
||||
if (customize.Race != Race.Hrothgar)
|
||||
{
|
||||
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 static bool DrawRaceGenderSelector(Customize customize, CharacterEquip equip, bool locked)
|
||||
private void DrawRaceGenderSelector()
|
||||
{
|
||||
var ret = DrawGenderSelector(customize, equip, locked);
|
||||
DrawGenderSelector();
|
||||
ImGui.SameLine();
|
||||
using var group = ImRaii.Group();
|
||||
ret |= DrawRaceCombo(customize, equip, locked);
|
||||
DrawRaceCombo();
|
||||
var gender = Glamourer.Customization.GetName(CustomName.Gender);
|
||||
var clan = Glamourer.Customization.GetName(CustomName.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);
|
||||
var icon = customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
|
||||
var restricted = customize.Race == Race.Hrothgar;
|
||||
var icon = _customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
|
||||
var restricted = _customize.Race == Race.Hrothgar;
|
||||
if (restricted)
|
||||
icon = FontAwesomeIcon.MarsDouble;
|
||||
|
||||
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted || locked, true))
|
||||
return false;
|
||||
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted, true))
|
||||
return;
|
||||
|
||||
var gender = customize.Gender == Gender.Male ? Gender.Female : Gender.Male;
|
||||
return false; //customize.ChangeGender(gender, locked ? CharacterEquip.Null : equip);
|
||||
var gender = _customize.Gender == Gender.Male ? Gender.Female : Gender.Male;
|
||||
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);
|
||||
using var combo = ImRaii.Combo("##subRaceCombo", customize.ClanName());
|
||||
using var combo = ImRaii.Combo("##subRaceCombo", _customize.ClanName());
|
||||
if (!combo)
|
||||
return false;
|
||||
return;
|
||||
|
||||
if (locked)
|
||||
ImGui.CloseCurrentPopup();
|
||||
|
||||
var ret = false;
|
||||
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
|
||||
{
|
||||
if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, customize.Gender), subRace == customize.Clan))
|
||||
ret |= false; //customize.ChangeRace(subRace, equip);
|
||||
if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, _customize.Gender), subRace == _customize.Clan)
|
||||
&& _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 static bool Checkbox(string label, bool current, Action<bool> setter, bool locked)
|
||||
private void Checkbox(string label, bool current, Action<bool> setter)
|
||||
{
|
||||
var tmp = current;
|
||||
var ret = false;
|
||||
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
|
||||
if (ImGui.Checkbox($"##{label}", ref tmp) && tmp == current && !locked)
|
||||
if (ImGui.Checkbox($"##{label}", ref tmp) && tmp != current)
|
||||
{
|
||||
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.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 _ = ImRaii.PushId((int)id);
|
||||
int value = id == _tempType ? _tempStorage : customization[id];
|
||||
var count = set.Count(id);
|
||||
int value = _customize[id];
|
||||
var count = _set.Count(id);
|
||||
ImGui.SetNextItemWidth(_comboSelectorSize);
|
||||
|
||||
var (min, max) = locked ? (value, value) : (0, count - 1);
|
||||
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
|
||||
if (ImGui.SliderInt("##slider", ref value, min, max, string.Empty, ImGuiSliderFlags.AlwaysClamp) && !locked)
|
||||
void OnChange(int v)
|
||||
{
|
||||
_tempStorage = (byte)value;
|
||||
_tempType = id;
|
||||
_customize[id] = (byte)v;
|
||||
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();
|
||||
ret |= InputInt("##input", id, --value, min, max, locked);
|
||||
|
||||
alpha.Pop();
|
||||
InputInt("##input", --value, 0, count - 1, OnChange);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(set.OptionName[(int)id]);
|
||||
|
||||
if (ret)
|
||||
customization[id] = _tempStorage;
|
||||
|
||||
return ret;
|
||||
ImGui.TextUnformatted(_set.OptionName[(int)id]);
|
||||
}
|
||||
|
||||
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;
|
||||
ImGui.SetNextItemWidth(_inputIntSize);
|
||||
if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue)
|
||||
&& !locked
|
||||
&& tmp != startValue + 1
|
||||
&& tmp >= minValue
|
||||
&& tmp <= maxValue)
|
||||
{
|
||||
_tempType = id;
|
||||
_tempStorage = (byte)(tmp - 1);
|
||||
}
|
||||
setter(tmp);
|
||||
|
||||
var ret = ImGui.IsItemDeactivatedAfterEdit() && !locked;
|
||||
if (!locked)
|
||||
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";
|
||||
|
||||
using var bigGroup = ImRaii.Group();
|
||||
using var _ = ImRaii.PushId((int)id);
|
||||
var count = set.Count(id);
|
||||
var label = set.Option(id);
|
||||
var count = _set.Count(id, _customize.Face);
|
||||
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)
|
||||
{
|
||||
label = $"{label} (Custom #{customize[id]})";
|
||||
label = $"{label} (Custom #{_customize[id]})";
|
||||
current = 0;
|
||||
custom = set.Data(id, 0);
|
||||
custom = _set.Data(id, 0);
|
||||
}
|
||||
|
||||
var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId);
|
||||
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
|
||||
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize) && !locked)
|
||||
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
|
||||
ImGui.OpenPopup(popupName);
|
||||
|
||||
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();
|
||||
using var group = ImRaii.Group();
|
||||
var (min, max) = locked ? (current, current) : (1, count);
|
||||
var ret = InputInt("##text", id, current, min, max, locked);
|
||||
if (ret)
|
||||
customize[id] = set.Data(id, _tempStorage).Value;
|
||||
InputInt("##text", current, 1, count, OnChange);
|
||||
|
||||
ImGui.TextUnformatted($"{label} ({custom.Value.Value})");
|
||||
|
||||
ret |= DrawIconPickerPopup(popupName, set, id, customize);
|
||||
|
||||
return ret;
|
||||
DrawIconPickerPopup(popupName, id, OnChange);
|
||||
}
|
||||
|
||||
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);
|
||||
if (!popup)
|
||||
return false;
|
||||
return;
|
||||
|
||||
var ret = false;
|
||||
var count = set.Count(id);
|
||||
var count = _set.Count(id, _customize.Face);
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||
.Push(ImGuiStyleVar.FrameRounding, 0);
|
||||
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);
|
||||
using var group = ImRaii.Group();
|
||||
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
|
||||
{
|
||||
customize[id] = custom.Value;
|
||||
ret = true;
|
||||
setter(custom.Value);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
|
|
@ -247,173 +247,156 @@ internal partial class Interface
|
|||
if (i % 8 != 7)
|
||||
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";
|
||||
using var _ = ImRaii.PushId((int)id);
|
||||
var ret = false;
|
||||
var count = set.Count(id);
|
||||
var label = set.Option(id);
|
||||
var (current, custom) = GetCurrentCustomization(set, id, customize);
|
||||
var count = _set.Count(id);
|
||||
var label = _set.Option(id);
|
||||
var (current, custom) = GetCurrentCustomization(id);
|
||||
|
||||
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
|
||||
if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, _framedIconSize)
|
||||
&& !locked)
|
||||
if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None,
|
||||
_framedIconSize))
|
||||
ImGui.OpenPopup(popupName);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (var group = ImRaii.Group())
|
||||
void OnChange(int v)
|
||||
{
|
||||
var (min, max) = locked ? (current, current) : (1, count);
|
||||
if (InputInt("##text", id, current, min, max, locked))
|
||||
{
|
||||
customize[id] = set.Data(id, current).Value;
|
||||
ret = true;
|
||||
_customize[id] = _set.Data(id, v).Value;
|
||||
foreach (var actor in _actors.Where(a => a && a.DrawObject))
|
||||
Glamourer.RedrawManager.UpdateCustomize(actor.DrawObject, _customize);
|
||||
}
|
||||
|
||||
using (var group = ImRaii.Group())
|
||||
{
|
||||
InputInt("##text", current, 1, count, OnChange);
|
||||
ImGui.TextUnformatted(label);
|
||||
}
|
||||
|
||||
return ret | DrawColorPickerPopup(popupName, set, id, customize);
|
||||
DrawColorPickerPopup(popupName, id, OnChange);
|
||||
}
|
||||
|
||||
private static (int, Customization.Customization) GetCurrentCustomization(CustomizationSet set, CustomizationId id,
|
||||
Customize customize)
|
||||
private (int, Customization.Customization) GetCurrentCustomization(CustomizationId id)
|
||||
{
|
||||
var current = set.DataByValue(id, customize[id], out var custom);
|
||||
if (set.IsAvailable(id) && current < 0)
|
||||
var current = _set.DataByValue(id, _customize[id], out var custom);
|
||||
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;
|
||||
custom = set.Data(id, 0);
|
||||
custom = _set.Data(id, 0);
|
||||
}
|
||||
|
||||
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);
|
||||
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)
|
||||
.Push(ImGuiStyleVar.FrameRounding, 0);
|
||||
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)))
|
||||
{
|
||||
customize[id] = custom.Value;
|
||||
ret = true;
|
||||
setter(custom.Value);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
if (i % 8 != 7)
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMultiIconSelector(CustomizationSet set, Customize customize, bool locked)
|
||||
private void DrawMultiIconSelector()
|
||||
{
|
||||
using var bigGroup = ImRaii.Group();
|
||||
using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos);
|
||||
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
|
||||
var ret = DrawMultiIcons(set, customize, locked);
|
||||
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))
|
||||
|
||||
void OnChange(int v)
|
||||
{
|
||||
customize[CustomizationId.FacialFeaturesTattoos] = (byte)value;
|
||||
ret = true;
|
||||
_customize[CustomizationId.FacialFeaturesTattoos] = (byte)v;
|
||||
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();
|
||||
var face = customize.Face;
|
||||
if (set.Faces.Count < face)
|
||||
face = 1;
|
||||
var face = _customize.Face;
|
||||
|
||||
var ret = false;
|
||||
var count = set.Count(CustomizationId.FacialFeaturesTattoos);
|
||||
var count = _set.Count(CustomizationId.FacialFeaturesTattoos);
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var enabled = customize.FacialFeatures[i];
|
||||
var feature = set.FacialFeature(face, i);
|
||||
var enabled = _customize.FacialFeatures[i];
|
||||
var feature = _set.FacialFeature(face, i);
|
||||
var icon = i == count - 1
|
||||
? LegacyTattoo ?? 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,
|
||||
Vector4.Zero, enabled ? Vector4.One : RedTint)
|
||||
&& !locked)
|
||||
Vector4.Zero, enabled ? Vector4.One : RedTint))
|
||||
{
|
||||
customize.FacialFeatures.Set(i, !enabled);
|
||||
ret = true;
|
||||
_customize.FacialFeatures.Set(i, !enabled);
|
||||
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);
|
||||
|
||||
if (i % 4 != 3)
|
||||
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 bigGroup = ImRaii.Group();
|
||||
var ret = false;
|
||||
int current = customize[id];
|
||||
var count = set.Count(id);
|
||||
int current = _customize[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);
|
||||
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)
|
||||
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;
|
||||
|
||||
customize[id] = (byte)i;
|
||||
ret = true;
|
||||
OnChange(i);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var (min, max) = locked ? (current, current) : (1, count);
|
||||
if (InputInt("##text", id, current, min, max, locked))
|
||||
{
|
||||
customize[id] = (byte)current;
|
||||
ret = true;
|
||||
}
|
||||
InputInt("##text", current, 1, count, OnChange);
|
||||
|
||||
ImGui.SameLine();
|
||||
alpha.Pop();
|
||||
ImGui.TextUnformatted(set.Option(id));
|
||||
|
||||
return ret;
|
||||
ImGui.TextUnformatted(_set.Option(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using Glamourer.State;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Numerics;
|
|||
using System.Reflection;
|
||||
using Dalamud.Interface;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Interop;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Glamourer.Gui;
|
||||
|
|
@ -20,13 +21,11 @@ internal partial class Interface
|
|||
private static float _inputIntSize;
|
||||
private static float _comboSelectorSize;
|
||||
private static float _raceSelectorWidth;
|
||||
private static bool _inGPose;
|
||||
|
||||
|
||||
private static void UpdateState()
|
||||
{
|
||||
// General
|
||||
_inGPose = ObjectManager.IsInGPose();
|
||||
_spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 };
|
||||
_actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Logging;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.State;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
|
||||
namespace Glamourer.Gui;
|
||||
|
||||
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 ActorTab _actorTab;
|
||||
private readonly DebugStateTab _debugStateTab;
|
||||
|
||||
public Interface(Glamourer plugin)
|
||||
: base(GetLabel())
|
||||
{
|
||||
|
|
@ -23,6 +114,8 @@ internal partial class Interface : Window, IDisposable
|
|||
MinimumSize = new Vector2(675, 675),
|
||||
MaximumSize = ImGui.GetIO().DisplaySize,
|
||||
};
|
||||
_actorTab = new ActorTab(_plugin.CurrentManipulations);
|
||||
_debugStateTab = new DebugStateTab(_plugin.CurrentManipulations);
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
|
|
@ -31,14 +124,22 @@ internal partial class Interface : Window, IDisposable
|
|||
if (!tabBar)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
UpdateState();
|
||||
|
||||
_actorTab.Draw();
|
||||
DrawSettingsTab();
|
||||
_debugStateTab.Draw();
|
||||
// DrawSaves();
|
||||
// DrawFixedDesignsTab();
|
||||
// DrawRevertablesTab();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PluginLog.Error($"Unexpected Error during Draw:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ internal partial class Interface
|
|||
//private bool _holdShift;
|
||||
//private bool _holdCtrl;
|
||||
//private const string DesignNamePopupLabel = "Save Design As...";
|
||||
//private const uint RedHeaderColor = 0xFF1818C0;
|
||||
//private const uint GreenHeaderColor = 0xFF18C018;
|
||||
|
||||
//
|
||||
//private void DrawPlayerHeader()
|
||||
//{
|
||||
|
|
|
|||
350
Glamourer/Interop/Actor.Identifier.cs
Normal file
350
Glamourer/Interop/Actor.Identifier.cs
Normal 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
232
Glamourer/Interop/Actor.cs
Normal 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;
|
||||
}
|
||||
193
Glamourer/Interop/ObjectManager.cs
Normal file
193
Glamourer/Interop/ObjectManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
23
Glamourer/Interop/Offsets.cs
Normal file
23
Glamourer/Interop/Offsets.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
287
Glamourer/Interop/RedrawManager.cs
Normal file
287
Glamourer/Interop/RedrawManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
208
Glamourer/State/CharacterSave.cs
Normal file
208
Glamourer/State/CharacterSave.cs
Normal 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();
|
||||
}
|
||||
63
Glamourer/State/CurrentDesign.cs
Normal file
63
Glamourer/State/CurrentDesign.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Glamourer/State/CurrentManipulations.cs
Normal file
73
Glamourer/State/CurrentManipulations.cs
Normal 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;
|
||||
//}
|
||||
//
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Glamourer.Interop;
|
||||
|
||||
namespace Glamourer;
|
||||
namespace Glamourer.State;
|
||||
|
||||
public class FixedDesigns
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using Dalamud.Configuration;
|
||||
|
||||
namespace Glamourer
|
||||
namespace Glamourer.State
|
||||
{
|
||||
public class GlamourerConfig : IPluginConfiguration
|
||||
{
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Glamourer.Interop;
|
||||
|
||||
namespace Glamourer;
|
||||
namespace Glamourer.Util;
|
||||
|
||||
public static class CharacterExtensions
|
||||
{
|
||||
|
|
@ -27,7 +28,7 @@ public static class CharacterExtensions
|
|||
|
||||
public static unsafe bool SetHatVisible(this Character a, bool visible)
|
||||
{
|
||||
var current = IsHatVisible(a);
|
||||
var current = a.IsHatVisible();
|
||||
if (current == visible)
|
||||
return false;
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ public static class CharacterExtensions
|
|||
|
||||
public static unsafe bool SetVisorToggled(this Character a, bool toggled)
|
||||
{
|
||||
var current = IsVisorToggled(a);
|
||||
var current = a.IsVisorToggled();
|
||||
if (current == toggled)
|
||||
return false;
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ public static class CharacterExtensions
|
|||
|
||||
public static unsafe bool SetWeaponHidden(this Character a, bool value)
|
||||
{
|
||||
var hidden = IsWeaponHidden(a);
|
||||
var hidden = a.IsWeaponHidden();
|
||||
if (hidden == value)
|
||||
return false;
|
||||
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.State;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer;
|
||||
namespace Glamourer.Util;
|
||||
|
||||
public static unsafe class CustomizeExtensions
|
||||
{
|
||||
|
|
@ -54,4 +56,87 @@ public static unsafe class CustomizeExtensions
|
|||
|
||||
public static string ClanName(this Customize customize)
|
||||
=> 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue