|
@@ -0,0 +1,267 @@
|
|
|
+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<UpdateFlags> _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; }
|
|
|
+
|
|
|
+ /// <summary> FinalScore,CurrentCombo </summary>
|
|
|
+ public List<Tuple<int, int>> CutHistory { get; set; }
|
|
|
+
|
|
|
+ public MainForm()
|
|
|
+ {
|
|
|
+ _client = new BeatSaberHttpStatusClient();
|
|
|
+ _client.Event += BeatSaber_Event;
|
|
|
+
|
|
|
+ Text = "Beat Saber Status Widget";
|
|
|
+ _queue = new BlockingCollection<UpdateFlags>();
|
|
|
+ CutHistory = new List<Tuple<int, int>>();
|
|
|
+
|
|
|
+ _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<int, int>(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<PointF>();
|
|
|
+ var comboPts = new List<PointF>();
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|