MainForm.cs 9.3 KB

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