Browse Source

first commit

HOME 3 years ago
parent
commit
6044486a14

+ 25 - 0
BeatWidget.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30309.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BsWidget", "BsWidget\BsWidget.csproj", "{2D981A37-94D5-442D-A945-0D0F777345BC}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{2D981A37-94D5-442D-A945-0D0F777345BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2D981A37-94D5-442D-A945-0D0F777345BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2D981A37-94D5-442D-A945-0D0F777345BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2D981A37-94D5-442D-A945-0D0F777345BC}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {70C509FE-8C9E-400D-A529-CD55D3CF312E}
+	EndGlobalSection
+EndGlobal

+ 46 - 0
BsWidget/BaseForm.Designer.cs

@@ -0,0 +1,46 @@
+namespace BsWidget
+{
+    partial class BaseForm
+    {
+        /// <summary>
+        /// 必需的设计器变量。
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary>
+        /// 清理所有正在使用的资源。
+        /// </summary>
+        /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+            {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Windows 窗体设计器生成的代码
+
+        /// <summary>
+        /// 设计器支持所需的方法 - 不要修改
+        /// 使用代码编辑器修改此方法的内容。
+        /// </summary>
+        private void InitializeComponent()
+        {
+            this.SuspendLayout();
+            // 
+            // MainForm
+            // 
+            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
+            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+            this.ControlBox = false;
+            this.Name = "BaseForm";
+            this.ResumeLayout(false);
+
+        }
+
+        #endregion
+    }
+}
+

+ 171 - 0
BsWidget/BaseForm.cs

@@ -0,0 +1,171 @@
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+
+namespace BsWidget
+{
+    public abstract partial class BaseForm : Form
+    {
+        private const int WM_NCLBUTTONDOWN = 0xA1;
+        private const int HT_CAPTION = 0x2;
+        private const int WS_EX_LAYERED = 0x00080000;
+        public const byte AcSrcOver = 0x00;
+        public const byte AcSrcAlpha = 0x01;
+        public const int UlwAlpha = 0x00000002;
+
+        [StructLayout(LayoutKind.Sequential)]
+        public struct POINT
+        {
+            public int x;
+            public int y;
+
+            public POINT(int x, int y)
+            {
+                this.x = x;
+                this.y = y;
+            }
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        public struct SIZE
+        {
+            public int cx;
+            public int cy;
+
+            public SIZE(int cx, int cy)
+            {
+                this.cx = cx;
+                this.cy = cy;
+            }
+        }
+
+        [StructLayout(LayoutKind.Sequential, Pack = 1)]
+        public struct Blendfunction
+        {
+            public byte BlendOp;
+            public byte BlendFlags;
+            public byte SourceConstantAlpha;
+            public byte AlphaFormat;
+        }
+
+        [DllImport("user32", ExactSpelling = true, SetLastError = true)]
+        public static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst, ref POINT pptDst, ref SIZE psize,
+            IntPtr hdcSrc, ref POINT pprSrc, int crKey, ref Blendfunction pblend, int dwFlags);
+
+        [DllImport("user32", ExactSpelling = true, SetLastError = true)]
+        public static extern IntPtr GetDC(IntPtr hWnd);
+
+        [DllImport("user32", ExactSpelling = true)]
+        public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDc);
+
+        [DllImport("gdi32", ExactSpelling = true, SetLastError = true)]
+        public static extern IntPtr CreateCompatibleDC(IntPtr hDc);
+
+        [DllImport("gdi32", ExactSpelling = true, SetLastError = true)]
+        public static extern bool DeleteDC(IntPtr hdc);
+
+        [DllImport("gdi32", ExactSpelling = true)]
+        public static extern IntPtr SelectObject(IntPtr hDc, IntPtr hObject);
+
+        [DllImport("gdi32", ExactSpelling = true, SetLastError = true)]
+        public static extern bool DeleteObject(IntPtr hObject);
+
+        private Size _viewSize;
+        private Bitmap _buffedBitmap;
+
+        public BaseForm() => InitializeComponent();
+
+        protected override CreateParams CreateParams
+        {
+            get
+            {
+                var cp = base.CreateParams;
+                cp.ExStyle |= WS_EX_LAYERED;
+                return cp;
+            }
+        }
+
+        protected override void OnSizeChanged(EventArgs e)
+        {
+            _viewSize = Size;
+            base.OnSizeChanged(e);
+
+            var old = _buffedBitmap;
+            var sz = ViewSize;
+            _buffedBitmap = new Bitmap(sz.Width, sz.Height, PixelFormat.Format32bppArgb);
+            UpdateGraphic();
+            old?.Dispose();
+        }
+
+        public Size ViewSize
+        {
+            get { return _viewSize; }
+            protected set
+            {
+                if (value.Width > 0 && value.Height > 0)
+                {
+                    MinimumSize = value;
+                    MaximumSize = value;
+                    _viewSize = value;
+                }
+            }
+        }
+
+        protected void UpdateGraphic()
+        {
+            if (_buffedBitmap == null) return;
+
+            using (var g = Graphics.FromImage(_buffedBitmap))
+            {
+                RenderGraphic(g);
+            }
+
+            var screenDc = GetDC(IntPtr.Zero);
+            var memDc = CreateCompatibleDC(screenDc);
+            var hBitmap = IntPtr.Zero;
+            var oldBitmap = IntPtr.Zero;
+
+            try
+            {
+                hBitmap = _buffedBitmap.GetHbitmap(Color.FromArgb(0)); // 创建GDI位图句柄,效率较低
+                oldBitmap = SelectObject(memDc, hBitmap);
+
+                var topPos = new POINT(Left, Top);
+                var pointSource = new POINT(0, 0);
+                var size = new SIZE(_buffedBitmap.Width, _buffedBitmap.Height);
+                var blend = new Blendfunction
+                {
+                    BlendOp = AcSrcOver,
+                    BlendFlags = 0,
+                    SourceConstantAlpha = 255,
+                    AlphaFormat = AcSrcAlpha
+                };
+
+                UpdateLayeredWindow(
+                    Handle, screenDc
+                    , ref topPos, ref size
+                    , memDc, ref pointSource
+                    , 0, ref blend,
+                    UlwAlpha);
+            }
+            catch (Exception ex)
+            {
+                Debug.Print(ex.ToString());
+            }
+            finally
+            {
+                SelectObject(memDc, oldBitmap);
+                DeleteObject(hBitmap);
+                DeleteObject(oldBitmap);
+                ReleaseDC(IntPtr.Zero, screenDc);
+                DeleteDC(screenDc);
+                DeleteDC(memDc);
+            }
+        }
+
+        protected abstract void RenderGraphic(Graphics g);
+    }
+}

