using BsWidget.BeatSaberHttpStatus; using BsWidget.BsYurHttpStatus; using BsWidgetShareCodes; using OpenHardwareMonitor.Hardware; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; using System.Linq; using System.Windows.Forms; namespace BsWidget { internal class MainForm : BaseForm { private Timer _updateTimer; private Timer _pcTimer; private BlockingCollection _queue; private BeatSaberHttpStatusClient _client; private BsYurHttpStatusClient _yurClient; private Computer _computer; private Font _smallFont = new Font("", 12, FontStyle.Regular, GraphicsUnit.Pixel); private Font _mediumFont = new Font("", 20, FontStyle.Regular, GraphicsUnit.Pixel); [Flags] private enum UpdateFlags { BeatMap = 1 << 0, Performance = 1 << 1, NoteFullyCut = 1 << 2, ClearAll = 1 << 3, RefreshAll = 1 << 4, CpuAndGpu = 1 << 5, } public Image SongIcon { get; set; } = Properties.Resources.sample_cover; public string SongName { get; set; } = "Song Name"; public string SongSubName { get; set; } = "Song Sub Name"; public string SongArtist { get; set; } = "Song Artist"; public string BeatMapper { get; set; } = "Mapper"; public string Difficulty { get; set; } = "Difficulty"; public double SongBpm { get; set; } = 123; public double SongNjs { get; set; } = 23; public int CurrentScore { get; set; } = 12345; public int CurrentCombo { get; set; } = 120; public string CurrentRank { get; set; } = "XX"; public int CurrentMaxScore { get; set; } public int CurrentCutPerSecond { get; set; } public float? CpuUsage { get; set; } = 12.3f; public float? CpuGhz { get; set; } = 2.333f; public float? GpuUsage { get; set; } = 45.6f; public YurStatus YurStatus { get; set; } = new YurStatus { HeartRate = 0, KcalPerMin = 0 }; /// FinalScore,CurrentCombo public List CutHistory { get; set; } public class HistoryModel { public DateTime Time { get; set; } public int CutScore { get; set; } public int CurrentCombo { get; set; } public float Percent { get; set; } public int CutsPerSecond { get; set; } public float HeartRate { get; set; } public float KcalPerMin { get; set; } } public MainForm() { KeyPreview = true; FormBorderStyle = FormBorderStyle.None; _client = new BeatSaberHttpStatusClient(); _client.Event += BeatSaber_Event; _yurClient = new BsYurHttpStatusClient(); _yurClient.Event += Yur_Event; _computer = new Computer(); _computer.CPUEnabled = true; _computer.GPUEnabled = true; _computer.FanControllerEnabled = false; _computer.HDDEnabled = false; _computer.MainboardEnabled = false; _computer.RAMEnabled = false; _computer.Open(); _queue = new BlockingCollection(); CutHistory = new List(); _updateTimer = new Timer { Interval = 25 }; _updateTimer.Tick += UpdateTimer_Tick; _pcTimer = new Timer { Interval = 750 }; _pcTimer.Tick += PcTimer_Tick; } protected override void OnLoad(EventArgs e) { Text = "Beat Saber Status Widget"; base.OnLoad(e); Font = new Font("", 30, FontStyle.Bold, GraphicsUnit.Pixel); TopMost = false; WindowState = FormWindowState.Normal; _updateTimer.Start(); _pcTimer.Start(); _client.Start(); _yurClient.Start(); } protected override void OnShown(EventArgs e) { WindowState = FormWindowState.Normal; Location = Screen.PrimaryScreen.Bounds.Location; ClientSize = Screen.PrimaryScreen.Bounds.Size; TopMost = true; base.OnShown(e); if (_queue.Count == 0) { _queue.Add(UpdateFlags.RefreshAll); } } protected override void OnFormClosing(FormClosingEventArgs e) { _updateTimer.Stop(); _pcTimer.Stop(); base.OnFormClosing(e); } protected override void OnFormClosed(FormClosedEventArgs e) { _client.Stop(); _yurClient.Stop(); _computer.Close(); base.OnFormClosed(e); } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (e.KeyCode == Keys.Escape) Application.Exit(); } private void BeatSaber_Event(object sender, BeatSaberStatusEventArgs e) { var flags = (UpdateFlags)0; if (e.Event == "menu" && CutHistory.Count > 0) { lock (CutHistory) { CutHistory.Clear(); } flags |= UpdateFlags.ClearAll; } if (null != e.Status.Beatmap) { var bytes = Convert.FromBase64String(e.Status.Beatmap.SongCover); using var stream = new MemoryStream(bytes); var newImg = Image.FromStream(stream); var old = SongIcon; SongIcon = newImg; old?.Dispose(); SongName = e.Status.Beatmap.SongName; SongSubName = e.Status.Beatmap.SongSubName; SongArtist = e.Status.Beatmap.SongAuthorName; BeatMapper = e.Status.Beatmap.LevelAuthorName; if (string.IsNullOrEmpty(BeatMapper)) BeatMapper = "Unknown Mapper"; Difficulty = e.Status.Beatmap.Difficulty.ToUpper(); if (Difficulty == "EXPERTPLUS") Difficulty = "EXPERT+"; SongBpm = e.Status.Beatmap.SongBPM; SongNjs = e.Status.Beatmap.NoteJumpSpeed; flags |= UpdateFlags.BeatMap; } if (null != e.Status.Performance) { CurrentScore = e.Status.Performance.Score; CurrentCombo = e.Status.Performance.Combo; CurrentRank = e.Status.Performance.Rank; CurrentMaxScore = e.Status.Performance.CurrentMaxScore; flags |= UpdateFlags.Performance; } if (e.Event == "noteFullyCut" && null != e.NoteCut?.FinalScore) { lock (CutHistory) { var now = DateTime.Now; var preNow = now.AddSeconds(-1); CurrentCutPerSecond = CutHistory.Count(p => p.Time > preNow); CutHistory.Add(new HistoryModel { Time = now, CutScore = e.NoteCut.FinalScore.Value, CurrentCombo = CurrentCombo, Percent = (float)CurrentScore / CurrentMaxScore, CutsPerSecond = CurrentCutPerSecond, HeartRate = YurStatus.HeartRate, KcalPerMin = YurStatus.KcalPerMin, }); } flags |= UpdateFlags.NoteFullyCut; } if (flags == 0) { } else { _queue.Add(flags); } } private void Yur_Event(object sender, YurStatus e) => YurStatus = e; private void PcTimer_Tick(object sender, EventArgs e) { try { foreach (var hardware in _computer.Hardware.Where(p => p.HardwareType == HardwareType.CPU || p.HardwareType == HardwareType.GpuNvidia)) { hardware.Update(); } } catch (Exception) { return; } try { var mhz = _computer.Hardware.Where(p => p.HardwareType == HardwareType.CPU).SelectMany(p => p.Sensors) .Where(p => p.SensorType == SensorType.Clock).Select(p => p.Value).Max(); if (mhz.HasValue) { CpuGhz = mhz / 1000f; } else { CpuGhz = null; } } catch { CpuGhz = null; } try { CpuUsage = _computer.Hardware.Where(p => p.HardwareType == HardwareType.CPU).SelectMany(p => p.Sensors) .Where(p => p.SensorType == SensorType.Load && p.Name == "CPU Total").Select(p => p.Value).Max(); } catch { CpuUsage = null; } try { GpuUsage = _computer.Hardware.Where(p => p.HardwareType == HardwareType.GpuNvidia).SelectMany(p => p.Sensors) .Where(p => p.SensorType == SensorType.Load && p.Name == "GPU Core").Select(p => p.Value).Max(); } catch { GpuUsage = null; } _queue.Add(UpdateFlags.CpuAndGpu); } private void UpdateTimer_Tick(object sender, EventArgs e) { if (_queue.Count == 0) return; UpdateGraphic(); } protected override void RenderGraphic(Graphics g) { const int margin = 10; const int chHeight = 64; var chWidth = ViewSize.Width - margin * 2; if (_queue.Count == 0) return; var flags = _queue.Take(); if (flags.HasFlag(UpdateFlags.ClearAll)) g.Clear(Color.Transparent); g.SetHighQuality(); if (flags.HasFlag(UpdateFlags.NoteFullyCut) || flags.HasFlag(UpdateFlags.RefreshAll)) { const int chLeft = margin; const int chTop = margin; var chBottom = chTop + chHeight; float RecordPixel = 2.5f; var LineWidth = 1.5f; var displayCount = (int)(chWidth / RecordPixel); using var scorePen = new Pen(Color.FromArgb(200, 255, 0, 0), LineWidth) { LineJoin = LineJoin.Round }; using var comboPen = new Pen(Color.FromArgb(200, 0, 0, 255), LineWidth) { LineJoin = LineJoin.Round }; using var percentPen = new Pen(Color.FromArgb(200, Color.Green), LineWidth) { LineJoin = LineJoin.Round }; using var cpsPen = new Pen(Color.Yellow, LineWidth) { LineJoin = LineJoin.Round }; using var heartPen = new Pen(Color.FromArgb(200, Color.White), LineWidth) { LineJoin = LineJoin.Round }; using var calPen = new Pen(Color.FromArgb(200, Color.Lime), LineWidth) { LineJoin = LineJoin.Round }; //clear region 0,210 Wx200 g.ClearRect(Color.Transparent, 0, chTop - 3, ViewSize.Width, chHeight + 6 + 20); //draw chart using var bgBrush = new SolidBrush(Color.FromArgb(90, 255, 255, 255)); g.FillRectangle(bgBrush, chLeft, chTop, chWidth, chHeight); g.DrawRectangle(Pens.White, chLeft - 1, chTop - 1, chWidth + 2, chHeight + 2); if (CutHistory.Count > 1) { HistoryModel[] items; lock (CutHistory) { var skip = CutHistory.Count > displayCount ? CutHistory.Count - displayCount : 0; items = CutHistory.Skip(skip).ToArray(); } if (items.Length > 1) { var minScore = items.Select(p => p.CutScore).Min(); var maxScore = items.Select(p => p.CutScore).Max(); var rngScore = maxScore - minScore; if (rngScore < 1) rngScore = 1; var minCombo = items.Select(p => p.CurrentCombo).Min(); var maxCombo = items.Select(p => p.CurrentCombo).Max(); var rngCombo = maxCombo - minCombo; if (rngCombo < 1) rngCombo = 1; var minHea = items.Select(p => p.HeartRate).Min(); var maxHea = items.Select(p => p.HeartRate).Max(); var rngHea = maxHea - minHea; if (rngHea < 1) rngHea = 1; var minCal = items.Select(p => p.KcalPerMin).Min(); var maxCal = items.Select(p => p.KcalPerMin).Max(); var rngCal = maxCal - minCal; if (rngCal < 1) rngCal = 1; var minCps = items.Select(p => p.CutsPerSecond).Min(); var maxCps = items.Select(p => p.CutsPerSecond).Max(); var rngCps = maxCps - minCps; if (rngCps < 1) rngCps = 1; var x = chWidth - items.Length * RecordPixel + chLeft + 1; var scorePts = new List(); var comboPts = new List(); var percentPts = new List(); var heaPts = new List(); var calPts = new List(); var cpsPts = new List(); for (var i = 0; i < items.Length; i++) { var item = items[i]; scorePts.Add(new PointF(x + RecordPixel * i, (chBottom - 1) - (chHeight - 2) * ((float)(item.CutScore - minScore) / rngScore))); comboPts.Add(new PointF(x + RecordPixel * i, (chBottom - 1) - (chHeight - 2) * ((float)(item.CurrentCombo - minCombo) / rngCombo))); percentPts.Add(new PointF(x + RecordPixel * i, (chBottom - 1) - (chHeight - 2) * item.Percent)); cpsPts.Add(new PointF(x + RecordPixel * i, (chBottom - 1) - (chHeight - 2) * ((float)(item.CutsPerSecond - minCps) / rngCps))); heaPts.Add(new PointF(x + RecordPixel * i, (chBottom - 1) - (chHeight - 2) * ((item.HeartRate - minHea) / rngHea))); calPts.Add(new PointF(x + RecordPixel * i, (chBottom - 1) - (chHeight - 2) * ((item.KcalPerMin - minCal) / rngCal))); } g.DrawLines(scorePen, scorePts.ToArray()); g.DrawLines(comboPen, comboPts.ToArray()); g.DrawLines(percentPen, percentPts.ToArray()); g.DrawLines(cpsPen, cpsPts.ToArray()); g.DrawLines(heartPen, heaPts.ToArray()); g.DrawLines(calPen, calPts.ToArray()); } } } if (flags.HasFlag(UpdateFlags.Performance) || flags.HasFlag(UpdateFlags.RefreshAll)) { float x = margin; float y = chHeight + margin * 2; //clear region 0,180 Wx180 g.ClearRect(Color.Transparent, 0, y, ViewSize.Width, 50); //draw text var sz = g.DrawStringWithOutline($"{CurrentScore:N0}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; sz = g.DrawStringWithRoundedRect("POINT", _smallFont, x, y + margin * 1.5f, Brushes.White, Brushes.Red, 2.5f); x += sz.Width + margin * 2; sz = g.DrawStringWithOutline($"{CurrentCombo}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; sz = g.DrawStringWithRoundedRect("COMBO", _smallFont, x, y + margin * 1.5f, Brushes.White, Brushes.Blue, 2.5f); x += sz.Width + margin * 2; sz = g.DrawStringWithOutline($"{CurrentRank}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; sz = g.DrawStringWithRoundedRect("RANK", _smallFont, x, y + margin * 1.5f, Brushes.White, Brushes.Green, 2.5f); x += sz.Width + margin * 2; sz = g.DrawStringWithOutline($"{CurrentCutPerSecond}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; sz = g.DrawStringWithRoundedRect("CUT / SEC", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.Yellow, 2.5f); x += sz.Width + margin * 2; sz = g.DrawStringWithOutline($"{YurStatus.HeartRate:N1}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; sz = g.DrawStringWithRoundedRect("HEART", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.White, 2.5f); x += sz.Width + margin * 2; sz = g.DrawStringWithOutline($"{YurStatus.KcalPerMin:N1}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; // ReSharper disable once RedundantAssignment sz = g.DrawStringWithRoundedRect("KCAL / MIN", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.Lime, 2.5f); } if (flags.HasFlag(UpdateFlags.BeatMap) || flags.HasFlag(UpdateFlags.RefreshAll)) { TopMost = false; Application.DoEvents(); TopMost = true; Application.DoEvents(); BringToFront(); Application.DoEvents(); const int coverSize = 130; g.ClearRect(Color.Transparent, 0, ViewSize.Height - (coverSize + 100), ViewSize.Width, coverSize + 100); var coverLeft = margin; var coverTop = ViewSize.Height - coverSize - margin; g.DrawImage(SongIcon, coverLeft, coverTop, coverSize, coverSize); g.DrawRectangle(Pens.White, coverLeft, coverTop, coverSize, coverSize); g.DrawStringWithRoundedRect(Difficulty, Font, coverLeft, coverTop - 100, Brushes.Black, Brushes.White); { float x = margin; var y = coverTop - 50; var sz = g.DrawStringWithOutline($"{SongBpm}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; sz = g.DrawStringWithRoundedRect("BPM", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.White, 2.5f); x += sz.Width + margin * 2; sz = g.DrawStringWithOutline($"{SongNjs}", Font, x, y, Color.Black, Color.White); x += sz.Width + margin * 2; // ReSharper disable once RedundantAssignment sz = g.DrawStringWithRoundedRect("NJS", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.White, 2.5f); } { var bootom = ViewSize.Height - margin; var left = coverSize + margin * 2; var fontDeltaH = 1.3f; var sz = g.MeasureString(SongName, _mediumFont); g.DrawStringWithOutline(SongName, _mediumFont, left, bootom - sz.Height * fontDeltaH, Color.Black, Color.White); sz = g.MeasureString(SongSubName, _mediumFont); g.DrawStringWithOutline(SongSubName, _mediumFont, left, bootom - (sz.Height * fontDeltaH) * 2, Color.Black, Color.White); sz = g.MeasureString(SongArtist, _mediumFont); g.DrawStringWithOutline(SongArtist, _mediumFont, left, bootom - (sz.Height * fontDeltaH) * 3, Color.Black, Color.White); sz = g.MeasureString(BeatMapper, _mediumFont); g.DrawStringWithOutline(BeatMapper, _mediumFont, left, bootom - (sz.Height * fontDeltaH) * 4, Color.Black, Color.White); } } if (flags.HasFlag(UpdateFlags.CpuAndGpu) || flags.HasFlag(UpdateFlags.RefreshAll)) { var width = 700; var height = 50; var field = 400; var left = ViewSize.Width - width; var top = ViewSize.Height - height; g.ClearRect(Color.Transparent, left, top, width, height); g.DrawStringWithOutline($"CPU: {CpuGhz:N1}GHz {CpuUsage:N1}%", Font, left, top, Color.Black, Color.White); g.DrawStringWithOutline($"GPU: {GpuUsage:N0}%", Font, left + field, top, Color.Black, Color.White); } } } }