├── .github └── workflows │ └── check-version-and-release.yaml ├── .gitignore ├── README.md ├── app ├── assets │ ├── 64x64.png │ ├── circle-check-icon.png │ ├── circle-cross-icon.png │ ├── circle-error-icon.png │ ├── circle-exclamation-icon.png │ ├── circle-gear-blue-icon.png │ ├── circle-gear-gray-icon.png │ ├── circle-info-icon.png │ ├── circle-minor-icon.png │ ├── circle-minus-icon.png │ ├── circle-plus-icon.png │ ├── close-icon.png │ ├── cogwheel-icon.png │ ├── discussion-icon.png │ ├── discussion-unavailable-icon.png │ ├── doc-icon.png │ ├── folder-external-icon.png │ ├── folder-icon.png │ ├── fonts │ │ └── NotoSans-Regular.ttf │ ├── link-icon.png │ ├── link-unavailable-icon.png │ ├── lock-icon.png │ ├── maximize-icon.png │ ├── minimize-icon.png │ ├── question-icon.png │ ├── reload-icon.png │ ├── reverse-arrow.png │ ├── side-arrow.png │ ├── triangles.png │ ├── unlock-icon.png │ ├── web-icon.png │ └── web-unavailable-icon.png ├── base.js ├── bass.dll ├── index.html ├── libbass.so ├── main.js ├── package-lock.json ├── package.json ├── script.js ├── search.js ├── signalr.js ├── style.css ├── timeline.js ├── tooltip.js └── utils.js ├── bass.dll ├── icon.ico ├── local-dist.bat ├── local-start.bat ├── package-lock.json ├── package.json └── src ├── Checks ├── AllModes │ ├── Compose │ │ ├── CheckAbnormalNodes.cs │ │ ├── CheckConcurrent.cs │ │ ├── CheckDrainTime.cs │ │ └── CheckInvisibleSlider.cs │ ├── Events │ │ ├── CheckBreaks.cs │ │ └── CheckStoryHitSounds.cs │ ├── General │ │ ├── Audio │ │ │ ├── CheckAudioFormat.cs │ │ │ ├── CheckAudioInVideo.cs │ │ │ ├── CheckAudioUsage.cs │ │ │ ├── CheckBitrate.cs │ │ │ ├── CheckCommonFinish.cs │ │ │ ├── CheckHitSoundDelay.cs │ │ │ ├── CheckHitSoundFormat.cs │ │ │ ├── CheckHitSoundImbalance.cs │ │ │ ├── CheckHitSoundLength.cs │ │ │ └── CheckMultipleAudio.cs │ │ ├── Files │ │ │ ├── CheckUnusedFiles.cs │ │ │ ├── CheckUpdateValidity.cs │ │ │ └── CheckZeroBytes.cs │ │ ├── Metadata │ │ │ ├── CheckGenreLanguage.cs │ │ │ ├── CheckGuestTags.cs │ │ │ ├── CheckInconsistentMetadata.cs │ │ │ ├── CheckMarkerFormat.cs │ │ │ ├── CheckMarkerSpacing.cs │ │ │ ├── CheckTitleMarkers.cs │ │ │ └── CheckUnicode.cs │ │ └── Resources │ │ │ ├── CheckBgPresence.cs │ │ │ ├── CheckBgResolution.cs │ │ │ ├── CheckMultipleVideo.cs │ │ │ ├── CheckOverlayLayer.cs │ │ │ ├── CheckSkinResolution.cs │ │ │ ├── CheckSpriteResolution.cs │ │ │ ├── CheckVideoOffset.cs │ │ │ └── CheckVideoResolution.cs │ ├── HitSounds │ │ ├── CheckHitSounds.cs │ │ └── CheckMuted.cs │ ├── Settings │ │ ├── CheckDefaultColours.cs │ │ ├── CheckDiffSettings.cs │ │ ├── CheckInconsistentSettings.cs │ │ ├── CheckLuminosity.cs │ │ └── CheckTickRate.cs │ ├── Spread │ │ └── CheckLowestDiff.cs │ └── Timing │ │ ├── CheckBeforeLine.cs │ │ ├── CheckConcurrentLines.cs │ │ ├── CheckFirstLine.cs │ │ ├── CheckInconsistentLines.cs │ │ ├── CheckKiaiUnsnap.cs │ │ ├── CheckPreview.cs │ │ ├── CheckUnsnaps.cs │ │ ├── CheckUnusedLines.cs │ │ └── CheckWrongSnapping.cs ├── Catch │ └── Compose │ │ └── CheckSpinnerGap.cs ├── Common.cs ├── Examples │ ├── CheckExample.cs │ └── GeneralCheckExample.cs ├── MapsetChecks.csproj ├── Standard │ ├── Compose │ │ ├── CheckAbnormalSpacing.cs │ │ ├── CheckAmbiguity.cs │ │ ├── CheckBurai.cs │ │ ├── CheckNinjaSpinner.cs │ │ ├── CheckObscuredReverse.cs │ │ └── CheckOffscreen.cs │ └── Spread │ │ ├── CheckCloseOverlap.cs │ │ ├── CheckMultipleReverses.cs │ │ ├── CheckShortSliders.cs │ │ ├── CheckSpaceVariation.cs │ │ ├── CheckSpinnerRecovery.cs │ │ └── CheckStackLeniency.cs └── Taiko │ └── Timing │ └── CheckInconsistentBarLines.cs ├── Framework ├── Checker.cs ├── CheckerRegistry.cs └── Objects │ ├── Attributes │ └── CheckAttribute.cs │ ├── BeatmapCheck.cs │ ├── BeatmapSetCheck.cs │ ├── Check.cs │ ├── GeneralCheck.cs │ ├── Issue.cs │ ├── IssueTemplate.cs │ ├── Metadata │ ├── BeatmapCheckMetadata.cs │ └── CheckMetadata.cs │ └── Resources │ ├── AudioBASS.cs │ └── FileAbstraction.cs ├── MapsetVerifier.csproj ├── Parser ├── MapsetParser.csproj ├── Objects │ ├── Beatmap.cs │ ├── BeatmapSet.cs │ ├── Events │ │ ├── Animation.cs │ │ ├── Background.cs │ │ ├── Break.cs │ │ ├── Sample.cs │ │ ├── Sprite.cs │ │ └── Video.cs │ ├── HitObject.cs │ ├── HitObjects │ │ ├── Circle.cs │ │ ├── HoldNote.cs │ │ ├── Slider.cs │ │ ├── Spinner.cs │ │ ├── Stackable.cs │ │ └── Taiko │ │ │ └── TaikoExtensions.cs │ ├── HitSample.cs │ ├── Osb.cs │ ├── TimingLine.cs │ └── TimingLines │ │ ├── InheritedLine.cs │ │ └── UninheritedLine.cs ├── Scoring │ ├── HitResult.cs │ └── HitWindows.cs ├── Settings │ ├── ColourSettings.cs │ ├── DifficultySettings.cs │ ├── GeneralSettings.cs │ └── MetadataSettings.cs ├── StarRating │ ├── DifficultyAttributes.cs │ ├── DifficultyCalculator.cs │ ├── Osu │ │ ├── OsuDifficultyAttributes.cs │ │ ├── OsuDifficultyCalculator.cs │ │ ├── Preprocessing │ │ │ └── OsuDifficultyHitObject.cs │ │ ├── Scoring │ │ │ └── OsuHitWindows.cs │ │ └── Skills │ │ │ ├── Aim.cs │ │ │ └── Speed.cs │ ├── Preprocessing │ │ └── DifficultyHitObject.cs │ ├── Skills │ │ ├── Skill.cs │ │ ├── StrainDecaySkill.cs │ │ └── StrainSkill.cs │ ├── Taiko │ │ ├── Evaluators │ │ │ ├── ColourEvaluator.cs │ │ │ └── StaminaEvaluator.cs │ │ ├── Preprocessing │ │ │ ├── Colour │ │ │ │ ├── Data │ │ │ │ │ ├── AlternatingMonoPattern.cs │ │ │ │ │ ├── MonoStreak.cs │ │ │ │ │ └── RepeatingHitPatterns.cs │ │ │ │ ├── TaikoColourDifficultyPreprocessor.cs │ │ │ │ └── TaikoDifficultyHitObjectColour.cs │ │ │ ├── Rhythm │ │ │ │ └── TaikoDifficultyHitObjectRhythm.cs │ │ │ └── TaikoDifficultyHitObject.cs │ │ ├── Scoring │ │ │ └── TaikoHitWindows.cs │ │ ├── Skills │ │ │ ├── Colour.cs │ │ │ ├── Peaks.cs │ │ │ ├── Rhythm.cs │ │ │ └── Stamina.cs │ │ ├── TaikoDifficultyAttributes.cs │ │ └── TaikoDifficultyCalculator.cs │ └── Utils │ │ ├── LimitedCapacityQueue.cs │ │ └── LimitedCapacityStack.cs └── Statics │ ├── EventStatic.cs │ ├── ParserStatic.cs │ ├── PathStatic.cs │ ├── SkinStatic.cs │ └── Timestamp.cs ├── Program.cs ├── Rendering ├── BeatmapInfoRenderer.cs ├── ChartRenderer.cs ├── ChecksRenderer.cs ├── DocumentationRenderer.cs ├── ExceptionRenderer.cs ├── Objects │ ├── Chart.cs │ ├── JSChart.cs │ ├── JSLineChart.cs │ ├── LineChart.cs │ ├── Point.cs │ └── Series.cs ├── OverlayRenderer.cs ├── OverviewRenderer.cs ├── Renderer.cs ├── SkillChartRenderer.cs ├── SnapshotsRenderer.cs └── TimelineRenderer.cs ├── Server ├── Host.cs ├── SignalHub.cs ├── State.cs └── Worker.cs ├── Snapshots ├── MapsetSnapshotter.csproj ├── Objects │ ├── DiffInstance.cs │ └── DiffTranslator.cs ├── Snapshotter.cs ├── TranslatorRegistry.cs └── Translators │ ├── ColoursTranslator.cs │ ├── DifficultyTranslator.cs │ ├── EditorTranslator.cs │ ├── EventsTranslator.cs │ ├── FilesTranslator.cs │ ├── GeneralTranslator.cs │ ├── HitObjectsTranslator.cs │ ├── MetadataTranslator.cs │ └── TimingTranslator.cs └── Track.cs /.gitignore: -------------------------------------------------------------------------------- 1 | # `api` simply contains the built `src`. 2 | app/api 3 | 4 | # `dist` is the local electron-builder product. 5 | dist 6 | 7 | # Dependencies are generated from `package.lock` files. 8 | **/node_modules/ 9 | 10 | # Temporary files 11 | *.swp 12 | *~ 13 | .DS_Store 14 | *.pyc 15 | 16 | # IDEs 17 | .vscode/ 18 | .idea/ 19 | .vs/ 20 | .fleet/ 21 | .cr/ 22 | 23 | # User-specific files 24 | *.suo 25 | *.user 26 | *.userosscache 27 | *.sln.docstates 28 | 29 | # Build results 30 | src/[Dd]ebug/ 31 | src/[Dd]ebugPublic/ 32 | src/[Rr]elease/ 33 | src/[Rr]eleases/ 34 | src/x64/ 35 | src/x86/ 36 | src/build/ 37 | src/bld/ 38 | src/[Bb]in/ 39 | src/[Oo]bj/ 40 | src/[Oo]ut/ 41 | src/msbuild.log 42 | src/msbuild.err 43 | src/msbuild.wrn -------------------------------------------------------------------------------- /app/assets/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/64x64.png -------------------------------------------------------------------------------- /app/assets/circle-check-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-check-icon.png -------------------------------------------------------------------------------- /app/assets/circle-cross-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-cross-icon.png -------------------------------------------------------------------------------- /app/assets/circle-error-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-error-icon.png -------------------------------------------------------------------------------- /app/assets/circle-exclamation-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-exclamation-icon.png -------------------------------------------------------------------------------- /app/assets/circle-gear-blue-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-gear-blue-icon.png -------------------------------------------------------------------------------- /app/assets/circle-gear-gray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-gear-gray-icon.png -------------------------------------------------------------------------------- /app/assets/circle-info-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-info-icon.png -------------------------------------------------------------------------------- /app/assets/circle-minor-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-minor-icon.png -------------------------------------------------------------------------------- /app/assets/circle-minus-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-minus-icon.png -------------------------------------------------------------------------------- /app/assets/circle-plus-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/circle-plus-icon.png -------------------------------------------------------------------------------- /app/assets/close-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/close-icon.png -------------------------------------------------------------------------------- /app/assets/cogwheel-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/cogwheel-icon.png -------------------------------------------------------------------------------- /app/assets/discussion-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/discussion-icon.png -------------------------------------------------------------------------------- /app/assets/discussion-unavailable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/discussion-unavailable-icon.png -------------------------------------------------------------------------------- /app/assets/doc-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/doc-icon.png -------------------------------------------------------------------------------- /app/assets/folder-external-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/folder-external-icon.png -------------------------------------------------------------------------------- /app/assets/folder-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/folder-icon.png -------------------------------------------------------------------------------- /app/assets/fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/fonts/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /app/assets/link-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/link-icon.png -------------------------------------------------------------------------------- /app/assets/link-unavailable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/link-unavailable-icon.png -------------------------------------------------------------------------------- /app/assets/lock-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/lock-icon.png -------------------------------------------------------------------------------- /app/assets/maximize-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/maximize-icon.png -------------------------------------------------------------------------------- /app/assets/minimize-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/minimize-icon.png -------------------------------------------------------------------------------- /app/assets/question-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/question-icon.png -------------------------------------------------------------------------------- /app/assets/reload-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/reload-icon.png -------------------------------------------------------------------------------- /app/assets/reverse-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/reverse-arrow.png -------------------------------------------------------------------------------- /app/assets/side-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/side-arrow.png -------------------------------------------------------------------------------- /app/assets/triangles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/triangles.png -------------------------------------------------------------------------------- /app/assets/unlock-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/unlock-icon.png -------------------------------------------------------------------------------- /app/assets/web-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/web-icon.png -------------------------------------------------------------------------------- /app/assets/web-unavailable-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/assets/web-unavailable-icon.png -------------------------------------------------------------------------------- /app/base.js: -------------------------------------------------------------------------------- 1 | // Contains the very core of the application, like window functions and how links should behave 2 | 3 | const {remote, shell} = require("electron"); 4 | window.$ = window.jQuery = require('jquery'); 5 | const utils = require('./utils.js'); 6 | 7 | const appdataPath = 8 | process.env.APPDATA || 9 | (process.platform == 'darwin' ? 10 | process.env.HOME + 'Library/Preferences' : 11 | process.env.HOME + "/.local/share"); 12 | const settingsPath = appdataPath + "/Mapset Verifier Externals/settings.ini"; 13 | 14 | /// Adds functionality to the top buttons, like minimize, maximize, etc. 15 | document.getElementById("top-button-close").addEventListener("click", () => remote.getCurrentWindow().close()); 16 | document.getElementById("top-button-maximize").addEventListener("click", () => 17 | { 18 | const window = remote.getCurrentWindow(); 19 | window.isMaximized() ? window.unmaximize() : window.maximize(); 20 | 21 | utils.saveFile(settingsPath, "maximized", window.isMaximized() ? "1" : "0"); 22 | }); 23 | document.getElementById("top-button-minimize").addEventListener("click", () => remote.getCurrentWindow().minimize()); 24 | 25 | /// Opens links in the default browser rather than in the electron one. 26 | $(document).on('click', 'a[href^="http"]', function (event) 27 | { 28 | event.preventDefault(); 29 | shell.openExternal(this.href); 30 | }); 31 | 32 | /// Prevents the electron browser from opening separate tabs upon, for example, dropping images. 33 | document.ondrop = (e) => 34 | { 35 | e.preventDefault(); 36 | return false; 37 | }; 38 | 39 | /// Remembers whether the window was maximized or windowed 40 | $(() => 41 | { 42 | const maximized = utils.readFile(settingsPath, "maximized"); 43 | if(maximized == "1") 44 | remote.getCurrentWindow().maximize(); 45 | }); -------------------------------------------------------------------------------- /app/bass.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/bass.dll -------------------------------------------------------------------------------- /app/libbass.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/app/libbass.so -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapsetverifier", 3 | "version": "1.9.0", 4 | "author": "Naxess ", 5 | "main": "main.js", 6 | "dependencies": { 7 | "@aspnet/signalr": "^1.1.4", 8 | "chart.js": "^2.9.4", 9 | "electron-localshortcut": "^3.1.0", 10 | "electron-updater": "5.0.0", 11 | "jquery": "^3.3.1", 12 | "winreg": "^1.2.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/signalr.js: -------------------------------------------------------------------------------- 1 | // Contains the implementation of SignalR for communication with the server 2 | 3 | const signalR = require('@aspnet/signalr'); 4 | 5 | /// If a connection attempt fails, retry in this many seconds. 6 | const retryMiliseconds = 500; 7 | 8 | /// Creates the hub connection 9 | var connection = new signalR.HubConnectionBuilder() 10 | .withUrl("http://localhost:5000/mapsetverifier/signalr", 11 | { 12 | skipNegotiation: true, 13 | transport: signalR.HttpTransportType.WebSockets 14 | }) 15 | .configureLogging(signalR.LogLevel.Information) 16 | .build(); 17 | 18 | module.exports = 19 | { 20 | /// Starts a new connection 21 | connectAnew: function() 22 | { 23 | connection = new signalR.HubConnectionBuilder() 24 | .withUrl("http://localhost:5000/mapsetverifier/signalr", 25 | { 26 | skipNegotiation: true, 27 | transport: signalR.HttpTransportType.WebSockets 28 | }) 29 | .configureLogging(signalR.LogLevel.Information) 30 | .build(); 31 | 32 | start(); 33 | }, 34 | 35 | /// Sends a message to the server 36 | send: function(key, value) 37 | { 38 | log("Sending message with key \"" + key + "\", and value \"" + value + "\"."); 39 | 40 | connection.invoke("ClientMessage", key, value) 41 | .catch(err => 42 | { 43 | log("Failed sending message, retrying in 1 second; " + (err != null ? err.message : "No response from server.")); 44 | setTimeout(() => 45 | { 46 | module.exports.send(key, value); 47 | }, retryMiliseconds); 48 | }); 49 | }, 50 | 51 | requestDocumentation: function() 52 | { 53 | module.exports.send("RequestDocumentation", ""); 54 | }, 55 | 56 | requestOverlay: function(value) 57 | { 58 | module.exports.send("RequestOverlay", value); 59 | }, 60 | 61 | requestBeatmapset: function(value) 62 | { 63 | module.exports.send("RequestBeatmapset", value); 64 | } 65 | } 66 | 67 | /// Sends a console log with SignalR in front to mark the origin 68 | function log(message) 69 | { 70 | //console.log("SignalR | " + message); 71 | } 72 | 73 | /// Starts the connection and logs result, retries if failed 74 | function start() 75 | { 76 | log("Connecting"); 77 | connection.start() 78 | .then(() => 79 | { 80 | log("Connected"); 81 | module.exports.send("Connected", ""); 82 | 83 | document.dispatchEvent(new CustomEvent("SignalR Connected", 84 | { 85 | detail: 86 | { 87 | connection: connection 88 | } 89 | })); 90 | }) 91 | .catch(err => 92 | { 93 | log("Error connecting, retrying in 1 second; " + (err != null ? err.message : "No response from server.")); 94 | setTimeout(() => start(), retryMiliseconds); 95 | }); 96 | }; 97 | 98 | /// Attempts to reconnect upon the connection closing 99 | connection.onclose(async () => 100 | { 101 | await start(); 102 | }); 103 | 104 | /// Logs messages received from the server 105 | connection.on("ServerMessage", (key, value) => 106 | { 107 | log("Received message with key \"" + key + "\", and value \"" + value + "\"."); 108 | }); 109 | 110 | start(); -------------------------------------------------------------------------------- /app/tooltip.js: -------------------------------------------------------------------------------- 1 | var spawnTimeout; 2 | var isSpawningLink = false; 3 | var tooltipExists = false; 4 | 5 | /// Returns the HTML structure of a tooltip. 6 | function getTooltip(tooltip) 7 | { 8 | return "
"; 9 | } 10 | 11 | /// Creates a tooltip object at the element position. 12 | function spawnTooltip(element, isLink = false) 13 | { 14 | var tooltipData = ""; 15 | if(!isLink) 16 | tooltipData = $(element).data("tooltip"); 17 | else 18 | tooltipData = $(element).attr("href"); 19 | 20 | var tooltip = getTooltip(tooltipData); 21 | var tooltipElement = $(tooltip).appendTo($("#wrapper")); 22 | var tooltipArrow = $(tooltipElement).children(".tooltip-arrow"); 23 | 24 | var newTop = $(element).offset().top + $(element).height() - 10; 25 | var newLeft = $(element).offset().left + $(element).outerWidth() / 2 - $(tooltipElement).width() / 2; 26 | 27 | var offsetX = 0; 28 | var newRight = newLeft + $(tooltipElement).width(); 29 | 30 | var borderLeft = 24; 31 | var borderRight = $(window).width() - 24; 32 | 33 | if(newLeft < borderLeft) offsetX = borderLeft - newLeft; 34 | if(newRight > borderRight) offsetX = borderRight - newRight; 35 | 36 | var padding = 4; 37 | 38 | $(tooltipElement).css({ "top": newTop, "left": newLeft + offsetX - padding }); 39 | $(tooltipArrow) .css({ "top": 0, "left": $(tooltipElement).width() / 2 - offsetX + padding}); 40 | 41 | $(tooltipElement).hide(); 42 | tooltipExists = true; 43 | isSpawningLink = isLink; 44 | spawnTimeout = setTimeout(() => 45 | { 46 | if(isLink) 47 | $(tooltipElement).addClass("link"); 48 | $(tooltipElement).fadeIn(100).show(); 49 | }, 200); 50 | } 51 | 52 | /// Removes all tooltip objects. 53 | function despawnTooltip() 54 | { 55 | var tooltipElement = $(".tooltip"); 56 | 57 | if(tooltipElement) 58 | { 59 | $(tooltipElement).stop(true).fadeOut(100); 60 | setTimeout(function() 61 | { 62 | $(tooltipElement).remove(); 63 | }, 100); 64 | } 65 | } 66 | 67 | /// Initializes the events necessary to spawn and despawn tooltips. 68 | (function() 69 | { 70 | $("body").mousemove(function(e) 71 | { 72 | // If the user moves their mouse, remove existing tooltips and clear spawn countdowns. 73 | if(tooltipExists) 74 | despawnTooltip(); 75 | if(spawnTimeout && !isSpawningLink) 76 | clearTimeout(spawnTimeout); 77 | 78 | if($(e.target).closest(".no-tooltip").length == 0) 79 | { 80 | // If they moved it over an element, prepare to open a tooltip in case the cursor doesn't move again. 81 | if($(e.target).is("[data-tooltip]")) 82 | spawnTooltip($(e.target)); 83 | else if($(e.target).parents("[data-tooltip]").length > 0) 84 | spawnTooltip($(e.target).parents("[data-tooltip]")); 85 | else if($(e.target).is("a[href!=\"\"]")) 86 | spawnTooltip($(e.target), true); 87 | } 88 | }); 89 | })(); -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | // Contains general utility functions 2 | 3 | window.$ = window.jQuery = require('jquery'); 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const util = require('util'); 8 | 9 | const readdirAsync = util.promisify(fs.readdir); 10 | const statAsync = util.promisify(fs.stat); 11 | 12 | module.exports = 13 | { 14 | /// Decodes special HTML characters such as & into &. 15 | decodeHtml: function(specialchars) 16 | { 17 | var textArea = document.createElement('textarea'); 18 | textArea.innerHTML = specialchars; 19 | return textArea.value; 20 | }, 21 | 22 | /// Returns a list of folder and file paths. 23 | getDirectory: async function(folderPath, foldersOnly = false) 24 | { 25 | const basepath = path.join(folderPath); 26 | const files = await readdirAsync(basepath); 27 | const stats = await Promise.all( 28 | files.map((filename) => 29 | statAsync(path.join(basepath, filename)) 30 | .then((stat) => ({ filename, stat })) 31 | ) 32 | ); 33 | const sortedFiles = stats.sort((b, a) => 34 | a.stat.mtime.getTime() - b.stat.mtime.getTime() 35 | ).filter((stat) => 36 | stat.stat.isDirectory() || !foldersOnly 37 | ).map((stat) => stat.filename); 38 | 39 | return sortedFiles; 40 | }, 41 | 42 | /// Saves a value to a key in a file, creates one if it doesn't exist. 43 | saveFile: function(filePath, key, value) 44 | { 45 | var read = ""; 46 | if(fs.existsSync(filePath)) 47 | read = fs.readFileSync(filePath); 48 | 49 | var content = ""; 50 | var success = false; 51 | 52 | if(read != "") 53 | { 54 | var splits = (read + "").split("\r\n"); 55 | for(var i = 0; i < splits.length; ++i) 56 | { 57 | if(splits[i].startsWith(key + ": ")) 58 | { 59 | content += key + ": " + value + "\r\n"; 60 | success = true; 61 | } 62 | else if(splits[i].length > 0) 63 | content += splits[i] + "\r\n"; 64 | } 65 | } 66 | 67 | if(!success) 68 | content += key + ": " + value + "\r\n"; 69 | 70 | try 71 | { 72 | fs.writeFileSync(filePath, content, 'utf-8'); 73 | } 74 | catch (exception) 75 | { 76 | console.log(exception.message); 77 | } 78 | }, 79 | 80 | /// Reads a value from a key in a file, returns an empty string if none exists. 81 | readFile: function(filePath, key) 82 | { 83 | if(fs.existsSync(filePath)) 84 | { 85 | var read = fs.readFileSync(filePath); 86 | 87 | var splits = (read + "").split("\r\n"); 88 | for(var i = 0; i < splits.length; ++i) 89 | if(splits[i].startsWith(key + ": ")) 90 | return splits[i].substr(key.length + ": ".length); 91 | } 92 | return ""; 93 | }, 94 | 95 | /// Returns the substring between two substrings in a string. 96 | scrape: function(data, start, end) 97 | { 98 | return data.substring(data.indexOf(start) + start.length).substring(0, data.substring(data.indexOf(start) + start.length).indexOf(end)); 99 | }, 100 | 101 | /// Escapes the data for use in data tags, adding backslashes in front of quotes, etc. 102 | escape: function(data) 103 | { 104 | return (data + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0'); 105 | } 106 | }; 107 | 108 | -------------------------------------------------------------------------------- /bass.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/bass.dll -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naxesss/MapsetVerifier/6e48f1269b0a484490a0ddc75df23b3400037a3d/icon.ico -------------------------------------------------------------------------------- /local-dist.bat: -------------------------------------------------------------------------------- 1 | dotnet build src -c Release -r win-x86 -o app/api/win-x86 --self-contained 2 | dotnet build src -c Release -r linux-x64 -o app/api/linux-x64 --self-contained 3 | 4 | call npm install 5 | call npm run dist 6 | 7 | pause -------------------------------------------------------------------------------- /local-start.bat: -------------------------------------------------------------------------------- 1 | dotnet build src -c Release -r win-x86 -o app/api/win-x86 --self-contained 2 | dotnet build src -c Release -r linux-x64 -o app/api/linux-x64 --self-contained 3 | 4 | call npm start -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapsetverifier", 3 | "version": "1.0.0", 4 | "author": "Naxess ", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Naxesss/MapsetVerifier.git" 8 | }, 9 | "main": "./app/main.js", 10 | "devDependencies": { 11 | "electron": "^5.0.6", 12 | "electron-builder": "^22.10.5" 13 | }, 14 | "dependencies": { 15 | "@aspnet/signalr": "^1.1.4", 16 | "chart.js": "^2.9.4", 17 | "electron-localshortcut": "^3.1.0", 18 | "electron-updater": "5.0.0", 19 | "jquery": "^3.5.1", 20 | "winreg": "^1.2.4" 21 | }, 22 | "scripts": { 23 | "postinstall": "install-app-deps", 24 | "start": "npm install && electron ./app", 25 | "dist": "electron-builder --x64 -w", 26 | "dist-all": "electron-builder --ia32 --x64 -wl", 27 | "release": "build -wl" 28 | }, 29 | "build": { 30 | "appId": "mapsetverifier", 31 | "productName": "Mapset Verifier", 32 | "asar": false, 33 | "extraFiles": [ 34 | { 35 | "from": "./app/bass.dll", 36 | "to": "." 37 | }, 38 | "bass.dll", 39 | { 40 | "from": "./app/libbass.so", 41 | "to": "." 42 | }, 43 | "libbass.so" 44 | ], 45 | "publish": [ 46 | { 47 | "provider": "github" 48 | } 49 | ], 50 | "linux": { 51 | "target": "tar.gz", 52 | "files": [ 53 | "!**/win-x86/*" 54 | ] 55 | }, 56 | "mac": { 57 | "target": "dmg", 58 | "icon": "build/icon.icns" 59 | }, 60 | "win": { 61 | "target": [ 62 | { 63 | "target": "nsis", 64 | "arch": [ 65 | "x64", 66 | "ia32" 67 | ] 68 | } 69 | ], 70 | "icon": "build/icon.ico", 71 | "files": [ 72 | "!**/linux-x64/*" 73 | ] 74 | }, 75 | "nsis": { 76 | "uninstallDisplayName": "${productName} Uninstaller.exe", 77 | "oneClick": false, 78 | "perMachine": true, 79 | "runAfterFinish": true, 80 | "allowElevation": true, 81 | "allowToChangeInstallationDirectory": true, 82 | "artifactName": "${productName} Installer.exe", 83 | "installerIcon": "build/icon.ico", 84 | "installerHeaderIcon": "build/icon.ico" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Checks/AllModes/Compose/CheckAbnormalNodes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using MapsetVerifier.Parser.Objects.HitObjects; 8 | using MapsetVerifier.Parser.Statics; 9 | 10 | namespace MapsetVerifier.Checks.AllModes.Compose 11 | { 12 | [Check] 13 | public class CheckAbnormalNodes : BeatmapCheck 14 | { 15 | public override CheckMetadata GetMetadata() => 16 | new BeatmapCheckMetadata 17 | { 18 | Category = "Compose", 19 | Message = "Abnormal amount of slider nodes.", 20 | Author = "Naxess", 21 | 22 | Documentation = new Dictionary 23 | { 24 | { 25 | "Purpose", 26 | @" 27 | Preventing mappers from writing inappropriate or otherwise harmful messages using slider nodes. 28 | 29 | https://i.imgur.com/rlCoEtZ.png 30 | An example of text being written with slider nodes in a way which can easily be hidden offscreen. 31 | " 32 | }, 33 | { 34 | "Reasoning", 35 | @" 36 | The code of conduct applies to all aspects of the ranking process, including the beatmap content itself, 37 | whether that only be visible through the editor or in gameplay as well." 38 | } 39 | } 40 | }; 41 | 42 | public override Dictionary GetTemplates() => 43 | new() 44 | { 45 | { 46 | "Abnormal", 47 | new IssueTemplate(Issue.Level.Warning, "{0} Slider contains {1} nodes.", "timestamp - ", "amount").WithCause("A slider contains more nodes than 10 times the square root of its length in pixels.") 48 | } 49 | }; 50 | 51 | public override IEnumerable GetIssues(Beatmap beatmap) 52 | { 53 | foreach (var hitObject in beatmap.HitObjects) 54 | if (hitObject is Slider slider && slider.NodePositions.Count > 10 * Math.Sqrt(slider.PixelLength)) 55 | yield return new Issue(GetTemplate("Abnormal"), beatmap, Timestamp.Get(slider), slider.NodePositions.Count); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Compose/CheckDrainTime.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | using MapsetVerifier.Parser.Statics; 7 | 8 | namespace MapsetVerifier.Checks.AllModes.Compose 9 | { 10 | [Check] 11 | public class CheckDrainTime : BeatmapCheck 12 | { 13 | public override CheckMetadata GetMetadata() => 14 | new BeatmapCheckMetadata 15 | { 16 | Category = "Compose", 17 | Message = "Too short drain time.", 18 | Author = "Naxess", 19 | 20 | Documentation = new Dictionary 21 | { 22 | { 23 | "Purpose", 24 | @" 25 | Prevents beatmaps from being too short, for example 10 seconds long. 26 | 27 | https://i.imgur.com/uNDPeJI.png 28 | A beatmap with a total mp3 length of ~21 seconds. 29 | " 30 | }, 31 | { 32 | "Reasoning", 33 | @" 34 | Beatmaps this short do not offer a substantial enough gameplay experience for the standards of 35 | the ranked section." 36 | } 37 | } 38 | }; 39 | 40 | public override Dictionary GetTemplates() => 41 | new() 42 | { 43 | { 44 | "Problem", 45 | new IssueTemplate(Issue.Level.Problem, "Less than 30 seconds of drain time, currently {0}.", "drain time").WithCause("The time from the first object to the end of the last object, subtracting any time between two objects " + "where a break exists, is in total less than 30 seconds.") 46 | } 47 | }; 48 | 49 | public override IEnumerable GetIssues(Beatmap beatmap) 50 | { 51 | if (beatmap.GetDrainTime() >= 30 * 1000) 52 | yield break; 53 | 54 | yield return new Issue(GetTemplate("Problem"), beatmap, Timestamp.Get(beatmap.GetDrainTime())); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Compose/CheckInvisibleSlider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using MapsetVerifier.Parser.Objects.HitObjects; 8 | using MapsetVerifier.Parser.Statics; 9 | 10 | namespace MapsetVerifier.Checks.AllModes.Compose 11 | { 12 | [Check] 13 | public class CheckInvisibleSlider : BeatmapCheck 14 | { 15 | public override CheckMetadata GetMetadata() => 16 | new BeatmapCheckMetadata 17 | { 18 | Category = "Compose", 19 | Message = "Invisible sliders.", 20 | Author = "Naxess", 21 | 22 | Documentation = new Dictionary 23 | { 24 | { 25 | "Purpose", 26 | @" 27 | Preventing objects from being invisible. 28 | 29 | https://i.imgur.com/xJIwdbA.png 30 | A slider with no nodes; looks like a circle on the timeline but is invisible on the playfield. 31 | " 32 | }, 33 | { 34 | "Reasoning", 35 | @" 36 | Although often used in combination with a storyboard to make up for the invisiblity through sprites, there 37 | is no way to force the storyboard to appear, meaning players may play the map unaware that they should have 38 | enabled something for a fair gameplay experience." 39 | } 40 | } 41 | }; 42 | 43 | public override Dictionary GetTemplates() => 44 | new() 45 | { 46 | { 47 | "Zero Nodes", 48 | new IssueTemplate(Issue.Level.Problem, "{0} has no slider nodes.", "timestamp - ").WithCause("A slider has no nodes.") 49 | }, 50 | 51 | { 52 | "Negative Length", 53 | new IssueTemplate(Issue.Level.Problem, "{0} has negative pixel length.", "timestamp - ").WithCause("A slider has a negative pixel length.") 54 | } 55 | }; 56 | 57 | public override IEnumerable GetIssues(Beatmap beatmap) 58 | { 59 | foreach (var slider in beatmap.HitObjects.OfType()) 60 | if (slider.NodePositions.Count == 0) 61 | yield return new Issue(GetTemplate("Zero Nodes"), beatmap, Timestamp.Get(slider)); 62 | else if (slider.PixelLength < 0) 63 | yield return new Issue(GetTemplate("Negative Length"), beatmap, Timestamp.Get(slider)); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Events/CheckStoryHitSounds.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | using MapsetVerifier.Parser.Objects.Events; 7 | using MapsetVerifier.Parser.Statics; 8 | 9 | namespace MapsetVerifier.Checks.AllModes.Events 10 | { 11 | [Check] 12 | public class CheckStoryHitSounds : BeatmapSetCheck 13 | { 14 | public override CheckMetadata GetMetadata() => 15 | new BeatmapCheckMetadata 16 | { 17 | Modes = 18 | [ 19 | // Mania uses storyboarded hit sounding due to hit sounds playing individually for each column otherwise. 20 | Beatmap.Mode.Standard, 21 | Beatmap.Mode.Taiko, 22 | Beatmap.Mode.Catch 23 | ], 24 | Category = "Events", 25 | Message = "Storyboarded hit sounds.", 26 | Author = "Naxess", 27 | 28 | Documentation = new Dictionary 29 | { 30 | { 31 | "Purpose", 32 | @" 33 | Preventing storyboard sounds from replacing or becoming ambigious with any beatmap hit sounds." 34 | }, 35 | { 36 | "Reasoning", 37 | @" 38 | Storyboarded hit sounds always play at the same time regardless of however late or early the player clicks 39 | on an object, meaning they do not provide proper active hit object feedback, unlike regular hit sounds. This 40 | contradicts the purpose of hit sounds and is likely to be confusing for players if similar samples as the 41 | hit sounds are used. 42 | 43 | Mania is exempt from this due to multiple objects at the same point in time being possible, leading 44 | to regular hit sounding working poorly, for example amplifying the volume if concurrent objects have 45 | the same hit sounds. 46 | " 47 | } 48 | } 49 | }; 50 | 51 | public override Dictionary GetTemplates() => 52 | new() 53 | { 54 | { 55 | "Storyboarded Hit Sound", 56 | new IssueTemplate(Issue.Level.Warning, "{0} Storyboarded hit sound ({1}, {2}%) from {3} file.", "timestamp - ", "path", "volume", ".osu/.osb").WithCause("The .osu file or .osb file contains storyboarded hit sounds.") 57 | } 58 | }; 59 | 60 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 61 | { 62 | foreach (var beatmap in beatmapSet.Beatmaps) 63 | { 64 | foreach (var storyHitSound in beatmap.Samples) 65 | foreach (var issue in GetStoryHitSoundIssue(beatmap, storyHitSound, ".osu")) 66 | yield return issue; 67 | 68 | if (beatmapSet.Osb == null) 69 | continue; 70 | 71 | foreach (var storyHitSound in beatmapSet.Osb.samples) 72 | foreach (var issue in GetStoryHitSoundIssue(beatmap, storyHitSound, ".osb")) 73 | yield return issue; 74 | } 75 | } 76 | 77 | private IEnumerable GetStoryHitSoundIssue(Beatmap beatmap, Sample sample, string origin) 78 | { 79 | yield return new Issue(GetTemplate("Storyboarded Hit Sound"), beatmap, Timestamp.Get(sample.time), sample.path, sample.volume, origin); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Audio/CheckAudioInVideo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using TagLib; 8 | 9 | namespace MapsetVerifier.Checks.AllModes.General.Audio 10 | { 11 | [Check] 12 | public class CheckAudioInVideo : GeneralCheck 13 | { 14 | public override CheckMetadata GetMetadata() => 15 | new() 16 | { 17 | Category = "Audio", 18 | Message = "Audio channels in video.", 19 | Author = "Naxess", 20 | 21 | Documentation = new Dictionary 22 | { 23 | { 24 | "Purpose", 25 | @" 26 | Reducing the file size of videos." 27 | }, 28 | { 29 | "Reasoning", 30 | @" 31 | The audio track of videos will not play and usually take a similar amount of file size as any other audio file, 32 | so not removing the audio track means a noticeable amount of resources are wasted, even if the audio track is 33 | empty but still present." 34 | } 35 | } 36 | }; 37 | 38 | public override Dictionary GetTemplates() => 39 | new() 40 | { 41 | { 42 | "Audio", 43 | new IssueTemplate(Issue.Level.Problem, "\"{0}\"", "path").WithCause("An audio track is present in one of the video files.") 44 | }, 45 | 46 | { 47 | "Leaves Folder", 48 | new IssueTemplate(Issue.Level.Problem, "\"{0}\" leaves the current song folder, which shouldn't ever happen.", "path").WithCause("The file path of a video file starts with two dots.") 49 | }, 50 | 51 | { 52 | "Missing", 53 | new IssueTemplate(Issue.Level.Warning, "\"{0}\" is missing, so unable to check that. Make sure you've downloaded with video.", "path").WithCause("A video file referenced is not present.") 54 | }, 55 | 56 | { 57 | "Exception", 58 | new IssueTemplate(Issue.Level.Error, Common.FILE_EXCEPTION_MESSAGE, "path", "exception info").WithCause("An exception occurred trying to parse a video file.") 59 | } 60 | }; 61 | 62 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 63 | { 64 | foreach (var issue in Common.GetTagOsuIssues(beatmapSet, beatmap => beatmap.Videos.Count > 0 ? beatmap.Videos.Select(video => video.path) : null, GetTemplate, tagFile => 65 | { 66 | // Executes for each non-faulty video file used in one of the beatmaps in the set. 67 | var issues = new List(); 68 | 69 | if (tagFile.file.Properties.MediaTypes.HasFlag(MediaTypes.Video) && tagFile.file.Properties.AudioChannels > 0) 70 | issues.Add(new Issue(GetTemplate("Audio"), null, tagFile.templateArgs[0])); 71 | 72 | return issues; 73 | })) 74 | // Returns issues from both non-faulty and faulty files. 75 | yield return issue; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Audio/CheckCommonFinish.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | 7 | namespace MapsetVerifier.Checks.AllModes.General.Audio 8 | { 9 | [Check] 10 | public class CheckCommonFinish : GeneralCheck 11 | { 12 | public override CheckMetadata GetMetadata() => 13 | new BeatmapCheckMetadata 14 | { 15 | Modes = 16 | [ 17 | // This check would take on another meaning if applied to taiko, since there you basically map with hit sounds. 18 | Beatmap.Mode.Standard, 19 | Beatmap.Mode.Catch, 20 | Beatmap.Mode.Mania 21 | ], 22 | Category = "Audio", 23 | Message = "Frequent finish hit sounds.", 24 | Author = "Naxess", 25 | 26 | Documentation = new Dictionary 27 | { 28 | { 29 | "Purpose", 30 | @" 31 | Discouraging normal/soft finish samples from playing too often to the point where it gets obnoxious 32 | without custom hit sounds." 33 | }, 34 | { 35 | "Reasoning", 36 | @" 37 | Although possibly fine when using custom samples, this will still get very jarring if the player 38 | turns off custom hit sounds and the finishes are used as frequently as claps/whistles, for example." 39 | } 40 | } 41 | }; 42 | 43 | public override Dictionary GetTemplates() => 44 | new() 45 | { 46 | { 47 | "Warning Common", 48 | new IssueTemplate(Issue.Level.Warning, "\"{0}\" may be obnoxious without custom samples. Used most commonly in {1}.", "path", "[difficulty]").WithCause("The usage of non-drum finish hit sounds to drain time ratio in a map is 2 seconds or more.") 49 | }, 50 | 51 | { 52 | "Warning Timestamp", 53 | new IssueTemplate(Issue.Level.Warning, "\"{0}\" may be obnoxious without custom samples. Used most frequently leading up to {1}.", "path", "timestamp in [difficulty]").WithCause("Non-drum finish hit sounds are used frequently in a short timespan.") 54 | } 55 | }; 56 | 57 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 58 | { 59 | foreach (var hsFile in beatmapSet.HitSoundFiles) 60 | { 61 | var sample = new HitSample(hsFile); 62 | 63 | if (sample.Sampleset == HitSample.SamplesetType.Drum || sample.HitSound != HitObject.HitSounds.Finish || sample.HitSource != HitSample.HitSourceType.Edge) 64 | continue; 65 | 66 | Common.CollectHitSoundFrequency(beatmapSet, hsFile, 9, out var mostFrequentTimestamp, out var uses); 67 | 68 | if (mostFrequentTimestamp != null) 69 | { 70 | yield return new Issue(GetTemplate("Warning Timestamp"), null, hsFile, mostFrequentTimestamp); 71 | } 72 | else 73 | { 74 | var mapCommonlyUsedIn = Common.GetBeatmapCommonlyUsedIn(beatmapSet, uses, 3000); 75 | 76 | if (mapCommonlyUsedIn != null) 77 | yield return new Issue(GetTemplate("Warning Common"), null, hsFile, mapCommonlyUsedIn); 78 | } 79 | } 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Audio/CheckHitSoundLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using MapsetVerifier.Framework.Objects; 5 | using MapsetVerifier.Framework.Objects.Attributes; 6 | using MapsetVerifier.Framework.Objects.Metadata; 7 | using MapsetVerifier.Framework.Objects.Resources; 8 | using MapsetVerifier.Parser.Objects; 9 | 10 | namespace MapsetVerifier.Checks.AllModes.General.Audio 11 | { 12 | [Check] 13 | public class CheckHitSoundLength : GeneralCheck 14 | { 15 | public override CheckMetadata GetMetadata() => 16 | new() 17 | { 18 | Category = "Audio", 19 | Message = "Too short hit sounds.", 20 | Author = "Naxess", 21 | 22 | Documentation = new Dictionary 23 | { 24 | { 25 | "Purpose", 26 | @" 27 | Ensuring hit sounds play consistently across multiple sound cards." 28 | }, 29 | { 30 | "Reasoning", 31 | @" 32 | Hit sounds shorter than ~25 ms (depending on soundcard) can result in no sound being played in-game. 33 | This makes it equivalent to a completely silent hit sound for some, while not for others. Muted hit 34 | sounds are fine having 0 ms duration though, since they don't play audio anyway. 35 | 36 | https://i.imgur.com/y9Zmxp3.png 37 | The 2.95 ms long hit sound from the example, as shown in Audacity. 38 | " 39 | }, 40 | { 41 | "Example", 42 | @"""soft-hitnormal.wav"" is shorter than 25 ms (2.95 ms) in https://osu.ppy.sh/beatmapsets/1527, 43 | see 00:25:880 (1) - . Set music to 0% and effect to 100% for clarity." 44 | } 45 | } 46 | }; 47 | 48 | public override Dictionary GetTemplates() => 49 | new() 50 | { 51 | { 52 | "Length", 53 | new IssueTemplate(Issue.Level.Problem, "\"{0}\" is shorter than 25 ms ({1} ms).", "path", "length").WithCause("A hit sound file is shorter than 25 ms and longer than 0 ms.") 54 | }, 55 | 56 | { 57 | "Unable to check", 58 | new IssueTemplate(Issue.Level.Error, Common.FILE_EXCEPTION_MESSAGE, "path", "exception info").WithCause("There was an error parsing a hit sound file.") 59 | } 60 | }; 61 | 62 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 63 | { 64 | foreach (var hsFile in beatmapSet.HitSoundFiles) 65 | { 66 | var hsPath = Path.Combine(beatmapSet.SongPath, hsFile); 67 | 68 | double duration = 0; 69 | Exception exception = null; 70 | 71 | try 72 | { 73 | duration = AudioBASS.GetDuration(hsPath); 74 | } 75 | catch (Exception ex) 76 | { 77 | exception = ex; 78 | } 79 | 80 | if (exception == null) 81 | { 82 | // Greater than 0 since 44-byte muted hit sounds are fine. 83 | if (duration < 25 && duration > 0) 84 | yield return new Issue(GetTemplate("Length"), null, hsFile, $"{duration:0.##}"); 85 | } 86 | else 87 | { 88 | yield return new Issue(GetTemplate("Unable to check"), null, hsFile, Common.ExceptionTag(exception)); 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Audio/CheckMultipleAudio.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using MapsetVerifier.Parser.Statics; 8 | 9 | namespace MapsetVerifier.Checks.AllModes.General.Audio 10 | { 11 | [Check] 12 | public class CheckMultipleAudio : GeneralCheck 13 | { 14 | public override CheckMetadata GetMetadata() => 15 | new() 16 | { 17 | Category = "Audio", 18 | Message = "Multiple or missing audio files.", 19 | Author = "Naxess", 20 | 21 | Documentation = new Dictionary 22 | { 23 | { 24 | "Purpose", 25 | @" 26 | Ensuring that each beatmapset only contains one audio file." 27 | }, 28 | { 29 | "Reasoning", 30 | @" 31 | Although this works well in-game, the website preview, metadata, tags, etc are all relying on that each 32 | beatmapset is based around a single song. As such, having multiple songs in a single beatmapset is not 33 | supported properly. Each song will also need its own spread, so having each set of difficulties in its 34 | own beatmapset makes things more organized." 35 | } 36 | } 37 | }; 38 | 39 | public override Dictionary GetTemplates() => 40 | new() 41 | { 42 | { 43 | "Multiple", 44 | new IssueTemplate(Issue.Level.Problem, "{0}", "audio file : difficulties").WithCause("There is more than one audio file used between all difficulties.") 45 | }, 46 | 47 | { 48 | "Missing", 49 | new IssueTemplate(Issue.Level.Problem, "No audio file could be found.").WithCause("There is no audio file used in any difficulty.") 50 | } 51 | }; 52 | 53 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 54 | { 55 | if (beatmapSet.Beatmaps.All(beatmap => beatmap.GetAudioFilePath() == null)) 56 | { 57 | foreach (var beatmap in beatmapSet.Beatmaps) 58 | yield return new Issue(GetTemplate("Missing"), beatmap); 59 | } 60 | else 61 | { 62 | var issues = Common.GetInconsistencies(beatmapSet, beatmap => beatmap.GetAudioFilePath() != null ? PathStatic.RelativePath(beatmap.GetAudioFilePath(), beatmap.SongPath) : "None", GetTemplate("Multiple")); 63 | 64 | foreach (var issue in issues) 65 | yield return issue; 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Files/CheckZeroBytes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using MapsetVerifier.Framework.Objects; 5 | using MapsetVerifier.Framework.Objects.Attributes; 6 | using MapsetVerifier.Framework.Objects.Metadata; 7 | using MapsetVerifier.Parser.Objects; 8 | using MapsetVerifier.Parser.Statics; 9 | 10 | namespace MapsetVerifier.Checks.AllModes.General.Files 11 | { 12 | [Check] 13 | public class CheckZeroBytes : GeneralCheck 14 | { 15 | public override CheckMetadata GetMetadata() => 16 | new() 17 | { 18 | Category = "Files", 19 | Message = "0-byte files.", 20 | Author = "Naxess", 21 | 22 | Documentation = new Dictionary 23 | { 24 | { 25 | "Purpose", 26 | @" 27 | Ensuring all files can be uploaded properly during the submission process." 28 | }, 29 | { 30 | "Reasoning", 31 | @" 32 | 0-byte files prevent other files in the song folder from properly uploading. Mappers sometimes attempt to silence 33 | certain hit sound files by completely removing its audio data, but this often results in a file completely devoid 34 | of any data which makes it 0-byte. Instead, use this 44-byte file 35 | which osu provides to mute hit sound files. 36 | 37 | https://i.imgur.com/Qb9z95T.png 38 | A 0-byte slidertick hit sound file. 39 | " 40 | } 41 | } 42 | }; 43 | 44 | public override Dictionary GetTemplates() => 45 | new() 46 | { 47 | { 48 | "0-byte", 49 | new IssueTemplate(Issue.Level.Problem, "\"{0}\"", "path").WithCause("A file in the song folder contains no data; consists of 0 bytes.") 50 | }, 51 | 52 | { 53 | "Exception", 54 | new IssueTemplate(Issue.Level.Error, Common.FILE_EXCEPTION_MESSAGE, "path", "exception info").WithCause("A file which was attempted to be checked could not be opened.") 55 | } 56 | }; 57 | 58 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 59 | { 60 | foreach (var filePath in beatmapSet.SongFilePaths) 61 | { 62 | Issue errorIssue = null; 63 | FileInfo file = null; 64 | 65 | try 66 | { 67 | file = new FileInfo(filePath); 68 | } 69 | catch (Exception exception) 70 | { 71 | errorIssue = new Issue(GetTemplate("Exception"), null, PathStatic.RelativePath(filePath, beatmapSet.SongPath), Common.ExceptionTag(exception)); 72 | } 73 | 74 | if (errorIssue != null) 75 | { 76 | yield return errorIssue; 77 | 78 | continue; 79 | } 80 | 81 | if (file.Length == 0) 82 | yield return new Issue(GetTemplate("0-byte"), null, PathStatic.RelativePath(filePath, beatmapSet.SongPath)); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Resources/CheckBgPresence.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using MapsetVerifier.Framework.Objects; 5 | using MapsetVerifier.Framework.Objects.Attributes; 6 | using MapsetVerifier.Framework.Objects.Metadata; 7 | using MapsetVerifier.Parser.Objects; 8 | 9 | namespace MapsetVerifier.Checks.AllModes.General.Resources 10 | { 11 | [Check] 12 | public class CheckBgPresence : GeneralCheck 13 | { 14 | public override CheckMetadata GetMetadata() => 15 | new() 16 | { 17 | Category = "Resources", 18 | Message = "Missing background.", 19 | Author = "Naxess", 20 | 21 | Documentation = new Dictionary 22 | { 23 | { 24 | "Purpose", 25 | @" 26 | Ensuring that each beatmap in a beatmapset has a background image present. 27 | 28 | https://i.imgur.com/P9TdA7K.jpg 29 | An example of a default non-seasonal background as shown in the editor. 30 | " 31 | }, 32 | { 33 | "Reasoning", 34 | @" 35 | Backgrounds help players recognize the beatmap, and the absence of one makes it look incomplete." 36 | } 37 | } 38 | }; 39 | 40 | public override Dictionary GetTemplates() => 41 | new() 42 | { 43 | { 44 | "All", 45 | new IssueTemplate(Issue.Level.Problem, "All difficulties are missing backgrounds.").WithCause("None of the difficulties have a background present.") 46 | }, 47 | 48 | { 49 | "One", 50 | new IssueTemplate(Issue.Level.Problem, "{0} has no background.", "difficulty").WithCause("One or more difficulties are missing backgrounds, but not all.") 51 | }, 52 | 53 | { 54 | "Missing", 55 | new IssueTemplate(Issue.Level.Problem, "{0} is missing its background file, \"{1}\".", "difficulty", "path").WithCause("A background file path is present, but no file exists where it is pointing.") 56 | } 57 | }; 58 | 59 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 60 | { 61 | if (beatmapSet.Beatmaps.All(beatmap => beatmap.Backgrounds.Count == 0)) 62 | { 63 | yield return new Issue(GetTemplate("All"), null); 64 | 65 | yield break; 66 | } 67 | 68 | foreach (var beatmap in beatmapSet.Beatmaps) 69 | { 70 | if (beatmap.Backgrounds.Count == 0) 71 | { 72 | yield return new Issue(GetTemplate("One"), null, beatmap.MetadataSettings.version); 73 | 74 | continue; 75 | } 76 | 77 | if (beatmapSet.SongPath == null) 78 | continue; 79 | 80 | foreach (var bg in beatmap.Backgrounds) 81 | { 82 | var path = beatmapSet.SongPath + Path.DirectorySeparatorChar + bg.path; 83 | 84 | if (!File.Exists(path)) 85 | yield return new Issue(GetTemplate("Missing"), null, beatmap.MetadataSettings.version, bg.path); 86 | } 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Resources/CheckOverlayLayer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | using MapsetVerifier.Parser.Objects.Events; 7 | 8 | namespace MapsetVerifier.Checks.AllModes.General.Resources 9 | { 10 | [Check] 11 | public class CheckOverlayLayer : GeneralCheck 12 | { 13 | public override CheckMetadata GetMetadata() => 14 | new() 15 | { 16 | Category = "Resources", 17 | Message = "Overlay layer usage.", 18 | Author = "Naxess", 19 | 20 | Documentation = new Dictionary 21 | { 22 | { 23 | "Purpose", 24 | @" 25 | Preventing storyboard elements from blocking the view of objects to the point where they become unnecessarily 26 | difficult, or even impossible, to read. 27 | 28 | https://i.imgur.com/rVZpeso.png 29 | A storyboard element appearing over a slider. 30 | " 31 | }, 32 | { 33 | "Reasoning", 34 | @" 35 | This layer appears over the object layer, meaning it can obscure objects. This is potentially very dangerous if not paid 36 | attention to, as it can screw over the experience of the map really badly if executed poorly. 37 |

