using BsWidget.BeatSaberHttpStatus; using BsWidget.BsYurHttpStatus; using BsWidgetShareCodes; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Text; using System.IO; using System.Linq; using System.Windows.Forms; namespace BsWidget { internal class MainForm : BaseForm { private Timer _updateTimer; private BlockingCollection _queue; private BeatSaberHttpStatusClient _client; private BsYurHttpStatusClient _yurClient; 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, } 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 YurStatus YurStatus { get; set; } = new YurStatus { HeartRate = 0, KcalPerMin = 0 }; /// FinalScore,CurrentCombo public List CutHistory { get; set; } public class HistoryModel { public int CutScore { get; set; } public int CurrentCombo { get; set; } public float Percent { 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; Text = "Beat Saber Status Widget"; _queue = new BlockingCollection(); CutHistory = new List(); _updateTimer = new Timer { Interval = 25 }; _updateTimer.Tick += UpdateTimer_Tick; } protected override void OnLoad(EventArgs e) { base.OnLoad(e); Font = new Font("", 30, FontStyle.Bold, GraphicsUnit.Pixel); TopMost = false; WindowState = FormWindowState.Normal; _updateTimer.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 OnClosing(CancelEventArgs e) { _client.Stop(); _updateTimer.Stop(); base.OnClosing(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) { if (e.Event == "noteMissed") { var bp = 0; } 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) { CutHistory.Add(new HistoryModel { CutScore = e.NoteCut.FinalScore.Value, CurrentCombo = CurrentCombo, Percent = (float)CurrentScore / CurrentMaxScore, HeartRate = YurStatus.HeartRate, KcalPerMin = YurStatus.KcalPerMin, }); } flags |= UpdateFlags.NoteFullyCut; } if (flags == 0) { var bp = 0; } else { _queue.Add(flags); } } private void Yur_Event(object sender, YurStatus e) => YurStatus = e; private void UpdateTimer_Tick(object sender, EventArgs e) { if (_queue.Count == 0) return; UpdateGraphic(); } protected override void RenderGraphic(Graphics g) { const int margin = 10; var ChWidth = ViewSize.Width - margin * 2; var ChHeight = 64; if (_queue.Count == 0) return; var flags = _queue.Take(); if (flags.HasFlag(UpdateFlags.ClearAll)) g.Clear(Color.Transparent); g.SetHighQuality(); float nextY = 0; if (flags.HasFlag(UpdateFlags.NoteFullyCut) || flags.HasFlag(UpdateFlags.RefreshAll)) { var ChLeft = margin; var ChTop = margin; var ChBottom = ChTop + ChHeight; float RecordPixel = 5; var LineWidth = 2f; 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 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 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(); 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)); 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(heartPen, heaPts.ToArray()); g.DrawLines(calPen, calPts.ToArray()); } } nextY = ChHeight + margin * 2; } 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($"{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; 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)) { const int coverSize = 130; g.ClearRect(Color.Transparent, 0, ViewSize.Height - (coverSize + 100), ViewSize.Width, coverSize + 100); var coverLeft = ViewSize.Width - coverSize - margin * 2; var coverTop = ViewSize.Height - coverSize - margin * 2; g.DrawImage(SongIcon, coverLeft, coverTop, coverSize, coverSize); g.DrawRectangle(Pens.White, coverLeft, coverTop, coverSize, coverSize); g.DrawStringWithRoundedRect(Difficulty, Font, coverLeft - 20, coverTop - 100, Brushes.Black, Brushes.White); { float x = coverLeft - 80; 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; sz = g.DrawStringWithRoundedRect("NJS", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.White, 2.5f); } { var bootom = ViewSize.Height - margin * 1.5f; var right = coverLeft - margin; var fontDeltaW = 1.3f; var fontDeltaH = 1.4f; var sz = g.MeasureString(SongName, _mediumFont); g.DrawStringWithOutline(SongName, _mediumFont, right - sz.Width * fontDeltaW, bootom - sz.Height * fontDeltaH, Color.Black, Color.White); sz = g.MeasureString(SongSubName, _mediumFont); g.DrawStringWithOutline(SongSubName, _mediumFont, right - sz.Width * fontDeltaW, bootom - (sz.Height * fontDeltaH) * 2, Color.Black, Color.White); sz = g.MeasureString(SongArtist, _mediumFont); g.DrawStringWithOutline(SongArtist, _mediumFont, right - sz.Width * fontDeltaW, bootom - (sz.Height * fontDeltaH) * 3, Color.Black, Color.White); sz = g.MeasureString(BeatMapper, _mediumFont); g.DrawStringWithOutline(BeatMapper, _mediumFont, right - sz.Width * fontDeltaW, bootom - (sz.Height * fontDeltaH) * 4, Color.Black, Color.White); } } if (flags.HasFlag(UpdateFlags.Performance) || flags.HasFlag(UpdateFlags.RefreshAll)) { // //clear region 0,180 Wx180 // g.ClearRect(Color.Transparent, 0, nextY + margin, ViewSize.Width, fontHeight); // //draw text // float x = margin; // float y = nextY + margin; // 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.Black, // Brushes.White, 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.Black, // Brushes.White, 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.Black, Brushes.White, // 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; // sz = g.DrawStringWithRoundedRect("KCAL/MIN", _smallFont, x, y + margin * 1.5f, Brushes.Black, // Brushes.White, 2.5f); } if (flags.HasFlag(UpdateFlags.BeatMap) || flags.HasFlag(UpdateFlags.RefreshAll)) { // //clear region // g.ClearRect(Color.Transparent, 0, 0, ViewSize.Width, margin + coverSize + margin); // //draw cover and text // g.DrawImage(SongIcon, margin, margin, coverSize, coverSize); // g.DrawRectangle(Pens.White, 10, 10, coverSize, coverSize); // float x = coverSize + margin; // var y = margin; // var sz = g.DrawStringWithOutline(SongName, Font, x, y, Color.Black, Color.White); // x += sz.Width + margin * 2; // sz = g.DrawStringWithRoundedRect(Difficulty, Font, x, y + 5, Brushes.Black, Brushes.White); // g.DrawRoundedRectangle(Pens.Black, new RectangleF(x + 2, y + 7, sz.Width - 4, sz.Height - 4), 8); // x += sz.Width + margin; // 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; // sz = g.DrawStringWithRoundedRect("NJS", _smallFont, x, y + margin * 1.5f, Brushes.Black, Brushes.White, // 2.5f); // x += sz.Width + margin; // x = coverSize + margin; // y = margin; // g.DrawStringWithOutline(SongSubName, Font, x, y + fontHeight, Color.Black, Color.White); // g.DrawStringWithOutline(SongArtist, Font, x, y + fontHeight * 2, Color.Black, Color.White); // g.DrawStringWithOutline(BeatMapper, Font, x, y + fontHeight * 3, Color.Black, Color.White); } 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, fontHeight); // //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("PERCENT", _smallFont, x, y + margin * 1.5f, Brushes.White, // Brushes.Green, 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; // sz = g.DrawStringWithRoundedRect("KCAL/MIN", _smallFont, x, y + margin * 1.5f, Brushes.Black, // Brushes.Lime, 2.5f); } } } internal static class GdiPlusExt { public static void ClearRect(this Graphics g, Color color, float x, float y, float w, float h) { g.SetClip(new RectangleF(x, y, w, h)); g.Clear(color); g.ResetClip(); } public static void SetHighQuality(this Graphics g) { g.CompositingMode = CompositingMode.SourceOver; g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.SmoothingMode = SmoothingMode.HighQuality; g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; } public static SizeF DrawStringWithOutline(this Graphics g, string text, Font font, float x, float y, Color outlineColor, Color fillColor, float outlineWidth = 1) { // assuming g is the Graphics object on which you want to draw the text using var p = new GraphicsPath(); p.AddString( text, // text to draw font.FontFamily, // or any other font family (int)FontStyle.Regular, // font style (bold, italic, etc.) g.DpiY * font.Size / 72, // em size new PointF(x, y), // location where to draw text new StringFormat()); // set options here (e.g. center alignment) using var outlinePen = new Pen(outlineColor, outlineWidth); using var fillBrush = new SolidBrush(fillColor); g.FillPath(fillBrush, p); g.DrawPath(outlinePen, p); return p.GetBounds().Size; } public static void DrawRoundedRectangle(this Graphics graphics, Pen pen, RectangleF bounds, int cornerRadius) { if (graphics == null) throw new ArgumentNullException("graphics"); if (pen == null) throw new ArgumentNullException("pen"); using var path = RoundedRect(bounds, cornerRadius); graphics.DrawPath(pen, path); } public static void FillRoundedRectangle(this Graphics graphics, Brush brush, RectangleF bounds, float cornerRadius) { if (graphics == null) throw new ArgumentNullException("graphics"); if (brush == null) throw new ArgumentNullException("brush"); using var path = RoundedRect(bounds, cornerRadius); graphics.FillPath(brush, path); } public static SizeF DrawStringWithRoundedRect(this Graphics g, string text, Font font, float x, float y, Brush textbBrushe, Brush bgBrush, float radus = 10) { var sz = g.MeasureString(text, font); g.FillRoundedRectangle(bgBrush, new RectangleF(x, y, sz.Width, sz.Height), radus); g.DrawString(text, font, textbBrushe, x, y); return sz; } private static GraphicsPath RoundedRect(RectangleF bounds, float radius) { var diameter = radius * 2; var size = new SizeF(diameter, diameter); var arc = new RectangleF(bounds.Location, size); var path = new GraphicsPath(); if (radius == 0) { path.AddRectangle(bounds); return path; } // top left arc path.AddArc(arc, 180, 90); // top right arc arc.X = bounds.Right - diameter; path.AddArc(arc, 270, 90); // bottom right arc arc.Y = bounds.Bottom - diameter; path.AddArc(arc, 0, 90); // bottom left arc arc.X = bounds.Left; path.AddArc(arc, 90, 90); path.CloseFigure(); return path; } } }