├── .gitattributes ├── .gitignore ├── Arcaea Server 2.sln ├── LICENSE ├── README.md ├── Team123it.Arcaea.MarveCube.LinkPlay ├── Core │ ├── LinkPlayClass.cs │ ├── LinkPlayConstructor.cs │ ├── LinkPlayCrypto.cs │ └── LinkPlayParser.cs ├── GlobalProperties.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── System.Enhance (Part) │ └── System.Enhance.ConsoleWriter.cs ├── Team123it.Arcaea.MarveCube.LinkPlay.csproj ├── appsettings.Development.json └── appsettings.json ├── Team123it.Arcaea.MarveCube.Standalone ├── .config │ └── dotnet-tools.json ├── Controllers │ └── SongController.cs ├── Core │ └── StandaloneTokenHelper.cs ├── GlobalProperties.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── System.Enhance (Part) │ ├── System.Enhance.AspNetCore │ │ └── RealIpFetcherMiddleware.cs │ ├── System.Enhance.Collections.cs │ ├── System.Enhance.ConsoleWriter.cs │ ├── System.Enhance.Net.HttpWebRequest.cs │ ├── System.Enhance.Random.cs │ ├── System.Enhance.Security.Cryptography.cs │ └── System.Enhance.Web.Json.cs ├── Team123it.Arcaea.MarveCube.Standalone.csproj ├── appsettings.Development.json └── appsettings.json ├── Team123it.Arcaea.MarveCube ├── .config │ └── dotnet-tools.json ├── App.config ├── Bots │ └── Backgrounds.cs ├── Controllers │ ├── AuthController.cs │ ├── BotController.cs │ ├── ComposeController.cs │ ├── ExtController.cs │ ├── FriendController.cs │ ├── MultiplayerController.cs │ ├── PresentController.cs │ ├── PurchaseController.cs │ ├── ScoreController.cs │ ├── ServeController.cs │ ├── StandaloneController.cs │ ├── UserController.cs │ └── WorldController.cs ├── Core │ ├── ArcaeaAPIException.cs │ ├── BotAPIExceptions.cs │ ├── ItemType.cs │ ├── PlayerInfo.cs │ ├── QueryLimit.cs │ ├── SingleScore.cs │ ├── SongEnums.cs │ ├── SongNotFoundException.cs │ ├── StaminaPurchaseType.cs │ └── StandaloneToken.cs ├── FirstStart │ └── FirstStart.cs ├── FirstStartData │ ├── ConfigExample.json │ └── Initialization.sql ├── GlobalProperties.cs ├── Processors │ ├── Background │ │ ├── FixedDatas.cs │ │ ├── LeaderBoard.cs │ │ ├── Player.cs │ │ ├── SecurityManager.cs │ │ ├── Synchronization.cs │ │ ├── Tokens.cs │ │ └── World.cs │ └── Front │ │ ├── Auth.cs │ │ ├── Bot.cs │ │ ├── Compose.cs │ │ ├── Friend.cs │ │ ├── Multiplayer.cs │ │ ├── Present.cs │ │ ├── Purchase.cs │ │ ├── Score.cs │ │ ├── Serve.cs │ │ ├── User.cs │ │ └── World.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── System.Enhance (Part) │ ├── System.Enhance.AspNetCore │ │ ├── DeChunkerMiddleware.cs │ │ └── RealIpFetcherMiddleware.cs │ ├── System.Enhance.Collections.cs │ ├── System.Enhance.MySql.Data.cs │ ├── System.Enhance.Random.cs │ ├── System.Enhance.Security.Cryptography.cs │ └── System.Enhance.Web.Json.cs ├── Team123it.Arcaea.MarveCube.csproj ├── appsettings.Development.json └── appsettings.json └── docs ├── songinfo.md ├── userbest.md ├── userbest30.md └── userinfo.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.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 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | [Pp]ublish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | .idea 365 | -------------------------------------------------------------------------------- /Arcaea Server 2.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Team123it.Arcaea.MarveCube", "Team123it.Arcaea.MarveCube\Team123it.Arcaea.MarveCube.csproj", "{1871EC64-123D-4CA1-92E1-EE7F4B515359}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Team123it.Arcaea.MarveCube.Standalone", "Team123it.Arcaea.MarveCube.Standalone\Team123it.Arcaea.MarveCube.Standalone.csproj", "{B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C9EAB9AD-D507-4412-96D8-28F68C5B0AA4}" 11 | ProjectSection(SolutionItems) = preProject 12 | LICENSE = LICENSE 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B676BE8F-4E2A-40FF-8D38-CC55A6516747}" 17 | ProjectSection(SolutionItems) = preProject 18 | docs\songinfo.md = docs\songinfo.md 19 | docs\userbest.md = docs\userbest.md 20 | docs\userbest30.md = docs\userbest30.md 21 | docs\userinfo.md = docs\userinfo.md 22 | EndProjectSection 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Team123it.Arcaea.MarveCube.LinkPlay", "Team123it.Arcaea.MarveCube.LinkPlay\Team123it.Arcaea.MarveCube.LinkPlay.csproj", "{8C0D9B41-31C4-40DD-A33D-43771F81554C}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|ARM64 = Debug|ARM64 29 | Debug|x64 = Debug|x64 30 | Release|ARM64 = Release|ARM64 31 | Release|x64 = Release|x64 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Debug|ARM64.ActiveCfg = Debug|x64 35 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Debug|ARM64.Build.0 = Debug|x64 36 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Debug|x64.ActiveCfg = Debug|x64 37 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Debug|x64.Build.0 = Debug|x64 38 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Release|ARM64.ActiveCfg = Release|x64 39 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Release|ARM64.Build.0 = Release|x64 40 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Release|x64.ActiveCfg = Release|x64 41 | {1871EC64-123D-4CA1-92E1-EE7F4B515359}.Release|x64.Build.0 = Release|x64 42 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Debug|ARM64.ActiveCfg = Debug|ARM64 43 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Debug|ARM64.Build.0 = Debug|ARM64 44 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Debug|x64.ActiveCfg = Debug|x64 45 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Debug|x64.Build.0 = Debug|x64 46 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Release|ARM64.ActiveCfg = Release|ARM64 47 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Release|ARM64.Build.0 = Release|ARM64 48 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Release|x64.ActiveCfg = Release|x64 49 | {B8AD8196-3878-45D1-B3E3-1092B2A2DFC5}.Release|x64.Build.0 = Release|x64 50 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Debug|ARM64.ActiveCfg = Debug|Any CPU 51 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Debug|ARM64.Build.0 = Debug|Any CPU 52 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Debug|x64.ActiveCfg = Debug|Any CPU 53 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Debug|x64.Build.0 = Debug|Any CPU 54 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Release|ARM64.ActiveCfg = Release|Any CPU 55 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Release|ARM64.Build.0 = Release|Any CPU 56 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Release|x64.ActiveCfg = Release|Any CPU 57 | {8C0D9B41-31C4-40DD-A33D-43771F81554C}.Release|x64.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {B676BE8F-4E2A-40FF-8D38-CC55A6516747} = {C9EAB9AD-D507-4412-96D8-28F68C5B0AA4} 64 | EndGlobalSection 65 | GlobalSection(ExtensibilityGlobals) = postSolution 66 | SolutionGuid = {646E1181-A60C-4A0B-AA6D-85C76E0CE72B} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 123 Open-Source Organization MIT Public License v2.0 2 | 3 | 4 | 5 | Copyright (C) 2015-2022 123 Open-Source Organization 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | 10 | 11 | 1. If you want to use the Software which is any-person-accessable, you can public or not public the Software by any way. 12 | 13 | 14 | 15 | 2. If you want to use the Software which only give access to some people, you can use the Software without limit, but you should not public the Software by any way including: 16 | 17 | 18 | 19 | Sharing with other persons who don't allowed to access the Software; 20 | 21 | Distrubuting the raw Software without grants 22 | 23 | 24 | 25 | or the Software copyright owner have the right to take legal actions, includes but not limited to: 26 | 27 | 28 | 29 | Require you to stop using the Software and/or the modified Software; 30 | 31 | Require you to apologize your action and publish it; 32 | 33 | Recover the income from the Software and/or the modified Software 34 | 35 | 36 | 37 | and/or achieve the requirements and the rights by filing lawsuits. 38 | 39 | 40 | 41 | 3. If you want to use the Software which only give access to some people and is expressly prohibited to modify the Software, you should obey the notice in section 2 and the following notice: 42 | 43 | 44 | 45 | Never modify it for any reasons, including repairing bugs(if this happens, you should submit an issue in the source of the Software,like Github, Gitlab or E-mail instead of repairing bugs by yourself). 46 | 47 | 48 | 49 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arcaea Server 2 2 | 3 | 高并发低占用的 Arcaea API 后端 基于ASP.NET Core 6.0 4 | 5 | ##### 支持的Arcaea客户端版本 6 | 7 | * Arcaea 3.10.0(c) 及以上版本 8 | 9 | ##### 项目结构 10 | 11 | * [Team123it.Arcaea.MarveCube](./Team123it.Arcaea.MarveCube) - Arcaea Server 2 主服务器后端 12 | * [Team123it.Arcaea.MarveCube.Standalone](./Team123it.Arcaea.MarveCube.Standalone) - Arcaea Server 2 独立下载服务器后端 13 | * [Team123it.Arcaea.MarveCube.LinkPlay](./Team123it.Arcaea.MarveCube.LinkPlay) - Arcaea Server 2 独立LinkPlay后端 14 | 15 | ##### 运行环境(主服务器程序与下载服务器均需要) 16 | 17 | * Microsoft.AspNetCore.App x64 运行时 6.0.0 及以上版本 18 | 19 | ##### 额外依赖环境(仅主服务器程序需要) 20 | 21 | * MySQL 8.0+ / MariaDB 10.0+ (用于存储服务器数据) 22 | * Redis 6.0+ (Windows端为Redis for Windows 3.0+) (用于存放下载Token等临时数据) 23 | 24 | ##### 最新版本(v0.3.7+)API地址(Endpoint) 25 | 26 | `{Your Domain}/years/19/` 27 | 28 | ##### 特点 29 | 30 | * 对于曲包id为 `unranked` 或难度定数为0(不存在应为-1)的曲目,程序会将成绩存储至 `bests_special` 而并非 `bests` 表中,因此这些曲目将不计入Best30计算 31 | * 玩家的个人游玩潜力值计算中仅存在Best30,不存在Recent10,因此潜力值在任何情况下都不会出现倒扣的情况 32 | * 玩家在曲目游玩结束并提交成绩后会视成绩以及难度给予玩家一定数量的记忆源点 33 | 34 | ##### 搭建之前…… 35 | 36 | 独立的下载服务器(Team123it.Arcaea.MarveCube.Standalone)可以与主服务器程序放在一起; 37 | 但我们极力建议您将其放在与主服务器不同的、带宽较为充足的服务器上,以减轻主服务器的带宽负担,并增强主服务器的安全性。 38 | 39 | 主服务器和下载服务器都需要存在谱面文件夹(包括该曲目的ogg音频文件以及aff谱面文件)(位置在 `{程序根目录}\data\static\Songs` ),其作用如下: 40 | 41 | 1. 下载服务器为玩家提供数据下载 42 | 2. 主服务器在玩家提交数据后检查MD5校验值是否正确 43 | 3. 主服务器在玩家登录账号后返回所有谱面以及音频文件的MD5校验值 44 | 45 | ##### 运行之前…… 46 | 47 | 1. 启动数据库程序 & Redis程序 48 | 2. 启动主服务器程序并按照提示进行初始化 49 | 50 | ##### 注意事项 51 | 52 | * 为减轻文件读写压力,Arcaea Server 2 服务端在第一次收到登录请求时,会将谱面文件夹中的所有ogg音频文件以及aff谱面文件的MD5校验值,保存在数据库的 `fixed_songs_checksum` 表中。 53 | * 若后续出现再次登录将直接返回数据库中存储的校验值而并非重新遍历计算校验值。 54 | * 但使用该方法时可能会出现谱面文件/音频文件需要更新的情况,这时请手动删除 `fixed_songs_checksum` 表中的对应文件的MD5校验值项,下一位玩家登录后将会自动更新MD5校验值。 55 | 56 | ##### 关于Link Play 57 | 58 | * 当前暂时不支持Link Play游玩,还请等待后续更新。 59 | 60 | ##### 关于Bot查分接口 61 | 62 | * Arcaea Server 2 服务端支持和 ArcaeaUnlimitedAPI 同样格式的数据查询,可以让QQ查分机器人接入服务器进行查分操作 63 | * 接口存在于以下位置 64 | 65 | ```url 66 | {API_ENDPOINT}/botarcapi 67 | ``` 68 | 69 | * 接口文档 70 | 71 | + [user/best30](/docs/userbest30.md) 72 | + [user/best](/docs/userbest.md) 73 | + [user/info](/docs/userinfo.md) 74 | + [song/info](/docs/songinfo.md) 75 | 76 | ##### 开源协议 77 | 78 | 本企划基于[123 Open-Source Organization MIT Public License 2.0](https://team123it.github.io/LICENSE.html)许可协议开源。 79 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/Core/LinkPlayConstructor.cs: -------------------------------------------------------------------------------- 1 | namespace Team123it.Arcaea.MarveCube.LinkPlay.Core 2 | { 3 | public class LinkPlayConstructor 4 | { 5 | public static LinkPlayConstructor CreateInstance() 6 | { 7 | return new LinkPlayConstructor(); 8 | } 9 | 10 | public static byte[] Command0C(Room room) 11 | { 12 | var returnedBytes = new List(); 13 | var packPrefix = new byte[] {0x06, 0x16, 0x0C, 0x09}; 14 | returnedBytes.AddRange(packPrefix); 15 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.RoomId)); 16 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CommandQueueLength)); 17 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Players[0].LastTimestamp)); 18 | returnedBytes.AddRange(BytesHelper.Int2Bytes((int)room.RoomState)[..1]); 19 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CountDown)); 20 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Timestamp)); 21 | return returnedBytes.ToArray(); 22 | } 23 | 24 | public static byte[] Command12(Room room, uint playerIndex) 25 | { 26 | var returnedBytes = new List(); 27 | var player = room.Players[playerIndex]; 28 | var packPrefix = new byte[] {0x06, 0x16, 0x12, 0x09}; 29 | returnedBytes.AddRange(packPrefix); 30 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.RoomId)); 31 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CommandQueueLength)); 32 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(player.LastTimestamp)); 33 | returnedBytes.AddRange(BitConverter.GetBytes(playerIndex)[..1]); 34 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(player.PlayerId)); 35 | returnedBytes.AddRange(BytesHelper.Int2Bytes(player.CharacterId)[..1]); 36 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(player.IsCharacterUncapped)[..1]); 37 | returnedBytes.AddRange(BytesHelper.Int2Bytes((int)player.Difficulty)[..1]); 38 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(player.Score)); 39 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(player.Timer)); 40 | returnedBytes.AddRange(BytesHelper.Int2Bytes((int)player.ClearType)[..1]); 41 | returnedBytes.AddRange(BytesHelper.Uint2Bytes((uint)player.PlayerState)[..1]); 42 | returnedBytes.AddRange(BytesHelper.Int2Bytes(player.DownloadPercent)[..1]); 43 | returnedBytes.AddRange(BytesHelper.Int2Bytes(player.OnlineState)[..1]); 44 | return returnedBytes.ToArray(); 45 | } 46 | 47 | public static byte[] Command13(Room room) 48 | { 49 | var returnedBytes = new List(); 50 | var packPrefix = new byte[] {0x06, 0x16, 0x13, 0x09}; 51 | returnedBytes.AddRange(packPrefix); 52 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.RoomId)); 53 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CommandQueueLength)); 54 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Players[0].LastTimestamp)); 55 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.HostId)); 56 | returnedBytes.AddRange(BytesHelper.Int2Bytes((int)room.RoomState)[..1]); 57 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CountDown)); 58 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Timestamp)); 59 | returnedBytes.AddRange(BytesHelper.Ushort2Bytes(room.SongIdx)); 60 | returnedBytes.AddRange(BytesHelper.Ushort2Bytes(room.Interval)); 61 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Times)[..7]); 62 | returnedBytes.AddRange(room.GetPlayerLastScore()); 63 | returnedBytes.AddRange(BytesHelper.Ushort2Bytes(room.LastSongIdx)); 64 | returnedBytes.AddRange(BytesHelper.Int2Bytes(room.RoundSwitch)[..1]); 65 | return returnedBytes.ToArray(); 66 | } 67 | 68 | public static byte[] Command15(Room room) 69 | { 70 | var returnedBytes = new List(); 71 | var packPrefix = new byte[] {0x06, 0x16, 0x15, 0x09}; 72 | returnedBytes.AddRange(packPrefix); 73 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.RoomId)); 74 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CommandQueueLength)); 75 | returnedBytes.AddRange(room.GetPlayerInfo()); 76 | returnedBytes.AddRange(room.SongUnlock); 77 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.HostId)); 78 | returnedBytes.AddRange(BytesHelper.Int2Bytes((int)room.RoomState)[..1]); 79 | returnedBytes.AddRange(BytesHelper.Uint2Bytes(room.CountDown)); 80 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Timestamp)); 81 | returnedBytes.AddRange(BytesHelper.Ushort2Bytes(room.SongIdx)); 82 | returnedBytes.AddRange(BytesHelper.Ushort2Bytes(room.Interval)); 83 | returnedBytes.AddRange(BytesHelper.Ulong2Bytes(room.Times)[..7]); 84 | returnedBytes.AddRange(room.GetPlayerLastScore()); 85 | returnedBytes.AddRange(BytesHelper.Ushort2Bytes(room.LastSongIdx)); 86 | returnedBytes.AddRange(BytesHelper.Int2Bytes(room.RoundSwitch)[..1]); 87 | return returnedBytes.ToArray(); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/Core/LinkPlayCrypto.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace Team123it.Arcaea.MarveCube.LinkPlay.Core 4 | { 5 | public class LinkPlayCrypto 6 | { 7 | private static readonly byte[] DefaultKey = {0x11, 0x45, 0x14, 0x19, 0x19, 0x19, 0x18, 0x00, 0x11, 0x45, 0x14, 0x19, 0x19, 0x19, 0x18, 0x00}; 8 | 9 | public static byte[] EncryptPack(byte[] token, byte[] body) 10 | { 11 | var random = new Random(); 12 | var iv = new byte[12]; random.NextBytes(iv); 13 | var pad = 16 - (body.Length % 16); // pkcs7 padding 14 | var padding = Enumerable.Repeat((byte)pad, pad).ToArray(); var padded = body.Concat(padding).ToArray(); 15 | var cipher = new byte[body.Length+pad]; var authTag = new byte[12]; 16 | using var aes = new AesGcm(DefaultKey); 17 | aes.Encrypt(iv, padded, cipher, authTag); 18 | var returnBytes = token.Concat(iv).Concat(authTag).Concat(cipher); 19 | return returnBytes.ToArray(); 20 | } 21 | 22 | public static byte[] DecryptPack(byte[] data) 23 | { 24 | var iv = data[8..20]; 25 | var authTag = data[20..32]; 26 | var cipher = data[32..]; 27 | var decrypted = new byte[cipher.Length]; 28 | using var aes = new AesGcm(DefaultKey); 29 | aes.Decrypt(iv, cipher, authTag, decrypted); 30 | 31 | var pad = decrypted[^1]; // removal of pkcs7 padding 32 | return decrypted.Take(decrypted.Length - pad).ToArray(); 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/Core/LinkPlayParser.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Newtonsoft.Json.Linq; 3 | using StackExchange.Redis; 4 | using static Team123it.Arcaea.MarveCube.LinkPlay.GlobalProperties; 5 | using static Team123it.Arcaea.MarveCube.LinkPlay.RoomManager; 6 | 7 | namespace Team123it.Arcaea.MarveCube.LinkPlay.Core 8 | { 9 | public class LinkPlayParser 10 | { 11 | public static void LinkPlayResp(byte[] data, EndPoint endPoint) 12 | { 13 | var returnBytes = new List(); 14 | switch (data[2]) 15 | { 16 | case 0x09: 17 | { 18 | returnBytes.AddRange(Ping(data)); 19 | break; 20 | } 21 | } 22 | Program.SendMsg(returnBytes.ToArray(), data[4..12], endPoint); 23 | } 24 | 25 | private static byte[] Ping(byte[] data) 26 | { 27 | var roomId = FetchRoomIdByToken(data[4..12]); 28 | var mDatabaseConnectUrl = $"{RedisServerUrl}:{RedisServerPort},password={RedisServerPassword}"; 29 | var conn = ConnectionMultiplexer.Connect(mDatabaseConnectUrl); 30 | var db = conn.GetDatabase(); 31 | var redisRoom = JObject.Parse(db.StringGet($"Arcaea-LinkPlay-{roomId}")); 32 | if (FetchRoomById(roomId) is null) 33 | { 34 | var room = new Room 35 | { 36 | RoomId = Convert.ToUInt64(roomId), 37 | RoomCode = redisRoom.Value("roomCode") 38 | }; 39 | var player = new Player 40 | { 41 | Token = BitConverter.ToUInt64(data[4..12]), 42 | StartCommandCount = BitConverter.ToInt32(data[12..16]), 43 | LastTimestamp = BitConverter.ToUInt64(data[16..24]), 44 | Score = BitConverter.ToUInt32(data[24..28]), 45 | Timer = BitConverter.ToUInt32(data[28..32]), 46 | PlayerState = (PlayerStates) data[32], 47 | Difficulty = (Difficulties) data[33], 48 | ClearType = (ClearTypes) data[34], 49 | DownloadPercent = data[35], 50 | CharacterId = data[36], 51 | IsCharacterUncapped = data[37] 52 | }; 53 | room.Players = new[] {player}; 54 | RegisterRoom(room, roomId); 55 | 56 | var returnBytes = LinkPlayConstructor.Command0C(room); 57 | return returnBytes; 58 | } 59 | else 60 | { 61 | var room = FetchRoomById(roomId)!.Value; 62 | var token = BitConverter.ToUInt64(data[4..12]).ToString(); 63 | if (redisRoom.Value("token")!.ToObject>()!.Contains(token)) 64 | { 65 | var returnBytes = LinkPlayConstructor.Command0C(room); 66 | return returnBytes; 67 | } 68 | else 69 | { 70 | for (var i = 0; i < 4; ++i) 71 | { 72 | if(room.Players[i].PlayerId != 0) 73 | { 74 | room.Players[i] = new Player() 75 | { 76 | Token = BitConverter.ToUInt64(data[4..12]), 77 | StartCommandCount = BitConverter.ToInt32(data[12..16]), 78 | LastTimestamp = BitConverter.ToUInt64(data[16..24]), 79 | Score = BitConverter.ToUInt32(data[24..28]), 80 | Timer = BitConverter.ToUInt32(data[28..32]), 81 | PlayerState = (PlayerStates) data[32], 82 | Difficulty = (Difficulties) data[33], 83 | ClearType = (ClearTypes) data[34], 84 | DownloadPercent = data[35], 85 | CharacterId = data[36], 86 | IsCharacterUncapped = data[37] 87 | }; 88 | } 89 | } 90 | UnRegisterRoom(roomId); 91 | RegisterRoom(room, roomId); 92 | var returnBytes = LinkPlayConstructor.Command0C(room); 93 | return returnBytes; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/GlobalProperties.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.Text; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using StackExchange.Redis; 6 | using Team123it.Arcaea.MarveCube.LinkPlay.Core; 7 | using static Team123it.Arcaea.MarveCube.LinkPlay.GlobalProperties; 8 | 9 | namespace Team123it.Arcaea.MarveCube.LinkPlay 10 | { 11 | public static class RoomManager 12 | { 13 | private static Dictionary _rooms = new(); 14 | public static void RegisterRoom(Room room, string roomId) { _rooms.Add(roomId, room); } 15 | public static void UnRegisterRoom(string roomId) { _rooms.Remove(roomId); } 16 | public static Room? FetchRoomById(string roomId) 17 | { 18 | if (_rooms.TryGetValue(roomId, out var room)) { return room; } 19 | else { return null; } 20 | } 21 | public static string FetchRoomIdByToken(byte[] data) 22 | { 23 | var mDatabaseConnectUrl = $"{RedisServerUrl}:{RedisServerPort},password={RedisServerPassword}"; 24 | var conn = ConnectionMultiplexer.Connect(mDatabaseConnectUrl); 25 | var db = conn.GetDatabase(); 26 | var roomData = JObject.Parse(db.StringGet($"Arcaea-LinkPlayToken-{BitConverter.ToUInt64(data)}")); 27 | var roomId = roomData.Value("roomId"); 28 | conn.Close(); 29 | return roomId!; 30 | } 31 | } 32 | 33 | /// 34 | /// 提供适用于 的全局属性的类。无法继承此类。 35 | /// 36 | public static class GlobalProperties 37 | { 38 | /// 39 | /// 获取当前服务器是否在维护中的标志。 40 | /// 注:为防止出现数据丢失或损坏或发生意外情况,获取过程中发生任何异常都将视为正在维护中。 41 | /// 42 | public static bool Maintaining 43 | { 44 | get 45 | { 46 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 47 | { 48 | try 49 | { 50 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 51 | if (settings.Value("settings")!.Value("isMaintaining")) { return true; } 52 | else { return false; } 53 | } 54 | catch { return true; } 55 | } 56 | else { return true; } 57 | } 58 | } 59 | 60 | public static string MultiplayerServerUrl 61 | { 62 | get 63 | { 64 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 65 | { 66 | try 67 | { 68 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 69 | var config = settings.Value("config"); 70 | string prefix = config!.Value("multiplayerServerUrl")!; 71 | return prefix; 72 | } 73 | catch 74 | { 75 | return null; 76 | } 77 | } 78 | else 79 | { 80 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 81 | } 82 | } 83 | } 84 | 85 | public static int MultiplayerServerPort 86 | { 87 | get 88 | { 89 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 90 | { 91 | try 92 | { 93 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 94 | var config = settings.Value("config"); 95 | int key = config!.Value("multiplayerServerPort"); 96 | return key; 97 | } 98 | catch 99 | { 100 | return 0; 101 | } 102 | } 103 | else 104 | { 105 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 106 | } 107 | } 108 | } 109 | 110 | /// 111 | /// 获取API的监听端口。 112 | /// 113 | /// 114 | /// 115 | public static string RedisServerUrl 116 | { 117 | get 118 | { 119 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 120 | { 121 | try 122 | { 123 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 124 | var config = settings.Value("config"); 125 | return config!.Value("redisServerUrl")!; 126 | } 127 | catch (Exception ex) 128 | { 129 | throw new JsonException($"配置文件 {Path.Combine(AppContext.BaseDirectory, "data", "config.json")} 读取失败: {ex.Message}"); 130 | } 131 | } 132 | else 133 | { 134 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 135 | } 136 | } 137 | } 138 | 139 | public static int RedisServerPort 140 | { 141 | get 142 | { 143 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 144 | { 145 | try 146 | { 147 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 148 | var config = settings.Value("config"); 149 | return config!.Value("redisServerPort"); 150 | } 151 | catch (Exception ex) 152 | { 153 | throw new JsonException($"配置文件 {Path.Combine(AppContext.BaseDirectory, "data", "config.json")} 读取失败: {ex.Message}"); 154 | } 155 | } 156 | else 157 | { 158 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 159 | } 160 | } 161 | } 162 | 163 | public static string RedisServerPassword 164 | { 165 | get 166 | { 167 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 168 | { 169 | try 170 | { 171 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 172 | var config = settings.Value("config"); 173 | return config!.Value("redisServerPassword")!; 174 | } 175 | catch (Exception ex) 176 | { 177 | throw new JsonException($"配置文件 {Path.Combine(AppContext.BaseDirectory, "data", "config.json")} 读取失败: {ex.Message}"); 178 | } 179 | } 180 | else 181 | { 182 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 183 | } 184 | } 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:39998", 8 | "sslPort": 44322 9 | } 10 | }, 11 | "profiles": { 12 | "Team123it.Arcaea.MarveCube.LinkPlay": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7296;http://localhost:5044", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/Team123it.Arcaea.MarveCube.LinkPlay.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | MarveCube 6 | 0.4.3 7 | 123 Open-Source Organization 8 | Arcaea Server 2 Standalone Link Play Server Part 9 | 0.4.4 10 | 0.4.3 11 | (C)Copyright 2015-2022 123 Open-Source Organization. All rights reserved. 12 | Arcaea Server 2 - High-Speed Protable Arcaea API Server 13 | enable 14 | enable 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.LinkPlay/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "6.0.1", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/Controllers/SongController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Enhance.Security.Cryptography; 3 | using System.Globalization; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using Team123it.Arcaea.MarveCube.Standalone.Core; 7 | using File2 = System.IO.File; 8 | 9 | namespace Team123it.Arcaea.MarveCube.Standalone.Controllers 10 | { 11 | [Route("song")] 12 | [ApiController] 13 | public class SongController : ControllerBase 14 | { 15 | private static readonly string[] Files = { "base.ogg", "3.ogg", "0.aff", "1.aff", "2.aff", "3.aff" }; 16 | 17 | [HttpGet("download")] 18 | public async Task DownloadSongData([FromQuery]string sid, [FromQuery]string file, [FromQuery]string token) 19 | { 20 | return await Task.Run(new Func(() => 21 | { 22 | try 23 | { 24 | if (!string.IsNullOrWhiteSpace(sid) && !string.IsNullOrWhiteSpace(file) && !string.IsNullOrWhiteSpace(token)) 25 | { 26 | token = token.Replace(" ", string.Empty); 27 | string plainToken = RC4Helper.Decrypt(token, StandaloneTokenHelper.GetToken().Result); 28 | // plainToken格式: "{userId}-{songId}-{DateTime.Now:yyyyMMddHHmmssfff}" 29 | int userId = int.Parse(plainToken.Split('-')[0]); 30 | string songId = plainToken.Split('-')[1]; 31 | var createTime = DateTime.ParseExact(plainToken.Split('-')[2], "yyyyMMddHHmmssfff", CultureInfo.CurrentCulture); 32 | if ((createTime - DateTime.Now).TotalHours < 1.5 && songId == sid) 33 | { 34 | if (Array.IndexOf(Files, file) != -1 && File2.Exists(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", sid, file))) 35 | { 36 | byte[] data = File2.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", sid, file)); 37 | Console.WriteLine($@"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][200 OK] Fetched file {sid}\{file}"); 38 | return new FileContentResult(data, "application/octet-stream") 39 | { 40 | FileDownloadName = file 41 | }; 42 | } 43 | else 44 | { 45 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][404 Not Found] Cannot find song file \"{sid}/{file}\". Please check your spelling and try again."); 46 | return NotFound("Cannot find song file. Please check your spelling and try again."); 47 | } 48 | } 49 | else 50 | { 51 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][401 Unauthorized] Download link has been expired. Please fetch a new download link from Project Arcaea Server."); 52 | return Unauthorized("Download link has been expired. Please fetch a new download link from Project Arcaea Server."); 53 | } 54 | } 55 | else 56 | { 57 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][400 Bad Request] Bad request."); 58 | return BadRequest("Bad request."); 59 | } 60 | } 61 | catch (FormatException) 62 | { 63 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][400 Bad Request] Invalid arguments.\n" + 64 | $"sid={sid}\n" + 65 | $"file={file}\n" + 66 | $"token={token}"); 67 | return BadRequest("Invalid arguments."); 68 | } 69 | catch(Exception ex) 70 | { 71 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][500 Internal Server Error] An unexpected error occurred."); 72 | Console.WriteLine(ex.ToString()); 73 | return new ContentResult() 74 | { 75 | Content = "An unexpected error occurred. Please contact 123 Open-Source Organization(Team123it) to solve the problem.", 76 | ContentType = "text/html", 77 | StatusCode = StatusCodes.Status500InternalServerError 78 | }; 79 | } 80 | })); 81 | } 82 | 83 | [HttpGet("test")] 84 | public async Task TestFetchSongData([FromQuery]string sid, [FromQuery]string token) 85 | { 86 | return await Task.Run(new Func(() => 87 | { 88 | try 89 | { 90 | if (!string.IsNullOrWhiteSpace(sid) && !string.IsNullOrWhiteSpace(token)) 91 | { 92 | if (token == StandaloneTokenHelper.GetToken().Result) 93 | { 94 | if (Directory.Exists(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", sid))) 95 | { 96 | var folder = new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", sid)); 97 | string[] fileExts = new[] { ".aff", ".ogg" }; 98 | var files = new List(); 99 | foreach (string ext in fileExts) 100 | { 101 | files.AddRange(from file in folder.GetFiles(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", sid)) 102 | where file.FullName.ToLower().EndsWith(ext) 103 | select file); 104 | } 105 | var r = new StringBuilder("Project Arcaea Standalone API Song Data Test Result\n") 106 | .Append("Song Id(sid):").AppendLine(sid); 107 | int fileCount = 0; 108 | foreach (var file in files) 109 | { 110 | r.Append("File Name: ").AppendLine(file.Name); 111 | r.Append("MD5 Checksum Result: "); 112 | byte[] md5RawHash = MD5.HashData(File2.ReadAllBytes(file.FullName)); 113 | string md5Hash = BitConverter.ToString(md5RawHash).Replace("-", string.Empty).ToLower(); 114 | r.AppendLine(md5Hash); 115 | fileCount++; 116 | } 117 | r.Append("Total File(s) Count: ").Append(fileCount); 118 | return Ok(r.ToString()); 119 | } 120 | else 121 | { 122 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][404 Not Found]Test failed: Cannot find data of the song '{sid}'"); 123 | return NotFound($"Test failed: Cannot find data of the song '{sid}'."); 124 | } 125 | } 126 | else 127 | { 128 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][403 Forbidden]Invalid test token: {token}."); 129 | return Forbid("Invalid token."); 130 | } 131 | } 132 | else 133 | { 134 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][400 Bad Request] Bad request."); 135 | return BadRequest("Bad request."); 136 | } 137 | } 138 | catch (FormatException) 139 | { 140 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][400 Bad Request] Invalid arguments.\n" + 141 | $"sid={sid}\n" + 142 | $"token={token}"); 143 | return BadRequest("Invalid arguments."); 144 | } 145 | catch (Exception ex) 146 | { 147 | Console.WriteLine($"[{DateTime.Now:yyyy-M-d H:mm:ss.fff}][{HttpContext.Connection.RemoteIpAddress}][500 Internal Server Error] An unexpected error occurred."); 148 | Console.WriteLine(ex.ToString()); 149 | return new ContentResult() 150 | { 151 | Content = "An unexpected error occurred. Please contact 123 Open-Source Organization(Team123it) to solve the problem.", 152 | ContentType = "text/html", 153 | StatusCode = StatusCodes.Status500InternalServerError 154 | }; 155 | } 156 | })); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/Core/StandaloneTokenHelper.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.Standalone.GlobalProperties; 2 | using Newtonsoft.Json.Linq; 3 | using System.Enhance.Net; 4 | 5 | namespace Team123it.Arcaea.MarveCube.Standalone.Core 6 | { 7 | public static class StandaloneTokenHelper 8 | { 9 | public static async Task GetToken() 10 | { 11 | try 12 | { 13 | string? response = await HttpWebRequest.SendHttpRequestAsync($"{MainServerURLPrefix}/standalone/token?StandaloneKey={StandaloneKey}"); 14 | if (!string.IsNullOrWhiteSpace(response)) 15 | { 16 | var respData = JObject.Parse(response); 17 | if (respData.Value("success")) 18 | { 19 | string token = respData.Value("value"); 20 | return token; 21 | } 22 | else 23 | { 24 | return string.Empty; 25 | } 26 | } 27 | else 28 | { 29 | return string.Empty; 30 | } 31 | } 32 | catch (Exception ex) 33 | { 34 | await Task.FromException(ex); 35 | return string.Empty; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/GlobalProperties.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using System.Text; 5 | 6 | namespace Team123it.Arcaea.MarveCube.Standalone 7 | { 8 | /// 9 | /// 提供适用于 的全局属性的类。无法继承此类。 10 | /// 11 | public static class GlobalProperties 12 | { 13 | /// 14 | /// 获取当前服务器是否在维护中的标志。 15 | /// 注:为防止出现数据丢失或损坏或发生意外情况,获取过程中发生任何异常都将视为正在维护中。 16 | /// 17 | public static bool Maintaining 18 | { 19 | get 20 | { 21 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 22 | { 23 | try 24 | { 25 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 26 | if (settings.Value("settings").Value("isMaintaining")) 27 | { 28 | return true; 29 | } 30 | else 31 | { 32 | return false; 33 | } 34 | } 35 | catch 36 | { 37 | return true; 38 | } 39 | } 40 | else 41 | { 42 | return true; 43 | } 44 | } 45 | } 46 | /// 47 | /// 获取HTTPS证书密码。 48 | /// 若证书密码设置项不存在则返回 49 | /// 50 | /// 51 | public static string? HttpsCertificatePassword 52 | { 53 | get 54 | { 55 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 56 | { 57 | try 58 | { 59 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 60 | var config = settings.Value("config"); 61 | string pass = Encoding.UTF8.GetString(Convert.FromBase64String(config.Value("httpsCerPass"))); 62 | return pass; 63 | } 64 | catch 65 | { 66 | return null; 67 | } 68 | } 69 | else 70 | { 71 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 72 | } 73 | } 74 | } 75 | 76 | public static string MainServerURLPrefix 77 | { 78 | get 79 | { 80 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 81 | { 82 | try 83 | { 84 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 85 | var config = settings.Value("config"); 86 | string prefix = config.Value("mainServerURLPrefix"); 87 | return prefix; 88 | } 89 | catch 90 | { 91 | return null; 92 | } 93 | } 94 | else 95 | { 96 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 97 | } 98 | } 99 | } 100 | 101 | public static string StandaloneKey 102 | { 103 | get 104 | { 105 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 106 | { 107 | try 108 | { 109 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 110 | var config = settings.Value("config"); 111 | string key = config.Value("standaloneKey"); 112 | return key; 113 | } 114 | catch 115 | { 116 | return null; 117 | } 118 | } 119 | else 120 | { 121 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 122 | } 123 | } 124 | } 125 | 126 | /// 127 | /// 获取API的监听端口。 128 | /// 129 | /// 130 | /// 131 | public static int ListenPort 132 | { 133 | get 134 | { 135 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 136 | { 137 | try 138 | { 139 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 140 | var config = settings.Value("config"); 141 | return config.Value("listenPort"); 142 | } 143 | catch (Exception ex) 144 | { 145 | throw new JsonException($"配置文件 {Path.Combine(AppContext.BaseDirectory, "data", "config.json")} 读取失败: {ex.Message}"); 146 | } 147 | } 148 | else 149 | { 150 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 151 | } 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/Program.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misaka12456/ArcaeaServer2/2bca55a17fa5248e23fea314b99e6086ba423edd/Team123it.Arcaea.MarveCube.Standalone/Program.cs -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:41144", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "Team123it.Arcaea.MarveCube.Standalone": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "weatherforecast", 17 | "applicationUrl": "http://localhost:5186", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "weatherforecast", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http.Features; 2 | using Microsoft.AspNetCore.HttpOverrides; 3 | 4 | namespace Team123it.Arcaea.MarveCube.Standalone 5 | { 6 | public class Startup 7 | { 8 | public Startup(IConfiguration configuration) 9 | { 10 | Configuration = configuration; 11 | } 12 | 13 | public IConfiguration Configuration { get; } 14 | 15 | // This method gets called by the runtime. Use this method to add services to the container. 16 | public void ConfigureServices(IServiceCollection services) 17 | { 18 | services.AddControllers(); 19 | services.Configure(options => options.BufferBody = true); 20 | // services.AddTransient(); 21 | } 22 | 23 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 24 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 25 | { 26 | if (env.IsDevelopment()) 27 | { 28 | app.UseDeveloperExceptionPage(); 29 | } 30 | app.UseRouting(); 31 | app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); 32 | // app.UseMiddleware(); 33 | app.UseEndpoints(endpoints => 34 | { 35 | endpoints.MapControllers(); 36 | }); 37 | 38 | app.Run(async context => 39 | { 40 | Console.ForegroundColor = ConsoleColor.Yellow; 41 | Console.WriteLine($"{DateTime.Now:[yyyy-M-d H:mm:ss]} Someone is trying to visit api without logining before.\n" + 42 | $"IP:{context.Connection.RemoteIpAddress}\n" + 43 | $"Visited Path:{context.Request.Path}"); 44 | Console.ResetColor(); 45 | await context.Response.WriteAsync("Sorry but this is not what you are waiting for...\n"); 46 | await context.Response.WriteAsync($"Your IP:{context.Connection.RemoteIpAddress}\n"); 47 | await context.Response.WriteAsync($"Current Path: {context.Request.Path}\n"); 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/System.Enhance (Part)/System.Enhance.AspNetCore/RealIpFetcherMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace System.Enhance.AspNetCore 4 | { 5 | public class RealIpFetcherMiddleware : IMiddleware 6 | { 7 | public RealIpFetcherMiddleware() 8 | { 9 | 10 | } 11 | 12 | public Task InvokeAsync(HttpContext context, RequestDelegate next) 13 | { 14 | var headers = context.Request.Headers; 15 | if (headers.ContainsKey("X-Forwarded-For")) 16 | { 17 | context.Connection.RemoteIpAddress = IPAddress.Parse(headers["X-Forwarded-For"].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries)[0]); 18 | } 19 | else if (headers.ContainsKey("X-Real-IP")) 20 | { 21 | context.Connection.RemoteIpAddress = IPAddress.Parse(headers["X-Real-IP"]); 22 | } 23 | return next(context); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/System.Enhance (Part)/System.Enhance.Collections.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.ComponentModel; 3 | 4 | namespace System.Enhance 5 | { 6 | /// 7 | /// 提供适用于 类的增强方法的类。无法继承此类。 8 | /// 9 | public static class Collections 10 | { 11 | /// 12 | /// 获取枚举的 特性中的说明。 13 | /// 14 | /// 当前 枚举实例。 15 | /// 成功返回枚举的特性说明文本,失败返回 16 | public static string? GetDescription(this Enum instance) 17 | { 18 | var type = instance.GetType(); 19 | var infos = type.GetMember(instance.ToString()); 20 | if (infos != null && infos.Length > 0) 21 | { 22 | object[] attrs = infos[0].GetCustomAttributes(typeof(DescriptionAttribute), false); 23 | if (attrs != null && attrs.Length > 0) 24 | { 25 | return ((DescriptionAttribute)attrs[0]).Description; 26 | } 27 | else 28 | { 29 | return null; 30 | } 31 | } 32 | else 33 | { 34 | return null; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/System.Enhance (Part)/System.Enhance.Net.HttpWebRequest.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | 6 | namespace System.Enhance.Net 7 | { 8 | /// 9 | /// 提供适用于 类的增强方法。无法继承此类。 10 | /// 11 | public static class HttpWebRequest 12 | { 13 | /// 14 | /// 使用HTTP协议请求访问指定的URL并获取响应所返回的数据文本。 15 | /// 16 | /// 必需。要访问的URL。 17 | /// 可选。请求的方法(GET/POST/PUT/DELETE)。默认为GET。 18 | /// 可选。请求的User-Agent参数值。默认为 。 19 | /// 可选。请求的Accept参数值。默认为 。 20 | /// 可选。请求的Content-Type参数值。默认为 (视为application/x-www-form-urlencoded) 。 21 | /// 可选。要将响应的数据解码成字符串所使用的编码类型。 默认为 (视为UTF-8编码)。 22 | /// 可选。请求的Header键值对集合。默认为 。 23 | /// 可选。请求体(Body)的数据。仅在请求方法为POST/PUT下可用。默认为 。 24 | /// 成功返回响应所返回的数据, 失败抛出异常。 25 | /// 26 | /// 27 | /// 28 | public static async Task SendHttpRequestAsync(string url,string method = "GET",string? userAgent = null, 29 | string? accept = null, HttpContent? content = null, 30 | Encoding? respEncoding = null, Dictionary? header = null) 31 | { 32 | try 33 | { 34 | if (string.IsNullOrWhiteSpace(url)) 35 | { 36 | throw new ArgumentException("请求访问的URL不能为空。"); 37 | } 38 | else 39 | { 40 | var methodType = method switch 41 | { 42 | "GET" => HttpMethod.Get, 43 | "POST" => HttpMethod.Post, 44 | "PUT" => HttpMethod.Put, 45 | "DELETE" => HttpMethod.Delete, 46 | _ => throw new ArgumentException($"无效的请求类型值: '{method}' 。值必须是以下之一: POST, GET, PUT, DELETE", nameof(method)) 47 | }; 48 | var client = new HttpClient(); 49 | if (userAgent != null) client.DefaultRequestHeaders.Add("User-Agent", userAgent); 50 | if (accept != null) client.DefaultRequestHeaders.Add("Accept", accept); 51 | if (header != null) 52 | { 53 | foreach (var keyValue in header!) 54 | { 55 | client.DefaultRequestHeaders.Add(keyValue.Key, keyValue.Value); 56 | } 57 | } 58 | using var reqMsg = new HttpRequestMessage(methodType, url); 59 | if (content != null) 60 | { 61 | reqMsg.Content = content; 62 | } 63 | else if (methodType == HttpMethod.Post && methodType == HttpMethod.Put) 64 | { 65 | reqMsg.Content = new FormUrlEncodedContent(new Dictionary()); 66 | } 67 | else 68 | { 69 | reqMsg.Content = null; 70 | } 71 | var resp = await client.SendAsync(reqMsg); 72 | if ((int)resp.StatusCode != 200) 73 | { 74 | throw new SocketException((int)resp.StatusCode); 75 | } 76 | else 77 | { 78 | var respReader = new StreamReader(resp.Content.ReadAsStreamAsync().Result, ((respEncoding != null) ? respEncoding! : Encoding.UTF8)); 79 | string respDataStr = respReader.ReadToEnd(); 80 | respReader.Close(); 81 | return respDataStr; 82 | } 83 | } 84 | } 85 | catch (HttpRequestException) 86 | { 87 | throw; 88 | } 89 | catch (SocketException) 90 | { 91 | throw; 92 | } 93 | catch (IOException ex) 94 | { 95 | throw new HttpRequestException("Failed reading data from the response stream.", ex); 96 | } 97 | catch (ArgumentException) 98 | { 99 | throw; 100 | } 101 | } 102 | 103 | /// 104 | /// 使用HTTP协议请求访问指定的URL并获取响应所返回的数据文本。 105 | /// 106 | /// 必需。要访问的URL。 107 | /// 可选。请求的方法(POST/PUT)。默认为POST。 108 | /// 可选。请求的User-Agent参数值。默认为 。 109 | /// 可选。请求的Accept参数值。默认为 。 110 | /// 可选。请求的Content-Type参数值。默认为 application/x-www-form-urlencoded 。 111 | /// 可选。要将响应的数据解码成字符串所使用的编码类型。 默认为 (视为UTF-8编码)。 112 | /// 可选。请求的Header键值对集合。默认为 。 113 | /// 可选。请求体(Body)的键值对数据集合(Form格式的数据)。默认为 。 114 | /// 成功返回响应所返回的数据, 失败抛出异常。 115 | /// 116 | /// 117 | /// 118 | public static async Task SendHttpFormRequestAsync(string url, string method = "POST", string? userAgent = null, 119 | string? accept = null, string reqContentType = "application/x-www-form-urlencoded", Encoding? respEncoding = null, 120 | Dictionary? header = null, Dictionary? reqBodyForm = null) 121 | { 122 | try 123 | { 124 | if (string.IsNullOrWhiteSpace(url)) 125 | { 126 | throw new ArgumentException("请求访问的URL不能为空。"); 127 | } 128 | else 129 | { 130 | var methodType = method switch 131 | { 132 | "POST" => HttpMethod.Post, 133 | "PUT" => HttpMethod.Put, 134 | _ => throw new ArgumentException($"无效的请求类型值: '{method}' 。值必须是以下之一: POST, PUT", nameof(method)) 135 | }; 136 | var client = new HttpClient(); 137 | if (userAgent != null) client.DefaultRequestHeaders.Add("User-Agent", userAgent); 138 | if (accept != null) client.DefaultRequestHeaders.Add("Accept", accept); 139 | if (header != null) 140 | { 141 | foreach (var keyValue in header!) 142 | { 143 | client.DefaultRequestHeaders.Add(keyValue.Key, keyValue.Value); 144 | } 145 | } 146 | using var reqMsg = new HttpRequestMessage(methodType, url); 147 | if (reqBodyForm != null) 148 | { 149 | var encodedForm = reqBodyForm.Select(i => WebUtility.UrlEncode(i.Key) + "=" + WebUtility.UrlEncode(i.Value)); 150 | reqMsg.Content = new StringContent(string.Join("&", encodedForm), null, reqContentType); 151 | // 解决方案来自 Stackoverflow "HttpClient: The uri string is too long" 152 | // 若直接使用UrlEncodedContent并使用url编码后超过640k的Dictionary作为初始化参数则会报错"The uri string is too long" 153 | } 154 | else 155 | { 156 | reqMsg.Content = null; 157 | } 158 | var resp = await client.SendAsync(reqMsg); 159 | if ((int)resp.StatusCode != 200) 160 | { 161 | throw new SocketException((int)resp.StatusCode); 162 | } 163 | else 164 | { 165 | var respReader = new StreamReader(resp.Content.ReadAsStreamAsync().Result, ((respEncoding != null) ? respEncoding! : Encoding.UTF8)); 166 | string respDataStr = respReader.ReadToEnd(); 167 | respReader.Close(); 168 | return respDataStr; 169 | } 170 | } 171 | } 172 | catch (HttpRequestException) 173 | { 174 | throw; 175 | } 176 | catch (SocketException) 177 | { 178 | throw; 179 | } 180 | catch (IOException ex) 181 | { 182 | throw new HttpRequestException("Failed reading data from the response stream.", ex); 183 | } 184 | catch (ArgumentException) 185 | { 186 | throw; 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/System.Enhance (Part)/System.Enhance.Random.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace System.Enhance 4 | { 5 | /// 6 | /// 提供适用于 类的增强方法的类。无法继承此类。 7 | /// 8 | public sealed class Random 9 | { 10 | /// 11 | /// 生成指定长度的随机字符串。 12 | /// 13 | /// 随机字符串的长度。 14 | /// 生成结果。 15 | public static string GenerateRandomString(int digits) 16 | { 17 | byte[] result = new byte[digits - 1]; 18 | RandomNumberGenerator.Create().GetBytes(result, 0, digits - 1); 19 | string resultStr = Convert.ToBase64String(result).Substring(0, digits - 1); 20 | return resultStr; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/System.Enhance (Part)/System.Enhance.Security.Cryptography.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace System.Enhance.Security.Cryptography 6 | { 7 | public static class MD5Helper 8 | { 9 | public static string MD5Encrypt(byte[] data) 10 | { 11 | var md5 = MD5.Create(); 12 | byte[] encrypted = md5.ComputeHash(data); 13 | md5.Clear(); 14 | string r = BitConverter.ToString(encrypted).Replace("-", string.Empty).ToLower(); 15 | return r; 16 | } 17 | 18 | public static string MD5Encrypt(string dataStr,Encoding? encoding = null) 19 | { 20 | var md5 = MD5.Create(); 21 | encoding ??= Encoding.UTF8; 22 | byte[] encrypted = md5.ComputeHash(encoding.GetBytes(dataStr)); 23 | md5.Clear(); 24 | string r = BitConverter.ToString(encrypted).Replace("-", string.Empty).ToLower(); 25 | return r; 26 | } 27 | } 28 | 29 | /// 30 | /// RC4加密算法类(RC4 Cryptography Helper) 31 | /// 32 | public static class RC4Helper 33 | { 34 | /// RC4加密算法 35 | /// 返回进过rc4加密过的字符 36 | /// 37 | /// 被加密的字符 38 | /// 密钥 39 | public static string Encrypt(string str, string ckey) 40 | { 41 | int[] s = new int[256]; 42 | for (int i = 0; i < 256; i++) 43 | { 44 | s[i] = i; 45 | } 46 | //密钥转数组 47 | char[] keys = ckey.ToCharArray();//密钥转字符数组 48 | int[] key = new int[keys.Length]; 49 | for (int i = 0; i < keys.Length; i++) 50 | { 51 | key[i] = keys[i]; 52 | } 53 | //明文转数组 54 | char[] datas = str.ToCharArray(); 55 | int[] mingwen = new int[datas.Length]; 56 | for (int i = 0; i < datas.Length; i++) 57 | { 58 | mingwen[i] = datas[i]; 59 | } 60 | 61 | //通过循环得到256位的数组(密钥) 62 | int j = 0; 63 | int k = 0; 64 | int length = key.Length; 65 | int a; 66 | for (int i = 0; i < 256; i++) 67 | { 68 | a = s[i]; 69 | j = (j + a + key[k]); 70 | if (j >= 256) 71 | { 72 | j = j % 256; 73 | } 74 | s[i] = s[j]; 75 | s[j] = a; 76 | if (++k >= length) 77 | { 78 | k = 0; 79 | } 80 | } 81 | //根据上面的256的密钥数组 和 明文得到密文数组 82 | int x = 0, y = 0, a2, b, c; 83 | int length2 = mingwen.Length; 84 | int[] miwen = new int[length2]; 85 | for (int i = 0; i < length2; i++) 86 | { 87 | x = x + 1; 88 | x = x % 256; 89 | a2 = s[x]; 90 | y = y + a2; 91 | y = y % 256; 92 | s[x] = b = s[y]; 93 | s[y] = a2; 94 | c = a2 + b; 95 | c = c % 256; 96 | miwen[i] = mingwen[i] ^ s[c]; 97 | } 98 | //密文数组转密文字符 99 | char[] mi = new char[miwen.Length]; 100 | for (int i = 0; i < miwen.Length; i++) 101 | { 102 | mi[i] = (char)miwen[i]; 103 | } 104 | string miwenstr = new string(mi); 105 | return Convert.ToBase64String(Encoding.UTF8.GetBytes(miwenstr)); 106 | } 107 | 108 | /// RC4解密算法 109 | /// 返回进过rc4解密过的字符 110 | /// 111 | /// 被解密的字符 112 | /// 密钥 113 | public static string Decrypt(string str, string ckey) 114 | { 115 | str = Encoding.UTF8.GetString(Convert.FromBase64String(str)); 116 | int[] s = new int[256]; 117 | for (int i = 0; i < 256; i++) 118 | { 119 | s[i] = i; 120 | } 121 | //密钥转数组 122 | char[] keys = ckey.ToCharArray();//密钥转字符数组 123 | int[] key = new int[keys.Length]; 124 | for (int i = 0; i < keys.Length; i++) 125 | { 126 | key[i] = keys[i]; 127 | } 128 | //密文转数组 129 | char[] datas = str.ToCharArray(); 130 | int[] miwen = new int[datas.Length]; 131 | for (int i = 0; i < datas.Length; i++) 132 | { 133 | miwen[i] = datas[i]; 134 | } 135 | 136 | //通过循环得到256位的数组(密钥) 137 | int j = 0; 138 | int k = 0; 139 | int length = key.Length; 140 | int a; 141 | for (int i = 0; i < 256; i++) 142 | { 143 | a = s[i]; 144 | j = (j + a + key[k]); 145 | if (j >= 256) 146 | { 147 | j = j % 256; 148 | } 149 | s[i] = s[j]; 150 | s[j] = a; 151 | if (++k >= length) 152 | { 153 | k = 0; 154 | } 155 | } 156 | //根据上面的256的密钥数组 和 密文得到明文数组 157 | int x = 0, y = 0, a2, b, c; 158 | int length2 = miwen.Length; 159 | int[] mingwen = new int[length2]; 160 | for (int i = 0; i < length2; i++) 161 | { 162 | x = x + 1; 163 | x = x % 256; 164 | a2 = s[x]; 165 | y = y + a2; 166 | y = y % 256; 167 | s[x] = b = s[y]; 168 | s[y] = a2; 169 | c = a2 + b; 170 | c = c % 256; 171 | mingwen[i] = miwen[i] ^ s[c]; 172 | } 173 | //明文数组转明文字符 174 | char[] ming = new char[mingwen.Length]; 175 | for (int i = 0; i < mingwen.Length; i++) 176 | { 177 | ming[i] = (char)mingwen[i]; 178 | } 179 | string mingwenstr = new string(ming); 180 | return mingwenstr; 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/System.Enhance (Part)/System.Enhance.Web.Json.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using Microsoft.AspNetCore.Mvc; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace System.Enhance.Web.Json 7 | { 8 | /// 9 | /// 表示一个 实例(Json数据)格式的 。 10 | /// 11 | public class JObjectResult : ActionResult 12 | { 13 | /// 14 | /// 使用指定的 实例初始化 类的新实例。 15 | /// 16 | /// 指定的 实例。 17 | public JObjectResult(JObject data) 18 | { 19 | JsonData = data ?? throw new NullReferenceException("未将对象引用设置到对象的实例。\r\ndata 值为null。"); 20 | } 21 | 22 | /// 23 | /// 使用指定的Json字符串初始化 类的新实例。 24 | /// 25 | /// 26 | public JObjectResult(string? jsonStr) 27 | { 28 | try 29 | { 30 | JsonData = (jsonStr == null) ? new JObject() : JObject.Parse(jsonStr); 31 | } 32 | catch(JsonReaderException ex) 33 | { 34 | throw new JsonReaderException(ex.Message, ex); 35 | } 36 | } 37 | public override void ExecuteResult(ActionContext context) 38 | { 39 | var resp = context.HttpContext.Response; 40 | resp.StatusCode = 200; 41 | resp.ContentType = "application/json"; 42 | resp.WriteAsync(JsonData.ToString(Formatting.None)); 43 | } 44 | 45 | /// 46 | /// 获取当前 实例对应的 实例(Json数据)。 47 | /// 48 | public JObject JsonData { get; } 49 | 50 | /// 51 | /// 将当前 实例对应的 Json 数据转换为Json字符串。 52 | /// 53 | /// 转换后的Json字符串。 54 | public override string ToString() 55 | { 56 | return JsonData.ToString(); 57 | } 58 | 59 | public string ToString(Formatting formatting,params JsonConverter[] converters) 60 | { 61 | return JsonData.ToString(formatting, converters); 62 | } 63 | } 64 | 65 | /// 66 | /// 表示一个 实例(Json数组数据)格式的 。 67 | /// 68 | public class JArrayResult : ActionResult 69 | { 70 | /// 71 | /// 获取当前 实例对应的 实例(Json数组数据)。 72 | /// 73 | public JArray JsonData { get; } 74 | 75 | /// 76 | /// 使用指定的 实例初始化 类的新实例。 77 | /// 78 | /// 指定的 实例。 79 | public JArrayResult(JArray data) 80 | { 81 | JsonData = data ?? throw new NullReferenceException("未将对象引用设置到对象的实例。\r\ndata 值为null。"); 82 | } 83 | 84 | /// 85 | /// 使用指定的Json字符串初始化 类的新实例。 86 | /// 87 | /// 88 | public JArrayResult(string? jsonStr) 89 | { 90 | try 91 | { 92 | JsonData = (jsonStr == null) ? new JArray() : JArray.Parse(jsonStr); 93 | } 94 | catch (JsonReaderException ex) 95 | { 96 | throw new JsonReaderException(ex.Message, ex); 97 | } 98 | } 99 | public override void ExecuteResult(ActionContext context) 100 | { 101 | var resp = context.HttpContext.Response; 102 | resp.StatusCode = 200; 103 | resp.ContentType = "application/json"; 104 | resp.WriteAsync(JsonData.ToString(Formatting.None)); 105 | } 106 | 107 | /// 108 | /// 将当前 实例对应的 Json 数据转换为Json字符串。 109 | /// 110 | /// 转换后的Json字符串。 111 | public override string ToString() 112 | { 113 | return JsonData.ToString(); 114 | } 115 | 116 | public string ToString(Formatting formatting, params JsonConverter[] converters) 117 | { 118 | return JsonData.ToString(formatting, converters); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/Team123it.Arcaea.MarveCube.Standalone.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | MarveCube.Standalone 6 | 0.4.3 7 | 123 Open-Source Organization 8 | Arcaea Server 2 Standalone Download Server Part 9 | 0.4.4 10 | 0.4.3 11 | (C)Copyright 2015-2022 123 Open-Source Organization. All rights reserved. 12 | 123 Marvelous Cube Standalone Version 13 | x64;ARM64 14 | enable 15 | enable 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube.Standalone/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "5.0.1", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Bots/Backgrounds.cs: -------------------------------------------------------------------------------- 1 | using MySql.Data.MySqlClient; 2 | using Team123it.Arcaea.MarveCube.Core; 3 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 4 | 5 | namespace Team123it.Arcaea.MarveCube.Bots 6 | { 7 | public static class Background 8 | { 9 | /// 10 | /// 检查Apikey的有效性。 11 | /// 12 | /// 13 | public static void CheckApiKey(string apikey) 14 | { 15 | var conn = new MySqlConnection(DatabaseConnectURL); 16 | try 17 | { 18 | conn.Open(); 19 | var cmd = conn.CreateCommand(); 20 | cmd.CommandText = "SELECT COUNT(*),is_banned FROM bots WHERE apikey=?apikey;"; 21 | cmd.Parameters.Add(new MySqlParameter("?apikey", apikey)); 22 | var rd = cmd.ExecuteReader(); 23 | rd.Read(); 24 | if (rd.GetInt32(0) == 1) 25 | { 26 | if (rd.GetBoolean(1)) 27 | { 28 | throw new BotAPIException(BotAPIException.APIExceptionType.BotIsBlocked,null); 29 | } 30 | } 31 | else 32 | { 33 | throw new BotAPIException(BotAPIException.APIExceptionType.InvalidApiKey,null); 34 | } 35 | } 36 | catch (BotAPIException) 37 | { 38 | throw; 39 | } 40 | finally 41 | { 42 | conn.Close(); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using System; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Team123it.Arcaea.MarveCube.Core; 7 | using Team123it.Arcaea.MarveCube.Processors.Front; 8 | using System.Enhance.Web.Json; 9 | 10 | namespace Team123it.Arcaea.MarveCube.Controllers 11 | { 12 | /// 13 | /// [API Controller]玩家账号验证相关API控制器类。
14 | /// 对应处理类: 15 | ///
16 | [ApiController] 17 | [Route("years/19/auth")] 18 | public class AuthController : ControllerBase 19 | { 20 | /// 21 | /// [API Action][POST]玩家登录。 22 | /// 23 | /// 基本验证(Basic Auth)参数。 24 | /// Json数据。 25 | [HttpPost("login")] 26 | public Task login([FromHeader]string Authorization) 27 | { 28 | return Task.Run(new Func(() => 29 | { 30 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 31 | if (Maintaining(out _)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 32 | if (Request.IsObsoleteClientVer()) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.NeedUpdateClient); 33 | if (Authorization.ToLower().StartsWith("basic")) 34 | { 35 | string raw = Authorization.Split(' ', StringSplitOptions.RemoveEmptyEntries)[1]; 36 | string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(raw)); 37 | string username = decoded.Split(':')[0]; 38 | string password = decoded.Split(':')[1]; 39 | return new JObjectResult(Auth.Login(Request, username, password)); 40 | } 41 | else 42 | { 43 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UsernameOrPasswordInvalid); 44 | } 45 | })); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/BotController.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Team123it.Arcaea.MarveCube.Core; 6 | using Team123it.Arcaea.MarveCube.Bots; 7 | using Newtonsoft.Json.Linq; 8 | using static Team123it.Arcaea.MarveCube.Core.BotAPIException; 9 | using Team123it.Arcaea.MarveCube.Processors.Front; 10 | using System.Enhance.Web.Json; 11 | 12 | namespace Team123it.Arcaea.MarveCube.Controllers 13 | { 14 | /// 15 | /// [API Controller]Arcaea查分Bot API相关控制器类。
16 | /// 对应处理类: 17 | ///
18 | [ApiController] 19 | [Route("botarcapi")] 20 | public class BotController : ControllerBase 21 | { 22 | [HttpGet("user")] 23 | public Task GetPlayerInfo([FromQuery] string? apikey,[FromQuery]string user) 24 | { 25 | return Task.Run(new Func(() => 26 | { 27 | if (apikey == null) return new BotAPIException(APIExceptionType.InvalidApiKey, null); 28 | try 29 | { 30 | Background.CheckApiKey(apikey); 31 | var r = new JObject() 32 | { 33 | {"status",0 }, 34 | {"content", Bot.GetPlayerInfo(user)} 35 | }; 36 | return new JObjectResult(r); 37 | } 38 | catch (BotAPIException ex) 39 | { 40 | return ex; 41 | } 42 | catch 43 | { 44 | return new BotAPIException(APIExceptionType.Others, null); 45 | } 46 | })); 47 | } 48 | 49 | [HttpGet("user/best")] 50 | public Task GetPlayerSongBest([FromQuery] string? apikey,[FromQuery]string user,[FromQuery]string songid,[FromQuery]int? difficulty, [FromQuery]bool withsonginfo, [FromQuery]bool withrecent) 51 | { 52 | return Task.Run(new Func(() => 53 | { 54 | if (apikey == null) return new BotAPIException(APIExceptionType.InvalidApiKey, null); 55 | try 56 | { 57 | Background.CheckApiKey(apikey); 58 | int diff; 59 | if (!difficulty.HasValue) diff = 2; 60 | else if (difficulty!.Value != 0 && difficulty!.Value != 1 && difficulty!.Value != 2 && difficulty!.Value != 3) throw new BotAPIException(APIExceptionType.DifficultyIsNotExist, null); 61 | else diff = difficulty!.Value; 62 | var r = new JObject() 63 | { 64 | {"status",0 }, 65 | {"content",Bot.QueryPlayerBestScore(user,songid,(SongDifficulty)diff, withsonginfo, withrecent)} 66 | }; 67 | return new JObjectResult(r); 68 | } 69 | catch (BotAPIException ex) 70 | { 71 | return ex; 72 | } 73 | catch 74 | { 75 | return new BotAPIException(APIExceptionType.Others, null); 76 | } 77 | })); 78 | } 79 | 80 | [HttpGet("user/info")] 81 | public Task GetPlayerRecentScore([FromQuery] string? apikey,[FromQuery]string user, [FromQuery]bool withsonginfo) 82 | { 83 | return Task.Run(new Func(() => 84 | { 85 | if (apikey == null) return new BotAPIException(APIExceptionType.InvalidApiKey, null); 86 | try 87 | { 88 | Background.CheckApiKey(apikey); 89 | var r = new JObject() 90 | { 91 | {"status",0 }, 92 | {"content",Bot.QueryPlayerRecentScore(user, withsonginfo)} 93 | }; 94 | return new JObjectResult(r); 95 | } 96 | catch (BotAPIException ex) 97 | { 98 | return ex; 99 | } 100 | catch 101 | { 102 | return new BotAPIException(APIExceptionType.Others, null); 103 | } 104 | })); 105 | } 106 | 107 | [HttpGet("user/best30")] 108 | public Task GetPlayerBest30([FromQuery]string? apikey,[FromQuery]string user, [FromQuery]bool withsonginfo = false, [FromQuery]bool withrecent = false) 109 | { 110 | return Task.Run(new Func(() => 111 | { 112 | if (apikey == null) return new BotAPIException(APIExceptionType.InvalidApiKey, null); 113 | try 114 | { 115 | Background.CheckApiKey(apikey); 116 | var r = new JObject() 117 | { 118 | {"status",0 }, 119 | {"content",Bot.QueryPlayerBest30(user, withsonginfo, withrecent)} 120 | }; 121 | return new JObjectResult(r); 122 | } 123 | catch (BotAPIException ex) 124 | { 125 | return ex; 126 | } 127 | catch (Exception ex) 128 | { 129 | Console.WriteLine(ex.ToString()); 130 | return new BotAPIException(APIExceptionType.Others, null); 131 | } 132 | })); 133 | } 134 | 135 | [HttpGet("song/info")] 136 | public Task GetSongDetails([FromQuery]string? apikey, [FromQuery]string songid) 137 | { 138 | return Task.Run(new Func(() => 139 | { 140 | if (apikey == null) return new BotAPIException(APIExceptionType.InvalidApiKey, null); 141 | try 142 | { 143 | Background.CheckApiKey(apikey); 144 | var r = new JObject() 145 | { 146 | {"status",0 }, 147 | {"content", Bot.GetSongInfo(songid) } 148 | }; 149 | return new JObjectResult(r); 150 | } 151 | catch (BotAPIException ex) 152 | { 153 | return ex; 154 | } 155 | catch (Exception ex) 156 | { 157 | Console.WriteLine(ex.ToString()); 158 | return new BotAPIException(APIExceptionType.Others, null); 159 | } 160 | })); 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/ComposeController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Team123it.Arcaea.MarveCube.Core; 4 | using Team123it.Arcaea.MarveCube.Processors.Front; 5 | using System.Threading.Tasks; 6 | using System.Enhance.Web.Json; 7 | using Newtonsoft.Json.Linq; 8 | using System; 9 | using Team123it.Arcaea.MarveCube.Processors.Background; 10 | 11 | namespace Team123it.Arcaea.MarveCube.Controllers 12 | { 13 | /// 14 | /// [API Controller]玩家数据获取相关API控制器类。
15 | /// 对应处理类: 16 | ///
17 | [ApiController] 18 | [Route("years/19/compose")] 19 | public class ComposeController : ControllerBase 20 | { 21 | /// 22 | /// [API Action][POST]玩家信息获取。 23 | /// 24 | /// Bearer Token参数。 25 | /// Json字符串。 26 | [HttpGet("aggregate")] 27 | public async Task aggregate([FromHeader]string Authorization,[FromQuery]string calls) 28 | { 29 | return await Task.Run(() => { 30 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 31 | if (Request.IsObsoleteClientVer()) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.NeedUpdateClient); 32 | if (Authorization.Trim().ToLower().StartsWith("bearer")) 33 | { 34 | string token = Authorization.Split(' ')[1]; 35 | if (Maintaining(out var players)) 36 | { 37 | uint? userId = Tokens.GetUserIdByToken(token); 38 | if (!userId.HasValue || !players.Contains((int)userId.Value)) 39 | { 40 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 41 | } 42 | } 43 | Console.WriteLine("calls=" + calls); 44 | if (string.IsNullOrWhiteSpace(calls)) 45 | { 46 | return new JObjectResult(Compose.FullAggregate(token)); 47 | } 48 | else 49 | { 50 | var callsObj = JArray.Parse(calls); 51 | var tinyCallsObj = new JObject() 52 | { 53 | {"endpoint","/user/me" }, 54 | {"id",0 } 55 | }; 56 | if ((callsObj.Count == 1 && ((JObject)callsObj[0]).ToString() == tinyCallsObj.ToString())) 57 | { 58 | return new JObjectResult(Compose.TinyAggregate(token)); 59 | } 60 | else 61 | { 62 | return new JObjectResult(Compose.FullAggregate(token, calls)); 63 | } 64 | } 65 | } 66 | else 67 | { 68 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 69 | } 70 | }); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/FriendController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Newtonsoft.Json.Linq; 7 | using Team123it.Arcaea.MarveCube.Core; 8 | using Team123it.Arcaea.MarveCube.Processors.Background; 9 | using Team123it.Arcaea.MarveCube.Processors.Front; 10 | using System.Enhance.Web.Json; 11 | 12 | namespace Team123it.Arcaea.MarveCube.Controllers 13 | { 14 | /// 15 | /// [API Controller]玩家好友管理相关API控制器类。
16 | /// 对应处理类: 17 | ///
18 | [Route("years/19/friend")] 19 | [ApiController] 20 | public class FriendController : ControllerBase 21 | { 22 | /// 23 | /// [API Action][POST]添加好友。 24 | /// 25 | /// Bearer Token参数。 26 | /// 要添加的好友所对应的玩家的9位好友id。 27 | /// Json字符串。 28 | [HttpPost("me/add")] 29 | public Task add([FromHeader] string Authorization,[FromForm]string friend_code) 30 | { 31 | return Task.Run(new Func(() => 32 | { 33 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 34 | if (Authorization.ToLower().Trim().StartsWith("bearer")) 35 | { 36 | string token = Authorization.Split(" ")[1]; 37 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 38 | if (Maintaining(out var players)) 39 | { 40 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 41 | { 42 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 43 | } 44 | } 45 | if (userid != null) //如果获取到了用户id 46 | { 47 | try 48 | { 49 | var result = Friend.AddFriend(userid.Value, (string)friend_code); 50 | var r = new JObject() 51 | { 52 | {"success",true }, 53 | {"value",result } 54 | }; 55 | return new JObjectResult(r); 56 | } 57 | catch (ArcaeaAPIException ex) //如果发生了异常 58 | { 59 | return ex; //直接返回对应异常的Json 60 | } 61 | catch (Exception) //发生了未知异常 62 | { 63 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 64 | } 65 | } 66 | else 67 | { 68 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 69 | } 70 | } 71 | else 72 | { 73 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 74 | } 75 | })); 76 | } 77 | 78 | /// 79 | /// {API Action][POST]删除好友。 80 | /// 81 | /// Bearer Token参数。 82 | /// 要删除的好友所对应的玩家的用户id(非好友id)。 83 | /// Json字符串。 84 | [HttpPost("me/delete")] 85 | public Task delete([FromHeader] string Authorization, [FromForm] int friend_id) 86 | { 87 | return Task.Run(new Func(() => 88 | { 89 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 90 | if (Authorization.ToLower().Trim().StartsWith("bearer")) 91 | { 92 | string token = Authorization.Split(" ")[1]; 93 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 94 | if (Maintaining(out var players)) 95 | { 96 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 97 | { 98 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 99 | } 100 | } 101 | if (userid != null) //如果获取到了用户id 102 | { 103 | try 104 | { 105 | var result = Friend.DeleteFriend(userid.Value, friend_id); 106 | var r = new JObject() 107 | { 108 | {"success",true }, 109 | {"value",result } 110 | }; 111 | return new JObjectResult(r); 112 | } 113 | catch (ArcaeaAPIException ex) //如果发生了异常 114 | { 115 | return ex; //直接返回对应异常的Json 116 | } 117 | catch (Exception) //发生了未知异常 118 | { 119 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 120 | } 121 | } 122 | else 123 | { 124 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 125 | } 126 | } 127 | else 128 | { 129 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 130 | } 131 | })); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/MultiplayerController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Newtonsoft.Json.Linq; 4 | using System.Threading.Tasks; 5 | using System; 6 | using System.Enhance.Web.Json; 7 | using Team123it.Arcaea.MarveCube.Processors.Front; 8 | using Team123it.Arcaea.MarveCube.Core; 9 | using Team123it.Arcaea.MarveCube.Processors.Background; 10 | 11 | namespace Team123it.Arcaea.MarveCube.Controllers 12 | { 13 | /// 14 | /// [API Controller]Link Play多人游玩相关API控制器类。
15 | /// 对应处理类: 16 | [Route("years/19/multiplayer")] 17 | [ApiController] 18 | public class MultiplayerController : ControllerBase 19 | { 20 | /// 21 | /// [API Action][POST]创建一个新的Link Play多人游戏房间。 22 | /// 23 | /// Bearer Token参数。 24 | /// 玩家的用户id。 25 | /// 26 | /// 包含Link Play客户端初始化数据的 类实例。 27 | /// 28 | /// Link Play客户端初始化数据包括: 29 | /// 30 | /// 31 | /// protocolVersion - UDP下Link Play传输协议(616协议[数据明文开头十六进制为06 16])的版本号。
32 | /// 截至Arcaea版本3.12.6该版本号值为9。 33 | ///
34 | /// 35 | /// clientSongMap - 玩家的客户端式Link Play曲目解锁表(使用idx作为曲目的唯一标识符)。
36 | /// (Link Play曲目解锁表类型说明详见 ) 37 | ///
38 | ///
39 | ///
40 | /// 41 | /// Json字符串。 42 | /// 43 | [HttpPost("me/room/create")] 44 | public async Task CreateRoom([FromHeader]string Authorization, [FromHeader]int i, [FromBody]JObject wrapper) 45 | { 46 | return await Task.Run(new Func(() => 47 | { 48 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 49 | if (Authorization.ToLower().Trim().StartsWith("bearer")) 50 | { 51 | string token = Authorization.Split(" ")[1]; 52 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 53 | if (Maintaining(out var players)) 54 | { 55 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 56 | { 57 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 58 | } 59 | } 60 | if (userid != null) //如果获取到了用户id 61 | { 62 | try 63 | { 64 | var result = Multiplayer.CreateRoom(i, wrapper.Value("clientSongMap")!); 65 | var r = new JObject() 66 | { 67 | { "success", true }, 68 | { "value", result } 69 | }; 70 | return new JObjectResult(r); 71 | } 72 | catch (ArcaeaAPIException ex) //如果发生了异常 73 | { 74 | return ex; //直接返回对应异常的Json 75 | } 76 | catch (Exception) //发生了未知异常 77 | { 78 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 79 | } 80 | } 81 | else 82 | { 83 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 84 | } 85 | } 86 | else 87 | { 88 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 89 | } 90 | })); 91 | } 92 | 93 | /// 94 | /// [API Action][POST]加入一个已有的Link Play多人游戏房间。 95 | /// 96 | /// Bearer Token参数。 97 | /// 要加入的6位房间号。 98 | /// 玩家的用户id。 99 | /// 100 | /// 包含Link Play客户端初始化数据的 类实例。 101 | /// 102 | /// Link Play客户端初始化数据包括: 103 | /// 104 | /// 105 | /// protocolVersion - UDP下Link Play传输协议(616协议[数据明文开头十六进制为06 16])的版本号。
106 | /// 截至Arcaea版本3.12.6该版本号值为9。 107 | ///
108 | /// 109 | /// clientSongMap - 玩家的客户端式Link Play曲目解锁表(使用idx作为曲目的唯一标识符)。
110 | /// (Link Play曲目解锁表类型说明详见 ) 111 | ///
112 | ///
113 | ///
114 | /// 115 | /// Json字符串。 116 | /// 117 | [HttpPost("me/room/join/{roomCode}")] 118 | public async Task JoinRoom([FromHeader]string Authorization, [FromRoute]string roomCode, [FromHeader]int i, [FromBody]JObject wrapper) 119 | { 120 | return await Task.Run(new Func(() => 121 | { 122 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 123 | if (Authorization.ToLower().Trim().StartsWith("bearer")) 124 | { 125 | string token = Authorization.Split(" ")[1]; 126 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 127 | if (Maintaining(out var players)) 128 | { 129 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 130 | { 131 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 132 | } 133 | } 134 | if (userid != null) //如果获取到了用户id 135 | { 136 | try 137 | { 138 | var result = Multiplayer.JoinRoom(roomCode, i, wrapper.Value("clientSongMap")!); 139 | var r = new JObject() 140 | { 141 | { "success", true }, 142 | { "value", result } 143 | }; 144 | return new JObjectResult(r); 145 | } 146 | catch (ArcaeaAPIException ex) //如果发生了异常 147 | { 148 | return ex; //直接返回对应异常的Json 149 | } 150 | catch (Exception) //发生了未知异常 151 | { 152 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 153 | } 154 | } 155 | else 156 | { 157 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 158 | } 159 | } 160 | else 161 | { 162 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 163 | } 164 | })); 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/PresentController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Enhance.Web.Json; 6 | using System.Threading.Tasks; 7 | using Team123it.Arcaea.MarveCube.Core; 8 | using Team123it.Arcaea.MarveCube.Processors.Background; 9 | using Newtonsoft.Json.Linq; 10 | using Team123it.Arcaea.MarveCube.Processors.Front; 11 | 12 | namespace Team123it.Arcaea.MarveCube.Controllers 13 | { 14 | /// 15 | /// [API Controller]礼物相关API控制器类。
16 | /// 对应处理类: 17 | ///
18 | [Route("years/19/present")] 19 | [ApiController] 20 | public class PresentController : ControllerBase 21 | { 22 | /// 23 | /// [API Action][POST]接收礼物。 24 | /// 25 | /// Bearer Token参数。 26 | /// 要接收的礼物id。 27 | /// Json字符串。 28 | [HttpPost("me/claim/{presentId}")] 29 | public async Task ClaimPresent([FromHeader]string Authorization, [FromRoute]string presentId) 30 | { 31 | return await Task.Run(new Func(() => 32 | { 33 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 34 | try 35 | { 36 | if (Authorization.Trim().ToLower().StartsWith("bearer")) 37 | { 38 | uint? user_id = Tokens.GetUserIdByToken(Authorization.Split(" ")[1]); 39 | if (Maintaining(out var players)) 40 | { 41 | if (!user_id.HasValue || !players.Contains((int)user_id.Value)) 42 | { 43 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 44 | } 45 | } 46 | if (!user_id.HasValue) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 47 | var r = new JObject() 48 | { 49 | { "success", true }, 50 | { "value", Present.ClaimPresent(user_id.Value, presentId) } 51 | }; 52 | return new JObjectResult(r); 53 | } 54 | else 55 | { 56 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 57 | } 58 | } 59 | catch (ArcaeaAPIException ex) 60 | { 61 | return ex; 62 | } 63 | catch (Exception) 64 | { 65 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 66 | } 67 | })); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/ServeController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Collections.Generic; 4 | using System.Enhance.Web.Json; 5 | using System.Threading.Tasks; 6 | using Team123it.Arcaea.MarveCube.Core; 7 | using Team123it.Arcaea.MarveCube.Processors.Background; 8 | using System; 9 | using Newtonsoft.Json.Linq; 10 | using System.Linq; 11 | using Team123it.Arcaea.MarveCube.Processors.Front; 12 | 13 | namespace Team123it.Arcaea.MarveCube.Controllers 14 | { 15 | /// 16 | /// [API Controller]数据下载相关API控制器类。
17 | /// 对应处理类: 18 | ///
19 | [Route("years/19/serve")] 20 | [ApiController] 21 | public class ServeController : ControllerBase 22 | { 23 | /// 24 | /// [API Action][GET]获取曲目下载的URL和校验值信息。 25 | /// 26 | /// Bearer Token参数。 27 | /// 要下载的曲目的id(sid)。 28 | /// 是否在返回的信息中包含曲目下载的URL。 29 | /// Json字符串。 30 | [HttpGet("download/me/song")] 31 | public async Task GetSongDownloadDetails([FromHeader]string Authorization, [FromQuery]IEnumerable sid, [FromQuery]bool url) 32 | { 33 | return await Task.Run(() => 34 | { 35 | try 36 | { 37 | if (PreparingForRelease(HttpContext.Request)) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.PreparingForRelease); 38 | if (Request.IsObsoleteClientVer()) return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.NeedUpdateClient); 39 | if (Authorization.Trim().ToLower().StartsWith("bearer")) 40 | { 41 | string token = Authorization.Split(' ')[1]; 42 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 43 | if (Maintaining(out var players)) 44 | { 45 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 46 | { 47 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 48 | } 49 | } 50 | if (userid != null) //如果获取到了用户id 51 | { 52 | var info = new PlayerInfo(userid.Value, out _); 53 | if (info.Banned.Value) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountHasBeenBlocked); 54 | var r = new JObject() 55 | { 56 | { "success", true }, 57 | { "value", Serve.GetDownloadAvailableSongs(userid.Value, sid.Any() ? sid : null, url) } 58 | }; 59 | Console.WriteLine(r.ToString()); 60 | return new JObjectResult(r); 61 | } 62 | else 63 | { 64 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 65 | } 66 | } 67 | else 68 | { 69 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 70 | } 71 | } 72 | catch (ArcaeaAPIException ex) 73 | { 74 | return ex; 75 | } 76 | catch (Exception ex) 77 | { 78 | Console.WriteLine(ex.ToString()); 79 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 80 | } 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/StandaloneController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Newtonsoft.Json.Linq; 3 | using System.Enhance.Web.Json; 4 | using System.Threading.Tasks; 5 | using Team123it.Arcaea.MarveCube.Core; 6 | 7 | namespace Team123it.Arcaea.MarveCube.Controllers 8 | { 9 | [Route("standalone")] 10 | [ApiController] 11 | public class StandaloneController : ControllerBase 12 | { 13 | [HttpGet("token")] 14 | public async Task GetCurrentStandaloneToken([FromQuery]string StandaloneKey) 15 | { 16 | return await Task.Run(() => 17 | { 18 | if (!string.IsNullOrWhiteSpace(StandaloneKey)) 19 | { 20 | if (StandaloneKey == StandaloneToken.Current.Key) 21 | { 22 | return new JObjectResult(new JObject() 23 | { 24 | { "success", true }, 25 | { "value", StandaloneToken.Current.Token } 26 | }); 27 | } 28 | else 29 | { 30 | return new JObjectResult(new JObject() 31 | { 32 | { "success", false }, 33 | { "error_code", 403 } 34 | }); 35 | } 36 | } 37 | else 38 | { 39 | return new JObjectResult(new JObject() 40 | { 41 | { "success", false }, 42 | { "error_code", 401 } 43 | }); 44 | } 45 | }); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Controllers/WorldController.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Newtonsoft.Json.Linq; 6 | using Team123it.Arcaea.MarveCube.Core; 7 | using Team123it.Arcaea.MarveCube.Processors.Background; 8 | using Team123it.Arcaea.MarveCube.Processors.Front; 9 | using World = Team123it.Arcaea.MarveCube.Processors.Front.World; 10 | using System.Enhance.Web.Json; 11 | 12 | namespace Team123it.Arcaea.MarveCube.Controllers 13 | { 14 | /// 15 | /// [API Controller]World模式相关API控制器类。
16 | /// 对应处理类: 17 | ///
18 | [Route("years/19/world")] 19 | [ApiController] 20 | public class WorldController : ControllerBase 21 | { 22 | /// 23 | /// [API Action][GET]获取当前玩家的完整世界模式数据。 24 | /// 25 | /// Bearer Token参数。 26 | /// Json字符串。 27 | [HttpGet("map/me")] 28 | public Task myFullWorldInfo([FromHeader]string Authorization) 29 | { 30 | return Task.Run(new Func(() => 31 | { 32 | if (Authorization.ToLower().StartsWith("bearer")) 33 | { 34 | string token = Authorization.Split(" ")[1]; 35 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 36 | if (Maintaining(out var players)) 37 | { 38 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 39 | { 40 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 41 | } 42 | } 43 | if (userid != null) //如果获取到了用户id 44 | { 45 | try 46 | { 47 | var result = World.GetAllWorldInfo(userid.Value); 48 | var r = new JObject() 49 | { 50 | {"success",true }, 51 | {"value",result } 52 | }; 53 | return new JObjectResult(r); 54 | } 55 | catch (ArcaeaAPIException ex) //如果发生了异常 56 | { 57 | return ex; //直接返回对应异常的Json 58 | } 59 | catch (Exception) //发生了未知异常 60 | { 61 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 62 | } 63 | } 64 | else 65 | { 66 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 67 | } 68 | } 69 | else 70 | { 71 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UsernameOrPasswordInvalid); 72 | } 73 | })); 74 | } 75 | 76 | /// 77 | /// [API Action][GET]选择并进入指定地图。 78 | /// 79 | /// Bearer Token参数。 80 | /// 指定地图的id。 81 | /// Json字符串。 82 | [HttpPost("map/me")] 83 | public Task entryWorldMap([FromHeader]string Authorization,[FromForm]string map_id) 84 | { 85 | return Task.Run(new Func(() => 86 | { 87 | if (Authorization.ToLower().StartsWith("bearer")) 88 | { 89 | string token = Authorization.Split(" ")[1]; 90 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 91 | if (Maintaining(out var players)) 92 | { 93 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 94 | { 95 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 96 | } 97 | } 98 | if (userid != null) //如果获取到了用户id 99 | { 100 | try 101 | { 102 | var result = World.GetUserMapInfo(userid.Value, map_id); 103 | var r = new JObject() 104 | { 105 | {"success",true }, 106 | {"value",result } 107 | }; 108 | return new JObjectResult(r); 109 | } 110 | catch (ArcaeaAPIException ex) //如果发生了异常 111 | { 112 | return ex; //直接返回对应异常的Json 113 | } 114 | catch (Exception ex) //发生了未知异常 115 | { 116 | Console.WriteLine(ex.ToString()); 117 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 118 | } 119 | } 120 | else 121 | { 122 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 123 | } 124 | } 125 | else 126 | { 127 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UsernameOrPasswordInvalid); 128 | } 129 | })); 130 | } 131 | 132 | /// 133 | /// [API Action][GET]获取单个地图的完整信息。 134 | /// 135 | /// Bearer Token参数。 136 | /// 指定地图的id。 137 | /// Json字符串。 138 | [HttpGet("map/me/{map_id}")] 139 | public Task getSingleMapInfo([FromHeader]string Authorization,string map_id) 140 | { 141 | return Task.Run(new Func(() => 142 | { 143 | if (Authorization.ToLower().StartsWith("bearer")) 144 | { 145 | string token = Authorization.Split(" ")[1]; 146 | uint? userid = Tokens.GetUserIdByToken(token); //获取token对应的用户id 147 | if (Maintaining(out var players)) 148 | { 149 | if (!userid.HasValue || !players.Contains((int)userid.Value)) 150 | { 151 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.ServerMaintaining); 152 | } 153 | } 154 | if (userid != null) //如果获取到了用户id 155 | { 156 | try 157 | { 158 | var result = World.GetUserSingleMap(userid.Value, map_id); 159 | var r = new JObject() 160 | { 161 | {"success",true }, 162 | {"value",result } 163 | }; 164 | return new JObjectResult(r); 165 | } 166 | catch (ArcaeaAPIException ex) //如果发生了异常 167 | { 168 | return ex; //直接返回对应异常的Json 169 | } 170 | catch (Exception ex) //发生了未知异常 171 | { 172 | Console.WriteLine(ex.ToString()); 173 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 174 | } 175 | } 176 | else 177 | { 178 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 179 | } 180 | } 181 | else 182 | { 183 | return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UsernameOrPasswordInvalid); 184 | } 185 | })); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/BotAPIExceptions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Enhance; 5 | using System.Collections.Generic; 6 | using System.Enhance.Web.Json; 7 | 8 | namespace Team123it.Arcaea.MarveCube.Core 9 | { 10 | /// 11 | /// 表示Arcaea Server 2 BotAPI返回的异常。 12 | /// 异常对应id请参考 枚举的注释。 13 | /// 14 | public class BotAPIException : Exception 15 | { 16 | /// 17 | /// API异常的类型。 18 | /// 19 | public APIExceptionType Type { get; } 20 | 21 | /// 22 | /// API异常的简短说明。 23 | /// 24 | public string Description { get; } 25 | 26 | /// 27 | /// 初始化 类的新实例。 28 | /// 29 | /// 异常类型。 30 | public BotAPIException(APIExceptionType type,KeyValuePair>? tag = null) 31 | { 32 | Type = type; 33 | Description = type.GetDescription()!; 34 | } 35 | 36 | /// 37 | /// 将当前 实例转换为Json字符串。 38 | /// 39 | /// 当前 实例。 40 | public static implicit operator string(BotAPIException CurrentException) 41 | { 42 | int error_code = (int)CurrentException.Type; 43 | string desc = CurrentException.Description; 44 | var result = new JObject() 45 | { 46 | {"status",error_code }, 47 | {"message",desc } 48 | }; 49 | return result.ToString(); 50 | } 51 | 52 | /// 53 | /// 将当前 实例转换为 类实例。 54 | /// 55 | /// 56 | public static implicit operator JObjectResult(BotAPIException CurrentException) 57 | { 58 | int error_code = (int)CurrentException.Type; 59 | string desc = CurrentException.Description; 60 | var result = new JObject() 61 | { 62 | {"status",error_code }, 63 | {"message",desc } 64 | }; 65 | return new JObjectResult(result); 66 | } 67 | 68 | /// 69 | /// 将当前 实例转换为(Json)字符串。 70 | /// 71 | /// 转换结果。 72 | public override string ToString() 73 | { 74 | int error_code = (int)Type; 75 | var result = new JObject() 76 | { 77 | {"status",error_code }, 78 | {"message",Description } 79 | }; 80 | return result.ToString(); 81 | } 82 | 83 | /// 84 | /// 表示 API异常的具体类型。 85 | /// 86 | public enum APIExceptionType 87 | { 88 | /// 89 | /// 发生未知错误 90 | /// 91 | [Description("An unexpected error occurred. Please contact Lowiro.")] 92 | Others = -100, 93 | /// 94 | /// 服务器正在维护中 95 | /// 96 | [Description("Server is maintaining. Please wait patiently before finishing the maintain. If you have any problem or question, please contact Lowiro.")] 97 | ServerMaintaining = -101, 98 | /// 99 | /// 玩家不存在 100 | /// 101 | [Description("This player isn't exist.")] 102 | PlayerNotExist = -200, 103 | /// 104 | /// 玩家账号被封禁 105 | /// 106 | [Description("This player is blocked.")] 107 | PlayerIsBlocked = -201, 108 | /// 109 | /// 曲目不存在 110 | /// 111 | [Description("This song isn't exist.")] 112 | SongIsNotExist = -300, 113 | /// 114 | /// 该曲目不存在当前难度 115 | /// 116 | [Description("This difficulty of the song isn't exist.")] 117 | DifficultyIsNotExist = -301, 118 | /// 119 | /// 别名对应的曲目数量过多(>1) 120 | /// 121 | [Description("Too many songs matching this alias. Please give more accurate alias, or give the sid.")] 122 | TooManySongsFromAlias = -302, 123 | /// 124 | /// 玩家未游玩曲目的当前难度 125 | /// 126 | [Description("Player didn't play the difficulty of the song before.")] 127 | PlayerNotPlayedThisDiff = -303, 128 | /// 129 | /// 玩家的最近游玩成绩为空 130 | /// 131 | [Description("Player didn't play any song(s).")] 132 | RecentScoreIsEmpty = -304, 133 | /// 134 | /// 无效的ApiKey 135 | /// 136 | [Description("Invalid apikey. Please check your apikey before executing the visit to this api again.")] 137 | InvalidApiKey = -400, 138 | /// 139 | /// Bot账号被封禁 140 | /// 141 | [Description("Your bot account is blocked. Please contact Lowiro to get more details or to appeal misblock action.")] 142 | BotIsBlocked = -401 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/ItemType.cs: -------------------------------------------------------------------------------- 1 | namespace Team123it.Arcaea.MarveCube.Core 2 | { 3 | public enum ItemType 4 | { 5 | Pack = 0, 6 | SingleSong = 1 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/QueryLimit.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 3 | using StackExchange.Redis; 4 | using System; 5 | 6 | namespace Team123it.Arcaea.MarveCube.Core 7 | { 8 | public static class QueryLimit 9 | { 10 | /// 11 | /// 对当前请求 实例对应的Bot Apikey进行API访问频率检查。 12 | /// 13 | /// 当前 实例。 14 | /// 若当前请求 实例对应的Bot Apikey已超过允许的QPS上限则返回 , 否则返回 15 | public static bool DoQueryLimit(this HttpRequest req,string apikey) 16 | { 17 | if (MDatabaseConnectURL == null) 18 | { 19 | Console.ForegroundColor = ConsoleColor.Red; 20 | string prefix = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); 21 | Console.WriteLine($"[{prefix}][Error]Cannot connect to Memory Database (Redis Server). Had you set the database url in the config file?"); 22 | return false; 23 | } 24 | using var conn = ConnectionMultiplexer.Connect(MDatabaseConnectURL); 25 | try 26 | { 27 | var db = conn.GetDatabase(); 28 | var limit = db.StringGetWithExpiry(apikey); 29 | uint limitTimes = (limit.Value != RedisValue.Null) ? (uint)limit.Value : 0; 30 | if (limitTimes > MaxQueryTimesPerSecond) 31 | { 32 | return false; 33 | } 34 | else 35 | { 36 | limitTimes++; 37 | db.KeyDelete(apikey); 38 | db.StringSet(apikey, limitTimes); 39 | db.KeyExpire(apikey, new TimeSpan(0, 0, 1)); 40 | return true; 41 | } 42 | } 43 | catch 44 | { 45 | return false; 46 | } 47 | finally 48 | { 49 | conn.Close(); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/SongEnums.cs: -------------------------------------------------------------------------------- 1 | namespace Team123it.Arcaea.MarveCube.Core 2 | { 3 | /// 4 | /// 表示曲目的难度。 5 | /// 6 | public enum SongDifficulty 7 | { 8 | /// 9 | /// Past难度。 10 | /// 11 | Past = 0, 12 | /// 13 | /// Present难度。 14 | /// 15 | Present = 1, 16 | /// 17 | /// Future难度。 18 | /// 19 | Future = 2, 20 | /// 21 | /// Beyond难度。 22 | /// 23 | Beyond = 3 24 | } 25 | 26 | /// 27 | /// 表示曲目完成的类型。 28 | /// 29 | public enum ClearType 30 | { 31 | /// 32 | /// [TL]曲目失败(Track Lost)。 33 | /// 34 | TrackLost = 0, 35 | /// 36 | /// [EC]简单回忆条通关(Track Complete)。 37 | /// 38 | EasyClear = 4, 39 | /// 40 | /// [NC]普通回忆条通关(Track Complete)。 41 | /// 42 | NormalClear = 1, 43 | /// 44 | /// [HC]困难回忆条通关(Track Complete)。 45 | /// 46 | HardClear = 5, 47 | /// 48 | /// [FR]全部连击(Full Recall)。 49 | /// 50 | FullRecall = 2, 51 | /// 52 | /// [PM]全部完美(Pure Memory)。 53 | /// 54 | PureMemory = 3 55 | } 56 | 57 | /// 58 | /// 表示曲目完成的评级。 59 | /// 60 | public enum GradeType 61 | { 62 | /// 63 | /// EX+评级(分数在990w及以上)。 64 | /// 65 | EX_Plus = 6, 66 | /// 67 | /// EX评级(分数在980w-9899999区间)。 68 | /// 69 | EX = 5, 70 | /// 71 | /// AA评级(分数950w-9799999区间)。 72 | /// 73 | AA = 4, 74 | /// 75 | /// A评级(分数920w-9499999区间)。 76 | /// 77 | A = 3, 78 | /// 79 | /// B评级(分数890w-9199999区间)。 80 | /// 81 | B = 2, 82 | /// 83 | /// C评级(分数860w-8899999区间)。 84 | /// 85 | C = 1, 86 | /// 87 | /// D评级(分数在8599999及以下)。 88 | /// 89 | D = 0 90 | } 91 | 92 | /// 93 | /// 提供适用于 系列枚举的静态方法的类。无法继承此类。 94 | /// 95 | public static class SongEnumsStatics 96 | { 97 | /// 98 | /// 将指定的分数转换为对应的 评级。 99 | /// 100 | /// 要转换的分数。 101 | /// 转换结果。 102 | public static GradeType ConvertScoreToGrade(uint score) 103 | { 104 | if (score >= 9900000) return GradeType.EX_Plus; 105 | else if (score >= 9800000 && score < 9900000) return GradeType.EX; 106 | else if (score >= 9500000 && score < 9800000) return GradeType.AA; 107 | else if (score >= 9200000 && score < 9500000) return GradeType.A; 108 | else if (score >= 8900000 && score < 9200000) return GradeType.B; 109 | else if (score >= 8600000 && score < 8900000) return GradeType.C; 110 | else return GradeType.D; 111 | } 112 | 113 | /// 114 | /// 检查当前 对应的曲目完成类型是否高于另一个 对应的曲目类型。 115 | /// 116 | /// 当前 。 117 | /// 要判断的另一个 。 118 | /// 当前 高于另一个 则为 ; 否则为 119 | public static bool CheckIsHigher(this ClearType clearType1,ClearType clearType2) 120 | { 121 | switch (clearType1) 122 | { 123 | case ClearType.PureMemory: 124 | if (clearType2 == ClearType.PureMemory) return false; 125 | else return true; 126 | case ClearType.FullRecall: 127 | if (clearType2 == ClearType.PureMemory || clearType2 == ClearType.FullRecall) return false; 128 | else return true; 129 | case ClearType.HardClear: 130 | if (clearType2 == ClearType.PureMemory || clearType2 == ClearType.FullRecall || clearType2 == ClearType.HardClear) return false; 131 | else return true; 132 | case ClearType.NormalClear: 133 | if (clearType2 == ClearType.TrackLost || clearType2 == ClearType.EasyClear) return true; 134 | else return false; 135 | case ClearType.EasyClear: 136 | if (clearType2 == ClearType.TrackLost) return true; 137 | else return false; 138 | case ClearType.TrackLost: 139 | return false; 140 | default: 141 | return false; 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/SongNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace Team123it.Arcaea.MarveCube.Core 6 | { 7 | /// 8 | /// 当找不到曲目信息时抛出的异常。 9 | /// 10 | public class SongNotFoundException : Exception 11 | { 12 | /// 13 | /// 无效曲目的曲目id。 14 | /// 15 | public string SongId { get; } 16 | 17 | /// 18 | /// 无效曲目的曲目难度。 19 | /// 若本 实例未特指某个难度,则本属性值为 20 | /// 21 | public SongDifficulty? Difficulty { get; } 22 | 23 | /// 24 | /// 解释说明当前异常的信息。 25 | /// 26 | public override string Message { get; } 27 | 28 | /// 29 | /// 初始化 类的新实例。 30 | /// 31 | /// 无效曲目的曲目id。 32 | /// 无效曲目的曲目难度。 33 | /// 若未特指某一难度,请将本参数置为 34 | public SongNotFoundException(string songId,SongDifficulty? songDiff) 35 | { 36 | SongId = songId; 37 | Difficulty = songDiff; 38 | var b = new StringBuilder("找不到id为 ").Append(songId).Append(" "); 39 | string diffStr = string.Empty; 40 | if (songDiff != null) { 41 | switch (songDiff) 42 | { 43 | case SongDifficulty.Past: 44 | diffStr = "Past"; 45 | break; 46 | case SongDifficulty.Present: 47 | diffStr = "Present"; 48 | break; 49 | case SongDifficulty.Future: 50 | diffStr = "Future"; 51 | break; 52 | case SongDifficulty.Beyond: 53 | diffStr = "Beyond"; 54 | break; 55 | } 56 | b.Append("且难度为 ").Append(diffStr).Append(" "); 57 | } 58 | b.Append("的曲目信息。"); 59 | Message = b.ToString(); 60 | } 61 | 62 | /// 63 | /// 创建并返回表示当前异常实例的字符串。 64 | /// 65 | /// 代表当前异常的字符串。 66 | public override string ToString() 67 | { 68 | var b = new StringBuilder(Message).Append("\r\n").Append((StackTrace != null) ? StackTrace : string.Empty); 69 | return b.ToString(); 70 | } 71 | 72 | /// 73 | /// 判断指定的对象是否与当前 实例相等。 74 | /// 75 | /// 要判断的对象。 76 | /// 判断结果。 77 | public override bool Equals(object obj) 78 | { 79 | if (obj == null || GetType() != obj.GetType()) 80 | { 81 | return false; 82 | } 83 | else 84 | { 85 | var instance = (SongNotFoundException)obj; 86 | if (SongId == instance.SongId) 87 | { 88 | if (Difficulty != null) 89 | { 90 | if (instance.Difficulty != null && Difficulty == instance.Difficulty) return true; 91 | else return false; 92 | } 93 | else 94 | { 95 | if (instance.Difficulty != null) return false; 96 | else return true; 97 | } 98 | } 99 | } 100 | 101 | return base.Equals(obj); 102 | } 103 | 104 | public static bool operator ==(SongNotFoundException left,SongNotFoundException right) 105 | { 106 | try 107 | { 108 | if (left.SongId == right.SongId) 109 | { 110 | if (left.Difficulty != null) 111 | { 112 | if (right.Difficulty != null && left.Difficulty == right.Difficulty) return true; 113 | else return false; 114 | } 115 | else 116 | { 117 | if (right.Difficulty != null) return false; 118 | else return true; 119 | } 120 | } else 121 | { 122 | return false; 123 | } 124 | } 125 | catch(NullReferenceException) 126 | { 127 | return false; 128 | } 129 | } 130 | 131 | public static bool operator !=(SongNotFoundException left,SongNotFoundException right) 132 | { 133 | return !(left == right); 134 | } 135 | 136 | /// 137 | /// 计算当前 实例的哈希值。 138 | /// 139 | /// 计算结果。 140 | public override int GetHashCode() 141 | { 142 | return int.Parse(Convert.ToBase64String(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(SongId)))); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/StaminaPurchaseType.cs: -------------------------------------------------------------------------------- 1 | namespace Team123it.Arcaea.MarveCube.Core 2 | { 3 | public enum StaminaPurchaseType 4 | { 5 | Fragment = 0, 6 | Memory = 1 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Core/StandaloneToken.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using System.Text.Json; 6 | 7 | namespace Team123it.Arcaea.MarveCube.Core 8 | { 9 | /// 10 | /// 表示一个适用于123 Marvelous Cube 独立模块(Standalone)项目的专用Token(独立项目Token)数据组。 11 | /// 12 | public struct StandaloneToken 13 | { 14 | /// 15 | /// 获取当前实时的 实例。 16 | /// 17 | public static StandaloneToken Current { get => GetCurrentToken(); } 18 | 19 | /// 20 | /// 当前 所表示的独立项目Token的原始字符串。 21 | /// 22 | public string Token { get; private set; } 23 | 24 | /// 25 | /// 下载服务器请求获取Token时使用的Key。 26 | /// 27 | public string Key { get; private set; } 28 | 29 | /// 30 | /// 获取当前实时的 实例。 31 | /// 32 | private static StandaloneToken GetCurrentToken() 33 | { 34 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json"))) 35 | { 36 | try 37 | { 38 | var settings = JObject.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "data", "config.json"), Encoding.UTF8)); 39 | var config = settings.Value("config"); 40 | return new StandaloneToken() { Token = config.Value("standaloneToken"), Key = config.Value("standaloneKey") }; 41 | } 42 | catch (Exception ex) 43 | { 44 | throw new JsonException($"配置文件 {Path.Combine(AppContext.BaseDirectory, "data", "config.json")} 读取失败: {ex.Message}"); 45 | } 46 | } 47 | else 48 | { 49 | throw new FileNotFoundException($"找不到配置文件(config.json): {Path.Combine(AppContext.BaseDirectory, "data", "config.json")}"); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/FirstStartData/ConfigExample.json: -------------------------------------------------------------------------------- 1 | /* 请在手动初始化 Arcaea Server 2 之前(FirstStart模块未完成之前手动初始化)按照注释将配置信息填写完整, 2 | 并将该文件改名为config.json(大小写敏感)复制到"程序根目录/data/"下 */ 3 | { 4 | "settings": { 5 | "isMaintaining": false, // 是否正在维护中 6 | "isPreparingForRelease": false, // [仅针对最新版本(见下)]是否正在准备发布 7 | "isOverrideAprilFools": false, // 是否覆盖愚人节模块检查(true时服务器永久开放愚人节模块; false时仅服务器时间4.1时开放) 8 | "isWorldEventMapTesting": false, // 是否正在测试世界模式限时地图(true时设置的测试员(见下)将可以进入不在活动时间范围内的限时地图) 9 | "eventMapTestPlayers": [], // 世界模式限时地图测试玩家的用户id列表 10 | "minSupportVer": "3.10.6", // 最低支持的Arcaea客户端版本 11 | "latestVersion": "3.12.10", // Arcaea客户端的最新版本 12 | "aprilFoolsStartTime": -1, // 愚人节开放的UNIX时间戳(单位为秒)(优先级高于上文的"服务器时间",低于"isOverrideAprilFools"的设置) 13 | "topRankLimit": 200 //Top玩家(#框)的最大排名限制(最高可填写为200) 14 | }, 15 | "config": { 16 | "dbIP": "数据库的ip", 17 | "dbPort": 3306, // 数据库的端口 18 | "dbUser": "登录数据库使用的用户名", 19 | "dbPass": "登录数据库使用的密码(Base64了)", 20 | "dbName": "操作的数据库的名称", 21 | "remoteDlPrefix": "http://127.0.0.1:51495", // 客户端请求远程下载时独立下载服务器的域名(包括http/https,结尾无斜杠) 22 | "standaloneKey": "12345678901234567890123456789012", // 独立下载服务器请求主服务器(获取下载Token)的时候使用的Key 23 | "standaloneToken": "abcdefghijklmnopqrstuvwxyz789012", // 独立下载服务器解密下载URL参数时使用的解密Token(即上文的"下载Token") 24 | "listenPort": 80, // 主服务器的监听端口 25 | "redisURL": "Redis服务器的IP/域名(默认为localhost)", 26 | "redisPort": 6379, // Redis服务器的连接端口 27 | "redisPswd": "Redis服务器连接时使用的密码(留空视为无密码,默认为空)", 28 | "linkPlayEndpoint": "Link Play多人游玩模块的UDP服务器终结点(Endpoint)地址(必须是IPv4地址)", 29 | "linkPlayPort": 21495 // Link Play多人游玩模块的UDP服务器连接端口 30 | } 31 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Background/FixedDatas.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using Newtonsoft.Json.Linq; 3 | using MySql.Data.MySqlClient; 4 | 5 | namespace Team123it.Arcaea.MarveCube.Processors.Background 6 | { 7 | public static class FixedDatas 8 | { 9 | public static JArray GetAllPackIds() 10 | { 11 | using var conn = new MySqlConnection(DatabaseConnectURL); 12 | try 13 | { 14 | conn.Open(); 15 | var cmd = conn.CreateCommand(); 16 | cmd.CommandText = "SELECT pid FROM fixed_packs;"; 17 | var rd = cmd.ExecuteReader(); 18 | var pids = new JArray(); 19 | while (rd.Read()) 20 | { 21 | pids.Add(rd.GetString(0)); 22 | } 23 | rd.Close(); 24 | return pids; 25 | } 26 | catch 27 | { 28 | return new JArray(); 29 | } 30 | finally 31 | { 32 | conn.Close(); 33 | } 34 | } 35 | 36 | /// 37 | /// 返回指定用户id对应的玩家所拥有的Beyond难度的曲目的sid数组。 38 | /// 39 | /// 玩家的用户id(非好友id)。 40 | /// 以World模式Beyond曲目格式(sid + "3")命名的string数组。 41 | public static JArray GetPlayerOwnBeyondSongIds(uint userId) 42 | { 43 | using var conn = new MySqlConnection(DatabaseConnectURL); 44 | try 45 | { 46 | conn.Open(); 47 | var cmd = conn.CreateCommand(); 48 | cmd.CommandText = "SELECT sid FROM user_bydunlocks WHERE user_id=?uid;"; 49 | cmd.Parameters.Add(new MySqlParameter("?uid", MySqlDbType.Int32) 50 | { 51 | Value = userId 52 | }); 53 | var rd = cmd.ExecuteReader(); 54 | var bydSids = new JArray(); 55 | while (rd.Read()) 56 | { 57 | bydSids.Add(rd.GetString(0) + "3"); 58 | } 59 | rd.Close(); 60 | return bydSids; 61 | } 62 | catch 63 | { 64 | return new JArray(); 65 | } 66 | finally 67 | { 68 | conn.Close(); 69 | } 70 | } 71 | 72 | /// 73 | /// 返回指定用户id对应的玩家所拥有的所有Beyond难度的曲目的sid数组。 74 | /// 75 | /// 玩家的用户id(非好友id)。 76 | /// 以World模式Beyond曲目格式(sid + "3")命名的string数组。 77 | public static JArray GetPlayerAllOwnBeyondSongIds(uint userId) 78 | { 79 | return GetPlayerOwnBeyondSongIds(userId); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Background/SecurityManager.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using MySql.Data.MySqlClient; 3 | using System; 4 | using Newtonsoft.Json.Linq; 5 | 6 | namespace Team123it.Arcaea.MarveCube.Processors.Background 7 | { 8 | public static class SecurityManager 9 | { 10 | /// 11 | /// 修改玩家的信用点数。 12 | /// 13 | /// 玩家的用户id(非好友id)。 14 | /// 要修改的数量(可以为负数)。 15 | public static void EditPlayerCreditPoint(uint userid,int range,string reason = "") 16 | { 17 | using var conn = new MySqlConnection(DatabaseConnectURL); 18 | conn.Open(); 19 | var cmd = conn.CreateCommand(); 20 | cmd.CommandText = "SELECT credit_point,name,credit_edit_reasons FROM users WHERE user_id=?uid;"; 21 | cmd.Parameters.Add(new MySqlParameter("?uid", MySqlDbType.Int32) 22 | { 23 | Value = userid 24 | }); 25 | var rd = cmd.ExecuteReader(); 26 | rd.Read(); 27 | int beforeCredit = rd.GetInt32(0); 28 | string username = rd.GetString(1); 29 | JArray reasons; 30 | if (rd.IsDBNull(2) || rd.GetString(2) == string.Empty) 31 | { 32 | reasons = new JArray(); 33 | } 34 | else 35 | { 36 | reasons = JArray.Parse(rd.GetString(2)); 37 | } 38 | rd.Close(); 39 | int afterCredit = beforeCredit + range; 40 | bool isBanned = false; 41 | if (afterCredit < 0) 42 | { 43 | afterCredit = 0; 44 | isBanned = true; 45 | } 46 | else if (afterCredit == 0) 47 | { 48 | isBanned = true; 49 | } 50 | if (reason != "") 51 | { 52 | reasons.Add(new JObject() 53 | { 54 | { "date", DateTime.Now.ToString("yyyy-M-d H:mm:ss") }, 55 | { "player", username + $"(id:{userid})" }, 56 | { "beforeCreditPoint", beforeCredit }, 57 | { "afterCreditPoint", afterCredit }, 58 | { "creditPointRange", range }, 59 | { "reason", reason } 60 | }); 61 | } 62 | else 63 | { 64 | reasons.Add(new JObject() 65 | { 66 | { "date", DateTime.Now.ToString("yyyy-M-d H:mm:ss") }, 67 | { "player", username + $"(id:{userid})" }, 68 | { "beforeCreditPoint", beforeCredit }, 69 | { "afterCreditPoint", afterCredit }, 70 | { "creditPointRange", range }, 71 | { "reason", "N/A" } 72 | }); 73 | } 74 | cmd.Parameters.Clear(); 75 | cmd.CommandText = "UPDATE users SET credit_point=?credit,credit_edit_reasons=?reasons,is_banned=?isBanned WHERE user_id=?uid;"; 76 | cmd.Parameters.Add(new MySqlParameter("?credit", MySqlDbType.Int32) 77 | { 78 | Value = afterCredit 79 | }); 80 | cmd.Parameters.Add(new MySqlParameter("?reasons", MySqlDbType.VarString) 81 | { 82 | Value = reasons.ToString() 83 | }); 84 | cmd.Parameters.Add(new MySqlParameter("?uid", MySqlDbType.Int32) 85 | { 86 | Value = userid 87 | }); 88 | cmd.Parameters.Add(new MySqlParameter("?isBanned", MySqlDbType.Int32) 89 | { 90 | Value = Convert.ToInt32(isBanned) 91 | }); 92 | cmd.ExecuteNonQuery(); 93 | conn.Close(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Background/Tokens.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 3 | using MySql.Data.MySqlClient; 4 | using System; 5 | using StackExchange.Redis; 6 | 7 | namespace Team123it.Arcaea.MarveCube.Processors.Background 8 | { 9 | /// 10 | /// 提供关于 Arcaea 客户端(用户) Token 处理的类。无法继承此类。 11 | /// 12 | public sealed class Tokens 13 | { 14 | /// 15 | /// 通过token获取对应的用户id(不是好友id)。 16 | /// 17 | /// 要获取的用户id对应的此用户的token。 18 | /// 获取的用户id, 若不存在则为 19 | public static uint? GetUserIdByToken(string token) 20 | { 21 | using var conn = new MySqlConnection(DatabaseConnectURL); 22 | conn.Open(); 23 | var cmd = conn.CreateCommand(); 24 | cmd.CommandText = $"SELECT COUNT(*),user_id FROM logins WHERE access_token=?token;"; 25 | cmd.Parameters.Add(new MySqlParameter("?token", MySqlDbType.VarString) 26 | { 27 | Value = token 28 | }); 29 | var rd = cmd.ExecuteReader(); 30 | rd.Read(); 31 | if (rd.GetInt32(0) == 1) 32 | { 33 | uint userid = (uint)rd.GetInt32(1); 34 | rd.Close(); 35 | conn.Close(); 36 | return userid; 37 | } else 38 | { 39 | rd.Close(); 40 | conn.Close(); 41 | return null; 42 | } 43 | } 44 | 45 | /// 46 | /// 通过token获取对应玩家的最近登入设备信息。 47 | /// 48 | /// 要获取的最近登入设备信息对应的玩家的token。 49 | /// 成功返回(设备型号名,设备uuid);
50 | /// 存在该玩家的登录信息但设备信息为空返回(,);
51 | /// 不存在该玩家的登录信息返回
52 | public static (string,string)? GetDeviceInfoByToken(string token) 53 | { 54 | using var conn = new MySqlConnection(DatabaseConnectURL); 55 | conn.Open(); 56 | var cmd = conn.CreateCommand(); 57 | cmd.CommandText = $"SELECT COUNT(*),COUNT(last_login_device),last_login_device,last_login_deviceId FROM logins WHERE access_token=?token;"; 58 | cmd.Parameters.Add(new MySqlParameter("?token", MySqlDbType.VarString) 59 | { 60 | Value = token 61 | }); 62 | var rd = cmd.ExecuteReader(); 63 | rd.Read(); 64 | if (rd.GetInt32(0) == 1) 65 | { 66 | if (rd.GetInt32(1) == 1) 67 | { 68 | string devName = rd.GetString(2); 69 | string devId = rd.GetString(3); 70 | rd.Close(); 71 | conn.Close(); 72 | return (devName, devId); 73 | } 74 | else 75 | { 76 | rd.Close(); 77 | conn.Close(); 78 | return (string.Empty, string.Empty); 79 | } 80 | } 81 | else 82 | { 83 | rd.Close(); 84 | conn.Close(); 85 | return null; 86 | } 87 | } 88 | 89 | /// 90 | /// 生成新的文件下载Token。 91 | /// 92 | /// 玩家账号的用户id。 93 | /// 94 | public static string? GenDownloadToken(int userId, string songid) 95 | { 96 | string token = Guid.NewGuid().ToString(); 97 | var redisConn = ConnectionMultiplexer.Connect(MDatabaseConnectURL); 98 | try 99 | { 100 | if (redisConn.IsConnected) 101 | { 102 | var redisDB = redisConn.GetDatabase(); 103 | string tokenValue = $"Arcaea-SongDownload-Token-{userId}-{songid}-{DateTime.Now:yyyyMMddHHmmssfff}"; 104 | var expireTime = TimeSpan.FromHours(1.5); 105 | bool r = redisDB.StringSet(token, tokenValue, expireTime); 106 | redisConn.Close(true); 107 | if (r) 108 | { 109 | return token; 110 | } 111 | else 112 | { 113 | return null; 114 | } 115 | } 116 | else 117 | { 118 | return null; 119 | } 120 | } 121 | catch 122 | { 123 | return null; 124 | } 125 | finally 126 | { 127 | redisConn.Close(); 128 | } 129 | } 130 | 131 | public static string? SetCustomDownloadToken(uint userId, string songId, string token) 132 | { 133 | var redisConn = ConnectionMultiplexer.Connect(MDatabaseConnectURL); 134 | try 135 | { 136 | if (redisConn.IsConnected) 137 | { 138 | var redisDB = redisConn.GetDatabase(); 139 | string tokenValue = $"Arcaea-SongDownload-Token-{userId}-{songId}-{DateTime.Now:yyyyMMddHHmmssfff}"; 140 | var expireTime = TimeSpan.FromHours(1.5); 141 | bool r = redisDB.StringSet(token, tokenValue, expireTime); 142 | redisConn.Close(true); 143 | if (r) 144 | { 145 | return token; 146 | } 147 | else 148 | { 149 | return null; 150 | } 151 | } 152 | else 153 | { 154 | return null; 155 | } 156 | } 157 | catch 158 | { 159 | return null; 160 | } 161 | finally 162 | { 163 | redisConn.Close(); 164 | } 165 | } 166 | 167 | public static uint? GetUserIdByDownloadToken(string downloadToken, out string? downloadSongId) 168 | { 169 | var redisConn = ConnectionMultiplexer.Connect(MDatabaseConnectURL); 170 | try 171 | { 172 | if (redisConn.IsConnected) 173 | { 174 | var redisDB = redisConn.GetDatabase(); 175 | var tokenValueWrapper = redisDB.StringGetWithExpiry(downloadToken); 176 | if (tokenValueWrapper.Value != RedisValue.Null) 177 | { 178 | string tokenValue = tokenValueWrapper.Value; 179 | if (tokenValue.StartsWith("Arcaea-SongDownload-Token-")) 180 | { 181 | uint userId = uint.Parse(tokenValue.Split('-')[3]); 182 | string songId = tokenValue.Split('-')[4]; 183 | downloadSongId = songId; 184 | return userId; 185 | } 186 | else 187 | { 188 | downloadSongId = null; 189 | return null; 190 | } 191 | } 192 | else 193 | { 194 | downloadSongId = null; 195 | return null; 196 | } 197 | } 198 | else 199 | { 200 | downloadSongId = null; 201 | return null; 202 | } 203 | } 204 | catch 205 | { 206 | downloadSongId = null; 207 | return null; 208 | } 209 | finally 210 | { 211 | redisConn.Close(); 212 | } 213 | } 214 | 215 | public static bool TryGetUserIdByDownloadToken(string downloadToken, out uint value, out string? downloadSongId) 216 | { 217 | var r = GetUserIdByDownloadToken(downloadToken, out string? downloadSongWrapper); 218 | if (r.HasValue) 219 | { 220 | value = r.Value; 221 | downloadSongId = downloadSongWrapper; 222 | return true; 223 | } 224 | else 225 | { 226 | value = 0; 227 | downloadSongId = null; 228 | return false; 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/Auth.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using MySql.Data.MySqlClient; 3 | using Microsoft.AspNetCore.Http; 4 | using Newtonsoft.Json.Linq; 5 | using System; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using Team123it.Arcaea.MarveCube.Core; 9 | using static Team123it.Arcaea.MarveCube.Core.ArcaeaAPIException; 10 | using Random = System.Enhance.Random; 11 | using ToolBox.UserAgentParse; 12 | 13 | namespace Team123it.Arcaea.MarveCube.Processors.Front 14 | { 15 | /// 16 | /// 玩家帐号验证相关API。
17 | /// 对应API前缀:/years/19/auth/ 18 | ///
19 | public class Auth 20 | { 21 | /// 22 | /// [API]玩家登录. 23 | /// 24 | /// 玩家帐号用户名/E-mail. 25 | /// 帐号密码. 26 | /// Json数据. 27 | public static string Login(HttpRequest req,string username,string password) 28 | { 29 | var conn = new MySqlConnection(DatabaseConnectURL); 30 | try 31 | { 32 | conn.Open(); 33 | string passEncrypted = BitConverter.ToString(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password))).Replace("-","").ToLower(); 34 | var cmd = new MySqlCommand($"SELECT COUNT(*),user_id FROM users WHERE name=?username AND password='{passEncrypted}'", conn); 35 | cmd.Parameters.Add(new MySqlParameter("?username", MySqlDbType.VarChar) 36 | { 37 | Value = username 38 | }); 39 | var data = cmd.ExecuteReader(); 40 | data.Read(); 41 | int result = data.GetInt32(0); 42 | if (result == 1) 43 | { 44 | int userid = (int)data.GetValue(1); 45 | data.Close(); 46 | cmd.CommandText = $"SELECT COUNT(*) FROM logins WHERE user_id={userid}"; 47 | long result2 = (long)cmd.ExecuteScalar(); 48 | if (result2 >= 1) 49 | { 50 | cmd.CommandText = $"DELETE FROM logins WHERE user_id={userid}"; 51 | cmd.ExecuteNonQuery(); 52 | } 53 | var ua = new UaUnit((string)req.Headers["User-Agent"]).Parse(); 54 | string devName = (ua != null) ? ua.PhoneModelCode : string.Empty; 55 | string devId = req.Headers["DeviceId"]; 56 | string token = Random.GenerateRandomString(15) + "="; 57 | cmd.CommandText = $"INSERT INTO logins (access_token,user_id,last_login_device,last_login_deviceId) VALUES (?token,?userid,?devName,?devId)"; 58 | cmd.Parameters.Add(new MySqlParameter("?token", MySqlDbType.VarChar) 59 | { 60 | Value = token 61 | }); 62 | cmd.Parameters.Add(new MySqlParameter("?userid", MySqlDbType.Int32) 63 | { 64 | Value = userid 65 | }); 66 | cmd.Parameters.Add(new MySqlParameter("?devName", MySqlDbType.VarString) 67 | { 68 | Value = devName 69 | }); 70 | cmd.Parameters.Add(new MySqlParameter("?devId", MySqlDbType.VarString) 71 | { 72 | Value = devId 73 | }); 74 | cmd.ExecuteNonQuery(); 75 | var response = new JObject() 76 | { 77 | {"success",true }, 78 | {"access_token",token }, 79 | {"token_type","Bearer" } 80 | }; 81 | return response.ToString(); 82 | } 83 | else 84 | { 85 | return new ArcaeaAPIException(APIExceptionType.UsernameOrPasswordInvalid); 86 | } 87 | } 88 | catch (ArcaeaAPIException) 89 | { 90 | throw;//return new ArcaeaAPIException(APIExceptionType.Other); 91 | } 92 | finally 93 | { 94 | conn.Close(); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/Compose.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 3 | using MySql.Data.MySqlClient; 4 | using Newtonsoft.Json.Linq; 5 | using System; 6 | using Team123it.Arcaea.MarveCube.Core; 7 | using Team123it.Arcaea.MarveCube.Processors.Background; 8 | using World2 = Team123it.Arcaea.MarveCube.Processors.Background.World; 9 | using System.Linq; 10 | 11 | namespace Team123it.Arcaea.MarveCube.Processors.Front 12 | { 13 | /// 14 | /// 数据获取相关API。
15 | /// 对应API前缀:/years/19/compose/ 16 | ///
17 | public class Compose 18 | { 19 | /// 20 | /// [API][完整版]获取用户信息. 21 | /// 22 | /// 来源token. 23 | /// Json数据. 24 | /// 25 | public static string FullAggregate(string token, string calls = "") 26 | { 27 | try 28 | { 29 | uint? userIdCheck = Tokens.GetUserIdByToken(token); 30 | if (userIdCheck == null) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 31 | uint userId = userIdCheck.Value; 32 | var user = new PlayerInfo(userId, out bool isExists); 33 | if (!isExists) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UserNotExist); //用户不存在 34 | if (user.Banned!.Value) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountHasBeenBlocked); 35 | //用户账号被冻结 36 | var r = new JObject() 37 | { 38 | {"success",true } 39 | }; 40 | var r_value = new JArray(); 41 | #region "value = 0:玩家数据 (定义参数:value0) | /user/me" 42 | var value0 = new JObject() 43 | { 44 | {"id",0 }, 45 | {"value",user.GetUserBaseInfoData() } 46 | }; 47 | #endregion 48 | #region "value = 1:曲包数据 (定义参数:value1,packs,singlePack,items,conn,cmd,rd) | /purchase/bundle/pack" 49 | var value1 = new JObject() 50 | { 51 | {"id", 1 }, 52 | {"value", Purchase.GetPurchaseData() } 53 | }; 54 | #endregion 55 | #region "value = 2:所有可下载的曲目元数据列表 (定义参数:value2|使用参数:conn,cmd,rd) | /serve/download/me/song?url=false" 56 | var value2 = new JObject() 57 | { 58 | {"id",2 }, 59 | {"value", Serve.GetDownloadAvailableSongs(userId, null, false) } 60 | }; 61 | #endregion 62 | #region "value = 3:服务器设置数据 (定义参数:value3,props,level_steps,level) | /game/info" 63 | var value3 = new JObject() 64 | { 65 | {"id",3 } 66 | }; 67 | var props = new JObject() 68 | { 69 | {"stamina_recover_tick", 1800000 }, 70 | {"curr_ts", Convert.ToInt64((DateTime.Now - DateTime.UnixEpoch).TotalMilliseconds) } // 当前的时间(毫秒为单位) 71 | }; 72 | using var conn = new MySqlConnection(DatabaseConnectURL); 73 | conn.Open(); 74 | var cmd = conn.CreateCommand(); 75 | cmd.CommandText = $"SELECT * FROM fixed_properties"; 76 | var rd = cmd.ExecuteReader(); 77 | while (rd.Read()) 78 | { 79 | switch (rd.GetString(0)) 80 | { 81 | case "max_stamina": //世界(World)模式 - 满体力数 82 | string? maxStaminaStr = rd.GetString(1); 83 | int convertedMaxStamina = Convert.ToInt32(maxStaminaStr); 84 | props.Add("max_stamina", (convertedMaxStamina > 0) ? convertedMaxStamina : 12); 85 | break; 86 | case "level_steps": //世界(World)模式 - 角色升级经验 87 | var level_steps = JArray.Parse(rd.GetString(1)); 88 | int level = 0; 89 | var levels = new JArray(); 90 | foreach (int currentLevelStep in level_steps) 91 | { 92 | level++; 93 | levels.Add(new JObject() 94 | { 95 | {"level",level }, 96 | {"level_exp",currentLevelStep } 97 | }); 98 | } 99 | props.Add("level_steps", levels); 100 | break; 101 | case "world_ranking_enabled": //是否启用初始曲包(base)世界排行榜 102 | props.Add("world_ranking_enabled", Convert.ToBoolean(Convert.ToInt32(rd.GetString(1)))); 103 | break; 104 | case "is_byd_chapter_unlocked": //世界(World)模式 - Beyond章节是否已解封 105 | props.Add("is_byd_chapter_unlocked", Convert.ToBoolean(int.Parse(rd.GetString(1)))); 106 | break; 107 | case "core_exp": //单位以太之滴的经验值 108 | props.Add("core_exp", Convert.ToInt32(rd.GetString(1))); 109 | break; 110 | } 111 | } 112 | rd.Close(); 113 | value3.Add("value", props); 114 | #endregion 115 | #region "value = 4: 礼物下发数据 (值类型:JArray) (定义参数:value4) | /present/me?lang=[语言id]" 116 | string langStr = "zh-Hans"; 117 | if (!string.IsNullOrEmpty(calls)) 118 | { 119 | var callsData = JArray.Parse(calls); 120 | langStr = (from call in callsData 121 | where ((JObject)call).Value("id") == 4 122 | select ((JObject)call).Value("endpoint").Split("?lang=")[1]).First(); 123 | } 124 | var value4 = new JObject() 125 | { 126 | {"id",4 }, 127 | {"value", Present.FetchAvailablePresents(user, langStr) } 128 | }; 129 | #endregion 130 | #region "value = 5: 世界模式数据 (定义参数:value5) | /world/map/me" 131 | cmd.CommandText = $"SELECT current_map FROM users WHERE user_id={user!.UserId!.Value};"; 132 | var currentMap = cmd.ExecuteScalar(); 133 | var value5 = new JObject() 134 | { 135 | {"id",5 }, 136 | { 137 | "value", new JObject() 138 | { 139 | {"current_map", (currentMap != null) ? Convert.ToString(currentMap) : string.Empty }, 140 | {"user_id",userId }, 141 | {"maps", JArray.FromObject(World2.GetAllMaps(userId,out _).Where(data => data.Value("map_id") == ((currentMap != null) ? Convert.ToString(currentMap) : string.Empty))) } 142 | // World2.GetAllMaps(userId,out _) 143 | } 144 | } 145 | }; 146 | #endregion 147 | r_value.Add(value0); 148 | r_value.Add(value1); 149 | r_value.Add(value2); 150 | r_value.Add(value3); 151 | r_value.Add(value4); 152 | r_value.Add(value5); 153 | r.Add("value", r_value); 154 | return r.ToString(); 155 | } 156 | catch (ArcaeaAPIException ex) 157 | { 158 | return ex; 159 | } 160 | //catch 161 | //{ 162 | // return new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 163 | //} 164 | } 165 | 166 | public static string TinyAggregate(string token) 167 | { 168 | try 169 | { 170 | uint? userIdCheck = Tokens.GetUserIdByToken(token); 171 | if (userIdCheck == null) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.LoggedInAnotherDevice); 172 | uint userId = userIdCheck.Value; 173 | var user = new PlayerInfo(userId, out bool isExists); 174 | if (!isExists) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UserNotExist); //用户不存在 175 | if (user.Banned!.Value) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountHasBeenBlocked); 176 | //用户账号被冻结 177 | var r = new JObject() 178 | { 179 | {"success",true } 180 | }; 181 | var r_value = new JArray(); 182 | #region "value = 0:玩家数据 (定义参数:value0)" 183 | var value0 = new JObject() 184 | { 185 | {"id",0 }, 186 | {"value",user.GetUserBaseInfoData() } 187 | }; 188 | #endregion 189 | r_value.Add(value0); 190 | r.Add("value", r_value); 191 | return r.ToString(); 192 | } 193 | catch (ArcaeaAPIException ex) 194 | { 195 | return ex; 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/Friend.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using MySql.Data.MySqlClient; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Configuration; 6 | using Team123it.Arcaea.MarveCube.Core; 7 | 8 | namespace Team123it.Arcaea.MarveCube.Processors.Front 9 | { 10 | /// 11 | /// 玩家好友管理相关API。
12 | /// 对应API前缀:/years/19/friend/ 13 | ///
14 | public class Friend 15 | { 16 | 17 | /// 18 | /// [API]添加好友。 19 | /// 20 | /// 玩家的用户id。 21 | /// 新好友玩家的好友id。 22 | /// 23 | public static JObject AddFriend(uint userid,string friendCode) 24 | { 25 | var me = new PlayerInfo(userid, out _); 26 | if (me.UserCode == friendCode) //不能添加自己为好友 27 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.CannotAddSelfAsFriend); 28 | var friendInfo = new PlayerInfo(friendCode,out bool isExists); 29 | if (!isExists) //用户不存在 30 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UserNotExist); 31 | using var conn = new MySqlConnection(DatabaseConnectURL); 32 | conn.Open(); 33 | var cmd = conn.CreateCommand(); 34 | cmd.CommandText = $"SELECT COUNT(*) FROM friend WHERE user_id_me={userid} AND user_id_other={friendInfo.UserId};"; 35 | long result = (long)cmd.ExecuteScalar(); 36 | if (result == 1) //如果已是好友 37 | { 38 | conn.Close(); 39 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UserIsAlreadyFriend); 40 | } else 41 | { 42 | cmd.CommandText = $"SELECT COUNT(*) FROM friend WHERE user_id_me={userid}"; 43 | long friendCounts = (long)cmd.ExecuteScalar(); 44 | if (friendCounts == long.Parse(ConfigurationManager.AppSettings["MaxFriendsCount"])) //如果好友列表已满 45 | { 46 | conn.Close(); 47 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.FriendListIsFull); 48 | } else //否则(添加好友) 49 | { 50 | cmd.CommandText = $"INSERT INTO friend (user_id_me,user_id_other) VALUES ({userid},{friendInfo.UserId.Value})"; 51 | cmd.ExecuteNonQuery(); 52 | string updatedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); 53 | cmd.CommandText = $"SELECT join_date FROM users WHERE user_id={userid}"; 54 | long join_date = (long)cmd.ExecuteNonQuery(); 55 | string createdAt = new DateTime(1970, 1, 1).AddSeconds(join_date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); 56 | conn.Close(); 57 | var r = new JObject() 58 | { 59 | {"user_id",userid }, 60 | {"updatedAt",updatedAt }, 61 | {"createdAt",createdAt }, 62 | {"friends", me.FriendsList} 63 | }; 64 | return r; 65 | } 66 | } 67 | } 68 | 69 | /// 70 | /// [API]删除好友。 71 | /// 72 | /// 玩家的用户id。 73 | /// 要删除的好友对应玩家的好友id。 74 | /// 75 | public static JObject DeleteFriend(uint userid,int friendid) 76 | { 77 | var me = new PlayerInfo(userid, out _); 78 | using var conn = new MySqlConnection(DatabaseConnectURL); 79 | conn.Open(); 80 | var cmd = conn.CreateCommand(); 81 | cmd.CommandText = $"DELETE FROM friend WHERE user_id_me={userid} AND user_id_other={friendid}"; 82 | cmd.ExecuteNonQuery(); 83 | string updatedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); 84 | cmd.CommandText = $"SELECT join_date FROM users WHERE user_id={userid}"; 85 | long join_date = (long)cmd.ExecuteScalar(); 86 | string createdAt = new DateTime(1970, 1, 1).AddSeconds(join_date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); 87 | conn.Close(); 88 | var r = new JObject() 89 | { 90 | {"user_id",userid }, 91 | {"updatedAt",updatedAt }, 92 | {"createdAt",createdAt }, 93 | {"friends", me.FriendsList} 94 | }; 95 | return r; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/Present.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using Newtonsoft.Json.Linq; 3 | using System.Linq; 4 | using Team123it.Arcaea.MarveCube.Core; 5 | using MySql.Data.MySqlClient; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | namespace Team123it.Arcaea.MarveCube.Processors.Front 10 | { 11 | public class Present 12 | { 13 | /// 14 | /// 接收指定的礼物。 15 | /// 16 | /// 玩家的用户id。 17 | /// 要接收的礼物id。 18 | /// 包含接收结果信息的 类实例。 19 | /// 20 | public static JObject ClaimPresent(uint userId, string presentId) 21 | { 22 | var p = new PlayerInfo(userId, out _); 23 | if (p.Banned!.Value) 24 | { 25 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountHasBeenBlocked); 26 | } 27 | if (p.ClaimedPresentsList != null && p.ClaimedPresentsList.Any(claimedPresent => ((string)claimedPresent) == presentId)) 28 | { 29 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AlreadyHasItem); 30 | } 31 | using var conn = new MySqlConnection(DatabaseConnectURL); 32 | try 33 | { 34 | var items = (from present in FetchAvailablePresents(p) 35 | where present.Value("present_id") == presentId 36 | select present.Value("items")).First(); 37 | conn.Open(); 38 | var cmd = conn.CreateCommand(); 39 | var claimedPresents = p.ClaimedPresentsList ?? new JArray(); 40 | foreach (JObject item in items) 41 | { 42 | switch (item.Value("type")) 43 | { 44 | case "memory": 45 | int amount = item.Value("amount"); 46 | int totalMemories = p.Ticket!.Value + amount; 47 | cmd.CommandText = $"UPDATE users SET ticket={totalMemories} WHERE user_id={p.UserId!.Value};"; 48 | cmd.ExecuteNonQuery(); 49 | break; 50 | } 51 | } 52 | claimedPresents.Add(presentId); 53 | cmd.CommandText = $"UPDATE users SET claimed_presents=?claimedPresents WHERE user_id={p.UserId!.Value};"; 54 | cmd.Parameters.Add(new MySqlParameter("?claimedPresents", MySqlDbType.Text) 55 | { 56 | Value = claimedPresents.ToString() 57 | }); 58 | cmd.ExecuteNonQuery(); 59 | p.RefreshData(); 60 | var r = new JObject() 61 | { 62 | { "user", p.GetUserBaseInfoData() }, 63 | { "items", items }, 64 | { "reward_char_stats", new JArray() } 65 | }; 66 | return r; 67 | } 68 | catch (ArcaeaAPIException) 69 | { 70 | throw; 71 | } 72 | catch (Exception ex) 73 | { 74 | Console.WriteLine(ex.ToString()); 75 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 76 | } 77 | finally 78 | { 79 | conn.Close(); 80 | } 81 | } 82 | 83 | /// 84 | /// 获取指定玩家可接收的礼物列表。 85 | /// 86 | /// 玩家对应的 类实例。 87 | /// 要获取的礼物信息使用的语言。默认为简体中文(zh-Hans)。 88 | /// 包括可接受礼物列表的 类实例。 89 | public static JArray FetchAvailablePresents(PlayerInfo info, string language = "zh-Hans") 90 | { 91 | var r = new JArray(); 92 | using var conn = new MySqlConnection(DatabaseConnectURL); 93 | try 94 | { 95 | conn.Open(); 96 | var cmd = conn.CreateCommand(); 97 | cmd.CommandText = $"SELECT * FROM fixed_presents;"; 98 | var rd = cmd.ExecuteReader(); 99 | while (rd.Read()) 100 | { 101 | if ((info.ClaimedPresentsList != null) && info.ClaimedPresentsList.ToObject>()!.Contains(rd.GetString("present_id"))) 102 | { 103 | continue; 104 | } 105 | var singlePresent = new JObject() 106 | { 107 | { "expire_ts", Convert.ToInt64((rd.GetDateTime("expire_time") - DateTime.UnixEpoch).TotalMilliseconds) }, 108 | { "is_claimed", (info.ClaimedPresentsList != null) && info.ClaimedPresentsList.ToObject>()!.Contains(rd.GetString("present_id")) }, 109 | { "description", rd.GetString($"description_{language}") }, 110 | { "present_id", rd.GetString("present_id") }, 111 | { "items", JArray.Parse(rd.GetString("items")) } 112 | }; 113 | Console.WriteLine(singlePresent.Value("expire_ts")); 114 | r.Add(singlePresent); 115 | } 116 | rd.Close(); 117 | return r; 118 | } 119 | catch (ArcaeaAPIException) 120 | { 121 | throw; 122 | } 123 | catch (Exception ex) 124 | { 125 | Console.WriteLine(ex.ToString()); 126 | return new JArray(); 127 | } 128 | finally 129 | { 130 | conn.Close(); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/Serve.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 3 | using MySql.Data.MySqlClient; 4 | using Newtonsoft.Json.Linq; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Enhance.Security.Cryptography; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Web; 11 | using Team123it.Arcaea.MarveCube.Core; 12 | using Team123it.Arcaea.MarveCube.Processors.Background; 13 | 14 | namespace Team123it.Arcaea.MarveCube.Processors.Front 15 | { 16 | /// 17 | /// 数据提供相关API。
18 | /// 对应API前缀:/years/19/serve/ 19 | ///
20 | public static class Serve 21 | { 22 | private static readonly string[] SongFileList = { "0.aff", "1.aff", "2.aff", "3.aff", "3.ogg", "base.ogg" }; 23 | public static JObject GetDownloadAvailableSongs(uint userId, IEnumerable? customSongIds = null, bool isUrlMode = true) 24 | { 25 | if (isUrlMode) 26 | { 27 | // StandaloneToken.ForceUpdateToken(); 28 | } 29 | var r = new JObject(); 30 | if (customSongIds == null) 31 | { 32 | var allSongsDirInfos = new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs")).GetDirectories(); 33 | foreach (var songDirInfo in allSongsDirInfos) 34 | { 35 | string songId = songDirInfo.Name; 36 | var songDetails = GetSingleSongDownloadDetails(userId, songId, isUrlMode); 37 | if (songDetails != null) 38 | { 39 | foreach (var songDetailsSingleton in songDetails) 40 | { 41 | r[songDetailsSingleton.Key] = songDetailsSingleton.Value; 42 | } 43 | } 44 | } 45 | } 46 | else 47 | { 48 | var selectedSongsDirInfos = from songDirInfo in new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs")).GetDirectories() 49 | where customSongIds.Contains(songDirInfo.Name) 50 | select songDirInfo; 51 | foreach (var songDirInfo in selectedSongsDirInfos) 52 | { 53 | string songId = songDirInfo.Name; 54 | var songDetails = GetSingleSongDownloadDetails(userId, songId, isUrlMode); 55 | foreach (var songDetailsSingleton in songDetails!) 56 | { 57 | r[songDetailsSingleton.Key] = songDetailsSingleton.Value; 58 | } 59 | } 60 | } 61 | return r; 62 | } 63 | 64 | private static JObject? GetSingleSongDownloadDetails(uint userId, string songId, bool isUrlMode = true) 65 | { 66 | using var conn = new MySqlConnection(DatabaseConnectURL); 67 | try 68 | { 69 | if (File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", songId, "Preparing.stat")) 70 | || !Directory.Exists(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", songId))) 71 | { 72 | if (isUrlMode) 73 | { 74 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.CannotGetThisItem); 75 | } 76 | else 77 | { 78 | return null; 79 | } 80 | } 81 | conn.Open(); 82 | var r = new JObject(); 83 | var audios = new JObject(); 84 | var charts = new JObject(); 85 | var songDirInfo = new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "data", "static", "Songs", songId)); 86 | var songFiles = from songFile in songDirInfo.GetFiles() 87 | where SongFileList.Contains(songFile.Name) 88 | select songFile; 89 | string token = RC4Helper.Encrypt($"{userId}-{songId}-{DateTime.Now:yyyyMMddHHmmssfff}", StandaloneToken.Current.Token); 90 | var cmd = conn.CreateCommand(); 91 | cmd.Parameters.Add(new MySqlParameter("?sid", MySqlDbType.VarChar) 92 | { 93 | Value = songId 94 | }); 95 | foreach (var songFile in songFiles) 96 | { 97 | cmd.CommandText = "SELECT checksum FROM fixed_songs_checksum WHERE sid=?sid AND filename=?filename;"; 98 | cmd.Parameters.Add(new MySqlParameter("?filename", MySqlDbType.VarChar) 99 | { 100 | Value = songFile.Name 101 | }); 102 | var checkSumR = cmd.ExecuteScalar(); 103 | string checkSum; 104 | if (checkSumR != null) 105 | { 106 | checkSum = checkSumR.ToString()!; 107 | } 108 | else 109 | { 110 | checkSum = MD5Helper.MD5Encrypt(File.ReadAllBytes(songFile.FullName)); 111 | cmd.CommandText = "INSERT INTO fixed_songs_checksum (`sid`,`filename`,`checksum`) VALUES (?sid,?filename,?checksum)"; 112 | var checkSumParam = new MySqlParameter("?checksum", MySqlDbType.VarChar) 113 | { 114 | Value = checkSum 115 | }; 116 | cmd.Parameters.Add(checkSumParam); 117 | cmd.ExecuteNonQuery(); 118 | cmd.Parameters.Remove(checkSumParam); 119 | } 120 | string url = $"{RemoteDownloadURLPrefix}/song/download?sid={HttpUtility.UrlEncode(songId)}&file={songFile.Name}&token={HttpUtility.UrlEncode(token)}"; 121 | if (songFile.Name.EndsWith(".ogg")) 122 | { 123 | if (songFile.Name == "base.ogg") 124 | { 125 | audios.Add("checksum", checkSum); 126 | if (isUrlMode) 127 | { 128 | audios.Add("url", url); 129 | } 130 | } 131 | else // audioOverride(难度专用音频)支持 132 | { 133 | int songNumber = int.Parse(songFile.Name.Split('.')[0]); 134 | var song = new JObject() 135 | { 136 | { "checksum", checkSum } 137 | }; 138 | if (isUrlMode) 139 | { 140 | song.Add("url", url); 141 | } 142 | audios.Add(songNumber.ToString(), song); 143 | } 144 | } 145 | else 146 | { 147 | int chartNumber = int.Parse(songFile.Name.Split('.')[0]); 148 | var chart = new JObject() 149 | { 150 | { "checksum", checkSum } 151 | }; 152 | if (isUrlMode) 153 | { 154 | chart.Add("url", url); 155 | } 156 | charts.Add(chartNumber.ToString(), chart); 157 | } 158 | cmd.Parameters.RemoveAt("?filename"); 159 | } 160 | r.Add("audio", audios); 161 | r.Add("chart", charts); 162 | if (isUrlMode) 163 | { 164 | Tokens.SetCustomDownloadToken(userId, songId, token); 165 | } 166 | return new JObject() 167 | { 168 | { songId, r } 169 | }; 170 | } 171 | catch (ArcaeaAPIException ex) 172 | { 173 | Console.WriteLine(ex.ToString()); 174 | throw ex; 175 | } 176 | catch (Exception ex) 177 | { 178 | Console.WriteLine(ex.ToString()); 179 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 180 | } 181 | finally 182 | { 183 | conn.Close(); 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/User.cs: -------------------------------------------------------------------------------- 1 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 2 | using MySql.Data.MySqlClient; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using Team123it.Arcaea.MarveCube.Core; 8 | using Random = System.Enhance.Random; 9 | using Team123it.Arcaea.MarveCube.Processors.Background; 10 | 11 | namespace Team123it.Arcaea.MarveCube.Processors.Front 12 | { 13 | /// 14 | /// 玩家用户数据相关API。
15 | /// 对应API前缀:/years/19/user/ 16 | ///
17 | public class User 18 | { 19 | /// 20 | /// [API]新玩家用户注册。 21 | /// 22 | /// 新玩家的昵称。 23 | /// 新玩家的密码。 24 | /// 新玩家的E-mail。 25 | /// Json数据。 26 | /// 27 | public static JObject Register(string name,string password,string email) 28 | { 29 | var r = new JObject(); 30 | using var conn = new MySqlConnection(DatabaseConnectURL); 31 | conn.Open(); 32 | var cmd = conn.CreateCommand(); 33 | cmd.CommandText = $"SELECT COUNT(*) FROM users WHERE name=?name"; 34 | cmd.Parameters.Add(new MySqlParameter("?name", MySqlDbType.VarChar) 35 | { 36 | Value = name 37 | }); 38 | bool isNameDuplicated = ((long)cmd.ExecuteScalar() == 1) ? true : false; //检查是否用户名重复 39 | if (isNameDuplicated) 40 | { 41 | conn.Close(); 42 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UsernameExists); 43 | } 44 | cmd.Parameters.Clear(); 45 | cmd.CommandText = $"SELECT COUNT(*) FROM users WHERE email=?email"; 46 | cmd.Parameters.Add(new MySqlParameter("?email", MySqlDbType.VarChar) 47 | { 48 | Value = email 49 | }); 50 | bool isEmailDuplicated = ((long)cmd.ExecuteScalar() == 1) ? true : false; //检查是否E-mail重复 51 | if (isEmailDuplicated) 52 | { 53 | conn.Close(); 54 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.EmailHasRegistered); 55 | } 56 | var tr = conn.BeginTransaction(); 57 | string passSHA256 = BitConverter.ToString(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(password))).Replace("-", "").ToLower(); 58 | byte[] save = new byte[1024]; 59 | RandomNumberGenerator.Create().GetBytes(save,0,1024); 60 | string user_code = BitConverter.ToUInt32(save).ToString().Substring(0,8); 61 | while (user_code.Length < 9) 62 | { 63 | user_code = "0" + user_code; 64 | } 65 | cmd.CommandText = $"INSERT INTO users (user_code,name,email,password,join_date,favorite_character) VALUES " + 66 | $"('{user_code}',?name,?email,'{passSHA256}',{(long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds},-1)"; 67 | cmd.Parameters.Add(new MySqlParameter("?name", MySqlDbType.VarChar) 68 | { 69 | Value = name 70 | }); 71 | cmd.ExecuteNonQuery(); 72 | cmd.Parameters.Clear(); 73 | cmd.CommandText = $"SELECT user_id FROM users WHERE name=?name"; 74 | cmd.Parameters.Add(new MySqlParameter("?name", MySqlDbType.VarChar) 75 | { 76 | Value = name 77 | }); 78 | int user_id = (int)cmd.ExecuteScalar(); 79 | cmd.CommandText = $"INSERT INTO user_chars (user_id,character_id,level,exp,level_exp,frag,prog,overdrive,skill_id) VALUES ({user_id},0,1,0,50,50,50,50,'gauge_easy');"; 80 | cmd.ExecuteNonQuery(); 81 | cmd.CommandText = $"INSERT INTO user_chars (user_id,character_id,level,exp,level_exp,frag,prog,overdrive) VALUES ({user_id},1,1,0,50,50,50,50);"; 82 | cmd.ExecuteNonQuery(); 83 | string token = Random.GenerateRandomString(15) + "="; 84 | cmd.CommandText = $"INSERT INTO logins (access_token,user_id) VALUES ('{token}',{user_id})"; 85 | cmd.ExecuteNonQuery(); 86 | tr.Commit(); 87 | conn.Close(); 88 | r.Add("success", true); 89 | r.Add("value", new JObject() 90 | { 91 | {"user_id",user_id }, 92 | {"access_token",token } 93 | }); 94 | return r; 95 | } 96 | 97 | /// 98 | /// [API]切换角色。 99 | /// 100 | /// 玩家的用户id。 101 | /// 新角色id。 102 | /// 技能是否已被封印。 103 | /// 104 | public static JObject ChangeCharacter(uint userid,uint charid,bool isSkillSealed) 105 | { 106 | int skill_sealed = isSkillSealed ? 1 : 0; 107 | var r = new JObject(); 108 | var conn = new MySqlConnection(DatabaseConnectURL); 109 | conn.Open(); 110 | var cmd = conn.CreateCommand(); 111 | cmd.CommandText = $"UPDATE users SET character_id={charid},is_skill_sealed={skill_sealed} WHERE user_id={userid}"; 112 | cmd.ExecuteNonQuery(); 113 | conn.Close(); 114 | r.Add("user_id", userid); 115 | r.Add("character", charid); 116 | return r; 117 | } 118 | 119 | /// 120 | /// [API]调整玩家的设置。 121 | /// 122 | /// 玩家的用户id(非好友id)。 123 | /// 设置的类型。 124 | /// favorite_character - 星标搭档设置
125 | /// is_hide_rating - 个人游玩潜力值隐藏/显示设置
126 | /// 设置的值。 127 | /// 玩家的微型Aggregate结果(TinyAggregate)的Json数据。 128 | public static JObject SetPlayerSettings(uint userid,string type,object value) 129 | { 130 | switch (type) 131 | { 132 | case "is_hide_rating": 133 | { 134 | bool hideRating = Convert.ToBoolean(value); 135 | using var conn = new MySqlConnection(DatabaseConnectURL); 136 | conn.Open(); 137 | var cmd = conn.CreateCommand(); 138 | cmd.CommandText = "UPDATE users SET is_hide_rating=?hideRating WHERE user_id=?uid;"; 139 | cmd.Parameters.Add(new MySqlParameter("?uid", MySqlDbType.Int32) 140 | { 141 | Value = userid 142 | }); 143 | cmd.Parameters.Add(new MySqlParameter("?hideRating", MySqlDbType.Int32) 144 | { 145 | Value = hideRating 146 | }); 147 | cmd.ExecuteNonQuery(); 148 | } 149 | var r = new JObject() 150 | { 151 | { "value", new PlayerInfo(userid,out _).GetUserBaseInfoData() } 152 | }; 153 | return r; 154 | case "favorite_character": 155 | { 156 | int favoriteChar = Convert.ToInt32(value); 157 | using var conn = new MySqlConnection(DatabaseConnectURL); 158 | conn.Open(); 159 | var cmd = conn.CreateCommand(); 160 | cmd.CommandText = "SELECT COUNT(character_id) FROM user_chars WHERE user_id=?uid AND character_id=?charId;"; 161 | cmd.Parameters.Add(new MySqlParameter("?uid", MySqlDbType.Int32) 162 | { 163 | Value = userid 164 | }); 165 | cmd.Parameters.Add(new MySqlParameter("?charId", MySqlDbType.Int32) 166 | { 167 | Value = favoriteChar 168 | }); 169 | int isCharExists = Convert.ToInt32(cmd.ExecuteScalar()); 170 | if (isCharExists == 1) 171 | { 172 | cmd.Parameters.Clear(); 173 | cmd.CommandText = "UPDATE users SET favorite_character=?favCharId WHERE user_id=?uid;"; 174 | cmd.Parameters.Add(new MySqlParameter("?uid", MySqlDbType.Int32) 175 | { 176 | Value = userid 177 | }); 178 | cmd.Parameters.Add(new MySqlParameter("?favCharId", MySqlDbType.Int32) 179 | { 180 | Value = favoriteChar 181 | }); 182 | cmd.ExecuteNonQuery(); 183 | conn.Close(); 184 | } 185 | else 186 | { 187 | conn.Close(); 188 | SecurityManager.EditPlayerCreditPoint(userid, -6, $"Attempted to switch favorite character to the character that the player doesn't possess. Character Id:{favoriteChar}"); 189 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountBlockWarning); 190 | } 191 | } 192 | var r2 = new JObject() 193 | { 194 | { "value", new PlayerInfo(userid,out _).GetUserBaseInfoData() } 195 | }; 196 | return r2; 197 | default: 198 | throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Processors/Front/World.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using Team123it.Arcaea.MarveCube.Core; 3 | using World2 = Team123it.Arcaea.MarveCube.Processors.Background.World; 4 | 5 | namespace Team123it.Arcaea.MarveCube.Processors.Front 6 | { 7 | /// 8 | /// 世界(World)模式相关API。
9 | /// 对应API前缀:/years/19/world/ 10 | ///
11 | public class World 12 | { 13 | /// 14 | /// 获取世界模式完整信息。 15 | /// 16 | /// 玩家的用户id。 17 | /// 获取到的世界模式信息数据 18 | /// 19 | public static JObject GetAllWorldInfo(uint userid) 20 | { 21 | var r = new JObject(); 22 | var info = new PlayerInfo(userid, out bool isExists); 23 | if (!isExists) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UserNotExist); //用户不存在 24 | if (info.Banned.Value) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountHasBeenBlocked); 25 | //用户账号被冻结 26 | r.Add("current_map", info.CurrentMap); 27 | r.Add("user_id", userid); 28 | r.Add("maps", World2.GetAllMaps(userid, out bool success)); 29 | if (!success) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 30 | return r; 31 | } 32 | 33 | /// 34 | /// 获取玩家指定地图的数据。 35 | /// 36 | /// 玩家的用户id。 37 | /// 地图id。 38 | /// 获取到的地图数据 39 | /// 40 | public static JObject GetUserMapInfo(uint userid,string mapid) 41 | { 42 | var r = World2.GetUserMap(userid, mapid, out bool success); 43 | if (!success) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.Other); 44 | return r; 45 | } 46 | 47 | /// 48 | /// 获取玩家的指定地图的完整信息。 49 | /// 50 | /// 玩家的用户id。 51 | /// 地图id。 52 | /// 获取到的地图完整信息 53 | /// 54 | public static JObject GetUserSingleMap(uint userid,string mapid) 55 | { 56 | var r = new JObject(); 57 | var info = new PlayerInfo(userid, out bool isExists); 58 | if (!isExists) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.UserNotExist); //用户不存在 59 | if (info.Banned.Value) throw new ArcaeaAPIException(ArcaeaAPIException.APIExceptionType.AccountHasBeenBlocked); 60 | //玩家账号已被冻结 61 | info.CurrentMap = mapid; 62 | r.Add("user_id", userid); 63 | r.Add("current_map", mapid); 64 | var userMapDetails = World2.GetUserMap(userid, mapid, out _); 65 | var userMap = World2.GetMap(mapid, out _); 66 | userMap.Remove("curr_position"); 67 | userMap.Remove("curr_capture"); 68 | userMap.Add("curr_position", userMapDetails.TryGetValue("curr_position", out var curr_position) ? (int)curr_position! : 0); 69 | userMap.Add("curr_capture", userMapDetails.TryGetValue("curr_capture", out var curr_capture) ? (double)curr_capture! : 0.0); 70 | r.Add("maps", new JArray() 71 | { 72 | { userMap } 73 | }); 74 | return r; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Program.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using static Team123it.Arcaea.MarveCube.GlobalProperties; 3 | using System; 4 | using System.IO; 5 | using System.Threading; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Team123it.Arcaea.MarveCube 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | Initialization(args); 18 | } 19 | 20 | public static void Initialization(string[] args) 21 | { 22 | Console.Clear(); 23 | Console.WriteLine("Welcome to Arcaea Server 2(123 MarveCube Public Version)."); 24 | Console.WriteLine($"(C)Copyright 2015-{DateTime.Now.Year} 123 Open-Source Organization(Team123it). All rights reserved."); 25 | Console.WriteLine(); 26 | Thread.Sleep(1000); 27 | Console.WriteLine("Please wait while system detecting the configurating state..."); 28 | if (!Directory.Exists(Path.Combine(AppContext.BaseDirectory,"data")) 29 | || (!File.Exists(Path.Combine(AppContext.BaseDirectory, "data", "config.json")))) 30 | { 31 | Console.WriteLine("Detected the very first start, starting initialization..."); 32 | FirstStart.FirstStart.FastInitialize(); 33 | Console.WriteLine("Config initialized, now starting api..."); 34 | CreateHostBuilder(args).Build().Run(); 35 | } 36 | else 37 | { 38 | Console.WriteLine("Detected exist configuration and data store, now starting api..."); 39 | CreateHostBuilder(args).Build().Run(); 40 | } 41 | Console.WriteLine("Api stopped. Press any key to exit program."); 42 | Console.ReadKey(true); 43 | Environment.Exit(0); 44 | } 45 | 46 | public static IHostBuilder CreateHostBuilder(string[] args) 47 | { 48 | var config = new ConfigurationBuilder() 49 | .Build(); 50 | return Host.CreateDefaultBuilder(args) 51 | .ConfigureLogging(logBuilder => 52 | { 53 | logBuilder.SetMinimumLevel(LogLevel.Trace); 54 | }) 55 | .ConfigureWebHostDefaults(webBuilder => 56 | { 57 | webBuilder.UseConfiguration(config); 58 | webBuilder.UseStartup(); 59 | webBuilder.UseUrls($"http://*:{ListenPort}"); 60 | }) 61 | .UseEnvironment("Development"); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:59742", 7 | "sslPort": 44355 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Team123it.Arcaea.MarveCube": { 21 | "commandName": "Project", 22 | "launchUrl": "", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Production" 25 | }, 26 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Enhance.AspNetCore; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Http.Features; 9 | using Microsoft.AspNetCore.HttpOverrides; 10 | using Microsoft.AspNetCore.ResponseCompression; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | 15 | namespace Team123it.Arcaea.MarveCube 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddControllers(); 30 | services.AddTransient(); 31 | services.Configure(options => options.BufferBody = true); 32 | } 33 | 34 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 35 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 36 | { 37 | if (env.IsDevelopment()) 38 | { 39 | app.UseDeveloperExceptionPage(); 40 | } 41 | 42 | app.UseMiddleware(); // Forcibly disable buffering(chunked) response 43 | 44 | app.UseRouting(); 45 | 46 | app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); 47 | 48 | app.UseEndpoints((endpoints) => 49 | { 50 | endpoints.MapControllers(); 51 | }); 52 | 53 | app.Run(async context => 54 | { 55 | Console.ForegroundColor = ConsoleColor.Yellow; 56 | Console.WriteLine($"{DateTime.Now:[yyyy-M-d H:mm:ss]} Someone is trying to visit api without logining before.\n" + 57 | $"IP:{context.Connection.RemoteIpAddress}\n" + 58 | $"Visited Path:{context.Request.Path}"); 59 | Console.ResetColor(); 60 | await context.Response.WriteAsync("Sorry but this is not what you are waiting for...\n"); 61 | await context.Response.WriteAsync($"Your IP:{context.Connection.RemoteIpAddress}\n"); 62 | await context.Response.WriteAsync($"Current Path: {context.Request.Path}\n"); 63 | }); 64 | } 65 | 66 | // by Misaka12456 2022.4 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/System.Enhance (Part)/System.Enhance.AspNetCore/DeChunkerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using System; 5 | using System.IO.Compression; 6 | using Microsoft.AspNetCore.Http.Features; 7 | 8 | namespace System.Enhance.AspNetCore 9 | { 10 | public class DeChunkerMiddleware : IMiddleware 11 | { 12 | public DeChunkerMiddleware() 13 | { 14 | 15 | } 16 | 17 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 18 | { 19 | // Disable Transfer-Encoding:chunked, use automatically Content-Length instead 20 | var feature = context.Features.Get(); 21 | feature?.DisableBuffering(); 22 | context.Response.Headers["Content-Encoding"] = "identity"; 23 | // For NGINX Server, we set this to forcibly disable buffering response 24 | // (lowiro curl client doesn't support buffering/chunked response) 25 | // (https://github.com/Misaka12456/ArcaeaServer2/issues/11) 26 | context.Response.Headers["X-Accel-Buffering"] = "no"; 27 | var originalBodyStream = context.Response.Body; 28 | using (var responseBody = new MemoryStream()) 29 | { 30 | context.Response.Body = responseBody; 31 | long length = 0; 32 | await next(context); 33 | // If you want to read the body, uncomment these lines. 34 | context.Response.Body.Seek(0, SeekOrigin.Begin); 35 | var body = await new StreamReader(context.Response.Body).ReadToEndAsync(); 36 | length = context.Response.Body.Length; 37 | context.Response.Body.Seek(0, SeekOrigin.Begin); 38 | context.Response.Headers.ContentLength = length; 39 | await responseBody.CopyToAsync(originalBodyStream); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/System.Enhance (Part)/System.Enhance.AspNetCore/RealIpFetcherMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | 5 | namespace System.Enhance.AspNetCore 6 | { 7 | public class RealIpFetcherMiddleware : IMiddleware 8 | { 9 | public RealIpFetcherMiddleware() 10 | { 11 | 12 | } 13 | 14 | public Task InvokeAsync(HttpContext context, RequestDelegate next) 15 | { 16 | var headers = context.Request.Headers; 17 | if (headers.ContainsKey("X-Forwarded-For")) 18 | { 19 | context.Connection.RemoteIpAddress = IPAddress.Parse(headers["X-Forwarded-For"].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries)[0]); 20 | } 21 | else if (headers.ContainsKey("X-Real-IP")) 22 | { 23 | context.Connection.RemoteIpAddress = IPAddress.Parse(headers["X-Real-IP"]); 24 | } 25 | return next(context); 26 | } 27 | } 28 | 29 | public class LargeDataProcessMiddleware : IMiddleware 30 | { 31 | public LargeDataProcessMiddleware() 32 | { 33 | 34 | } 35 | 36 | public Task InvokeAsync(HttpContext context, RequestDelegate next) 37 | { 38 | var headers = context.Request.Headers; 39 | if (headers.ContainsKey("Transfer-Encoding")) 40 | { 41 | context.Response.Headers.Remove("Transfer-Encoding"); 42 | } 43 | if (int.TryParse(context.Response.Headers["Content-Length"], out int contentLen) && (contentLen >= 1024)) 44 | { 45 | context.Response.Headers.Remove("Content-Length"); 46 | context.Response.Headers.Add("Transfer-Encoding", "Chunked"); 47 | } 48 | return next(context); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/System.Enhance (Part)/System.Enhance.Collections.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using System.ComponentModel; 3 | 4 | namespace System.Enhance 5 | { 6 | /// 7 | /// 提供适用于 类的增强方法的类。无法继承此类。 8 | /// 9 | public static class Collections 10 | { 11 | /// 12 | /// 获取枚举的 特性中的说明。 13 | /// 14 | /// 当前 枚举实例。 15 | /// 成功返回枚举的特性说明文本,失败返回 16 | public static string? GetDescription(this Enum instance) 17 | { 18 | var type = instance.GetType(); 19 | var infos = type.GetMember(instance.ToString()); 20 | if (infos != null && infos.Length > 0) 21 | { 22 | object[] attrs = infos[0].GetCustomAttributes(typeof(DescriptionAttribute), false); 23 | if (attrs != null && attrs.Length > 0) 24 | { 25 | return ((DescriptionAttribute)attrs[0]).Description; 26 | } 27 | else 28 | { 29 | return null; 30 | } 31 | } 32 | else 33 | { 34 | return null; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/System.Enhance (Part)/System.Enhance.MySql.Data.cs: -------------------------------------------------------------------------------- 1 | using MySql.Data.MySqlClient; 2 | using System.Collections; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace System.Enhance.MySql.Data 7 | { 8 | public class MysqlExecutor 9 | { 10 | public static bool ExecuteSqlFileData(string sqlConnString, string varData) 11 | { 12 | var stream = new MemoryStream(); 13 | var ws = new StreamWriter(stream, Encoding.UTF8); 14 | ws.Write(varData); 15 | ws.Flush(); 16 | stream.Seek(0, SeekOrigin.Begin); 17 | var rs = new StreamReader(stream, Encoding.UTF8); 18 | var alSql = new ArrayList(); 19 | string commandText = ""; 20 | string varLine = ""; 21 | while (rs.Peek() > -1) 22 | { 23 | varLine = rs.ReadLine(); 24 | if (varLine == "") 25 | { 26 | continue; 27 | } 28 | if (varLine != "GO") 29 | { 30 | commandText += varLine; 31 | commandText += "\r\n"; 32 | } 33 | else 34 | { 35 | commandText += ""; 36 | } 37 | } 38 | alSql.Add(commandText); 39 | rs.Close(); 40 | try 41 | { 42 | ExecuteCommand(sqlConnString, alSql); 43 | return true; 44 | } 45 | catch (Exception) 46 | { 47 | throw; 48 | } 49 | } 50 | private static void ExecuteCommand(string sqlConnString, ArrayList varSqlList) 51 | { 52 | using var conn = new MySqlConnection(sqlConnString); 53 | conn.Open(); 54 | var cmd = conn.CreateCommand(); 55 | try 56 | { 57 | foreach (string varcommandText in varSqlList) 58 | { 59 | cmd.CommandText = varcommandText; 60 | cmd.ExecuteNonQuery(); 61 | } 62 | } 63 | catch (Exception) 64 | { 65 | throw; 66 | } 67 | finally 68 | { 69 | conn.Close(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/System.Enhance (Part)/System.Enhance.Random.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace System.Enhance 4 | { 5 | /// 6 | /// 提供适用于 类的增强方法的类。无法继承此类。 7 | /// 8 | public sealed class Random 9 | { 10 | /// 11 | /// 生成指定长度的随机字符串。 12 | /// 13 | /// 随机字符串的长度。 14 | /// 生成结果。 15 | public static string GenerateRandomString(int digits) 16 | { 17 | byte[] result = new byte[digits - 1]; 18 | RandomNumberGenerator.Create().GetBytes(result, 0, digits - 1); 19 | string resultStr = Convert.ToBase64String(result).Substring(0, digits - 1); 20 | return resultStr; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/System.Enhance (Part)/System.Enhance.Web.Json.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace System.Enhance.Web.Json 8 | { 9 | /// 10 | /// 表示一个 实例(Json数据)格式的 。 11 | /// 12 | public class JObjectResult : ActionResult 13 | { 14 | /// 15 | /// 使用指定的 实例初始化 类的新实例。 16 | /// 17 | /// 指定的 实例。 18 | public JObjectResult(JObject data) 19 | { 20 | JsonData = data ?? throw new NullReferenceException("未将对象引用设置到对象的实例。\r\ndata 值为null。"); 21 | } 22 | 23 | /// 24 | /// 使用指定的Json字符串初始化 类的新实例。 25 | /// 26 | /// 27 | public JObjectResult(string? jsonStr) 28 | { 29 | try 30 | { 31 | JsonData = (jsonStr == null) ? new JObject() : JObject.Parse(jsonStr); 32 | } 33 | catch(JsonReaderException ex) 34 | { 35 | throw new JsonReaderException(ex.Message, ex); 36 | } 37 | } 38 | public override void ExecuteResult(ActionContext context) 39 | { 40 | var resp = context.HttpContext.Response; 41 | resp.StatusCode = 200; 42 | resp.ContentType = "application/json"; 43 | resp.WriteAsync(JsonData.ToString(Formatting.None)); 44 | } 45 | 46 | /// 47 | /// 获取当前 实例对应的 实例(Json数据)。 48 | /// 49 | public JObject JsonData { get; } 50 | 51 | /// 52 | /// 将当前 实例对应的 Json 数据转换为Json字符串。 53 | /// 54 | /// 转换后的Json字符串。 55 | public override string ToString() 56 | { 57 | return JsonData.ToString(); 58 | } 59 | 60 | public string ToString(Formatting formatting,params JsonConverter[] converters) 61 | { 62 | return JsonData.ToString(formatting, converters); 63 | } 64 | } 65 | 66 | /// 67 | /// 表示一个 实例(Json数组数据)格式的 。 68 | /// 69 | public class JArrayResult : ActionResult 70 | { 71 | /// 72 | /// 获取当前 实例对应的 实例(Json数组数据)。 73 | /// 74 | public JArray JsonData { get; } 75 | 76 | /// 77 | /// 使用指定的 实例初始化 类的新实例。 78 | /// 79 | /// 指定的 实例。 80 | public JArrayResult(JArray data) 81 | { 82 | JsonData = data ?? throw new NullReferenceException("未将对象引用设置到对象的实例。\r\ndata 值为null。"); 83 | } 84 | 85 | /// 86 | /// 使用指定的Json字符串初始化 类的新实例。 87 | /// 88 | /// 89 | public JArrayResult(string? jsonStr) 90 | { 91 | try 92 | { 93 | JsonData = (jsonStr == null) ? new JArray() : JArray.Parse(jsonStr); 94 | } 95 | catch (JsonReaderException ex) 96 | { 97 | throw new JsonReaderException(ex.Message, ex); 98 | } 99 | } 100 | public override void ExecuteResult(ActionContext context) 101 | { 102 | var resp = context.HttpContext.Response; 103 | resp.StatusCode = 200; 104 | resp.ContentType = "application/json"; 105 | resp.WriteAsync(JsonData.ToString(Formatting.None)); 106 | } 107 | 108 | /// 109 | /// 将当前 实例对应的 Json 数据转换为Json字符串。 110 | /// 111 | /// 转换后的Json字符串。 112 | public override string ToString() 113 | { 114 | return JsonData.ToString(); 115 | } 116 | 117 | public string ToString(Formatting formatting, params JsonConverter[] converters) 118 | { 119 | return JsonData.ToString(formatting, converters); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/Team123it.Arcaea.MarveCube.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | MarveCube 6 | 0.4.4 7 | 123 Open-Source Organization 8 | Arcaea Server 2(123 Marvelous Cube Open-Source Version) 9 | 0.4.4.1 10 | 0.4.4.1 11 | (C)Copyright 2015-2022 123 Open-Source Organization. All rights reserved. 12 | Arcaea Server 2 - High-Speed Protable Arcaea API Server 13 | x64;ARM64 14 | 3ec8699b-b822-4ec9-83d6-25f853a0fab7 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Never 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Team123it.Arcaea.MarveCube/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /docs/songinfo.md: -------------------------------------------------------------------------------- 1 | ## song/info 2 | 3 | | arguments | description | optional | 4 | |:-----------|:---------------------------------------------------------------------------|-------------------------------------------------| 5 | | songid | sid in Arcaea songlist | true when songname is not null, otherwise false | 6 | 7 | #### Example 8 | 9 | + `{apiurl}/botarcapi/song/info?songname=infinity` 10 | 11 | ###### Return data 12 | 13 | ```json 14 | { 15 | "status": 0, 16 | "content": { 17 | "id": "infinityheaven", 18 | "title_localized": { 19 | "en": "Infinity Heaven" 20 | }, 21 | "artist": "HyuN", 22 | "bpm": "160", 23 | "bpm_base": 160.0, 24 | "set": "base", 25 | "set_friendly": "Arcaea", 26 | "world_unlock": false, 27 | "remote_dl": false, 28 | "side": 0, 29 | "time": 154, 30 | "date": 1491868800, 31 | "version": "1.0", 32 | "difficulties": [ 33 | { 34 | "ratingClass": 0, 35 | "chartDesigner": "Nitro", 36 | "jacketDesigner": "Tagtraume", 37 | "jacketOverride": false, 38 | "realrating": 15, 39 | "totalNotes": 336 40 | }, 41 | { 42 | "ratingClass": 1, 43 | "chartDesigner": "Nitro", 44 | "jacketDesigner": "Tagtraume", 45 | "jacketOverride": false, 46 | "realrating": 55, 47 | "totalNotes": 545 48 | }, 49 | { 50 | "ratingClass": 2, 51 | "chartDesigner": "Nitro", 52 | "jacketDesigner": "Tagtraume", 53 | "jacketOverride": false, 54 | "realrating": 75, 55 | "totalNotes": 853 56 | }, 57 | { 58 | "ratingClass": 3, 59 | "chartDesigner": "Nitr∞", 60 | "jacketDesigner": "Tagtraume", 61 | "jacketOverride": true, 62 | "realrating": 96, 63 | "totalNotes": 986 64 | } 65 | ] 66 | } 67 | } 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /docs/userbest.md: -------------------------------------------------------------------------------- 1 | ## user/best 2 | 3 | | arguments | description | optional | 4 | |:-------------|:---------------------------------------------------------------------------|-------------------------------------------------| 5 | | user | user name or 9-digit user code | true when usercode is not null, otherwise false | 6 | | songid | sid in Arcaea songlist | true when songname is not null, otherwise false | 7 | | difficulty | accept format are 0/1/2/3 | false | 8 | | withrecent | boolean. if true, will reply with recent_score | true | 9 | | withsonginfo | boolean. if true, will reply with songinfo | true | 10 | 11 | #### Example 12 | 13 | + `{apiurl}/botarcapi/user/best?user=ToasterKoishi&songid=ifi&difficulty=2&withrecent=true&withsonginfo=true` 14 | 15 | ###### Return data 16 | 17 | ```json 18 | { 19 | "status": 0, 20 | "content": { 21 | "account_info": { 22 | "code": "062596721", 23 | "name": "ToasterKoishi", 24 | "user_id": 4, 25 | "is_mutual": false, 26 | "is_char_uncapped_override": false, 27 | "is_char_uncapped": true, 28 | "is_skill_sealed": false, 29 | "rating": 1274, 30 | "join_date": 1487816563340, 31 | "character": 12 32 | }, 33 | "record": { 34 | "score": 9979257, 35 | "health": 100, 36 | "rating": 12.796285000000001, 37 | "song_id": "ifi", 38 | "modifier": 0, 39 | "difficulty": 2, 40 | "clear_type": 1, 41 | "best_clear_type": 5, 42 | "time_played": 1598919831344, 43 | "near_count": 5, 44 | "miss_count": 1, 45 | "perfect_count": 1570, 46 | "shiny_perfect_count": 1466 47 | }, 48 | "songinfo": [ 49 | { 50 | "id": "ifi", 51 | "title_localized": { 52 | "en": "#1f1e33" 53 | }, 54 | "artist": "かめりあ(EDP)", 55 | "bpm": "181", 56 | "bpm_base": 181.0, 57 | "set": "vs", 58 | "set_friendly": "Black Fate", 59 | "world_unlock": false, 60 | "remote_dl": true, 61 | "side": 1, 62 | "time": 163, 63 | "date": 1590537604, 64 | "version": "3.0", 65 | "difficulties": [ 66 | { 67 | "ratingClass": 0, 68 | "chartDesigner": "夜浪", 69 | "jacketDesigner": "望月けい", 70 | "jacketOverride": false, 71 | "realrating": 55, 72 | "totalNotes": 765 73 | }, 74 | { 75 | "ratingClass": 1, 76 | "chartDesigner": "夜浪", 77 | "jacketDesigner": "望月けい", 78 | "jacketOverride": false, 79 | "realrating": 92, 80 | "totalNotes": 1144 81 | }, 82 | { 83 | "ratingClass": 2, 84 | "chartDesigner": "夜浪 VS 東星 \"Convergence\"", 85 | "jacketDesigner": "望月けい", 86 | "jacketOverride": false, 87 | "realrating": 109, 88 | "totalNotes": 1576 89 | } 90 | ] 91 | } 92 | ], 93 | "recent_score": { 94 | "user_id": 4, 95 | "score": 9979350, 96 | "health": 100, 97 | "rating": 11.59675, 98 | "song_id": "melodyoflove", 99 | "modifier": 0, 100 | "difficulty": 2, 101 | "clear_type": 1, 102 | "best_clear_type": 3, 103 | "time_played": 1647570474485, 104 | "near_count": 2, 105 | "miss_count": 1, 106 | "perfect_count": 928, 107 | "shiny_perfect_count": 833 108 | }, 109 | "recent_songinfo": { 110 | "id": "melodyoflove", 111 | "title_localized": { 112 | "en": "A Wandering Melody of Love", 113 | "ja": "迷える音色は恋の唄" 114 | }, 115 | "artist": "からとPαnchii少年 feat.はるの", 116 | "bpm": "165", 117 | "bpm_base": 165.0, 118 | "set": "omatsuri", 119 | "set_friendly": "Sunset Radiance", 120 | "world_unlock": false, 121 | "remote_dl": true, 122 | "side": 0, 123 | "time": 134, 124 | "date": 1566432002, 125 | "version": "2.3", 126 | "difficulties": [ 127 | { 128 | "ratingClass": 0, 129 | "chartDesigner": "恋のToaster", 130 | "jacketDesigner": "シエラ", 131 | "jacketOverride": false, 132 | "realrating": 35, 133 | "totalNotes": 422 134 | }, 135 | { 136 | "ratingClass": 1, 137 | "chartDesigner": "恋のToaster", 138 | "jacketDesigner": "シエラ", 139 | "jacketOverride": false, 140 | "realrating": 75, 141 | "totalNotes": 670 142 | }, 143 | { 144 | "ratingClass": 2, 145 | "chartDesigner": "恋のToaster", 146 | "jacketDesigner": "シエラ", 147 | "jacketOverride": false, 148 | "realrating": 97, 149 | "totalNotes": 931 150 | } 151 | ] 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | -------------------------------------------------------------------------------- /docs/userbest30.md: -------------------------------------------------------------------------------- 1 | ## user/best30 2 | 3 | | arguments | description | optional | 4 | |:-------------|:--------------------------------------------------------------------------------|-------------------------------------------------| 5 | | user | user name or 9-digit user code | true when usercode is not null, otherwise false | 6 | | withrecent | boolean. if true, will reply with recent_score | true | 7 | | withsonginfo | boolean. if true, will reply with songinfo | true | 8 | 9 | #### Example 10 | 11 | + `{apiurl}/botarcapi/user/best30?user=ToasterKoishi&withrecent=true&withsonginfo=true` 12 | 13 | ###### Return data (Edited for readability) 14 | 15 | ```json 16 | { 17 | "status": 0, 18 | "content": { 19 | "best30_avg": 12.707672500000001, 20 | "recent10_avg": 12.836982499999998, 21 | "account_info": { 22 | "code": "062596721", 23 | "name": "ToasterKoishi", 24 | "user_id": 4, 25 | "is_mutual": false, 26 | "is_char_uncapped_override": false, 27 | "is_char_uncapped": true, 28 | "is_skill_sealed": false, 29 | "rating": 1274, 30 | "join_date": 1487816563340, 31 | "character": 12 32 | }, 33 | "best30_list": [ 34 | { 35 | "score": 9956548, 36 | "health": 100, 37 | "rating": 13.082740000000001, 38 | "song_id": "grievouslady", 39 | "modifier": 0, 40 | "difficulty": 2, 41 | "clear_type": 1, 42 | "best_clear_type": 5, 43 | "time_played": 1614911430950, 44 | "near_count": 7, 45 | "miss_count": 3, 46 | "perfect_count": 1440, 47 | "shiny_perfect_count": 1376 48 | }, 49 | { 50 | "score": 9884488, 51 | "health": 100, 52 | "rating": 12.92244, 53 | "song_id": "tempestissimo", 54 | "modifier": 0, 55 | "difficulty": 3, 56 | "clear_type": 1, 57 | "best_clear_type": 5, 58 | "time_played": 1591566895228, 59 | "near_count": 20, 60 | "miss_count": 8, 61 | "perfect_count": 1512, 62 | "shiny_perfect_count": 1372 63 | } 64 | ], 65 | "best30_songinfo": [ 66 | { 67 | "id": "grievouslady", 68 | "title_localized": { 69 | "en": "Grievous Lady" 70 | }, 71 | "artist": "Team Grimoire vs Laur", 72 | "bpm": "210", 73 | "bpm_base": 210.0, 74 | "set": "yugamu", 75 | "set_friendly": "Vicious Labyrinth", 76 | "world_unlock": false, 77 | "remote_dl": true, 78 | "side": 1, 79 | "time": 141, 80 | "date": 1509667208, 81 | "version": "1.5", 82 | "difficulties": [ 83 | { 84 | "ratingClass": 0, 85 | "chartDesigner": "迷路第一層", 86 | "jacketDesigner": "シエラ", 87 | "jacketOverride": false, 88 | "realrating": 65, 89 | "totalNotes": 956 90 | }, 91 | { 92 | "ratingClass": 1, 93 | "chartDesigner": "迷路第二層", 94 | "jacketDesigner": "シエラ", 95 | "jacketOverride": false, 96 | "realrating": 93, 97 | "totalNotes": 1194 98 | }, 99 | { 100 | "ratingClass": 2, 101 | "chartDesigner": "迷路深層", 102 | "jacketDesigner": "シエラ", 103 | "jacketOverride": false, 104 | "realrating": 113, 105 | "totalNotes": 1450 106 | } 107 | ] 108 | }, 109 | { 110 | "id": "tempestissimo", 111 | "title_localized": { 112 | "en": "Tempestissimo" 113 | }, 114 | "artist": "t+pazolite", 115 | "bpm": "231", 116 | "bpm_base": 231.0, 117 | "set": "vs", 118 | "set_friendly": "Black Fate", 119 | "world_unlock": false, 120 | "remote_dl": true, 121 | "side": 1, 122 | "time": 137, 123 | "date": 1590537605, 124 | "version": "3.0", 125 | "difficulties": [ 126 | { 127 | "ratingClass": 0, 128 | "chartDesigner": "Prelude - Ouverture", 129 | "jacketDesigner": "シエラ", 130 | "jacketOverride": false, 131 | "realrating": 65, 132 | "totalNotes": 919 133 | }, 134 | { 135 | "ratingClass": 1, 136 | "chartDesigner": "Convergence - Intermezzo", 137 | "jacketDesigner": "シエラ", 138 | "jacketOverride": false, 139 | "realrating": 95, 140 | "totalNotes": 1034 141 | }, 142 | { 143 | "ratingClass": 2, 144 | "chartDesigner": "Onslaught - Crescendo", 145 | "jacketDesigner": "シエラ", 146 | "jacketOverride": false, 147 | "realrating": 106, 148 | "totalNotes": 1254 149 | }, 150 | { 151 | "ratingClass": 3, 152 | "chartDesigner": "Finale - The Tempest", 153 | "jacketDesigner": "シエラ", 154 | "jacketOverride": true, 155 | "realrating": 115, 156 | "totalNotes": 1540 157 | } 158 | ] 159 | } 160 | ], 161 | "recent_score": { 162 | "user_id": 4, 163 | "score": 9979350, 164 | "health": 100, 165 | "rating": 11.59675, 166 | "song_id": "melodyoflove", 167 | "modifier": 0, 168 | "difficulty": 2, 169 | "clear_type": 1, 170 | "best_clear_type": 3, 171 | "time_played": 1647570474485, 172 | "near_count": 2, 173 | "miss_count": 1, 174 | "perfect_count": 928, 175 | "shiny_perfect_count": 833 176 | }, 177 | "recent_songinfo": { 178 | "id": "melodyoflove", 179 | "title_localized": { 180 | "en": "A Wandering Melody of Love", 181 | "ja": "迷える音色は恋の唄" 182 | }, 183 | "artist": "からとPαnchii少年 feat.はるの", 184 | "bpm": "165", 185 | "bpm_base": 165.0, 186 | "set": "omatsuri", 187 | "set_friendly": "Sunset Radiance", 188 | "world_unlock": false, 189 | "remote_dl": true, 190 | "side": 0, 191 | "time": 134, 192 | "date": 1566432002, 193 | "version": "2.3", 194 | "difficulties": [ 195 | { 196 | "ratingClass": 0, 197 | "chartDesigner": "恋のToaster", 198 | "jacketDesigner": "シエラ", 199 | "jacketOverride": false, 200 | "realrating": 35, 201 | "totalNotes": 422 202 | }, 203 | { 204 | "ratingClass": 1, 205 | "chartDesigner": "恋のToaster", 206 | "jacketDesigner": "シエラ", 207 | "jacketOverride": false, 208 | "realrating": 75, 209 | "totalNotes": 670 210 | }, 211 | { 212 | "ratingClass": 2, 213 | "chartDesigner": "恋のToaster", 214 | "jacketDesigner": "シエラ", 215 | "jacketOverride": false, 216 | "realrating": 97, 217 | "totalNotes": 931 218 | } 219 | ] 220 | } 221 | } 222 | } 223 | ``` 224 | -------------------------------------------------------------------------------- /docs/userinfo.md: -------------------------------------------------------------------------------- 1 | ## user/info 2 | 3 | | arguments | description | optional | 4 | |:-------------|:----------------------------------------------------------------|-------------------------------------------------| 5 | | user | user name or 9-digit user code | true when usercode is not null, otherwise false | 6 | | usercode | 9-digit user code | true when user is not null, otherwise false | 7 | | withsonginfo | boolean. if true, will reply with songinfo | true | 8 | 9 | #### Example 10 | 11 | + `{apiurl}/botarcapi/user/info?user=ToasterKoishi&withsonginfo=true` 12 | 13 | ###### Return data 14 | 15 | ```json 16 | { 17 | "status": 0, 18 | "content": { 19 | "account_info": { 20 | "code": "062596721", 21 | "name": "ToasterKoishi", 22 | "user_id": 4, 23 | "is_mutual": false, 24 | "is_char_uncapped_override": false, 25 | "is_char_uncapped": true, 26 | "is_skill_sealed": false, 27 | "rating": 1274, 28 | "join_date": 1487816563340, 29 | "character": 12 30 | }, 31 | "recent_score": [ 32 | { 33 | "score": 9979350, 34 | "health": 100, 35 | "rating": 11.59675, 36 | "song_id": "melodyoflove", 37 | "modifier": 0, 38 | "difficulty": 2, 39 | "clear_type": 1, 40 | "best_clear_type": 3, 41 | "time_played": 1647570474485, 42 | "near_count": 2, 43 | "miss_count": 1, 44 | "perfect_count": 928, 45 | "shiny_perfect_count": 833 46 | } 47 | ], 48 | "songinfo": [ 49 | { 50 | "id": "melodyoflove", 51 | "title_localized": { 52 | "en": "A Wandering Melody of Love", 53 | "ja": "迷える音色は恋の唄" 54 | }, 55 | "artist": "からとPαnchii少年 feat.はるの", 56 | "bpm": "165", 57 | "bpm_base": 165.0, 58 | "set": "omatsuri", 59 | "set_friendly": "Sunset Radiance", 60 | "world_unlock": false, 61 | "remote_dl": true, 62 | "side": 0, 63 | "time": 134, 64 | "date": 1566432002, 65 | "version": "2.3", 66 | "difficulties": [ 67 | { 68 | "ratingClass": 0, 69 | "chartDesigner": "恋のToaster", 70 | "jacketDesigner": "シエラ", 71 | "jacketOverride": false, 72 | "realrating": 35, 73 | "totalNotes": 422 74 | }, 75 | { 76 | "ratingClass": 1, 77 | "chartDesigner": "恋のToaster", 78 | "jacketDesigner": "シエラ", 79 | "jacketOverride": false, 80 | "realrating": 75, 81 | "totalNotes": 670 82 | }, 83 | { 84 | "ratingClass": 2, 85 | "chartDesigner": "恋のToaster", 86 | "jacketDesigner": "シエラ", 87 | "jacketOverride": false, 88 | "realrating": 97, 89 | "totalNotes": 931 90 | } 91 | ] 92 | }, 93 | ] 94 | } 95 | } 96 | ``` 97 | --------------------------------------------------------------------------------