38 | As a rough guideline, ensure that the position of aimed objects are clear and that indicators are identifiable (so for 39 | example when to start spinning or that a slider reverses). Basically don't cover up important gameplay elements unless 40 | otherwise clear." 41 | } 42 | } 43 | }; 44 | 45 | public override Dictionary GetTemplates() => 46 | new() 47 | { 48 | { 49 | "Warning", 50 | new IssueTemplate(Issue.Level.Warning, "\"{0}\" Check the {1} to see where it appears.", "file name", ".osu/.osb").WithCause("A storyboard sprite or animation is using the overlay layer.") 51 | } 52 | }; 53 | 54 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 55 | { 56 | // Checks .osu-specific storyboard elements. 57 | foreach (var beatmap in beatmapSet.Beatmaps) 58 | foreach (var sprite in beatmap.Sprites) 59 | if (sprite.layer == Sprite.Layer.Overlay) 60 | yield return new Issue(GetTemplate("Warning"), beatmap, sprite.path, ".osu"); 61 | 62 | foreach (var beatmap in beatmapSet.Beatmaps) 63 | foreach (var animation in beatmap.Animations) 64 | if (animation.layer == Sprite.Layer.Overlay) 65 | yield return new Issue(GetTemplate("Warning"), beatmap, animation.path, ".osu"); 66 | 67 | // Checks .osb storyboard elements. 68 | if (beatmapSet.Osb == null) 69 | yield break; 70 | 71 | foreach (var sprite in beatmapSet.Osb.sprites) 72 | if (sprite.layer == Sprite.Layer.Overlay) 73 | yield return new Issue(GetTemplate("Warning"), null, sprite.path, ".osb"); 74 | 75 | foreach (var animation in beatmapSet.Osb.animations) 76 | if (animation.layer == Sprite.Layer.Overlay) 77 | yield return new Issue(GetTemplate("Warning"), null, animation.path, ".osb"); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/General/Resources/CheckVideoOffset.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | 7 | namespace MapsetVerifier.Checks.AllModes.General.Resources 8 | { 9 | [Check] 10 | public class CheckVideoOffset : GeneralCheck 11 | { 12 | public override CheckMetadata GetMetadata() => 13 | new() 14 | { 15 | Category = "Resources", 16 | Message = "Inconsistent video offset.", 17 | Author = "Naxess", 18 | 19 | Documentation = new Dictionary 20 | { 21 | { 22 | "Purpose", 23 | @" 24 | Ensuring that the video aligns with the song consistently for all difficulties. 25 | 26 | https://i.imgur.com/RDRL3qG.png 27 | Two difficulties with different video offsets, as shown in the respective .osu files. The second 28 | argument, after ""Video"", is the offset in ms. 29 | " 30 | }, 31 | { 32 | "Reasoning", 33 | @" 34 | Since many videos tend to match the music in some way, for example do transitions on downbeats, it wouldn't 35 | make much sense having difficulty-dependent video offsets, as all difficulties are based around the same song 36 | starting at the same point in time." 37 | } 38 | } 39 | }; 40 | 41 | public override Dictionary GetTemplates() => 42 | new() 43 | { 44 | { 45 | "Multiple", 46 | new IssueTemplate(Issue.Level.Problem, "{0}", "video offset : difficulties").WithCause("There is more than one video offset used between all difficulties.") 47 | } 48 | }; 49 | 50 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 51 | { 52 | foreach (var issue in Common.GetInconsistencies(beatmapSet, beatmap => beatmap.Videos.Count > 0 ? beatmap.Videos[0].offset.ToString() : null, GetTemplate("Multiple"))) 53 | yield return issue; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Settings/CheckDefaultColours.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | 8 | namespace MapsetVerifier.Checks.AllModes.Settings 9 | { 10 | [Check] 11 | public class CheckDefaultColours : BeatmapCheck 12 | { 13 | public override CheckMetadata GetMetadata() => 14 | new BeatmapCheckMetadata 15 | { 16 | Modes = 17 | [ 18 | // Does not apply to taiko, due to always using red/blue. 19 | // Does not apply to mania, due to not having combo colours (based on column instead). 20 | Beatmap.Mode.Standard, 21 | Beatmap.Mode.Catch 22 | ], 23 | Category = "Settings", 24 | Message = "Default combo colours without forced skin.", 25 | Author = "Naxess", 26 | 27 | Documentation = new Dictionary 28 | { 29 | { 30 | "Purpose", 31 | @" 32 | Preventing the combo colours chosen without additional input from blending into the background. 33 | 34 | https://i.imgur.com/G5vTU7f.png 35 | The combo colour section in song setup without custom colours ticked. 36 | " 37 | }, 38 | { 39 | "Reasoning", 40 | @" 41 | If you leave the combo colour setting as it is when you create a beatmap, no [Colours] section will 42 | be created in the .osu, meaning the skins of users will override them. Since we can't control which 43 | colours they may use or force them to dim the background, the colours may blend into the background 44 | making for an unfair gameplay experience. 45 |

