diff --git a/MosswartMassacre/CharacterStats.cs b/MosswartMassacre/CharacterStats.cs
new file mode 100644
index 0000000..6d16f03
--- /dev/null
+++ b/MosswartMassacre/CharacterStats.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using Decal.Adapter;
+using Decal.Adapter.Wrappers;
+using Newtonsoft.Json;
+
+namespace MosswartMassacre
+{
+ public struct AllegianceInfoRecord
+ {
+ public string name;
+ public int rank;
+ public int race;
+ public int gender;
+
+ public AllegianceInfoRecord(string _name, int _rank, int _race, int _gender)
+ {
+ name = _name;
+ rank = _rank;
+ race = _race;
+ gender = _gender;
+ }
+ }
+
+ public static class CharacterStats
+ {
+ // Cached allegiance data (populated from network messages)
+ private static string allegianceName;
+ private static int allegianceSize;
+ private static int followers;
+ private static AllegianceInfoRecord monarch;
+ private static AllegianceInfoRecord patron;
+ private static int allegianceRank;
+
+ // Cached luminance data (populated from network messages)
+ private static long luminanceEarned = -1;
+ private static long luminanceTotal = -1;
+
+ // Cached title data (populated from network messages)
+ private static int currentTitle = -1;
+
+ ///
+ /// Reset all cached data. Call on plugin init.
+ ///
+ internal static void Init()
+ {
+ allegianceName = null;
+ allegianceSize = 0;
+ followers = 0;
+ monarch = new AllegianceInfoRecord();
+ patron = new AllegianceInfoRecord();
+ allegianceRank = 0;
+ luminanceEarned = -1;
+ luminanceTotal = -1;
+ currentTitle = -1;
+ }
+
+ ///
+ /// Process game event 0x0020 - Allegiance info.
+ /// Extracts monarch, patron, rank, followers from the allegiance tree.
+ /// Reference: TreeStats Character.cs:642-745
+ ///
+ internal static void ProcessAllegianceInfoMessage(NetworkMessageEventArgs e)
+ {
+ try
+ {
+ allegianceName = e.Message.Value("allegianceName");
+ allegianceSize = e.Message.Value("allegianceSize");
+ followers = e.Message.Value("followers");
+
+ monarch = new AllegianceInfoRecord();
+ patron = new AllegianceInfoRecord();
+
+ MessageStruct records = e.Message.Struct("records");
+ int currentId = CoreManager.Current.CharacterFilter.Id;
+ var parentMap = new Dictionary();
+ var recordMap = new Dictionary();
+
+ for (int i = 0; i < records.Count; i++)
+ {
+ var record = records.Struct(i);
+ int charId = record.Value("character");
+ int treeParent = record.Value("treeParent");
+
+ parentMap[charId] = treeParent;
+ recordMap[charId] = new AllegianceInfoRecord(
+ record.Value("name"),
+ record.Value("rank"),
+ record.Value("race"),
+ record.Value("gender"));
+
+ // Monarch: treeParent <= 1
+ if (treeParent <= 1)
+ {
+ monarch = recordMap[charId];
+ }
+ }
+
+ // Patron: parent of current character
+ if (parentMap.ContainsKey(currentId) && recordMap.ContainsKey(parentMap[currentId]))
+ {
+ patron = recordMap[parentMap[currentId]];
+ }
+
+ // Our rank from the record
+ if (recordMap.ContainsKey(currentId))
+ {
+ allegianceRank = recordMap[currentId].rank;
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[CharStats] Allegiance processing error: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Process game event 0x0013 - Character property data.
+ /// Extracts luminance from QWORD keys 6 and 7.
+ /// Reference: TreeStats Character.cs:582-640
+ ///
+ internal static void ProcessCharacterPropertyData(NetworkMessageEventArgs e)
+ {
+ try
+ {
+ MessageStruct props = e.Message.Struct("properties");
+ MessageStruct qwords = props.Struct("qwords");
+
+ for (int i = 0; i < qwords.Count; i++)
+ {
+ var tmpStruct = qwords.Struct(i);
+ long key = tmpStruct.Value("key");
+ long value = tmpStruct.Value("value");
+
+ if (key == 6) // AvailableLuminance
+ luminanceEarned = value;
+ else if (key == 7) // MaximumLuminance
+ luminanceTotal = value;
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[CharStats] Property processing error: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Process game event 0x0029 - Titles list.
+ /// Extracts current title ID.
+ /// Reference: TreeStats Character.cs:551-580
+ ///
+ internal static void ProcessTitlesMessage(NetworkMessageEventArgs e)
+ {
+ try
+ {
+ currentTitle = e.Message.Value("current");
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[CharStats] Title processing error: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Process game event 0x002b - Set title (when player changes title).
+ ///
+ internal static void ProcessSetTitleMessage(NetworkMessageEventArgs e)
+ {
+ try
+ {
+ currentTitle = e.Message.Value("title");
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[CharStats] Set title error: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Collect all character data and send via WebSocket.
+ /// Called on login (after delay) and every 10 minutes.
+ ///
+ internal static void CollectAndSend()
+ {
+ if (!PluginCore.WebSocketEnabled)
+ return;
+
+ try
+ {
+ var cf = CoreManager.Current.CharacterFilter;
+ var culture = new CultureInfo("en-US");
+
+ // --- Attributes ---
+ var attributes = new Dictionary();
+ foreach (var attr in cf.Attributes)
+ {
+ attributes[attr.Name.ToLower()] = new
+ {
+ @base = attr.Base,
+ creation = attr.Creation
+ };
+ }
+
+ // --- Vitals (base values) ---
+ var vitals = new Dictionary();
+ foreach (var vital in cf.Vitals)
+ {
+ vitals[vital.Name.ToLower()] = new
+ {
+ @base = vital.Base
+ };
+ }
+
+ // --- Skills ---
+ var skills = new Dictionary();
+ Decal.Filters.FileService fs = CoreManager.Current.FileService as Decal.Filters.FileService;
+ if (fs != null)
+ {
+ for (int i = 0; i < fs.SkillTable.Length; i++)
+ {
+ Decal.Interop.Filters.SkillInfo skillinfo = null;
+ try
+ {
+ skillinfo = cf.Underlying.get_Skill(
+ (Decal.Interop.Filters.eSkillID)fs.SkillTable[i].Id);
+
+ string name = skillinfo.Name.ToLower().Replace(" ", "_");
+ string training = skillinfo.Training.ToString();
+ // Training enum returns "eTrainSpecialized" etc, strip "eTrain" prefix
+ if (training.Length > 6)
+ training = training.Substring(6);
+
+ skills[name] = new
+ {
+ @base = skillinfo.Base,
+ training = training
+ };
+ }
+ finally
+ {
+ if (skillinfo != null)
+ {
+ Marshal.ReleaseComObject(skillinfo);
+ skillinfo = null;
+ }
+ }
+ }
+ }
+
+ // --- Allegiance ---
+ object allegiance = null;
+ if (allegianceName != null)
+ {
+ allegiance = new
+ {
+ name = allegianceName,
+ monarch = monarch.name != null ? new
+ {
+ name = monarch.name,
+ race = monarch.race,
+ rank = monarch.rank,
+ gender = monarch.gender
+ } : null,
+ patron = patron.name != null ? new
+ {
+ name = patron.name,
+ race = patron.race,
+ rank = patron.rank,
+ gender = patron.gender
+ } : null,
+ rank = allegianceRank,
+ followers = followers
+ };
+ }
+
+ // --- Build payload ---
+ var payload = new
+ {
+ type = "character_stats",
+ timestamp = DateTime.UtcNow.ToString("o"),
+ character_name = cf.Name,
+ level = cf.Level,
+ race = cf.Race,
+ gender = cf.Gender,
+ birth = cf.Birth.ToString(culture),
+ total_xp = cf.TotalXP,
+ unassigned_xp = cf.UnassignedXP,
+ skill_credits = cf.SkillPoints,
+ deaths = cf.Deaths,
+ luminance_earned = luminanceEarned >= 0 ? (long?)luminanceEarned : null,
+ luminance_total = luminanceTotal >= 0 ? (long?)luminanceTotal : null,
+ current_title = currentTitle >= 0 ? (int?)currentTitle : null,
+ attributes = attributes,
+ vitals = vitals,
+ skills = skills,
+ allegiance = allegiance
+ };
+
+ _ = WebSocket.SendCharacterStatsAsync(payload);
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[CharStats] Error collecting stats: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index 0c1df39..555baa8 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -35,10 +35,12 @@
TRACE;VVS_REFERENCED;DECAL_INTEROP
prompt
4
+ x86
+ true
- ..\..\..\..\Documents\Decal Plugins\UtilityBelt\0Harmony.dll
+ lib\0Harmony.dll
False
@@ -49,18 +51,18 @@
False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\Decal.FileService.dll
+ lib\Decal.FileService.dll
-
+
False
False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Core.DLL
+ lib\Decal.Interop.Core.DLL
False
-
+
False
False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Filters.DLL
+ lib\Decal.Interop.Filters.DLL
False
@@ -68,16 +70,16 @@
False
lib\Decal.Interop.Inject.dll
-
+
False
False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.D3DService.DLL
+ lib\Decal.Interop.D3DService.DLL
False
False
False
- ..\..\..\..\..\..\Program Files (x86)\Decal 3.0\.NET 4.0 PIA\Decal.Interop.Input.DLL
+ lib\Decal.Interop.Input.DLL
False
@@ -224,12 +226,12 @@
True
True
-
+
False
- bin\Debug\utank2-i.dll
+ lib\utank2-i.dll
- ..\..\..\..\..\..\Games\Decal Plugins\Virindi\VirindiChatSystem5\VCS5.dll
+ lib\VCS5.dll
lib\VirindiViewService.dll
@@ -333,6 +335,7 @@
+
@@ -354,24 +357,17 @@
-
- {FF7F5F6D-34E0-4B6F-B3BB-8141DE2EF732}
- 2
- 0
- 0
- primary
- False
+
+ lib\Decal.dll
False
-
-
- {572B87C4-93BD-46B3-A291-CD58181D25DC}
- 2
- 0
- 0
- primary
- False
+
+
+ lib\decalnet.dll
True
-
+
+
+
+
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index 7a5d24a..d784259 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -64,6 +64,7 @@ namespace MosswartMassacre
internal static Timer updateTimer;
private static Timer vitalsTimer;
private static System.Windows.Forms.Timer commandTimer;
+ private static Timer characterStatsTimer;
private static readonly Queue pendingCommands = new Queue();
public static bool RareMetaEnabled { get; set; } = true;
@@ -182,6 +183,8 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.CreateObject += OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += OnInventoryChange;
+ // Subscribe to server messages for allegiance/luminance/title data
+ CoreManager.Current.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch;
// Initialize VVS view after character login
ViewManager.ViewInit();
@@ -251,7 +254,8 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.CreateObject -= OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject -= OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject -= OnInventoryChange;
-
+ // Unsubscribe from server dispatch
+ CoreManager.Current.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch;
// Stop and dispose of the timers
if (updateTimer != null)
@@ -284,6 +288,15 @@ namespace MosswartMassacre
questStreamingTimer = null;
}
+ // Stop and dispose character stats timer
+ if (characterStatsTimer != null)
+ {
+ characterStatsTimer.Stop();
+ characterStatsTimer.Elapsed -= OnCharacterStatsUpdate;
+ characterStatsTimer.Dispose();
+ characterStatsTimer = null;
+ }
+
// Dispose quest manager
if (questManager != null)
{
@@ -403,6 +416,34 @@ namespace MosswartMassacre
WriteToChat($"[ERROR] Quest streaming initialization failed: {ex.Message}");
}
+ // Initialize character stats streaming
+ try
+ {
+ CharacterStats.Init();
+
+ // Start 10-minute character stats timer
+ characterStatsTimer = new Timer(600000); // 10 minutes
+ characterStatsTimer.Elapsed += OnCharacterStatsUpdate;
+ characterStatsTimer.AutoReset = true;
+ characterStatsTimer.Start();
+
+ // Send initial stats after 5-second delay (let CharacterFilter populate)
+ var initialDelay = new Timer(5000);
+ initialDelay.AutoReset = false;
+ initialDelay.Elapsed += (s, args) =>
+ {
+ CharacterStats.CollectAndSend();
+ ((Timer)s).Dispose();
+ };
+ initialDelay.Start();
+
+ WriteToChat("[OK] Character stats streaming initialized (10-min interval)");
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[ERROR] Character stats initialization failed: {ex.Message}");
+ }
+
}
#region Quest Streaming Methods
@@ -1161,6 +1202,50 @@ namespace MosswartMassacre
}
}
+ private static void OnCharacterStatsUpdate(object sender, ElapsedEventArgs e)
+ {
+ try
+ {
+ CharacterStats.CollectAndSend();
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[CharStats] Timer error: {ex.Message}");
+ }
+ }
+
+ private void EchoFilter_ServerDispatch(object sender, NetworkMessageEventArgs e)
+ {
+ try
+ {
+ if (e.Message.Type == 0xF7B0) // Game Event
+ {
+ int eventId = (int)e.Message["event"];
+
+ if (eventId == 0x0020) // Allegiance info
+ {
+ CharacterStats.ProcessAllegianceInfoMessage(e);
+ }
+ else if (eventId == 0x0013) // Login Character (properties)
+ {
+ CharacterStats.ProcessCharacterPropertyData(e);
+ }
+ else if (eventId == 0x0029) // Titles list
+ {
+ CharacterStats.ProcessTitlesMessage(e);
+ }
+ else if (eventId == 0x002b) // Set title
+ {
+ CharacterStats.ProcessSetTitleMessage(e);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[CharStats] ServerDispatch error: {ex.Message}");
+ }
+ }
+
private void CalculateKillsPerInterval()
{
double minutesElapsed = (DateTime.Now - statsStartTime).TotalMinutes;
diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs
index 50279d5..288b74d 100644
--- a/MosswartMassacre/WebSocket.cs
+++ b/MosswartMassacre/WebSocket.cs
@@ -296,6 +296,12 @@ namespace MosswartMassacre
await SendEncodedAsync(json, CancellationToken.None);
}
+ public static async Task SendCharacterStatsAsync(object statsData)
+ {
+ var json = JsonConvert.SerializeObject(statsData);
+ await SendEncodedAsync(json, CancellationToken.None);
+ }
+
public static async Task SendQuestDataAsync(string questName, string countdown)
{
var envelope = new
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index 8e18ba6..b78f6c8 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ
diff --git a/MosswartMassacre/lib/Decal.FileService.dll b/MosswartMassacre/lib/Decal.FileService.dll
new file mode 100644
index 0000000..85e6d85
Binary files /dev/null and b/MosswartMassacre/lib/Decal.FileService.dll differ
diff --git a/MosswartMassacre/lib/Decal.Interop.D3DService.DLL b/MosswartMassacre/lib/Decal.Interop.D3DService.DLL
new file mode 100644
index 0000000..dcf1559
Binary files /dev/null and b/MosswartMassacre/lib/Decal.Interop.D3DService.DLL differ
diff --git a/MosswartMassacre/lib/Decal.Interop.Filters.DLL b/MosswartMassacre/lib/Decal.Interop.Filters.DLL
new file mode 100644
index 0000000..0600e62
Binary files /dev/null and b/MosswartMassacre/lib/Decal.Interop.Filters.DLL differ
diff --git a/MosswartMassacre/lib/Decal.Interop.Input.DLL b/MosswartMassacre/lib/Decal.Interop.Input.DLL
new file mode 100644
index 0000000..b73bed2
Binary files /dev/null and b/MosswartMassacre/lib/Decal.Interop.Input.DLL differ
diff --git a/MosswartMassacre/lib/Decal.dll b/MosswartMassacre/lib/Decal.dll
new file mode 100644
index 0000000..bbe634c
Binary files /dev/null and b/MosswartMassacre/lib/Decal.dll differ
diff --git a/MosswartMassacre/lib/VCS5.dll b/MosswartMassacre/lib/VCS5.dll
new file mode 100644
index 0000000..022af65
Binary files /dev/null and b/MosswartMassacre/lib/VCS5.dll differ
diff --git a/MosswartMassacre/lib/decalnet.dll b/MosswartMassacre/lib/decalnet.dll
new file mode 100644
index 0000000..2657264
Binary files /dev/null and b/MosswartMassacre/lib/decalnet.dll differ