diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index f6eb87a..a94f31d 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -350,6 +350,7 @@
+
@@ -366,6 +367,7 @@
+
Shared\Spells\Spells.csv
diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs
index f7d2ba7..d58250a 100644
--- a/MosswartMassacre/PluginCore.cs
+++ b/MosswartMassacre/PluginCore.cs
@@ -148,11 +148,38 @@ namespace MosswartMassacre
private EquipmentCantripStateTracker _equipmentCantripStateTracker;
private NearbyObjectsTracker _nearbyObjectsTracker;
private VitalSharingTracker _vitalSharingTracker;
+ private static PluginCore _instance;
+ public static VitalSharingTracker VitalSharingTracker => _instance?._vitalSharingTracker;
+
+ public static void SetVitalSharingEnabled(bool enabled)
+ {
+ try
+ {
+ PluginSettings.Instance.VitalSharingEnabled = enabled;
+ var tracker = _instance?._vitalSharingTracker;
+ if (tracker == null) return;
+ if (enabled)
+ {
+ tracker.Start();
+ WriteToChat("[VitalShare] ENABLED");
+ }
+ else
+ {
+ tracker.Stop();
+ WriteToChat("[VitalShare] DISABLED");
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteToChat($"[VitalShare] Toggle error: {ex.Message}");
+ }
+ }
protected override void Startup()
{
try
{
+ _instance = this;
// Set MyHost - for hot reload scenarios, Host might be null
if (Host != null)
{
@@ -1149,7 +1176,7 @@ namespace MosswartMassacre
{
if (args.Length < 2)
{
- WriteToChat("Usage: /mm vitalsharing |tag remove |tags>");
+ WriteToChat("Usage: /mm vitalsharing ");
return;
}
@@ -1162,82 +1189,26 @@ namespace MosswartMassacre
WriteToChat("[VitalShare] WebSocket must be enabled first. Run /mm ws enable");
return;
}
- PluginSettings.Instance.VitalSharingEnabled = true;
- _vitalSharingTracker?.Start();
- WriteToChat("[VitalShare] ENABLED (will stream to Overlord)");
+ SetVitalSharingEnabled(true);
}
else if (sub == "off")
{
- PluginSettings.Instance.VitalSharingEnabled = false;
- _vitalSharingTracker?.Stop();
- WriteToChat("[VitalShare] DISABLED");
+ SetVitalSharingEnabled(false);
}
else if (sub == "status")
{
var enabled = PluginSettings.Instance.VitalSharingEnabled;
var active = _vitalSharingTracker?.IsActive == true;
- var tags = PluginSettings.Instance.VitalSharingTags;
- var tagStr = (tags == null || tags.Count == 0) ? "(none)" : string.Join(", ", tags);
- WriteToChat($"[VitalShare] enabled={enabled} active={active} tags={tagStr}");
+ var tag = PluginSettings.Instance.CharTag ?? "";
+ WriteToChat($"[VitalShare] enabled={enabled} active={active} tag='{tag}'");
}
- else if (sub == "tags")
+ else if (sub == "overlay")
{
- var tags = PluginSettings.Instance.VitalSharingTags;
- if (tags == null || tags.Count == 0)
- WriteToChat("[VitalShare] No tags set");
- else
- WriteToChat("[VitalShare] Tags: " + string.Join(", ", tags));
- }
- else if (sub == "tag")
- {
- if (args.Length < 4)
- {
- WriteToChat("Usage: /mm vitalsharing tag ");
- return;
- }
- var action = args[2].ToLowerInvariant();
- var name = args[3].Trim();
- if (string.IsNullOrEmpty(name))
- {
- WriteToChat("Tag name cannot be empty");
- return;
- }
- var tags = PluginSettings.Instance.VitalSharingTags ?? new System.Collections.Generic.List();
- if (action == "add")
- {
- if (!tags.Contains(name, StringComparer.OrdinalIgnoreCase))
- {
- tags.Add(name);
- PluginSettings.Instance.VitalSharingTags = tags;
- WriteToChat($"[VitalShare] Added tag: {name}");
- }
- else
- {
- WriteToChat($"[VitalShare] Tag already set: {name}");
- }
- }
- else if (action == "remove")
- {
- var match = tags.Find(t => t.Equals(name, StringComparison.OrdinalIgnoreCase));
- if (match != null)
- {
- tags.Remove(match);
- PluginSettings.Instance.VitalSharingTags = tags;
- WriteToChat($"[VitalShare] Removed tag: {name}");
- }
- else
- {
- WriteToChat($"[VitalShare] Tag not found: {name}");
- }
- }
- else
- {
- WriteToChat("Usage: /mm vitalsharing tag ");
- }
+ Views.VitalSharingOverlayView.ToggleWindow();
}
else
{
- WriteToChat("Usage: /mm vitalsharing |tag remove |tags>");
+ WriteToChat("Usage: /mm vitalsharing ");
}
}, "Enable/disable vital sharing with other MosswartOverlord clients");
diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs
index db0716a..90e14b1 100644
--- a/MosswartMassacre/PluginSettings.cs
+++ b/MosswartMassacre/PluginSettings.cs
@@ -25,7 +25,6 @@ namespace MosswartMassacre
private bool _verboseLogging = false;
private bool _autoUpdateEnabled = true;
private bool _vitalSharingEnabled = false;
- private List _vitalSharingTags = new List();
private ChestLooterSettings _chestLooterSettings = new ChestLooterSettings();
public static PluginSettings Instance => _instance
@@ -205,16 +204,6 @@ namespace MosswartMassacre
set { _vitalSharingEnabled = value; Save(); }
}
- public List VitalSharingTags
- {
- get
- {
- if (_vitalSharingTags == null) _vitalSharingTags = new List();
- return _vitalSharingTags;
- }
- set { _vitalSharingTags = value ?? new List(); Save(); }
- }
-
public ChestLooterSettings ChestLooterSettings
{
get
diff --git a/MosswartMassacre/ViewXML/mainViewTabbed.xml b/MosswartMassacre/ViewXML/mainViewTabbed.xml
index fdd5ed7..c958f39 100644
--- a/MosswartMassacre/ViewXML/mainViewTabbed.xml
+++ b/MosswartMassacre/ViewXML/mainViewTabbed.xml
@@ -38,10 +38,15 @@
+
+
+
+
-
-
-
+
+
+
+
diff --git a/MosswartMassacre/ViewXML/vitalSharingOverlay.xml b/MosswartMassacre/ViewXML/vitalSharingOverlay.xml
new file mode 100644
index 0000000..7913790
--- /dev/null
+++ b/MosswartMassacre/ViewXML/vitalSharingOverlay.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MosswartMassacre/Views/VVSTabbedMainView.cs b/MosswartMassacre/Views/VVSTabbedMainView.cs
index 97a5a3b..fe9573b 100644
--- a/MosswartMassacre/Views/VVSTabbedMainView.cs
+++ b/MosswartMassacre/Views/VVSTabbedMainView.cs
@@ -32,6 +32,8 @@ namespace MosswartMassacre.Views
private HudCheckBox chkRareMetaEnabled;
private HudCheckBox chkWebSocketEnabled;
private HudCheckBox chkAutoUpdateEnabled;
+ private HudCheckBox chkVitalSharingEnabled;
+ private HudButton btnVitalSharingOverlay;
private HudTextBox txtCharTag;
private HudTextBox txtVTankPath;
#endregion
@@ -229,9 +231,11 @@ namespace MosswartMassacre.Views
chkRareMetaEnabled = GetControl("chkRareMetaEnabled");
chkWebSocketEnabled = GetControl("chkWebSocketEnabled");
chkAutoUpdateEnabled = GetControl("chkAutoUpdateEnabled");
+ chkVitalSharingEnabled = GetControl("chkVitalSharingEnabled");
+ btnVitalSharingOverlay = GetControl("btnVitalSharingOverlay");
txtCharTag = GetControl("txtCharTag");
txtVTankPath = GetControl("txtVTankPath");
-
+
// Hook up settings events
if (chkRareMetaEnabled != null)
chkRareMetaEnabled.Change += OnRareMetaSettingChanged;
@@ -239,6 +243,10 @@ namespace MosswartMassacre.Views
chkWebSocketEnabled.Change += OnWebSocketSettingChanged;
if (chkAutoUpdateEnabled != null)
chkAutoUpdateEnabled.Change += OnAutoUpdateSettingChanged;
+ if (chkVitalSharingEnabled != null)
+ chkVitalSharingEnabled.Change += OnVitalSharingSettingChanged;
+ if (btnVitalSharingOverlay != null)
+ btnVitalSharingOverlay.Hit += OnVitalSharingOverlayClicked;
if (txtCharTag != null)
txtCharTag.Change += OnCharTagChanged;
if (txtVTankPath != null)
@@ -464,6 +472,30 @@ namespace MosswartMassacre.Views
}
}
+ private void OnVitalSharingSettingChanged(object sender, EventArgs e)
+ {
+ try
+ {
+ PluginCore.SetVitalSharingEnabled(chkVitalSharingEnabled.Checked);
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"Error in vital sharing setting change: {ex.Message}");
+ }
+ }
+
+ private void OnVitalSharingOverlayClicked(object sender, EventArgs e)
+ {
+ try
+ {
+ VitalSharingOverlayView.ToggleWindow();
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"Error opening vital sharing overlay: {ex.Message}");
+ }
+ }
+
private void OnVTankPathChanged(object sender, EventArgs e)
{
try
@@ -617,6 +649,8 @@ namespace MosswartMassacre.Views
{
if (PluginSettings.IsInitialized)
{
+ if (chkVitalSharingEnabled != null)
+ chkVitalSharingEnabled.Checked = PluginSettings.Instance.VitalSharingEnabled;
if (chkRareMetaEnabled != null)
chkRareMetaEnabled.Checked = PluginSettings.Instance.RareMetaEnabled;
if (chkWebSocketEnabled != null)
diff --git a/MosswartMassacre/Views/VitalSharingOverlayView.cs b/MosswartMassacre/Views/VitalSharingOverlayView.cs
new file mode 100644
index 0000000..b8a88e9
--- /dev/null
+++ b/MosswartMassacre/Views/VitalSharingOverlayView.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Drawing;
+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.
+ ///
+ internal class VitalSharingOverlayView : VVSBaseView
+ {
+ private static VitalSharingOverlayView _instance;
+
+ private HudList _peerList;
+ private System.Timers.Timer _refreshTimer;
+
+ // 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 VitalSharingOverlayView() : base(null)
+ {
+ CreateFromXMLResource("MosswartMassacre.ViewXML.vitalSharingOverlay.xml");
+ Init();
+ }
+
+ public static void ToggleWindow()
+ {
+ try
+ {
+ if (_instance == null)
+ {
+ _instance = new VitalSharingOverlayView();
+ _instance.Show();
+ }
+ else if (_instance.IsVisible)
+ {
+ _instance.Hide();
+ }
+ else
+ {
+ _instance.Show();
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[VitalShare] Overlay toggle error: {ex.Message}");
+ }
+ }
+
+ public static void ForceClose()
+ {
+ try
+ {
+ _instance?.Dispose();
+ _instance = null;
+ }
+ catch { }
+ }
+
+ private void Init()
+ {
+ try
+ {
+ _peerList = GetControl("lstPeers");
+ if (_peerList != 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);
+ }
+
+ _refreshTimer = new System.Timers.Timer(500);
+ _refreshTimer.AutoReset = true;
+ _refreshTimer.Elapsed += (s, e) =>
+ {
+ try { RefreshPeers(); }
+ catch (Exception ex) { PluginCore.WriteToChat($"[VitalShare] Overlay refresh error: {ex.Message}"); }
+ };
+ _refreshTimer.Start();
+
+ RefreshPeers();
+ }
+ catch (Exception ex)
+ {
+ PluginCore.WriteToChat($"[VitalShare] Overlay init error: {ex.Message}");
+ }
+ }
+
+ private void RefreshPeers()
+ {
+ if (_peerList == null) return;
+
+ var snaps = VitalSharingTracker.GetPeerSnapshots();
+
+ _peerList.ClearRows();
+
+ if (snaps == null || snaps.Length == 0)
+ {
+ 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;
+ }
+
+ Array.Sort(snaps, (a, b) =>
+ string.Compare(a.CharacterName, b.CharacterName, StringComparison.OrdinalIgnoreCase));
+
+ foreach (var p in snaps)
+ {
+ 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;
+ }
+ }
+
+ private static string FormatPct(int cur, int max)
+ {
+ 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 + "%";
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ try
+ {
+ if (_refreshTimer != null)
+ {
+ _refreshTimer.Stop();
+ _refreshTimer.Dispose();
+ _refreshTimer = null;
+ }
+ }
+ catch { }
+ }
+ base.Dispose(disposing);
+ if (_instance == this) _instance = null;
+ }
+ }
+}
diff --git a/MosswartMassacre/VitalSharingTracker.cs b/MosswartMassacre/VitalSharingTracker.cs
index d58c12f..2dd0ed0 100644
--- a/MosswartMassacre/VitalSharingTracker.cs
+++ b/MosswartMassacre/VitalSharingTracker.cs
@@ -61,6 +61,30 @@ namespace MosswartMassacre
public bool IsActive => _active;
+ // ── Peer snapshot state (used by the in-game overlay) ──
+ public class PeerSnapshot
+ {
+ public string CharacterName;
+ public int CurrentHealth, MaxHealth;
+ public int CurrentStamina, MaxStamina;
+ public int CurrentMana, MaxMana;
+ public DateTime LastUpdate;
+ }
+
+ private static readonly Dictionary _peers =
+ new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private static readonly object _peersLock = new object();
+
+ public static PeerSnapshot[] GetPeerSnapshots()
+ {
+ lock (_peersLock)
+ {
+ var arr = new PeerSnapshot[_peers.Count];
+ _peers.Values.CopyTo(arr, 0);
+ return arr;
+ }
+ }
+
public VitalSharingTracker(IPluginLogger logger)
{
_logger = logger;
@@ -187,6 +211,20 @@ namespace MosswartMassacre
int maxM = (int?)root["max_mana"] ?? 0;
if (playerId == 0 || maxH == 0) return;
+ // Cache for overlay view
+ lock (_peersLock)
+ {
+ if (!_peers.TryGetValue(fromChar, out var snap))
+ {
+ snap = new PeerSnapshot { CharacterName = fromChar };
+ _peers[fromChar] = snap;
+ }
+ snap.CurrentHealth = curH; snap.MaxHealth = maxH;
+ snap.CurrentStamina = curS; snap.MaxStamina = maxS;
+ snap.CurrentMana = curM; snap.MaxMana = maxM;
+ snap.LastUpdate = DateTime.UtcNow;
+ }
+
Enqueue(() =>
{
try
@@ -637,8 +675,10 @@ namespace MosswartMassacre
{
try
{
- var tags = PluginSettings.Instance.VitalSharingTags;
- return tags != null ? tags.ToArray() : new string[0];
+ // Reuse the existing CharTag setting as a single-value tag
+ var tag = PluginSettings.Instance.CharTag;
+ if (string.IsNullOrWhiteSpace(tag)) return new string[0];
+ return new[] { tag };
}
catch { return new string[0]; }
}
diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll
index 0c18f58..fe32eff 100644
Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