MainForm.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. using BsWidget.BeatSaberHttpStatus;
  2. using System;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.ComponentModel;
  6. using System.Drawing;
  7. using System.Drawing.Drawing2D;
  8. using System.Drawing.Text;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Windows.Forms;
  12. namespace BsWidget
  13. {
  14. internal class MainForm : BaseForm
  15. {
  16. private Timer _updateTimer;
  17. private BlockingCollection<UpdateFlags> _queue;
  18. private BeatSaberHttpStatusClient _client;
  19. [Flags]
  20. private enum UpdateFlags
  21. {
  22. BeatMap = 1 << 0,
  23. Performance = 1 << 1,
  24. NoteFullyCut = 1 << 2,
  25. ClearAll = 1 << 3,
  26. }
  27. public Image SongIcon { get; set; }
  28. public string SongName { get; set; }
  29. public string SongSubName { get; set; }
  30. public string SongArtist { get; set; }
  31. public string BeatMapper { get; set; }
  32. public string Difficulty { get; set; }
  33. public double SongBpm { get; set; }
  34. public double SongNjs { get; set; }
  35. public int CurrentScore { get; set; }
  36. public int CurrentCombo { get; set; }
  37. public string CurrentRank { get; set; }
  38. public int CurrentMaxScore { get; set; }
  39. /// <summary> FinalScore,CurrentCombo </summary>
  40. public List<Tuple<int, int>> CutHistory { get; set; }
  41. public MainForm()
  42. {
  43. KeyPreview = true;
  44. _client = new BeatSaberHttpStatusClient();
  45. _client.Event += BeatSaber_Event;
  46. Text = "Beat Saber Status Widget";
  47. _queue = new BlockingCollection<UpdateFlags>();
  48. CutHistory = new List<Tuple<int, int>>();
  49. _updateTimer = new Timer { Interval = 25 };
  50. _updateTimer.Tick += UpdateTimer_Tick;
  51. }
  52. protected override void OnLoad(EventArgs e)
  53. {
  54. base.OnLoad(e);
  55. Font = new Font("", 20, FontStyle.Bold, GraphicsUnit.Pixel);
  56. TopMost = true;
  57. WindowState = FormWindowState.Normal;
  58. Left = 0;
  59. Top = 0;
  60. ClientSize = new Size(1920 / 2, 1080 / 2);
  61. _updateTimer.Start();
  62. _client.Start();
  63. }
  64. protected override void OnClosing(CancelEventArgs e)
  65. {
  66. _client.Stop();
  67. _updateTimer.Stop();
  68. base.OnClosing(e);
  69. }
  70. protected override void OnKeyDown(KeyEventArgs e)
  71. {
  72. base.OnKeyDown(e);
  73. if (e.KeyCode == Keys.Escape) Application.Exit();
  74. }
  75. private void BeatSaber_Event(object sender, BeatSaberStatusEventArgs e)
  76. {
  77. if (e.Event == "noteMissed")
  78. {
  79. var bp = 0;
  80. }
  81. var flags = (UpdateFlags)0;
  82. if (e.Event == "menu" && CutHistory.Count > 0)
  83. {
  84. CutHistory.Clear();
  85. flags |= UpdateFlags.ClearAll;
  86. }
  87. if (null != e.Status.Beatmap)
  88. {
  89. var bytes = Convert.FromBase64String(e.Status.Beatmap.SongCover);
  90. using var stream = new MemoryStream(bytes);
  91. var newImg = Image.FromStream(stream);
  92. var old = SongIcon;
  93. SongIcon = newImg;
  94. old?.Dispose();
  95. SongName = e.Status.Beatmap.SongName;
  96. SongSubName = e.Status.Beatmap.SongSubName;
  97. SongArtist = e.Status.Beatmap.SongAuthorName;
  98. BeatMapper = e.Status.Beatmap.LevelAuthorName;
  99. if (string.IsNullOrEmpty(BeatMapper)) BeatMapper = "Unknown Mapper";
  100. Difficulty = e.Status.Beatmap.Difficulty;
  101. if (Difficulty == "ExpertPlus") Difficulty = "Expert+";
  102. SongBpm = e.Status.Beatmap.SongBPM;
  103. SongNjs = e.Status.Beatmap.NoteJumpSpeed;
  104. flags |= UpdateFlags.BeatMap;
  105. }
  106. if (null != e.Status.Performance)
  107. {
  108. CurrentScore = e.Status.Performance.Score;
  109. CurrentCombo = e.Status.Performance.Combo;
  110. CurrentRank = e.Status.Performance.Rank;
  111. CurrentMaxScore = e.Status.Performance.CurrentMaxScore;
  112. flags |= UpdateFlags.Performance;
  113. }
  114. if (e.Event == "noteFullyCut" && null != e.NoteCut?.FinalScore)
  115. {
  116. CutHistory.Add(new Tuple<int, int>(e.NoteCut.FinalScore.Value, CurrentCombo));
  117. flags |= UpdateFlags.NoteFullyCut;
  118. }
  119. if (flags == 0)
  120. {
  121. var bp = 0;
  122. }
  123. else
  124. {
  125. _queue.Add(flags);
  126. }
  127. }
  128. private void UpdateTimer_Tick(object sender, EventArgs e)
  129. {
  130. if (_queue.Count == 0) return;
  131. UpdateGraphic();
  132. }
  133. protected override void RenderGraphic(Graphics g)
  134. {
  135. if (_queue.Count == 0) return;
  136. var flags = _queue.Take();
  137. if (flags.HasFlag(UpdateFlags.ClearAll)) g.Clear(Color.Transparent);
  138. g.SetHighQuality();
  139. if (flags.HasFlag(UpdateFlags.BeatMap))
  140. {
  141. // Cover: 10,10 128x128
  142. // SongName: 148,10
  143. // SongSubName: 148,40
  144. // SongArtist: 148,70
  145. // Mapper: 148,100
  146. // Diff/BMP/NJS: 10,148
  147. //clear region 0,0 Wx180
  148. g.ClearRect(Color.Transparent, 0, 0, ViewSize.Width, 180);
  149. //draw cover and text
  150. g.DrawImage(SongIcon, 10, 10, 128, 128);
  151. g.DrawRectangle(Pens.White, 10, 10, 128, 128);
  152. g.DrawString(SongName, Font, Brushes.White, 148, 10);
  153. g.DrawString(SongSubName, Font, Brushes.White, 148, 40);
  154. g.DrawString(SongArtist, Font, Brushes.White, 148, 70);
  155. g.DrawString(BeatMapper, Font, Brushes.White, 148, 100);
  156. g.DrawString($"[{Difficulty}] {SongBpm} BPM {SongNjs} NJS", Font, Brushes.White, 10, 148);
  157. }
  158. if (flags.HasFlag(UpdateFlags.Performance))
  159. {
  160. //clear region 0,180 Wx180
  161. g.ClearRect(Color.Transparent, 0, 180, ViewSize.Width, 25);
  162. //draw text
  163. g.DrawString($"{CurrentScore:N0} POINTS {CurrentCombo} COMBO {CurrentRank} RANK", Font, Brushes.White, 10, 180);
  164. }
  165. if (flags.HasFlag(UpdateFlags.NoteFullyCut) && CutHistory.Count > 1)
  166. {
  167. const int ChLeft = 10;
  168. const int ChTop = 210;
  169. const int ChWidth = 320;
  170. const int ChHeight = 100;
  171. const int ChBottom = ChTop + ChHeight;
  172. const float RecordPixel = 2;
  173. const float LineWidth = 3f;
  174. const int DisplayCount = (int)(ChWidth / RecordPixel);
  175. using var scorePen = new Pen(Color.FromArgb(233, 255, 0, 0), LineWidth) { LineJoin = LineJoin.Round };
  176. using var comboPen = new Pen(Color.FromArgb(233, 0, 0, 255), LineWidth) { LineJoin = LineJoin.Round };
  177. //clear region 0,210 Wx200
  178. g.ClearRect(Color.Transparent, 0, ChTop - 3, ViewSize.Width, ChHeight + 6);
  179. //draw chart
  180. using var bgBrush = new SolidBrush(Color.FromArgb(90, 0, 0, 0));
  181. g.FillRectangle(bgBrush, ChLeft, ChTop, ChWidth, ChHeight);
  182. g.DrawRectangle(Pens.White, ChLeft - 1, ChTop - 1, ChWidth + 2, ChHeight + 2);
  183. var skip = CutHistory.Count > DisplayCount ? CutHistory.Count - DisplayCount : 0;
  184. var items = CutHistory.Skip(skip).ToArray();
  185. var minScore = items.Select(p => p.Item1).Min();
  186. var maxScore = items.Select(p => p.Item1).Max();
  187. var rngScore = maxScore - minScore;
  188. if (rngScore < 1) rngScore = 1;
  189. var minCombo = items.Select(p => p.Item2).Min();
  190. var maxCombo = items.Select(p => p.Item2).Max();
  191. var rngCombo = maxCombo - minCombo;
  192. if (rngCombo < 1) rngCombo = 1;
  193. var x = ChWidth - items.Length * RecordPixel + ChLeft + 1;
  194. var scorePts = new List<PointF>();
  195. var comboPts = new List<PointF>();
  196. for (int i = 0; i < items.Length; i++)
  197. {
  198. var item = items[i];
  199. scorePts.Add(new PointF(x + RecordPixel * i, (ChBottom - 1) - (ChHeight - 2) * ((float)(item.Item1 - minScore) / rngScore)));
  200. comboPts.Add(new PointF(x + RecordPixel * i, (ChBottom - 1) - (ChHeight - 2) * ((float)(item.Item2 - minCombo) / rngCombo)));
  201. }
  202. g.DrawLines(scorePen, scorePts.ToArray());
  203. g.DrawLines(comboPen, comboPts.ToArray());
  204. }
  205. }
  206. }
  207. internal static class GdiPlusExt
  208. {
  209. public static void ClearRect(this Graphics g, Color color, int x, int y, int w, int h)
  210. {
  211. g.SetClip(new Rectangle(x, y, w, h));
  212. g.Clear(color);
  213. g.ResetClip();
  214. }
  215. public static void SetHighQuality(this Graphics g)
  216. {
  217. g.CompositingMode = CompositingMode.SourceOver;
  218. g.InterpolationMode = InterpolationMode.HighQualityBicubic;
  219. g.PixelOffsetMode = PixelOffsetMode.HighQuality;
  220. g.SmoothingMode = SmoothingMode.HighQuality;
  221. g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
  222. }
  223. }
  224. }