├── .gitignore ├── DanmakuFrostMaster ├── AssParser.cs ├── BaseTypeDef.cs ├── BilibiliDanmakuXmlParser.cs ├── DanmakuFrostMaster.cs ├── DanmakuFrostMaster.csproj ├── DanmakuRender.cs ├── DanmakuYSlotManager.cs ├── Logger.cs └── Properties │ ├── AssemblyInfo.cs │ └── DanmakuFrostMaster.rd.xml ├── DanmakuFrostMasterDemo ├── App.xaml ├── App.xaml.cs ├── AssDemoPage.xaml ├── AssDemoPage.xaml.cs ├── Assets │ ├── LockScreenLogo.scale-200.png │ ├── SplashScreen.scale-200.png │ ├── Square150x150Logo.scale-200.png │ ├── Square44x44Logo.scale-200.png │ ├── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── StoreLogo.png │ └── Wide310x150Logo.scale-200.png ├── BilibiliDemo1Page.xaml ├── BilibiliDemo1Page.xaml.cs ├── BilibiliDemo2Page.xaml ├── BilibiliDemo2Page.xaml.cs ├── DanmakuDemoPage.xaml ├── DanmakuDemoPage.xaml.cs ├── DanmakuFrostMasterDemo.csproj ├── DanmakuFrostMasterDemo.sln ├── DemoFiles │ ├── demo_2.mp4 │ ├── demo_2_danmaku.xml │ ├── demo_3.mp4 │ ├── demo_3_danmaku.xml │ ├── demo_3_en-us.sub │ ├── demo_3_ja-jp.sub │ └── demo_3_zh-t.sub ├── DemoScreenshots │ ├── demo1.png │ ├── demo2-1.png │ ├── demo2-2.png │ ├── demo3.png │ └── demo4.png ├── MainPage.xaml ├── MainPage.xaml.cs ├── Package.appxmanifest └── Properties │ ├── AssemblyInfo.cs │ └── Default.rd.xml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/AssParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using Windows.UI; 8 | 9 | namespace Atelier39 10 | { 11 | public static class AssParser 12 | { 13 | public static List GetDanmakuList(string assStr) 14 | { 15 | try 16 | { 17 | List list = new List(); 18 | Dictionary styleDict = new Dictionary(); 19 | 20 | using (StringReader stringReader = new StringReader(assStr)) 21 | { 22 | int videoWidth = 0; 23 | int videoHeight = 0; 24 | int dialogueFormatSegmentCount = 10; 25 | 26 | string line = stringReader.ReadLine(); 27 | while (line != null) 28 | { 29 | if (!string.IsNullOrWhiteSpace(line)) 30 | { 31 | if (line.StartsWith("PlayResX:")) 32 | { 33 | int.TryParse(line.Substring(9).Trim(), out videoWidth); 34 | } 35 | else if (line.StartsWith("PlayResY:")) 36 | { 37 | int.TryParse(line.Substring(9).Trim(), out videoHeight); 38 | } 39 | else if (line.StartsWith("Style:")) 40 | { 41 | try 42 | { 43 | line = line.Substring(6).Trim(); 44 | string[] splitArray = line.Split(','); 45 | if (splitArray.Length >= 22) 46 | { 47 | string styleName = splitArray[0]; 48 | if (!styleDict.ContainsKey(styleName)) 49 | { 50 | styleDict.Add(styleName, new AssStyle()); 51 | } 52 | AssStyle style = styleDict[styleName]; 53 | 54 | style.FontFamilyName = splitArray[1]; 55 | float.TryParse(splitArray[2], out style.FontSize); 56 | 57 | Match textColorMatch = Regex.Match(splitArray[3], "[A-Fa-f0-9]{8}"); 58 | if (textColorMatch.Success) 59 | { 60 | string colorStr = textColorMatch.Value; 61 | if (TryParseHexToByte(colorStr.Substring(0, 2), out byte a) 62 | && TryParseHexToByte(colorStr.Substring(6, 2), out byte r) 63 | && TryParseHexToByte(colorStr.Substring(4, 2), out byte g) 64 | && TryParseHexToByte(colorStr.Substring(2, 2), out byte b)) 65 | { 66 | style.Alpha = (byte)(byte.MaxValue - a); 67 | style.TextColor = Color.FromArgb(byte.MaxValue, r, g, b); 68 | } 69 | } 70 | Match outlineColorMatch = Regex.Match(splitArray[5], "[A-Fa-f0-9]{8}"); 71 | if (outlineColorMatch.Success) 72 | { 73 | string colorStr = outlineColorMatch.Value; 74 | if (TryParseHexToByte(colorStr.Substring(0, 2), out byte a) 75 | && TryParseHexToByte(colorStr.Substring(6, 2), out byte r) 76 | && TryParseHexToByte(colorStr.Substring(4, 2), out byte g) 77 | && TryParseHexToByte(colorStr.Substring(2, 2), out byte b)) 78 | { 79 | style.OutlineColor = Color.FromArgb(byte.MaxValue, r, g, b); 80 | } 81 | } 82 | 83 | style.IsBold = splitArray[7] == "-1"; 84 | style.HasOutline = splitArray[15] == "1"; 85 | float.TryParse(splitArray[16], out style.OutlineSize); 86 | 87 | if (int.TryParse(splitArray[18], out int positionMode)) 88 | { 89 | style.AlignmentMode = (DanmakuAlignmentMode)positionMode; 90 | } 91 | if (int.TryParse(splitArray[19], out int marginLeft)) 92 | { 93 | style.MarginLeft = marginLeft; 94 | } 95 | if (int.TryParse(splitArray[20], out int marginRight)) 96 | { 97 | style.MarginRight = marginRight; 98 | } 99 | if (int.TryParse(splitArray[21], out int marginBottom)) 100 | { 101 | style.MarginBottom = marginBottom; 102 | } 103 | } 104 | } 105 | catch (Exception ex) 106 | { 107 | Debug.WriteLine($"Failed to parse ASS style ({line}): {ex.Message}"); 108 | } 109 | } 110 | else if (line.StartsWith("[Events]")) 111 | { 112 | line = stringReader.ReadLine(); 113 | if (line.StartsWith("Format:")) 114 | { 115 | dialogueFormatSegmentCount = line.Split(',').Length; 116 | } 117 | } 118 | else if (line.StartsWith("Dialogue:")) 119 | { 120 | try 121 | { 122 | line = line.Substring(8).Trim(); 123 | string[] splitArray = line.Split(','); 124 | string subtitleText = splitArray.Last().Trim(); 125 | if (splitArray.Length > dialogueFormatSegmentCount) 126 | { 127 | subtitleText = splitArray[dialogueFormatSegmentCount - 1]; 128 | for (int i = dialogueFormatSegmentCount; i < splitArray.Length; i++) 129 | { 130 | subtitleText = string.Concat(subtitleText, ",", splitArray[i]); 131 | } 132 | } 133 | 134 | if (!string.IsNullOrWhiteSpace(subtitleText)) 135 | { 136 | DanmakuItem danmakuItem = new DanmakuItem(); 137 | danmakuItem.AllowDensityControl = false; 138 | danmakuItem.Mode = DanmakuMode.Advanced; 139 | 140 | if (TimeSpan.TryParse(splitArray[1], out TimeSpan startTime) && TimeSpan.TryParse(splitArray[2], out TimeSpan endTime) && endTime.TotalMilliseconds > 0) 141 | { 142 | danmakuItem.StartMs = (uint)Math.Max(startTime.TotalMilliseconds, 0); 143 | danmakuItem.DurationMs = (uint)endTime.TotalMilliseconds - danmakuItem.StartMs; 144 | 145 | string styleName = splitArray[3]; 146 | if (styleName.StartsWith("*")) 147 | { 148 | styleName = styleName.Substring(1); 149 | } 150 | AssStyle style = styleDict.ContainsKey(styleName) ? styleDict[styleName] : new AssStyle(); 151 | danmakuItem.StartAlpha = style.Alpha; 152 | danmakuItem.EndAlpha = style.Alpha; 153 | danmakuItem.IsBold = style.IsBold; 154 | danmakuItem.HasOutline = style.HasOutline; 155 | danmakuItem.BaseFontSize = style.FontSize; 156 | danmakuItem.OutlineSize = style.OutlineSize; 157 | danmakuItem.FontFamilyName = style.FontFamilyName; 158 | danmakuItem.TextColor = style.TextColor; 159 | danmakuItem.OutlineColor = style.OutlineColor; 160 | danmakuItem.MarginLeft = style.MarginLeft; 161 | danmakuItem.MarginRight = style.MarginRight; 162 | danmakuItem.MarginBottom = style.MarginBottom; 163 | danmakuItem.AlignmentMode = style.AlignmentMode; 164 | 165 | if (int.TryParse(splitArray[5], out int marginLeft) && marginLeft != 0) 166 | { 167 | danmakuItem.MarginLeft = marginLeft; 168 | } 169 | if (int.TryParse(splitArray[6], out int marginRight) && marginRight != 0) 170 | { 171 | danmakuItem.MarginRight = marginRight; 172 | } 173 | if (int.TryParse(splitArray[7], out int marginBottom) && marginBottom != 0) 174 | { 175 | danmakuItem.MarginBottom = marginBottom; 176 | } 177 | 178 | if (subtitleText.StartsWith("{") && subtitleText.Contains("}") && !subtitleText.EndsWith("}")) 179 | { 180 | Match fnMatch = Regex.Match(subtitleText, @"\\fn(?.+?)[\\\}]"); 181 | if (fnMatch.Success) 182 | { 183 | danmakuItem.FontFamilyName = fnMatch.Groups["font"].Value; 184 | } 185 | 186 | Match fsMatch = Regex.Match(subtitleText, @"\\fs(?\d+)"); 187 | if (fsMatch.Success && int.TryParse(fsMatch.Groups["fs"].Value, out int fs)) 188 | { 189 | danmakuItem.BaseFontSize = fs; 190 | } 191 | 192 | Match bordMatch = Regex.Match(subtitleText, @"\\bord(?\d+)"); 193 | if (bordMatch.Success && int.TryParse(bordMatch.Groups["bord"].Value, out int bord)) 194 | { 195 | danmakuItem.OutlineSize = bord; 196 | danmakuItem.HasOutline = bord > 0; 197 | } 198 | 199 | MatchCollection colorMatchCollection = Regex.Matches(subtitleText, @"\\(?\d)c.*?(?[A-Fa-f0-9]{6,8})"); 200 | foreach (Match colorMatch in colorMatchCollection) 201 | { 202 | int colorType = int.Parse(colorMatch.Groups["colorType"].Value); 203 | if (colorType == 1 || colorType == 3) 204 | { 205 | string colorStr = colorMatch.Groups["color"].Value; 206 | if (colorStr.Length > 6) 207 | { 208 | colorStr = colorStr.Substring(colorStr.Length - 6, 6); 209 | } 210 | if (TryParseHexToByte(colorStr.Substring(4, 2), out byte r) 211 | && TryParseHexToByte(colorStr.Substring(2, 2), out byte g) 212 | && TryParseHexToByte(colorStr.Substring(0, 2), out byte b)) 213 | { 214 | if (colorType == 3) 215 | { 216 | danmakuItem.OutlineColor = Color.FromArgb(byte.MaxValue, r, g, b); 217 | } 218 | else 219 | { 220 | danmakuItem.TextColor = Color.FromArgb(byte.MaxValue, r, g, b); 221 | } 222 | } 223 | } 224 | } 225 | 226 | Match fadMatch = Regex.Match(subtitleText, @"\\fad\((?\d+),(?\d+)\)"); 227 | if (fadMatch.Success) 228 | { 229 | uint fadeInMs = uint.Parse(fadMatch.Groups["fadeInMs"].Value); 230 | if (fadeInMs > 0) 231 | { 232 | danmakuItem.StartAlpha = 0; 233 | danmakuItem.AlphaDurationMs = fadeInMs; 234 | } 235 | else 236 | { 237 | uint fadeOutMs = uint.Parse(fadMatch.Groups["fadeOutMs"].Value); 238 | if (fadeOutMs > 0) 239 | { 240 | danmakuItem.EndAlpha = 0; 241 | danmakuItem.AlphaDurationMs = fadeOutMs; 242 | } 243 | } 244 | } 245 | 246 | Match moveMatch = Regex.Match(subtitleText, @"\\move\((?[\d\.,]+?)\)"); 247 | if (moveMatch.Success) 248 | { 249 | danmakuItem.AlignmentMode = DanmakuAlignmentMode.Default; 250 | 251 | string moveStr = moveMatch.Groups["move"].Value; 252 | string[] moveSplitArray = moveStr.Split(','); 253 | if (moveSplitArray.Length >= 4) 254 | { 255 | float startX = float.Parse(moveSplitArray[0]); 256 | float startY = float.Parse(moveSplitArray[1]); 257 | float endX = float.Parse(moveSplitArray[2]); 258 | float endY = float.Parse(moveSplitArray[3]); 259 | if (videoWidth > 0 && videoHeight > 0) 260 | { 261 | startX = Math.Min(startX / videoWidth, 0.99f); 262 | startY = Math.Min(startY / videoHeight, 0.99f); 263 | endX = Math.Min(endX / videoWidth, 0.99f); 264 | endY = Math.Min(endY / videoHeight, 0.99f); 265 | } 266 | danmakuItem.StartX = startX; 267 | danmakuItem.EndX = endX; 268 | danmakuItem.StartY = startY; 269 | danmakuItem.EndY = endY; 270 | 271 | if (moveSplitArray.Length >= 6) 272 | { 273 | if (ulong.TryParse(moveSplitArray[4], out ulong delayMs) && ulong.TryParse(moveSplitArray[5], out ulong durationMs)) 274 | { 275 | danmakuItem.TranslationDelayMs = delayMs; 276 | danmakuItem.TranslationDurationMs = durationMs; 277 | } 278 | } 279 | } 280 | } 281 | else 282 | { 283 | Match posMatch = Regex.Match(subtitleText, @"\\pos\((?[\d\.]+),(?[\d\.]+)\)"); 284 | if (posMatch.Success) 285 | { 286 | danmakuItem.AlignmentMode = DanmakuAlignmentMode.Default; 287 | 288 | float x = float.Parse(posMatch.Groups["x"].Value); 289 | float y = float.Parse(posMatch.Groups["y"].Value); 290 | if (videoWidth > 0 && videoHeight > 0) 291 | { 292 | x = Math.Min(x / videoWidth, 0.99f); 293 | y = Math.Min(y / videoHeight, 0.99f); 294 | } 295 | danmakuItem.StartX = x; 296 | danmakuItem.EndX = x; 297 | danmakuItem.StartY = y; 298 | danmakuItem.EndY = y; 299 | 300 | Match anMatch = Regex.Match(subtitleText, @"\\an(?\d+)"); 301 | if (anMatch.Success) 302 | { 303 | int an = int.Parse(anMatch.Groups["an"].Value); 304 | danmakuItem.AnchorMode = (DanmakuAlignmentMode)an; 305 | } 306 | else 307 | { 308 | danmakuItem.AnchorMode = style.AlignmentMode; 309 | } 310 | } 311 | } 312 | 313 | Match fryMatch = Regex.Match(subtitleText, @"\\fry(?[0-9\.]+)"); 314 | if (fryMatch.Success && float.TryParse(fryMatch.Groups["fry"].Value, out float fry)) 315 | { 316 | danmakuItem.RotateY = fry; 317 | } 318 | 319 | Match frzMatch = Regex.Match(subtitleText, @"\\frz(?[0-9\.]+)"); 320 | if (frzMatch.Success && float.TryParse(frzMatch.Groups["frz"].Value, out float frz)) 321 | { 322 | danmakuItem.RotateZ = -frz; 323 | } 324 | 325 | subtitleText = Regex.Replace(subtitleText, @"\{.*?\}", string.Empty); 326 | if (Regex.IsMatch(subtitleText, @"^m \d+")) 327 | { 328 | // Skip drawing command 329 | continue; 330 | } 331 | } 332 | danmakuItem.Text = subtitleText.Replace(@"\n", "\n").Replace(@"\N", "\n"); 333 | 334 | danmakuItem.BaseFontSize = Math.Max(danmakuItem.BaseFontSize * 0.75f, 2); 335 | danmakuItem.OutlineSize *= 2; 336 | if (danmakuItem.FontFamilyName.EndsWith(" Bold")) 337 | { 338 | danmakuItem.FontFamilyName = danmakuItem.FontFamilyName.Substring(0, danmakuItem.FontFamilyName.Length - 5); 339 | } 340 | if (danmakuItem.FontFamilyName.StartsWith("@")) 341 | { 342 | danmakuItem.FontFamilyName = danmakuItem.FontFamilyName.Substring(1); 343 | } 344 | 345 | if (danmakuItem.AlignmentMode == DanmakuAlignmentMode.LowerCenter && danmakuItem.AlphaDurationMs == 0) 346 | { 347 | danmakuItem.Mode = DanmakuMode.Bottom; 348 | } 349 | 350 | list.Add(danmakuItem); 351 | } 352 | } 353 | } 354 | catch (Exception ex) 355 | { 356 | Debug.WriteLine($"Failed to parse ASS dialogue ({line}): {ex.Message}"); 357 | } 358 | } 359 | } 360 | line = stringReader.ReadLine(); 361 | } 362 | } 363 | 364 | list.Sort((a, b) => (int)a.StartMs - (int)b.StartMs); 365 | // Avoid startMs/endMs overlapping so they are not rendered to stacking lines as much as possible 366 | for (int i = 0; i < list.Count - 1; i++) 367 | { 368 | if (list[i].Mode == DanmakuMode.Bottom 369 | && list[i + 1].Mode == DanmakuMode.Bottom 370 | && list[i].DurationMs > 100 371 | && list[i + 1].StartMs - list[i].StartMs - list[i].DurationMs < 60) 372 | { 373 | list[i].DurationMs = list[i + 1].StartMs - list[i].StartMs - 60; 374 | } 375 | } 376 | 377 | return list; 378 | } 379 | catch (Exception ex) 380 | { 381 | Debug.WriteLine($"Failed to parse ASS: {ex.Message}"); 382 | return null; 383 | } 384 | } 385 | 386 | private static bool TryParseHexToByte(string str, out byte value) 387 | { 388 | value = 0; 389 | if (string.IsNullOrWhiteSpace(str) || str.Length > 2) 390 | { 391 | return false; 392 | } 393 | 394 | str = str.ToUpper(); 395 | char c0 = str[0]; 396 | if (!((c0 >= '0' && c0 <= '9') || (c0 >= 'A' && c0 <= 'F'))) 397 | { 398 | return false; 399 | } 400 | 401 | if (str.Length == 1) 402 | { 403 | if (c0 >= 'A' && c0 <= 'F') 404 | { 405 | value = (byte)(c0 - 'A' + 10); 406 | } 407 | else 408 | { 409 | value = (byte)(c0 - '0'); 410 | } 411 | } 412 | else 413 | { 414 | if (c0 >= 'A' && c0 <= 'F') 415 | { 416 | value = (byte)((c0 - 'A' + 10) << 4); 417 | } 418 | else 419 | { 420 | value = (byte)((c0 - '0') << 4); 421 | } 422 | 423 | char c1 = str[1]; 424 | if (!((c1 >= '0' && c1 <= '9') || (c1 >= 'A' && c1 <= 'F'))) 425 | { 426 | return false; 427 | } 428 | 429 | if (c1 >= 'A' && c1 <= 'F') 430 | { 431 | value += (byte)(c1 - 'A' + 10); 432 | } 433 | else 434 | { 435 | value += (byte)(c1 - '0'); 436 | } 437 | } 438 | 439 | return true; 440 | } 441 | 442 | private class AssStyle 443 | { 444 | public int MarginLeft = 0; 445 | public int MarginRight = 0; 446 | public int MarginBottom = 20; 447 | public byte Alpha = byte.MaxValue; 448 | public bool IsBold = true; 449 | public bool HasOutline = true; 450 | public float FontSize = (uint)DanmakuItem.DefaultBaseFontSize; 451 | public float OutlineSize = 2f; 452 | public string FontFamilyName = null; 453 | public Color TextColor = Colors.White; 454 | public Color OutlineColor = Colors.Black; 455 | public DanmakuAlignmentMode AlignmentMode = DanmakuAlignmentMode.LowerCenter; 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/BaseTypeDef.cs: -------------------------------------------------------------------------------- 1 | using Windows.UI; 2 | 3 | namespace Atelier39 4 | { 5 | public enum DanmakuMode 6 | { 7 | Unknown = 0, 8 | Rolling = 1, 9 | Bottom = 4, 10 | Top = 5, 11 | ReverseRolling = 6, 12 | Advanced = 7, 13 | //Code = 8, 14 | Subtitle = 9 15 | } 16 | 17 | public enum DanmakuPool 18 | { 19 | Normal = 0, 20 | Subtitle = 1, 21 | Special = 2 22 | } 23 | 24 | public enum DanmakuFontSize 25 | { 26 | Smallest = 1, 27 | Smaller = 2, 28 | Normal = 3, 29 | Larger = 4, 30 | Largest = 5 31 | } 32 | 33 | public enum DanmakuAlignmentMode 34 | { 35 | Default = 0, 36 | LowerLeft = 1, 37 | LowerCenter = 2, 38 | LowerRight = 3, 39 | MiddleLeft = 4, 40 | MiddleCenter = 5, 41 | MiddleRight = 6, 42 | UpperLeft = 7, 43 | UpperCenter = 8, 44 | UpperRight = 9 45 | } 46 | 47 | public class DanmakuItem 48 | { 49 | public static float DefaultBaseFontSize = 22; 50 | 51 | /// 52 | /// Used to sort danmaku with the same StartMs 53 | /// 54 | public ulong Id; 55 | public uint StartMs; 56 | public bool HasBorder; 57 | public bool HasOutline = true; 58 | public bool AllowDensityControl = true; 59 | public bool IsRealtime; 60 | public float BaseFontSize; 61 | public float OutlineSize = 2f; 62 | public string FontFamilyName; 63 | public string Text; 64 | public bool? IsBold; 65 | public DanmakuMode Mode; 66 | public Color TextColor = Colors.White; 67 | public Color OutlineColor = Colors.Black; 68 | 69 | #region For Advanced mode 70 | 71 | public float StartX; 72 | public float StartY; 73 | public float EndX; 74 | public float EndY; 75 | 76 | public int MarginLeft; 77 | public int MarginRight; 78 | public int MarginBottom; 79 | public DanmakuAlignmentMode AlignmentMode = DanmakuAlignmentMode.Default; 80 | public DanmakuAlignmentMode AnchorMode = DanmakuAlignmentMode.UpperLeft; 81 | 82 | public byte StartAlpha; 83 | public byte EndAlpha; 84 | 85 | public ulong DurationMs; 86 | public ulong TranslationDurationMs; 87 | public ulong TranslationDelayMs; 88 | public ulong AlphaDurationMs; 89 | public ulong AlphaDelayMs; 90 | 91 | /// 92 | /// Degree 93 | /// 94 | public float RotateZ; 95 | /// 96 | /// Degree 97 | /// 98 | public float RotateY; 99 | 100 | public bool KeepDefinedFontSize; 101 | 102 | #endregion 103 | } 104 | 105 | public static class DanmakuDefaultLayerDef 106 | { 107 | public const uint DefaultLayerId = 0; 108 | public const uint RollingLayerId = 0; 109 | public const uint ReverseRollingLayerId = 1; 110 | public const uint TopLayerId = 2; 111 | public const uint BottomLayerId = 3; 112 | public const uint AdvancedLayerId = 4; 113 | public const uint SubtitleLayerId = 5; 114 | public const uint DefaultLayerCount = 6; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/BilibiliDanmakuXmlParser.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Net; 6 | using System.Text.RegularExpressions; 7 | using Windows.UI; 8 | 9 | namespace Atelier39 10 | { 11 | public static class BilibiliDanmakuXmlParser 12 | { 13 | public static List GetDanmakuList(string xmlStr, IList regexFilterList, bool mergeDuplicate, out uint totalCount, out uint filteredCount, out uint mergedCount) 14 | { 15 | totalCount = 0; 16 | filteredCount = 0; 17 | mergedCount = 0; 18 | List list = new List(); 19 | Dictionary> duplicatedDanmakuDict = new Dictionary>(); 20 | 21 | if (string.IsNullOrWhiteSpace(xmlStr)) 22 | { 23 | return list; 24 | } 25 | 26 | bool isNewFormat = xmlStr.Contains(""); 27 | 28 | // Old format: 我从未见过如此厚颜无耻之猴 29 | // New format: 啊啊啊啊啊啊啊啊 30 | // Mode 7 format: [0,0.5,"1-1",3,"为了那个傻傻的放电妹",0,0,0.99,0.5,3000,0,true,"幼圆",1] 31 | MatchCollection matchCollection = Regex.Matches(xmlStr, @".+?)"">(?[\s\S]*?)"); 32 | foreach (Match match in matchCollection) 33 | { 34 | totalCount++; 35 | 36 | string tagStr = match.Groups["tag"].Value; 37 | string contentStr = match.Groups["content"].Value; 38 | DanmakuMode danmakuMode = DanmakuMode.Unknown; 39 | 40 | if (string.IsNullOrWhiteSpace(tagStr) || string.IsNullOrWhiteSpace(contentStr)) 41 | { 42 | filteredCount++; 43 | continue; 44 | } 45 | contentStr = WebUtility.HtmlDecode(contentStr).Replace("/n", "\n").Replace("\\n", "\n").Trim(); 46 | 47 | if (mergeDuplicate) 48 | { 49 | string[] pArray = tagStr.Split(','); 50 | if (pArray.Length >= 4) 51 | { 52 | if (int.TryParse(pArray[isNewFormat ? 3 : 1], out int mode) && double.TryParse(pArray[isNewFormat ? 2 : 0], out double time)) 53 | { 54 | danmakuMode = (DanmakuMode)mode; 55 | if ((danmakuMode == DanmakuMode.Rolling || danmakuMode == DanmakuMode.Top || danmakuMode == DanmakuMode.Bottom) && time >= 0) 56 | { 57 | uint startMs = (uint)(isNewFormat ? time : time * 1000); 58 | 59 | if (!duplicatedDanmakuDict.ContainsKey(contentStr)) 60 | { 61 | duplicatedDanmakuDict.Add(contentStr, new List() { new DuplicatedDanmakuItem() { StartMs = startMs, Count = 1 } }); 62 | } 63 | else 64 | { 65 | bool merged = false; 66 | List duplicatedDanmakuList = duplicatedDanmakuDict[contentStr]; 67 | foreach (DuplicatedDanmakuItem duplicatedDanmaku in duplicatedDanmakuList) 68 | { 69 | if (Math.Abs((int)(startMs - duplicatedDanmaku.StartMs)) <= 20000) // Merge duplicate danmaku in timeframe of 20s 70 | { 71 | merged = true; 72 | duplicatedDanmaku.Count++; 73 | break; 74 | } 75 | } 76 | if (merged) 77 | { 78 | //Debug.WriteLine($"Warning: merged danmaku: {contentStr} at {startMs}"); 79 | mergedCount++; 80 | continue; 81 | } 82 | else 83 | { 84 | duplicatedDanmakuList.Add(new DuplicatedDanmakuItem() { StartMs = startMs, Count = 1 }); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | if (danmakuMode != DanmakuMode.Advanced && danmakuMode != DanmakuMode.Subtitle && regexFilterList != null && regexFilterList.Count > 0) 93 | { 94 | bool filtered = false; 95 | foreach (string regexFilter in regexFilterList) 96 | { 97 | if (Regex.IsMatch(contentStr, regexFilter)) 98 | { 99 | Debug.WriteLine($"Warning: filtered danmaku: {contentStr}"); 100 | filtered = true; 101 | filteredCount++; 102 | break; 103 | } 104 | } 105 | if (filtered) 106 | { 107 | continue; 108 | } 109 | } 110 | 111 | DanmakuItem item = ParseDanmakuItem(tagStr, contentStr, isNewFormat); 112 | if (item != null) 113 | { 114 | list.Add(item); 115 | } 116 | else 117 | { 118 | Debug.WriteLine($"Failed to create danmaku: {contentStr}"); 119 | } 120 | } 121 | 122 | if (duplicatedDanmakuDict.Count > 0) 123 | { 124 | foreach (DanmakuItem item in list) 125 | { 126 | if (item.Mode == DanmakuMode.Rolling || item.Mode == DanmakuMode.Top || item.Mode == DanmakuMode.Bottom) 127 | { 128 | if (duplicatedDanmakuDict.ContainsKey(item.Text)) 129 | { 130 | List duplicatedDanmakuList = duplicatedDanmakuDict[item.Text]; 131 | foreach (DuplicatedDanmakuItem duplicatedDanmaku in duplicatedDanmakuList) 132 | { 133 | if (duplicatedDanmaku.Count > 1 && item.StartMs == duplicatedDanmaku.StartMs) 134 | { 135 | item.Text = $"{item.Text}\u00D7{duplicatedDanmaku.Count}"; 136 | break; 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | 144 | BilibiliDanmakuSorter.Sort(list); 145 | 146 | return list; 147 | } 148 | 149 | public static List GetSubtitleList(string jsonArrayStr) 150 | { 151 | List list = new List(); 152 | if (!string.IsNullOrWhiteSpace(jsonArrayStr)) 153 | { 154 | JObject jObject = JObject.Parse(jsonArrayStr); 155 | JToken bodyArray = jObject["body"]; 156 | if (bodyArray != null) 157 | { 158 | foreach (JToken jToken in bodyArray) 159 | { 160 | try 161 | { 162 | double fromMs = jToken["from"].ToObject() * 1000; 163 | double toMs = jToken["to"].ToObject() * 1000; 164 | if (toMs > fromMs) 165 | { 166 | string content = jToken["content"].ToString(); 167 | if (!string.IsNullOrWhiteSpace(content)) 168 | { 169 | DanmakuItem item = new DanmakuItem 170 | { 171 | Mode = DanmakuMode.Subtitle, 172 | StartMs = (uint)fromMs, 173 | DurationMs = (uint)(toMs - fromMs), 174 | Text = content, 175 | TextColor = Colors.White, 176 | BaseFontSize = DanmakuItem.DefaultBaseFontSize, 177 | HasOutline = false, 178 | AllowDensityControl = false 179 | }; 180 | string[] sentenceArray = item.Text.Split('\n'); 181 | for (int i = 0; i < sentenceArray.Length; i++) 182 | { 183 | sentenceArray[i] = sentenceArray[i].Trim(); 184 | } 185 | item.Text = string.Join("\n", sentenceArray); 186 | list.Add(item); 187 | } 188 | } 189 | } 190 | catch 191 | { 192 | Debug.WriteLine($"Failed to parse subtitle entry: {jToken}"); 193 | } 194 | } 195 | } 196 | } 197 | 198 | BilibiliDanmakuSorter.Sort(list); 199 | return list; 200 | } 201 | 202 | /// 203 | /// Return null if danmaku can't be parsed 204 | /// 205 | private static DanmakuItem ParseDanmakuItem(string tagStr, string content, bool isNewFormat) 206 | { 207 | string[] pArray = tagStr.Split(','); 208 | if (pArray.Length < 8) 209 | { 210 | return null; 211 | } 212 | 213 | try 214 | { 215 | DanmakuItem danmakuItem = new DanmakuItem 216 | { 217 | Id = isNewFormat ? ulong.Parse(pArray[0]) : 0, 218 | HasBorder = false, 219 | Text = content, 220 | TextColor = ParseColor(uint.Parse(pArray[isNewFormat ? 5 : 3])) 221 | }; 222 | 223 | double startMs = isNewFormat ? double.Parse(pArray[2]) : double.Parse(pArray[0]) * 1000; 224 | if (startMs < 0) 225 | { 226 | startMs = 0; 227 | } 228 | danmakuItem.StartMs = (uint)startMs; 229 | 230 | int mode = int.Parse(pArray[isNewFormat ? 3 : 1]); 231 | switch (mode) 232 | { 233 | case (int)DanmakuMode.Rolling: 234 | { 235 | danmakuItem.Mode = DanmakuMode.Rolling; 236 | break; 237 | } 238 | case (int)DanmakuMode.Bottom: 239 | { 240 | danmakuItem.Mode = DanmakuMode.Bottom; 241 | break; 242 | } 243 | case (int)DanmakuMode.Top: 244 | { 245 | danmakuItem.Mode = DanmakuMode.Top; 246 | break; 247 | } 248 | case (int)DanmakuMode.ReverseRolling: 249 | { 250 | danmakuItem.Mode = DanmakuMode.ReverseRolling; 251 | break; 252 | } 253 | case (int)DanmakuMode.Advanced: 254 | { 255 | danmakuItem.Mode = DanmakuMode.Advanced; 256 | break; 257 | } 258 | default: 259 | { 260 | Debug.WriteLine($"Skip unknown danmaku type: {mode}"); 261 | return null; 262 | } 263 | } 264 | 265 | int fontSize = int.Parse(pArray[isNewFormat ? 4 : 2]); 266 | switch (danmakuItem.Mode) 267 | { 268 | case DanmakuMode.Rolling: 269 | case DanmakuMode.Bottom: 270 | case DanmakuMode.Top: 271 | case DanmakuMode.ReverseRolling: 272 | { 273 | fontSize -= fontSize % 2 == 1 ? 3 : 2; 274 | break; 275 | } 276 | case DanmakuMode.Advanced: 277 | { 278 | fontSize += 4; // Experimental adjustment 279 | break; 280 | } 281 | } 282 | if (fontSize < 2) 283 | { 284 | fontSize = 2; 285 | } 286 | danmakuItem.BaseFontSize = fontSize; 287 | 288 | if (danmakuItem.Mode == DanmakuMode.Advanced) 289 | { 290 | if (!content.StartsWith("[") || !content.EndsWith("]")) 291 | { 292 | return null; 293 | } 294 | 295 | danmakuItem.AllowDensityControl = false; 296 | 297 | string[] valueArray; 298 | try 299 | { 300 | JArray jArray = JArray.Parse(content); 301 | valueArray = new string[jArray.Count]; 302 | for (int i = 0; i < valueArray.Length; i++) 303 | { 304 | valueArray[i] = jArray[i].ToString(); 305 | } 306 | 307 | if (valueArray.Length < 5) 308 | { 309 | return null; 310 | } 311 | danmakuItem.Text = WebUtility.HtmlDecode(valueArray[4]).Replace("/n", "\n").Replace("\\n", "\n"); 312 | if (string.IsNullOrWhiteSpace(danmakuItem.Text)) 313 | { 314 | return null; 315 | } 316 | 317 | danmakuItem.StartX = string.IsNullOrWhiteSpace(valueArray[0]) ? 0f : float.Parse(valueArray[0]); 318 | danmakuItem.StartY = string.IsNullOrWhiteSpace(valueArray[1]) ? 0f : float.Parse(valueArray[1]); 319 | danmakuItem.EndX = danmakuItem.StartX; 320 | danmakuItem.EndY = danmakuItem.StartY; 321 | 322 | string[] opacitySplit = valueArray[2].Split('-'); 323 | danmakuItem.StartAlpha = (byte)(Math.Max(float.Parse(opacitySplit[0]), 0) * byte.MaxValue); 324 | danmakuItem.EndAlpha = opacitySplit.Length > 1 ? (byte)(Math.Max(float.Parse(opacitySplit[1]), 0) * byte.MaxValue) : danmakuItem.StartAlpha; 325 | 326 | danmakuItem.DurationMs = (ulong)(float.Parse(valueArray[3]) * 1000); 327 | danmakuItem.TranslationDurationMs = danmakuItem.DurationMs; 328 | danmakuItem.TranslationDelayMs = 0; 329 | danmakuItem.AlphaDurationMs = danmakuItem.DurationMs; 330 | danmakuItem.AlphaDelayMs = 0; 331 | 332 | if (valueArray.Length >= 7) 333 | { 334 | danmakuItem.RotateZ = string.IsNullOrWhiteSpace(valueArray[5]) ? 0f : float.Parse(valueArray[5]); 335 | danmakuItem.RotateY = string.IsNullOrWhiteSpace(valueArray[6]) ? 0f : float.Parse(valueArray[6]); 336 | } 337 | else 338 | { 339 | danmakuItem.RotateZ = 0f; 340 | danmakuItem.RotateY = 0f; 341 | } 342 | 343 | if (valueArray.Length >= 11) 344 | { 345 | danmakuItem.EndX = string.IsNullOrWhiteSpace(valueArray[7]) ? 0f : float.Parse(valueArray[7]); 346 | danmakuItem.EndY = string.IsNullOrWhiteSpace(valueArray[8]) ? 0f : float.Parse(valueArray[8]); 347 | if (!string.IsNullOrWhiteSpace(valueArray[9])) 348 | { 349 | danmakuItem.TranslationDurationMs = (ulong)(float.Parse(valueArray[9])); 350 | } 351 | if (!string.IsNullOrWhiteSpace(valueArray[10])) 352 | { 353 | string translationDelayValue = valueArray[10]; 354 | if (translationDelayValue == "0") // To be compatible with legacy style 355 | { 356 | danmakuItem.TranslationDelayMs = 0; 357 | } 358 | else 359 | { 360 | danmakuItem.TranslationDelayMs = (ulong)(float.Parse(translationDelayValue)); 361 | } 362 | } 363 | } 364 | 365 | //if (valueArray.Length >= 12 && (valueArray[11].Equals("true", StringComparison.OrdinalIgnoreCase) || valueArray[11] == "1")) 366 | //{ 367 | // danmakuItem.HasOutline = false; 368 | //} 369 | //else 370 | //{ 371 | // danmakuItem.OutlineColor = danmakuItem.TextColor.R + danmakuItem.TextColor.G + danmakuItem.TextColor.B > 32 ? Colors.Black : Colors.White; 372 | // danmakuItem.OutlineColor.A = danmakuItem.TextColor.A; 373 | //} 374 | danmakuItem.HasOutline = false; 375 | 376 | //if (valueArray.Length >= 13) 377 | //{ 378 | // string fontFamilyName = valueArray[12]; 379 | // if (!string.IsNullOrWhiteSpace(fontFamilyName)) 380 | // { 381 | // danmakuItem.FontFamilyName = fontFamilyName.Replace("\"", string.Empty); 382 | // } 383 | //} 384 | danmakuItem.FontFamilyName = "Consolas"; // Default monospaced font 385 | 386 | danmakuItem.KeepDefinedFontSize = true; 387 | } 388 | catch (Exception ex) 389 | { 390 | Debug.WriteLine($"Failed to parse advanced mode danmaku: {content} Exception: {ex.Message}"); 391 | return null; 392 | } 393 | } 394 | 395 | return danmakuItem; 396 | } 397 | catch (Exception ex) 398 | { 399 | Debug.WriteLine($"Failed to parse danmaku tag: {tagStr} Exception: {ex.Message}"); 400 | return null; 401 | } 402 | } 403 | 404 | private static Color ParseColor(uint colorValue) 405 | { 406 | colorValue = colorValue & 0xFFFFFF; // Ingore alpha 407 | uint b = 0xFF & colorValue; 408 | uint g = (0xFF00 & colorValue) >> 8; 409 | uint r = (0xFF0000 & colorValue) >> 16; 410 | return Color.FromArgb(byte.MaxValue, (byte)r, (byte)g, (byte)b); 411 | } 412 | 413 | private class DuplicatedDanmakuItem 414 | { 415 | public uint StartMs; 416 | public uint Count; 417 | } 418 | 419 | private static class BilibiliDanmakuSorter 420 | { 421 | public static void Sort(IList list) 422 | { 423 | Merge(list, 0, list.Count - 1); 424 | } 425 | 426 | private static void Merge(IList list, int p, int r) 427 | { 428 | if (p < r) 429 | { 430 | int mid = (p + r) / 2; 431 | Merge(list, p, mid); 432 | Merge(list, mid + 1, r); 433 | MergeArray(list, p, mid, r); 434 | } 435 | } 436 | 437 | private static void MergeArray(IList list, int p, int mid, int r) 438 | { 439 | DanmakuItem[] tmp = new DanmakuItem[r - p + 1]; 440 | int i = p, j = mid + 1; 441 | int m = mid, n = r; 442 | int k = 0; 443 | 444 | while (i <= m && j <= n) 445 | { 446 | if (list[i].StartMs < list[j].StartMs) 447 | { 448 | tmp[k++] = list[i++]; 449 | } 450 | else if (list[i].StartMs > list[j].StartMs) 451 | { 452 | tmp[k++] = list[j++]; 453 | } 454 | else if (list[i].Mode == DanmakuMode.Advanced) 455 | { 456 | // Compare Id 457 | if (list[i].Id <= list[j].Id) 458 | { 459 | tmp[k++] = list[i++]; 460 | } 461 | else 462 | { 463 | tmp[k++] = list[j++]; 464 | } 465 | } 466 | else 467 | { 468 | tmp[k++] = list[i++]; 469 | } 470 | } 471 | 472 | while (i <= m) 473 | { 474 | tmp[k++] = list[i++]; 475 | } 476 | 477 | while (j <= n) 478 | { 479 | tmp[k++] = list[j++]; 480 | } 481 | 482 | for (i = 0; i < r - p + 1; i++) 483 | { 484 | list[p + i] = tmp[i]; 485 | } 486 | } 487 | } 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/DanmakuFrostMaster.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Graphics.Canvas.Text; 2 | using Microsoft.Graphics.Canvas.UI.Xaml; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using Windows.Foundation; 8 | using Windows.Foundation.Diagnostics; 9 | using Windows.UI; 10 | 11 | namespace Atelier39 12 | { 13 | public class DanmakuFrostMaster 14 | { 15 | public const uint DefaultDanmakuLayerCount = DanmakuDefaultLayerDef.DefaultLayerCount; 16 | 17 | private readonly DanmakuRender _render; 18 | private readonly Queue _updateTimeQueue = new Queue(); 19 | private readonly AutoResetEvent _updateEvent = new AutoResetEvent(false); 20 | private readonly ManualResetEventSlim _pauseEvent = new ManualResetEventSlim(true); 21 | private List _danmakuList = new List(); 22 | private volatile bool _hasSubtitle; 23 | private volatile bool _isRenderEnabled; 24 | private volatile bool _isSeeking; 25 | private volatile bool _isClosing; 26 | private volatile int _lastIndex; 27 | private volatile uint _lastTimeMs; 28 | private volatile int _subtitleIndexAfterSeek = -1; 29 | 30 | public bool DebugMode 31 | { 32 | get => _render.DebugMode; 33 | set => _render.DebugMode = value; 34 | } 35 | 36 | /// 37 | /// Must be called in UI thread 38 | /// 39 | /// Rolling area ratio = value * 0.1 (value: 1 to 10) 40 | public DanmakuFrostMaster(CanvasAnimatedControl canvas, LoggingChannel loggingChannel = null) 41 | { 42 | Logger.SetLogger(loggingChannel); 43 | 44 | _render = new DanmakuRender(canvas); 45 | 46 | Windows.System.Threading.ThreadPool.RunAsync(Updater_DoWork).AsTask(); 47 | _isRenderEnabled = true; 48 | 49 | Logger.Log("DanmakuFrostMaster is created"); 50 | } 51 | 52 | public void SetAutoControlDensity(bool value) 53 | { 54 | _render.SetAutoControlDensity(value); 55 | } 56 | 57 | /// 58 | /// Default: -1 59 | /// 1->10 2->20 3->30 4-50 -1->unlimited 60 | /// 61 | public void SetRollingDensity(int value) 62 | { 63 | _render.SetRollingDensity(value); 64 | } 65 | 66 | /// 67 | /// Default: 9 68 | /// 69 | /// in (0,10] 70 | public void SetRollingAreaRatio(int value) 71 | { 72 | _render.SetRollingAreaRatio(value); 73 | } 74 | 75 | /// 76 | /// Default: 5 77 | /// speed = value * 0.02 78 | /// 79 | /// in [1,10] 80 | public void SetRollingSpeed(int value) 81 | { 82 | _render.SetRollingSpeed(value); 83 | } 84 | 85 | /// in [0,1] 86 | public void SetOpacity(double value) 87 | { 88 | _render.SetOpacity(value); 89 | } 90 | 91 | public void SetIsTextBold(bool value) 92 | { 93 | _render.SetIsTextBold(value); 94 | } 95 | 96 | public void SetDanmakuFontSizeOffset(DanmakuFontSize value) 97 | { 98 | _render.SetDanmakuFontSizeOffset(value); 99 | } 100 | 101 | public void SetSubtitleFontSize(DanmakuFontSize value) 102 | { 103 | _render.SetSubtitleFontSizeOffset(value); 104 | } 105 | 106 | /// 107 | /// Null or empty to use system default 108 | /// 109 | public void SetFontFamilyName(string value) 110 | { 111 | _render.SetDefaultFontFamilyName(value); 112 | } 113 | 114 | public void SetBorderColor(Color borderColor) 115 | { 116 | _render.SetBorderColor(borderColor); 117 | } 118 | 119 | public void SetNoOverlapSubtitle(bool value) 120 | { 121 | _render.SetNoOverlapSubtitle(value); 122 | } 123 | 124 | public void UpdateTime(uint currentMs) 125 | { 126 | lock (_updateTimeQueue) 127 | { 128 | _updateTimeQueue.Enqueue(currentMs); 129 | } 130 | _updateEvent.Set(); 131 | } 132 | 133 | public void Pause() 134 | { 135 | if (!_isClosing) 136 | { 137 | _pauseEvent.Reset(); 138 | _render.Pause(); 139 | } 140 | } 141 | 142 | public void Resume() 143 | { 144 | _render.Start(); 145 | _pauseEvent.Set(); 146 | } 147 | 148 | public void Stop() 149 | { 150 | Pause(); 151 | _render.Stop(); 152 | lock (_updateTimeQueue) 153 | { 154 | _updateTimeQueue.Clear(); 155 | } 156 | } 157 | 158 | public void Restart() 159 | { 160 | Seek(0); 161 | } 162 | 163 | public void SetRenderState(bool renderDanmaku, bool renderSubtitle) 164 | { 165 | _isRenderEnabled = renderDanmaku || renderSubtitle; 166 | _render.SetRenderState(renderDanmaku, renderSubtitle); 167 | } 168 | 169 | public void SetLayerRenderState(uint layerId, bool render) 170 | { 171 | _render.SetLayerRenderState(layerId, render); 172 | } 173 | 174 | public void SetSubtitleLayer(uint layerId) 175 | { 176 | _render.SetSubtitleLayer(layerId); 177 | } 178 | 179 | public void Seek(uint targetMs) 180 | { 181 | _isSeeking = true; 182 | Stop(); 183 | lock (_danmakuList) 184 | { 185 | _lastIndex = 0; 186 | if (_danmakuList.Count > 0) 187 | { 188 | while (_danmakuList[_lastIndex].StartMs < targetMs) 189 | { 190 | _lastIndex++; 191 | if (_lastIndex >= _danmakuList.Count) 192 | { 193 | break; 194 | } 195 | } 196 | if (_hasSubtitle) 197 | { 198 | _render.ClearLayer(DanmakuDefaultLayerDef.SubtitleLayerId); 199 | int index = _lastIndex - 1; 200 | while (index >= 0 && _danmakuList[index].Mode != DanmakuMode.Subtitle) 201 | { 202 | index--; 203 | } 204 | if (index >= 0 && index != _lastIndex && _danmakuList[index].StartMs + _danmakuList[index].DurationMs > targetMs) 205 | { 206 | _subtitleIndexAfterSeek = index; 207 | } 208 | } 209 | } 210 | _lastTimeMs = targetMs; 211 | Resume(); 212 | _isSeeking = false; 213 | } 214 | } 215 | 216 | public void Clear() 217 | { 218 | _danmakuList?.Clear(); 219 | } 220 | 221 | public void Close() 222 | { 223 | if (!_isClosing) 224 | { 225 | _isRenderEnabled = false; 226 | _isClosing = true; 227 | 228 | _pauseEvent.Set(); 229 | _updateEvent.Set(); 230 | _render.Close(); 231 | 232 | Logger.Log("DanmakuFrostMaster is closed"); 233 | } 234 | } 235 | 236 | public void AddRealtimeDanmaku(DanmakuItem item, bool insertToList, uint layerId = DanmakuDefaultLayerDef.DefaultLayerId) 237 | { 238 | item.AllowDensityControl = false; 239 | item.IsRealtime = true; 240 | _render.RenderDanmakuItem(layerId, item); 241 | if (insertToList) 242 | { 243 | lock (_danmakuList) 244 | { 245 | bool added = false; 246 | for (int i = 0; i < _danmakuList.Count; i++) 247 | { 248 | if (_danmakuList[i].StartMs > item.StartMs) 249 | { 250 | _danmakuList.Insert(i, item); 251 | added = true; 252 | break; 253 | } 254 | } 255 | if (!added) 256 | { 257 | // No danmaku in current list 258 | _danmakuList.Add(item); 259 | } 260 | } 261 | } 262 | } 263 | 264 | /// Must be pre-ordered by StartMs 265 | public void SetDanmakuList(List danmakuList) 266 | { 267 | Clear(); 268 | _danmakuList = danmakuList ?? new List(); 269 | } 270 | 271 | /// Must be pre-ordered by StartMs 272 | public void SetSubtitleList(IList subtitleList) 273 | { 274 | _render.ClearLayer(DanmakuDefaultLayerDef.SubtitleLayerId); 275 | lock (_danmakuList) 276 | { 277 | for (int i = _danmakuList.Count - 1; i >= 0; i--) 278 | { 279 | if (_danmakuList[i].Mode == DanmakuMode.Subtitle) 280 | { 281 | _danmakuList.RemoveAt(i); 282 | } 283 | } 284 | 285 | if (subtitleList.Count > 0) 286 | { 287 | _hasSubtitle = true; 288 | 289 | int index1 = 0, index2 = 0; 290 | while (index1 < _danmakuList.Count && index2 < subtitleList.Count) 291 | { 292 | if (_danmakuList[index1].StartMs > subtitleList[index2].StartMs) 293 | { 294 | _danmakuList.Insert(index1, subtitleList[index2]); 295 | if (_lastTimeMs > 0 && subtitleList[index2].StartMs < _lastTimeMs && subtitleList[index2].StartMs + subtitleList[index2].DurationMs > _lastTimeMs) 296 | { 297 | _render.RenderDanmakuItem(DanmakuDefaultLayerDef.SubtitleLayerId, subtitleList[index2]); 298 | } 299 | index2++; 300 | } 301 | index1++; 302 | } 303 | if (index1 == _danmakuList.Count && index2 < subtitleList.Count) 304 | { 305 | for (; index2 < subtitleList.Count; index2++) 306 | { 307 | _danmakuList.Add(subtitleList[index2]); 308 | if (_lastTimeMs > 0 && subtitleList[index2].StartMs < _lastTimeMs && subtitleList[index2].StartMs + subtitleList[index2].DurationMs > _lastTimeMs) 309 | { 310 | _render.RenderDanmakuItem(DanmakuDefaultLayerDef.SubtitleLayerId, subtitleList[index2]); 311 | } 312 | } 313 | } 314 | 315 | if (_lastIndex >= _danmakuList.Count) 316 | { 317 | _lastIndex = _danmakuList.Count - 1; 318 | } 319 | } 320 | } 321 | } 322 | 323 | public static List GetSystemFontFamilyList() 324 | { 325 | List fontList = CanvasTextFormat.GetSystemFontFamilies(new[] { "zh-CN" }).ToList(); 326 | fontList.Sort(); 327 | return fontList; 328 | } 329 | 330 | private void Updater_DoWork(IAsyncAction action) 331 | { 332 | try 333 | { 334 | while (!_isClosing) 335 | { 336 | _updateEvent.WaitOne(); 337 | _pauseEvent.Wait(); 338 | 339 | uint currentTimeMs = 0; 340 | lock (_updateTimeQueue) 341 | { 342 | if (_updateTimeQueue.Count > 0) 343 | { 344 | currentTimeMs = _updateTimeQueue.Dequeue(); 345 | } 346 | } 347 | if (currentTimeMs == 0) 348 | { 349 | continue; 350 | } 351 | 352 | lock (_danmakuList) 353 | { 354 | // Check if app/thread has been suspended for a while or danmaku engine has been restarted 355 | if (currentTimeMs < _lastTimeMs || currentTimeMs - _lastTimeMs > 5000) 356 | { 357 | Logger.Log("Reseek after a long time suspension"); 358 | Seek(currentTimeMs); 359 | } 360 | else 361 | { 362 | _lastTimeMs = currentTimeMs; 363 | } 364 | 365 | bool subtitleRendered = false; 366 | while (currentTimeMs > 0 && _lastIndex < _danmakuList.Count && _danmakuList[_lastIndex].StartMs <= currentTimeMs) 367 | { 368 | if (_isClosing) 369 | { 370 | return; 371 | } 372 | if (_isSeeking) 373 | { 374 | break; 375 | } 376 | 377 | bool skip = false; 378 | 379 | if (_danmakuList[_lastIndex].IsRealtime) 380 | { 381 | _danmakuList[_lastIndex].IsRealtime = false; 382 | skip = true; 383 | } 384 | 385 | if (!skip && _isRenderEnabled) 386 | { 387 | uint layerId; 388 | switch (_danmakuList[_lastIndex].Mode) 389 | { 390 | case DanmakuMode.Bottom: 391 | { 392 | layerId = DanmakuDefaultLayerDef.BottomLayerId; 393 | break; 394 | } 395 | case DanmakuMode.Top: 396 | { 397 | layerId = DanmakuDefaultLayerDef.TopLayerId; 398 | break; 399 | } 400 | case DanmakuMode.ReverseRolling: 401 | { 402 | layerId = DanmakuDefaultLayerDef.ReverseRollingLayerId; 403 | break; 404 | } 405 | case DanmakuMode.Advanced: 406 | { 407 | layerId = DanmakuDefaultLayerDef.AdvancedLayerId; 408 | break; 409 | } 410 | case DanmakuMode.Subtitle: 411 | { 412 | subtitleRendered = true; 413 | layerId = DanmakuDefaultLayerDef.SubtitleLayerId; 414 | break; 415 | } 416 | default: 417 | { 418 | layerId = DanmakuDefaultLayerDef.RollingLayerId; 419 | break; 420 | } 421 | } 422 | _render.RenderDanmakuItem(layerId, _danmakuList[_lastIndex]); 423 | } 424 | 425 | _lastIndex++; 426 | } 427 | 428 | if (_subtitleIndexAfterSeek >= 0 && _subtitleIndexAfterSeek < _danmakuList.Count) 429 | { 430 | if (!subtitleRendered) 431 | { 432 | _render.RenderDanmakuItem(DanmakuDefaultLayerDef.SubtitleLayerId, _danmakuList[_subtitleIndexAfterSeek]); 433 | } 434 | _subtitleIndexAfterSeek = -1; 435 | } 436 | } 437 | } 438 | } 439 | catch (Exception ex) 440 | { 441 | Logger.Log(ex.Message); 442 | } 443 | finally 444 | { 445 | Logger.Log("Exited"); 446 | } 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/DanmakuFrostMaster.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {6F3A0BE3-1C98-4489-96D8-33018A6F5BD4} 8 | Library 9 | Properties 10 | DanmakuFrostMaster 11 | DanmakuFrostMaster 12 | en-US 13 | UAP 14 | 10.0.18362.0 15 | 10.0.16299.0 16 | 14 17 | 512 18 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | 20 | 21 | AnyCPU 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 27 | prompt 28 | 4 29 | 30 | 31 | AnyCPU 32 | pdbonly 33 | true 34 | bin\Release\ 35 | TRACE;NETFX_CORE;WINDOWS_UWP 36 | prompt 37 | 4 38 | 39 | 40 | x86 41 | true 42 | bin\x86\Debug\ 43 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 44 | ;2008 45 | full 46 | false 47 | prompt 48 | 49 | 50 | x86 51 | bin\x86\Release\ 52 | TRACE;NETFX_CORE;WINDOWS_UWP 53 | true 54 | ;2008 55 | pdbonly 56 | false 57 | prompt 58 | 59 | 60 | ARM 61 | true 62 | bin\ARM\Debug\ 63 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 64 | ;2008 65 | full 66 | false 67 | prompt 68 | 69 | 70 | ARM 71 | bin\ARM\Release\ 72 | TRACE;NETFX_CORE;WINDOWS_UWP 73 | true 74 | ;2008 75 | pdbonly 76 | false 77 | prompt 78 | 79 | 80 | ARM64 81 | true 82 | bin\ARM64\Debug\ 83 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 84 | ;2008 85 | full 86 | false 87 | prompt 88 | 89 | 90 | ARM64 91 | bin\ARM64\Release\ 92 | TRACE;NETFX_CORE;WINDOWS_UWP 93 | true 94 | ;2008 95 | pdbonly 96 | false 97 | prompt 98 | 99 | 100 | x64 101 | true 102 | bin\x64\Debug\ 103 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 104 | ;2008 105 | full 106 | false 107 | prompt 108 | 109 | 110 | x64 111 | bin\x64\Release\ 112 | TRACE;NETFX_CORE;WINDOWS_UWP 113 | true 114 | ;2008 115 | pdbonly 116 | false 117 | prompt 118 | 119 | 120 | PackageReference 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 6.2.10 136 | 137 | 138 | 13.0.1 139 | 140 | 141 | 1.26.0 142 | 143 | 144 | 145 | 14.0 146 | 147 | 148 | 155 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/DanmakuYSlotManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Atelier39 4 | { 5 | internal class DanmakuYSlotManager 6 | { 7 | private Slot[] _ySlotArray; 8 | private readonly Random _random; 9 | 10 | public DanmakuYSlotManager(uint length) 11 | { 12 | _ySlotArray = new Slot[length]; 13 | _random = new Random(); 14 | } 15 | 16 | public void UpdateLength(uint newLength) 17 | { 18 | lock (_ySlotArray) 19 | { 20 | _ySlotArray = new Slot[newLength]; 21 | } 22 | } 23 | 24 | /// 25 | /// Get an available Y position for Danmaku 26 | /// 27 | /// Is any slot occupied 28 | public bool GetY(uint danmakuId, uint height, out uint y) 29 | { 30 | if (height > _ySlotArray.Length) 31 | { 32 | // Danmaku's height is larger than total available height 33 | y = 0; 34 | return false; 35 | } 36 | 37 | lock (_ySlotArray) 38 | { 39 | uint index = 0; 40 | while (index + height < _ySlotArray.Length) 41 | { 42 | bool found = true; 43 | for (uint i = 0; i < height; i++) 44 | { 45 | if (_ySlotArray[index + i].Length > 0) 46 | { 47 | // Move to next available slot 48 | found = false; 49 | index = index + i + _ySlotArray[index + i].Length; 50 | break; 51 | } 52 | } 53 | if (found) 54 | { 55 | _ySlotArray[index].Id = danmakuId; 56 | _ySlotArray[index].Length = height; 57 | y = index; 58 | return true; 59 | } 60 | } 61 | // Can't find available slot, then return a random Y. 62 | y = (uint)_random.Next(0, _ySlotArray.Length - (int)height); 63 | return false; 64 | } 65 | } 66 | 67 | public void ReleaseYSlot(uint danmakuId, uint y) 68 | { 69 | lock (_ySlotArray) 70 | { 71 | if (y < _ySlotArray.Length && _ySlotArray[y].Id == danmakuId) 72 | { 73 | _ySlotArray[y].Id = 0; 74 | _ySlotArray[y].Length = 0; 75 | } 76 | } 77 | } 78 | 79 | /// 80 | /// Thread safe 81 | /// 82 | public void Clear() 83 | { 84 | lock (_ySlotArray) 85 | { 86 | for (int i = 0; i < _ySlotArray.Length; i++) 87 | { 88 | _ySlotArray[i].Id = 0; 89 | _ySlotArray[i].Length = 0; 90 | } 91 | } 92 | } 93 | 94 | private struct Slot 95 | { 96 | public uint Id; 97 | public uint Length; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/Logger.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using System.Runtime.CompilerServices; 4 | using Windows.Foundation.Diagnostics; 5 | 6 | namespace Atelier39 7 | { 8 | internal static class Logger 9 | { 10 | private static LoggingChannel _logChannel; 11 | 12 | public static void SetLogger(LoggingChannel loggingChannel) 13 | { 14 | _logChannel = loggingChannel; 15 | } 16 | 17 | public static void Log(string message, LoggingLevel level = LoggingLevel.Information, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0) 18 | { 19 | if (!string.IsNullOrWhiteSpace(filePath)) 20 | { 21 | filePath = Path.GetFileName(filePath); 22 | } 23 | message = $"{filePath}({lineNumber})->{memberName}(): {message}"; 24 | Debug.WriteLine(message); 25 | _logChannel?.LogMessage(message, level); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DanmakuFrostMaster/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DanmakuFrostMaster")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DanmakuFrostMaster")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Version information for an assembly consists of the following four values: 18 | // 19 | // Major Version 20 | // Minor Version 21 | // Build Number 22 | // Revision 23 | // 24 | // You can specify all the values or you can default the Build and Revision Numbers 25 | // by using the '*' as shown below: 26 | // [assembly: AssemblyVersion("1.0.*")] 27 | [assembly: AssemblyVersion("1.0.0.0")] 28 | [assembly: AssemblyFileVersion("1.0.0.0")] 29 | [assembly: ComVisible(false)] -------------------------------------------------------------------------------- /DanmakuFrostMaster/Properties/DanmakuFrostMaster.rd.xml: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /DanmakuFrostMasterDemo/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | -------------------------------------------------------------------------------- /DanmakuFrostMasterDemo/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices.WindowsRuntime; 6 | using Windows.ApplicationModel; 7 | using Windows.ApplicationModel.Activation; 8 | using Windows.Foundation; 9 | using Windows.Foundation.Collections; 10 | using Windows.UI.Xaml; 11 | using Windows.UI.Xaml.Controls; 12 | using Windows.UI.Xaml.Controls.Primitives; 13 | using Windows.UI.Xaml.Data; 14 | using Windows.UI.Xaml.Input; 15 | using Windows.UI.Xaml.Media; 16 | using Windows.UI.Xaml.Navigation; 17 | 18 | namespace DanmakuFrostMasterDemo 19 | { 20 | /// 21 | /// Provides application-specific behavior to supplement the default Application class. 22 | /// 23 | sealed partial class App : Application 24 | { 25 | /// 26 | /// Initializes the singleton application object. This is the first line of authored code 27 | /// executed, and as such is the logical equivalent of main() or WinMain(). 28 | /// 29 | public App() 30 | { 31 | this.InitializeComponent(); 32 | this.Suspending += OnSuspending; 33 | } 34 | 35 | /// 36 | /// Invoked when the application is launched normally by the end user. Other entry points 37 | /// will be used such as when the application is launched to open a specific file. 38 | /// 39 | /// Details about the launch request and process. 40 | protected override void OnLaunched(LaunchActivatedEventArgs e) 41 | { 42 | Frame rootFrame = Window.Current.Content as Frame; 43 | 44 | // Do not repeat app initialization when the Window already has content, 45 | // just ensure that the window is active 46 | if (rootFrame == null) 47 | { 48 | // Create a Frame to act as the navigation context and navigate to the first page 49 | rootFrame = new Frame(); 50 | 51 | rootFrame.NavigationFailed += OnNavigationFailed; 52 | 53 | if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) 54 | { 55 | //TODO: Load state from previously suspended application 56 | } 57 | 58 | // Place the frame in the current Window 59 | Window.Current.Content = rootFrame; 60 | } 61 | 62 | if (e.PrelaunchActivated == false) 63 | { 64 | if (rootFrame.Content == null) 65 | { 66 | // When the navigation stack isn't restored navigate to the first page, 67 | // configuring the new page by passing required information as a navigation 68 | // parameter 69 | rootFrame.Navigate(typeof(MainPage), e.Arguments); 70 | } 71 | // Ensure the current window is active 72 | Window.Current.Activate(); 73 | } 74 | } 75 | 76 | /// 77 | /// Invoked when Navigation to a certain page fails 78 | /// 79 | /// The Frame which failed navigation 80 | /// Details about the navigation failure 81 | void OnNavigationFailed(object sender, NavigationFailedEventArgs e) 82 | { 83 | throw new Exception("Failed to load Page " + e.SourcePageType.FullName); 84 | } 85 | 86 | /// 87 | /// Invoked when application execution is being suspended. Application state is saved 88 | /// without knowing whether the application will be terminated or resumed with the contents 89 | /// of memory still intact. 90 | /// 91 | /// The source of the suspend request. 92 | /// Details about the suspend request. 93 | private void OnSuspending(object sender, SuspendingEventArgs e) 94 | { 95 | var deferral = e.SuspendingOperation.GetDeferral(); 96 | //TODO: Save application state and stop any background activity 97 | deferral.Complete(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /DanmakuFrostMasterDemo/AssDemoPage.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |