MosswartOverlord/docs/plans/2026-02-26-plugin-character-stats-design.md
erik 9c91ed0afb Add plugin character stats streaming design document
Design for adding character_stats event to MosswartMassacre plugin,
covering data collection from CharacterFilter API, network message
interception for allegiance/luminance, and 10-minute timer send.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:30:37 +00:00

7 KiB

Plugin Character Stats Streaming - Design Document

Overview

Add character stats streaming to the MosswartMassacre Decal plugin. Sends a character_stats JSON payload via the existing WebSocket connection to MosswartOverlord on login and every 10 minutes.

Scope: MosswartMassacre plugin only. The backend (MosswartOverlord) already handles this event type.


Architecture

A new CharacterStats class collects data from three sources and sends it via the existing WebSocket:

  1. CharacterFilter API — level, XP, deaths, race, gender, birth, attributes, vitals, skills
  2. Network message interception — allegiance (event 0x0020), luminance & title (event 0x0013)
  3. Separate 10-minute timer — triggers the send, plus an immediate send on login

Uses Newtonsoft.Json with anonymous objects (same as existing SendVitalsAsync, SendChatTextAsync).


Data Sources

Data Decal API
Level CharacterFilter.Level
Deaths CharacterFilter.Deaths
Skill Credits CharacterFilter.SkillPoints
Total XP CharacterFilter.TotalXP (Int64)
Unassigned XP CharacterFilter.UnassignedXP (Int64)
Race CharacterFilter.Race (string)
Gender CharacterFilter.Gender (string)
Birth CharacterFilter.Birth (DateTime)
Attributes (6) CharacterFilter.Attributes collection — .Name, .Base, .Creation
Vitals (3) base CharacterFilter.Vitals collection — .Name, .Base
Skills (all) CharacterFilter.Underlying.get_Skill((eSkillID)fs.SkillTable[i].Id).Name, .Base, .Training
Allegiance EchoFilter.ServerDispatch → game event 0x0020 message fields
Luminance EchoFilter.ServerDispatch → game event 0x0013 QWORD keys 6, 7
Current Title EchoFilter.ServerDispatch → game event 0x0029 or 0x002b

Skill Access Pattern (from TreeStats reference)

Skills require COM interop with careful cleanup:

Decal.Filters.FileService fs = CoreManager.Current.FileService as Decal.Filters.FileService;
Decal.Interop.Filters.SkillInfo skillinfo = null;

for (int i = 0; i < fs.SkillTable.Length; ++i)
{
    try
    {
        skillinfo = CoreManager.Current.CharacterFilter.Underlying.get_Skill(
            (Decal.Interop.Filters.eSkillID)fs.SkillTable[i].Id);

        string name = skillinfo.Name.ToLower().Replace(" ", "_");
        string training = skillinfo.Training.ToString().Substring(6); // Strip "eTrain" prefix
        int baseValue = skillinfo.Base;
    }
    finally
    {
        if (skillinfo != null)
        {
            System.Runtime.InteropServices.Marshal.ReleaseComObject(skillinfo);
            skillinfo = null;
        }
    }
}

Allegiance Message Processing (from TreeStats reference)

Game event 0x0020 contains allegiance tree:

void ProcessAllegianceInfo(NetworkMessageEventArgs e)
{
    allegianceName = e.Message.Value<string>("allegianceName");
    allegianceSize = e.Message.Value<Int32>("allegianceSize");
    followers = e.Message.Value<Int32>("followers");

    MessageStruct records = e.Message.Struct("records");
    // Walk tree to find monarch (treeParent == 0) and patron (parent of current char)
}

Luminance from Character Properties (from TreeStats reference)

Game event 0x0013 contains QWORD properties:

// QWORD key 6 = AvailableLuminance (luminance_earned)
// QWORD key 7 = MaximumLuminance (luminance_total)

Data Flow

Login Complete
    ├──▶ Hook EchoFilter.ServerDispatch (allegiance + luminance/title)
    ├──▶ Start 10-minute characterStatsTimer
    └──▶ Send first stats after 5-second delay (let CharacterFilter populate)

Every 10 minutes (+ on login):
    CharacterStats.CollectAndSend()
        ├── CharacterFilter: level, XP, deaths, race, attributes, vitals
        ├── FileService + get_Skill(): all skills with training levels
        ├── Cached network data: allegiance, luminance, title
        ├── Build anonymous object with Newtonsoft.Json
        └── WebSocket.SendCharacterStatsAsync(payload)
                └── Existing WebSocket → /ws/position → MosswartOverlord

Network Messages (event-driven, cached for next stats send):
    0x0020 → allegiance name, monarch, patron, rank, followers
    0x0013 → luminance_earned, luminance_total
    0x0029 → current_title

JSON Payload

Matches the MosswartOverlord backend contract:

{
  "type": "character_stats",
  "timestamp": "2026-02-26T12:34:56Z",
  "character_name": "Barris",
  "level": 275,
  "race": "Aluvian",
  "gender": "Male",
  "birth": "2018-03-15 14:22:33",
  "total_xp": 191226310247,
  "unassigned_xp": 0,
  "skill_credits": 0,
  "luminance_earned": 500000,
  "luminance_total": 1500000,
  "deaths": 3175,
  "current_title": 42,
  "attributes": {
    "strength": {"base": 290, "creation": 100},
    "endurance": {"base": 200, "creation": 100},
    "coordination": {"base": 240, "creation": 100},
    "quickness": {"base": 220, "creation": 10},
    "focus": {"base": 250, "creation": 100},
    "self": {"base": 200, "creation": 100}
  },
  "vitals": {
    "health": {"base": 341},
    "stamina": {"base": 400},
    "mana": {"base": 300}
  },
  "skills": {
    "war_magic": {"base": 533, "training": "Specialized"},
    "melee_defense": {"base": 488, "training": "Specialized"},
    "life_magic": {"base": 440, "training": "Trained"},
    "arcane_lore": {"base": 10, "training": "Untrained"}
  },
  "allegiance": {
    "name": "Knights of Dereth",
    "monarch": {"name": "KingName", "race": 1, "rank": 0, "gender": 0},
    "patron": {"name": "PatronName", "race": 2, "rank": 5, "gender": 1},
    "rank": 10,
    "followers": 5
  }
}

Files Modified

File Changes
New: CharacterStats.cs Data collection, network message processing, allegiance/luminance caching
PluginCore.cs Hook EchoFilter.ServerDispatch, create 10-min timer, initial send on login, cleanup on shutdown
WebSocket.cs Add SendCharacterStatsAsync() method

Implementation Details

  • Skill COM cleanup: Every SkillInfo from get_Skill() must be released with Marshal.ReleaseComObject() in a finally block
  • Allegiance timing: Network message 0x0020 arrives asynchronously after login. First stats send may have null allegiance; subsequent sends will include it
  • Login delay: Wait 5 seconds after LoginComplete before first send to let CharacterFilter fully populate
  • Timer: New System.Timers.Timer at 600,000ms (10 min), separate from vitals timer
  • Error handling: Try/catch around entire collection — log errors, don't crash the plugin
  • Training string: skillinfo.Training.ToString() returns values like "eTrainSpecialized" — strip first 6 chars to get "Specialized"

What's NOT in Scope

  • UI changes in the plugin (no new tab/view)
  • Vassal list (just monarch + patron + rank + follower count)
  • Buffed skill values (base only, matching TreeStats)
  • Historical tracking (backend supports it, not a plugin concern)