46 | If you set a preferred skin in the beatmap however, for example default, that skin will be used over 47 | any user skin, but many players switch skins to get away from default, so would not recommend this. 48 | If you want the default colours, simply tick the ""Enable Custom Colours"" checkbox instead." 49 | } 50 | } 51 | }; 52 | 53 | public override Dictionary GetTemplates() => 54 | new() 55 | { 56 | { 57 | "Default", 58 | new IssueTemplate(Issue.Level.Problem, "Default combo colours without preferred skin.").WithCause("A beatmap has no custom combo colours and does not have any preferred skin.") 59 | } 60 | }; 61 | 62 | public override IEnumerable GetIssues(Beatmap beatmap) 63 | { 64 | if (beatmap.GeneralSettings.skinPreference != "Default" && !beatmap.ColourSettings.combos.Any()) 65 | yield return new Issue(GetTemplate("Default"), beatmap); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Settings/CheckTickRate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using MathNet.Numerics; 8 | 9 | namespace MapsetVerifier.Checks.AllModes.Settings 10 | { 11 | [Check] 12 | public class CheckTickRate : BeatmapCheck 13 | { 14 | public override CheckMetadata GetMetadata() => 15 | new BeatmapCheckMetadata 16 | { 17 | Category = "Settings", 18 | Message = "Slider tick rates not aligning with any common beat snap divisor.", 19 | Author = "Naxess", 20 | 21 | Documentation = new Dictionary 22 | { 23 | { 24 | "Purpose", 25 | @" 26 | Ensuring that slider ticks align with the song's beat structure. 27 | 28 | https://i.imgur.com/2NVm2aB.png 29 | A 1/1 slider with an asymmetric tick rate (neither a tick in the middle nor two equally distanced from it). 30 | " 31 | }, 32 | { 33 | "Reasoning", 34 | @" 35 | Slider ticks, just like any other object, should align with the song in some way. If slider ticks are going after a 1/5 beat 36 | structure, for instance, that's either extremely rare or much more likely a mistake." 37 | } 38 | } 39 | }; 40 | 41 | public override Dictionary GetTemplates() => 42 | new() 43 | { 44 | { 45 | "Tick Rate", 46 | new IssueTemplate(Issue.Level.Problem, "{0} {1}.", "setting", "value").WithCause("The slider tick rate setting of a beatmap is using an incorrect or otherwise extremely uncommon divisor." + "Common tick rates include any full integer as well as 1/2, 4/3, and 3/2. Excludes precision errors.") 47 | } 48 | }; 49 | 50 | public override IEnumerable GetIssues(Beatmap beatmap) 51 | { 52 | var issue = GetTickRateIssue(beatmap.DifficultySettings.sliderTickRate, "slider tick rate", beatmap); 53 | 54 | if (issue != null) 55 | yield return issue; 56 | } 57 | 58 | /// 59 | /// Returns an issue when the given tick rate does not align with any integer value, 1/2, 3/2 or 4/3. 60 | /// Rounds the value to the closest 1/100th to avoid precision errors. 61 | /// 62 | private Issue GetTickRateIssue(float tickRate, string type, Beatmap beatmap) 63 | { 64 | var approxTickRate = Math.Round(tickRate * 1000) / 1000; 65 | 66 | if (tickRate - Math.Floor(tickRate) != 0 && !approxTickRate.AlmostEqual(0.5) && !approxTickRate.AlmostEqual(1.333) && !approxTickRate.AlmostEqual(1.5)) 67 | return new Issue(GetTemplate("Tick Rate"), beatmap, approxTickRate, type); 68 | 69 | return null; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Timing/CheckFirstLine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | using MapsetVerifier.Parser.Statics; 7 | 8 | namespace MapsetVerifier.Checks.AllModes.Timing 9 | { 10 | [Check] 11 | public class CheckFirstLine : BeatmapCheck 12 | { 13 | public override CheckMetadata GetMetadata() => 14 | new BeatmapCheckMetadata 15 | { 16 | Category = "Timing", 17 | Message = "First line toggles kiai or is inherited.", 18 | Author = "Naxess", 19 | 20 | Documentation = new Dictionary 21 | { 22 | { 23 | "Purpose", 24 | @" 25 | Preventing effects from happening and inherited lines before the first uninherited line." 26 | }, 27 | { 28 | "Reasoning", 29 | @" 30 | If you toggle kiai on the first line, then when the player starts the beatmap, kiai will instantly trigger and apply 31 | from the beginning until the next line. 32 | 33 | https://i.imgur.com/9F3LoR3.png 34 | The game preventing you from enabling kiai on the first timing line. 35 | 36 | 37 | If you place an inherited line before the first uninherited line, then the game will 38 | think the whole section isn't timed, causing the default bpm to be used and the inherited line to malfunction since 39 | it has nothing to inherit. 40 | 41 | https://i.imgur.com/yqSEObl.png 42 | The first line being inherited, as seen from the timing view. 43 | " 44 | } 45 | } 46 | }; 47 | 48 | public override Dictionary GetTemplates() => 49 | new() 50 | { 51 | { 52 | "Inherited", 53 | new IssueTemplate(Issue.Level.Problem, "{0} First timing line is inherited.", "timestamp - ").WithCause("The first timing line of a beatmap is inherited.") 54 | }, 55 | 56 | { 57 | "Toggles Kiai", 58 | new IssueTemplate(Issue.Level.Problem, "{0} First timing line toggles kiai.", "timestamp - ").WithCause("The first timing line of a beatmap has kiai enabled.") 59 | }, 60 | 61 | { 62 | "No Lines", 63 | new IssueTemplate(Issue.Level.Problem, "There are no timing lines.").WithCause("A beatmap has no timing lines.") 64 | } 65 | }; 66 | 67 | public override IEnumerable GetIssues(Beatmap beatmap) 68 | { 69 | if (beatmap.TimingLines.Count == 0) 70 | { 71 | yield return new Issue(GetTemplate("No Lines"), beatmap); 72 | 73 | yield break; 74 | } 75 | 76 | var line = beatmap.TimingLines[0]; 77 | 78 | if (!line.Uninherited) 79 | yield return new Issue(GetTemplate("Inherited"), beatmap, Timestamp.Get(line.Offset)); 80 | else if (line.Kiai) 81 | yield return new Issue(GetTemplate("Toggles Kiai"), beatmap, Timestamp.Get(line.Offset)); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/Checks/AllModes/Timing/CheckPreview.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | 7 | namespace MapsetVerifier.Checks.AllModes.Timing 8 | { 9 | [Check] 10 | public class CheckPreview : BeatmapSetCheck 11 | { 12 | public override CheckMetadata GetMetadata() => 13 | new BeatmapCheckMetadata 14 | { 15 | Category = "Timing", 16 | Message = "Inconsistent or unset preview time.", 17 | Author = "Naxess", 18 | 19 | Documentation = new Dictionary 20 | { 21 | { 22 | "Purpose", 23 | @" 24 | Ensuring that preview times are set and consistent for all beatmaps in the set." 25 | }, 26 | { 27 | "Reasoning", 28 | @" 29 | Without a set preview time the game will automatically pick a point to use as preview, but this rarely aligns with 30 | any beat or start of measure in the song. Additionally, not selecting a preview point will cause the web to use the 31 | whole song as preview, rather than the usual 10 second limit. Which difficulty is used to take preview time from is 32 | also not necessarily consistent between the web and the client. 33 |

