MosswartMassacre/MosswartMassacre/EquipmentCantripStateTracker.cs

419 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using Decal.Filters;
namespace MosswartMassacre
{
internal sealed class EquipmentCantripStateTracker : IDisposable
{
private readonly IPluginLogger _logger;
private readonly Timer _debounceTimer;
private readonly Dictionary<int, DateTime> _lastManaUpdateByItemId = new Dictionary<int, DateTime>();
internal EquipmentCantripStateTracker(IPluginLogger logger)
{
_logger = logger;
_debounceTimer = new Timer();
_debounceTimer.Interval = 250;
_debounceTimer.Tick += DebounceTimer_Tick;
}
internal void Initialize()
{
try
{
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
RememberManaSnapshotTime(wo);
}
RequestRefresh();
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Initialize error: {ex.Message}");
}
}
internal void Cleanup()
{
_debounceTimer.Stop();
_lastManaUpdateByItemId.Clear();
}
internal void OnCreateObject(object sender, CreateObjectEventArgs e)
{
try
{
if (!IsPotentiallyRelevant(e.New))
return;
RememberManaSnapshotTime(e.New);
RequestRefresh();
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Create error: {ex.Message}");
}
}
internal void OnReleaseObject(object sender, ReleaseObjectEventArgs e)
{
try
{
if (_lastManaUpdateByItemId.Remove(e.Released.Id) || IsPotentiallyRelevant(e.Released))
{
RequestRefresh();
}
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Release error: {ex.Message}");
}
}
internal void OnChangeObject(object sender, ChangeObjectEventArgs e)
{
try
{
if (e.Change == WorldChangeType.IdentReceived || e.Change == WorldChangeType.ManaChange)
{
RememberManaSnapshotTime(e.Changed);
}
if (IsPotentiallyRelevant(e.Changed) || _lastManaUpdateByItemId.ContainsKey(e.Changed.Id))
{
RequestRefresh();
}
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Change error: {ex.Message}");
}
}
internal void OnChangeEnchantments(object sender, ChangeEnchantmentsEventArgs e)
{
RequestRefresh();
}
private void DebounceTimer_Tick(object sender, EventArgs e)
{
_debounceTimer.Stop();
SendSnapshot();
}
private void RequestRefresh()
{
_debounceTimer.Stop();
_debounceTimer.Start();
}
private void SendSnapshot()
{
try
{
if (!PluginCore.WebSocketEnabled)
return;
var items = BuildSnapshotItems();
_ = WebSocket.SendEquipmentCantripStateAsync(items.ToArray());
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Send error: {ex.Message}");
}
}
private List<object> BuildSnapshotItems()
{
var snapshotItems = new List<object>();
FileService fileService = null;
try
{
fileService = CoreManager.Current.Filter<FileService>();
if (fileService?.SpellTable == null)
return snapshotItems;
var activeCharacterSpells = GetActiveCharacterItemSpellData(fileService);
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
{
if (!ShouldDisplayInManaPanel(wo))
continue;
var snapshot = BuildItemSnapshot(wo, fileService, activeCharacterSpells);
if (snapshot != null)
{
snapshotItems.Add(snapshot);
}
}
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Snapshot build error: {ex.Message}");
}
return snapshotItems;
}
private List<Spell> GetActiveCharacterItemSpellData(FileService fileService)
{
var activeSpells = new List<Spell>();
var cf = CoreManager.Current.CharacterFilter;
try
{
int enchantmentCount = cf.Underlying.EnchantmentCount;
for (int i = 0; i < enchantmentCount; i++)
{
Decal.Interop.Filters.Enchantment enchantment = null;
try
{
enchantment = cf.Underlying.get_Enchantment(i);
if (enchantment == null || enchantment.TimeRemaining > 0)
continue;
var spell = fileService.SpellTable.GetById(enchantment.SpellID);
if (spell != null)
{
activeSpells.Add(spell);
}
}
finally
{
if (enchantment != null)
{
System.Runtime.InteropServices.Marshal.ReleaseComObject(enchantment);
}
}
}
}
catch (Exception ex)
{
_logger?.Log($"[EquipCantrip] Active enchantment read error: {ex.Message}");
}
return activeSpells;
}
private object BuildItemSnapshot(WorldObject wo, FileService fileService, List<Spell> activeCharacterSpells)
{
var itemState = RecalculateState(wo, fileService, activeCharacterSpells);
if (itemState == EquipmentCantripItemState.NotActivatable)
return null;
int maximumMana = wo.Values(LongValueKey.MaximumMana, 0);
int currentMana = itemState == EquipmentCantripItemState.Unknown ? 0 : CalculateCurrentMana(wo, itemState);
double? manaTimeRemainingSeconds = null;
if (itemState == EquipmentCantripItemState.Active)
{
var remaining = CalculateManaTimeRemaining(wo, currentMana);
manaTimeRemainingSeconds = remaining.TotalSeconds;
}
return new
{
item_id = wo.Id,
state = ToWireState(itemState),
current_mana = itemState == EquipmentCantripItemState.Unknown ? (int?)null : currentMana,
max_mana = itemState == EquipmentCantripItemState.Unknown ? (int?)null : maximumMana,
mana_time_remaining_seconds = manaTimeRemainingSeconds
};
}
private EquipmentCantripItemState RecalculateState(WorldObject wo, FileService fileService, List<Spell> activeCharacterSpells)
{
if (wo == null || !wo.HasIdData)
return EquipmentCantripItemState.Unknown;
if (wo.SpellCount == 0 || wo.Values(LongValueKey.MaximumMana, 0) == 0)
return EquipmentCantripItemState.NotActivatable;
if (wo.Values(LongValueKey.CurrentMana, 0) == 0)
return EquipmentCantripItemState.NotActive;
for (int i = 0; i < wo.SpellCount; i++)
{
int spellOnItemId = wo.Spell(i);
if (wo.Exists(LongValueKey.AssociatedSpell) && wo.Values(LongValueKey.AssociatedSpell) == spellOnItemId)
continue;
var spellOnItem = fileService.SpellTable.GetById(spellOnItemId);
if (spellOnItem == null)
continue;
if (spellOnItem.IsDebuff || spellOnItem.IsOffensive)
continue;
bool thisSpellIsActive = false;
for (int j = 0; j < wo.ActiveSpellCount; j++)
{
var activeSpellOnItem = fileService.SpellTable.GetById(wo.ActiveSpell(j));
if (SpellMatchesOrSurpasses(activeSpellOnItem, spellOnItem))
{
thisSpellIsActive = true;
break;
}
}
if (thisSpellIsActive)
continue;
foreach (var activeSpellOnChar in activeCharacterSpells)
{
if (SpellMatchesOrSurpasses(activeSpellOnChar, spellOnItem))
{
thisSpellIsActive = true;
break;
}
}
if (!thisSpellIsActive)
return EquipmentCantripItemState.NotActive;
}
return EquipmentCantripItemState.Active;
}
private static bool SpellMatchesOrSurpasses(Spell activeSpell, Spell requiredSpell)
{
if (activeSpell == null || requiredSpell == null)
return false;
return activeSpell.Family == requiredSpell.Family && activeSpell.Difficulty >= requiredSpell.Difficulty;
}
private int CalculateCurrentMana(WorldObject wo, EquipmentCantripItemState itemState)
{
if (wo == null)
return 0;
if (itemState == EquipmentCantripItemState.Unknown || itemState == EquipmentCantripItemState.NotActivatable)
return 0;
if (itemState == EquipmentCantripItemState.NotActive)
return wo.Values(LongValueKey.CurrentMana, 0);
var secondsPerBurn = GetSecondsPerBurn(wo);
if (secondsPerBurn <= 0)
return wo.Values(LongValueKey.CurrentMana, 0);
if (!_lastManaUpdateByItemId.TryGetValue(wo.Id, out var lastUpdate))
{
lastUpdate = DateTime.UtcNow;
_lastManaUpdateByItemId[wo.Id] = lastUpdate;
}
var timeSinceLastUpdate = DateTime.UtcNow - lastUpdate;
int burnedMana = (int)(timeSinceLastUpdate.TotalSeconds / secondsPerBurn);
int currentMana = wo.Values(LongValueKey.CurrentMana, 0);
if (burnedMana > currentMana)
burnedMana = currentMana;
return Math.Max(currentMana - burnedMana, 0);
}
private TimeSpan CalculateManaTimeRemaining(WorldObject wo, int calculatedCurrentMana)
{
var secondsPerBurn = GetSecondsPerBurn(wo);
if (secondsPerBurn <= 0)
return new TimeSpan(99, 99, 0);
return TimeSpan.FromSeconds(calculatedCurrentMana * secondsPerBurn);
}
private double GetSecondsPerBurn(WorldObject wo)
{
if (wo == null || !wo.HasIdData)
return 0;
double manaRateOfChange = wo.Values(DoubleValueKey.ManaRateOfChange, 0);
if (manaRateOfChange == 0)
return 0;
return ((int)Math.Ceiling(-0.2 / manaRateOfChange)) * 5;
}
private void RememberManaSnapshotTime(WorldObject wo)
{
if (wo == null)
return;
_lastManaUpdateByItemId[wo.Id] = DateTime.UtcNow;
}
private bool IsPotentiallyRelevant(WorldObject wo)
{
return wo != null && (ItemIsEquippedByMe(wo) || _lastManaUpdateByItemId.ContainsKey(wo.Id));
}
private bool ShouldDisplayInManaPanel(WorldObject wo)
{
if (wo == null || !ItemIsEquippedByMe(wo))
return false;
if (!string.IsNullOrEmpty(wo.Name) && wo.Name.IndexOf("Aetheria", StringComparison.OrdinalIgnoreCase) >= 0)
return false;
if (wo.Values(LongValueKey.EquipableSlots, 0) == 134217728)
return false;
if (wo.Values(LongValueKey.EquippedSlots, 0) == 8388608)
return false;
return true;
}
private bool ItemIsEquippedByMe(WorldObject wo)
{
int equippedSlots = wo.Values(LongValueKey.EquippedSlots, 0);
if (equippedSlots <= 0)
return false;
if (wo.Values(LongValueKey.Slot, -1) == -1)
return wo.Container == CoreManager.Current.CharacterFilter.Id;
return true;
}
private static string ToWireState(EquipmentCantripItemState itemState)
{
switch (itemState)
{
case EquipmentCantripItemState.Active:
return "active";
case EquipmentCantripItemState.NotActive:
return "not_active";
case EquipmentCantripItemState.NotActivatable:
return "not_activatable";
default:
return "unknown";
}
}
public void Dispose()
{
Cleanup();
_debounceTimer.Tick -= DebounceTimer_Tick;
_debounceTimer.Dispose();
}
private enum EquipmentCantripItemState
{
Unknown,
NotActivatable,
Active,
NotActive
}
}
}