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