419 lines
14 KiB
C#
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
|
|
}
|
|
}
|
|
}
|