feat(vitalsharing): UB-style DxHud overlay + self in peer list
- Rewrite VitalSharingOverlayView as a DxHud drawn directly onto the game surface with the same row layout UB NetworkUI uses: full-width HP bar on top, Stamina + Mana bars side-by-side on the bottom, plus per-row border and name/percentage text. Colours use the sidebar palette (#ff4444 / #ffaa00 / #4488ff). - Always include the local character as the first row so the overlay shows something even before any peer traffic arrives. Previously the window said "(no peers)" until another subscribed client streamed vitals in. - VitalSharingTracker.GetSelfSnapshot() reads current vitals via CoreManager.Actions.Vital. - Drop the old HudList-based XML view (no longer used). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29a54b720f
commit
d220970130
5 changed files with 248 additions and 123 deletions
|
|
@ -367,7 +367,6 @@
|
|||
<EmbeddedResource Include="ViewXML\flagTracker.xml" />
|
||||
<EmbeddedResource Include="ViewXML\mainView.xml" />
|
||||
<EmbeddedResource Include="ViewXML\mainViewTabbed.xml" />
|
||||
<EmbeddedResource Include="ViewXML\vitalSharingOverlay.xml" />
|
||||
<EmbeddedResource Include="..\Shared\Spells\Spells.csv">
|
||||
<Link>Shared\Spells\Spells.csv</Link>
|
||||
</EmbeddedResource>
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<view icon="7735" title="Vital Sharing" width="360" height="260">
|
||||
<control progid="DecalControls.FixedLayout" clipped="">
|
||||
<control progid="DecalControls.StaticText" name="lblHeader" left="8" top="6" width="340" height="16" text="Vital Sharing Peers" style="FontBold"/>
|
||||
<control progid="DecalControls.StaticText" name="lblCol0" left="8" top="24" width="160" height="14" text="Character"/>
|
||||
<control progid="DecalControls.StaticText" name="lblCol1" left="170" top="24" width="55" height="14" text="HP"/>
|
||||
<control progid="DecalControls.StaticText" name="lblCol2" left="227" top="24" width="55" height="14" text="Stam"/>
|
||||
<control progid="DecalControls.StaticText" name="lblCol3" left="284" top="24" width="55" height="14" text="Mana"/>
|
||||
<control progid="DecalControls.List" name="lstPeers" left="6" top="42" width="340" height="200"/>
|
||||
</control>
|
||||
</view>
|
||||
|
|
@ -1,53 +1,57 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using Decal.Adapter;
|
||||
using VirindiViewService;
|
||||
using VirindiViewService.Controls;
|
||||
|
||||
namespace MosswartMassacre.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 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().
|
||||
/// </summary>
|
||||
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<HudList>("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<VitalSharingTracker.PeerSnapshot> BuildRows()
|
||||
{
|
||||
if (disposing)
|
||||
var rows = new List<VitalSharingTracker.PeerSnapshot>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,30 @@ namespace MosswartMassacre
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot for the local character (first row in the overlay so you
|
||||
/// always see yourself even before any peer traffic arrives).
|
||||
/// </summary>
|
||||
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;
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue