mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2026-02-06 07:54:37 +01:00
208 lines
8.6 KiB
C#
208 lines
8.6 KiB
C#
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
|
using Glamourer.Designs;
|
|
using Glamourer.Interop.Penumbra;
|
|
using Glamourer.State;
|
|
using OtterGui.Services;
|
|
using Penumbra.GameData.Actors;
|
|
using Penumbra.GameData.Enums;
|
|
using Penumbra.GameData.Files.MaterialStructs;
|
|
using Penumbra.GameData.Interop;
|
|
using Penumbra.GameData.Structs;
|
|
|
|
namespace Glamourer.Interop.Material;
|
|
|
|
public sealed unsafe class MaterialManager : IRequiredService, IDisposable
|
|
{
|
|
private readonly PrepareColorSet _event;
|
|
private readonly StateManager _stateManager;
|
|
private readonly PenumbraService _penumbra;
|
|
private readonly ActorManager _actors;
|
|
private readonly Configuration _config;
|
|
|
|
private int _lastSlot;
|
|
|
|
private readonly ThreadLocal<List<MaterialValueIndex>> _deleteList = new(() => []);
|
|
|
|
public MaterialManager(PrepareColorSet prepareColorSet, StateManager stateManager, ActorManager actors, PenumbraService penumbra,
|
|
Configuration config)
|
|
{
|
|
_stateManager = stateManager;
|
|
_actors = actors;
|
|
_penumbra = penumbra;
|
|
_config = config;
|
|
_event = prepareColorSet;
|
|
_event.Subscribe(OnPrepareColorSet, PrepareColorSet.Priority.MaterialManager);
|
|
}
|
|
|
|
public void Dispose()
|
|
=> _event.Unsubscribe(OnPrepareColorSet);
|
|
|
|
private void OnPrepareColorSet(CharacterBase* characterBase, MaterialResourceHandle* material, ref StainIds stain, ref nint ret)
|
|
{
|
|
if (!_config.UseAdvancedDyes)
|
|
return;
|
|
|
|
var actor = _penumbra.GameObjectFromDrawObject(characterBase);
|
|
var validType = FindType(characterBase, actor, out var type);
|
|
var (slotId, materialId) = FindMaterial(characterBase, material);
|
|
|
|
if (!validType
|
|
|| type is not MaterialValueIndex.DrawObjectType.Human && slotId > 0
|
|
|| !actor.Identifier(_actors, out var identifier)
|
|
|| !_stateManager.TryGetValue(identifier, out var state))
|
|
return;
|
|
|
|
var min = MaterialValueIndex.Min(type, slotId, materialId);
|
|
var max = MaterialValueIndex.Max(type, slotId, materialId);
|
|
var values = state.Materials.GetValues(min, max);
|
|
if (values.Length == 0)
|
|
return;
|
|
|
|
if (!PrepareColorSet.TryGetColorTable(material, stain, out var baseColorSet))
|
|
return;
|
|
|
|
var drawData = type switch
|
|
{
|
|
MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, slotId),
|
|
_ => GetTempSlot((Weapon*)characterBase),
|
|
};
|
|
UpdateMaterialValues(state, values, drawData, ref baseColorSet);
|
|
|
|
if (MaterialService.GenerateNewColorTable(baseColorSet, out var texture))
|
|
ret = (nint)texture;
|
|
}
|
|
|
|
/// <summary> Update and apply the glamourer state of an actor according to the application sources when updated by the game. </summary>
|
|
private void UpdateMaterialValues(ActorState state, ReadOnlySpan<(uint Key, MaterialValueState Value)> values, CharacterWeapon drawData,
|
|
ref ColorTable colorTable)
|
|
{
|
|
var deleteList = _deleteList.Value!;
|
|
deleteList.Clear();
|
|
for (var i = 0; i < values.Length; ++i)
|
|
{
|
|
var idx = MaterialValueIndex.FromKey(values[i].Key);
|
|
var materialValue = values[i].Value;
|
|
ref var row = ref colorTable[idx.RowIndex];
|
|
var newGame = new ColorRow(row);
|
|
if (materialValue.EqualGame(newGame, drawData))
|
|
materialValue.Model.Apply(ref row);
|
|
else
|
|
switch (materialValue.Source)
|
|
{
|
|
case StateSource.Pending:
|
|
materialValue.Model.Apply(ref row);
|
|
state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.Manual),
|
|
out _);
|
|
break;
|
|
case StateSource.IpcPending:
|
|
materialValue.Model.Apply(ref row);
|
|
state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, StateSource.IpcManual),
|
|
out _);
|
|
break;
|
|
case StateSource.IpcManual:
|
|
case StateSource.Manual:
|
|
deleteList.Add(idx);
|
|
break;
|
|
case StateSource.Fixed:
|
|
case StateSource.IpcFixed:
|
|
materialValue.Model.Apply(ref row);
|
|
state.Materials.UpdateValue(idx, new MaterialValueState(newGame, materialValue.Model, drawData, materialValue.Source),
|
|
out _);
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach (var idx in deleteList)
|
|
_stateManager.ResetMaterialValue(state, idx, ApplySettings.Game);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the index of a material by searching through a draw objects pointers.
|
|
/// Tries to take shortcuts for consecutive searches like when a character is newly created.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private (byte SlotId, byte MaterialId) FindMaterial(CharacterBase* characterBase, MaterialResourceHandle* material)
|
|
{
|
|
for (var i = _lastSlot; i < characterBase->SlotCount; ++i)
|
|
{
|
|
var idx = MaterialService.MaterialsPerModel * i;
|
|
for (var j = 0; j < MaterialService.MaterialsPerModel; ++j)
|
|
{
|
|
var mat = (nint)characterBase->Materials[idx++];
|
|
if (mat != (nint)material)
|
|
continue;
|
|
|
|
_lastSlot = i;
|
|
return ((byte)i, (byte)j);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < _lastSlot; ++i)
|
|
{
|
|
var idx = MaterialService.MaterialsPerModel * i;
|
|
for (var j = 0; j < MaterialService.MaterialsPerModel; ++j)
|
|
{
|
|
var mat = (nint)characterBase->Materials[idx++];
|
|
if (mat != (nint)material)
|
|
continue;
|
|
|
|
_lastSlot = i;
|
|
return ((byte)i, (byte)j);
|
|
}
|
|
}
|
|
|
|
return (byte.MaxValue, byte.MaxValue);
|
|
}
|
|
|
|
/// <summary> Find the type of the given draw object by checking the actors pointers. </summary>
|
|
private static bool FindType(CharacterBase* characterBase, Actor actor, out MaterialValueIndex.DrawObjectType type)
|
|
{
|
|
type = MaterialValueIndex.DrawObjectType.Human;
|
|
if (!actor.Valid)
|
|
return false;
|
|
|
|
if (actor.Model.AsCharacterBase == characterBase)
|
|
return true;
|
|
|
|
if (!actor.AsObject->IsCharacter())
|
|
return false;
|
|
|
|
if (actor.AsCharacter->DrawData.WeaponData[0].DrawObject == characterBase)
|
|
{
|
|
type = MaterialValueIndex.DrawObjectType.Mainhand;
|
|
return true;
|
|
}
|
|
|
|
if (actor.AsCharacter->DrawData.WeaponData[1].DrawObject == characterBase)
|
|
{
|
|
type = MaterialValueIndex.DrawObjectType.Offhand;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary> We need to get the temporary set, variant and stain that is currently being set if it is available. </summary>
|
|
private static CharacterWeapon GetTempSlot(Human* human, byte slotId)
|
|
{
|
|
if (human->ChangedEquipData == null)
|
|
return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0);
|
|
|
|
return ((CharacterArmor*)human->ChangedEquipData + slotId * 3)->ToWeapon(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// We need to get the temporary set, variant and stain that is currently being set if it is available.
|
|
/// Weapons do not change in skeleton id without being reconstructed, so this is not changeable data.
|
|
/// </summary>
|
|
private static CharacterWeapon GetTempSlot(Weapon* weapon)
|
|
{
|
|
// TODO: Fix offset
|
|
var changedData = *(void**)((byte*)weapon + 0x918);
|
|
if (changedData == null)
|
|
return new CharacterWeapon(weapon->ModelSetId, weapon->SecondaryId, (Variant)weapon->Variant, StainIds.FromWeapon(*weapon));
|
|
|
|
return new CharacterWeapon(weapon->ModelSetId, *(SecondaryId*)changedData, ((Variant*)changedData)[2], new StainIds(((StainId*)changedData)[3], ((StainId*)changedData)[4]));
|
|
}
|
|
}
|