+ 120 - 0
BsWidget/BaseForm.resx

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>

+ 50 - 0
BsWidget/BeatSaberHttpStatus/BeatSaberHttpStatusClient.cs

@@ -0,0 +1,50 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using WebSocketSharp;
+
+namespace BsWidget.BeatSaberHttpStatus
+{
+    internal class BeatSaberHttpStatusClient
+    {
+        private bool _isRunning;
+        private WebSocket _webSocket;
+
+        public event EventHandler<BeatSaberStatusEventArgs> Event;
+
+        public void Start()
+        {
+            _isRunning = true;
+            _webSocket = new WebSocket("ws://localhost:6557/socket");
+            _webSocket.OnMessage += WebSocket_OnMessage;
+            _webSocket.OnClose += WebSocket_OnClose;
+            _webSocket.OnError += WebSocket_OnError;
+            _webSocket.Connect();
+        }
+
+        public void Stop()
+        {
+            _isRunning = false;
+            _webSocket.Close();
+        }
+
+        private void WebSocket_OnError(object sender, ErrorEventArgs e)
+        {
+            if (_isRunning) _webSocket.Connect();
+        }
+
+        private void WebSocket_OnClose(object sender, CloseEventArgs e)
+        {
+            if (_isRunning) _webSocket.Connect();
+        }
+
+        private void WebSocket_OnMessage(object sender, MessageEventArgs e)
+        {
+            //parse json
+            var evt = JsonConvert.DeserializeObject<BeatSaberStatusEventArgs>(e.Data);
+            OnEvent(evt);
+        }
+
+        protected virtual void OnEvent(BeatSaberStatusEventArgs e) => Event?.Invoke(this, e);
+    }
+}

+ 62 - 0
BsWidget/BeatSaberHttpStatus/BeatSaberStatusEventArgs.cs

@@ -0,0 +1,62 @@
+namespace BsWidget.BeatSaberHttpStatus
+{
+    internal class BeatSaberStatusEventArgs
+    {
+        public string Event { get; set; }
+
+        public BsStatus Status { get; set; }
+
+        public NoteCutInfo NoteCut { get; set; }
+    }
+
+    internal class NoteCutInfo
+    {
+        public int? FinalScore { get; set; }
+    }
+
+    internal class BsStatus
+    {
+        public GameStatus Game { get; set; }
+
+        public BeatmapStatus Beatmap { get; set; }
+
+        public PerformanceStatus Performance { get; set; }
+
+        public ModStatus Mod { get; set; }
+
+        public PlayerSettingsStatus PlayerSettings { get; set; }
+    }
+
+    internal class PlayerSettingsStatus
+    {
+    }
+
+    internal class ModStatus
+    {
+    }
+
+    internal class PerformanceStatus
+    {
+        public int Score { get; set; }
+        public int CurrentMaxScore { get; set; }
+        public string Rank { get; set; }
+        public int Combo { get; set; }
+    }
+
+    internal class BeatmapStatus
+    {
+        public string SongCover { get; set; }
+        public string SongName { get; set; }
+        public string SongSubName { get; set; }
+        public string SongAuthorName { get; set; }
+        public string LevelAuthorName { get; set; }
+        public string Difficulty { get; set; }
+        public double SongBPM { get; set; }
+        public double NoteJumpSpeed { get; set; }
+    }
+
+    internal class GameStatus
+    {
+        public string Scene { get; set; }
+    }
+}

