using BsWidget.BeatSaberHttpStatus; 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; [Flags] private enum UpdateFlags { BeatMap = 1 << 0, Performance = 1 << 1, NoteFullyCut = 1 << 2, ClearAll = 1 << 3, } public Image SongIcon { get; set; } public string SongName { get; set; } public string SongSubName { get; set; } public string SongArtist { get; set; } public string BeatMapper { get; set; } public string Difficulty { get; set; } public double SongBpm { get; set; } public double SongNjs { get; set; } public int CurrentScore { get; set; } public int CurrentCombo { get; set; } public string CurrentRank { get; set; } public int CurrentMaxScore { get; set; } /// FinalScore,CurrentCombo public List> CutHistory { get; set; } public MainForm() { _client = new BeatSaberHttpStatusClient(); _client.Event += BeatSaber_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("", 20, FontStyle.Bold, GraphicsUnit.Pixel); TopMost = true; WindowState = FormWindowState.Normal; Left = 0; Top = 0; ClientSize = new Size(1920 / 2, 1080 / 2); _updateTimer.Start(); _client.Start(); } protected override void OnClosing(CancelEventArgs e) { _client.Stop(); _updateTimer.Stop(); base.OnClosing(e); } 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) { 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; 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) { CutHistory.Add(new Tuple(e.NoteCut.FinalScore.Value, CurrentCombo)); flags |= UpdateFlags.NoteFullyCut; } if (flags == 0) { var bp = 0; } else { _queue.Add(flags); } } private void UpdateTimer_Tick(object sender, EventArgs e) { if (_queue.Count == 0) return; UpdateGraphic(); } protected override void RenderGraphic(Graphics g) { if (_queue.Count == 0) return; var flags = _queue.Take(); if (flags.HasFlag(UpdateFlags.ClearAll)) g.Clear(Color.Transparent); g.SetHighQuality(); if (flags.HasFlag(UpdateFlags.BeatMap)) { // Cover: 10,10 128x128 // SongName: 148,10 // SongSubName: 148,40 // SongArtist: 148,70 // Mapper: 148,100 // Diff/BMP/NJS: 10,148 //clear region 0,0 Wx180 g.ClearRect(Color.Transparent, 0, 0, ViewSize.Width, 180); //draw cover and text g.DrawImage(SongIcon, 10, 10, 128, 128); g.DrawRectangle(Pens.White, 10, 10, 128, 128); g.DrawString(SongName, Font, Brushes.White, 148, 10); g.DrawString(SongSubName, Font, Brushes.White, 148, 40); g.DrawString(SongArtist, Font, Brushes.White, 148, 70); g.DrawString(BeatMapper, Font, Brushes.White, 148, 100); g.DrawString($"[{Difficulty}] {SongBpm} BPM {SongNjs} NJS", Font, Brushes.White, 10, 148); } if (flags.HasFlag(UpdateFlags.Performance)) { //clear region 0,180 Wx180 g.ClearRect(Color.Transparent, 0, 180, ViewSize.Width, 20); //draw text g.DrawString($"{CurrentScore:N0} POINTS {CurrentCombo} COMBO {CurrentRank} RANK", Font, Brushes.White, 10, 180); } if (flags.HasFlag(UpdateFlags.NoteFullyCut) && CutHistory.Count > 1) { const int ChLeft = 10; const int ChTop = 210; const int ChWidth = 320; const int ChHeight = 100; const int ChBottom = ChTop + ChHeight; const float RecordPixel = 2; const float LineWidth = 3f; const int DisplayCount = (int)(ChWidth / RecordPixel); using var scorePen = new Pen(Color.FromArgb(233, 255, 0, 0), LineWidth) { LineJoin = LineJoin.Round }; using var comboPen = new Pen(Color.FromArgb(233, 0, 0, 255), LineWidth) { LineJoin = LineJoin.Round }; //clear region 0,210 Wx200 g.ClearRect(Color.Transparent, 0, ChTop - 1, ViewSize.Width, ChHeight + 2); //draw chart using var bgBrush = new SolidBrush(Color.FromArgb(90, 0, 0, 0)); g.FillRectangle(bgBrush, ChLeft, ChTop, ChWidth, ChHeight); g.DrawRectangle(Pens.White, ChLeft - 1, ChTop - 1, ChWidth + 2, ChHeight + 2); var skip = CutHistory.Count > DisplayCount ? CutHistory.Count - DisplayCount : 0; var items = CutHistory.Skip(skip).ToArray(); var minScore = items.Select(p => p.Item1).Min(); var maxScore = items.Select(p => p.Item1).Max(); var rngScore = maxScore - minScore; if (rngScore < 1) rngScore = 1; var minCombo = items.Select(p => p.Item2).Min(); var maxCombo = items.Select(p => p.Item2).Max(); var rngCombo = maxCombo - minCombo; if (rngCombo < 1) rngCombo = 1; var x = ChWidth - items.Length * RecordPixel + ChLeft; var scorePts = new List(); var comboPts = new List(); for (int i = 0; i < items.Length; i++) { var item = items[i]; scorePts.Add(new PointF(x + RecordPixel * i, (ChBottom - 1) - (ChHeight - 2) * ((float)(item.Item1 - minScore) / rngScore))); comboPts.Add(new PointF(x + RecordPixel * i, (ChBottom - 1) - (ChHeight - 2) * ((float)(item.Item2 - minCombo) / rngCombo))); } g.DrawLines(scorePen, scorePts.ToArray()); g.DrawLines(comboPen, comboPts.ToArray()); } } } internal static class GdiPlusExt { public static void ClearRect(this Graphics g, Color color, int x, int y, int w, int h) { g.SetClip(new Rectangle(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.ClearTypeGridFit; } } }