mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Optimizations
This commit is contained in:
parent
805615d9f4
commit
5204bb723d
8 changed files with 424 additions and 301 deletions
|
|
@ -1,11 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
|
@ -14,193 +15,258 @@ namespace Dalamud.Game.Inventory;
|
|||
/// This class provides events for the players in-game inventory.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal class GameInventory : IDisposable, IServiceType, IGameInventory
|
||||
{
|
||||
private static readonly ModuleLog Log = new("GameInventory");
|
||||
|
||||
private readonly List<IGameInventory.GameInventoryEventArgs> changelog = new();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
private readonly Dictionary<GameInventoryType, Dictionary<int, InventoryItem>> inventoryCache;
|
||||
private readonly GameInventoryType[] inventoryTypes;
|
||||
private readonly GameInventoryItem[][] inventoryItems;
|
||||
private readonly unsafe GameInventoryItem*[] inventoryItemsPointers;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private GameInventory()
|
||||
private unsafe GameInventory()
|
||||
{
|
||||
this.inventoryCache = new Dictionary<GameInventoryType, Dictionary<int, InventoryItem>>();
|
||||
this.inventoryTypes = Enum.GetValues<GameInventoryType>();
|
||||
|
||||
foreach (var inventoryType in Enum.GetValues<GameInventoryType>())
|
||||
// Using GC.AllocateArray(pinned: true), so that Unsafe.AsPointer(ref array[0]) does not fail.
|
||||
this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][];
|
||||
this.inventoryItemsPointers = new GameInventoryItem*[this.inventoryTypes.Length];
|
||||
for (var i = 0; i < this.inventoryItems.Length; i++)
|
||||
{
|
||||
this.inventoryCache.Add(inventoryType, new Dictionary<int, InventoryItem>());
|
||||
this.inventoryItems[i] = GC.AllocateArray<GameInventoryItem>(1, true);
|
||||
this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref this.inventoryItems[i][0]);
|
||||
}
|
||||
|
||||
|
||||
this.framework.Update += this.OnFrameworkUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemMovedDelegate? ItemMoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemRemovedDelegate? ItemRemoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemAddedDelegate? ItemAdded;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemChangedDelegate? ItemChanged;
|
||||
public event IGameInventory.InventoryChangeDelegate? InventoryChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.framework.Update -= this.OnFrameworkUpdate;
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework1)
|
||||
{
|
||||
// If no one is listening for event's then we don't need to track anything.
|
||||
if (!this.AnyListeners()) return;
|
||||
|
||||
var performanceMonitor = Stopwatch.StartNew();
|
||||
|
||||
var changelog = new List<GameInventoryItemChangelog>();
|
||||
|
||||
foreach (var (inventoryType, cachedInventoryItems) in this.inventoryCache)
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Span{T}"/> view of <see cref="InventoryItem"/>s, wrapped as <see cref="GameInventoryItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The inventory type.</param>
|
||||
/// <returns>The span.</returns>
|
||||
private static unsafe Span<GameInventoryItem> GetItemsForInventory(GameInventoryType type)
|
||||
{
|
||||
var inventoryManager = InventoryManager.Instance();
|
||||
if (inventoryManager is null) return default;
|
||||
|
||||
var inventory = inventoryManager->GetInventoryContainer((InventoryType)type);
|
||||
if (inventory is null) return default;
|
||||
|
||||
return new(inventory->Items, (int)inventory->Size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for the first index of <paramref name="type"/>, or the supposed position one should be if none could be found.
|
||||
/// </summary>
|
||||
/// <param name="span">The span to look in.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>The index.</returns>
|
||||
private static int FindTypeIndex(Span<IGameInventory.GameInventoryEventArgs> span, GameInventoryEvent type)
|
||||
{
|
||||
// Use linear lookup if span size is small enough
|
||||
if (span.Length < 64)
|
||||
{
|
||||
foreach (var item in this.GetItemsForInventory(inventoryType))
|
||||
var i = 0;
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (cachedInventoryItems.TryGetValue(item.Slot, out var inventoryItem))
|
||||
if (type <= span[i].Type)
|
||||
break;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
var lo = 0;
|
||||
var hi = span.Length - 1;
|
||||
while (lo <= hi)
|
||||
{
|
||||
var i = lo + ((hi - lo) >> 1);
|
||||
var type2 = span[i].Type;
|
||||
if (type == type2)
|
||||
return i;
|
||||
if (type < type2)
|
||||
lo = i + 1;
|
||||
else
|
||||
hi = i - 1;
|
||||
}
|
||||
|
||||
return lo;
|
||||
}
|
||||
|
||||
private unsafe void OnFrameworkUpdate(IFramework framework1)
|
||||
{
|
||||
// TODO: Uncomment this
|
||||
// // If no one is listening for event's then we don't need to track anything.
|
||||
// if (this.InventoryChanged is null) return;
|
||||
|
||||
for (var i = 0; i < this.inventoryTypes.Length;)
|
||||
{
|
||||
var oldItemsArray = this.inventoryItems[i];
|
||||
var oldItemsLength = oldItemsArray.Length;
|
||||
var oldItemsPointer = this.inventoryItemsPointers[i];
|
||||
|
||||
var resizeRequired = 0;
|
||||
foreach (ref var newItem in GetItemsForInventory(this.inventoryTypes[i]))
|
||||
{
|
||||
var slot = newItem.InternalItem.Slot;
|
||||
if (slot >= oldItemsLength)
|
||||
{
|
||||
// Gained Item
|
||||
// If the item we have cached has an item id of 0, then we expect it to be an empty slot.
|
||||
// However, if the item we see in the game data has an item id that is not 0, then it now has an item.
|
||||
if (inventoryItem.ItemID is 0 && item.ItemID is not 0)
|
||||
{
|
||||
var gameInventoryItem = new GameInventoryItem(item);
|
||||
this.ItemAdded?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem);
|
||||
changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Added, gameInventoryItem));
|
||||
|
||||
Log.Verbose($"New Item Added to {inventoryType}: {item.ItemID}");
|
||||
this.inventoryCache[inventoryType][item.Slot] = item;
|
||||
}
|
||||
|
||||
// Removed Item
|
||||
// If the item we have cached has an item id of not 0, then we expect it to have an item.
|
||||
// However, if the item we see in the game data has an item id that is 0, then it was removed from this inventory.
|
||||
if (inventoryItem.ItemID is not 0 && item.ItemID is 0)
|
||||
{
|
||||
var gameInventoryItem = new GameInventoryItem(inventoryItem);
|
||||
this.ItemRemoved?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem);
|
||||
changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Removed, gameInventoryItem));
|
||||
|
||||
Log.Verbose($"Item Removed from {inventoryType}: {inventoryItem.ItemID}");
|
||||
this.inventoryCache[inventoryType][item.Slot] = item;
|
||||
}
|
||||
|
||||
// Changed Item
|
||||
// If the item we have cached, does not match the item that we see in the game data
|
||||
// AND if neither item is empty, then the item has been changed.
|
||||
if (this.IsItemChanged(inventoryItem, item) && inventoryItem.ItemID is not 0 && item.ItemID is not 0)
|
||||
{
|
||||
var gameInventoryItem = new GameInventoryItem(inventoryItem);
|
||||
this.ItemChanged?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem);
|
||||
|
||||
Log.Verbose($"Item Changed {inventoryType}: {inventoryItem.ItemID}");
|
||||
this.inventoryCache[inventoryType][item.Slot] = item;
|
||||
}
|
||||
resizeRequired = Math.Max(resizeRequired, slot + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We already checked the range above. Go raw.
|
||||
ref var oldItem = ref oldItemsPointer[slot];
|
||||
|
||||
if (oldItem.IsEmpty)
|
||||
{
|
||||
if (newItem.IsEmpty)
|
||||
continue;
|
||||
this.changelog.Add(new(GameInventoryEvent.Added, default, newItem));
|
||||
}
|
||||
else
|
||||
{
|
||||
cachedInventoryItems.Add(item.Slot, item);
|
||||
if (newItem.IsEmpty)
|
||||
this.changelog.Add(new(GameInventoryEvent.Removed, oldItem, default));
|
||||
else if (!oldItem.Equals(newItem))
|
||||
this.changelog.Add(new(GameInventoryEvent.Changed, oldItem, newItem));
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}");
|
||||
oldItem = newItem;
|
||||
}
|
||||
|
||||
// Did the max slot number get changed?
|
||||
if (resizeRequired != 0)
|
||||
{
|
||||
// Resize our buffer, and then try again.
|
||||
var oldItemsExpanded = GC.AllocateArray<GameInventoryItem>(resizeRequired, true);
|
||||
oldItemsArray.CopyTo(oldItemsExpanded, 0);
|
||||
this.inventoryItems[i] = oldItemsExpanded;
|
||||
this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref oldItemsExpanded[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Proceed to the next inventory.
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve changelog for item moved
|
||||
// Group all changelogs that have the same itemId, and check if there was an add and a remove event for that item.
|
||||
foreach (var itemGroup in changelog.GroupBy(log => log.Item.ItemId))
|
||||
|
||||
// Was there any change? If not, stop further processing.
|
||||
if (this.changelog.Count == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var hasAdd = false;
|
||||
var hasRemove = false;
|
||||
|
||||
foreach (var log in itemGroup)
|
||||
// From this point, the size of changelog shall not change.
|
||||
var span = CollectionsMarshal.AsSpan(this.changelog);
|
||||
|
||||
span.Sort((a, b) => a.Type.CompareTo(b.Type));
|
||||
var addedFrom = FindTypeIndex(span, GameInventoryEvent.Added);
|
||||
var removedFrom = FindTypeIndex(span, GameInventoryEvent.Removed);
|
||||
var changedFrom = FindTypeIndex(span, GameInventoryEvent.Changed);
|
||||
|
||||
// Resolve changelog for item moved, from 1 added + 1 removed
|
||||
for (var iAdded = addedFrom; iAdded < removedFrom; iAdded++)
|
||||
{
|
||||
switch (log.State)
|
||||
ref var added = ref span[iAdded];
|
||||
for (var iRemoved = removedFrom; iRemoved < changedFrom; iRemoved++)
|
||||
{
|
||||
case GameInventoryChangelogState.Added:
|
||||
hasAdd = true;
|
||||
ref var removed = ref span[iRemoved];
|
||||
if (added.Target.ItemId == removed.Source.ItemId)
|
||||
{
|
||||
span[iAdded] = new(GameInventoryEvent.Moved, span[iRemoved].Source, span[iAdded].Target);
|
||||
span[iRemoved] = default;
|
||||
Log.Verbose($"[{iAdded}] Interpreting instead as: {span[iAdded]}");
|
||||
Log.Verbose($"[{iRemoved}] Discarding");
|
||||
break;
|
||||
|
||||
case GameInventoryChangelogState.Removed:
|
||||
hasRemove = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var itemMoved = hasAdd && hasRemove;
|
||||
if (itemMoved)
|
||||
// Resolve changelog for item moved, from 2 changeds
|
||||
for (var i = changedFrom; i < this.changelog.Count; i++)
|
||||
{
|
||||
var added = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Added);
|
||||
var removed = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Removed);
|
||||
if (added is null || removed is null) continue;
|
||||
|
||||
this.ItemMoved?.Invoke(removed.Item.ContainerType, removed.Item.InventorySlot, added.Item.ContainerType, added.Item.InventorySlot, added.Item);
|
||||
|
||||
Log.Verbose($"Item Moved {removed.Item.ContainerType}:{removed.Item.InventorySlot} -> {added.Item.ContainerType}:{added.Item.InventorySlot}: {added.Item.ItemId}");
|
||||
if (span[i].IsEmpty)
|
||||
continue;
|
||||
|
||||
ref var e1 = ref span[i];
|
||||
for (var j = i + 1; j < this.changelog.Count; j++)
|
||||
{
|
||||
ref var e2 = ref span[j];
|
||||
if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId)
|
||||
{
|
||||
if (e1.Target.IsEmpty)
|
||||
{
|
||||
// e1 got moved to e2
|
||||
e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target);
|
||||
e2 = default;
|
||||
Log.Verbose($"[{i}] Interpreting instead as: {e1}");
|
||||
Log.Verbose($"[{j}] Discarding");
|
||||
}
|
||||
else if (e2.Target.IsEmpty)
|
||||
{
|
||||
// e2 got moved to e1
|
||||
e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target);
|
||||
e2 = default;
|
||||
Log.Verbose($"[{i}] Interpreting instead as: {e1}");
|
||||
Log.Verbose($"[{j}] Discarding");
|
||||
}
|
||||
else
|
||||
{
|
||||
// e1 and e2 got swapped
|
||||
(e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target),
|
||||
new(GameInventoryEvent.Moved, e2.Target, e1.Target));
|
||||
|
||||
Log.Verbose($"[{i}] Interpreting instead as: {e1}");
|
||||
Log.Verbose($"[{j}] Interpreting instead as: {e2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out the emptied out entries.
|
||||
// We do not care about the order of items in the changelog anymore.
|
||||
for (var i = 0; i < span.Length;)
|
||||
{
|
||||
if (span[i].IsEmpty)
|
||||
{
|
||||
span[i] = span[^1];
|
||||
span = span[..^1];
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Actually broadcast the changes to subscribers.
|
||||
if (!span.IsEmpty)
|
||||
this.InventoryChanged?.Invoke(span);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.changelog.Clear();
|
||||
}
|
||||
|
||||
var elapsed = performanceMonitor.Elapsed;
|
||||
|
||||
Log.Verbose($"Processing Time: {elapsed.Ticks}ticks :: {elapsed.TotalMilliseconds}ms");
|
||||
}
|
||||
|
||||
private bool AnyListeners()
|
||||
{
|
||||
if (this.ItemMoved is not null) return true;
|
||||
if (this.ItemRemoved is not null) return true;
|
||||
if (this.ItemAdded is not null) return true;
|
||||
if (this.ItemChanged is not null) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe ReadOnlySpan<InventoryItem> GetItemsForInventory(GameInventoryType type)
|
||||
{
|
||||
var inventoryManager = InventoryManager.Instance();
|
||||
if (inventoryManager is null) return ReadOnlySpan<InventoryItem>.Empty;
|
||||
|
||||
var inventory = inventoryManager->GetInventoryContainer((InventoryType)type);
|
||||
if (inventory is null) return ReadOnlySpan<InventoryItem>.Empty;
|
||||
|
||||
return new ReadOnlySpan<InventoryItem>(inventory->Items, (int)inventory->Size);
|
||||
}
|
||||
|
||||
private bool IsItemChanged(InventoryItem a, InventoryItem b)
|
||||
{
|
||||
if (a.Container != b.Container) return true; // Shouldn't be possible, but shouldn't hurt.
|
||||
if (a.Slot != b.Slot) return true; // Shouldn't be possible, but shouldn't hurt.
|
||||
if (a.ItemID != b.ItemID) return true;
|
||||
if (a.Quantity != b.Quantity) return true;
|
||||
if (a.Spiritbond != b.Spiritbond) return true;
|
||||
if (a.Condition != b.Condition) return true;
|
||||
if (a.Flags != b.Flags) return true;
|
||||
if (a.CrafterContentID != b.CrafterContentID) return true;
|
||||
if (this.IsMateriaChanged(a, b)) return true;
|
||||
if (this.IsMateriaGradeChanged(a, b)) return true;
|
||||
if (a.Stain != b.Stain) return true;
|
||||
if (a.GlamourID != b.GlamourID) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b)
|
||||
=> new ReadOnlySpan<ushort>(a.Materia, 5) != new ReadOnlySpan<ushort>(b.Materia, 5);
|
||||
|
||||
private unsafe bool IsMateriaGradeChanged(InventoryItem a, InventoryItem b)
|
||||
=> new ReadOnlySpan<byte>(a.MateriaGrade, 5) != new ReadOnlySpan<byte>(b.MateriaGrade, 5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -222,47 +288,19 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven
|
|||
/// </summary>
|
||||
public GameInventoryPluginScoped()
|
||||
{
|
||||
this.gameInventoryService.ItemMoved += this.OnItemMovedForward;
|
||||
this.gameInventoryService.ItemRemoved += this.OnItemRemovedForward;
|
||||
this.gameInventoryService.ItemAdded += this.OnItemAddedForward;
|
||||
this.gameInventoryService.ItemChanged += this.OnItemChangedForward;
|
||||
this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemMovedDelegate? ItemMoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemRemovedDelegate? ItemRemoved;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemAddedDelegate? ItemAdded;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IGameInventory.OnItemChangedDelegate? ItemChanged;
|
||||
public event IGameInventory.InventoryChangeDelegate? InventoryChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.gameInventoryService.ItemMoved -= this.OnItemMovedForward;
|
||||
this.gameInventoryService.ItemRemoved -= this.OnItemRemovedForward;
|
||||
this.gameInventoryService.ItemAdded -= this.OnItemAddedForward;
|
||||
this.gameInventoryService.ItemChanged -= this.OnItemChangedForward;
|
||||
|
||||
this.ItemMoved = null;
|
||||
this.ItemRemoved = null;
|
||||
this.ItemAdded = null;
|
||||
this.ItemChanged = null;
|
||||
this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward;
|
||||
this.InventoryChanged = null;
|
||||
}
|
||||
|
||||
private void OnItemMovedForward(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item)
|
||||
=> this.ItemMoved?.Invoke(source, sourceSlot, destination, destinationSlot, item);
|
||||
|
||||
private void OnItemRemovedForward(GameInventoryType source, uint sourceSlot, GameInventoryItem item)
|
||||
=> this.ItemRemoved?.Invoke(source, sourceSlot, item);
|
||||
|
||||
private void OnItemAddedForward(GameInventoryType destination, uint destinationSlot, GameInventoryItem item)
|
||||
=> this.ItemAdded?.Invoke(destination, destinationSlot, item);
|
||||
|
||||
private void OnItemChangedForward(GameInventoryType inventory, uint slot, GameInventoryItem item)
|
||||
=> this.ItemChanged?.Invoke(inventory, slot, item);
|
||||
private void OnInventoryChangedForward(ReadOnlySpan<IGameInventory.GameInventoryEventArgs> events)
|
||||
=> this.InventoryChanged?.Invoke(events);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing an inventory item change event.
|
||||
/// </summary>
|
||||
internal class GameInventoryItemChangelog
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameInventoryItemChangelog"/> class.
|
||||
/// </summary>
|
||||
/// <param name="state">Item state.</param>
|
||||
/// <param name="item">Item.</param>
|
||||
internal GameInventoryItemChangelog(GameInventoryChangelogState state, GameInventoryItem item)
|
||||
{
|
||||
this.State = state;
|
||||
this.Item = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state of this changelog event.
|
||||
/// </summary>
|
||||
internal GameInventoryChangelogState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item for this changelog event.
|
||||
/// </summary>
|
||||
internal GameInventoryItem Item { get; }
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a item's changelog state.
|
||||
/// </summary>
|
||||
internal enum GameInventoryChangelogState
|
||||
{
|
||||
/// <summary>
|
||||
/// Item was added to an inventory.
|
||||
/// </summary>
|
||||
Added,
|
||||
|
||||
/// <summary>
|
||||
/// Item was removed from an inventory.
|
||||
/// </summary>
|
||||
Removed,
|
||||
}
|
||||
34
Dalamud/Game/Inventory/GameInventoryEvent.cs
Normal file
34
Dalamud/Game/Inventory/GameInventoryEvent.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
namespace Dalamud.Game.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a item's changelog state.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum GameInventoryEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// A value indicating that there was no event.<br />
|
||||
/// You should not see this value, unless you explicitly used it yourself, or APIs using this enum say otherwise.
|
||||
/// </summary>
|
||||
Empty = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Item was added to an inventory.
|
||||
/// </summary>
|
||||
Added = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Item was removed from an inventory.
|
||||
/// </summary>
|
||||
Removed = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Properties are changed for an item in an inventory.
|
||||
/// </summary>
|
||||
Changed = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Item has been moved, possibly across different inventories.
|
||||
/// </summary>
|
||||
Moved = 1 << 3,
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
|
|
@ -7,92 +9,160 @@ namespace Dalamud.Game.Inventory;
|
|||
/// <summary>
|
||||
/// Dalamud wrapper around a ClientStructs InventoryItem.
|
||||
/// </summary>
|
||||
public unsafe class GameInventoryItem
|
||||
[StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)]
|
||||
public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
||||
{
|
||||
private InventoryItem internalItem;
|
||||
/// <summary>
|
||||
/// An empty instance of <see cref="GameInventoryItem"/>.
|
||||
/// </summary>
|
||||
internal static readonly GameInventoryItem Empty = default;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameInventoryItem"/> class.
|
||||
/// The actual data.
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
internal readonly InventoryItem InternalItem;
|
||||
|
||||
private const int StructSizeInBytes = 0x38;
|
||||
|
||||
/// <summary>
|
||||
/// The view of the backing data, in <see cref="ulong"/>.
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
private fixed ulong dataUInt64[StructSizeInBytes / 0x8];
|
||||
|
||||
static GameInventoryItem()
|
||||
{
|
||||
Debug.Assert(
|
||||
sizeof(InventoryItem) == StructSizeInBytes,
|
||||
$"Definition of {nameof(InventoryItem)} has been changed. " +
|
||||
$"Update {nameof(StructSizeInBytes)} to {sizeof(InventoryItem)} to accommodate for the size change.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameInventoryItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="item">Inventory item to wrap.</param>
|
||||
internal GameInventoryItem(InventoryItem item)
|
||||
{
|
||||
this.internalItem = item;
|
||||
}
|
||||
internal GameInventoryItem(InventoryItem item) => this.InternalItem = item;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the this <see cref="GameInventoryItem"/> is empty.
|
||||
/// </summary>
|
||||
public bool IsEmpty => this.InternalItem.ItemID == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the container inventory type.
|
||||
/// </summary>
|
||||
public GameInventoryType ContainerType => (GameInventoryType)this.internalItem.Container;
|
||||
public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inventory slot index this item is in.
|
||||
/// </summary>
|
||||
public uint InventorySlot => (uint)this.internalItem.Slot;
|
||||
public uint InventorySlot => (uint)this.InternalItem.Slot;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item id.
|
||||
/// </summary>
|
||||
public uint ItemId => this.internalItem.ItemID;
|
||||
public uint ItemId => this.InternalItem.ItemID;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quantity of items in this item stack.
|
||||
/// </summary>
|
||||
public uint Quantity => this.internalItem.Quantity;
|
||||
public uint Quantity => this.InternalItem.Quantity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the spiritbond of this item.
|
||||
/// </summary>
|
||||
public uint Spiritbond => this.internalItem.Spiritbond;
|
||||
public uint Spiritbond => this.InternalItem.Spiritbond;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repair condition of this item.
|
||||
/// </summary>
|
||||
public uint Condition => this.internalItem.Condition;
|
||||
public uint Condition => this.InternalItem.Condition;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item is High Quality.
|
||||
/// </summary>
|
||||
public bool IsHq => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.HQ);
|
||||
public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HQ) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item has a company crest applied.
|
||||
/// </summary>
|
||||
public bool IsCompanyCrestApplied => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied);
|
||||
|
||||
public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the item is a relic.
|
||||
/// </summary>
|
||||
public bool IsRelic => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Relic);
|
||||
public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the is a collectable.
|
||||
/// </summary>
|
||||
public bool IsCollectable => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Collectable);
|
||||
public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of materia types.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ushort> Materia => new(Unsafe.AsPointer(ref this.internalItem.Materia[0]), 5);
|
||||
public ReadOnlySpan<ushort> Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of materia grades.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ushort> MateriaGrade => new(Unsafe.AsPointer(ref this.internalItem.MateriaGrade[0]), 5);
|
||||
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color used for this item.
|
||||
/// </summary>
|
||||
public byte Stain => this.internalItem.Stain;
|
||||
public byte Stain => this.InternalItem.Stain;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glamour id for this item.
|
||||
/// </summary>
|
||||
public uint GlmaourId => this.internalItem.GlamourID;
|
||||
public uint GlmaourId => this.InternalItem.GlamourID;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items crafter's content id.
|
||||
/// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now.
|
||||
/// </summary>
|
||||
internal ulong CrafterContentId => this.internalItem.CrafterContentID;
|
||||
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
|
||||
|
||||
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
|
||||
|
||||
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);
|
||||
|
||||
/// <inheritdoc/>
|
||||
readonly bool IEquatable<GameInventoryItem>.Equals(GameInventoryItem other) => this.Equals(other);
|
||||
|
||||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns><c>true</c> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <c>false</c>.</returns>
|
||||
public readonly bool Equals(in GameInventoryItem other)
|
||||
{
|
||||
for (var i = 0; i < StructSizeInBytes / 8; i++)
|
||||
{
|
||||
if (this.dataUInt64[i] != other.dataUInt64[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.Equals(object?)" />
|
||||
public override bool Equals(object obj) => obj is GameInventoryItem gii && this.Equals(gii);
|
||||
|
||||
/// <inheritdoc cref="object.GetHashCode" />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var k = 0x5a8447b91aff51b4UL;
|
||||
for (var i = 0; i < StructSizeInBytes / 8; i++)
|
||||
k ^= this.dataUInt64[i];
|
||||
return unchecked((int)(k ^ (k >> 32)));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="object.ToString"/>
|
||||
public override string ToString() =>
|
||||
this.IsEmpty
|
||||
? "<Empty>"
|
||||
: $"Item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/// <summary>
|
||||
/// Enum representing various player inventories.
|
||||
/// </summary>
|
||||
public enum GameInventoryType : uint
|
||||
public enum GameInventoryType : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// First panel of main player inventory.
|
||||
|
|
@ -348,4 +348,9 @@ public enum GameInventoryType : uint
|
|||
/// Eighth panel of housing interior storeroom inventory.
|
||||
/// </summary>
|
||||
HousingInteriorStoreroom8 = 27008,
|
||||
|
||||
/// <summary>
|
||||
/// An invalid value.
|
||||
/// </summary>
|
||||
Invalid = ushort.MaxValue,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,62 +8,83 @@ namespace Dalamud.Plugin.Services;
|
|||
public interface IGameInventory
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate function for when an item is moved from one inventory to the next.
|
||||
/// Delegate function to be called when inventories have been changed.
|
||||
/// </summary>
|
||||
/// <param name="source">Which inventory the item was moved from.</param>
|
||||
/// <param name="sourceSlot">The slot this item was moved from.</param>
|
||||
/// <param name="destination">Which inventory the item was moved to.</param>
|
||||
/// <param name="destinationSlot">The slot this item was moved to.</param>
|
||||
/// <param name="item">The item moved.</param>
|
||||
public delegate void OnItemMovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate function for when an item is removed from an inventory.
|
||||
/// </summary>
|
||||
/// <param name="source">Which inventory the item was removed from.</param>
|
||||
/// <param name="sourceSlot">The slot this item was removed from.</param>
|
||||
/// <param name="item">The item removed.</param>
|
||||
public delegate void OnItemRemovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate function for when an item is added to an inventory.
|
||||
/// </summary>
|
||||
/// <param name="destination">Which inventory the item was added to.</param>
|
||||
/// <param name="destinationSlot">The slot this item was added to.</param>
|
||||
/// <param name="item">The item added.</param>
|
||||
public delegate void OnItemAddedDelegate(GameInventoryType destination, uint destinationSlot, GameInventoryItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate function for when an items properties are changed.
|
||||
/// </summary>
|
||||
/// <param name="inventory">Which inventory the item that was changed is in.</param>
|
||||
/// <param name="slot">The slot the item that was changed is in.</param>
|
||||
/// <param name="item">The item changed.</param>
|
||||
public delegate void OnItemChangedDelegate(GameInventoryType inventory, uint slot, GameInventoryItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is moved from one inventory to another.
|
||||
/// </summary>
|
||||
public event OnItemMovedDelegate ItemMoved;
|
||||
/// <param name="events">The events.</param>
|
||||
public delegate void InventoryChangeDelegate(ReadOnlySpan<GameInventoryEventArgs> events);
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is removed from one inventory.
|
||||
/// Event that is fired when the inventory has been changed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event will also be fired when an item is moved from one inventory to another.
|
||||
/// </remarks>
|
||||
public event OnItemRemovedDelegate ItemRemoved;
|
||||
public event InventoryChangeDelegate InventoryChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an item is added to one inventory.
|
||||
/// Argument for <see cref="InventoryChangeDelegate"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event will also be fired when an item is moved from one inventory to another.
|
||||
/// </remarks>
|
||||
public event OnItemAddedDelegate ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when an items properties are changed.
|
||||
/// </summary>
|
||||
public event OnItemChangedDelegate ItemChanged;
|
||||
public readonly struct GameInventoryEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the event.
|
||||
/// </summary>
|
||||
public readonly GameInventoryEvent Type;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the item in the source inventory.<br />
|
||||
/// Relevant if <see cref="Type"/> is <see cref="GameInventoryEvent.Moved"/>, <see cref="GameInventoryEvent.Changed"/>, or <see cref="GameInventoryEvent.Removed"/>.
|
||||
/// </summary>
|
||||
public readonly GameInventoryItem Source;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the item in the target inventory<br />
|
||||
/// Relevant if <see cref="Type"/> is <see cref="GameInventoryEvent.Moved"/>, <see cref="GameInventoryEvent.Changed"/>, or <see cref="GameInventoryEvent.Added"/>.
|
||||
/// </summary>
|
||||
public readonly GameInventoryItem Target;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameInventoryEventArgs"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the event.</param>
|
||||
/// <param name="source">The source inventory item.</param>
|
||||
/// <param name="target">The target inventory item.</param>
|
||||
public GameInventoryEventArgs(GameInventoryEvent type, GameInventoryItem source, GameInventoryItem target)
|
||||
{
|
||||
this.Type = type;
|
||||
this.Source = source;
|
||||
this.Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance of <see cref="GameInventoryEventArgs"/> contains no information.
|
||||
/// </summary>
|
||||
public bool IsEmpty => this.Type == GameInventoryEvent.Empty;
|
||||
|
||||
// TODO: are the following two aliases useful?
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the source inventory.<br />
|
||||
/// Relevant for <see cref="GameInventoryEvent.Moved"/> and <see cref="GameInventoryEvent.Removed"/>.
|
||||
/// </summary>
|
||||
public GameInventoryType SourceType => this.Source.ContainerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the target inventory.<br />
|
||||
/// Relevant for <see cref="GameInventoryEvent.Moved"/>, <see cref="GameInventoryEvent.Changed"/>, and
|
||||
/// <see cref="GameInventoryEvent.Added"/>.
|
||||
/// </summary>
|
||||
public GameInventoryType TargetType => this.Target.ContainerType;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.Type switch
|
||||
{
|
||||
GameInventoryEvent.Empty =>
|
||||
$"<{this.Type}>",
|
||||
GameInventoryEvent.Added =>
|
||||
$"<{this.Type}> ({this.Target})",
|
||||
GameInventoryEvent.Removed =>
|
||||
$"<{this.Type}> ({this.Source})",
|
||||
GameInventoryEvent.Changed or GameInventoryEvent.Moved =>
|
||||
$"<{this.Type}> ({this.Source}) to ({this.Target})",
|
||||
_ => $"<Type={this.Type}> {this.Source} => {this.Target}",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit cc668752416a8459a3c23345c51277e359803de8
|
||||
Subproject commit 090e0c244df668454616026188c1363e5d25a1bc
|
||||
Loading…
Add table
Add a link
Reference in a new issue