+ 98 - 0
BsWidget/BsWidget.csproj

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="C:\NuGetLocalRepo\ILRepack.2.0.18\build\ILRepack.props" Condition="Exists('C:\NuGetLocalRepo\ILRepack.2.0.18\build\ILRepack.props')" />
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{2D981A37-94D5-442D-A945-0D0F777345BC}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <RootNamespace>BsWidget</RootNamespace>
+    <AssemblyName>BsWidget</AssemblyName>
+    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <Deterministic>true</Deterministic>
+    <NuGetPackageImportStamp>
+    </NuGetPackageImportStamp>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+  <PropertyGroup>
+    <RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
+      <HintPath>C:\NuGetLocalRepo\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Deployment" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Windows.Forms" />
+    <Reference Include="System.Xml" />
+    <Reference Include="websocket-sharp, Version=1.0.2.59611, Culture=neutral, PublicKeyToken=5660b08a1845a91e, processorArchitecture=MSIL">
+      <HintPath>C:\NuGetLocalRepo\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="BeatSaberHttpStatus\BeatSaberHttpStatusClient.cs" />
+    <Compile Include="BeatSaberHttpStatus\BeatSaberStatusEventArgs.cs" />
+    <Compile Include="BaseForm.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="BaseForm.Designer.cs">
+      <DependentUpon>BaseForm.cs</DependentUpon>
+    </Compile>
+    <Compile Include="MainForm.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <EmbeddedResource Include="BaseForm.resx">
+      <DependentUpon>BaseForm.cs</DependentUpon>
+    </EmbeddedResource>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <PostBuildEvent>if $(ConfigurationName) == Release setlocal enabledelayedexpansion enableextensions
+if $(ConfigurationName) == Release set DLL_LIST=
+if $(ConfigurationName) == Release for %25%25x in ($(TargetDir)*.dll) do set DLL_LIST=!DLL_LIST! "%25%25x"
+if $(ConfigurationName) == Release echo dlls: !DLL_LIST!
+
+if $(ConfigurationName) == Release if not exist "$(TargetDir)Packed" md "$(TargetDir)Packed"
+if $(ConfigurationName) == Release $(ILRepack) /ndebug "/out:$(TargetDir)Packed\$(TargetFileName)" "$(TargetPath)" !DLL_LIST!
+if $(ConfigurationName) == Release if exist "$(TargetDir)Packed\$(TargetFileName).config" del "$(TargetDir)Packed\$(TargetFileName).config"</PostBuildEvent>
+  </PropertyGroup>
+  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+    <PropertyGroup>
+      <ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
+    </PropertyGroup>
+    <Error Condition="!Exists('C:\NuGetLocalRepo\ILRepack.2.0.18\build\ILRepack.props')" Text="$([System.String]::Format('$(ErrorText)', 'C:\NuGetLocalRepo\ILRepack.2.0.18\build\ILRepack.props'))" />
+  </Target>
+</Project>

+ 267 - 0
BsWidget/MainForm.cs

@@ -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;
+        }
+    }
+}

+ 22 - 0
BsWidget/Program.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace BsWidget
+{
+    static class Program
+    {
+        /// <summary>
+        /// 应用程序的主入口点。
+        /// </summary>
+        [STAThread]
+        static void Main()
+        {
+            Application.EnableVisualStyles();
+            Application.SetCompatibleTextRenderingDefault(false);
+            Application.Run(new MainForm());
+        }
+    }
+}

+ 36 - 0
BsWidget/Properties/AssemblyInfo.cs

@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("BsWidget")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("BsWidget")]
+[assembly: AssemblyCopyright("Copyright ©  2020")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
+[assembly: Guid("2d981a37-94d5-442d-a945-0d0f777345bc")]
+
+// 程序集的版本信息由下列四个值组成: 
+//
+//      主版本
+//      次版本
+//      生成号
+//      修订号
+//
+//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
+//通过使用 "*",如下所示:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 6 - 0
BsWidget/packages.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="ILRepack" version="2.0.18" targetFramework="net461" />
+  <package id="Newtonsoft.Json" version="12.0.3" targetFramework="net461" />
+  <package id="WebSocketSharp" version="1.0.3-rc11" targetFramework="net461" />
+</packages>

+ 6 - 0
NuGet.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?> 
+<configuration> 
+  <config> 
+    <add key="repositorypath" value="C:\NuGetLocalRepo" /> 
+  </config> 
+</configuration>