34 | Similarly to metadata and timing, preview points should really just be a global setting for the whole beatmapset and 35 | not difficulty-specific." 36 | } 37 | } 38 | }; 39 | 40 | public override Dictionary GetTemplates() => 41 | new() 42 | { 43 | { 44 | "Not Set", 45 | new IssueTemplate(Issue.Level.Problem, "Preview time is not set.").WithCause("The preview time of a beatmap is missing.") 46 | }, 47 | 48 | { 49 | "Inconsistent", 50 | new IssueTemplate(Issue.Level.Problem, "Preview time is inconsistent, see {0}.", "difficulty").WithCause("The preview time of a beatmap is different from the reference beatmap.") 51 | } 52 | }; 53 | 54 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 55 | { 56 | var refBeatmap = beatmapSet.Beatmaps[0]; 57 | 58 | foreach (var beatmap in beatmapSet.Beatmaps) 59 | // Here we do care if the floats differ. It should be exactly -1. Anything else is treated as an actual offset. 60 | // ReSharper disable twice CompareOfFloatsByEqualityOperator 61 | if (beatmap.GeneralSettings.previewTime == -1) 62 | yield return new Issue(GetTemplate("Not Set"), beatmap); 63 | 64 | else if (beatmap.GeneralSettings.previewTime != refBeatmap.GeneralSettings.previewTime) 65 | yield return new Issue(GetTemplate("Inconsistent"), beatmap, refBeatmap); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/Checks/Examples/CheckExample.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Metadata; 4 | using MapsetVerifier.Parser.Objects; 5 | 6 | namespace MapsetVerifier.Checks.Examples 7 | { 8 | // This attribute tells the framework that it's a check it should register. 9 | // Since this is just an example class, we're not going to register this. 10 | // [Check] 11 | public class CheckExample : BeatmapCheck 12 | { 13 | /// 14 | /// Determines which modes the check shows for, in which category the check appears, the message for the check, 15 | /// etc. 16 | /// 17 | public override CheckMetadata GetMetadata() => 18 | new BeatmapCheckMetadata 19 | { 20 | Modes = 21 | [ 22 | Beatmap.Mode.Standard, 23 | Beatmap.Mode.Catch 24 | ], 25 | Difficulties = 26 | [ 27 | Beatmap.Difficulty.Easy, 28 | Beatmap.Difficulty.Normal, 29 | Beatmap.Difficulty.Hard 30 | ], 31 | Category = "Example", 32 | Message = "Difficulty name is present in the beatmap.", 33 | Author = "Naxess", 34 | Documentation = new Dictionary 35 | { 36 | { "Purpose", "Show an example of a custom check." }, 37 | { "Reasoning", "Examples teach through practice." } 38 | } 39 | }; 40 | 41 | /// 42 | /// Returns a dictionary of issue templates, which determine how each sub-issue is formatted, the issue level, 43 | /// etc. 44 | /// 45 | public override Dictionary GetTemplates() => 46 | new() 47 | { 48 | { 49 | "DiffName", 50 | new IssueTemplate(Issue.Level.Warning, "The difficulty name is {0}.", "difficulty name") 51 | } 52 | }; 53 | 54 | public override IEnumerable GetIssues(Beatmap beatmap) 55 | { 56 | yield return new Issue(GetTemplate("DiffName"), beatmap, beatmap.MetadataSettings.version); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Checks/Examples/GeneralCheckExample.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Metadata; 4 | using MapsetVerifier.Parser.Objects; 5 | 6 | namespace MapsetVerifier.Checks.Examples 7 | { 8 | // This attribute tells the framework that it's a check it should register. 9 | // Since this is just an example class, we're not going to register this. 10 | // [Check] 11 | public class GeneralCheckExample : GeneralCheck 12 | { 13 | /// 14 | /// Determines which modes the check shows for, in which category the check appears, the message for the check, 15 | /// etc. 16 | /// 17 | public override CheckMetadata GetMetadata() => 18 | new() 19 | { 20 | Category = "Example", 21 | Message = "Difficulty names are present in the beatmap.", 22 | Author = "Naxess", 23 | Documentation = new Dictionary 24 | { 25 | { "Purpose", "Show an example of a custom general check." }, 26 | { "Reasoning", "Examples teach through practice." } 27 | } 28 | }; 29 | 30 | /// 31 | /// Returns a dictionary of issue templates, which determine how each sub-issue is formatted, the issue level, 32 | /// etc. 33 | /// 34 | public override Dictionary GetTemplates() => 35 | new() 36 | { 37 | { 38 | "DiffName", 39 | new IssueTemplate(Issue.Level.Warning, "One of the difficulty names is {0}.", "difficulty name") 40 | } 41 | }; 42 | 43 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 44 | { 45 | foreach (var beatmap in beatmapSet.Beatmaps) 46 | yield return new Issue(GetTemplate("DiffName"), null, beatmap.MetadataSettings.version); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Checks/MapsetChecks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | full 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ..\MapsetParser\bin\Debug\netcoreapp3.1\MapsetParser.dll 26 | 27 | 28 | ..\MapsetVerifierFramework\bin\Debug\netcoreapp3.1\MapsetVerifierFramework.dll 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Checks/Standard/Compose/CheckNinjaSpinner.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects; 3 | using MapsetVerifier.Framework.Objects.Attributes; 4 | using MapsetVerifier.Framework.Objects.Metadata; 5 | using MapsetVerifier.Parser.Objects; 6 | using MapsetVerifier.Parser.Objects.HitObjects; 7 | using MapsetVerifier.Parser.Statics; 8 | 9 | namespace MapsetVerifier.Checks.Standard.Compose 10 | { 11 | [Check] 12 | public class CheckNinjaSpinner : BeatmapCheck 13 | { 14 | public override CheckMetadata GetMetadata() => 15 | new BeatmapCheckMetadata 16 | { 17 | Modes = 18 | [ 19 | Beatmap.Mode.Standard 20 | ], 21 | Category = "Compose", 22 | Message = "Too short spinner.", 23 | Author = "Naxess", 24 | 25 | Documentation = new Dictionary 26 | { 27 | { 28 | "Purpose", 29 | @" 30 | Preventing spinners from being so short that you almost need to memorize them in order to react 31 | to them before they end." 32 | }, 33 | { 34 | "Reasoning", 35 | @" 36 | Players generally react much slower than auto, so if auto can't even acheive 1000 points on the 37 | spinner, players will likely not get any points at all, much less pass it without losing accuracy. 38 | In general, these are just not fun to play due to needing to memorize them for a fair experience." 39 | } 40 | } 41 | }; 42 | 43 | public override Dictionary GetTemplates() => 44 | new() 45 | { 46 | { 47 | "Problem", 48 | new IssueTemplate(Issue.Level.Problem, "{0} Spinner is too short, auto cannot achieve 1000 points on this.", "timestamp - ").WithCause("A spinner is predicted to, based on the OD and BPM, not be able to achieve 1000 points on this, and by a " + "margin to account for any inconsistencies.") 49 | }, 50 | 51 | { 52 | "Warning", 53 | new IssueTemplate(Issue.Level.Warning, "{0} Spinner may be too short, ensure auto can achieve 1000 points on this.", "timestamp - ").WithCause("Same as the other check, but without the margin, meaning the threshold is lower.") 54 | } 55 | }; 56 | 57 | public override IEnumerable GetIssues(Beatmap beatmap) 58 | { 59 | foreach (var hitObject in beatmap.HitObjects) 60 | { 61 | if (hitObject is not Spinner spinner) 62 | continue; 63 | 64 | double od = beatmap.DifficultySettings.overallDifficulty; 65 | 66 | var warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // anything above this works fine 67 | 68 | var problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // anything above this only works sometimes 69 | 70 | if (problemThreshold > spinner.endTime - spinner.time) 71 | yield return new Issue(GetTemplate("Problem"), beatmap, Timestamp.Get(spinner)); 72 | 73 | else if (warningThreshold > spinner.endTime - spinner.time) 74 | yield return new Issue(GetTemplate("Warning"), beatmap, Timestamp.Get(spinner)); 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/Checks/Standard/Spread/CheckShortSliders.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using MapsetVerifier.Parser.Objects.HitObjects; 8 | using MapsetVerifier.Parser.Statics; 9 | 10 | namespace MapsetVerifier.Checks.Standard.Spread 11 | { 12 | [Check] 13 | public class CheckShortSliders : BeatmapCheck 14 | { 15 | public override CheckMetadata GetMetadata() => 16 | new BeatmapCheckMetadata 17 | { 18 | Modes = 19 | [ 20 | Beatmap.Mode.Standard 21 | ], 22 | Difficulties = 23 | [ 24 | Beatmap.Difficulty.Easy 25 | ], 26 | Category = "Spread", 27 | Message = "Too short sliders.", 28 | Author = "Naxess", 29 | 30 | Documentation = new Dictionary 31 | { 32 | { 33 | "Purpose", 34 | @" 35 | Preventing slider head and tail from being too close in time for easy difficulties." 36 | }, 37 | { 38 | "Reasoning", 39 | @" 40 | Newer players need time to comprehend when to hold down and let go of sliders. If a slider ends too quickly, 41 | the action of pressing the slider and very shortly afterwards letting it go will sometimes be difficult to 42 | handle. The action of lifting a key is similar in difficulty to pressing a key for newer players. So any 43 | distance in time you wouldn't place circles apart, you shouldn't place slider head and tail apart either." 44 | } 45 | } 46 | }; 47 | 48 | public override Dictionary GetTemplates() => 49 | new() 50 | { 51 | { 52 | "Too Short", 53 | new IssueTemplate(Issue.Level.Warning, "{0} {1} ms, expected at least {2}.", "timestamp - ", "duration", "threshold").WithCause("A slider in an Easy difficulty is less than 125 ms (240 bpm 1/2).") 54 | } 55 | }; 56 | 57 | public override IEnumerable GetIssues(Beatmap beatmap) 58 | { 59 | // Shortest length before warning is 1/2 at 240 BPM, 125 ms. 60 | const double timeThreshold = 125; 61 | 62 | foreach (var slider in beatmap.HitObjects.OfType()) 63 | if (slider.EndTime - slider.time < timeThreshold) 64 | yield return new Issue(GetTemplate("Too Short"), beatmap, Timestamp.Get(slider), $"{slider.EndTime - slider.time:0.##}", timeThreshold); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Checks/Taiko/Timing/CheckInconsistentBarLines.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | using MapsetVerifier.Framework.Objects.Attributes; 5 | using MapsetVerifier.Framework.Objects.Metadata; 6 | using MapsetVerifier.Parser.Objects; 7 | using MapsetVerifier.Parser.Objects.TimingLines; 8 | using MapsetVerifier.Parser.Statics; 9 | 10 | namespace MapsetVerifier.Checks.Taiko.Timing 11 | { 12 | [Check] 13 | public class CheckInconsistentBarLines : BeatmapSetCheck 14 | { 15 | public override CheckMetadata GetMetadata() => 16 | new BeatmapCheckMetadata 17 | { 18 | Modes = 19 | [ 20 | Beatmap.Mode.Taiko 21 | ], 22 | Category = "Timing", 23 | Message = "Inconsistent omitted bar lines.", 24 | Author = "Naxess", 25 | 26 | Documentation = new Dictionary 27 | { 28 | { 29 | "Purpose", 30 | @" 31 | Ensuring that bar lines are consistent between all difficulties." 32 | }, 33 | { 34 | "Reasoning", 35 | @" 36 | Since all difficulties in a set are based around a single song, and bar lines are meant to act as a point of reference 37 | for timing in gameplay, it would make the most sense if all difficulties would use the same bar lines. For this reason, 38 | if one difficulty skips one, others probably should too." 39 | } 40 | } 41 | }; 42 | 43 | public override Dictionary GetTemplates() => 44 | new() 45 | { 46 | { 47 | "Inconsistent", 48 | new IssueTemplate(Issue.Level.Problem, "{0} Inconsistent omitted bar line, see {1}.", "timestamp - ", "difficulty").WithCause("A beatmap does not omit bar line where the reference beatmap does, or visa versa.") 49 | } 50 | }; 51 | 52 | public override IEnumerable GetIssues(BeatmapSet beatmapSet) 53 | { 54 | var taikoBeatmaps = beatmapSet.Beatmaps.Where(beatmap => beatmap.GeneralSettings.mode == Beatmap.Mode.Taiko).ToList(); 55 | 56 | var refBeatmap = taikoBeatmaps.First(); 57 | 58 | foreach (var beatmap in taikoBeatmaps) 59 | foreach (var line in refBeatmap.TimingLines.OfType()) 60 | { 61 | var respectiveLine = beatmap.TimingLines.OfType().FirstOrDefault(otherLine => Timestamp.Round(otherLine.Offset) == Timestamp.Round(line.Offset)); 62 | 63 | if (respectiveLine == null) 64 | // Inconsistent lines, which is the responsibility of another check, so we skip this case. 65 | continue; 66 | 67 | double offset = Timestamp.Round(line.Offset); 68 | 69 | if (line.OmitsBarLine != respectiveLine.OmitsBarLine) 70 | yield return new Issue(GetTemplate("Inconsistent"), beatmap, Timestamp.Get(offset), refBeatmap); 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/Framework/CheckerRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using MapsetVerifier.Framework.Objects; 4 | 5 | namespace MapsetVerifier.Framework 6 | { 7 | public static class CheckerRegistry 8 | { 9 | private static readonly List checks = []; 10 | 11 | /// Adds the given check to the list of checks to process when checking for issues. 12 | public static void RegisterCheck(Check check) 13 | { 14 | if (check == null) 15 | return; 16 | 17 | checks.Add(check); 18 | } 19 | 20 | /// Returns all checks which are processed when checking for issues. 21 | public static List GetChecks() => [..checks]; 22 | 23 | /// 24 | /// Returns checks which are processed beatmap-wise when checking for issues. 25 | /// These are isolated from the set for optimization purposes. 26 | /// 27 | public static IEnumerable GetBeatmapChecks() => checks.OfType(); 28 | 29 | /// 30 | /// Returns checks which are processed beatmapset-wise when checking for issues. 31 | /// These are often checks which need to compare between difficulties in a set. 32 | /// 33 | public static IEnumerable GetBeatmapSetChecks() => checks.OfType(); 34 | 35 | /// 36 | /// Returns checks which are processed beatmapset-wise when checking for issues and stored in a seperate difficulty. 37 | /// These are general checks which are independent from any specific difficulty, for example checking files. 38 | /// 39 | public static IEnumerable GetGeneralChecks() => checks.OfType(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Framework/Objects/Attributes/CheckAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MapsetVerifier.Framework.Objects.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class CheckAttribute : Attribute 7 | { 8 | // Used to identify which classes to add to checks in plugins. 9 | } 10 | } -------------------------------------------------------------------------------- /src/Framework/Objects/BeatmapCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Parser.Objects; 3 | 4 | namespace MapsetVerifier.Framework.Objects 5 | { 6 | public abstract class BeatmapCheck : Check 7 | { 8 | public abstract IEnumerable GetIssues(Beatmap beatmap); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Framework/Objects/BeatmapSetCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Parser.Objects; 3 | 4 | namespace MapsetVerifier.Framework.Objects 5 | { 6 | public abstract class BeatmapSetCheck : Check 7 | { 8 | public abstract IEnumerable GetIssues(BeatmapSet beatmapSet); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Framework/Objects/Check.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Framework.Objects.Metadata; 3 | 4 | namespace MapsetVerifier.Framework.Objects 5 | { 6 | public abstract class Check 7 | { 8 | public IssueTemplate GetTemplate(string template) => GetTemplates()[template]; 9 | 10 | public abstract Dictionary GetTemplates(); 11 | public abstract CheckMetadata GetMetadata(); 12 | 13 | public override string ToString() => GetMetadata().Message; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Framework/Objects/GeneralCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MapsetVerifier.Parser.Objects; 3 | 4 | namespace MapsetVerifier.Framework.Objects 5 | { 6 | public abstract class GeneralCheck : Check 7 | { 8 | public abstract IEnumerable GetIssues(BeatmapSet beatmapSet); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Framework/Objects/IssueTemplate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MapsetVerifier.Framework.Objects 6 | { 7 | public class IssueTemplate 8 | { 9 | private readonly object[] defaultArguments; 10 | 11 | private readonly string format; 12 | 13 | public string Cause { get; private set; } 14 | 15 | /// 16 | /// Constructs a new issue template with the given issue level, format and default arguments. 17 | /// 18 | /// The type and priority of the issue (e.g. minor/warning/unrankable). 19 | /// The formatting string, use {0}, {1}, etc to insert arguments. 20 | /// 21 | /// The default arguments for the format string, supply as many of these as you have {0}, 22 | /// {1}, etc. 23 | /// 24 | public IssueTemplate(Issue.Level level, string format, params object[] defaultArguments) 25 | { 26 | Level = level; 27 | 28 | this.format = format; 29 | this.defaultArguments = defaultArguments; 30 | 31 | for (var i = 0; i < defaultArguments.Length; ++i) 32 | if (!format.Contains("{" + i + "}")) 33 | throw new ArgumentException($"\"{format}\" There are {defaultArguments.Length} default arguments given, but the format string does not contain any \"{{{i}}}\", which makes the latter one(s) useless. Ensure there are an equal amount of {{0}}, {{1}}, etc as there are default arguments."); 34 | 35 | if (format.Contains("{" + defaultArguments.Length + "}")) 36 | throw new ArgumentException($"\"{format}\" There are {defaultArguments.Length} default arguments given, but the format string contains an unused argument place, \"{{{defaultArguments.Length}}}\". Ensure there are an equal amount of {{0}}, {{1}}, etc as there are default arguments."); 37 | 38 | Cause = null; 39 | } 40 | 41 | public Issue.Level Level { get; } 42 | 43 | /// Returns the template with a given cause, which will be shown below the issue template in the documentation. 44 | public IssueTemplate WithCause(string cause) 45 | { 46 | Cause = cause; 47 | 48 | return this; 49 | } 50 | 51 | /// Returns the format with {0}, {1}, etc. replaced with the respective given arguments. 52 | public string Format(object[] arguments) 53 | { 54 | if (arguments.Length != defaultArguments.Length) 55 | throw new ArgumentException($"The format for a template is \"{format}\", which takes {defaultArguments.Length} arguments (according to the default argument amount), but was given the unexpected argument amount {arguments.Length}. Make sure that, when creating a new issue, you supply it with the correct amount of arguments for its template."); 56 | 57 | // Trimming format and args separately allows for "timestamp - " in "{0} /.../" without double spacing. 58 | // This way we still maintain any double space causing things like incorrect filenames within arguments. 59 | return string.Format(format.Trim(), arguments.Select(arg => arg.ToString()?.Trim()).ToArray()); 60 | } 61 | 62 | /// Returns the default arguments for this template. 63 | public IEnumerable GetDefaultArguments() => defaultArguments; 64 | 65 | public override string ToString() => Format(defaultArguments); 66 | } 67 | } -------------------------------------------------------------------------------- /src/Framework/Objects/Metadata/BeatmapCheckMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using MapsetVerifier.Parser.Objects; 5 | 6 | namespace MapsetVerifier.Framework.Objects.Metadata 7 | { 8 | public class BeatmapCheckMetadata : CheckMetadata 9 | { 10 | /// 11 | /// Can be initialized like this: 12 | /// 13 | /// new GeneralCheckMetadata() { Category = "", Message = "", Author = "", ... } 14 | /// 15 | public BeatmapCheckMetadata() { } 16 | 17 | /// The mode(s) this check applies to, by default all. 18 | public Beatmap.Mode[] Modes { get; set; } = 19 | [ 20 | Beatmap.Mode.Standard, 21 | Beatmap.Mode.Taiko, 22 | Beatmap.Mode.Catch, 23 | Beatmap.Mode.Mania 24 | ]; 25 | 26 | /// The difficulties this check applies to, by default all. 27 | public Beatmap.Difficulty[] Difficulties { get; set; } = 28 | [ 29 | Beatmap.Difficulty.Easy, 30 | Beatmap.Difficulty.Normal, 31 | Beatmap.Difficulty.Hard, 32 | Beatmap.Difficulty.Insane, 33 | Beatmap.Difficulty.Expert, 34 | Beatmap.Difficulty.Ultra 35 | ]; 36 | 37 | public override string GetMode() 38 | { 39 | if (Modes.Contains(Beatmap.Mode.Standard) && Modes.Contains(Beatmap.Mode.Taiko) && Modes.Contains(Beatmap.Mode.Catch) && Modes.Contains(Beatmap.Mode.Mania)) 40 | return "All Modes"; 41 | 42 | if (Modes.Length == 0) 43 | return "No Modes"; 44 | 45 | var modes = new List(); 46 | 47 | foreach (var mode in Modes) 48 | modes.Add(Enum.GetName(typeof(Beatmap.Mode), mode)); 49 | 50 | return string.Join(" ", modes); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Framework/Objects/Metadata/CheckMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MapsetVerifier.Framework.Objects.Metadata 4 | { 5 | public class CheckMetadata 6 | { 7 | /// 8 | /// Can be initialized like this: 9 | /// 10 | /// new CheckMetadata() { Category = "", Message = "", Author = "", ... } 11 | /// 12 | public CheckMetadata() { } 13 | 14 | /// The name of the category this check falls under, by default "Other". 15 | public string Category { get; set; } = "Other"; 16 | 17 | /// 18 | /// A message explaining what went wrong in, preferably, one sentence. By default 19 | /// "Custom check returned one or more issues." 20 | /// 21 | /// "No" is used as prefix in the application if there were no issues, so make sure 22 | /// adding "No" in front of the message makes sense. 23 | /// 24 | /// 25 | public string Message { get; set; } = "Custom check returned one or more issues."; 26 | 27 | /// The user(s) who developed the check. By default "Unknown". 28 | public string Author { get; set; } = "Unknown"; 29 | 30 | /// 31 | /// A list of title-description pairs used to document the intent and reasoning behind the check. By default empty. 32 | /// 33 | /// Checks should not be followed blindly; if someone doubts that the check is worth enforcing, 34 | /// this should convince them it is. 35 | /// 36 | /// 37 | public Dictionary Documentation { get; set; } = new(); 38 | 39 | public virtual string GetMode() => "General"; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Framework/Objects/Resources/FileAbstraction.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using File = TagLib.File; 3 | 4 | namespace MapsetVerifier.Framework.Objects.Resources 5 | { 6 | public class FileAbstraction : File.IFileAbstraction 7 | { 8 | public string error; 9 | 10 | public FileAbstraction(string filePath) 11 | { 12 | error = null; 13 | 14 | ReadStream = filePath != null ? new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) : null; 15 | 16 | Name = filePath; 17 | } 18 | 19 | public string Name { get; } 20 | 21 | public Stream ReadStream { get; } 22 | 23 | public Stream WriteStream => ReadStream; 24 | 25 | public void CloseStream(Stream stream) => stream.Position = 0; 26 | 27 | public File GetTagFile() 28 | { 29 | if (Name == null) 30 | { 31 | error = "Name cannot be null."; 32 | 33 | return null; 34 | } 35 | 36 | if (ReadStream == null) 37 | { 38 | error = "Could not open file for reading."; 39 | 40 | return null; 41 | } 42 | 43 | return File.Create(this); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/MapsetVerifier.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | full 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Parser/MapsetParser.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | true 5 | netcoreapp3.1 6 | 7 | 8 | 9 | MapsetParser.xml 10 | 1701;1702;1591 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Parser/Objects/Events/Animation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace MapsetVerifier.Parser.Objects.Events 7 | { 8 | public class Animation : Sprite 9 | { 10 | // Animation,Fail,Centre,"spr\scn1_spr3_2_b.png",320,280,2,40,LoopForever 11 | // Animation, layer, origin, filename, x offset, y offset, frame count, frame delay, loop type 12 | 13 | public readonly int frameCount; 14 | public readonly double frameDelay; 15 | 16 | public readonly List framePaths; 17 | public readonly bool loops; 18 | 19 | public Animation(string[] args) : base(args) 20 | { 21 | frameCount = GetFrameCount(args); 22 | frameDelay = GetFrameDelay(args); 23 | loops = IsLooping(args); 24 | 25 | framePaths = GetFramePaths().ToList(); 26 | } 27 | 28 | /// 29 | /// Returns the amount of frames this animation contains. 30 | /// Determines how many "filename_i" to use, where i starts at 0. 31 | /// 32 | private int GetFrameCount(string[] args) => int.Parse(args[6]); 33 | 34 | /// Returns the delay between each frame of this animation in miliseconds. 35 | private double GetFrameDelay(string[] args) => double.Parse(args[7], CultureInfo.InvariantCulture); 36 | 37 | /// Returns whether the animation loops, by default true. 38 | private bool IsLooping(string[] args) => 39 | // Does not exist in file version 5. 40 | args?[8] != "LoopOnce"; 41 | 42 | /// Returns all relative file paths for all frames used. 43 | public IEnumerable GetFramePaths() 44 | { 45 | for (var i = 0; i < frameCount; ++i) 46 | yield return path.Insert(path.LastIndexOf(".", StringComparison.Ordinal), i.ToString()); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Parser/Objects/Events/Background.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Numerics; 3 | using MapsetVerifier.Parser.Statics; 4 | 5 | namespace MapsetVerifier.Parser.Objects.Events 6 | { 7 | public class Background 8 | { 9 | public readonly Vector2? offset; 10 | // 0,0,"apple is oral.jpg",0,0 11 | // Background, offset (unused), filename, x offset, y offset 12 | 13 | public readonly string path; 14 | 15 | /// The path in lowercase without extension or quotationmarks. 16 | public readonly string strippedPath; 17 | 18 | public Background(string[] args) 19 | { 20 | path = GetPath(args); 21 | offset = GetOffset(args); 22 | 23 | strippedPath = PathStatic.ParsePath(path, true); 24 | } 25 | 26 | /// Returns the file path which this background uses. Retains case and extension. 27 | private string GetPath(string[] args) => PathStatic.ParsePath(args[2], retainCase: true); 28 | 29 | /// 30 | /// Returns the positional offset from the top left corner of the screen, if specified, otherwise null. 31 | /// This value is currently unused by the game. 32 | /// 33 | private Vector2? GetOffset(string[] args) 34 | { 35 | // Does not exist in file version 9. 36 | if (args.Length > 4) 37 | return new Vector2(float.Parse(args[3], CultureInfo.InvariantCulture), float.Parse(args[4], CultureInfo.InvariantCulture)); 38 | 39 | return null; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Parser/Objects/Events/Break.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace MapsetVerifier.Parser.Objects.Events 4 | { 5 | public class Break 6 | { 7 | public readonly double endTime; 8 | // 2,66281,71774 9 | // 2, start, end 10 | 11 | /* Notes: 12 | * - Assuming no extensions of pre- and post break times or rounding errors, 13 | * - pre break is 200 ms long 14 | * - post break is 525 ms long 15 | * - Saving the beatmap corrects any abnormal break times 16 | * - Abnormal break times do not show up in the editor, but do in gameplay. 17 | */ 18 | 19 | public readonly double time; 20 | 21 | public Break(string[] args) 22 | { 23 | time = GetTime(args); 24 | endTime = GetEndTime(args); 25 | } 26 | 27 | /// 28 | /// Returns the visual start time of the break. 29 | /// See for where HP stops draining. 30 | /// 31 | private double GetTime(string[] args) => double.Parse(args[1], CultureInfo.InvariantCulture); 32 | 33 | /// 34 | /// Returns the visual end time of the break. 35 | /// See for where HP starts draining again. 36 | /// 37 | private double GetEndTime(string[] args) => double.Parse(args[2], CultureInfo.InvariantCulture); 38 | 39 | /// 40 | /// Returns the duration between the end of the object before the break and the start of the 41 | /// object after it. During this time, no health will be drained. 42 | /// 43 | public double GetDuration(Beatmap beatmap) => GetRealEnd(beatmap) - GetRealStart(beatmap); 44 | 45 | /// Returns the end time of the object before the break. 46 | public double GetRealStart(Beatmap beatmap) => beatmap.GetPrevHitObject(time).GetEndTime(); 47 | 48 | /// Returns the start time of the object after the break, if any, otherwise the end of the map. 49 | public double GetRealEnd(Beatmap beatmap) => beatmap.GetNextHitObject(endTime)?.time ?? beatmap.GetPlayTime(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Parser/Objects/Events/Sample.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using MapsetVerifier.Parser.Statics; 3 | 4 | namespace MapsetVerifier.Parser.Objects.Events 5 | { 6 | public class Sample 7 | { 8 | /// The layer the hit sound is audible on, for example only when passing a section if "Pass". 9 | public enum Layer 10 | { 11 | Background = 0, 12 | Fail = 1, 13 | Pass = 2, 14 | Foreground = 3, 15 | Unknown 16 | } 17 | 18 | public readonly Layer layer; 19 | public readonly string path; 20 | 21 | /// The path in lowercase without extension or quotationmarks. 22 | public readonly string strippedPath; 23 | // Sample,15707,0,"drum-hitnormal.wav",60 24 | // Sample, time, layer, path, volume 25 | 26 | public readonly double time; 27 | public readonly float volume; 28 | 29 | public Sample(string[] args) 30 | { 31 | time = GetTime(args); 32 | layer = GetLayer(args); 33 | path = GetPath(args); 34 | volume = GetVolume(args); 35 | 36 | strippedPath = PathStatic.ParsePath(path, true); 37 | } 38 | 39 | /// Returns after how many miliseconds this storyboard sample will play. 40 | private double GetTime(string[] args) => double.Parse(args[1], CultureInfo.InvariantCulture); 41 | 42 | /// Returns on which layer the storyboard sample will play (e.g. Fail or Pass). 43 | private Layer GetLayer(string[] args) => ParserStatic.GetEnumMatch(args[2]) ?? Layer.Unknown; 44 | 45 | /// Returns the file path which this sample uses. Retains case and extension. 46 | private string GetPath(string[] args) => PathStatic.ParsePath(args[3], retainCase: true); 47 | 48 | /// Returns the volume percentage (0-100) that this sample will play at. 49 | private float GetVolume(string[] args) 50 | { 51 | // Does not exist in file version 5. 52 | if (args.Length > 4) 53 | return float.Parse(args[4], CultureInfo.InvariantCulture); 54 | 55 | // 100% volume is default. 56 | return 100.0f; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Parser/Objects/Events/Sprite.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Numerics; 3 | using MapsetVerifier.Parser.Statics; 4 | 5 | namespace MapsetVerifier.Parser.Objects.Events 6 | { 7 | public class Sprite 8 | { 9 | // Sprite,Foreground,Centre,"SB\whitenamebar.png",320,240 10 | // Sprite, layer, origin, filename, x offset, y offset 11 | 12 | public enum Layer 13 | { 14 | Background = 0, 15 | Fail = 1, 16 | Pass = 2, 17 | Foreground = 3, 18 | Overlay = 4, 19 | Unknown 20 | } 21 | 22 | public enum Origin 23 | { 24 | TopLeft = 0, 25 | Centre = 1, 26 | CentreLeft = 2, 27 | TopRight = 3, 28 | BottomCentre = 4, 29 | TopCentre = 5, 30 | Custom = 6, 31 | CentreRight = 7, 32 | BottomLeft = 8, 33 | BottomRight = 9, 34 | Unknown 35 | } 36 | 37 | public readonly Layer layer; 38 | public readonly Vector2 offset; 39 | public readonly Origin origin; 40 | public readonly string path; 41 | 42 | /// The path in lowercase without extension or quotationmarks. 43 | public readonly string strippedPath; 44 | 45 | public Sprite(string[] args) 46 | { 47 | layer = GetLayer(args); 48 | origin = GetOrigin(args); 49 | path = GetPath(args); 50 | offset = GetOffset(args); 51 | 52 | strippedPath = PathStatic.ParsePath(path, true); 53 | } 54 | 55 | /// Returns the layer which this sprite exists on (e.g. Foreground, Pass, or Overlay). 56 | private Layer GetLayer(string[] args) => ParserStatic.GetEnumMatch(args[1]) ?? Layer.Unknown; 57 | 58 | /// 59 | /// Returns the local origin of the sprite, determining around which point it is transformed 60 | /// (e.g. TopLeft, Center, or Bottom). 61 | /// 62 | private Origin GetOrigin(string[] args) => ParserStatic.GetEnumMatch(args[2]) ?? Origin.Unknown; 63 | 64 | /// Returns the file path which this sprite uses. Retains case sensitivity and extension. 65 | private string GetPath(string[] args) => PathStatic.ParsePath(args[3], retainCase: true); 66 | 67 | /// 68 | /// Returns the positional offset from the top left corner of the screen, if specified, 69 | /// otherwise default (320, 240). 70 | /// 71 | private Vector2 GetOffset(string[] args) 72 | { 73 | if (args.Length > 4) 74 | return new Vector2(float.Parse(args[4], CultureInfo.InvariantCulture), float.Parse(args[5], CultureInfo.InvariantCulture)); 75 | 76 | // default coordinates 77 | return new Vector2(320, 240); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/Parser/Objects/Events/Video.cs: -------------------------------------------------------------------------------- 1 | using MapsetVerifier.Parser.Statics; 2 | 3 | namespace MapsetVerifier.Parser.Objects.Events 4 | { 5 | public class Video 6 | { 7 | // Video,-320,"aragoto.avi" 8 | // Video, offset, filename 9 | 10 | public readonly int offset; 11 | public readonly string path; 12 | 13 | /// The path in lowercase without extension or quotationmarks. 14 | public readonly string strippedPath; 15 | 16 | public Video(string[] args) 17 | { 18 | offset = GetOffset(args); 19 | path = GetPath(args); 20 | 21 | strippedPath = PathStatic.ParsePath(path, true); 22 | } 23 | 24 | /// Returns the temporal offset of the video (i.e. when it should start playing). 25 | private int GetOffset(string[] args) => int.Parse(args[1]); 26 | 27 | /// Returns the file path which this video uses. Retains case and extension. 28 | private string GetPath(string[] args) => PathStatic.ParsePath(args[2], retainCase: true); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Parser/Objects/HitObjects/Circle.cs: -------------------------------------------------------------------------------- 1 | namespace MapsetVerifier.Parser.Objects.HitObjects 2 | { 3 | public class Circle : Stackable 4 | { 5 | // This is the same as the base class, just a different name. 6 | public Circle(string[] args, Beatmap beatmap) : base(args, beatmap) { } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Parser/Objects/HitObjects/HoldNote.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace MapsetVerifier.Parser.Objects.HitObjects 4 | { 5 | public class HoldNote : HitObject 6 | { 7 | // 448,192,243437,128,2,247861:0:0:0:0: 8 | // x, y, time, typeFlags, hitsound, endTime:extras 9 | 10 | public readonly double endTime; 11 | 12 | public HoldNote(string[] args, Beatmap beatmap) : base(args, beatmap) => 13 | endTime = GetEndTime(args); 14 | 15 | private double GetEndTime(string[] args) => double.Parse(args[5].Split(':')[0], CultureInfo.InvariantCulture); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Parser/Objects/HitObjects/Spinner.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Linq; 3 | 4 | namespace MapsetVerifier.Parser.Objects.HitObjects 5 | { 6 | public class Spinner : HitObject 7 | { 8 | public readonly double endTime; 9 | 10 | public Spinner(string[] args, Beatmap beatmap) : base(args, beatmap) 11 | { 12 | endTime = GetEndTime(args); 13 | 14 | usedHitSamples = GetUsedHitSamples().ToList(); 15 | } 16 | 17 | private double GetEndTime(string[] args) => double.Parse(args[5], CultureInfo.InvariantCulture); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Parser/Objects/HitObjects/Stackable.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace MapsetVerifier.Parser.Objects.HitObjects 4 | { 5 | public class Stackable : HitObject 6 | { 7 | public bool isOnSlider; 8 | public int stackIndex; 9 | 10 | protected Stackable(string[] args, Beatmap beatmap) : base(args, beatmap) { } 11 | 12 | public Vector2 UnstackedPosition => base.Position; 13 | public override Vector2 Position => GetStackedPosition(base.Position); 14 | 15 | private Vector2 GetStackedPosition(Vector2 position) => new(position.X + GetStackOffset(), position.Y + GetStackOffset()); 16 | 17 | private float GetStackOffset() => stackIndex * (beatmap?.DifficultySettings.GetCircleRadius() ?? 0) * -0.1f; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Parser/Objects/HitObjects/Taiko/TaikoExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace MapsetVerifier.Parser.Objects.HitObjects.Taiko 2 | { 3 | public static class TaikoExtensions 4 | { 5 | public static bool IsDon(this Circle circle) => circle.hitSound != HitObject.HitSounds.Clap && circle.hitSound != HitObject.HitSounds.Whistle; 6 | 7 | public static bool IsFinisher(this HitObject hitObject) => hitObject.HasHitSound(HitObject.HitSounds.Finish); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Parser/Objects/Osb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using MapsetVerifier.Parser.Objects.Events; 5 | using MapsetVerifier.Parser.Statics; 6 | 7 | namespace MapsetVerifier.Parser.Objects 8 | { 9 | public class Osb 10 | { 11 | public readonly List animations; 12 | 13 | public readonly List backgrounds; 14 | public readonly List breaks; 15 | public readonly List samples; 16 | public readonly List sprites; 17 | public readonly List