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