diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index a94f31d..614e483 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -367,7 +367,6 @@
-
Shared\Spells\Spells.csv
diff --git a/MosswartMassacre/ViewXML/vitalSharingOverlay.xml b/MosswartMassacre/ViewXML/vitalSharingOverlay.xml
deleted file mode 100644
index 7913790..0000000
--- a/MosswartMassacre/ViewXML/vitalSharingOverlay.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MosswartMassacre/Views/VitalSharingOverlayView.cs b/MosswartMassacre/Views/VitalSharingOverlayView.cs
index b8a88e9..b90407e 100644
--- a/MosswartMassacre/Views/VitalSharingOverlayView.cs
+++ b/MosswartMassacre/Views/VitalSharingOverlayView.cs
@@ -1,53 +1,57 @@
using System;
+using System.Collections.Generic;
using System.Drawing;
+using Decal.Adapter;
using VirindiViewService;
-using VirindiViewService.Controls;
namespace MosswartMassacre.Views
{
///
- /// In-game overlay window showing live vitals for every character that has
- /// opted-in to vital sharing via MosswartOverlord. Modeled after UB's
- /// NetworkUI — one row per peer with HP/Stamina/Mana percentages coloured
- /// to match the web sidebar palette.
+ /// In-game vital sharing overlay — a DxHud drawn directly onto the game
+ /// surface so the visual matches UtilityBelt's NetworkUI (DrawCharNameBackground).
+ /// One row per peer (including the local character), showing name, HP bar
+ /// across the full row, then Stamina (left half) and Mana (right half) at
+ /// the bottom half of the row. Colours match the MosswartOverlord player
+ /// sidebar palette (red / orange / blue).
+ ///
+ /// Toggle with /mm vitalsharing overlay, via the Settings tab button, or
+ /// VitalSharingOverlayView.ToggleWindow().
///
- internal class VitalSharingOverlayView : VVSBaseView
+ internal static class VitalSharingOverlayView
{
- private static VitalSharingOverlayView _instance;
+ // Layout constants — identical to UB NetworkUI so rows line up the same.
+ private const int HUD_X_OFFSET = 50;
+ private const int HUD_Y_OFFSET = 200;
+ private const int ROW_SIZE = 20;
+ private const int HUD_WIDTH = 260;
+ private const int CHAR_NAME_WIDTH = 260;
+ private const int HEALTH_BAR_HEIGHT = 11;
- private HudList _peerList;
- private System.Timers.Timer _refreshTimer;
+ // Colors matched to MosswartOverlord player sidebar (#ff4444 / #ffaa00 / #4488ff)
+ private const int HUD_OPACITY = 200;
+ private static readonly Color COLOR_HP_FILL = Color.FromArgb(HUD_OPACITY, 255, 68, 68);
+ private static readonly Color COLOR_HP_BG = Color.FromArgb(HUD_OPACITY, 60, 0, 0);
+ private static readonly Color COLOR_STA_FILL = Color.FromArgb(HUD_OPACITY, 255, 170, 0);
+ private static readonly Color COLOR_STA_BG = Color.FromArgb(HUD_OPACITY, 70, 30, 0);
+ private static readonly Color COLOR_MANA_FILL = Color.FromArgb(HUD_OPACITY, 68, 136, 255);
+ private static readonly Color COLOR_MANA_BG = Color.FromArgb(HUD_OPACITY, 0, 20, 55);
+ private static readonly Color COLOR_BORDER = Color.Black;
+ private static readonly Color COLOR_TEXT = Color.White;
+ private static readonly Color COLOR_TEXT_DARK = Color.FromArgb(255, 0, 0, 0);
- // Colors matched to MosswartOverlord player sidebar
- // health: #ff4444, stamina: #ffaa00, mana: #4488ff
- private static readonly Color COLOR_HP = Color.FromArgb(255, 68, 68);
- private static readonly Color COLOR_STA = Color.FromArgb(255, 170, 0);
- private static readonly Color COLOR_MANA = Color.FromArgb(68, 136, 255);
- private static readonly Color COLOR_NAME = Color.White;
+ private static DxHud _hud;
+ private static System.Windows.Forms.Timer _redrawTimer;
+ private static bool _visible;
+ private static int _lastRowCount;
- private VitalSharingOverlayView() : base(null)
- {
- CreateFromXMLResource("MosswartMassacre.ViewXML.vitalSharingOverlay.xml");
- Init();
- }
+ public static bool IsVisible => _visible;
public static void ToggleWindow()
{
try
{
- if (_instance == null)
- {
- _instance = new VitalSharingOverlayView();
- _instance.Show();
- }
- else if (_instance.IsVisible)
- {
- _instance.Hide();
- }
- else
- {
- _instance.Show();
- }
+ if (_visible) Hide();
+ else Show();
}
catch (Exception ex)
{
@@ -55,117 +59,226 @@ namespace MosswartMassacre.Views
}
}
- public static void ForceClose()
+ public static void Show()
{
try
{
- _instance?.Dispose();
- _instance = null;
- }
- catch { }
- }
-
- private void Init()
- {
- try
- {
- _peerList = GetControl("lstPeers");
- if (_peerList != null)
+ if (_visible && _hud != null)
{
- // columns: name, hp%, sta%, mana%
- _peerList.AddColumn(typeof(HudStaticText), 160, null);
- _peerList.AddColumn(typeof(HudStaticText), 55, null);
- _peerList.AddColumn(typeof(HudStaticText), 55, null);
- _peerList.AddColumn(typeof(HudStaticText), 55, null);
+ _hud.Enabled = true;
+ return;
}
- _refreshTimer = new System.Timers.Timer(500);
- _refreshTimer.AutoReset = true;
- _refreshTimer.Elapsed += (s, e) =>
+ var rows = Math.Max(1, CountRows());
+ var size = new Size(HUD_WIDTH, rows * ROW_SIZE + 5);
+ _hud = new DxHud(new Point(HUD_X_OFFSET, HUD_Y_OFFSET), size, 0)
{
- try { RefreshPeers(); }
- catch (Exception ex) { PluginCore.WriteToChat($"[VitalShare] Overlay refresh error: {ex.Message}"); }
+ Enabled = true,
+ Alpha = 255,
};
- _refreshTimer.Start();
+ _visible = true;
+ _lastRowCount = rows;
- RefreshPeers();
+ if (_redrawTimer == null)
+ {
+ _redrawTimer = new System.Windows.Forms.Timer();
+ _redrawTimer.Interval = 250; // 4 fps is plenty for vital bars
+ _redrawTimer.Tick += (s, e) => Render();
+ }
+ _redrawTimer.Start();
+
+ Render();
}
catch (Exception ex)
{
- PluginCore.WriteToChat($"[VitalShare] Overlay init error: {ex.Message}");
+ PluginCore.WriteToChat($"[VitalShare] Overlay show error: {ex.Message}");
}
}
- private void RefreshPeers()
+ public static void Hide()
{
- if (_peerList == null) return;
-
- var snaps = VitalSharingTracker.GetPeerSnapshots();
-
- _peerList.ClearRows();
-
- if (snaps == null || snaps.Length == 0)
+ try
{
- var row = _peerList.AddRow();
- ((HudStaticText)row[0]).Text = "(no peers)";
- ((HudStaticText)row[0]).TextColor = Color.Gray;
- ((HudStaticText)row[1]).Text = "";
- ((HudStaticText)row[2]).Text = "";
- ((HudStaticText)row[3]).Text = "";
- return;
+ _redrawTimer?.Stop();
+ if (_hud != null)
+ {
+ _hud.Enabled = false;
+ _hud.Dispose();
+ _hud = null;
+ }
+ _visible = false;
}
-
- Array.Sort(snaps, (a, b) =>
- string.Compare(a.CharacterName, b.CharacterName, StringComparison.OrdinalIgnoreCase));
-
- foreach (var p in snaps)
+ catch (Exception ex)
{
- var row = _peerList.AddRow();
-
- var name = (HudStaticText)row[0];
- name.Text = p.CharacterName;
- name.TextColor = COLOR_NAME;
-
- var hp = (HudStaticText)row[1];
- hp.Text = FormatPct(p.CurrentHealth, p.MaxHealth);
- hp.TextColor = COLOR_HP;
-
- var sta = (HudStaticText)row[2];
- sta.Text = FormatPct(p.CurrentStamina, p.MaxStamina);
- sta.TextColor = COLOR_STA;
-
- var mana = (HudStaticText)row[3];
- mana.Text = FormatPct(p.CurrentMana, p.MaxMana);
- mana.TextColor = COLOR_MANA;
+ PluginCore.WriteToChat($"[VitalShare] Overlay hide error: {ex.Message}");
}
}
- private static string FormatPct(int cur, int max)
+ private static int CountRows()
{
- if (max <= 0) return "--";
- int pct = (int)Math.Round(100.0 * cur / max);
- if (pct < 0) pct = 0;
- if (pct > 100) pct = 100;
- return pct + "%";
+ int n = VitalSharingTracker.GetPeerSnapshots()?.Length ?? 0;
+ // self is always drawn as the first row
+ return n + 1;
}
- protected override void Dispose(bool disposing)
+ private static List BuildRows()
{
- if (disposing)
+ var rows = new List();
+ var self = VitalSharingTracker.GetSelfSnapshot();
+ if (self != null) rows.Add(self);
+
+ var peers = VitalSharingTracker.GetPeerSnapshots();
+ if (peers != null)
{
+ Array.Sort(peers, (a, b) =>
+ string.Compare(a.CharacterName, b.CharacterName, StringComparison.OrdinalIgnoreCase));
+ foreach (var p in peers)
+ {
+ if (self != null && string.Equals(p.CharacterName, self.CharacterName, StringComparison.OrdinalIgnoreCase))
+ continue;
+ rows.Add(p);
+ }
+ }
+ return rows;
+ }
+
+ private static void Render()
+ {
+ try
+ {
+ if (_hud == null || !_visible) return;
+
+ var rows = BuildRows();
+ if (rows.Count == 0) return;
+
+ // Resize the hud if the row count changed
+ if (rows.Count != _lastRowCount)
+ {
+ _lastRowCount = rows.Count;
+ // DxHud has no resize; re-create if size changed
+ var loc = _hud.Location;
+ _hud.Dispose();
+ _hud = new DxHud(loc, new Size(HUD_WIDTH, rows.Count * ROW_SIZE + 5), 0)
+ {
+ Enabled = true,
+ Alpha = 255,
+ };
+ }
+
+ var tex = _hud.Texture;
+ tex.BeginRender();
try
{
- if (_refreshTimer != null)
+ // Clear background
+ tex.Fill(new Rectangle(0, 0, HUD_WIDTH, rows.Count * ROW_SIZE + 5), Color.Transparent);
+
+ int offset = 0;
+ foreach (var p in rows)
{
- _refreshTimer.Stop();
- _refreshTimer.Dispose();
- _refreshTimer = null;
+ DrawRowBackground(tex, offset, p);
+ offset += ROW_SIZE;
}
+
+ // Text passes
+ tex.BeginText("Arial", 7, 100, false, 1, 255);
+ try
+ {
+ offset = 0;
+ foreach (var p in rows)
+ {
+ DrawRowText(tex, offset, p);
+ offset += ROW_SIZE;
+ }
+ }
+ finally { tex.EndText(); }
+
+ tex.BeginText("Arial", 5, 100, false);
+ try
+ {
+ offset = 0;
+ foreach (var p in rows)
+ {
+ DrawRowTextSmall(tex, offset, p);
+ offset += ROW_SIZE;
+ }
+ }
+ finally { tex.EndText(); }
+ }
+ finally
+ {
+ tex.EndRender();
}
- catch { }
}
- base.Dispose(disposing);
- if (_instance == this) _instance = null;
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[VitalShare] Overlay render error: {ex.Message}");
+ }
+ }
+
+ private static double Ratio(int cur, int max)
+ {
+ if (max <= 0) return 0;
+ var r = (double)cur / max;
+ if (r < 0) return 0;
+ if (r > 1) return 1;
+ return r;
+ }
+
+ private static void DrawRowBackground(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
+ {
+ var area = new Rectangle(1, offset + 1, HUD_WIDTH - 2, ROW_SIZE - 2);
+ double hp = Ratio(p.CurrentHealth, p.MaxHealth);
+ double sta = Ratio(p.CurrentStamina, p.MaxStamina);
+ double mana = Ratio(p.CurrentMana, p.MaxMana);
+
+ // Health bar (full row width, top half)
+ tex.Fill(new Rectangle(area.Left, area.Top, CHAR_NAME_WIDTH, HEALTH_BAR_HEIGHT), COLOR_HP_BG);
+ tex.Fill(new Rectangle(area.Left, area.Top, (int)(CHAR_NAME_WIDTH * hp), HEALTH_BAR_HEIGHT), COLOR_HP_FILL);
+
+ // Stamina bar (left half, bottom)
+ int halfW = CHAR_NAME_WIDTH / 2;
+ int bottomH = area.Height - HEALTH_BAR_HEIGHT;
+ tex.Fill(new Rectangle(area.Left, area.Top + HEALTH_BAR_HEIGHT, halfW, bottomH), COLOR_STA_BG);
+ tex.Fill(new Rectangle(area.Left, area.Top + HEALTH_BAR_HEIGHT, (int)(halfW * sta), bottomH), COLOR_STA_FILL);
+
+ // Mana bar (right half, bottom)
+ tex.Fill(new Rectangle(area.Left + halfW, area.Top + HEALTH_BAR_HEIGHT, halfW, bottomH), COLOR_MANA_BG);
+ tex.Fill(new Rectangle(area.Left + halfW, area.Top + HEALTH_BAR_HEIGHT, (int)(halfW * mana), bottomH), COLOR_MANA_FILL);
+
+ // Outer border
+ var tl = new PointF(area.Left, area.Top);
+ var tr = new PointF(area.Left + CHAR_NAME_WIDTH, area.Top);
+ var br = new PointF(area.Left + CHAR_NAME_WIDTH, area.Top + area.Height);
+ var bl = new PointF(area.Left, area.Top + area.Height);
+ tex.DrawLine(tl, tr, COLOR_BORDER, 1);
+ tex.DrawLine(tr, br, COLOR_BORDER, 1);
+ tex.DrawLine(br, bl, COLOR_BORDER, 1);
+ tex.DrawLine(bl, tl, COLOR_BORDER, 1);
+ }
+
+ private static void DrawRowText(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
+ {
+ var area = new Rectangle(0, offset, HUD_WIDTH, ROW_SIZE);
+ // name (top-left)
+ tex.WriteText(p.CharacterName, COLOR_TEXT, WriteTextFormats.SingleLine,
+ new Rectangle(area.X + 4, area.Y + 1, CHAR_NAME_WIDTH, ROW_SIZE));
+ // hp % (top-right)
+ int hpPct = p.MaxHealth > 0 ? (int)Math.Round(100.0 * p.CurrentHealth / p.MaxHealth) : 0;
+ tex.WriteText($"{hpPct}%", COLOR_TEXT, WriteTextFormats.Right,
+ new Rectangle(area.X + 4, area.Y + 1, CHAR_NAME_WIDTH - 8, ROW_SIZE));
+ }
+
+ private static void DrawRowTextSmall(DxTexture tex, int offset, VitalSharingTracker.PeerSnapshot p)
+ {
+ var area = new Rectangle(0, offset, HUD_WIDTH, ROW_SIZE);
+ int staPct = p.MaxStamina > 0 ? (int)Math.Round(100.0 * p.CurrentStamina / p.MaxStamina) : 0;
+ int manaPct = p.MaxMana > 0 ? (int)Math.Round(100.0 * p.CurrentMana / p.MaxMana) : 0;
+ // stamina % bottom-left
+ tex.WriteText($"{staPct}%", COLOR_TEXT_DARK, WriteTextFormats.SingleLine,
+ new Rectangle(area.X + 4, area.Y + HEALTH_BAR_HEIGHT, CHAR_NAME_WIDTH, ROW_SIZE));
+ // mana % bottom-right-half
+ tex.WriteText($"{manaPct}%", COLOR_TEXT_DARK, WriteTextFormats.SingleLine,
+ new Rectangle(area.X + 4 + (CHAR_NAME_WIDTH / 2), area.Y + HEALTH_BAR_HEIGHT, CHAR_NAME_WIDTH, ROW_SIZE));
}
}
}
diff --git a/MosswartMassacre/VitalSharingTracker.cs b/MosswartMassacre/VitalSharingTracker.cs
index 2dd0ed0..2a8a3fc 100644
--- a/MosswartMassacre/VitalSharingTracker.cs
+++ b/MosswartMassacre/VitalSharingTracker.cs
@@ -85,6 +85,30 @@ namespace MosswartMassacre
}
}
+ ///
+ /// Snapshot for the local character (first row in the overlay so you
+ /// always see yourself even before any peer traffic arrives).
+ ///
+ public static PeerSnapshot GetSelfSnapshot()
+ {
+ try
+ {
+ var actions = CoreManager.Current.Actions;
+ return new PeerSnapshot
+ {
+ CharacterName = CoreManager.Current.CharacterFilter?.Name ?? "",
+ CurrentHealth = actions.Vital[VitalType.CurrentHealth],
+ MaxHealth = actions.Vital[VitalType.MaximumHealth],
+ CurrentStamina = actions.Vital[VitalType.CurrentStamina],
+ MaxStamina = actions.Vital[VitalType.MaximumStamina],
+ CurrentMana = actions.Vital[VitalType.CurrentMana],
+ MaxMana = actions.Vital[VitalType.MaximumMana],
+ LastUpdate = DateTime.UtcNow,
+ };
+ }
+ catch { return null; }
+ }
+
public VitalSharingTracker(IPluginLogger logger)
{
_logger = logger;
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index fe32eff..5e1fb33 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