feat: stream live equipment cantrip states
This commit is contained in:
parent
e20f9df256
commit
0fbd100b7b
6 changed files with 477 additions and 10 deletions
419
MosswartMassacre/EquipmentCantripStateTracker.cs
Normal file
419
MosswartMassacre/EquipmentCantripStateTracker.cs
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Decal.Adapter;
|
||||
using Decal.Adapter.Wrappers;
|
||||
using Decal.Filters;
|
||||
|
|
@ -920,21 +921,29 @@ namespace MosswartMassacre
|
|||
}
|
||||
|
||||
// Scan active spells for cantrips using CharacterFilter.Enchantments
|
||||
var enchantments = characterFilter.Enchantments;
|
||||
if (enchantments != null)
|
||||
int enchantmentCount = characterFilter.Underlying.EnchantmentCount;
|
||||
for (int i = 0; i < enchantmentCount; i++)
|
||||
{
|
||||
for (int i = 0; i < enchantments.Count; i++)
|
||||
Decal.Interop.Filters.Enchantment ench = null;
|
||||
try
|
||||
{
|
||||
var ench = enchantments[i];
|
||||
var spell = SpellManager.GetSpell(ench.SpellId);
|
||||
ench = characterFilter.Underlying.get_Enchantment(i);
|
||||
if (ench == null)
|
||||
continue;
|
||||
|
||||
var spell = SpellManager.GetSpell(ench.SpellID);
|
||||
if (spell != null && spell.CantripLevel != Mag.Shared.Spells.Spell.CantripLevels.None)
|
||||
{
|
||||
DetectCantrip(ench.SpellId);
|
||||
DetectCantrip(ench.SpellID);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ench != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(ench);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
|
||||
// Compute final icon IDs for all cantrips after refresh
|
||||
|
|
@ -1929,4 +1938,4 @@ namespace MosswartMassacre
|
|||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@
|
|||
<Compile Include="RareTracker.cs" />
|
||||
<Compile Include="ClientTelemetry.cs" />
|
||||
<Compile Include="DecalHarmonyClean.cs" />
|
||||
<Compile Include="EquipmentCantripStateTracker.cs" />
|
||||
<Compile Include="FlagTrackerData.cs" />
|
||||
<Compile Include="MossyInventory.cs" />
|
||||
<Compile Include="NavRoute.cs" />
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ namespace MosswartMassacre
|
|||
private QuestStreamingService _questStreamingService;
|
||||
private CommandRouter _commandRouter;
|
||||
private LiveInventoryTracker _liveInventoryTracker;
|
||||
private EquipmentCantripStateTracker _equipmentCantripStateTracker;
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
|
|
@ -188,6 +189,13 @@ namespace MosswartMassacre
|
|||
CoreManager.Current.WorldFilter.ReleaseObject -= _liveInventoryTracker.OnReleaseObject;
|
||||
CoreManager.Current.WorldFilter.ChangeObject -= _liveInventoryTracker.OnChangeObject;
|
||||
}
|
||||
if (_equipmentCantripStateTracker != null)
|
||||
{
|
||||
CoreManager.Current.WorldFilter.CreateObject -= _equipmentCantripStateTracker.OnCreateObject;
|
||||
CoreManager.Current.WorldFilter.ReleaseObject -= _equipmentCantripStateTracker.OnReleaseObject;
|
||||
CoreManager.Current.WorldFilter.ChangeObject -= _equipmentCantripStateTracker.OnChangeObject;
|
||||
CoreManager.Current.CharacterFilter.ChangeEnchantments -= _equipmentCantripStateTracker.OnChangeEnchantments;
|
||||
}
|
||||
if (_gameEventRouter != null)
|
||||
CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
|
||||
WebSocket.OnServerCommand -= HandleServerCommand;
|
||||
|
|
@ -227,6 +235,7 @@ namespace MosswartMassacre
|
|||
|
||||
// Initialize live inventory tracker (delta WebSocket messages)
|
||||
_liveInventoryTracker = new LiveInventoryTracker(this);
|
||||
_equipmentCantripStateTracker = new EquipmentCantripStateTracker(this);
|
||||
|
||||
// Initialize chat event router (rareTracker set later in LoginComplete)
|
||||
_chatEventRouter = new ChatEventRouter(
|
||||
|
|
@ -253,6 +262,10 @@ namespace MosswartMassacre
|
|||
CoreManager.Current.WorldFilter.CreateObject += _liveInventoryTracker.OnCreateObject;
|
||||
CoreManager.Current.WorldFilter.ReleaseObject += _liveInventoryTracker.OnReleaseObject;
|
||||
CoreManager.Current.WorldFilter.ChangeObject += _liveInventoryTracker.OnChangeObject;
|
||||
CoreManager.Current.WorldFilter.CreateObject += _equipmentCantripStateTracker.OnCreateObject;
|
||||
CoreManager.Current.WorldFilter.ReleaseObject += _equipmentCantripStateTracker.OnReleaseObject;
|
||||
CoreManager.Current.WorldFilter.ChangeObject += _equipmentCantripStateTracker.OnChangeObject;
|
||||
CoreManager.Current.CharacterFilter.ChangeEnchantments += _equipmentCantripStateTracker.OnChangeEnchantments;
|
||||
|
||||
// Initialize VVS view after character login
|
||||
ViewManager.ViewInit();
|
||||
|
|
@ -425,6 +438,15 @@ namespace MosswartMassacre
|
|||
CoreManager.Current.WorldFilter.ChangeObject -= _liveInventoryTracker.OnChangeObject;
|
||||
_liveInventoryTracker.Cleanup();
|
||||
}
|
||||
if (_equipmentCantripStateTracker != null)
|
||||
{
|
||||
CoreManager.Current.WorldFilter.CreateObject -= _equipmentCantripStateTracker.OnCreateObject;
|
||||
CoreManager.Current.WorldFilter.ReleaseObject -= _equipmentCantripStateTracker.OnReleaseObject;
|
||||
CoreManager.Current.WorldFilter.ChangeObject -= _equipmentCantripStateTracker.OnChangeObject;
|
||||
CoreManager.Current.CharacterFilter.ChangeEnchantments -= _equipmentCantripStateTracker.OnChangeEnchantments;
|
||||
_equipmentCantripStateTracker.Dispose();
|
||||
_equipmentCantripStateTracker = null;
|
||||
}
|
||||
|
||||
// Clean up Harmony patches
|
||||
DecalHarmonyClean.Cleanup();
|
||||
|
|
@ -514,6 +536,7 @@ namespace MosswartMassacre
|
|||
|
||||
// Initialize live inventory tracking (after full inventory dump)
|
||||
_liveInventoryTracker?.Initialize();
|
||||
_equipmentCantripStateTracker?.Initialize();
|
||||
|
||||
// Initialize quest manager for always-on quest streaming
|
||||
try
|
||||
|
|
@ -641,6 +664,7 @@ namespace MosswartMassacre
|
|||
|
||||
// 7b. Reinitialize live inventory tracking
|
||||
_liveInventoryTracker?.Initialize();
|
||||
_equipmentCantripStateTracker?.Initialize();
|
||||
|
||||
// 8. Reinitialize quest manager for hot reload
|
||||
try
|
||||
|
|
|
|||
|
|
@ -335,6 +335,20 @@ namespace MosswartMassacre
|
|||
await SendEncodedAsync(json, CancellationToken.None);
|
||||
}
|
||||
|
||||
public static async Task SendEquipmentCantripStateAsync(object[] items)
|
||||
{
|
||||
var envelope = new
|
||||
{
|
||||
type = "equipment_cantrip_state",
|
||||
timestamp = DateTime.UtcNow.ToString("o"),
|
||||
character_name = CoreManager.Current.CharacterFilter.Name,
|
||||
items = items
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(envelope);
|
||||
await SendEncodedAsync(json, CancellationToken.None);
|
||||
}
|
||||
|
||||
public static async Task SendQuestDataAsync(string questName, string countdown)
|
||||
{
|
||||
var envelope = new
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue