Compare commits
6 commits
9e9a94f159
...
64e690f625
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e690f625 | ||
|
|
0713e96a99 | ||
|
|
f9264f2767 | ||
|
|
c90e888d32 | ||
|
|
366cca8cb6 | ||
|
|
4845a67c1f |
17 changed files with 1510 additions and 1153 deletions
|
|
@ -26,6 +26,8 @@ namespace MosswartMassacre
|
|||
|
||||
public static class CharacterStats
|
||||
{
|
||||
private static IPluginLogger _logger;
|
||||
|
||||
// Cached allegiance data (populated from network messages)
|
||||
private static string allegianceName;
|
||||
private static int allegianceSize;
|
||||
|
|
@ -44,8 +46,9 @@ namespace MosswartMassacre
|
|||
/// <summary>
|
||||
/// Reset all cached data. Call on plugin init.
|
||||
/// </summary>
|
||||
internal static void Init()
|
||||
internal static void Init(IPluginLogger logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
allegianceName = null;
|
||||
allegianceSize = 0;
|
||||
followers = 0;
|
||||
|
|
@ -112,7 +115,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[CharStats] Allegiance processing error: {ex.Message}");
|
||||
_logger?.Log($"[CharStats] Allegiance processing error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,15 +137,15 @@ namespace MosswartMassacre
|
|||
long key = tmpStruct.Value<Int64>("key");
|
||||
long value = tmpStruct.Value<Int64>("value");
|
||||
|
||||
if (key == 6) // AvailableLuminance
|
||||
if (key == Constants.AvailableLuminanceKey)
|
||||
luminanceEarned = value;
|
||||
else if (key == 7) // MaximumLuminance
|
||||
else if (key == Constants.MaximumLuminanceKey)
|
||||
luminanceTotal = value;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[CharStats] Property processing error: {ex.Message}");
|
||||
_logger?.Log($"[CharStats] Property processing error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,14 +165,14 @@ namespace MosswartMassacre
|
|||
int key = BitConverter.ToInt32(raw, 5);
|
||||
long value = BitConverter.ToInt64(raw, 9);
|
||||
|
||||
if (key == 6) // AvailableLuminance
|
||||
if (key == Constants.AvailableLuminanceKey)
|
||||
luminanceEarned = value;
|
||||
else if (key == 7) // MaximumLuminance
|
||||
else if (key == Constants.MaximumLuminanceKey)
|
||||
luminanceTotal = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[CharStats] Int64 property update error: {ex.Message}");
|
||||
_logger?.Log($"[CharStats] Int64 property update error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +189,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[CharStats] Title processing error: {ex.Message}");
|
||||
_logger?.Log($"[CharStats] Title processing error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +204,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[CharStats] Set title error: {ex.Message}");
|
||||
_logger?.Log($"[CharStats] Set title error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -329,7 +332,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[CharStats] Error collecting stats: {ex.Message}");
|
||||
_logger?.Log($"[CharStats] Error collecting stats: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
90
MosswartMassacre/ChatEventRouter.cs
Normal file
90
MosswartMassacre/ChatEventRouter.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Decal.Adapter;
|
||||
using Decal.Adapter.Wrappers;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Routes chat events to the appropriate handler (KillTracker, RareTracker, etc.)
|
||||
/// Replaces the big if/else chain in PluginCore.OnChatText.
|
||||
/// </summary>
|
||||
internal class ChatEventRouter
|
||||
{
|
||||
private readonly IPluginLogger _logger;
|
||||
private readonly KillTracker _killTracker;
|
||||
private RareTracker _rareTracker;
|
||||
private readonly Action<int> _onRareCountChanged;
|
||||
private readonly Action<string> _onAllegianceReport;
|
||||
|
||||
internal void SetRareTracker(RareTracker rareTracker) => _rareTracker = rareTracker;
|
||||
|
||||
internal ChatEventRouter(
|
||||
IPluginLogger logger,
|
||||
KillTracker killTracker,
|
||||
RareTracker rareTracker,
|
||||
Action<int> onRareCountChanged,
|
||||
Action<string> onAllegianceReport)
|
||||
{
|
||||
_logger = logger;
|
||||
_killTracker = killTracker;
|
||||
_rareTracker = rareTracker;
|
||||
_onRareCountChanged = onRareCountChanged;
|
||||
_onAllegianceReport = onAllegianceReport;
|
||||
}
|
||||
|
||||
internal void OnChatText(object sender, ChatTextInterceptEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_killTracker.CheckForKill(e.Text);
|
||||
|
||||
if (_rareTracker != null && _rareTracker.CheckForRare(e.Text, out string rareText))
|
||||
{
|
||||
_killTracker.RareCount = _rareTracker.RareCount;
|
||||
_onRareCountChanged?.Invoke(_rareTracker.RareCount);
|
||||
}
|
||||
|
||||
if (e.Color == 18 && e.Text.EndsWith("!report\""))
|
||||
{
|
||||
TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
|
||||
string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_rareTracker?.RareCount ?? 0}";
|
||||
_logger?.Log($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
|
||||
_onAllegianceReport?.Invoke(reportMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log("Error processing chat message: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams all chat text to WebSocket (separate handler from the filtered one above).
|
||||
/// </summary>
|
||||
internal static async void AllChatText(object sender, ChatTextInterceptEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
string cleaned = NormalizeChatLine(e.Text);
|
||||
await WebSocket.SendChatTextAsync(e.Color, cleaned);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChatLine(string raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw))
|
||||
return raw;
|
||||
|
||||
var noTags = Regex.Replace(raw, "<[^>]+>", "");
|
||||
var trimmed = noTags.TrimEnd('\r', '\n');
|
||||
var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
|
||||
|
||||
return collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
MosswartMassacre/CommandRouter.cs
Normal file
67
MosswartMassacre/CommandRouter.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Dictionary-based /mm command dispatcher. Commands are registered with descriptions
|
||||
/// and routed by name lookup instead of a giant switch statement.
|
||||
/// </summary>
|
||||
internal class CommandRouter
|
||||
{
|
||||
private readonly Dictionary<string, (Action<string[]> handler, string description)> _commands
|
||||
= new Dictionary<string, (Action<string[]>, string)>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Register a command with its handler and help description.
|
||||
/// </summary>
|
||||
internal void Register(string name, Action<string[]> handler, string description)
|
||||
{
|
||||
_commands[name] = (handler, description);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch a raw /mm command string. Returns false if the command was not found.
|
||||
/// </summary>
|
||||
internal bool Dispatch(string rawText)
|
||||
{
|
||||
string[] args = rawText.Substring(3).Trim().Split(' ');
|
||||
|
||||
if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
|
||||
{
|
||||
PluginCore.WriteToChat("Usage: /mm <command>. Try /mm help");
|
||||
return true;
|
||||
}
|
||||
|
||||
string subCommand = args[0].ToLower();
|
||||
|
||||
if (subCommand == "help")
|
||||
{
|
||||
PrintHelp();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_commands.TryGetValue(subCommand, out var entry))
|
||||
{
|
||||
entry.handler(args);
|
||||
return true;
|
||||
}
|
||||
|
||||
PluginCore.WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void PrintHelp()
|
||||
{
|
||||
PluginCore.WriteToChat("Mosswart Massacre Commands:");
|
||||
foreach (var kvp in _commands)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Value.description))
|
||||
{
|
||||
PluginCore.WriteToChat($"/mm {kvp.Key,-18} - {kvp.Value.description}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
MosswartMassacre/Constants.cs
Normal file
30
MosswartMassacre/Constants.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized constants for timer intervals, message type IDs, and property keys.
|
||||
/// </summary>
|
||||
internal static class Constants
|
||||
{
|
||||
// Timer intervals (milliseconds)
|
||||
internal const int StatsUpdateIntervalMs = 1000;
|
||||
internal const int VitalsUpdateIntervalMs = 5000;
|
||||
internal const int CommandProcessIntervalMs = 10;
|
||||
internal const int QuestStreamingIntervalMs = 30000;
|
||||
internal const int CharacterStatsIntervalMs = 600000; // 10 minutes
|
||||
internal const int LoginDelayMs = 5000;
|
||||
|
||||
// Network message types
|
||||
internal const int GameEventMessageType = 0xF7B0;
|
||||
internal const int PrivateUpdatePropertyInt64 = 0x02CF;
|
||||
|
||||
// Game event IDs (sub-events within 0xF7B0)
|
||||
internal const int AllegianceInfoEvent = 0x0020;
|
||||
internal const int LoginCharacterEvent = 0x0013;
|
||||
internal const int TitlesListEvent = 0x0029;
|
||||
internal const int SetTitleEvent = 0x002b;
|
||||
|
||||
// Int64 property keys
|
||||
internal const int AvailableLuminanceKey = 6;
|
||||
internal const int MaximumLuminanceKey = 7;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,9 +62,9 @@ namespace MosswartMassacre
|
|||
// PATHWAY 2: Target Host.Actions.AddChatText (what our plugin uses)
|
||||
PatchHostActions();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Only log if completely unable to apply any patches
|
||||
AddDebugLog($"ApplyDecalPatches failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,13 +92,15 @@ namespace MosswartMassacre
|
|||
{
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"PatchHooksWrapper single patch failed ({prefixMethodName}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"PatchHooksWrapper failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,16 +141,18 @@ namespace MosswartMassacre
|
|||
{
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"PatchHostActions single patch failed ({prefixMethodName}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PATHWAY 3: Try to patch at PluginHost level
|
||||
PatchPluginHost();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"PatchHostActions failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,36 +163,31 @@ namespace MosswartMassacre
|
|||
{
|
||||
try
|
||||
{
|
||||
// Try to patch CoreManager.Current.Actions if it's different
|
||||
try
|
||||
var coreActions = CoreManager.Current?.Actions;
|
||||
if (coreActions != null && coreActions != PluginCore.MyHost?.Actions)
|
||||
{
|
||||
var coreActions = CoreManager.Current?.Actions;
|
||||
if (coreActions != null && coreActions != PluginCore.MyHost?.Actions)
|
||||
var coreActionsMethods = coreActions.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw" || m.Name == "AddStatusText").ToArray();
|
||||
|
||||
foreach (var method in coreActionsMethods)
|
||||
{
|
||||
var coreActionsMethods = coreActions.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m => m.Name == "AddChatText" || m.Name == "AddChatTextRaw" || m.Name == "AddStatusText").ToArray();
|
||||
|
||||
foreach (var method in coreActionsMethods)
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
try
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
try
|
||||
{
|
||||
string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
string prefixMethodName = "AddChatTextPrefixCore" + parameters.Length;
|
||||
ApplySinglePatch(method, prefixMethodName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"PatchPluginHost single patch failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"PatchPluginHost failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,9 +199,9 @@ namespace MosswartMassacre
|
|||
try
|
||||
{
|
||||
// Get our prefix method
|
||||
var prefixMethod = typeof(DecalPatchMethods).GetMethod(prefixMethodName,
|
||||
var prefixMethod = typeof(DecalPatchMethods).GetMethod(prefixMethodName,
|
||||
BindingFlags.Static | BindingFlags.Public);
|
||||
|
||||
|
||||
if (prefixMethod != null)
|
||||
{
|
||||
// Use UtilityBelt's exact approach
|
||||
|
|
@ -210,8 +209,9 @@ namespace MosswartMassacre
|
|||
patchesApplied = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"ApplySinglePatch failed ({prefixMethodName}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -244,8 +244,9 @@ namespace MosswartMassacre
|
|||
}
|
||||
patchesApplied = false;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddDebugLog($"Cleanup failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -390,8 +391,9 @@ namespace MosswartMassacre
|
|||
Task.Run(() => WebSocket.SendChatTextAsync(color, text));
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
DecalHarmonyClean.AddDebugLog($"ProcessInterceptedMessage failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
55
MosswartMassacre/GameEventRouter.cs
Normal file
55
MosswartMassacre/GameEventRouter.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using Decal.Adapter;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Routes EchoFilter.ServerDispatch network messages to the appropriate handlers.
|
||||
/// Owns the routing of 0xF7B0 sub-events and 0x02CF to CharacterStats.
|
||||
/// </summary>
|
||||
internal class GameEventRouter
|
||||
{
|
||||
private readonly IPluginLogger _logger;
|
||||
|
||||
internal GameEventRouter(IPluginLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal void OnServerDispatch(object sender, NetworkMessageEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Message.Type == Constants.GameEventMessageType)
|
||||
{
|
||||
int eventId = (int)e.Message["event"];
|
||||
|
||||
if (eventId == Constants.AllegianceInfoEvent)
|
||||
{
|
||||
CharacterStats.ProcessAllegianceInfoMessage(e);
|
||||
}
|
||||
else if (eventId == Constants.LoginCharacterEvent)
|
||||
{
|
||||
CharacterStats.ProcessCharacterPropertyData(e);
|
||||
}
|
||||
else if (eventId == Constants.TitlesListEvent)
|
||||
{
|
||||
CharacterStats.ProcessTitlesMessage(e);
|
||||
}
|
||||
else if (eventId == Constants.SetTitleEvent)
|
||||
{
|
||||
CharacterStats.ProcessSetTitleMessage(e);
|
||||
}
|
||||
}
|
||||
else if (e.Message.Type == Constants.PrivateUpdatePropertyInt64)
|
||||
{
|
||||
CharacterStats.ProcessPropertyInt64Update(e);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[CharStats] ServerDispatch error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
MosswartMassacre/IGameStats.cs
Normal file
19
MosswartMassacre/IGameStats.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides game statistics for WebSocket telemetry payloads.
|
||||
/// Replaces direct static field access on PluginCore.
|
||||
/// </summary>
|
||||
public interface IGameStats
|
||||
{
|
||||
int TotalKills { get; }
|
||||
double KillsPerHour { get; }
|
||||
int SessionDeaths { get; }
|
||||
int TotalDeaths { get; }
|
||||
int CachedPrismaticCount { get; }
|
||||
string CharTag { get; }
|
||||
DateTime StatsStartTime { get; }
|
||||
}
|
||||
}
|
||||
11
MosswartMassacre/IPluginLogger.cs
Normal file
11
MosswartMassacre/IPluginLogger.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for writing messages to the game chat window.
|
||||
/// Eliminates direct PluginCore.WriteToChat() dependencies from manager classes.
|
||||
/// </summary>
|
||||
public interface IPluginLogger
|
||||
{
|
||||
void Log(string message);
|
||||
}
|
||||
}
|
||||
184
MosswartMassacre/InventoryMonitor.cs
Normal file
184
MosswartMassacre/InventoryMonitor.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Decal.Adapter;
|
||||
using Decal.Adapter.Wrappers;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks Prismatic Taper inventory counts using event-driven delta math.
|
||||
/// Avoids expensive inventory scans during gameplay.
|
||||
/// </summary>
|
||||
internal class InventoryMonitor
|
||||
{
|
||||
private readonly IPluginLogger _logger;
|
||||
private readonly Dictionary<int, int> _trackedTaperContainers = new Dictionary<int, int>();
|
||||
private readonly Dictionary<int, int> _lastKnownStackSizes = new Dictionary<int, int>();
|
||||
|
||||
internal int CachedPrismaticCount { get; private set; }
|
||||
internal int LastPrismaticCount { get; private set; }
|
||||
|
||||
internal InventoryMonitor(IPluginLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
LastPrismaticCount = CachedPrismaticCount;
|
||||
CachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
|
||||
|
||||
_trackedTaperContainers.Clear();
|
||||
_lastKnownStackSizes.Clear();
|
||||
|
||||
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
|
||||
{
|
||||
if (wo.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase) &&
|
||||
IsPlayerOwnedContainer(wo.Container))
|
||||
{
|
||||
int stackCount = wo.Values(LongValueKey.StackCount, 1);
|
||||
_trackedTaperContainers[wo.Id] = wo.Container;
|
||||
_lastKnownStackSizes[wo.Id] = stackCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[TAPER] Error initializing count: {ex.Message}");
|
||||
CachedPrismaticCount = 0;
|
||||
LastPrismaticCount = 0;
|
||||
_trackedTaperContainers.Clear();
|
||||
_lastKnownStackSizes.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnInventoryCreate(object sender, CreateObjectEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = e.New;
|
||||
if (IsPlayerOwnedContainer(item.Container) &&
|
||||
item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
LastPrismaticCount = CachedPrismaticCount;
|
||||
int stackCount = item.Values(LongValueKey.StackCount, 1);
|
||||
CachedPrismaticCount += stackCount;
|
||||
|
||||
_trackedTaperContainers[item.Id] = item.Container;
|
||||
_lastKnownStackSizes[item.Id] = stackCount;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[TAPER] Error in OnInventoryCreate: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnInventoryRelease(object sender, ReleaseObjectEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = e.Released;
|
||||
if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_trackedTaperContainers.TryGetValue(item.Id, out int previousContainer))
|
||||
{
|
||||
if (IsPlayerOwnedContainer(previousContainer))
|
||||
{
|
||||
LastPrismaticCount = CachedPrismaticCount;
|
||||
int stackCount = item.Values(LongValueKey.StackCount, 1);
|
||||
CachedPrismaticCount -= stackCount;
|
||||
}
|
||||
|
||||
_trackedTaperContainers.Remove(item.Id);
|
||||
_lastKnownStackSizes.Remove(item.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
LastPrismaticCount = CachedPrismaticCount;
|
||||
CachedPrismaticCount = Utils.GetItemStackSize("Prismatic Taper");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[TAPER] Error in OnInventoryRelease: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnInventoryChange(object sender, ChangeObjectEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var item = e.Changed;
|
||||
if (item.Name.Equals("Prismatic Taper", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bool isInPlayerContainer = IsPlayerOwnedContainer(item.Container);
|
||||
|
||||
if (isInPlayerContainer)
|
||||
{
|
||||
bool wasAlreadyTracked = _trackedTaperContainers.ContainsKey(item.Id);
|
||||
_trackedTaperContainers[item.Id] = item.Container;
|
||||
|
||||
int currentStack = item.Values(LongValueKey.StackCount, 1);
|
||||
|
||||
if (!wasAlreadyTracked)
|
||||
{
|
||||
LastPrismaticCount = CachedPrismaticCount;
|
||||
CachedPrismaticCount += currentStack;
|
||||
}
|
||||
else if (_lastKnownStackSizes.TryGetValue(item.Id, out int previousStack))
|
||||
{
|
||||
int stackDelta = currentStack - previousStack;
|
||||
if (stackDelta != 0)
|
||||
{
|
||||
LastPrismaticCount = CachedPrismaticCount;
|
||||
CachedPrismaticCount += stackDelta;
|
||||
}
|
||||
}
|
||||
|
||||
_lastKnownStackSizes[item.Id] = currentStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[TAPER] Error in OnInventoryChange: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void Cleanup()
|
||||
{
|
||||
_trackedTaperContainers.Clear();
|
||||
_lastKnownStackSizes.Clear();
|
||||
}
|
||||
|
||||
internal int TrackedTaperCount => _trackedTaperContainers.Count;
|
||||
internal int KnownStackSizesCount => _lastKnownStackSizes.Count;
|
||||
|
||||
private static bool IsPlayerOwnedContainer(int containerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (containerId == CoreManager.Current.CharacterFilter.Id)
|
||||
return true;
|
||||
|
||||
WorldObject container = CoreManager.Current.WorldFilter[containerId];
|
||||
if (container != null &&
|
||||
container.ObjectClass == ObjectClass.Container &&
|
||||
container.Container == CoreManager.Current.CharacterFilter.Id)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
176
MosswartMassacre/KillTracker.cs
Normal file
176
MosswartMassacre/KillTracker.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Timers;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks kills, deaths, and kill rate calculations.
|
||||
/// Owns the 1-second stats update timer.
|
||||
/// </summary>
|
||||
internal class KillTracker
|
||||
{
|
||||
private readonly IPluginLogger _logger;
|
||||
private readonly Action<int, double, double> _onStatsUpdated;
|
||||
private readonly Action<TimeSpan> _onElapsedUpdated;
|
||||
|
||||
private int _totalKills;
|
||||
private int _sessionDeaths;
|
||||
private int _totalDeaths;
|
||||
private double _killsPer5Min;
|
||||
private double _killsPerHour;
|
||||
private DateTime _lastKillTime = DateTime.Now;
|
||||
private DateTime _statsStartTime = DateTime.Now;
|
||||
private Timer _updateTimer;
|
||||
|
||||
// Kill message patterns — all 35+ patterns preserved exactly
|
||||
private static readonly string[] KillPatterns = new string[]
|
||||
{
|
||||
@"^You flatten (?<targetname>.+)'s body with the force of your assault!$",
|
||||
@"^You bring (?<targetname>.+) to a fiery end!$",
|
||||
@"^You beat (?<targetname>.+) to a lifeless pulp!$",
|
||||
@"^You smite (?<targetname>.+) mightily!$",
|
||||
@"^You obliterate (?<targetname>.+)!$",
|
||||
@"^You run (?<targetname>.+) through!$",
|
||||
@"^You reduce (?<targetname>.+) to a sizzling, oozing mass!$",
|
||||
@"^You knock (?<targetname>.+) into next Morningthaw!$",
|
||||
@"^You split (?<targetname>.+) apart!$",
|
||||
@"^You cleave (?<targetname>.+) in twain!$",
|
||||
@"^You slay (?<targetname>.+) viciously enough to impart death several times over!$",
|
||||
@"^You reduce (?<targetname>.+) to a drained, twisted corpse!$",
|
||||
@"^Your killing blow nearly turns (?<targetname>.+) inside-out!$",
|
||||
@"^Your attack stops (?<targetname>.+) cold!$",
|
||||
@"^Your lightning coruscates over (?<targetname>.+)'s mortal remains!$",
|
||||
@"^Your assault sends (?<targetname>.+) to an icy death!$",
|
||||
@"^You killed (?<targetname>.+)!$",
|
||||
@"^The thunder of crushing (?<targetname>.+) is followed by the deafening silence of death!$",
|
||||
@"^The deadly force of your attack is so strong that (?<targetname>.+)'s ancestors feel it!$",
|
||||
@"^(?<targetname>.+)'s seared corpse smolders before you!$",
|
||||
@"^(?<targetname>.+) is reduced to cinders!$",
|
||||
@"^(?<targetname>.+) is shattered by your assault!$",
|
||||
@"^(?<targetname>.+) catches your attack, with dire consequences!$",
|
||||
@"^(?<targetname>.+) is utterly destroyed by your attack!$",
|
||||
@"^(?<targetname>.+) suffers a frozen fate!$",
|
||||
@"^(?<targetname>.+)'s perforated corpse falls before you!$",
|
||||
@"^(?<targetname>.+) is fatally punctured!$",
|
||||
@"^(?<targetname>.+)'s death is preceded by a sharp, stabbing pain!$",
|
||||
@"^(?<targetname>.+) is torn to ribbons by your assault!$",
|
||||
@"^(?<targetname>.+) is liquified by your attack!$",
|
||||
@"^(?<targetname>.+)'s last strength dissolves before you!$",
|
||||
@"^Electricity tears (?<targetname>.+) apart!$",
|
||||
@"^Blistered by lightning, (?<targetname>.+) falls!$",
|
||||
@"^(?<targetname>.+)'s last strength withers before you!$",
|
||||
@"^(?<targetname>.+) is dessicated by your attack!$",
|
||||
@"^(?<targetname>.+) is incinerated by your assault!$"
|
||||
};
|
||||
|
||||
internal int TotalKills => _totalKills;
|
||||
internal double KillsPerHour => _killsPerHour;
|
||||
internal double KillsPer5Min => _killsPer5Min;
|
||||
internal int SessionDeaths => _sessionDeaths;
|
||||
internal int TotalDeaths => _totalDeaths;
|
||||
internal DateTime StatsStartTime => _statsStartTime;
|
||||
internal DateTime LastKillTime => _lastKillTime;
|
||||
internal int RareCount { get; set; }
|
||||
|
||||
/// <param name="logger">Logger for chat output</param>
|
||||
/// <param name="onStatsUpdated">Callback(totalKills, killsPer5Min, killsPerHour) for UI updates</param>
|
||||
/// <param name="onElapsedUpdated">Callback(elapsed) for UI elapsed time updates</param>
|
||||
internal KillTracker(IPluginLogger logger, Action<int, double, double> onStatsUpdated, Action<TimeSpan> onElapsedUpdated)
|
||||
{
|
||||
_logger = logger;
|
||||
_onStatsUpdated = onStatsUpdated;
|
||||
_onElapsedUpdated = onElapsedUpdated;
|
||||
}
|
||||
|
||||
internal void Start()
|
||||
{
|
||||
_updateTimer = new Timer(Constants.StatsUpdateIntervalMs);
|
||||
_updateTimer.Elapsed += UpdateStats;
|
||||
_updateTimer.Start();
|
||||
}
|
||||
|
||||
internal void Stop()
|
||||
{
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
_updateTimer.Stop();
|
||||
_updateTimer.Dispose();
|
||||
_updateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool CheckForKill(string text)
|
||||
{
|
||||
if (IsKilledByMeMessage(text))
|
||||
{
|
||||
_totalKills++;
|
||||
_lastKillTime = DateTime.Now;
|
||||
CalculateKillsPerInterval();
|
||||
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void OnDeath()
|
||||
{
|
||||
_sessionDeaths++;
|
||||
}
|
||||
|
||||
internal void SetTotalDeaths(int totalDeaths)
|
||||
{
|
||||
_totalDeaths = totalDeaths;
|
||||
}
|
||||
|
||||
internal void RestartStats()
|
||||
{
|
||||
_totalKills = 0;
|
||||
RareCount = 0;
|
||||
_sessionDeaths = 0;
|
||||
_statsStartTime = DateTime.Now;
|
||||
_killsPer5Min = 0;
|
||||
_killsPerHour = 0;
|
||||
|
||||
_logger?.Log($"Stats have been reset. Session deaths: {_sessionDeaths}, Total deaths: {_totalDeaths}");
|
||||
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
|
||||
}
|
||||
|
||||
private void UpdateStats(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
TimeSpan elapsed = DateTime.Now - _statsStartTime;
|
||||
_onElapsedUpdated?.Invoke(elapsed);
|
||||
|
||||
CalculateKillsPerInterval();
|
||||
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log("Error updating stats: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateKillsPerInterval()
|
||||
{
|
||||
double minutesElapsed = (DateTime.Now - _statsStartTime).TotalMinutes;
|
||||
|
||||
if (minutesElapsed > 0)
|
||||
{
|
||||
_killsPer5Min = (_totalKills / minutesElapsed) * 5;
|
||||
_killsPerHour = (_totalKills / minutesElapsed) * 60;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsKilledByMeMessage(string text)
|
||||
{
|
||||
foreach (string pattern in KillPatterns)
|
||||
{
|
||||
if (Regex.IsMatch(text, pattern))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -304,6 +304,16 @@
|
|||
<Compile Include="..\Shared\VCS_Connector.cs">
|
||||
<Link>Shared\VCS_Connector.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="ChatEventRouter.cs" />
|
||||
<Compile Include="CommandRouter.cs" />
|
||||
<Compile Include="Constants.cs" />
|
||||
<Compile Include="GameEventRouter.cs" />
|
||||
<Compile Include="IGameStats.cs" />
|
||||
<Compile Include="IPluginLogger.cs" />
|
||||
<Compile Include="QuestStreamingService.cs" />
|
||||
<Compile Include="InventoryMonitor.cs" />
|
||||
<Compile Include="KillTracker.cs" />
|
||||
<Compile Include="RareTracker.cs" />
|
||||
<Compile Include="ClientTelemetry.cs" />
|
||||
<Compile Include="DecalHarmonyClean.cs" />
|
||||
<Compile Include="FlagTrackerData.cs" />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
133
MosswartMassacre/QuestStreamingService.cs
Normal file
133
MosswartMassacre/QuestStreamingService.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Timers;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams high-priority quest timer data via WebSocket on a 30-second interval.
|
||||
/// </summary>
|
||||
internal class QuestStreamingService
|
||||
{
|
||||
private readonly IPluginLogger _logger;
|
||||
private Timer _timer;
|
||||
|
||||
internal QuestStreamingService(IPluginLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal void Start()
|
||||
{
|
||||
_timer = new Timer(Constants.QuestStreamingIntervalMs);
|
||||
_timer.Elapsed += OnTimerElapsed;
|
||||
_timer.AutoReset = true;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
internal void Stop()
|
||||
{
|
||||
if (_timer != null)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Elapsed -= OnTimerElapsed;
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IsRunning => _timer != null && _timer.Enabled;
|
||||
|
||||
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (PluginSettings.Instance?.VerboseLogging == true)
|
||||
{
|
||||
_logger?.Log("[QUEST-STREAM] Timer fired, checking conditions...");
|
||||
}
|
||||
|
||||
if (!PluginCore.WebSocketEnabled)
|
||||
{
|
||||
if (PluginSettings.Instance?.VerboseLogging == true)
|
||||
{
|
||||
_logger?.Log("[QUEST-STREAM] WebSocket not enabled, skipping");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var questManager = PluginCore.questManager;
|
||||
if (questManager?.QuestList == null || questManager.QuestList.Count == 0)
|
||||
{
|
||||
if (PluginSettings.Instance?.VerboseLogging == true)
|
||||
{
|
||||
_logger?.Log($"[QUEST-STREAM] No quest data available (null: {questManager?.QuestList == null}, count: {questManager?.QuestList?.Count ?? 0})");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
var priorityQuests = questManager.QuestList
|
||||
.Where(q => IsHighPriorityQuest(q.Id))
|
||||
.GroupBy(q => q.Id)
|
||||
.Select(g => g.First())
|
||||
.ToList();
|
||||
|
||||
if (PluginSettings.Instance?.VerboseLogging == true)
|
||||
{
|
||||
_logger?.Log($"[QUEST-STREAM] Found {priorityQuests.Count} priority quests to stream");
|
||||
}
|
||||
|
||||
foreach (var quest in priorityQuests)
|
||||
{
|
||||
try
|
||||
{
|
||||
string questName = questManager.GetFriendlyQuestName(quest.Id);
|
||||
long timeRemaining = quest.ExpireTime - currentTime;
|
||||
string countdown = FormatCountdown(timeRemaining);
|
||||
|
||||
if (PluginSettings.Instance?.VerboseLogging == true)
|
||||
{
|
||||
_logger?.Log($"[QUEST-STREAM] Sending: {questName} - {countdown}");
|
||||
}
|
||||
|
||||
System.Threading.Tasks.Task.Run(() => WebSocket.SendQuestDataAsync(questName, countdown));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[QUEST-STREAM] Error streaming quest {quest.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[QUEST-STREAM] Error in timer handler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsHighPriorityQuest(string questId)
|
||||
{
|
||||
return questId == "stipendtimer_0812" ||
|
||||
questId == "augmentationblankgemacquired" ||
|
||||
questId == "insatiableeaterjaw";
|
||||
}
|
||||
|
||||
internal static string FormatCountdown(long seconds)
|
||||
{
|
||||
if (seconds <= 0)
|
||||
return "READY";
|
||||
|
||||
var timeSpan = TimeSpan.FromSeconds(seconds);
|
||||
|
||||
if (timeSpan.TotalDays >= 1)
|
||||
return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours:D2}h";
|
||||
else if (timeSpan.TotalHours >= 1)
|
||||
return $"{timeSpan.Hours}h {timeSpan.Minutes:D2}m";
|
||||
else if (timeSpan.TotalMinutes >= 1)
|
||||
return $"{timeSpan.Minutes}m {timeSpan.Seconds:D2}s";
|
||||
else
|
||||
return $"{timeSpan.Seconds}s";
|
||||
}
|
||||
}
|
||||
}
|
||||
71
MosswartMassacre/RareTracker.cs
Normal file
71
MosswartMassacre/RareTracker.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Decal.Adapter;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks rare item discoveries, handles rare meta state toggles,
|
||||
/// and sends rare notifications via WebSocket.
|
||||
/// </summary>
|
||||
internal class RareTracker
|
||||
{
|
||||
private readonly IPluginLogger _logger;
|
||||
private readonly string _characterName;
|
||||
|
||||
internal int RareCount { get; set; }
|
||||
internal bool RareMetaEnabled { get; set; } = true;
|
||||
|
||||
internal RareTracker(IPluginLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_characterName = CoreManager.Current.CharacterFilter.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the chat text is a rare discovery by this character.
|
||||
/// If so, increments count, triggers meta switch, allegiance announce, and WebSocket notification.
|
||||
/// Returns true if a rare was found.
|
||||
/// </summary>
|
||||
internal bool CheckForRare(string text, out string rareText)
|
||||
{
|
||||
if (IsRareDiscoveryMessage(text, out rareText))
|
||||
{
|
||||
RareCount++;
|
||||
|
||||
if (RareMetaEnabled)
|
||||
{
|
||||
PluginCore.Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
|
||||
}
|
||||
|
||||
DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
|
||||
_ = WebSocket.SendRareAsync(rareText);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void ToggleRareMeta()
|
||||
{
|
||||
PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
|
||||
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
|
||||
}
|
||||
|
||||
private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
|
||||
{
|
||||
rareTextOnly = null;
|
||||
|
||||
string pattern = @"^(?<name>['A-Za-z ]+)\shas discovered the (?<item>.*?)!$";
|
||||
Match match = Regex.Match(text, pattern);
|
||||
|
||||
if (match.Success && match.Groups["name"].Value == _characterName)
|
||||
{
|
||||
rareTextOnly = match.Groups["item"].Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,9 +72,9 @@ namespace MosswartMassacre
|
|||
return 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Swallow any errors and signal failure
|
||||
PluginCore.WriteToChat($"[VTank] SetSetting error ({setting}): {ex.Message}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ namespace MosswartMassacre
|
|||
private const string SharedSecret = "your_shared_secret";
|
||||
private const int IntervalSec = 5;
|
||||
private static string SessionId = "";
|
||||
private static IPluginLogger _logger;
|
||||
private static IGameStats _gameStats;
|
||||
|
||||
// ─── cached prismatic taper count ─── (now handled by PluginCore event system)
|
||||
|
||||
|
|
@ -51,13 +53,16 @@ namespace MosswartMassacre
|
|||
|
||||
// ─── public API ─────────────────────────────
|
||||
|
||||
public static void SetLogger(IPluginLogger logger) => _logger = logger;
|
||||
public static void SetGameStats(IGameStats gameStats) => _gameStats = gameStats;
|
||||
|
||||
public static void Start()
|
||||
{
|
||||
if (_enabled) return;
|
||||
_enabled = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
PluginCore.WriteToChat("[WebSocket] connecting…");
|
||||
_logger?.Log("[WebSocket] connecting…");
|
||||
_ = Task.Run(ConnectAndLoopAsync);
|
||||
|
||||
}
|
||||
|
|
@ -72,7 +77,7 @@ namespace MosswartMassacre
|
|||
_ws?.Dispose();
|
||||
_ws = null;
|
||||
|
||||
PluginCore.WriteToChat("[WebSocket] DISABLED");
|
||||
_logger?.Log("[WebSocket] DISABLED");
|
||||
}
|
||||
|
||||
// ─── connect / receive / telemetry loop ──────────────────────
|
||||
|
|
@ -87,7 +92,7 @@ namespace MosswartMassacre
|
|||
_ws = new ClientWebSocket();
|
||||
_ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret);
|
||||
await _ws.ConnectAsync(WsEndpoint, _cts.Token);
|
||||
PluginCore.WriteToChat("[WebSocket] CONNECTED");
|
||||
_logger?.Log("[WebSocket] CONNECTED");
|
||||
SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
||||
|
||||
// ─── Register this socket under our character name ───
|
||||
|
|
@ -98,7 +103,7 @@ namespace MosswartMassacre
|
|||
};
|
||||
var regJson = JsonConvert.SerializeObject(registerEnvelope);
|
||||
await SendEncodedAsync(regJson, _cts.Token);
|
||||
PluginCore.WriteToChat("[WebSocket] REGISTERED");
|
||||
_logger?.Log("[WebSocket] REGISTERED");
|
||||
|
||||
var buffer = new byte[4096];
|
||||
|
||||
|
|
@ -118,7 +123,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}");
|
||||
_logger?.Log($"[WebSocket] receive error: {ex.Message}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +156,7 @@ namespace MosswartMassacre
|
|||
});
|
||||
|
||||
// 5) Inline telemetry loop
|
||||
PluginCore.WriteToChat("[WebSocket] Starting telemetry loop");
|
||||
_logger?.Log("[WebSocket] Starting telemetry loop");
|
||||
while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
|
|
@ -161,7 +166,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WebSocket] Telemetry failed: {ex.Message}");
|
||||
_logger?.Log($"[WebSocket] Telemetry failed: {ex.Message}");
|
||||
break; // Exit telemetry loop on failure
|
||||
}
|
||||
|
||||
|
|
@ -171,30 +176,30 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
PluginCore.WriteToChat("[WebSocket] Telemetry loop cancelled");
|
||||
_logger?.Log("[WebSocket] Telemetry loop cancelled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Log why telemetry loop exited
|
||||
PluginCore.WriteToChat($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}");
|
||||
_logger?.Log($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}");
|
||||
|
||||
// Wait for receive loop to finish
|
||||
await receiveTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
PluginCore.WriteToChat("[WebSocket] Connection cancelled");
|
||||
_logger?.Log("[WebSocket] Connection cancelled");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WebSocket] Connection error: {ex.Message}");
|
||||
_logger?.Log($"[WebSocket] Connection error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
var finalState = _ws?.State.ToString() ?? "null";
|
||||
PluginCore.WriteToChat($"[WebSocket] Cleaning up connection - Final state: {finalState}");
|
||||
_logger?.Log($"[WebSocket] Cleaning up connection - Final state: {finalState}");
|
||||
_ws?.Abort();
|
||||
_ws?.Dispose();
|
||||
_ws = null;
|
||||
|
|
@ -203,7 +208,7 @@ namespace MosswartMassacre
|
|||
// Pause before reconnecting
|
||||
if (_enabled)
|
||||
{
|
||||
PluginCore.WriteToChat("[WebSocket] Reconnecting in 2 seconds...");
|
||||
_logger?.Log("[WebSocket] Reconnecting in 2 seconds...");
|
||||
try { await Task.Delay(2000, CancellationToken.None); } catch { }
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +339,7 @@ namespace MosswartMassacre
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WebSocket] Send error: {ex.Message}");
|
||||
_logger?.Log($"[WebSocket] Send error: {ex.Message}");
|
||||
_ws?.Abort();
|
||||
_ws?.Dispose();
|
||||
_ws = null;
|
||||
|
|
@ -347,33 +352,31 @@ namespace MosswartMassacre
|
|||
|
||||
// ─── payload builder ──────────────────────────────
|
||||
|
||||
// Removed old cache system - now using PluginCore.cachedPrismaticCount
|
||||
|
||||
private static string BuildPayloadJson()
|
||||
{
|
||||
var tele = new ClientTelemetry();
|
||||
var coords = Coordinates.Me;
|
||||
var stats = _gameStats;
|
||||
var payload = new
|
||||
{
|
||||
type = "telemetry",
|
||||
character_name = CoreManager.Current.CharacterFilter.Name,
|
||||
char_tag = PluginCore.CharTag,
|
||||
char_tag = stats?.CharTag ?? "",
|
||||
session_id = SessionInfo.GuidString,
|
||||
timestamp = DateTime.UtcNow.ToString("o"),
|
||||
ew = coords.EW,
|
||||
ns = coords.NS,
|
||||
z = coords.Z,
|
||||
kills = PluginCore.totalKills,
|
||||
kills_per_hour = PluginCore.killsPerHour.ToString("F0"),
|
||||
onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"),
|
||||
deaths = PluginCore.sessionDeaths.ToString(),
|
||||
total_deaths = PluginCore.totalDeaths.ToString(),
|
||||
prismatic_taper_count = PluginCore.cachedPrismaticCount.ToString(),
|
||||
kills = stats?.TotalKills ?? 0,
|
||||
kills_per_hour = (stats?.KillsPerHour ?? 0).ToString("F0"),
|
||||
onlinetime = (DateTime.Now - (stats?.StatsStartTime ?? DateTime.Now)).ToString(@"dd\.hh\:mm\:ss"),
|
||||
deaths = (stats?.SessionDeaths ?? 0).ToString(),
|
||||
total_deaths = (stats?.TotalDeaths ?? 0).ToString(),
|
||||
prismatic_taper_count = (stats?.CachedPrismaticCount ?? 0).ToString(),
|
||||
vt_state = VtankControl.VtGetMetaState(),
|
||||
mem_mb = tele.MemoryBytes,
|
||||
cpu_pct = tele.GetCpuUsage(),
|
||||
mem_handles = tele.HandleCount
|
||||
|
||||
};
|
||||
return JsonConvert.SerializeObject(payload);
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue